Python中一切都是物件。類提供了建立新型別物件的機制。這篇教程中,我們不談類和麵向物件的基本知識,而專注在更好地理解Python物件導向程式設計上。假設我們使用新風格的python類,它們繼承自object父類。
定義類
class 語句可以定義一系列的屬性、變數、方法,他們被該類的例項物件所共享
。下面給出一個簡單類定義:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
class Account(object): num_accounts = 0 def __init__(self, name, balance): self.name = name self.balance = balance Account.num_accounts += 1 def del_account(self): Account.num_accounts -= 1 def deposit(self, amt): self.balance = self.balance + amt def withdraw(self, amt): self.balance = self.balance - amt def inquiry(self): return self.balance |
類定義引入了以下新物件:
- 類物件
- 例項物件
- 方法物件
類物件
程式執行過程中遇到類定義時,就會建立新的名稱空間,名稱空間包含所有類變數和方法定義的名稱繫結。注意該名稱空間並沒有建立類方法可以使用的新區域性作用域,因此在方法中訪問變數需要全限定名稱。上一節的Account
類演示了該特性;嘗試訪問num_of_accounts
變數的方法需要使用全限定名稱Account.num_of_accounts
,否則,如果沒有在__init__
方法中使用全限定名稱,會引發如下錯誤:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
class Account(object): num_accounts = 0 def __init__(self, name, balance): self.name = name self.balance = balance num_accounts += 1 def del_account(self): Account.num_accounts -= 1 def deposit(self, amt): self.balance = self.balance + amt def withdraw(self, amt): self.balance = self.balance - amt def inquiry(self): return self.balance >>> acct = Account('obi', 10) Traceback (most recent call last): File "python", line 1, in <module> File "python", line 9, in __init__ UnboundLocalError: local variable 'num_accounts' referenced before assignment |
類定義執行的最後,會建立一個類物件。在進入類定義之前有效的那個作用域現在被恢復了,同時類物件被繫結到類定義頭的類名上。
先偏離下話題,你可能會問如果建立的類是物件,那麼類物件的類是什麼呢?。與一切都是物件的python哲學一致,類物件確實有個類,即python新風格類中的type
類。
1 2 |
>>> type(Account) <class 'type'> |
讓你更迷惑一點,Account型別的型別是type。type類是個元類,用於建立其他類,我們稍後教程中再介紹。
類物件支援屬性引用和例項化。屬性通過標準的點語法引用,即物件後跟句點,然後是屬性名:obj.name。有效的屬性名是類物件建立後類名稱空間中出現的所有變數和方法名。例如:
1 2 3 4 |
>>> Account.num_accounts >>> 0 >>> Account.deposit >>> <unbound method Account.deposit> |
類例項化使用函式表示法。例項化會像普通函式一樣無引數呼叫類物件,如下文中的Account類:
1 |
>>> Account() |
類物件例項化之後,會返回例項物件,如果類中定義了__init__
方法,就會呼叫,例項物件作為第一個引數傳遞過去。這個方法會進行使用者自定義的初始化過程,比如例項變數的初始化。Account
類為例,賬戶name和balance會被設定,例項物件的數目增加1。
例項物件
如果類物件是餅乾切割刀,餅乾就是例項化類物件的結果。例項物件上的全部有效操作為對屬性、資料和方法物件的引用。
方法物件
方法物件和函式物件類似。如果x
是Account
類的例項,x.deposit
就是方法物件的例子。方法定義中有個附加引數,self
。self
指向類例項。為什麼我們需要把例項作為引數傳遞給方法?方法呼叫能最好地說明:
1 2 3 |
>>> x = Account() >>> x.inquiry() 10 |
例項方法呼叫時發生了什麼?你應該注意到x.inquiry()
呼叫時沒有引數,雖然方法定義包含self
引數。那麼這個引數到底發生了什麼?
特殊之處在於方法所作用的物件被作為函式的第一個引數傳遞過去。在我們的例子中,對x.inquiry()
的呼叫等價於Account.f(x)
。一般,呼叫n引數的方法等同於將方法的作用物件插入到第一個引數位置。
python教程上講:
當引用的例項屬性不是資料屬性時,就會搜尋類。如果名稱表示一個合法的函式物件,例項物件和函式物件將會被打包到一個抽象物件,即方法物件中。包含引數列表的方法物件被呼叫時,將會根據例項物件和引數列表建立一個新的引數列表,然後函式物件將會使用新的引數列表被呼叫。
這適用於所有的例項方法物件,包括__init__
方法。self引數其實不是一個關鍵字,任何有效的引數名都可以使用,如下Account類定義所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
class Account(object): num_accounts = 0 def __init__(obj, name, balance): obj.name = name obj.balance = balance Account.num_accounts += 1 def del_account(obj): Account.num_accounts -= 1 def deposit(obj, amt): obj.balance = obj.balance + amt def withdraw(obj, amt): obj.balance = obj.balance - amt def inquiry(obj): return obj.balance >>> Account.num_accounts >>> 0 >>> x = Account('obi', 0) >>> x.deposit(10) >>> Account.inquiry(x) >>> 10 |
靜態和類方法
類中定義的方法預設由例項呼叫。但是,我們也可以通過對應的@staticmethod
和@classmethod
裝飾器來定義靜態或類方法。
靜態方法
靜態方式是類名稱空間中的普通函式。引用類的靜態方法返回的是函式型別,而不是非繫結方法型別:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
class Account(object): num_accounts = 0 def __init__(self, name, balance): self.name = name self.balance = balance Account.num_accounts += 1 def del_account(self): Account.num_accounts -= 1 def deposit(self, amt): self.balance = self.balance + amt def withdraw(self, amt): self.balance = self.balance - amt def inquiry(self): return "Name={}, balance={}".format(self.name, self.balance) @staticmethod def type(): return "Current Account" >>> Account.deposit <unbound method Account.deposit> >>> Account.type <function type at 0x106893668> |
使用@staticmethod
裝飾器來定義靜態方法,這些方法不需要self
引數。靜態方法可以更好地組織與類相關的程式碼,也可以在子類中被重寫。
類方法
類方法由類自身來呼叫,而不是例項。類方法使用@classmethod
裝飾器定義,作為第一個引數被傳遞給方法的是類
而不是例項
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
import json class Account(object): num_accounts = 0 def __init__(self, name, balance): self.name = name self.balance = balance Account.num_accounts += 1 def del_account(self): Account.num_accounts -= 1 def deposit(self, amt): self.balance = self.balance + amt def withdraw(self, amt): self.balance = self.balance - amt def inquiry(self): return "Name={}, balance={}".format(self.name, self.balance) @classmethod def from_json(cls, params_json): params = json.loads(params_json) return cls(params.get("name"), params.get("balance")) @staticmethod def type(): return "Current Account" |
類方法一個常見的用法是作為物件建立的工廠。假如Account
類的資料格式有很多種,比如元組、json字串等。由於Python類只能定義一個__init__
方法,所以類方法在這些情形中就很方便。以上文Account
類為例,我們想根據一個json字串物件來初始化一個賬戶,我們定義一個類工廠方法from_json
,它讀取json字串物件,解析引數,根據引數建立賬戶物件。另一個類例項的例子是dict.fromkeys
方法,它從一組鍵和值序列中建立dict物件。
Python特殊方法
有時我們希望自定義類。這需要改變類物件建立和初始化的方法,或者對某些操作提供多型行為。多型行為允許定製在類定義中某些如+
等python操作的自身實現。Python的特殊方法可以做到這些。這些方法一般都是__*__
形式,其中*
表示方法名。如__init__
和__new__
來自定義物件建立和初始化,__getitem__
、__get__
、__add__
、__sub__
來模擬python內建型別,還有__getattribute__
、__getattr__
等來定製屬性訪問。只有為數不多的特殊方法,我們討論一些重要的特殊方法來做個簡單理解,python文件有全部方法的列表。
進行物件建立的特殊方法
新的類例項通過兩階段過程建立,__new__
方法建立新例項,__init__
初始化該例項。使用者已經很熟悉__init__
方法的定義;但使用者很少定義__new__
方法,但是如果想自定義類例項的建立,也是可以的。
屬性訪問的特殊方法
我們可以通過實現以下方法來定製類例項的屬性訪問。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
class Account(object): num_accounts = 0 def __init__(self, name, balance): self.name = name self.balance = balance Account.num_accounts += 1 def del_account(self): Account.num_accounts -= 1 def __getattr__(self, name): return "Hey I dont see any attribute called {}".format(name) def deposit(self, amt): self.balance = self.balance + amt def withdraw(self, amt): self.balance = self.balance - amt def inquiry(self): return "Name={}, balance={}".format(self.name, self.balance) @classmethod def from_dict(cls, params): params_dict = json.loads(params) return cls(params_dict.get("name"), params_dict.get("balance")) @staticmethod def type(): return "Current Account" x = Account('obi', 0) |
__getattr__(self, name)__
:這個方法只有當name既不是例項屬性也不能在物件的類繼承鏈中找到時才會被呼叫。這個方法應當返回屬性值或者引發AttributeError
異常。例如,如果x是Account類的例項,嘗試訪問不存在的屬性將會呼叫這個方法。
1 2 3 |
>>> acct = Account("obi", 10) >>> acct.number Hey I dont see any attribute called number |
注意如果 __getattr__
引用不存在的例項屬性,可能會發生死迴圈,因為__getattr__
方法不斷被呼叫。
2.__setattr__(self, name, value)__
:這個方法當屬性賦值發生時呼叫。__setattr__
將會把值插入到例項屬性字典中,而不是使用self.name=value
,因為它會導致遞迴呼叫的死迴圈。
3.__delattr__(self, name)__
:del obj
發生時呼叫。
4.__getattribute__(self, name)__
:這個方法會被一直呼叫以實現類例項的屬性訪問。
型別模擬的特殊方法
對某些型別,Python定義了某些特定語法;比如,列表和元組的元素可以通過索引表示法來訪問,數值可以通過+
操作符來進行加法等等。我們可以建立自己的使用這些特殊語法的類,python直譯器遇到這些特殊語法時就會呼叫我們實現的方法。我們在下面用一個簡單的例子來演示這個特性,它模擬python列表的基本用法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
class CustomList(object): def __init__(self, container=None): # the class is just a wrapper around another list to # illustrate special methods if container is None: self.container = [] else: self.container = container def __len__(self): # called when a user calls len(CustomList instance) return len(self.container) def __getitem__(self, index): # called when a user uses square brackets for indexing return self.container[index] def __setitem__(self, index, value): # called when a user performs an index assignment if index <= len(self.container): self.container[index] = value else: raise IndexError() def __contains__(self, value): # called when the user uses the 'in' keyword return value in self.container def append(self, value): self.container.append(value) def __repr__(self): return str(self.container) def __add__(self, otherList): # provides support for the use of the + operator return CustomList(self.container + otherList.container) |
上面,CustomList
是個真實列表的簡單包裝器。我們為了演示實現了一些自定義方法:
__len__(self)
:對CustomList
例項呼叫len()
函式時被呼叫。
1 2 3 4 5 6 7 |
>>> myList = CustomList() >>> myList.append(1) >>> myList.append(2) >>> myList.append(3) >>> myList.append(4) >>> len(myList) 4 |
2.__getitem__(self, value)
:提供CustomList類例項的方括號索引用法支援:
1 2 3 4 5 6 7 |
>>> myList = CustomList() >>> myList.append(1) >>> myList.append(2) >>> myList.append(3) >>> myList.append(4) >>> myList[3] 4 |
3.__setitem__(self, key, value)
:當對CustomList類例項上self[key]賦值時呼叫。
1 2 3 4 5 6 7 8 9 |
>>> myList = CustomList() >>> myList.append(1) >>> myList.append(2) >>> myList.append(3) >>> myList.append(4) >>> myList[3] = 100 4 >>> myList[3] 100 |
4.__contains__(self, key)
:成員檢測時呼叫。如果包含該項就返回true,否則false。
1 2 3 4 5 6 7 |
>>> myList = CustomList() >>> myList.append(1) >>> myList.append(2) >>> myList.append(3) >>> myList.append(4) >>> 4 in myList True |
5.__repr__(self)
:當用print列印self時呼叫,將會列印self的物件表示。
1 2 3 4 5 6 7 |
>>> myList = CustomList() >>> myList.append(1) >>> myList.append(2) >>> myList.append(3) >>> myList.append(4) >>> print myList [1, 2, 3, 4] |
6.__add__(self, otherList)
:使用+
操作符來計算兩個CustomList例項相加時呼叫。
1 2 3 4 5 6 7 8 9 |
>>> myList = CustomList() >>> otherList = CustomList() >>> otherList.append(100) >>> myList.append(1) >>> myList.append(2) >>> myList.append(3) >>> myList.append(4) >>> myList + otherList + otherList [1, 2, 3, 4, 100, 100] |
上面的例子演示瞭如何通過定義某些特殊類方法來定製類行為。可以在Python文件中檢視這些自定義方法的完整列表。在接下來的教程中,我們會將特殊方法放到一起來討論,並解釋描述符這個在python物件導向程式設計中廣泛使用的重要功能。
進一步閱讀
- Python核心參考
- Python資料模型