2011年9月10日 星期六

模組名稱空間

在Python中,模組實際的作用之一是作為名稱空間,你所建立的變數,最大有效範圍就是模組範圍,而你所定義的函式名稱、類別名稱,實際上也是以模組作為名稱空間。例如:

  • mod.py
x = 10

def some():
    print(x)
    
class Other:
    def __init__(self, arg):
        self.arg = arg
    def show(self):
        print(self.arg)

在這個模組中定義了x變數、some函式與Other類別,你可以在另一個模組中這麼使用它:
import mod

print(mod.x)  # 10

mod.some()    # 10

other = mod.Other(20)
other.show()  # 20

實際上,Python中一個可作為名稱空間的物件,都具有__dict__特性(參考 特性名稱空間 ),模組也不例外。例如,若要存取上例中mod模組的x變數、some函式或Other類別,亦可如下操作:
import mod

print(mod.__dict__['x'])            # 10

mod.__dict__['some']()              # 10

other = mod.__dict__['Other'](20)   
other.show()                        # 20
      

你可以使用from import語句,於目前模組中建立與被匯入模組相同的變數名稱。例如:
from mod import *

print(x)              # 10

some()                # 10

other = Other(20) 
other.show()          # 20

使用from import語句時,若最後是*結尾,則會將被匯入模組中所有變數,在當前模組中都建立相同的名稱。如果你有些變數,並不想被from import *建立同名變數,則可以用底線作為開頭。例如:
  • mod.py
x = 10

def some():
    print(x)
    
class Other:
    def __init__(self, arg):
        self.arg = arg
    def show(self):
        print(self.arg)
        
_y = 10

上例中,mod模組有個_y變數,它不會被from import *用來建立同名變數。例如:
from mod import *

print(x)            # 10
some()              # 10
other = Other(20)
other.show()        # 20

print(_y)           # NameError: name '_y' is not defined

不過這個避免被建立同名變數的方式,僅適用於from import *,其它的import形式就不起作用了。例如:
from mod import _y
print(_y)           # 10

相對的,你可以在模組中建立一個__all__變數,參考至串列實例,內含想要被其它模組from import *的變數名單,如果有這麼一個__all__變數,則只有名單中的變數,才可以被其它模組from import *。例如:
__all__ = ['x', 'some']

x = 10

def some():
    print(x)
    
class Other:
    def __init__(self, arg):
        self.arg = arg
    def show(self):
        print(self.arg)

上例中,僅開放x變數與some函式被其它模組from import *。所以:
from mod import *

print(x)           # 10

some()             # 10

other = Other(20)  # NameError: name 'Other' is not defined

匯入套件

如果你在dir1目錄中有有個名為mod的模組檔案,而dir2中也有個名為mod的模組檔案。若sys.path中包括dir1與dir2。如果現在你的應用程式中,撰寫了以下的程式:

import mod
...

那麼,會匯入的僅有dir1/mod模組檔案。模組檔案的尋找,是以sys.path中的路徑,先找到符合的名稱就使用。

如果要同時使用到dir1/mod.py與dir2/mod.py,那麼單純僅使用sys.path來尋找模組無法達到這個目的。同名模組的名稱衝突問題,可以藉由套件(Package)的使用來解決。在Python中,套件的實體結構就是目錄,而每個套件目錄中必須放置一個__init__.py檔案。例如:
[sys.path 之一]
              |
              [dir1]

              |    |__init__.py
              |    |mod.py
              |    |helper.py
              |    [subdir1]
              |            |__init__.py
              |            |ooo.py
              [dir2]
                   |__init__.py
                   |mod.py

每個套件資料夾都要有一個__init__.py。在上面的例子中,若要在程式中使用dir1/mod.py模組檔案,則可以:
import dir1.mod
print(dir1.mod.name)

在上例中,假設mod中有個name變數,則要取用該變數,則是透過「套件.模組.變數」的方式來取得,透過套件階層的安排,可以為模組分門別類,在程式中則使用「套件.模組」的方式來減少名稱衝突的問題,而若要檢閱模組,套件階層也有助於尋找出模組檔案的正確位置。

若遇到套件階層名稱較長,例如cc.openhome.web.mvc.xxxmodel這樣名稱,在程式中撰寫冗長,則可以透過import as來讓程式便於撰寫。例如:
import cc.openhome.web.mvc.xxxmodel as xxxmodel
xxxmodel.dosome()

或者是透過from import在目前模組中建立想要存取的變數名稱即可。例如:
from cc.openhome.web.mvc.xxxmodel import dosome
dosome()

在Python中,如果為同一套件中的模組檔案,無需時完整import語法來匯入模組,而可以透過相對匯入語法來彼此匯入。例如:
from .helper import func1
func1()

注意,在上面的from語句後面的模組名稱前有個 . ,這表示相對匯入同一套件中的helper.py模組檔案,從而避免了必須從sys.path中尋找,以及在同一套件中模組彼此引用的語法冗長。

每個套件目錄中都必須有個__init__.py,當你匯入套件時,該套件目錄中的__init__.py也會被執行,你可以在其中作一些匯入套件時所必須進行的初始化動作。

import、import as、from import

接續 匯入模組,使用import匯入模組,會以被匯入的模組名稱在當前模組命名一個同名的名稱,被匯入模組中的變數,其實是以被匯入模組為名稱空間。例如,若有個some.py:

name = 'Justin'

若你匯入some模組,要存取some模組中的name變數,則必須使用some.name。事實上,你可以使用del刪除與被匯入模組同名的變數:
import some
print(some.name)  # Justin
del some
print(some.name)  # NameError: name 'some' is not defined

上例中,於del some後,由於目前模組中已沒有some變數,因而發生錯誤。你可以在del與被匯入模組同名的變數,之後再度使用import設回該變數。例如:
import some
print(some.name)  # Justin
del some
import some
print(some.name)  # Justin
      

再次匯入模組,可以設回與被匯入模組的同名變數,但不會再度執行被匯入模組,import模組時只會執行模組一次,再次import是不會再度執行模組的。

如果想要知道現在到底匯入了多少模組,則可以使用sys.modules得知,這是個字典物件。例如:

>>> import sys
>>> sys.modules.keys()
dict_keys(['heapq', 'sre_compile', '_collections', 'locale', '_multibytecodec',
'functools', 'encodings', 'site', 'operator', 'io', '__main__', 'copyreg', '_hea
pq', '_weakref', 'abc', 'builtins', 'errno', 'itertools', 'sre_constants', 're',
 'encodings.latin_1', 'collections', 'ntpath', '_sre', 'nt', 'genericpath', 'sta
t', 'zipimport', '_codecs', '_bisect', 'encodings.utf_8', 'sys', 'codecs', 'os.p
ath', '_functools', '_locale', 'keyword', 'bisect', '_codecs_tw', 'signal', 'wea
kref', '_io', '_weakrefset', 'encodings.cp950', 'encodings.aliases', 'sre_parse'
, 'os', '_abcoll'])
>>>


如果你使用del刪除某個與被匯入模組同名的變數,並不會影響sys.modules。例如:
import sys
import some
print(some.name)     # Justin
del some
print(sys.modules['some'].name)  # Justin
print(some.name)    # NameError: name 'some' is not defined

如果你想要改變被匯入模組在當前模組中的變數名稱,則可以使用import as。例如:
import some as other
print(other.name)   # Justin

import as改變的是被匯入模組在當前模組的名稱,而不是sys.modules中的名稱,以上例來說,sys.modules中的名稱仍然是some。上例比較像是:
import some
other = some
del some
print(other.name)

可以使用from import語「複製」出模組中的名稱。例如:
import some
from some import name

print(name)            # Justin
print(some.name)       # Justin

name = 'caterpillar'  
print(name)            # caterpillar
print(some.name)       # Justin

上例比較像是:
import some
name = some.name

print(name)
print(some.name)

name = 'caterpillar'
print(name)
print(some.name)

你可以一次匯入兩個以上的名稱,例如:
from some import x, y

或者是將模組中的名稱全部匯入,例如:
from some import *

Python是個不斷演進的語言,有些新語法或功能會不相容於舊版本Python,甚至破壞舊版本Python原本的功能,如果你想要體驗一下將來版本可能有的新語法或功能,則可以透過__future__模組。例如在Python 2.6中,print是個陳述句,而在Python 3中,print是個函式,若要在Python 2.6中使用Python 3中的print函式,則可以:
from __future__ import print_function

匯入模組

接續 簡介模組 的內容。sys.path陣列中的字串元素其實是由幾個來源所組成:所執行檔案(模組)的所在目錄、PYTHONPATH環境變數的內容、標準程式庫搜尋目錄、.pth檔案中所列出的目錄。

例如,若在Windows上,有個模組檔案位於C:\workspace,而Python安裝於C:\winware\python31:

import sys
print(sys.path)

若你如下執行程式:

C:\workspace>c:\winware\python31\python demo.py
['C:\\workspace', 'C:\\Windows\\system32\\python31.zip', 'c:\\winware\\python31\
\DLLs', 'c:\\winware\\python31\\lib', 'c:\\winware\\python31\\lib\\plat-win', 'c
:\\winware\\python31', 'c:\\winware\\python31\\lib\\site-packages']

C:\workspace>set PYTHONPATH=C:\lib

C:\workspace>c:\winware\python31\python demo.py
['C:\\workspace', 'C:\\lib', 'C:\\Windows\\system32\\python31.zip', 'c:\\winware
\\python31\\DLLs', 'c:\\winware\\python31\\lib', 'c:\\winware\\python31\\lib\\pl
at-win', 'c:\\winware\\python31', 'c:\\winware\\python31\\lib\\site-packages']

C:\workspace>


C:\\workspace是所執行檔案所在目錄、C:\\lib是PYTHONPATH的設定內容,其它是標準程式庫搜尋目錄,至於.pth檔案所列出的目錄,是指你可以在Python安裝目錄,或者是安裝目錄的lib\site-packages目錄中建立.pth檔案,當中一行一行列出搜尋模組檔案的目錄,如果該目錄確實存在,則會列為sys.path的內容。例如,若在C:\winware\python31中放置mydir.pth,內容如下:
C:\lib2
C:\lib3

而在c:\winware\python31\lib\site-packages中放置一個mydir.pth,內容如下:
C:\lib4
C:\lib5

如果所列的目錄確實存在,則再度執行demo.py,會顯示如下:

C:\workspace>c:\winware\python31\python demo.py
['C:\\workspace', 'C:\\lib', 'C:\\Windows\\system32\\python31.zip', 'C:\\Winware
\\Python31\\DLLs', 'C:\\Winware\\Python31\\lib', 'C:\\Winware\\Python31\\lib\\pl
at-win', 'C:\\Winware\\Python31', 'C:\\lib2', 'C:\\lib3', 'C:\\Winware\\Python31
\\lib\\site-packages', 'C:\\lib4', 'C:\\lib5']

C:\workspace>


import會在sys.path中尋找.py或已編譯的.pyc,如果.py尚未編譯則編譯為.pyc,而後載入執行一次,再次import同一個模組並不會再執行一次模組,如果找到.py與.pyc,而.pyc的版本並沒有比.py舊,則略過編譯直接執行,如果僅找到.pyc而沒有找到.py,也會直接執行。

在執行時期,你可以動態地改變sys.path的內容,來改變搜尋模組的路徑。例如,若有個superman.py放在C:\library中,而superman.py的內容如下:
print('superman modulle')
name = 'Justin'

而你執行的檔案為:
import sys
print(sys.path, end='\n\n')

sys.path.append('c:\library')
print(sys.path, end='\n\n')

import superman
print(superman.name)

則執行的結果會是:

C:\workspace>c:\winware\python31\python demo.py
['C:\\workspace', 'C:\\lib', 'C:\\Windows\\system32\\python31.zip', 'C:\\Winware
\\Python31\\DLLs', 'C:\\Winware\\Python31\\lib', 'C:\\Winware\\Python31\\lib\\pl
at-win', 'C:\\Winware\\Python31', 'C:\\Winware\\Python31\\lib\\site-packages']

['C:\\workspace', 'C:\\lib', 'C:\\Windows\\system32\\python31.zip', 'C:\\Winware
\\Python31\\DLLs', 'C:\\Winware\\Python31\\lib', 'C:\\Winware\\Python31\\lib\\pl
at-win', 'C:\\Winware\\Python31', 'C:\\Winware\\Python31\\lib\\site-packages', '
c:\\library']

superman modulle
Justin

C:\workspace>


Python的import是執行時期的運算,import某個模組,就會執行該模組中定義的內容,被import的模組名稱會成為目前模組的變數,而被import的模組中的變數,就是以被import模組名稱為名稱空間。

物件相等性

果你定義了類別時沒有定義__eq__()方法,則預設使用==比較兩個實例時,會得到與使用is比較相同的結果。例如:
>>> class Some:
...     pass
...
>>> s1 = Some()
>>> s2 = Some()
>>> s1 == s2
False
>>> s3 = s1
>>> s1 == s3
True
>>>


你可以定義__eq__()方法,來定義使用==運算時的實質比較結果。例如:
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y
        
    def __eq__(self, that):
        if not isinstance(that, Point):
            return False
        return self.x == that.x and self.y == that.y

p1 = Point(1, 1)
p2 = Point(1, 1)
print(p1 == p2)    # True
print(p1 is p2)    # False

如果你試圖將以上定義的Point實例置入集合物件,則會發生錯誤:
pset = {p1} # TypeError: unhashable type: 'Point'

要將實例置入Python的集合物件,該實例必須定義__hash__()方法。
在許多場合,例如將物件加入一些群集 (Collection)時,會同時利用__eq__()與__hash__()來判斷是否加入的是(實質上)相同的物件。來看看定義__hash__()時必須遵守的約定(取自java.lang.Object的hashCode() 說明 ):
  • 在同一個應用程式執行期間,對同一物件呼叫 __hash__()方法,必須回傳相同的整數結果。
  • 如果兩個物件使用__eq__()測試 結果為相等, 則這兩個物件呼叫__hash__()時,必須獲得相同的整數結果。
  • 如果兩個物件使用 __eq__()測試結果為不相等, 則這兩個物件呼叫__hash__()時,可以獲得不同的整數結果。

以集合物件為例,會先使用__hash__()得出該將物件放至哪個雜湊桶(Hash buckets)中,如果雜 湊桶有物件,再進一步使用__eq__()確定實質相等性,從而確定集合中不會有重複的物件。以下是定義了__hash__()的Point版本:
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y
        
    def __eq__(self, that):
        if not isinstance(that, Point):
            return False
        return self.x == that.x and self.y == that.y
        
    def __hash__(self):
        return 41 * (41 + self.x) + self.y

p1 = Point(1, 1)
p2 = Point(1, 1)
pset = {p1}
print(p2 in pset)    # True

一個重要的觀念是,定義__eq__()與__hash__()時,最好 別使用狀態會改變的資料成員。你可能會想,以這個例子來說,點會移動,如果移動了就不是相同的點了,不是嗎?若x、y是個允許會變動的成 員,那麼就會發生這個情況:
p1 = Point(1, 1)
pset = {p1}
print(p1 in pset)    # True
p1.x = 2
print(p1 in pset)    # False

明 明是記憶體中同一個物件,但置入集合後,最後跟我說不包括p1?這是因為,你改變了x,算出來的__hash__()也就改變了,使用in嘗試比對時,會看看新算出來的雜湊桶中是不是有物件,而根本不是置入p1的雜湊桶中尋找,結果就是False了。
一個限制修改不可變動成員的方式,是定義__setattr__()方法。例如:
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __setattr__(self, name, value):
        if not name in self.__dict__:
            self.__dict__[name] = value
        elif name == 'x' or name == 'y':
            raise TypeError('Point(x, y) is immutable')
    
    def __eq__(self, that):
        if not isinstance(that, Point):
            return False
        return self.x == that.x and self.y == that.y
        
    def __hash__(self):
        return 41 * (41 + self.x) + self.y

p1 = Point(1, 1)
p2 = Point(1, 1)
pset = {p1}
print(p1 in pset)
p1.x = 2   # TypeError: Point(x, y) is immutable

再來看看在實作__eq__()時要遵守的約定(取自java.lang.Object的 equals() 說明 ):
  • 反身性 (Reflexive):x == x的結果要是True。
  • 對稱性 (Symmetric):x == y與y == x的結果必須相同。
  • 傳遞性 (Transitive):x == y、y == z的結果都是True,則x == z的結果也必須是True。
  • 一 致性(Consistent):同一個執行期間,對x == y的多次呼叫,結果必須相同。
  • 對 任何非None的x,x == None必須傳回False。

目前定義的Point,其__eq__()方法滿足以上幾個約定(你可以自行寫程式測試)。現在考慮繼承的情況,你要定義3D的點:
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __eq__(self, that):
        if not isinstance(that, Point):
            return False
        return self.x == that.x and self.y == that.y
        

class Point3D(Point):
    def __init__(self, x, y, z):
        super(Point3D, self).__init__(x, y)
        self.z = z
        
    def __eq__(self, that):        
        if not isinstance(that, Point3D):
            return False
        return super(Point3D, self).__eq__(that) and self.z == that.z
        
        
p1 = Point(1, 1)
p2 = Point3D(1, 1, 1)

print(p1 == p2)

在繼承的情況下,若==兩旁運算元有一個是子類別實例,則會使用子類別的__eq__()版本進行比對。在上面的定義之下,直接將2D與3D的點視作不同的類型,這避免了2D點與3D點(父、子類別)進行比較時,無法符合對稱性、傳遞性合約的問題。

抽象類別

定義類別,本身就是在進行抽象化,如果一個類別定義時不完整,有些狀態或行為必須留待子類別來具體實現,則它是個抽象類別(Abstract Class)。例如,在定義銀行帳戶時,你也許想將 一些帳戶的共同狀態與行為定義在父類別中:

class Account:
    def withdraw(self, amount):
        if amount >= self.balance:
            self.balance -= amount
        else:
            raise ValueError('餘額不足')

    def __str__(self):
        return ('Id:\t\t' + self.id +
               '\nName:\t\t' + self.name +
               '\nBalance:\t' + str(self.balance))

顯然地,這個類別的定義不完整,
self.id、self.name、self.balance沒有定義,嘗試使用這個類別進行操作時,就會發生直譯錯誤:
acct = Account()
print(acct)

你可以繼承這個類別來實作未完整的定義:
class CheckingAccount(Account):
    def __init__(self, id, name):
        self.id = id
        self.name = name
        self.balance = 0
        self.overdraftlimit = 30000

    def withdraw(self, amount):
        if amount <= self.balance + self.overdraftlimit:
            self.balance -= amount
        else:
            raise ValueError('超出信用')

    def __str__(self):
        return (super(CheckingAccount, self).__str__() + 
                '\nOverdraft limit\t' + str(self.overdraftlimit));
                
acct = CheckingAccount('E1223', 'Justin Lin')
print(acct)

現在的問題是,實際上開發人員還是可以用Account()實例化,也許您可以修改一下Account的定義:
class Account:
    def __init__():
        raise NotImplementedError("Account is abstract")
       
    ...略

如此,嘗試使用Account()實例化後,在初始化方法中就會引發錯誤(不過,實際上Account實例確實有產生了,但就這邊的需求來說,目的算已達到)。

像Python這類的動態語言,沒有Java的abstractinterface這種機制來規範一個類別所需實作的介面,遵循物件之間的協定基本上是開發 人員的自我約束(當然,還得有適當的說明文件)。如果你非得有個方式,強制實現某個公開協定,那該怎麼作?像上面一樣,藉由直譯錯誤是一種方式,實際上
視你的需求而定(是否可實例化、子類別是否定義初始化方法等),還有許多模擬的方式,不過在Python中,可以使用Meta class@abstractmethod來達到規範的需求。

舉個例子來說,
您想要設計一個猜數字遊戲,猜數字遊戲的流程大致就是:
顯示訊息(歡迎
隨 機產生數字

遊戲迴圈
   
顯示訊息(提示使用者輸入
    取得使用者輸入
    比較是否猜中   顯示訊息(輸入正確與否

在描述流程輸廓時,並沒有提及如何顯示訊息、沒有提及如何取得使用者輸 入等具體的作法,只是歸納出一些共同的流程步驟
import random
from abc import ABCMeta, abstractmethod

class GuessGame(metaclass=ABCMeta):
    @abstractmethod
    def message(self, msg):
        pass

    @abstractmethod
    def guess(self):
        pass     

    def go(self):
        self.message(self.welcome)
        number = int(random.random() * 10)
        while True:
            guess = self.guess();
            if guess > number:
                self.message(self.bigger)
            elif guess < number:
                self.message(self.smaller)
            else:
                break
        self.message(self.correct)

現在GuessGame是個抽象類別,如果你嘗試實例化GuessGame
game = GuessGame()

則會引發錯誤:
TypeError: Can't instantiate abstract class GuessGame with abstract methods guess, message

如果是個文字模式下的猜數字遊戲,可以將顯示訊息、取得使用者輸入等以文字模式下的具體作法實現出來。例如:
class ConsoleGame(GuessGame):
    def __init__(self):
        self.welcome = "歡迎"
        self.prompt = "輸入數字:"
        self.correct = "猜中了"
        self.bigger = "你猜的比較大"
        self.smaller = "你猜的比較小"
    
    def message(self, msg):
        print(msg)
    
    def guess(self):
        return int(input(self.prompt))

game = ConsoleGame()
game.go()

如果子類別忘了實作某個方法,則該子類別仍被視為一個抽象類別,如果嘗試實例化抽象類別就會引發錯誤。例如若忘了實作message(),就會發生以下錯誤:
TypeError: Can't instantiate abstract class ConsoleGame with abstract methods message

所以,如果你真的想要模擬Java中interface的作用,則可以定義一個抽象類別,完全沒有實作的方法即可。例如:
import random
from abc import ABCMeta, abstractmethod

class Flyer(metaclass=ABCMeta): # 就像是Java中的interface
    @abstractmethod
    def fly(self):
        pass

class Bird:
    pass
    
class Sparrow(Bird, Flyer):  # 就像Java中繼承Bird類別並實作Flyer介面
    def fly(self):
        print('麻雀飛')

s = Sparrow()
s.fly()

繼承

你建立了一個銀行帳戶類別:

class Account:
    def __init__(self, id, name):
        self.id = id
        self.name = name
        self.balance = 0
        
    def deposit(self, amount):
        self.balance += amount
    
    def withdraw(self, amount):
        if amount >= self.balance:
            self.balance -= amount
        else:
            raise ValueError('餘額不足')

    def __str__(self):
        return ('Id:\t\t' + self.id +
               '\nName:\t\t' + self.name +
               '\nBalance:\t' + str(self.balance))

在這個類別中,雖然沒有聲明,但你已經使用了繼承,在Python中,所有類別都繼承自object類別。上例其實相當於:
class Account(object):
    ...略

在Python中繼承的語法,是在類別名稱旁使用括號表明要繼承的父類別。例如,
你為以上的類別建立了一個支票帳戶:
class CheckingAccount(Account):
    def __init__(self, id, name):
        super(CheckingAccount, self).__init__(id, name) # 呼叫父類別__init__()
        self.overdraftlimit = 30000

    def withdraw(self, amount):
        if amount <= self.balance + self.overdraftlimit:
            self.balance -= amount
        else:
            raise ValueError('超出信用')

    def __str__(self):
        return (super(CheckingAccount, self).__str__() + 
                '\nOverdraft limit\t' + str(self.overdraftlimit));

在上例中,你繼承了Account來定義一個CheckingAccount子類別。如果在子類別中,需要呼叫父類別的某個方法,則可以使用super()指定類別名稱與物件,這會將目前實例綁定至所指定父類別方法的第一個引數。

在上例中,你重新定義了withdraw()與__str__()方法,在操作實例方法時,是從子類別開始尋找是否有定義,否則就搜尋父類別中是否有定義方法。所以:
acct = CheckingAccount('E1234', 'Justin Lin')
print(acct, end='\n\n')
acct.deposit(1000)      # 使用 Account 的 deposit() 定義
print(acct, end='\n\n')
acct.withdraw(2000)     # 使用 CheckingAccount 的 withdraw() 定義
print(acct, end='\n\n')

在呼叫acct的deposit()方法時,由於CheckingAccount並沒有定義,所以呼叫的是Account的deposit(),而呼叫withdraw()時,則是使用CheckingAccount上有定義的withdraw()。

在Python中,可以進行多重繼承,這個時候要注意搜尋的順序,是從子類別開始,接著是同一階層父類別由左至右搜尋,再至更上層同一階層父類別由左至右搜尋,直到達到頂層為止。例如:
class A(object):
    def method1(self):
        print('A.method1')
        
    def method2(self):
        print('A.method2')
        
class B(A):
    def method3(self):
        print('B.method3')
        
class C(A):
    def method2(self):
        print('C.method2')
        
    def method3(self):
        print('C.method3')
        
class D(B, C):
    def method4(self):
        print('C.method4')

d = D()
d.method4() # 在 D 找到,C.method4
d.method3() # 以 D->B 順序找到,B.method3
d.method2() # 以 D->B->C 順序找到,C.method2
d.method1() # 以 D->B->C->A 順序找到,A.method1

在Python中,類別有個__bases__特性,記錄著所繼承的父類別,__bases__是個
Tuple,有趣的是,你可以改變__bases__動態改變繼承的父類別。例如:
>>> class A:
...     def method(self):
...         print('A method')
...
>>> class B:
...     def method(self):
...         print('B method')
...
>>> class C(A):
...     pass
...
>>> c = C()
>>> c.method()
A method
>>> C.__bases__
(<class '__main__.A'>,)
>>> C.__bases__ = (B,)
>>> c.method()
B method
>>>

在上例中,C原本來繼承自A類別,透過修改__bases__實際參考的Tuple,C改變繼承B,而尋找特性或方法時,也就改尋找B父類別,因此最後執行的是從B繼承下來的method()。