第 6 章:面向对象的 Jython

本章将涵盖面向对象编程的基础知识。如果您熟悉这些概念,我将从介绍为什么要使用面向对象代码开始,然后介绍所有基本语法,最后我将向您展示一个非平凡的示例。

面向对象编程是一种将代码打包成数据和行为捆绑包的编程方法。在 Jython 中,您可以使用类定义来定义此捆绑包的模板。使用第一个类编写后,您可以实例化对象的副本,并让它们相互作用。这有助于您将代码组织成更小、更易于管理的捆绑包。

在本章中,我将交替使用 Python 和 Jython - 对于常规的面向对象编程 - Python 的两种方言非常相似,以至于两种语言之间没有实质性的区别。不过,让我们先介绍一下 - 让我们看看一些基本语法,看看这到底是怎么回事。

基本语法

编写类非常简单。它本质上是关于管理某种“状态”并公开一些用于操作该状态的函数。在面向对象术语中 - 我们只是将这些函数称为“方法”。

让我们从创建一个汽车类开始。目标是创建一个对象来管理它在二维平面上的位置。我们希望能够告诉它转向、向前移动,并且我们希望能够查询对象以找出它当前的位置。

class Car(object):

    NORTH = 0
    EAST = 1
    SOUTH = 2
    WEST = 3

    def __init__(self, x=0, y=0):
        self.x = x
        self.y = y
        self.direction = 0

    def turn_right(self):
        self.direction += 1
        self.direction = self.direction % 4

    def turn_left(self):
        self.direction -= 1
        self.direction = self.direction % 4

    def move(self, distance):
        if self.direction == self.NORTH:
            self.y += distance
        elif self.direction == self.SOUTH:
            self.y -= distance
        elif self.direction == self.EAST:
            self.x += distance
        else:
            self.x -= distance

    def position(self):
        return (self.x, self.y)

我们将详细介绍该类定义,但现在,让我们看看如何创建一个汽车、移动它并询问汽车它在哪里。

from car import Car

def test_car():
    c = Car()
    c.turn_right()
    c.move(5)
    assert (5, 0) ==  c.position()

    c.turn_left()
    c.move(3)

    assert (5, 3) == c.position()

if __name__ == '__main__':
    test_car()

将类想象成一种特殊的函数,它就像一个工厂,可以生成对象实例。对于对类的每次调用 - 您都在创建一个新的独立的副本。

创建汽车实例后,我们可以简单地调用附加到汽车类的函数,并且该对象将管理它自己的位置。从测试代码的角度来看 - 我们不需要管理汽车的位置 - 也不需要管理汽车指向的方向。我们只需告诉它移动 - 它就会做正确的事情。

让我们详细介绍一下语法,看看这里到底发生了什么。

在第 1 行,我们声明我们的 Car 对象是根“object”类的子类。Python 与许多面向对象语言一样,有一个“根”对象,所有其他对象都基于它。这个“object”类定义了所有类都可以重用的基本行为。

Python 实际上有两种类型的类 - “新式”和旧式。旧的声明类的方式不需要你输入“object” - 你偶尔会在一些 Python 代码中看到旧式类的用法,但这不被认为是一种好的做法。只需为你的任何基类子类化“object”,你的生活就会更简单 [1].

第 3 到 6 行声明了任何汽车可以指向的方向的类属性。这些是属性,因此它们可以在汽车对象的任何对象实例之间共享。

现在是好东西了。

第 8 到 11 行声明了对象初始化器。在某些语言中,你可能熟悉构造函数 - 在 Jython 中,我们有一个初始化器,它允许我们在创建时将值传递到对象中。

在我们的初始化器中,我们将汽车的初始位置设置为二维平面上的 (0, 0),然后将汽车的方向初始化为指向北方。到目前为止,相当简单。

函数签名使用 Python 的默认参数列表功能,因此我们不必显式地将初始位置设置为 (0,0),但引入了一个新的参数,称为“self”。这是一个对当前对象的引用。

记住 - 你的类定义正在创建对象实例。一旦你的对象被创建,它就拥有自己的内部变量集来管理。你的对象不可避免地需要访问这些变量以及任何类的内部方法。Python 会将对当前对象的引用作为第一个参数传递给你的所有实例方法。

如果你来自其他面向对象的语言,你可能熟悉“this”变量。与 C++ 或 Java 不同,Python 不会神奇地将引用引入可访问变量的命名空间,但这与 Python 使事物明确以提高清晰度的理念一致。

当我们想要分配初始 x,y 位置时,我们只需要将值分配给对象上的名称“x”和“y”。将 x 和 y 的值绑定到 self 使位置值可供任何访问 self 的代码访问 - 即对象的其他方法。这里有一个小细节 - 在 Python 中,你可以在技术上随意命名参数。没有什么能阻止你将第一个参数称为“this”而不是“self”,但社区标准是使用“self” [2].

第 13 到 19 行声明了两种方法,用于将车辆转向不同的方向。请注意,方向从未被 Car 对象的调用者直接操作。我们只是要求汽车转向,汽车改变了自己的内部“方向”状态。

第 21 到 29 行定义了当我们向前移动汽车时汽车应该移动到的位置。内部方向变量告知汽车它应该如何操作 x 和 y 位置。请注意,汽车对象的调用者永远不需要知道汽车的确切指向方向。调用者只需要告诉对象转向并向前移动。如何使用该消息的具体细节被抽象掉了。

对于几十行代码来说,这还不错。

这种隐藏内部细节的概念称为封装。这是面向对象编程的核心概念。正如你从这个简单的例子中看到的那样 - 它允许你构建代码,以便你可以为你的代码用户提供一个简化的接口。

拥有一个简化的接口意味着我们可以在转向和移动的函数调用背后发生各种行为 - 但调用者可以忽略所有这些细节,专注于使用汽车,而不是管理汽车。

只要方法签名不改变,调用者实际上就不需要关心任何事情。我们可以轻松地将持久性添加到此类中 - 因此我们可以将汽车的状态保存到磁盘并从磁盘加载。

首先,引入 pickle 模块 - pickle 允许我们将 Python 对象转换为可以稍后还原为完整对象的字节字符串。

import pickle

现在,只需添加两个新方法来加载和保存对象的状态。

def save(self):
    state = (self.direction, self.x, self.y)
    pickle.dump(state, open('mycar.pickle','wb'))

def load(self):
    state = pickle.load(open('mycar.pickle','rb'))
    (self.direction, self.x, self.y) = state

只需在转向和移动方法的末尾添加对 save() 的调用,对象就会自动将所有相关的内部值保存到磁盘。

使用汽车对象的人甚至不需要知道它正在保存到磁盘,因为汽车对象在后台处理它。

def turn_right(self):
    self.direction += 1
    self.direction = self.direction % 4
    self.save()

def turn_left(self):
    self.direction -= 1
    self.direction = self.direction % 4
    self.save()

def move(self, distance):
    if self.direction == self.NORTH:
        self.y += distance
    elif self.direction == self.SOUTH:
        self.y -= distance
    elif self.direction == self.EAST:
        self.x += distance
    else:
        self.x -= distance
    self.save()

现在,当你调用转向或移动方法时,汽车会自动将自身保存到磁盘。如果你想从之前保存的 pickle 文件中重建汽车对象的状态,你可以简单地调用 load() 方法。

对象属性查找

如果你一直在关注,你可能想知道 NORTH、SOUTH、EAST 和 WEST 变量是如何绑定到 self 的。我们在对象初始化期间从未真正将它们分配给 self 变量 - 那么当我们调用 move() 时发生了什么?Jythpon 究竟是如何解析这四个变量的值的?

现在似乎是展示 Jython 如何解析名称查找的好时机。

方向名称实际上已绑定到 Car 类。当您尝试对对象访问任何名称时,Jython 对象系统会做一些魔法,它首先搜索绑定到 'self' 的任何内容。如果 python 无法在 self 上使用该名称解析任何属性,它会向上遍历对象图到类定义。方向属性 NORTH、SOUTH、EAST、WEST 已绑定到类定义 - 因此名称解析成功,我们获得了类属性的值。

一个非常简短的例子将有助于澄清这一点

>>> class Foobar(object):
...   def __init__(self):
...     self.somevar = 42
...   class_attr = 99
...
>>>
>>> obj = Foobar()
>>> obj.somevar
42
>>> obj.class_attr
99
>>> obj.not_there
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Foobar' object has no attribute 'not_there'
>>>

因此,这里的关键区别在于您将值绑定到什么。您绑定到 self 的值仅对单个对象可用。您绑定到类定义的值对该类的所有实例都可用。所有实例之间共享类属性是一个关键区别,因为修改类属性会影响所有实例。如果您没有注意,这可能会导致意想不到的副作用,因为变量可能会在您没有预料到的情况下改变值。

>>> other = Foobar()
>>> other.somevar
42
>>> other.class_attr
99
>>> # obj and other will have different values for somevar
>>> obj.somevar = 77
>>> obj.somevar
77
>>> other.somevar
42
>>> # Now show that we have the same copy of class_attr
>>> other.class_attr = 66
>>> other.class_attr
66
>>> obj.class_attr
66

我认为强调 Python 的对象系统是多么透明很重要。对象属性只是存储在一个普通的 python 字典中。您可以通过查看 __dict__ 属性直接访问此字典。

>>> obj = Foobar()
>>> obj.__dict__
{'somevar': 42}

请注意,这里没有引用类的方法或类属性。我将再次重申 - Python 将向上遍历您的继承图 - 并转到类定义以查找 Foobar 的方法和 foobar 的类属性。

相同的技巧可用于检查类的所有属性,只需查看类定义的 __dict__ 属性,您就会找到您的类属性以及附加到您的类定义的所有方法

>>> Foobar.__dict__
{'__module__': '__main__',
    'class_attr': 99,
    '__dict__': <attribute '__dict__' of 'Foobar' objects>,
    '__init__': <function __init__ at 1>}

这种透明性可以与使用闭包和在运行时将新函数绑定到您的类定义的动态编程技术相结合。我们将在本章后面重新讨论这一点,当我们查看动态生成函数时,最后简要介绍元编程。

继承和重载

在汽车示例中,我们从根对象类型继承。您也可以对自己的类进行子类化,以专门化对象的行为。如果您注意到代码中自然存在一种结构,其中您有许多不同的类都共享一些共同的行为,您可能需要这样做。

使用对象,您可以编写一个类,然后使用继承重用它,以自动获得对父类中预先存在的行为和属性的访问权限。您的“基本”对象将从根“对象”类继承行为,但任何后续子类都将从您自己的类继承。

让我们来看一个使用一些动物类的简单示例,看看它是如何工作的。定义一个名为“animals.py”的模块,其中包含以下代码

class Animal(object)
def sound(self)
return “I don’t make any sounds”
class Goat(Animal)
def sound(self)
return “Bleeattt!”
class Rabbit(Animal)
def jump(self)
return “hippity hop hippity hop”
class Jackalope(Goat, Rabbit)
pass

现在您应该能够使用 jython 解释器探索该模块

>>> from animals import *
>>> animal = Animal()
>>> goat = Goat()
>>> rabbit = Rabbit()
>>> jack = Jackalope()
>>> animal.sound()
"I don't make any sounds"
>>> animal.jump()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Animal' object has no attribute 'jump'
>>> rabbit.sound()
"I don't make any sounds"
>>> rabbit.jump()
'hippity hop hippity hop'
>>> goat.sound()
'Bleeattt!'
>>> goat.jump()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Goat' object has no attribute 'jump'
>>> jack.jump()
'hippity hop hippity hop'
>>> jack.sound()
'Bleeattt!'

继承是一个非常简单的概念,当您声明您的类时,您只需指定要重用的父类。然后,您的新类可以自动访问超类的所有方法和属性。请注意,山羊不能跳,兔子也不能发出任何声音,但杰克洛普可以访问兔子和山羊的方法。

使用单继承 - 当您的类只从一个父类继承时 - 解析在何处查找属性或方法的规则非常简单。如果当前对象没有匹配的属性,Jython 只会向上查找父类。

现在需要指出的是,Rabbit 类是一种 Animal 类型 - Python 运行时可以通过使用 isinstance 函数以编程方式告诉您这一点

>>> isinstance(bunny, Rabbit)
True
>>> isinstance(bunny, Animal)
True
>>> isinstance(bunny, Goat)
False

对于许多类,您可能希望扩展父类的行为,而不是仅仅完全覆盖它。为此,您需要使用 super()。让我们像这样专门化 Rabbit 类。

class EasterBunny(Rabbit):
    def sound(self):
        orig = super(EasterBunny, self).sound()
        return "%s - but I have eggs!" % orig

如果你现在尝试让这只兔子说话,它将扩展来自基础 Rabbit 类的原始 sound() 方法。

>>> bunny = EasterBunny()
>>> bunny.sound()
"I don't make any sounds - but I have eggs!"

这并不难。在这些示例中,我只演示了继承方法可以被调用,但你可以对绑定到 self 的属性做完全相同的事情。

对于多重继承,事情变得非常棘手。事实上,解析属性查找方式的规则很容易填满整章(如果你不相信我,可以在 Google 上搜索“Python 2.3 方法解析顺序”)。本章没有足够的空间来正确地涵盖这个主题,这应该是一个很好的指示,表明你真的不想使用多重继承。

更高级的抽象

使用普通类的抽象很棒,但如果你的代码看起来自然地适合语言的语法,那就更好了。Python 支持各种下划线方法 - 以双下划线“_”符号开头和结尾的方法,允许你重载对象的行为。这意味着你的对象看起来会更紧密地与语言本身集成。

使用下划线方法,你可以为你的对象提供逻辑和数学运算的行为。你甚至可以使你的对象的行为更像标准的内置类型,如列表、集合或字典。

from __future__ import with_statement from contextlib import closing

with closing(open(‘simplefile’,’w’)) as fout
fout.writelines([“blah”])
with closing(open(‘simplefile’,’r’)) as fin
print fin.readlines()

上面的代码片段只是打开一个文件,写入一些文本,然后我们只是读取内容。并不令人兴奋。Python 中的大多数对象可以使用 pickle 模块序列化为字符串。我们可以利用 pickle 将完整的对象写入磁盘。让我们看看这个功能版本的代码

from __future__ import with_statement
from contextlib import closing
from pickle import dumps, loads

def write_object(fout, obj):
    data = dumps(obj)
    fout.write("%020d" % len(data))
    fout.write(data)

def read_object(fin):
    length = int(fin.read(20))
    obj = loads(fin.read(length))
    return obj

class Simple(object):
    def __init__(self, value):
        self.value = value
    def __unicode__(self):
        return "Simple[%s]" % self.value

with closing(open('simplefile','wb')) as fout:
    for i in range(10):
        obj = Simple(i)
        write_object(fout, obj)

print "Loading objects from disk!"
print '=' * 20

with closing(open('simplefile','rb')) as fin:
    for i in range(10):
        print read_object(fin)

这应该输出类似这样的内容

Loading objects from disk!
====================
Simple[0]
Simple[1]
Simple[2]
Simple[3]
Simple[4]
Simple[5]
Simple[6]
Simple[7]
Simple[8]
Simple[9]

所以现在我们正在做一些有趣的事情。让我们看看这里到底发生了什么。

首先,你会注意到 Simple 对象正在渲染一个漂亮的 - Simple 对象可以使用 __unicode__ 方法渲染自身。这显然比之前使用尖括号和十六进制代码渲染对象有了改进。

write_object 函数相当简单,我们只是使用 pickle 模块将我们的对象转换为字符串,计算字符串的长度,然后将长度和实际的序列化对象写入磁盘。

这很好,但读取端有点笨拙。我们并不知道何时停止读取。我们可以使用迭代协议来解决这个问题。这让我们看到了使用 Python 中对象的最佳理由之一。

协议

在 Python 中,我们有“鸭子类型”。如果它听起来像鸭子,嘎嘎叫起来像鸭子,看起来像鸭子 - 那么 - 它就是一只鸭子。这与 C# 或 Java 等更严格的语言形成鲜明对比,这些语言具有正式的接口定义。鸭子类型的一个好处是 Python 有对象“协议”的概念。

如果你恰好实现了正确的方法 - python 会将你的对象识别为某种类型的“东西”。

迭代器是看起来像列表的对象,允许你读取下一个对象。实现迭代器协议很简单 - 只需实现 next() 方法和 __iter__ 方法,你就可以开始摇滚了。让我们看看它的实际应用

class PickleStream(object):
    """
    This stream can be used to stream objects off of a raw file stream
    """
    def __init__(self, file):
        self.file = file

    def write(self, obj):
        data = dumps(obj)
        length = len(data)
        self.file.write("%020d" % length)
        self.file.write(data)

    def __iter__(self):
        return self

    def next(self):
        data = self.file.read(20)
        if len(data) == 0:
            raise StopIteration
        length = int(data)well
        return loads(self.file.read(length))

    def close(self):
        self.file.close()

上面的类将允许你包装一个简单的文件对象,你现在可以向它发送原始的 python 对象以写入文件,或者你可以像流只是一个对象列表一样读取对象。写入和读取变得更加简单

with closing(PickleStream(open('simplefile','wb'))) as stream:
    for i in range(10):
        obj = Simple(i)
        stream.write(obj)

with closing(PickleStream(open('simplefile','rb'))) as stream:
    for obj in stream:
        print obj

将序列化细节抽象到 PickleStream 中,让我们“忘记”关于我们如何写入磁盘的细节。我们只关心对象在调用 write() 方法时会做正确的事情。

迭代协议可以用于更高级的用途,但即使在这个示例中,它也应该很明显它有多有用。虽然你可以使用 read() 方法来实现读取行为,但只需将流用作可以循环遍历的东西,就可以使代码更容易理解。

旁白:每个人似乎都会遇到的一个常见问题

似乎困扰着每个 Python 程序员的一个特殊问题是,当你在方法签名中使用默认值时。

>>> class Tricky(object):
...   def mutate(self, x=[]):
...     x.append(1)
...     return x
...
>>> obj = Tricky()
>>> obj.mutate()
[1]
>>> obj.mutate()
[1, 1]
>>> obj.mutate()
[1, 1, 1]

这里发生的事情是,实例方法“mutate”是一个对象。方法对象在方法对象内部的属性中存储“x”的默认值。因此,当您去修改列表时,实际上是在更改方法本身的属性的值。请记住 - 这是因为当您调用 mutate 方法时,您只是访问 Tricky 对象上的可调用属性。

方法的运行时绑定

Python 中一个有趣的特性是实例方法实际上只是挂在类定义上的属性 - 函数就像任何其他变量一样是属性,只是它们碰巧是“可调用”的。

甚至可以使用 new 模块在运行时创建和绑定函数到类定义,以创建实例方法。在下面的示例中,您可以看到可以定义一个没有任何内容的类,然后在运行时将方法绑定到类定义。

>>> def some_func(self, x, y):
...   print "I'm in object: %s" % self
...   return x * y
...
>>> import new
>>> class Foo(object): pass
...
>>> f = Foo()
>>> f
<__main__.Foo object at 0x1>
>>> Foo.mymethod = new.instancemethod(some_func, f, Foo)
>>> f.mymethod(6,3)
I'm in object: <__main__.Foo object at 0x1>
18

当您调用“mymethod”方法时,会调用相同的属性查找机制。Python 会根据“self”对象查找名称。当它在那里找不到任何东西时,它会转到类定义。当它在那里找到它时,会返回实例方法对象。然后用两个参数调用函数,您就可以看到最终结果。

Jython 中这种动态性非常强大。您可以编写在程序运行时生成函数的代码,然后将这些函数绑定到对象。您可以执行所有这些操作,因为在 Jython 中,类被称为“一等公民”。类定义本身就是一个实际的对象 - 就像任何其他对象一样。操作类就像操作任何其他对象一样容易。

闭包和传递对象

Python 支持嵌套作用域的概念 - 这可以通过在另一个函数内部保留一些状态信息来使用。这种技术在动态语言之外并不常见,因此您可能以前从未见过。让我们看一个简单的例子

def adder(x):
    def inner(y):
        return x + y
    return inner

>>> func = adder(5)
>>> func
<function inner at 0x7adf0>
>>> func(8)
13

这很酷 - 我们实际上可以从其他函数的模板中创建函数。如果您能想到一种参数化函数行为的方法,那么动态创建新函数就成为可能。您可以将柯里化视为创建模板的另一种方式 - 这次您正在为新函数创建模板。

一旦您获得了一些经验,这将是一个非常强大的工具。请记住 - Python 中的一切都是对象 - 即使函数在 Python 中也是一等公民,因此您也可以将它们作为参数传递。这方面的实际应用是从具有某些基本已知行为的“基础”函数中部分构建新函数。

让我们以之前的加法闭包为例,将其转换为更通用的形式

def arith(math_func, x):
    def inner(y):
        return math_func(x, y)
    return inner

def adder(x, y):
    return x + y

>>> func = arith(adder, 91)
>>> func(5)
96

这种技术称为柯里化 - 您现在正在基于以前的函数创建新的函数对象。最常见的用途是创建装饰器。在 Python 中,您可以定义特殊类型的对象,这些对象可以包装您的方法并添加额外的行为。一些装饰器已经内置,例如“property”、“classmethod”和“staticmethod”。一旦您有了装饰器,您就可以将其撒在另一个函数上以添加新行为。

装饰器语法看起来像这样

@decorator_func_name(arg1, arg2, arg3, ...)
def some_functions(x, y, z, ...):
    # Do something useful here
    pass

假设我们有一些方法需要大量的计算资源才能运行,但结果随着时间的推移变化不大。如果我们可以缓存结果,这样每次都不必运行计算,那不是很好吗?

这是我们具有缓慢计算方法的类

import time
class Foobar(object):
    def slow_compute(self, *args, **kwargs):
        time.sleep(1)
        return args, kwargs, 42

现在让我们使用装饰器函数缓存值。我们的策略是,对于任何名为 X 的函数,以及一些参数列表,我们想要创建一个唯一的名称并将最终计算的值保存到该名称。我们希望我们的缓存值具有可读的名称,因此我们希望重用原始函数名称,以及第一次传入的参数。

让我们开始写代码吧!

import hashlib
def cache(func):
    """
    This decorator will add a _cache_functionName_HEXDIGEST
    attribute after the first invocation of an instance method to
    store cached values.
    """
    # Obtain the function's name
    func_name = func.func_name
    # Compute a unique value for the unnamed and named arguments
    arghash = hashlib.sha1(str(args) + str(kwargs)).hexdigest()
    cache_name = '_cache_%s_%s' % (func_name, arghash)
    def inner(self, *args, **kwargs):
        if hasattr(self, cache_name):
            # If we have a cached value, just use it
            print "Fetching cached value from : %s" % cache_name
            return getattr(self, cache_name)
        result = func(self, *args, **kwargs)
        setattr(self, cache_name, result)
        return result
    return inner

这段代码中只有两个新技巧。

  1. 我使用 hashlib 模块将函数的参数转换为唯一的单个字符串。
  2. 使用 getattr、hasattr 和 setattr 来操作实例对象上的缓存值。

现在,如果我们想缓存慢速方法,我们只需在方法声明上方添加一个 @cache 行。

@cache
def slow_compute(self, *args, **kwargs):
    time.sleep(1)
    return args, kwargs, 42

太棒了 - 现在我们可以将这个缓存装饰器重用于我们想要的任何方法。假设我们现在希望我们的缓存每调用 3 次就失效一次。这种柯里化的实际应用只是对原始缓存代码的略微修改。

import hashlib
def cache(loop_iter):
    def function_closure(func):
        func_name = func.func_name
        def closure(self, loop_iter, *args, **kwargs):
            arghash = hashlib.sha1(str(args) + str(kwargs)).hexdigest()
            cache_name = '_cache_%s_%s' % (func_name, arghash)
            counter_name = '_counter_%s_%s' % (func_name, arghash)
            if hasattr(self, cache_name):
                # If we have a cached value, just use it
                print "Fetching cached value from : %s" % cache_name
                loop_iter -= 1
                setattr(self, counter_name, loop_iter)
                result = getattr(self, cache_name)
                if loop_iter == 0:
                    delattr(self, counter_name)
                    delattr(self, cache_name)
                    print "Cleared cached value"
                return result
            result = func(self, *args, **kwargs)
            setattr(self, cache_name, result)
            setattr(self, counter_name, loop_iter)
            return result
        return closure
    return function_closure

现在我们可以自由地将 @cache 用于任何慢速方法,缓存将免费提供,包括自动失效缓存值。只需像这样使用它

@cache(10)
def slow_compute(self, *args, **kwargs):
    # TODO: stuff goes here...
    pass

回顾 - 以及如何将所有这些整合在一起的示例

现在,我要请你发挥一下想象力。我们已经非常快地涵盖了很多内容。

我们可以

  • 查找对象中的属性(使用 __dict__ 属性)。
  • 检查对象是否属于特定类层次结构(使用 isinstance 函数)。
  • 使用柯里化将其他函数构建成函数,甚至将这些函数绑定到任意名称

这太棒了 - 我们现在拥有了根据类属性生成复杂方法所需的所有基本构建块。想象一个简单的通讯录应用程序,其中包含一个简单的联系人。

class Contact(object):
    first_name = str
    last_name = str
    date_of_birth = datetime.Date

假设我们知道如何保存和加载到数据库,我们可以使用函数生成技术自动生成 load() 和 save() 方法,并将它们绑定到我们的 Contact 类中。我们可以使用我们的内省技术来确定需要保存到数据库中的属性。我们甚至可以将特殊方法扩展到我们的 Contact 类中,以便我们可以遍历所有类属性,并神奇地生成“searchby_first_name”和“searchby_last_name”方法。

看到这有多强大吗?我们可以编写极少的代码,并且可以生成所有用于保存、加载和搜索数据库中记录的所需专门行为。由于我们以编程方式完成所有这些操作,因此我们可以显着减少需要手动编写的代码量,并且通过这样做,我们可以减少在系统中引入错误的可能性。

我们将在后面的章节中进行这样的操作。构建一个简单的数据库抽象层,以演示如何创建自己的对象系统,该系统将自动知道如何读写数据库。

[1]新式类提供了许多旧式类中没有的实用功能。如果你最终将旧式类和新式类混合在一起,通常会得到意想不到的行为,这会让你感到惊讶 - 而且不是以好的方式。它会让你感到惊讶,让你整夜睡不着觉,想知道为什么你的代码无法正常工作,你会诅咒两种类型的类都存在的事实。
[2]Python 的优势之一是可读性 - 你的代码和其他代码的可读性。社区标准极大地提高了代码的可读性。