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()。

建構、初始與消滅

在之前的文件中所提到的,__init__()方法是定義物件建立後初始化的流程,也就是執行到__init__()方法時,物件實際上已建構完成,傳入__init__()的引數,並不是作為建構物件之用,而是作為初始物件之用。

實際上要決定如何建構物件,必須定義__new__()方法,這個方法的第一個參數總是傳入類別本身,之後可接任意參數作為建構物件之用。

__new__() 方法可以傳回物件,如果傳回的物件是第一個參數的類別實例,則會執行__init__()方法(如果有定義的話),而__init__()方法的第一個參 數綁定所傳回的物件。如果沒有傳回第一個參數的類別實例(傳回別的實例或None),則不會執行__init__()方法(即使有定義)。

一個簡單測試建構與初始流程的例子如下所示:


>>> class Some:
...     def __new__(clz, isClzInstance):
...         print('__new__')
...         if isClzInstance:
...             return object.__new__(clz)
...         else:
...             return None
...     def __init__(self, isClzInstance):
...         print('__init__')
...         print(isClzInstance)
...
>>> Some(True)
__new__
__init__
True
>>> Some(False)
__new__
>>>


由於若傳回__new__()第一個參數的類別實例,就會執行__init__()方法,所以__new__()與__init__()通常會具有相同個數的參數。藉由定義__new__()方法,可以決定如何建構物件與初始物件,一個應用的例子,就是實作 Singleton 模式。例如:
class Singleton:
    __single = None
    def __new__(clz):
        if not Singleton.__single:
            Singleton.__single = object.__new__(clz)
        return Singleton.__single
        
    def doSomething(self):
        print("do something...XD")

singleton1 = Singleton()
singleton1.doSomething()  # do something...XD

singleton2 = Singleton()
singleton2.doSomething()  # do something...XD

print(singleton1 is singleton2)  # True

在上面的程式中,一旦Singleton.__single有參考至物件,則直接傳回,從客戶端的角度則如常執行Singleton()來建構物件,但實際上傳回的都會是同一個物件(由於傳回的是Singleton實例,所以若有定義__init__()則會被執行,就單例物件來說,可於__init__()中進行狀態的回復)。

如果要定義物件被垃圾收集(Garbage collection)時,所要進行的資源清除動作,則可以定義__del__()方法,物件會被資源回收的資格,基本上就是參考至物件的變數計數為0的時候。例如:

>>> class Some:
...     def __del__(self):
...         print('__del__')
...
>>> s = Some()
>>> s = None
__del__
>>> s1 = Some()
>>> s2 = s1
>>> del s1
>>> del s2
__del__
>>>

在上例中,將s參考至None時,原先所參考的物件其變數計數就為0,所以在回收物件前執行了__del__(),而使用del刪除s1時,s2仍參考至物件,直到s2也被del刪除,物件符合回收資格,回收物件前執行了__del__()。

特殊方法名稱

在Python中定義類別時,有些__name__的特殊函式名稱,是用定義運算子或特定操作的行為。舉個例子來說,可以定義一個有理數類別,並定義其+、-、*、/等行為:

class Rational:
    def __init__(self, n, d):  # 物件建立之後所要建立的初始化動作
        self.numer = n
        self.denom = d
    
    def __str__(self):   # 定義物件的字串描述
        return str(self.numer) + '/' + str(self.denom)
    
    def __add__(self, that):  # 定義 + 運算
        return Rational(self.numer * that.denom + that.numer * self.denom, 
                        self.denom * that.denom)
    
    def __sub__(self, that):  # 定義 - 運算
        return Rational(self.numer * that.denom - that.numer * self.denom,
                        self.denom * that.denom)
                           
    def __mul__(self, that):  # 定義 * 運算
        return Rational(self.numer * that.numer, 
                        self.denom * that.denom)
        
    def __truediv__(self, that):   # 定義 / 運算
        return Rational(self.numer * that.denom,
                        self.denom * that.denom)

    def __eq__(self, that):   # 定義 == 運算
        return self.numer * that.denom == that.numer * self.denom

x = Rational(1, 2)
y = Rational(2, 3)
z = Rational(2, 3)
print(x)       # 1/2
print(y)       # 2/3
print(x + y)   # 7/6
print(x - y)   # -1/6
print(x * y)   # 2/6
print(x / y)   # 3/6
print(x == y)  # False
print(y == z)  # True

__init__()定義物件建立後要執行的初始化過程,相對它的是__del__()方法,在物件被回收前會被執行(因為回收物件的時間不一定,所以不建議用在要求立即性的情況)。常見的+、-、*、/、==等操作,則分別是由__add__()__sub__()__mul__()__truediv__()(//則是由__floordiv__()定義)與__eq__()定義。

__str__()用來定義傳回物件描述字串,通常用來描述的字串是對使用者友善的說明文字,如果對物件
使用str(),所呼叫的就是__str__()。如果要定義對開發人員較有意義的描述,例如傳回產生實例的類別名稱之類的,則可以定義__repr__(),如果對物件使用repr(),則所呼叫的就是__repr__()。

特性名稱空間 中看過的例子則是__getattr__()方法,相對它的方法是__setattr__(),而用來定義實例的特性被設定時該作什麼動作。例如:
>>> class Some:
...     def __setattr__(self, name, value):
...         print(name, value)
...     def __init__(self):
...         self.x = 10
...
>>> s = Some()
x 10
>>> s.w = 100
w 100
>>> s.__dict__['z'] = 200
>>> print(s.__dict__)
{'z': 200}
>>>


一旦有定義__setattr__(),則所有以 . 運算子來設定特性的操作,都會呼叫__setattr__(),但直接對實例的__dict__操作則不會。

__getitem__()__setitem__()則用來設定[]運算子的行為。例如:

>>> class Some:
...     def __init__(self):
...         self.inner = {}
...     def __setitem__(self, name, value):
...         self.inner[name] = value
...     def __getitem__(self, name):
...         return self.inner[name]
...
>>> s = Some()
>>> s[0] = 100
>>> s['Justin'] = 'Message'
>>> s[0]
100
>>> s['Justin']
'Message'
>>>


在設計程式的過程中,經常有的需求之一,就是希望逐一取得某物件內部的所有資料(或物件),像是取得串列中所有的資料,或取 得集合中所有的資料。

因為串列是有序結構並有索引特性,而集合則為無序不重複的特性,兩者所提供的公開存取方法也不相同,
如 何以一致方式取得不同資料結構的群集物件是個問題。

在Python中,你可以讓物件實作__iter__()方法,這個方法可以傳回一個迭代器(Iterator),一個具有__next__()方法的物件。迭代器走訪物件內容收集物件後傳回,每次呼叫迭代器物件的__next__()方法,必須傳回群集的下一個元素,如果沒有下一個元素了,則丟出StopIteration物件。例如:
class Some:
    class Iterator:
        def __init__(self, length):
            self.length = length
            self.number = -1
        def __next__(self):
            self.number = self.number + 1
            if self.number == self.length:
                raise StopIteration
            return self.number
    
    def __init__(self, length):
        self.length = length

    def __iter__(self):
        return Some.Iterator(self.length)

s = Some(3)
it = iter(s)
print(next(it))   # 0
print(next(it))   # 1
print(next(it))   # 2
print(next(it))   # StopIteration

實際上,你可以使用iter()來代為呼叫物件的__iter__()方法,使用next()方法代為呼叫物件的__next__()方法。事實上,你可以結合for in迴圈來提取物件,for in迴圈會透過__iter__()取得迭代器,然後在每次迴圈中呼叫__next__()方法,而後遇到StopIteration丟出後離開迴圈。例如:
for n in Some(10):
    print(n)       # 顯示 0 到 9

以上先簡介一些簡單的特殊方法名稱,詳細的特殊方法說明,可以參考 Special method names

特性名稱空間

一個事實是:類別(Class)或實例(Instance)本身的作用是作為特性(Property)的名稱空間(Namespace)。類別或實例本身會擁有一個__dict__特性參考至一個字典物件,其中記錄著類別或實例所擁有的特性。例如:

>>> class Math:
...     PI = 3.14159
...
>>> Math.PI
3.14159
>>> print(Math.__dict__)
{'__dict__': <attribute '__dict__' of 'Math' objects>, '__module__': '__main__',
 'PI': 3.14159, '__weakref__': <attribute '__weakref__' of 'Math' objects>, '__d
oc__': None}
>>> Math.__dict__['PI']
3.14159
>>>


在上例中,Math類別上定義了PI特性,這記錄在Math.__dict__中,你嘗試使用Math.PI,則使用Math.__dict__['PI']來尋找出對應的值。如果你試著透過實例來取得PI:
>>> m = Math()
>>> m.PI
3.14159
>>> print(m.__dict__)
{}
>>>


實際上m所參考的實例,其__dict__中並沒有PI,此時會到Math.__dict__中找看看有無PI。這是Python中尋找特性的順序:如果實例的__dict__中沒有,則到產生實例的類別__dict__中尋找。如果你試著在m實例上設定PI特性:
>>> m.PI
3.14
>>> Math.PI
3.14159
>>> print(m.__dict__)
{'PI': 3.14}
>>> print(Math.__dict__)
{'__dict__': <attribute '__dict__' of 'Math' objects>, '__module__': '__main__',
 'PI': 3.14159, '__weakref__': <attribute '__weakref__' of 'Math' objects>, '__d
oc__': None}
>>>


實際上你並沒有改變Math.__dict__中的PI,而是在實例m.__dict__中新增一個PI,而你嘗試使用實例存取PI時,由於m.__dict__中已經有了,就直接取得該值。

這也說明了,為什麼實例方法的第一個參數會綁定至實例:
>>> class Some:
...     def setx(self, x):
...         self.x = x
...
>>> s = Some()
>>> print(Some.__dict__)
{'__dict__': <attribute '__dict__' of 'Some' objects>, '__weakref__': <attribute
 '__weakref__' of 'Some' objects>, '__module__': '__main__', 'setx': <function s
etx at 0x018FA078>, '__doc__': None}
>>> print(s.__dict__)
{}
>>> s.setx(10)
>>> print(s.__dict__)
{'x': 10}
>>>


類別中所定義的函式,其實就是類別的特性,也就是在類別的__dict__中可以找到該名稱。實例方法的第一個參數self綁定實例,透過self.x來設定特性值,也就是在self.__dict__中添增特性。

由於Python可以動態地為類別添加屬性,即使是未添加屬性前就已建立的物件,在類別動態添加屬性之後, 也可依Python的名稱空間搜尋順序套用上新的屬性,用這種方式,您可以為類別動態地添加方法。例如:
class Some:
    def __init__(self, x):
        self.x = x
        
s = Some(1)
Some.service = lambda self, y: print('do service...', self.x + y)
s.service(2)    # do service... 3

如果你要刪除物件上的某個特性,則可以使用del。例如:
>>> class Some:
...     pass
...
>>> s = Some()
>>> s.x = 10
>>> print(s.__dict__)
{'x': 10}
>>> del s.x
>>> print(s.__dict__)
{}
>>>

如果你試著在實例上呼叫某個方法,而該實例上沒有該綁定方法時(被@staticmethod或@classmethod修飾的函式),則會試著去類別__dict__中尋找,並以類別呼叫方式來執行函式。例如:
>>> class Some:
...     @staticmethod
...     def service():
...         print('XD')
...
>>> s = Some()
>>> s.service()
XD
>>>


在上例中,嘗試執行s.service(),由於s並沒有service()的綁定方法(因為被@staticmethod修飾),所以嘗試尋找Some.service()執行。

實際上,如果嘗試透過實例取得某個特性,如果實例的__dict__中沒有,則到產 生實例的類別__dict__中尋找,如果類別__dict__仍沒有,則會試著呼叫__getattr__()來傳回,如果沒有定義 __getattr__()方法,則會引發AttributeError,如果有__getattr__(),則看__getattr__()如何處理。例如:

>>> class Some:
...     w = 10
...     def __getattr__(self, name):
...         if name == 'w':
...             return 20
...
>>> s = Some()
>>> s.w
10
>>> s.x
>>> class Some:
...     def __getattr__(self, name):
...         if name == 'w':
...             return 20
...         else:
...             raise AttributeError(name)
...
>>> s = Some()
>>> s.w
20
>>> s.x
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 6, in __getattr__
AttributeError: x
>>>


在類別中的函式執行過程中若有定義實例特性時,具特性名稱是以__開頭,則該名稱會被加工處理。例如:
>>> class Some:
...     def __init__(self):
...         self.__x = 10
...
>>> s = Some()
>>> s.__x

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Some' object has no attribute '__x'
>>> print(s.__dict__)
{'_Some__x': 10}
>>> s._Some__x
10
>>>


實例變數若以__name這樣的名稱,則會自動轉換為「_類別名__name」這樣的名稱儲存在實例的__dict__中,以__開頭的變數名稱,Python沒有真正阻止你存取它,但這提示不希望你直接存取。

如果不想要直接使用實例的__dict__來取得特性字典物件,則可以使用vars(),vars()會代為呼叫實例的__dict__。例如:

>>> class Some:
...     def __init__(self):
...         self.x = 10
...         self.y = 20
...
>>> s = Some()
>>> vars(s)
{'y': 20, 'x': 10}
>>>


靜態方法、類別方法

如果你定義了一個類別:

class Some:
    def __init__(self, x):
        self.x = x
    
    def service(self, y):
        print('do service...', self.x + y)

你可以透過Some實例來操作service()方法。例如:
s = Some(10)
s.service(2)  # do service... 12

表面上,上例像是:
s = Some(10)
Some.service(s, 2)

s所參考的實例,會綁定至service()的第一個參數,而所給定的引數,會指定service()方法的第二個參數,實際上在這類的情況下,service()方法是一個
綁定方法(Bound method),而每個實例會有自己的綁定方法。例如:
s1 = Some(10)
service = s1.service
service(5)    # do service... 15

s2 = Some(20)
service = s2.service
service(5)    # do service... 25

而Some類別本身亦擁有一個service()函式,例如:
s1 = Some(10)
Some.service(s1, 5)  # do service... 15

s2 = Some(20)
Some.service(s2, 5)  # do service... 25

service = Some.service
service(s1, 5)       # do service... 15
service(s2, 5)       # do service... 25

如果在定義類別時,類別中的函式沒有任何參數,則該函式無法成為綁定方法,因為試圖將實例作為第一個參數時會發生錯誤。例如:
class Other:
    def service():
        print('do service...')
        
o = Other()
o.service()  # TypeError: service() takes no arguments (1 given)

像上例中service()只能作為Other上的一個函式來使用:
Other.service()     # do service...

如果你在定義類別時希望某個函式,完全不要作為實例的綁定方法,也就是不要將第一個參數綁定為所建立的實例,則可以使用@staticmethod加以修飾。例如:
class Some:
    @staticmethod
    def service(x, y):
        print('do service...', x + y)

Some.service(10, 20) # do service... 25

s = Some()
s.service(10, 20)    # do service... 25
s.service(10)        # TypeError: service() takes exactly 2 positional arguments (1 given)

雖然你可以透過實例來呼叫@staticmethod所修飾的靜態方法,但建議透過類別名稱來呼叫。類似的,建議透過實例來呼叫實例的綁定方法。一個沒有使用@staticmethod宣告而又帶有參數的函式,就如先前所看到的,可以用實例方法來使用,也可以用靜態方法的方式呼叫,這是Python3給的方便性,讓你不一定得使用@staticmethod來區別,只要你知道自己在作什麼。事實上,你還可以這麼作:
class Some:
    def __init__(self, x):
        self.x = x
    
    def service(self, y):
        print('do service...', self.x + y)

class Other:
    pass

o = Other()
o.x = 100
Some.service(o, 200)    # do service... 25
      

Some.service()的第一個參數可以任何物件,只要它有個x屬性。

你還可以使用@classmethod來修飾一個函式成為類別方法,這樣的方法第一個參數永遠綁定為類別物件本身,無論是以實例方法來呼叫,或是以靜態方法來呼叫。例如:
class Some:
    def __init__(self, x):
        self.x = x
        
    @classmethod
    def service(clz, y):
        print('do service...', clz, y)

s = Some(10)
s.service(20)      # do service... <class '__main__.Some'> 20
Some.service(30)   # do service... <class '__main__.Some'> 30

定義類別

在Python中要定義類別非常的簡單,例如你可以定義一個帳戶(Account)類別:

class Account:
    pass

def deposit(acct, amount):
    if amount <= 0:
        raise ValueError('must be positive')
    acct.balance += amount
       
def withdraw(acct, amount):
    if amount <= acct.balance:
        acct.balance -= amount
    else:
        raise RuntimeError('balance not enough')
        
acct = Account()
acct.number = '123-456-789'
acct.name = 'Justin'
acct.balance = 0

print(acct.number)    # 123-456-789
print(acct.name)      # Justin

deposit(acct, 100)
print(acct.balance)   # 100
withdraw(acct, 50)
print(acct.balance)   # 50

上例中,class用來定義一個類別,現在還沒有要定義類別特性(Property)等,所以使用pass先略過。要建立類別的實例(Instance),直接呼叫Account()來建立。建立物件之後,你可以直接在物件上添增特性,透過 . 來存取特性。

上面這個例子的帳號、名稱、餘額是在程式碼中自行建立,並透過函式來作存提款的動作。事實上,你可以將物件建立後的初始化動作,以及會用到的相關操作定義在類別之中。來看看下面這個例子:
class Account:
    def __init__(self, number, name):
        self.number = number
        self.name = name
        self.balance = 0
        
    def deposit(self, amount):
        if amount <= 0:
            raise ValueError('must be positive')
        self.balance += amount
        
    def withdraw(self, amount):
        if amount <= self.balance:
            self.balance -= amount
        else:
            raise RuntimeError('balance not enough')

這個類別已包含幾個Python中定義類別的觀念。首先是:
def __init__(self, number, name):
    self.number = number
    self.name = name
    self.balance = 0

__init__這個特定的名稱,用來定義類別的實例建立之後,要進行的初始化動作。第一個self參數代表建立的類別實例,在Python中,實例可操作的方法,第一個參數必須明確作為接受實例之用,慣例上取名為self名稱。__init__之後則可指定初始化時所必須給定的資料。在上面的例子中,會在建立的實例上增加number、name與balance特性。
你可以用以下方式來建構物件:
acct = Account('123-456-789', 'Justin')

每個建構出來的Account實例,都會擁有自己的資料狀態,可以直接透過物件及 . 來存取特性:
print(acct.number)    # 123-456-789
print(acct.name)      # Justin

再來看到方法(Method)定義:
def deposit(self, amount):
    if amount <= 0:
        raise ValueError('must be positive')
    self.balance += amount
       
def withdraw(self, amount):
    if amount <= self.balance:
        self.balance -= amount
    else:
        raise RuntimeError('balance not enough')

同樣的道理,
在Python中,實例可操作的方法,第一個參數必須明確作為接受實例之用,之後若要在方法中存取物件特性或呼叫物件方法,則必須明確以傳入的物件搭配.運算,例如self.balance這樣的方式。以下則是呼叫方法的幾個例子:
acct.deposit(100)
print(acct.balance)   # 100
acct.withdraw(50)
print(acct.balance)   # 50

在Python中,函式是物件,對於定義在類別中的方法也是一種函式,自然你也可以將之指定給別的變數。例如:
acct1 = Account('123-456-789', 'Justin')
deposit1 = acct1.deposit
withdraw1 = acct1.withdraw
deposit1(100)
withdraw1(50)
print(acct1.balance)

acct2 = Account('987-654-321', 'Momor')
deposit2 = acct2.deposit
withdraw2 = acct2.withdraw
deposit2(200)
withdraw2(100)
print(acct2.balance)

acct1.deposit = acct2.deposit
acct1.deposit(1000)      # 你其實是將錢存到 acct2 去
print(acct1.balance)     # 50
print(acct2.balance)     # 1100

上面這個例子其實也指出了,在Python中,方法總是跟隨著物件而建立,而self是綁定為所建立的物件,別以為將方法指定給另一個物件可以欺騙Python,就如上面的例子中,倒數第三行的結果,其實是將錢存到acct2所參考的物件。

yield 產生器

你可以在函式中包括yield來「產生」值,表面上看來,yield就像是return會傳回值,但又不中斷函式的執行:

>>> def myrange(n):
...     x = 0
...     while True:
...         yield x
...         x += 1
...         if x == n:
...             break
...
>>> for i in myrange(10):
...     print(i, end='')
... print()
...
0123456789
>>>


上面的程式模擬了內建函式range()的作用。表面上看來,你在myrange()函式中使用yield傳回值,然後執行for in迴圈,接著再使用myrange()傳回下一個值,再執行for in迴圈,就好似myrange()執行過後沒有結束似的。

實際上,在def所定義的本體中,若包括yield運算式,則Python會將之編譯為一個產生器(Generator)。例如:
>>> myrange(10)
<generator object myrange at 0x01C98440>
>>> dir(myrange(10))
['__class__', '__delattr__', '__doc__', '__eq__', '__format__', '__ge__', '__get
attribute__', '__gt__', '__hash__', '__init__', '__iter__', '__le__', '__lt__',
'__name__', '__ne__', '__new__', '__next__', '__reduce__', '__reduce_ex__', '__r
epr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'close', 'gi
_code', 'gi_frame', 'gi_running', 'send', 'throw']
>>>

產生器物件是個具有迭代器(Iterator)介面的物件,也就是說,它具有__next__()方法,可以使用next()函式來取出下一個值,若無法產生下一個值,則會丟出StopIteration物件。例如:
>>> g = myrange(3)
>>> next(g)
0
>>> next(g)
1
>>> next(g)
2
>>> next(g)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
>>>


這也就是為何在第一個例子中,在for in迴圈呼叫myrange()會有那樣的結果。一個函式若包括yield,則會傳回產生器物件,而該函式基本上可以包括return,不過不可以指明傳回值(也就是只能傳回None)。return只是用來結束函式的執行流程。例如:
>>> def myrange(n):
...     x = 0
...     while True:
...         yield x
...         return
...
>>> g = myrange(3)
>>> next(g)
0
>>> next(g)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
>>>

在上例中,第一個next(g)後,函式的執行流程就因return而結束了,嘗試再執行next(g),就函式定義來看,無法再執行到yield運算式,所以就函式定義來看,StopIteration是因為無法執行到yield運算式而丟出的。

先前談過
for 包含式(Comprehension),實際上,for包含式與迭代器都是一個叫產生器運算式的語言特性。在 for 包含式(Comprehension) 中最後一個例子也有提到,使用()與for包含式時,實際上是建立一個產生器。例如:
>>> (i ** 2 for i in range(3))
<generator object <genexpr> at 0x01C98440>
>>> g = (i ** 2 for i in range(3))
>>> next(g)
0
>>> next(g)
1
>>> next(g)
4
>>> next(g)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
>>> for p in (i ** 2 for i in range(3)):
...     print(p)
...
0
1
4
>>>


從Python 2.5開始,yield從陳述改為運算式,也就是yield除了「產生」指定的值之外,會有一個運算結果,yield運算結果預設是None,你可以透過產生器的send()方法傳入一個值,這個值就成為yield的運算結果。這給了你一個與產生器溝通的機會。例如:
>>> def myrange(n):
...     x = 0
...     while True:
...         val = (yield x)
...         if val is not None:
...             x = val
...         else:
...             x += 1
...         if x >= n:
...             break
...
>>> g = myrange(10)
>>> next(g)
0
>>> next(g)
1
>>> next(g)
2
>>> g.send(0)
0
>>> next(g)
1
>>> next(g)
2
>>> g.send(5)
5
>>> next(g)
6
>>> next(g)
7
>>>



變數範圍

在Python中,變數無需宣告就可以直接使用並指定值,除非特別使用globalnonlocal指明,否則變數範圍(Scope)總是在指定值時建立。例如:

>>> x = 10
>>> y = 10
>>> def some():
...     x = 20
...     print(x)
...     print(y)
...
>>> some()
20
10
>>> print(x)
10
>>> print(y)
10
>>>


在 上例中,some()函式中的x,其實是在函式範圍中所建立的新x,其覆蓋了外部的x範圍,所以你所設定的是區域變數x,指定值給變數時,就是在該範圍中 建立該變數,就是指這樣的情況。如果取用變數時,也是從當時的範圍開始尋找,若沒有,才找尋外部範圍,例如上例中,取用x時,在區域範圍找到區域變數x, 所以顯示的是區域變數x的值,而不是外部變數x,取用y時,由於區域範圍找不到,所以取得外部y的值。

記得變數範圍的建立是在指定時發生,而不是在取用時發生,所以像下面這個例子:
>>> x = 20
>>> def some(x = x):
...     print(x)
...
>>> some()
20
>>> some(30)
30
>>> print(x)
20
>>>


是定義了一個some()函式,將當時外部變數x的值20設定給參數x作為預設值。

實際上,在Python中變數可以在四個範圍中建立或尋找。內建(Builtin)、全域(Global)、外包函式(Endosing function)、函式(Local functon)。一個例子如下:
  • demo.py
x = 10         # 全域

def outer():
   y = 20        # 在 outer() 函式範圍
   
   def inner():
       z = 30    # 在 inner() 函式範圍
       print(x)  # 內建範圍 print,全域的 x
     print(y)  # 內建範圍 print,外包 outer() 函式的 y
   
   prnt(x)       # 內建範圍 print,全域的 x

取用名稱時(而不是指定),一定是從最內層往外尋找。Python中所謂全域,實際上是以模組檔案為界,以上例來說,x實際上是demo模組範圍中的變數,不會橫跨所有模組範圍。

注意,到print名稱,實際上它是內建範圍,在Python 3中有個builtins模組,該模組中的變數,會自動被所有的模組所擁有。例如:

>>> import builtins
>>> dir(builtins)
['ArithmeticError', 'AssertionError', 'AttributeError', 'BaseException', 'Buffer
Error', 'BytesWarning', 'DeprecationWarning', 'EOFError', 'Ellipsis', 'Environme
ntError', 'Exception', 'False', 'FloatingPointError', 'FutureWarning', 'Generato
rExit', 'IOError', 'ImportError', 'ImportWarning', 'IndentationError', 'IndexErr
or', 'KeyError', 'KeyboardInterrupt', 'LookupError', 'MemoryError', 'NameError',
 'None', 'NotImplemented', 'NotImplementedError', 'OSError', 'OverflowError', 'P
endingDeprecationWarning', 'ReferenceError', 'RuntimeError', 'RuntimeWarning', '
StopIteration', 'SyntaxError', 'SyntaxWarning', 'SystemError', 'SystemExit', 'Ta
bError', 'True', 'TypeError', 'UnboundLocalError', 'UnicodeDecodeError', 'Unicod
eEncodeError', 'UnicodeError', 'UnicodeTranslateError', 'UnicodeWarning', 'UserW
arning', 'ValueError', 'Warning', 'WindowsError', 'ZeroDivisionError', '__build_
class__', '__debug__', '__doc__', '__import__', '__name__', '__package__', 'abs'
, 'all', 'any', 'ascii', 'bin', 'bool', 'bytearray', 'bytes', 'chr', 'classmetho
d', 'compile', 'complex', 'copyright', 'credits', 'delattr', 'dict', 'dir', 'div
mod', 'enumerate', 'eval', 'exec', 'exit', 'filter', 'float', 'format', 'frozens
et', 'getattr', 'globals', 'hasattr', 'hash', 'help', 'hex', 'id', 'input', 'int
', 'isinstance', 'issubclass', 'iter', 'len', 'license', 'list', 'locals', 'map'
, 'max', 'memoryview', 'min', 'next', 'object', 'oct', 'open', 'ord', 'pow', 'pr
int', 'property', 'quit', 'range', 'repr', 'reversed', 'round', 'set', 'setattr'
, 'slice', 'sorted', 'staticmethod', 'str', 'sum', 'super', 'tuple', 'type', 'va
rs', 'zip']
>>>


所以許多Python中可以直接使用的函式,其名稱實際上是在builtins模組之中。基本上,你也可以將變數建立至builtins中。例如:
import builtins
import sys
builtins.argv = sys.argv
print(argv[1])

變數指定值的同時就確立其所在的範圍。如果你希望指定值的變數是全域範圍的話,則可以使用global指明(雖然並不鼓勵全域變數)。例如:

>>> x = 10
>>> def some():
...     global x
...     x = 20
...
>>> some()
>>> print(x)
20
>>>


來看看以下這個會發生什麼事情?
>>> x = 10
>>> def some():
...     print(x)
...     x = 20
...
>>> some()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in some
UnboundLocalError: local variable 'x' referenced before assignment
>>>


在some()函式中,print(x)中的x其實是some()函式中的區域變數x,因為範圍建立總是在指定時發生,在some()函式的第二行中,有個x指定,所以Python直譯器看到的是這個x,而你在指定x值之前,就要顯示其值,就會發生錯誤。上面這個錯誤可以這麼修改:
>>> x = 10
>>> def some():
...     global x
...     print(x)
...     x = 20
...
>>> some()
10
>>>


在Python 3中新增了nonlocal,可以讓你指明變數並非區域變數,請依照函式(Local functon)、外包函式(Endosing function)、全域(Global)、內建(Builtin)的順序來尋找,即使是指定運算。例如:
x = 10
def outer():
   x = 100         # 這是在 outer() 函式範圍的 x
   def inner():
       nonlocal x
       x = 1000    # 改變的是 outer() 函式的 x
   inner()
   print(x)        # 顯示 1000

outer()
print(x)           # 顯示 10

因此,在
lambda 運算式 看過的一個例子:
>>> def func():
...     x = 10
...     def getX():
...         return x
...     def setX(n):
...         x = n
...     return (getX, setX)
...
>>> getX, setX = func()
>>> getX()
10
>>> setX(20)
>>> getX()
10
>>>


在 上例中,func()中的setX()宣告的x,其實是setX()中的區域變數x,其覆蓋了外部func()的x,所以你的n是指定給區域變數x。如果你要改變func()中的x值,則可以在setX()函式中宣告nonlocal。例如:
>>> def func():
...     x = 10
...     def getX():
...         return x
...     def setX(n):
...         nonlocal x
...         x = n
...     return (getX, setX)
...
>>> getX, setX = func()
>>> getX()
10
>>> setX(20)
>>> getX()
20
>>>