Skip to content

2.6 实现类和对象 ​

INFO

来源:2.6 Implementing Classes and Objects

对应:无

在面向对象编程范式(object-oriented programming paradigm)下工作时,我们用对象隐喻来指导程序的组织。如何表示和操作数据,大部分逻辑都在类声明中表达。本节会看到,类和对象本身只用函数和字典就能表示。以这种方式实现对象系统,是为了说明:使用对象隐喻并不需要一门特殊的编程语言。即使编程语言没有内置对象系统,程序一样可以是面向对象的。

要实现对象,我们会放弃点表示法(它确实需要语言内置支持),转而创建分派字典,让它们的行为与内置对象系统的元素非常相似。前面已经见过如何用分派字典实现消息传递。要完整实现一个对象系统,我们会在实例、类和基类之间发送消息,而它们都是包含属性的字典。

我们不会实现完整的 Python 对象系统——它包含本书没有涉及的功能(比如元类(meta-class)和静态方法)。我们只关注用户定义的类,不涉及多重继承,也不涉及内省行为(比如返回实例所属的类)。我们的实现并不打算遵循 Python 类型系统的精确规范,而是要实现支撑对象隐喻的核心功能。

2.6.1 实例 ​

先从实例开始。实例有具名属性,比如一个账户的余额,可以设置,也可以读取。我们用一个分派字典实现实例,它能响应「get」和「set」属性值的消息。属性本身存放在一个名为 attributes 的局部字典里。

本章前面已经讲过,字典本身就是抽象数据类型。我们用列表实现字典,用「对」实现列表,再用函数实现「对」。既然用字典实现对象系统,那就别忘了,我们同样可以只靠函数来实现对象。

开始实现之前,先假定已经有一个类的实现,它能查找任何不属于实例的名称。我们把一个类传给 make_instance,作为形参 cls。

python
>>> def make_instance(cls):
        """返回一个新的对象实例,它是一个分派字典。"""
        def get_value(name):
            if name in attributes:
                return attributes[name]
            else:
                value = cls['get'](name)
                return bind_method(value, instance)
        def set_value(name, value):
            attributes[name] = value
        attributes = {}
        instance = {'get': get_value, 'set': set_value}
        return instance

instance 是一个分派字典,能响应 get 和 set 消息。set 消息对应 Python 对象系统里的属性赋值:所有被赋值的属性都直接存进对象本地的属性字典。处理 get 消息时,如果 name 不在本地的 attributes 字典里,就到类里查找。如果类返回的值是一个函数,就必须把它绑定到实例上。

绑定方法值。 make_instance 里的 get_value 函数用 get 消息在类中查找具名属性,然后调用 bind_method。方法绑定只对函数值生效:它把 instance 作为第一个参数插入函数值,从而由函数值创建出绑定方法值:

python
>>> def bind_method(value, instance):
        """如果 value 可调用,就返回绑定方法,否则返回 value。"""
        if callable(value):
            def method(*args):
                return value(instance, *args)
            return method
        else:
            return value

按照这个定义,方法被调用时,第一个形参 self 会绑定到 instance 的值。

2.6.2 类 ​

不论是在 Python 的对象系统里,还是在我们自己实现的系统中,类本身也是一个对象。为简单起见,我们说类本身没有类。(在 Python 中,类确实有类;几乎所有类都共享同一个类,叫做 type。)一个类能响应 get 和 set 消息,也能响应 new 消息:

python
>>> def make_class(attributes, base_class=None):
        """返回一个新的类,它是一个分派字典。"""
        def get_value(name):
            if name in attributes:
                return attributes[name]
            elif base_class is not None:
                return base_class['get'](name)
        def set_value(name, value):
            attributes[name] = value
        def new(*args):
            return init_instance(cls, *args)
        cls = {'get': get_value, 'set': set_value, 'new': new}
        return cls

和实例不同,类的 get 函数在找不到属性时不会去查它的类,而是查它的基类(base_class)。类不需要绑定方法。

初始化。 make_class 里的 new 函数会调用 init_instance,后者先创建一个新实例,再调用一个名为 __init__ 的方法。

python
>>> def init_instance(cls, *args):
    """返回一个类型为 cls、并用 args 初始化的新对象。"""
    instance = make_instance(cls)
    init = cls['get']('__init__')
    if init:
        init(instance, *args)
    return instance

最后一个函数补全了整个对象系统。现在有了实例:处理 set 时在本地设置,处理 get 时则回退到类中。实例在类里查到一个名称后,会把自己绑定到函数值上,从而创建方法。最后,类可以创建新实例,并在实例创建后立即调用它们的 __init__ 构造函数。

在这个对象系统中,用户唯一需要调用的函数是 make_class,其他功能都靠消息传递实现。同样,Python 的对象系统通过 class 语句启用,其他功能都靠点表达式和对类的调用实现。

2.6.3 使用已实现的对象 ​

现在回到上一节的银行账户例子。用我们自己实现的对象系统,创建 Account 类、CheckingAccount 子类,并分别创建各自的实例。

Account 类由 make_account_class 函数创建,它的结构与 Python 中的 class 语句相似,只是最后会调用 make_class。

python
>>> def make_account_class():
        """返回 Account 类,它有 deposit 和 withdraw 方法。"""
        interest = 0.02
        def __init__(self, account_holder):
            self['set']('holder', account_holder)
            self['set']('balance', 0)
        def deposit(self, amount):
            """把账户余额增加 amount,并返回新余额。"""
            new_balance = self['get']('balance') + amount
            self['set']('balance', new_balance)
            return self['get']('balance')
        def withdraw(self, amount):
            """把账户余额减少 amount,并返回新余额。"""
            balance = self['get']('balance')
            if amount > balance:
                return 'Insufficient funds'
            self['set']('balance', balance - amount)
            return self['get']('balance')
        return make_class(locals())

最后对 locals 的调用返回一个以字符串为键的字典,其中包含当前局部帧里的名称-值绑定。

Account 类最终通过赋值实例化。

python
>>> Account = make_account_class()

然后通过 new 消息创建一个账户实例,创建时需要给新账户一个名称。

python
>>> kirk_account = Account['new']('kirk')

之后向 kirk_account 发送 get 消息,就能取出属性和方法。调用方法即可更新账户余额。

python
>>> kirk_account['get']('holder')
'kirk'
>>> kirk_account['get']('interest')
0.02
>>> kirk_account['get']('deposit')(20)
20
>>> kirk_account['get']('withdraw')(5)
15

和 Python 对象系统一样,设置实例的属性,不会改变它所属类中对应的属性。

python
>>> kirk_account['set']('interest', 0.04)
>>> Account['get']('interest')
0.02

继承。 重载类的一部分属性,就能创建子类 CheckingAccount。这里我们改写 withdraw 方法来收取费用,同时降低利率。

python
>>> def make_checking_account_class():
        """返回 CheckingAccount 类,它在取款时收取 1 美元手续费。"""
        interest = 0.01
        withdraw_fee = 1
        def withdraw(self, amount):
            fee = self['get']('withdraw_fee')
            return Account['get']('withdraw')(self, amount + fee)
        return make_class(locals(), Account)

这个实现里,我们从子类的 withdraw 函数调用基类 Account 的 withdraw 函数,就像在 Python 内置对象系统里那样。和之前一样,我们可以创建子类本身,再创建一个实例。

python
>>> CheckingAccount = make_checking_account_class()
>>> jack_acct = CheckingAccount['new']('Spock')

存款行为完全一样,构造函数也一样。取款由专门的 withdraw 方法收取 1 美元的费用,interest 则取 CheckingAccount 里新的较低值。

python
>>> jack_acct['get']('interest')
0.01
>>> jack_acct['get']('deposit')(20)
20
>>> jack_acct['get']('withdraw')(5)
14

我们基于字典构建的对象系统,在实现上和 Python 内置的对象系统非常相似。在 Python 中,任何用户定义类的实例都有一个特殊属性 __dict__,它把该对象的局部实例属性存进一个字典,很像我们的 attributes 字典。Python 的不同之处在于,它区分了一些特殊方法,这些方法与内置函数交互,确保这些函数对许多不同类型的参数都能正确运行。操作不同类型的函数,是下一节的主题。

基于 MIT 许可发布