简介
Python 对象系统在很大程度上基于一种涉及字典(dictionary)的实现方式。本节将对此进行讨论。
Python 对象系统在很大程度上基于一种涉及字典(dictionary)的实现方式。本节将对此进行讨论。
要记住,字典(dictionary)是一个命名值的集合。
stock = {
'name' : 'GOOG',
'shares' : 100,
'price' : 490.1
}
字典通常用于简单的数据结构。不过,它们也用于解释器的关键部分,并且可能是 Python 中最重要的数据类型。
在一个模块(module)中,字典(dictionary)会保存所有的全局变量和函数。
## foo.py
x = 42
def bar():
...
def spam():
...
如果你查看 foo.__dict__ 或 globals(),就会看到这个字典。
{
'x' : 42,
'bar' : <function bar>,
'spam' : <function spam>
}
用户定义的对象(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 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>
}
实例和类是相互关联的。__class__ 属性会指向对应的类。
>>> s = Stock('GOOG', 100, 490.1)
>>> s.__dict__
{ 'name': 'GOOG', 'shares': 100, 'price': 490.1 }
>>> s.__class__
<class '__main__.Stock'>
>>>
实例字典保存着每个实例独有的数据,而类字典保存着 所有 实例共同共享的数据。
当你使用对象时,会使用 . 运算符来访问数据和方法。
x = obj.name ## 获取
obj.name = value ## 设置
del obj.name ## 删除
这些操作直接与底层的字典相关联。
修改对象的操作会更新底层的字典。
>>> 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' }
>>>
假设你要读取一个实例的属性。
x = obj.name
该属性可能存在于两个地方:
必须检查这两个字典。首先,检查本地的 __dict__。如果未找到,则通过 __class__ 查找类的 __dict__。
>>> s = Stock(...)
>>> s.name
'GOOG'
>>> s.cost()
49010.0
>>>
这种查找机制就是 类 的成员能够被所有实例共享的方式。
类可以从其他类继承。
class A(B, C):
...
基类(Base Class)会存储在每个类的一个元组中。
>>> A.__bases__
(<class '__main__.B'>, <class '__main__.C'>)
>>>
这提供了与父类的链接。
从逻辑上讲,查找属性的过程如下。首先,检查本地的 __dict__。如果未找到,则查找类的 __dict__。如果在类中也未找到,则通过 __bases__ 查找基类(Base Class)。不过,接下来会讨论其中一些微妙的方面。
在继承(Inheritance)层次结构中,属性是通过按顺序遍历继承树来查找的。
class A: pass
class B(A): pass
class C(A): pass
class D(B): pass
class E(D): pass
对于单继承,存在一条通向顶层的单一路径。找到第一个匹配项时,查找就会停止。
Python 会预先计算出一个继承链,并将其存储在类的 MRO 属性中。你可以查看它。
>>> E.__mro__
(<class '__main__.E'>, <class '__main__.D'>,
<class '__main__.B'>, <class '__main__.A'>,
<type 'object'>)
>>>
这个链被称为方法解析顺序。为了查找一个属性,Python 会按顺序遍历 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)”模式是一种包含代码片段的类。
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)
>>>
在交互式 shell 中,查看你创建的两个实例的底层字典:
>>> goog.__dict__
... 查看输出...
>>> ibm.__dict__
... 查看输出...
>>>
尝试为上述实例之一设置一个新属性:
>>> goog.date = '6/11/2007'
>>> goog.__dict__
... 查看输出...
>>> ibm.__dict__
... 查看输出...
>>>
在上述输出中,你会注意到 goog 实例有一个 date 属性,而 ibm 实例没有。需要注意的是,Python 实际上对属性并没有任何限制。例如,实例的属性并不局限于在 __init__() 方法中设置的那些。
不要直接设置属性,而是尝试将一个新值直接放入 __dict__ 对象中:
>>> goog.__dict__['time'] = '9:45am'
>>> goog.time
'9:45am'
>>>
在这里,你会真正意识到实例只是字典之上的一层封装。注意:需要强调的是,直接操作字典的情况并不常见——你应该始终编写代码来使用 (.) 语法。
构成类定义的内容由该类的所有实例共享。注意,所有实例都有一个指向其关联类的链接:
>>> 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
>>>
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
>>>
创建一个从 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'>)
>>>
下面展示了如何找到上述实例 n 的 cost() 方法:
>>> for cls in n.__class__.__mro__:
if 'cost' in cls.__dict__:
break
>>> cls
<class '__main__.Stock'>
>>> cls.__dict__['cost']
<function cost at 0x101aed598>
>>>
恭喜你!你已经完成了“字典回顾”实验。你可以在 LabEx 中练习更多实验来提升你的技能。