Python 对象系统基础

Beginner

This tutorial is from open-source community. Access the source code

简介

Python 对象系统在很大程度上基于一种涉及字典(dictionary)的实现方式。本节将对此进行讨论。

这是一个实验(Guided Lab),提供逐步指导来帮助你学习和实践。请仔细按照说明完成每个步骤,获得实际操作经验。根据历史数据,这是一个 中级 级别的实验,完成率为 73%。获得了学习者 100% 的好评率。

再谈字典(Dictionary)

要记住,字典(dictionary)是一个命名值的集合。

stock = {
    'name' : 'GOOG',
    'shares' : 100,
    'price' : 490.1
}

字典通常用于简单的数据结构。不过,它们也用于解释器的关键部分,并且可能是 Python 中最重要的数据类型

字典(Dict)与模块(Module)

在一个模块(module)中,字典(dictionary)会保存所有的全局变量和函数。

## foo.py

x = 42
def bar():
   ...

def spam():
   ...

如果你查看 foo.__dict__globals(),就会看到这个字典。

{
    'x' : 42,
    'bar' : <function bar>,
    'spam' : <function spam>
}

字典(Dict)与对象(Object)

用户定义的对象(object)同样使用字典(dictionary)来存储实例数据和类数据。实际上,整个对象系统大多是构建在字典之上的一层额外抽象。

字典用于保存实例数据,即 __dict__

>>> from stock import Stock
>>> s = Stock('GOOG', 100, 490.1)
>>> s.__dict__
{'name' : 'GOOG', 'shares' : 100, 'price': 490.1 }

当你对 self 进行赋值操作时,就会填充这个字典(以及实例)。

class Stock:
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

实例数据 self.__dict__ 看起来是这样的:

{
    'name': 'GOOG',
    'shares': 100,
    'price': 490.1
}

每个实例都有自己独立的私有字典。

s = Stock('GOOG', 100, 490.1)     ## {'name' : 'GOOG','shares' : 100, 'price': 490.1 }
t = Stock('AAPL', 50, 123.45)     ## {'name' : 'AAPL','shares' : 50, 'price': 123.45 }

如果你创建了某个类的 100 个实例,就会有 100 个字典用于存储数据。

类成员(Class Member)

还有一个独立的字典用于保存方法。

class Stock:
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

    def cost(self):
        return self.shares * self.price

    def sell(self, nshares):
        self.shares -= nshares

这个字典存储在 Stock.__dict__ 中。

{
    'cost': <function>,
    'sell': <function>,
    '__init__': <function>
}

实例(Instance)与类(Class)

实例和类是相互关联的。__class__ 属性会指向对应的类。

>>> s = Stock('GOOG', 100, 490.1)
>>> s.__dict__
{ 'name': 'GOOG', 'shares': 100, 'price': 490.1 }
>>> s.__class__
<class '__main__.Stock'>
>>>

实例字典保存着每个实例独有的数据,而类字典保存着 所有 实例共同共享的数据。

属性访问(Attribute Access)

当你使用对象时,会使用 . 运算符来访问数据和方法。

x = obj.name          ## 获取
obj.name = value      ## 设置
del obj.name          ## 删除

这些操作直接与底层的字典相关联。

修改实例(Instance)

修改对象的操作会更新底层的字典。

>>> s = Stock('GOOG', 100, 490.1)
>>> s.__dict__
{ 'name':'GOOG', 'shares': 100, 'price': 490.1 }
>>> s.shares = 50       ## 设置
>>> s.date = '6/7/2007' ## 设置
>>> s.__dict__
{ 'name': 'GOOG', 'shares': 50, 'price': 490.1, 'date': '6/7/2007' }
>>> del s.shares        ## 删除
>>> s.__dict__
{ 'name': 'GOOG', 'price': 490.1, 'date': '6/7/2007' }
>>>

读取属性(Attribute)

假设你要读取一个实例的属性。

x = obj.name

该属性可能存在于两个地方:

  • 本地实例字典。
  • 类字典。

必须检查这两个字典。首先,检查本地的 __dict__。如果未找到,则通过 __class__ 查找类的 __dict__

>>> s = Stock(...)
>>> s.name
'GOOG'
>>> s.cost()
49010.0
>>>

这种查找机制就是 的成员能够被所有实例共享的方式。

继承(Inheritance)的工作原理

类可以从其他类继承。

class A(B, C):
   ...

基类(Base Class)会存储在每个类的一个元组中。

>>> A.__bases__
(<class '__main__.B'>, <class '__main__.C'>)
>>>

这提供了与父类的链接。

通过继承(Inheritance)读取属性(Attribute)

从逻辑上讲,查找属性的过程如下。首先,检查本地的 __dict__。如果未找到,则查找类的 __dict__。如果在类中也未找到,则通过 __bases__ 查找基类(Base Class)。不过,接下来会讨论其中一些微妙的方面。

通过单继承(Single Inheritance)读取属性(Attribute)

在继承(Inheritance)层次结构中,属性是通过按顺序遍历继承树来查找的。

class A: pass
class B(A): pass
class C(A): pass
class D(B): pass
class E(D): pass

对于单继承,存在一条通向顶层的单一路径。找到第一个匹配项时,查找就会停止。

方法解析顺序(Method Resolution Order,MRO)

Python 会预先计算出一个继承链,并将其存储在类的 MRO 属性中。你可以查看它。

>>> E.__mro__
(<class '__main__.E'>, <class '__main__.D'>,
 <class '__main__.B'>, <class '__main__.A'>,
 <type 'object'>)
>>>

这个链被称为方法解析顺序。为了查找一个属性,Python 会按顺序遍历 MRO。第一个匹配项即为结果。

多重继承(Multiple Inheritance)中的 MRO

在多重继承中,没有单一的路径通向顶层。让我们来看一个例子。

class A: pass
class B: pass
class C(A, B): pass
class D(B): pass
class E(C, D): pass

当你访问一个属性时会发生什么呢?

e = E()
e.attr

会进行属性搜索过程,但搜索顺序是怎样的呢?这就是个问题了。

Python 使用“协作式多重继承(cooperative multiple inheritance)”,它遵循一些关于类排序的规则。

  • 总是先检查子类,再检查父类
  • (如果有多个)父类总是按照列出的顺序进行检查

MRO 是根据这些规则对层次结构中的所有类进行排序计算得出的。

>>> E.__mro__
(
  <class 'E'>,
  <class 'C'>,
  <class 'A'>,
  <class 'D'>,
  <class 'B'>,
  <class 'object'>)
>>>

底层的算法被称为“C3 线性化算法(C3 Linearization Algorithm)”。只要你记住,类层次结构遵循的排序规则就像你的房子着火了,你必须疏散时遵循的规则一样——先救孩子,再救父母,具体细节并不重要。

一种奇特的代码复用方式(涉及多重继承)

考虑两个完全不相关的对象:

class Dog:
    def noise(self):
        return 'Bark'

    def chase(self):
        return 'Chasing!'

class LoudDog(Dog):
    def noise(self):
        ## 与下面的 LoudBike 有代码共性
        return super().noise().upper()

以及

class Bike:
    def noise(self):
        return 'On Your Left'

    def pedal(self):
        return 'Pedaling!'

class LoudBike(Bike):
    def noise(self):
        ## 与上面的 LoudDog 有代码共性
        return super().noise().upper()

LoudDog.noise()LoudBike.noise() 的实现中存在代码共性。实际上,代码是完全相同的。自然地,这样的代码必然会吸引软件工程师的注意。

“混入(Mixin)”模式

“混入(Mixin)”模式是一种包含代码片段的类。

class Loud:
    def noise(self):
        return super().noise().upper()

这个类不能单独使用。它通过继承与其他类混合。

class LoudDog(Loud, Dog):
    pass

class LoudBike(Loud, Bike):
    pass

神奇的是,现在“大声”的功能只实现了一次,就被复用在了两个完全不相关的类中。这种技巧是 Python 中多重继承的主要用途之一。

为什么使用 super()

重写方法时,始终要使用 super()

class Loud:
    def noise(self):
        return super().noise().upper()

super() 会将调用委托给 MRO 中的“下一个类”。

棘手的是,你并不知道这个“下一个类”具体是什么。尤其是在使用多重继承时,你更难确定它是什么。

一些注意事项

多重继承是一个强大的工具。要记住,能力越大,责任越大。框架/库有时会用它来实现涉及组件组合的高级功能。现在,忘了你看到的这些吧。

在第 4 节中,你定义了一个表示股票持仓的类 Stock。在这个练习中,我们将使用这个类。重启解释器并创建几个实例:

>>> ================================ RESTART ================================
>>> from stock import Stock
>>> goog = Stock('GOOG',100,490.10)
>>> ibm  = Stock('IBM',50, 91.23)
>>>

练习 5.1:实例的表示

在交互式 shell 中,查看你创建的两个实例的底层字典:

>>> goog.__dict__
... 查看输出...
>>> ibm.__dict__
... 查看输出...
>>>

练习 5.2:实例数据的修改

尝试为上述实例之一设置一个新属性:

>>> goog.date = '6/11/2007'
>>> goog.__dict__
... 查看输出...
>>> ibm.__dict__
... 查看输出...
>>>

在上述输出中,你会注意到 goog 实例有一个 date 属性,而 ibm 实例没有。需要注意的是,Python 实际上对属性并没有任何限制。例如,实例的属性并不局限于在 __init__() 方法中设置的那些。

不要直接设置属性,而是尝试将一个新值直接放入 __dict__ 对象中:

>>> goog.__dict__['time'] = '9:45am'
>>> goog.time
'9:45am'
>>>

在这里,你会真正意识到实例只是字典之上的一层封装。注意:需要强调的是,直接操作字典的情况并不常见——你应该始终编写代码来使用 (.) 语法。

练习 5.3:类的作用

构成类定义的内容由该类的所有实例共享。注意,所有实例都有一个指向其关联类的链接:

>>> goog.__class__
... 查看输出...
>>> ibm.__class__
... 查看输出...
>>>

尝试在实例上调用一个方法:

>>> goog.cost()
49010.0
>>> ibm.cost()
4561.5
>>>

注意,名称 cost 既不在 goog.__dict__ 中定义,也不在 ibm.__dict__ 中定义。相反,它是由类字典提供的。试试这个:

>>> Stock.__dict__['cost']
... 查看输出...
>>>

尝试通过字典直接调用 cost() 方法:

>>> Stock.__dict__['cost'](goog)
49010.0
>>> Stock.__dict__['cost'](ibm)
4561.5
>>>

注意你是如何调用类定义中定义的函数的,以及 self 参数是如何获取实例的。

尝试向 Stock 类添加一个新属性:

>>> Stock.foo = 42
>>>

注意这个新属性现在如何在所有实例上显示:

>>> goog.foo
42
>>> ibm.foo
42
>>>

然而,注意它并不是实例字典的一部分:

>>> goog.__dict__
... 查看输出并注意没有 'foo' 属性...
>>>

你可以在实例上访问 foo 属性的原因是,如果 Python 在实例本身找不到某个东西,它总是会检查类字典。

注意:本练习的这部分说明了所谓的类变量。例如,假设你有这样一个类:

class Foo(object):
     a = 13                  ## 类变量
     def __init__(self,b):
         self.b = b          ## 实例变量

在这个类中,在类本身的主体中赋值的变量 a 是一个“类变量”。它由所有创建的实例共享。例如:

>>> f = Foo(10)
>>> g = Foo(20)
>>> f.a          ## 检查类变量(两个实例相同)
13
>>> g.a
13
>>> f.b          ## 检查实例变量(不同)
10
>>> g.b
20
>>> Foo.a = 42   ## 更改类变量的值
>>> f.a
42
>>> g.a
42
>>>

练习 5.4:绑定方法

Python 一个微妙的特性是,调用方法实际上涉及两个步骤,以及所谓的绑定方法。例如:

>>> s = goog.sell
>>> s
<bound method Stock.sell of Stock('GOOG', 100, 490.1)>
>>> s(25)
>>> goog.shares
75
>>>

绑定方法实际上包含了调用方法所需的所有部分。例如,它们会记录实现该方法的函数:

>>> s.__func__
<function sell at 0x10049af50>
>>>

这与 Stock 字典中的值相同。

>>> Stock.__dict__['sell']
<function sell at 0x10049af50>
>>>

绑定方法还会记录实例,即 self 参数。

>>> s.__self__
Stock('GOOG',75,490.1)
>>>

当你使用 () 调用函数时,所有部分就会组合在一起。例如,调用 s(25) 实际上是这样做的:

>>> s.__func__(s.__self__, 25)    ## 与 s(25) 相同
>>> goog.shares
50
>>>

练习 5.5:继承

创建一个从 Stock 类继承的新类。

>>> class NewStock(Stock):
        def yow(self):
            print('Yow!')

>>> n = NewStock('ACME', 50, 123.45)
>>> n.cost()
6172.50
>>> n.yow()
Yow!
>>>

继承是通过扩展属性搜索过程来实现的。__bases__ 属性包含一个直接父类的元组:

>>> NewStock.__bases__
(<class 'stock.Stock'>,)
>>>

__mro__ 属性包含所有父类的元组,按照搜索属性的顺序排列。

>>> NewStock.__mro__
(<class '__main__.NewStock'>, <class 'stock.Stock'>, <class 'object'>)
>>>

下面展示了如何找到上述实例 ncost() 方法:

>>> for cls in n.__class__.__mro__:
        if 'cost' in cls.__dict__:
            break

>>> cls
<class '__main__.Stock'>
>>> cls.__dict__['cost']
<function cost at 0x101aed598>
>>>

总结

恭喜你!你已经完成了“字典回顾”实验。你可以在 LabEx 中练习更多实验来提升你的技能。