SICP Python 描述 2.5 面向對象編程

2.5 面向對象編程

來源:2.5 Object-Oriented Programminghtml

譯者:飛龍python

協議:CC BY-NC-SA 4.0git

面向對象編程(OOP)是一種用於組織程序的方法,它組合了這一章引入的許多概念。就像抽象數據類型那樣,對象建立了數據使用和實現之間的抽象界限。相似消息傳遞中的分發字典,對象響應行爲請求。就像可變的數據結構,對象擁有局部狀態,而且不能直接從全局環境訪問。Python 對象系統提供了新的語法,更易於爲組織程序實現全部這些實用的技巧。程序員

可是對象系統不只僅提供了便利;它也爲程序設計添加了新的隱喻,其中程序中的幾個部分彼此交互。每一個對象將局部狀態和行爲綁定,以一種方式在數據抽象背後隱藏兩者的複雜性。咱們的約束程序的例子經過在約束和鏈接器以前傳遞消息,產生了這種隱喻。Python 對象系統使用新的途徑擴展了這種隱喻,來表達程序的不一樣部分如何互相關聯,以及互相通訊。對象不只僅會傳遞消息,還會和其它相同類型的對象共享行爲,以及從相關的類型那裏繼承特性。github

面向對象編程的範式使用本身的詞彙來強化對象隱喻。咱們已經看到了,對象是擁有方法和屬性的數據值,能夠經過點運算符來訪問。每一個對象都擁有一個類型,叫作類。Python 中能夠定義新的類,就像定義函數那樣。算法

2.5.1 對象和類

類能夠用做全部類型爲該類的對象的模板。每一個對象都是某個特定類的實例。咱們目前使用的對象都擁有內建類型,可是咱們能夠定義新的類,就像定義函數那樣。類的定義規定了在該類的對象之間共享的屬性和方法。咱們會經過從新觀察銀行帳戶的例子,來介紹類的語句。express

在介紹局部狀態時,咱們看到,銀行帳戶能夠天然地建模爲擁有balance的可變值。銀行帳戶對象應該擁有withdraw方法,在可用的狀況下,它會更新帳戶餘額,並返回所請求的金額。咱們但願添加一些額外的行爲來完善帳戶抽象:銀行帳戶應該可以返回它的當前餘額,返回帳戶持有者的名稱,以及接受存款。編程

Account類容許咱們建立銀行帳戶的多個實例。建立新對象實例的動做被稱爲實例化該類。Python 中實例化某個類的語法相似於函數的調用語句。這裏,咱們使用參數'Jim'(帳戶持有者的名稱)來調用Account數組

>>> a = Account('Jim')

對象的屬性是和對象關聯的名值對,它能夠經過點運算符來訪問。屬性特定於具體的對象,而不是類的全部對象,也叫作實例屬性。每一個Account對象都擁有本身的餘額和帳戶持有者名稱,它們是實例屬性的一個例子。在更寬泛的編程社羣中,實例屬性可能也叫作字段、屬性或者實例變量。網絡

>>> a.holder
'Jim'
>>> a.balance
0

操做對象或執行對象特定計算的函數叫作方法。方法的反作用和返回值可能依賴或改變對象的其它屬性。例如,depositAccount對象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的調用返回了不一樣的結果。

2.5.2 類的定義

用戶定義的類由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,在被調用來實例化的時候,綁定到傳給該類的參數上。

構造器將實例屬性名稱balance0綁定。它也將屬性名稱holder綁定到account_holder上。形參account_holder__init__方法的局部名稱。另外一方面,經過最後一個賦值語句綁定的名稱holder是一直存在的,由於它使用點運算符被存儲爲self的屬性。

定義了Account類以後,咱們就能夠實例化它:

>>> a = Account('Jim')

這個對Account類的「調用」建立了新的對象,它是Account的實例,以後以兩個參數調用了構造函數__init__:新建立的對象和字符串'Jim'。按照慣例,咱們使用名稱self來命名構造器的第一個參數,由於它綁定到了被實例化的對象上。這個慣例在幾乎全部 Python 代碼中都適用。

如今,咱們可使用點運算符來訪問對象的balanceholder

>>> a.balance
0
>>> a.holder
'Jim'

身份。每一個新的帳戶實例都有本身的餘額屬性,它的值獨立於相同類的其它對象。

>>> b = Account('Jack')
>>> b.balance = 200
>>> [acc.balance for acc in (a, b)]
[0, 200]

爲了強化這種隔離,每一個用戶定義類的實例對象都有個獨特的身份。對象身份使用isis not運算符來比較。

>>> a is a
True
>>> a is not b
True

雖然由同一個調用來構造,綁定到ab的對象並不相同。一般,使用賦值將對象綁定到新名稱並不會建立新的對象。

>>> c = a
>>> c is a
True

用戶定義類的新對象只在類(好比Account)使用調用表達式被實例化的時候建立。

方法。對象方法也由class語句組中的def語句定義。下面,depositwithdraw都被定義爲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上。求解點運算符的詳細過程會在下一節中展現。

2.5.3 消息傳遞和點表達式

方法定義在類中,而實例屬性一般在構造器中賦值,兩者都是面向對象編程的基本元素。這兩個概念很大程度上相似於數據值的消息傳遞實現中的分發字典。對象使用點運算符接受消息,可是消息並非任意的、值爲字符串的鍵,而是類的局部名稱。對象也擁有具名的局部狀態值(實例屬性),可是這個狀態可使用點運算符訪問和操做,並不須要在實現中使用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'>

這兩個結果的惟一不一樣點是,前者是個標準的二元函數,帶有參數selfamount。後者是一元方法,當方法被調用時,名稱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 的慣例規定,若是屬性名稱如下劃線開始,它只能在方法或類中訪問,而不能被類的用戶訪問。

2.5.4 類屬性

有些屬性值在特定類的全部對象之間共享。這樣的屬性關聯到類自己,而不是類的任何獨立實例。例如,讓咱們假設銀行以固定的利率對餘額支付利息。這個利率可能會改變,可是它是在全部帳戶中共享的單一值。

類屬性由class語句組中的賦值語句建立,位於任何方法定義以外。在更寬泛的開發者社羣中,類屬性也被叫作類變量或靜態變量。下面的類語句以名稱interestAccount建立了類屬性。

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

爲了求解點表達式:

  1. 求出點左邊的<expression>,會產生點運算符的對象。

  2. <name>會和對象的實例屬性匹配;若是該名稱的屬性存在,會返回它的值。

  3. 若是<name>不存在於實例屬性,那麼會在類中查找<name>,這會產生類的屬性值。

  4. 這個值會被返回,若是它是個函數,則會返回綁定方法。

在這個求值過程當中,實例屬性在類的屬性以前查找,就像局部名稱具備高於全局的優先級。定義在類中的方法,在求值過程的第三步綁定到了點運算符的對象上。在類中查找名稱的過程有額外的差別,在咱們引入類繼承的時候就會出現。

賦值。全部包含點運算符的賦值語句都會做用於右邊的對象。若是對象是個實例,那麼賦值就會設置實例屬性。若是對象是個類,那麼賦值會設置類屬性。做爲這條規則的結果,對對象屬性的賦值不能影響類的屬性。下面的例子展現了這個區別。

若是咱們向帳戶實例的具名屬性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

2.5.5 繼承

在使用 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

CheckingAccountAccount的特化。在 OOP 的術語中,通用的帳戶會做爲CheckingAccount的基類,而CheckingAccountAccount的子類(術語「父類」和「超類」一般等同於「基類」,而「派生類」一般等同於「子類」)。

子類繼承了基類的屬性,可是可能覆蓋特定屬性,包括特定的方法。使用繼承,咱們只須要關注基類和子類之間有什麼不一樣。任何咱們在子類未指定的東西會自動假設和基類中相同。

繼承也在對象隱喻中有重要做用,不只僅是一種實用的組織方式。繼承意味着在類之間表達「is-a」關係,它和「has-a」關係相反。活期帳戶是(is-a)一種特殊類型的帳戶,因此讓CheckingAccount繼承Account是繼承的合理使用。另外一方面,銀行擁有(has-a)所管理的銀行帳戶的列表,因此兩者都不該繼承另外一個。反之,帳戶對象的列表應該天然地表現爲銀行帳戶的實例屬性。

2.5.6 使用繼承

咱們經過將基類放置到類名稱後面的圓括號內來指定繼承。首先,咱們提供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 解析點表達式中的名稱時,實例上並無這個屬性,它會在類中查找該名稱。實際上,在類中「查找名稱」的行爲會在原始對象的類的繼承鏈中的每一個基類中查找。咱們能夠遞歸定義這個過程,爲了在類中查找名稱:

  1. 若是類中有帶有這個名稱的屬性,返回屬性值。

  2. 不然,若是有基類的話,在基類中查找該名稱。

deposit中,Python 會首先在實例中查找名稱,以後在CheckingAccount類中。最後,它會在Account中查找,這裏是deposit定義的地方。根據咱們對點運算符的求值規則,因爲deposit是在checking實例的類中查找到的函數,點運算符求值爲綁定方法。這個方法以參數10調用,這會以綁定到checking對象的self和綁定到10amount調用deposit方法。

對象的類會始終保持不變。即便deposit方法在Account類中找到,deposit以綁定到CheckingAccount實例的self調用,而不是Account的實例。

譯者注:CheckingAccount的實例也是Account的實例,這個說法是有問題的。

調用祖先。被覆蓋的屬性仍然能夠經過類對象來訪問。例如,咱們能夠經過以包含withdraw_charge的參數調用Accountwithdraw方法,來實現CheckingAccountwithdraw方法。

要注意咱們調用self.withdraw_charge而不是等價的CheckingAccount.withdraw_charge。前者的好處就是繼承自CheckingAccount的類可能會覆蓋支取費用。若是是這樣的話,咱們但願咱們的withdraw實現使用新的值而不是舊的值。

2.5.7 多重繼承

Python 支持子類從多個基類繼承屬性的概念,這是一種叫作多重繼承的語言特性。

假設咱們從Account繼承了SavingsAccount,每次存錢的時候向客戶收取一筆小費用。

>>> class SavingsAccount(Account):
        deposit_charge = 2
        def deposit(self, amount):
            return Account.deposit(self, amount - self.deposit_charge)

以後,一個聰明的總經理設想了AsSeenOnTVAccount,它擁有CheckingAccountSavingsAccount的最佳特性:支取和存入的費用,以及較低的利率。它將儲蓄帳戶和活期存款帳戶合二爲一!「若是咱們構建了它」,總經理解釋道,「一些人會註冊並支付全部這些費用。甚至咱們會給他們一美圓。」

>>> class AsSeenOnTVAccount(CheckingAccount, SavingsAccount):
        def __init__(self, account_holder):
            self.holder = account_holder
            self.balance = 1           # A free dollar!

實際上,這個實現就完整了。存款和取款都須要費用,使用了定義在CheckingAccountSavingsAccount中的相應函數。

>>> 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方法的引用,它定義在AccountCheckingAccount中?下面的圖展現了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 的原做者使用一篇原文章的引用來描述它。

2.5.8 對象的做用

Python 對象系統爲使數據抽象和消息傳遞更加便捷和靈活而設計。類、方法、繼承和點運算符的特化語法均可以讓咱們在程序中造成對象隱喻,它可以提高咱們組織大型程序的能力。

特別是,咱們但願咱們的對象系統在不一樣層面上促進關注分離。每一個程序中的對象都封裝和管理程序狀態的一部分,每一個類語句都定義了一些函數,它們實現了程序整體邏輯的一部分。抽象界限強制了大型程序不一樣層面之間的邊界。

面向對象編程適合於對系統建模,這些系統擁有相互分離並交互的部分。例如,不一樣用戶在社交網絡中互動,不一樣角色在遊戲中互動,以及不一樣圖形在物理模擬中互動。在表現這種系統的時候,程序中的對象一般天然地映射爲被建模系統中的對象,類用於表現它們的類型和關係。

另外一方面,類可能不會提供用於實現特定的抽象的最佳機制。函數式抽象提供了更加天然的隱喻,用於表現輸入和輸出的關係。一我的不該該強迫本身把程序中的每一個細微的邏輯都塞到類裏面,尤爲是當定義獨立函數來操做數據變得十分天然的時候。函數也強制了關注分離。

相似 Python 的多範式語言容許程序員爲合適的問題匹配合適的範式。爲了簡化程序,或使程序模塊化,肯定什麼時候引入新的類,而不是新的函數,是軟件工程中的重要設計技巧,這須要仔細關注。

相關文章
相關標籤/搜索