來源:2.5 Object-Oriented Programminghtml
譯者:飛龍python
協議:CC BY-NC-SA 4.0git
面向對象編程(OOP)是一種用於組織程序的方法,它組合了這一章引入的許多概念。就像抽象數據類型那樣,對象建立了數據使用和實現之間的抽象界限。相似消息傳遞中的分發字典,對象響應行爲請求。就像可變的數據結構,對象擁有局部狀態,而且不能直接從全局環境訪問。Python 對象系統提供了新的語法,更易於爲組織程序實現全部這些實用的技巧。程序員
可是對象系統不只僅提供了便利;它也爲程序設計添加了新的隱喻,其中程序中的幾個部分彼此交互。每一個對象將局部狀態和行爲綁定,以一種方式在數據抽象背後隱藏兩者的複雜性。咱們的約束程序的例子經過在約束和鏈接器以前傳遞消息,產生了這種隱喻。Python 對象系統使用新的途徑擴展了這種隱喻,來表達程序的不一樣部分如何互相關聯,以及互相通訊。對象不只僅會傳遞消息,還會和其它相同類型的對象共享行爲,以及從相關的類型那裏繼承特性。github
面向對象編程的範式使用本身的詞彙來強化對象隱喻。咱們已經看到了,對象是擁有方法和屬性的數據值,能夠經過點運算符來訪問。每一個對象都擁有一個類型,叫作類。Python 中能夠定義新的類,就像定義函數那樣。算法
類能夠用做全部類型爲該類的對象的模板。每一個對象都是某個特定類的實例。咱們目前使用的對象都擁有內建類型,可是咱們能夠定義新的類,就像定義函數那樣。類的定義規定了在該類的對象之間共享的屬性和方法。咱們會經過從新觀察銀行帳戶的例子,來介紹類的語句。express
在介紹局部狀態時,咱們看到,銀行帳戶能夠天然地建模爲擁有balance
的可變值。銀行帳戶對象應該擁有withdraw
方法,在可用的狀況下,它會更新帳戶餘額,並返回所請求的金額。咱們但願添加一些額外的行爲來完善帳戶抽象:銀行帳戶應該可以返回它的當前餘額,返回帳戶持有者的名稱,以及接受存款。編程
Account
類容許咱們建立銀行帳戶的多個實例。建立新對象實例的動做被稱爲實例化該類。Python 中實例化某個類的語法相似於函數的調用語句。這裏,咱們使用參數'Jim'
(帳戶持有者的名稱)來調用Account
。數組
>>> a = Account('Jim')
對象的屬性是和對象關聯的名值對,它能夠經過點運算符來訪問。屬性特定於具體的對象,而不是類的全部對象,也叫作實例屬性。每一個Account
對象都擁有本身的餘額和帳戶持有者名稱,它們是實例屬性的一個例子。在更寬泛的編程社羣中,實例屬性可能也叫作字段、屬性或者實例變量。網絡
>>> a.holder 'Jim' >>> a.balance 0
操做對象或執行對象特定計算的函數叫作方法。方法的反作用和返回值可能依賴或改變對象的其它屬性。例如,deposit
是Account
對象a
上的方法。它接受一個參數,即須要存入的金額,修改對象的balance
屬性,並返回產生的餘額。
>>> a.deposit(15) 15
在 OOP 中,咱們說方法能夠在特定對象上調用。做爲調用withdraw
方法的結果,要麼取錢成功,餘額減小並返回,要麼請求被拒絕,帳戶打印出錯誤信息。
>>> a.withdraw(10) # The withdraw method returns the balance after withdrawal 5 >>> a.balance # The balance attribute has changed 5 >>> a.withdraw(10) 'Insufficient funds'
像上面展現的那樣,方法的行爲取決於對象屬性的改變。兩次以相同參數對withdraw
的調用返回了不一樣的結果。
用戶定義的類由class
語句建立,它只包含單個子句。類的語句定義了類的名稱和基類(會在繼承那一節討論),以後包含了定義類屬性的語句組:
class <name>(<base class>): <suite>
當類的語句被執行時,新的類會被建立,而且在當前環境第一幀綁定到<name>
上。以後會執行語句組。任何名稱都會在class
語句的<suite>
中綁定,經過def
或賦值語句,建立或修改類的屬性。
類一般圍繞實例屬性來組織,實例屬性是名值對,不和類自己關聯但和類的每一個對象關聯。經過爲實例化新對象定義方法,類規定了它的對象的實例屬性。
class
語句的<suite>
部分包含def
語句,它們爲該類的對象定義了新的方法。用於實例化對象的方法在 Python 中擁有特殊的名稱,__init__
(init
兩邊分別有兩個下劃線),它叫作類的構造器。
>>> class Account(object): def __init__(self, account_holder): self.balance = 0 self.holder = account_holder
Account
的__init__
方法有兩個形參。第一個是self
,綁定到新建立的Account
對象上。第二個參數,account_holder
,在被調用來實例化的時候,綁定到傳給該類的參數上。
構造器將實例屬性名稱balance
與0
綁定。它也將屬性名稱holder
綁定到account_holder
上。形參account_holder
是__init__
方法的局部名稱。另外一方面,經過最後一個賦值語句綁定的名稱holder
是一直存在的,由於它使用點運算符被存儲爲self
的屬性。
定義了Account
類以後,咱們就能夠實例化它:
>>> a = Account('Jim')
這個對Account
類的「調用」建立了新的對象,它是Account
的實例,以後以兩個參數調用了構造函數__init__
:新建立的對象和字符串'Jim'
。按照慣例,咱們使用名稱self
來命名構造器的第一個參數,由於它綁定到了被實例化的對象上。這個慣例在幾乎全部 Python 代碼中都適用。
如今,咱們可使用點運算符來訪問對象的balance
和holder
。
>>> a.balance 0 >>> a.holder 'Jim'
身份。每一個新的帳戶實例都有本身的餘額屬性,它的值獨立於相同類的其它對象。
>>> b = Account('Jack') >>> b.balance = 200 >>> [acc.balance for acc in (a, b)] [0, 200]
爲了強化這種隔離,每一個用戶定義類的實例對象都有個獨特的身份。對象身份使用is
和is not
運算符來比較。
>>> a is a True >>> a is not b True
雖然由同一個調用來構造,綁定到a
和b
的對象並不相同。一般,使用賦值將對象綁定到新名稱並不會建立新的對象。
>>> c = a >>> c is a True
用戶定義類的新對象只在類(好比Account
)使用調用表達式被實例化的時候建立。
方法。對象方法也由class
語句組中的def
語句定義。下面,deposit
和withdraw
都被定義爲Account
類的對象上的方法:
>>> class Account(object): def __init__(self, account_holder): self.balance = 0 self.holder = account_holder def deposit(self, amount): self.balance = self.balance + amount return self.balance def withdraw(self, amount): if amount > self.balance: return 'Insufficient funds' self.balance = self.balance - amount return self.balance
雖然方法定義和函數定義在聲明方式上並無區別,方法定義有不一樣的效果。由class
語句中的def
語句建立的函數值綁定到了聲明的名稱上,可是隻在類的局部綁定爲一個屬性。這個值可使用點運算符在類的實例上做爲方法來調用。
每一個方法定義一樣包含特殊的首個參數self
,它綁定到方法所調用的對象上。例如,讓咱們假設deposit
在特定的Account
對象上調用,而且傳遞了一個對象值:要存入的金額。對象自己綁定到了self
上,而參數綁定到了amount
上。全部被調用的方法可以經過self
參數來訪問對象,因此它們能夠訪問並操做對象的狀態。
爲了調用這些方法,咱們再次使用點運算符,就像下面這樣:
>>> tom_account = Account('Tom') >>> tom_account.deposit(100) 100 >>> tom_account.withdraw(90) 10 >>> tom_account.withdraw(90) 'Insufficient funds' >>> tom_account.holder 'Tom'
當一個方法經過點運算符調用時,對象自己(這個例子中綁定到了tom_account
)起到了雙重做用。首先,它決定了withdraw
意味着哪一個名稱;withdraw
並非環境中的名稱,而是Account
類局部的名稱。其次,當withdraw
方法調用時,它綁定到了第一個參數self
上。求解點運算符的詳細過程會在下一節中展現。
方法定義在類中,而實例屬性一般在構造器中賦值,兩者都是面向對象編程的基本元素。這兩個概念很大程度上相似於數據值的消息傳遞實現中的分發字典。對象使用點運算符接受消息,可是消息並非任意的、值爲字符串的鍵,而是類的局部名稱。對象也擁有具名的局部狀態值(實例屬性),可是這個狀態可使用點運算符訪問和操做,並不須要在實現中使用nonlocal
語句。
消息傳遞的核心概念,就是數據值應該經過響應消息而擁有行爲,這些消息和它們所表示的抽象類型相關。點運算符是 Python 的語法特徵,它造成了消息傳遞的隱喻。使用帶有內建對象系統語言的優勢是,消息傳遞可以和其它語言特性,例如賦值語句無縫對接。咱們並不須要不一樣的消息來「獲取」和「設置」關聯到局部屬性名稱的值;語言的語法容許咱們直接使用消息名稱。
點表達式。相似tom_account.deposit
的代碼片斷叫作點表達式。點表達式包含一個表達式,一個點和一個名稱:
<expression> . <name>
<expression>
可爲任意的 Python 有效表達式,可是<name>
必須是個簡單的名稱(而不是求值爲name
的表達式)。點表達式會使用提供的<name>
,對值爲<expression>
的對象求出屬性的值。
內建的函數getattr
也會按名稱返回對象的屬性。它是等價於點運算符的函數。使用getattr
,咱們就能使用字符串來查找某個屬性,就像分發字典那樣:
>>> getattr(tom_account, 'balance') 10
咱們也可使用hasattr
測試對象是否擁有某個具名屬性:
>>> hasattr(tom_account, 'deposit') True
對象的屬性包含全部實例屬性,以及全部定義在類中的屬性(包括方法)。方法是須要特別處理的類的屬性。
方法和函數。當一個方法在對象上調用時,對象隱式地做爲第一個參數傳遞給方法。也就是說,點運算符左邊值爲<expression>
的對象,會自動傳給點運算符右邊的方法,做爲第一個參數。因此,對象綁定到了參數self
上。
爲了自動實現self
的綁定,Python 區分函數和綁定方法。咱們已經在這門課的開始建立了前者,然後者在方法調用時將對象和函數組合到一塊兒。綁定方法的值已經將第一個函數關聯到所調用的實例,當方法調用時實例會被命名爲self
。
經過在點運算符的返回值上調用type
,咱們能夠在交互式解釋器中看到它們的差別。做爲類的屬性,方法只是個函數,可是做爲實例屬性,它是綁定方法:
>>> type(Account.deposit) <class 'function'> >>> type(tom_account.deposit) <class 'method'>
這兩個結果的惟一不一樣點是,前者是個標準的二元函數,帶有參數self
和amount
。後者是一元方法,當方法被調用時,名稱self
自動綁定到了名爲tom_account
的對象上,而名稱amount
會被綁定到傳遞給方法的參數上。這兩個值,不管函數值或綁定方法的值,都和相同的deposit
函數體所關聯。
咱們能夠以兩種方式調用deposit
:做爲函數或做爲綁定方法。在前者的例子中,咱們必須爲self
參數顯式提供實參。而對於後者,self
參數已經自動綁定了。
>>> Account.deposit(tom_account, 1001) # The deposit function requires 2 arguments 1011 >>> tom_account.deposit(1000) # The deposit method takes 1 argument 2011
函數getattr
的表現就像運算符那樣:它的第一個參數是對象,而第二個參數(名稱)是定義在類中的方法。以後,getattr
返回綁定方法的值。另外一方面,若是第一個參數是個類,getattr
會直接返回屬性值,它僅僅是個函數。
實踐指南:命名慣例。類名稱一般以首字母大寫來編寫(也叫做駝峯拼寫法,由於名稱中間的大寫字母像駝峯)。方法名稱遵循函數命名的慣例,使用如下劃線分隔的小寫字母。
有的時候,有些實例變量和方法的維護和對象的一致性相關,咱們不想讓用戶看到或使用它們。它們並非由類定義的一部分抽象,而是一部分實現。Python 的慣例規定,若是屬性名稱如下劃線開始,它只能在方法或類中訪問,而不能被類的用戶訪問。
有些屬性值在特定類的全部對象之間共享。這樣的屬性關聯到類自己,而不是類的任何獨立實例。例如,讓咱們假設銀行以固定的利率對餘額支付利息。這個利率可能會改變,可是它是在全部帳戶中共享的單一值。
類屬性由class
語句組中的賦值語句建立,位於任何方法定義以外。在更寬泛的開發者社羣中,類屬性也被叫作類變量或靜態變量。下面的類語句以名稱interest
爲Account
建立了類屬性。
>>> class Account(object): interest = 0.02 # A class attribute def __init__(self, account_holder): self.balance = 0 self.holder = account_holder # Additional methods would be defined here
這個屬性仍舊能夠經過類的任何實例來訪問。
>>> tom_account = Account('Tom') >>> jim_account = Account('Jim') >>> tom_account.interest 0.02 >>> jim_account.interest 0.02
可是,對類屬性的單一賦值語句會改變全部該類實例上的屬性值。
>>> Account.interest = 0.04 >>> tom_account.interest 0.04 >>> jim_account.interest 0.04
屬性名稱。咱們已經在咱們的對象系統中引入了足夠的複雜性,咱們須要規定名稱如何解析爲特定的屬性。畢竟,咱們能夠輕易擁有同名的類屬性和實例屬性。
像咱們看到的那樣,點運算符由表達式、點和名稱組成:
<expression> . <name>
爲了求解點表達式:
求出點左邊的<expression>
,會產生點運算符的對象。
<name>
會和對象的實例屬性匹配;若是該名稱的屬性存在,會返回它的值。
若是<name>
不存在於實例屬性,那麼會在類中查找<name>
,這會產生類的屬性值。
這個值會被返回,若是它是個函數,則會返回綁定方法。
在這個求值過程當中,實例屬性在類的屬性以前查找,就像局部名稱具備高於全局的優先級。定義在類中的方法,在求值過程的第三步綁定到了點運算符的對象上。在類中查找名稱的過程有額外的差別,在咱們引入類繼承的時候就會出現。
賦值。全部包含點運算符的賦值語句都會做用於右邊的對象。若是對象是個實例,那麼賦值就會設置實例屬性。若是對象是個類,那麼賦值會設置類屬性。做爲這條規則的結果,對對象屬性的賦值不能影響類的屬性。下面的例子展現了這個區別。
若是咱們向帳戶實例的具名屬性interest
賦值,咱們會建立屬性的新實例,它和現有的類屬性具備相同名稱。
>>> jim_account.interest = 0.08
這個屬性值會經過點運算符返回:
>>> jim_account.interest 0.08
可是,類屬性interest
會保持爲原始值,它能夠經過全部其餘帳戶返回。
>>> tom_account.interest 0.04
類屬性interest
的改動會影響tom_account
,可是jim_account
的實例屬性不受影響。
>>> Account.interest = 0.05 # changing the class attribute >>> tom_account.interest # changes instances without like-named instance attributes 0.05 >>> jim_account.interest # but the existing instance attribute is unaffected 0.08
在使用 OOP 範式時,咱們一般會發現,不一樣的抽象數據結構是相關的。特別是,咱們發現類似的類在特化的程度上有區別。兩個類可能擁有類似的屬性,可是一個表示另外一個的特殊狀況。
例如,咱們可能但願實現一個活期帳戶,它不一樣於標準的帳戶。活期帳戶對每筆取款都收取額外的 $1,而且具備較低的利率。這裏,咱們演示上述行爲:
>>> ch = CheckingAccount('Tom') >>> ch.interest # Lower interest rate for checking accounts 0.01 >>> ch.deposit(20) # Deposits are the same 20 >>> ch.withdraw(5) # withdrawals decrease balance by an extra charge 14
CheckingAccount
是Account
的特化。在 OOP 的術語中,通用的帳戶會做爲CheckingAccount
的基類,而CheckingAccount
是Account
的子類(術語「父類」和「超類」一般等同於「基類」,而「派生類」一般等同於「子類」)。
子類繼承了基類的屬性,可是可能覆蓋特定屬性,包括特定的方法。使用繼承,咱們只須要關注基類和子類之間有什麼不一樣。任何咱們在子類未指定的東西會自動假設和基類中相同。
繼承也在對象隱喻中有重要做用,不只僅是一種實用的組織方式。繼承意味着在類之間表達「is-a」關係,它和「has-a」關係相反。活期帳戶是(is-a)一種特殊類型的帳戶,因此讓CheckingAccount
繼承Account
是繼承的合理使用。另外一方面,銀行擁有(has-a)所管理的銀行帳戶的列表,因此兩者都不該繼承另外一個。反之,帳戶對象的列表應該天然地表現爲銀行帳戶的實例屬性。
咱們經過將基類放置到類名稱後面的圓括號內來指定繼承。首先,咱們提供Account
類的完整實現,也包含類和方法的文檔字符串。
>>> class Account(object): """A bank account that has a non-negative balance.""" interest = 0.02 def __init__(self, account_holder): self.balance = 0 self.holder = account_holder def deposit(self, amount): """Increase the account balance by amount and return the new balance.""" self.balance = self.balance + amount return self.balance def withdraw(self, amount): """Decrease the account balance by amount and return the new balance.""" if amount > self.balance: return 'Insufficient funds' self.balance = self.balance - amount return self.balance
CheckingAccount
的完整實如今下面:
>>> class CheckingAccount(Account): """A bank account that charges for withdrawals.""" withdraw_charge = 1 interest = 0.01 def withdraw(self, amount): return Account.withdraw(self, amount + self.withdraw_charge)
這裏,咱們引入了類屬性withdraw_charge
,它特定於CheckingAccount
類。咱們將一個更低的值賦給interest
屬性。咱們也定義了新的withdraw
方法來覆蓋定義在Account
對象中的行爲。類語句組中沒有更多的語句,全部其它行爲都從基類Account
中繼承。
>>> checking = CheckingAccount('Sam') >>> checking.deposit(10) 10 >>> checking.withdraw(5) 4 >>> checking.interest 0.01
checking.deposit
表達式是用於存款的綁定方法,它定義在Account
類中,當 Python 解析點表達式中的名稱時,實例上並無這個屬性,它會在類中查找該名稱。實際上,在類中「查找名稱」的行爲會在原始對象的類的繼承鏈中的每一個基類中查找。咱們能夠遞歸定義這個過程,爲了在類中查找名稱:
若是類中有帶有這個名稱的屬性,返回屬性值。
不然,若是有基類的話,在基類中查找該名稱。
在deposit
中,Python 會首先在實例中查找名稱,以後在CheckingAccount
類中。最後,它會在Account
中查找,這裏是deposit
定義的地方。根據咱們對點運算符的求值規則,因爲deposit
是在checking
實例的類中查找到的函數,點運算符求值爲綁定方法。這個方法以參數10
調用,這會以綁定到checking
對象的self
和綁定到10
的amount
調用deposit
方法。
對象的類會始終保持不變。即便deposit
方法在Account
類中找到,deposit
以綁定到CheckingAccount
實例的self
調用,而不是Account
的實例。
譯者注:
CheckingAccount
的實例也是Account
的實例,這個說法是有問題的。
調用祖先。被覆蓋的屬性仍然能夠經過類對象來訪問。例如,咱們能夠經過以包含withdraw_charge
的參數調用Account
的withdraw
方法,來實現CheckingAccount
的withdraw
方法。
要注意咱們調用self.withdraw_charge
而不是等價的CheckingAccount.withdraw_charge
。前者的好處就是繼承自CheckingAccount
的類可能會覆蓋支取費用。若是是這樣的話,咱們但願咱們的withdraw
實現使用新的值而不是舊的值。
Python 支持子類從多個基類繼承屬性的概念,這是一種叫作多重繼承的語言特性。
假設咱們從Account
繼承了SavingsAccount
,每次存錢的時候向客戶收取一筆小費用。
>>> class SavingsAccount(Account): deposit_charge = 2 def deposit(self, amount): return Account.deposit(self, amount - self.deposit_charge)
以後,一個聰明的總經理設想了AsSeenOnTVAccount
,它擁有CheckingAccount
和SavingsAccount
的最佳特性:支取和存入的費用,以及較低的利率。它將儲蓄帳戶和活期存款帳戶合二爲一!「若是咱們構建了它」,總經理解釋道,「一些人會註冊並支付全部這些費用。甚至咱們會給他們一美圓。」
>>> class AsSeenOnTVAccount(CheckingAccount, SavingsAccount): def __init__(self, account_holder): self.holder = account_holder self.balance = 1 # A free dollar!
實際上,這個實現就完整了。存款和取款都須要費用,使用了定義在CheckingAccount
和SavingsAccount
中的相應函數。
>>> such_a_deal = AsSeenOnTVAccount("John") >>> such_a_deal.balance 1 >>> such_a_deal.deposit(20) # $2 fee from SavingsAccount.deposit 19 >>> such_a_deal.withdraw(5) # $1 fee from CheckingAccount.withdraw 13
就像預期那樣,沒有歧義的引用會正確解析:
>>> such_a_deal.deposit_charge 2 >>> such_a_deal.withdraw_charge 1
可是若是引用有歧義呢,好比withdraw
方法的引用,它定義在Account
和CheckingAccount
中?下面的圖展現了AsSeenOnTVAccount
類的繼承圖。每一個箭頭都從子類指向基類。
對於像這樣的簡單「菱形」,Python 從左到右解析名稱,以後向上。這個例子中,Python 按下列順序檢查名稱,直到找到了具備該名稱的屬性:
AsSeenOnTVAccount, CheckingAccount, SavingsAccount, Account, object
繼承順序的問題沒有正確的解法,由於咱們可能會給某個派生類高於其餘類的優先級。可是,任何支持多重繼承的編程語言必須始終選擇同一個順序,便於語言的用戶預測程序的行爲。
擴展閱讀。Python 使用一種叫作 C3 Method Resolution Ordering 的遞歸算法來解析名稱。任何類的方法解析順序都使用全部類上的mro
方法來查詢。
>>> [c.__name__ for c in AsSeenOnTVAccount.mro()] ['AsSeenOnTVAccount', 'CheckingAccount', 'SavingsAccount', 'Account', 'object']
這個用於查詢方法解析順序的算法並非這門課的主題,可是 Python 的原做者使用一篇原文章的引用來描述它。
Python 對象系統爲使數據抽象和消息傳遞更加便捷和靈活而設計。類、方法、繼承和點運算符的特化語法均可以讓咱們在程序中造成對象隱喻,它可以提高咱們組織大型程序的能力。
特別是,咱們但願咱們的對象系統在不一樣層面上促進關注分離。每一個程序中的對象都封裝和管理程序狀態的一部分,每一個類語句都定義了一些函數,它們實現了程序整體邏輯的一部分。抽象界限強制了大型程序不一樣層面之間的邊界。
面向對象編程適合於對系統建模,這些系統擁有相互分離並交互的部分。例如,不一樣用戶在社交網絡中互動,不一樣角色在遊戲中互動,以及不一樣圖形在物理模擬中互動。在表現這種系統的時候,程序中的對象一般天然地映射爲被建模系統中的對象,類用於表現它們的類型和關係。
另外一方面,類可能不會提供用於實現特定的抽象的最佳機制。函數式抽象提供了更加天然的隱喻,用於表現輸入和輸出的關係。一我的不該該強迫本身把程序中的每一個細微的邏輯都塞到類裏面,尤爲是當定義獨立函數來操做數據變得十分天然的時候。函數也強制了關注分離。
相似 Python 的多範式語言容許程序員爲合適的問題匹配合適的範式。爲了簡化程序,或使程序模塊化,肯定什麼時候引入新的類,而不是新的函數,是軟件工程中的重要設計技巧,這須要仔細關注。