翻譯:《實用的Python編程》05_01_Dicts_revisited
- 2021 年 3 月 12 日
- 筆記
- Python, 實用的Python編程
目錄 | 上一節 (4.4 異常) | 下一節 (5.2 封裝)
5.1 再談字典
Python 對象系統主要基於字典實現。本節將對此進行討論。
字典
字典是命名值(named values)的集合。
stock = {
'name' : 'GOOG',
'shares' : 100,
'price' : 490.1
}
雖然字典常用於簡單的數據結構,但是字典也用於解釋器的關鍵部分。字典可能是 Python 中最重要的數據類型。
字典和模組
在模組內,字典存儲所有的全局變數和函數。
# foo.py
x = 42
def bar():
...
def spam():
...
可以通過 foo.__dict__
或 globals()
查看該字典。
{
'x' : 42,
'bar' : <function bar>,
'spam' : <function spam>
}
字典和對象
用戶定義對象的時候也使用到了實例字典和類字典。事實上,整個對象系統主要是基於字典實現的。
字典存儲實例數據,如 __dict__
:
>>> 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 # Getting
obj.name = value # Setting
del obj.name # Deleting
這些操作直接與字典綁定到一起。
修改實例
修改對象的操作會更新底層字典:
>>> s = Stock('GOOG', 100, 490.1)
>>> s.__dict__
{ 'name':'GOOG', 'shares': 100, 'price': 490.1 }
>>> s.shares = 50 # Setting
>>> s.date = '6/7/2007' # Setting
>>> s.__dict__
{ 'name': 'GOOG', 'shares': 50, 'price': 490.1, 'date': '6/7/2007' }
>>> del s.shares # Deleting
>>> 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):
...
在每個類中,父類存儲在一個元組中:
>>> A.__bases__
(<class '__main__.B'>, <class '__main__.C'>)
>>>
子類通過 __bases__
屬性可以鏈接到父類。
多繼承中的屬性查找
從邏輯上講,查找屬性的過程如下:首先,檢查局部字典 __dict__
。如果沒有找到,檢查類字典 __dict__
。如果在類中還是沒有找到,通過 __bases__
屬性在父類中查找。這裡面有一些小細節,我們接下來討論。
單繼承中的屬性查找
在繼承層級結構中,通過按順序遍歷繼承樹來找到屬性。
class A: pass
class B(A): pass
class C(A): pass
class D(B): pass
class E(D): pass
在單繼承中,因為到達上層父類的路徑只有一條,所以當找到第一個匹配的屬性時即可停止。
方法解析順序(MRO)
Python 會預先計算繼承鏈並將其存儲到類的 MRO 屬性中。你可以像這樣查看:
>>> E.__mro__
(<class '__main__.E'>, <class '__main__.D'>,
<class '__main__.B'>, <class '__main__.A'>,
<type 'object'>)
>>>
該繼承鏈稱為 方法解析順序(Method Resolution Order)。為了找到屬性,Python 按順序遍歷 MRO,第一個匹配的屬性即是要找的屬性。(譯註:有關 MRO 的更多資訊,請查看 //www.python.org/download/releases/2.3/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),協作多繼承遵守的類排序規則如下:
- 總是在檢查父類之前檢查子類
- 父類(如果有多個)總是按照列出的順序檢查
根據該規則, 通過按層級結構對所有的類進行排序,然後計算出方法解析順序。
>>> 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):
# Code commonality with LoudBike (below)
return super().noise().upper()
和
class Bike:
def noise(self):
return 'On Your Left'
def pedal(self):
return 'Pedaling!'
class LoudBike(Bike):
def noise(self):
# Code commonality with LoudDog (above)
return super().noise().upper()
LoudDog.noise()
方法和LoudBike.noise()
方法中有一些通用的程式碼。事實上,這些通用的程式碼是完全一樣的。自然,這樣的程式碼勢必會吸引軟體工程師。
“Mixin” 模式
Mixin 模式(pattern)是包含一部分程式碼片段的類。
class Loud:
def noise(self):
return super().noise().upper()
該類不能單獨使用。通過繼承和其它類混合使用。
class LoudDog(Loud, Dog):
pass
class LoudBike(Loud, Bike):
pass
神奇的是,noise()
方法只實現了一次,卻在兩個完全不相關的類中使用。這種技巧是 Python 多繼承的主要用途之一。
為什麼使用 super()
當要覆蓋一個方法的時候,總是使用 super()
函數。
class Loud:
def noise(self):
return super().noise().upper()
super()
函數代表 MRO 中的下一個類(譯註:LoudDog 的 MRO 是 LoudDog>Loud>Dog>object
。因為 Loud 的父類 object 沒有定義 noise() 方法,所以 LoudDog 的實例在 Loud 中找不到 noise() 方法。然後 LoudDog 的實例就會到 MRO 中 Loud 的下一個類 Dog 中尋找)。
麻煩的是你不知道它是什麼,尤其是使用多繼承的時候。
注意事項
多繼承是一種強大的機制。使用這種強大的機制時請牢記「權利越大,責任越大」。有時候,框架或者庫使用多繼承來實現一些高級特性,如組件組合。
練習
在第 4 節中,定義了一個表示股票持有資訊的類 Stock
。在本節練習中,我們將使用該類。請重新啟動解釋器並創建一些 Stock
類的實例:
>>> ================================ RESTART ================================
>>> from stock import Stock
>>> goog = Stock('GOOG',100,490.10)
>>> ibm = Stock('IBM',50, 91.23)
>>>
練習 5.1:實例的表示
在互動式 shell 中,檢查 goog
和 ibm
兩個實例的底層字典:
>>> goog.__dict__
... look at the output ...
>>> ibm.__dict__
... look at the output ...
>>>
練習 5.2:修改實例屬性
嘗試給上述其中一個實例添加新屬性:
>>> goog.date = '6/11/2007'
>>> goog.__dict__
... look at output ...
>>> ibm.__dict__
... look at output ...
>>>
在上述輸出中,你會發現 goog
實例具有 date
屬性,但是 ibm
實例沒有。重要的是要注意,Python 對實例屬性確實沒有任何限制。例如,實例屬性不限於 __init__()
方法中設置的屬性。
嘗試直接添加一個新的值到 __dict__
對象中:
>>> goog.__dict__['time'] = '9:45am'
>>> goog.time
'9:45am'
>>>
在這裡,你會發現一個事實,實例僅僅是字典頂部的一層。注意:應該強調的是,直接操作字典並不常見——你應該始終使用語法 (.) 編寫程式碼。
練習 5.3:類的作用
類中的定義被類的所有實例所共享。所有的實例都有一個鏈接,指向它們的關聯類:
>>> goog.__class__
... look at output ...
>>> ibm.__class__
... look at output ...
>>>
嘗試在實例上調用方法:
>>> goog.cost()
49010.0
>>> ibm.cost()
4561.5
>>>
名字 ‘cost’ 既不在 goog.__dict__
中定義,也不在 ibm.__dict__
中定義。相反,而是由類字典提供的。請嘗試以下程式碼:
>>> Stock.__dict__['cost']
... look at output ...
>>>
嘗試直接通過字典調用 cost()
方法:
>>> Stock.__dict__['cost'](goog)
49010.0
>>> Stock.__dict__['cost'](ibm)
4561.5
>>>
你是如何調用類中定義的函數,那麼 self
就是怎麼調用實例的。
嘗試給 Stock
類添加新屬性::
>>> Stock.foo = 42
>>>
該新屬性會出現在所有實例中:
>>> goog.foo
42
>>> ibm.foo
42
>>>
但是,foo
並不屬於實例字典:
>>> goog.__dict__
... look at output and notice there is no 'foo' attribute ...
>>>
你可以訪問 foo
屬性的原因是:當 Python 在實例字典中查找不到某個屬性時,那麼它就會到類字典中查找。
注意:本部分主要闡明什麼是類變數。假設你有這樣一個類:
class Foo(object):
a = 13 # Class variable
def __init__(self,b):
self.b = b # Instance variable
在 Foo 類中,因為變數 a
在類體(body of the class)中被賦值,所以 a
是「類變數(class variable)」。變數 a
可以被 Foo 類的所有實例所共享。示例:
>>> f = Foo(10)
>>> g = Foo(20)
>>> f.a # Inspect the class variable (same for both instances)
13
>>> g.a
13
>>> f.b # Inspect the instance variable (differs)
10
>>> g.b
20
>>> Foo.a = 42 # Change the value of the class variable
>>> 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) # Same as 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'>)
>>>
實例 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>
>>>
目錄 | 上一節 (4.4 異常) | 下一節 (5.2 封裝)
註:完整翻譯見 //github.com/codists/practical-python-zh