翻譯:《實用的Python程式設計》05_01_Dicts_revisited

codists發表於2021-03-12

目錄 | 上一節 (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 中,檢查 googibm 兩個例項的底層字典:

>>> 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

相關文章