天天看點

翻譯:《實用的Python程式設計》05_01_Dicts_revisited5.1 再談字典

目錄 | 上一節 (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 的更多資訊,請檢視 https://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 封裝)

注:完整翻譯見 https://github.com/codists/practical-python-zh