【廖雪峰python進階筆記】物件導向程式設計

Datawhale發表於2018-07-09

1. 定義類並建立例項

在Python中,類通過 class 關鍵字定義。以 Person 為例,定義一個Person類如下:

class Person(object):
    pass

按照 Python 的程式設計習慣,類名以大寫字母開頭,緊接著是(object),表示該類是從哪個類繼承下來的。類的繼承將在後面的章節講解,現在我們只需要簡單地從object類繼承。

有了Person類的定義,就可以建立出具體的xiaoming、xiaohong等例項。建立例項使用 類名+(),類似函式呼叫的形式建立:

xiaoming = Person()
xiaohong = Person()

2. 建立屬性

雖然可以通過Person類建立出xiaoming、xiaohong等例項,但是這些例項看上除了地址不同外,沒有什麼其他不同。在現實世界中,區分xiaoming、xiaohong要依靠他們各自的名字、性別、生日等屬性。

如何讓每個例項擁有各自不同的屬性?由於Python是動態語言,對每一個例項,都可以直接給他們的屬性賦值,例如,給xiaoming這個例項加上name、gender和birth屬性:

xiaoming = Person()
xiaoming.name = 'Xiao Ming'
xiaoming.gender = 'Male'
xiaoming.birth = '1990-1-1'

給xiaohong加上的屬性不一定要和xiaoming相同:

xiaohong = Person()
xiaohong.name = 'Xiao Hong'
xiaohong.school = 'No. 1 High School'
xiaohong.grade = 2

例項的屬性可以像普通變數一樣進行操作:

xiaohong.grade = xiaohong.grade + 1

例項
請建立包含兩個 Person 類的例項的 list,並給兩個例項的 name 賦值,然後按照 name 進行排序。

class Person(object):
    pass
p1 = Person()
p1.name = 'Bart'

p2 = Person()
p2.name = 'Adam'

p3 = Person()
p3.name = 'Lisa'

L1 = [p1, p2, p3]
L2 = sorted(L1, lambda p1, p2: cmp(p1.name, p2.name))

print L2[0].name
print L2[1].name
print L2[2].name

3.初始化屬性

雖然我們可以自由地給一個例項繫結各種屬性,但是,現實世界中,一種型別的例項應該擁有相同名字的屬性。例如,Person類應該在建立的時候就擁有 name、gender 和 birth 屬性,怎麼辦?

在定義 Person 類時,可以為Person類新增一個特殊的__init__()方法,當建立例項時,__init__()方法被自動呼叫,我們就能在此為每個例項都統一加上以下屬性:

class Person(object):
    def __init__(self, name, gender, birth):
        self.name = name
        self.gender = gender
        self.birth = birth

__init__() 方法的第一個引數必須是 self(也可以用別的名字,但建議使用習慣用法),後續引數則可以自由指定,和定義函式沒有任何區別。

相應地,建立例項時,就必須要提供除 self 以外的引數:

xiaoming = Person('Xiao Ming', 'Male', '1991-1-1')
xiaohong = Person('Xiao Hong', 'Female', '1992-2-2')

有了__init__()方法,每個Person例項在建立時,都會有 name、gender 和 birth 這3個屬性,並且,被賦予不同的屬性值,訪問屬性使用.操作符:

print xiaoming.name
# 輸出 'Xiao Ming'
print xiaohong.birth
# 輸出 '1992-2-2'

要特別注意的是,初學者定義__init__()方法常常忘記了 self 引數:

>>> class Person(object):
...     def __init__(name, gender, birth):
...         pass
... 
>>> xiaoming = Person('Xiao Ming', 'Male', '1990-1-1')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: __init__() takes exactly 3 arguments (4 given)

這會導致建立失敗或執行不正常,因為第一個引數name被Python直譯器傳入了例項的引用,從而導致整個方法的呼叫引數位置全部沒有對上。

例項
請定義Person類的init方法,除了接受 name、gender 和 birth 外,還可接受任意關鍵字引數,並把他們都作為屬性賦值給例項。
分析
要定義關鍵字引數,使用 **kw;
除了可以直接使用self.name = ‘xxx’設定一個屬性外,還可以通過 setattr(self, ‘name’, ‘xxx’) 設定屬性。
參考程式碼:

class Person(object):
    def __init__(self, name, gender, birth, **kw):
        self.name = name
        self.gender = gender
        self.birth = birth
        for k, v in kw.iteritems():
            setattr(self, k, v)
xiaoming = Person('Xiao Ming', 'Male', '1990-1-1', job='Student')
print xiaoming.name
print xiaoming.job

4. 訪問限制

我們可以給一個例項繫結很多屬性,如果有些屬性不希望被外部訪問到怎麼辦?

Python對屬性許可權的控制是通過屬性名來實現的,如果一個屬性由雙下劃線開頭(__),該屬性就無法被外部訪問。看例子:

class Person(object):
    def __init__(self, name):
        self.name = name
        self._title = 'Mr'
        self.__job = 'Student'
p = Person('Bob')
print p.name
# => Bob
print p._title
# => Mr
print p.__job
# => Error
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Person' object has no attribute '__job'

可見,只有以雙下劃線開頭的”__job”不能直接被外部訪問。

但是,如果一個屬性以"__xxx__"的形式定義,那它又可以被外部訪問了,以"__xxx__"定義的屬性在Python的類中被稱為特殊屬性,有很多預定義的特殊屬性可以使用,通常我們不要把普通屬性用"__xxx__"定義。

以單下劃線開頭的屬性"_xxx"雖然也可以被外部訪問,但是,按照習慣,他們不應該被外部訪問。

5. 建立屬性

類是模板,而例項則是根據類建立的物件。

繫結在一個例項上的屬性不會影響其他例項,但是,類本身也是一個物件,如果在類上繫結一個屬性,則所有例項都可以訪問類的屬性,並且,所有例項訪問的類屬性都是同一個!也就是說,例項屬性每個例項各自擁有,互相獨立,而類屬性有且只有一份。

定義類屬性可以直接在 class 中定義:

class Person(object):
    address = 'Earth'
    def __init__(self, name):
        self.name = name

因為類屬性是直接繫結在類上的,所以,訪問類屬性不需要建立例項,就可以直接訪問:

print Person.address
# => Earth

對一個例項呼叫類的屬性也是可以訪問的,所有例項都可以訪問到它所屬的類的屬性:

p1 = Person('Bob')
p2 = Person('Alice')
print p1.address
# => Earth
print p2.address
# => Earth

由於Python是動態語言,類屬性也是可以動態新增和修改的:

Person.address = 'China'
print p1.address
# => 'China'
print p2.address
# => 'China'

因為類屬性只有一份,所以,當Person類的address改變時,所有例項訪問到的類屬性都改變了。
例項
請給 Person 類新增一個類屬性 count,每建立一個例項,count 屬性就加 1,這樣就可以統計出一共建立了多少個 Person 的例項。

class Person(object):
    count = 0
    def __init__(self,name):
        self.name = name
        Person.count +=1

p1 = Person('Bob')
print Person.count

p2 = Person('Alice')
print Person.count

6. 解決類屬性和例項屬性名字衝突

修改類屬性會導致所有例項訪問到的類屬性全部都受影響,但是,如果在例項變數上修改類屬性會發生什麼問題呢?

class Person(object):
    address = 'Earth'
    def __init__(self, name):
        self.name = name

p1 = Person('Bob')
p2 = Person('Alice')

print 'Person.address = ' + Person.address

p1.address = 'China'
print 'p1.address = ' + p1.address

print 'Person.address = ' + Person.address
print 'p2.address = ' + p2.address

#結果如下:
Person.address = Earth
p1.address = China
Person.address = Earth
p2.address = Earth

我們發現,在設定了 p1.address = ‘China’ 後,p1訪問 address 確實變成了 ‘China’,但是,Person.address和p2.address仍然是’Earch’,怎麼回事?

原因是 p1.address = ‘China’並沒有改變 Person 的 address,而是給 p1這個例項繫結了例項屬性address ,對p1來說,它有一個例項屬性address(值是’China’),而它所屬的類Person也有一個類屬性address,所以:

訪問 p1.address 時,優先查詢例項屬性,返回’China’。

訪問 p2.address 時,p2沒有例項屬性address,但是有類屬性address,因此返回’Earth’。

可見,當例項屬性和類屬性重名時,例項屬性優先順序高,它將遮蔽掉對類屬性的訪問

當我們把 p1 的 address 例項屬性刪除後,訪問 p1.address 就又返回類屬性的值 ‘Earth’了:

del p1.address
print p1.address
# => Earth

可見,千萬不要在例項上修改類屬性,它實際上並沒有修改類屬性,而是給例項繫結了一個例項屬性。

7. 定義例項方法

一個例項的私有屬性就是以__開頭的屬性,無法被外部訪問,那這些屬性定義有什麼用?

雖然私有屬性無法從外部訪問,但是,從類的內部是可以訪問的。除了可以定義例項的屬性外,還可以定義例項的方法

例項的方法就是在類中定義的函式,它的第一個引數永遠是 self,指向呼叫該方法的例項本身,其他引數和一個普通函式是完全一樣的:

class Person(object):
    def __init__(self, name):
        self.__name = name
    def get_name(self):
        return self.__name

get_name(self) 就是一個例項方法,它的第一個引數是self。__init__(self, name)其實也可看做是一個特殊的例項方法。

呼叫例項方法必須在例項上呼叫:

p1 = Person('Bob')
print p1.get_name()  # self不需要顯式傳入
# => Bob

在例項方法內部,可以訪問所有例項屬性,這樣,如果外部需要訪問私有屬性,可以通過方法呼叫獲得,這種資料封裝的形式除了能保護內部資料一致性外,還可以簡化外部呼叫的難度。

8.方法也是屬性

我們在 class 中定義的例項方法其實也是屬性,它實際上是一個函式物件:

class Person(object):
    def __init__(self, name, score):
        self.name = name
        self.score = score
    def get_grade(self):
        return 'A'

p1 = Person('Bob', 90)
print p1.get_grade
# => <bound method Person.get_grade of <__main__.Person object at 0x109e58510>>
print p1.get_grade()
# => A

也就是說,p1.get_grade 返回的是一個函式物件,但這個函式是一個繫結到例項的函式,p1.get_grade() 才是方法呼叫。

因為方法也是一個屬性,所以,它也可以動態地新增到例項上,只是需要用 types.MethodType() 把一個函式變為一個方法:

import types
def fn_get_grade(self):
    if self.score >= 80:
        return 'A'
    if self.score >= 60:
        return 'B'
    return 'C'

class Person(object):
    def __init__(self, name, score):
        self.name = name
        self.score = score

p1 = Person('Bob', 90)
p1.get_grade = types.MethodType(fn_get_grade, p1, Person)
print p1.get_grade()
# => A
p2 = Person('Alice', 65)
print p2.get_grade()
# ERROR: AttributeError: 'Person' object has no attribute 'get_grade'
# 因為p2例項並沒有繫結get_grade

給一個例項動態新增方法並不常見,直接在class中定義要更直觀。

例項
由於屬性可以是普通的值物件,如 str,int 等,也可以是方法,還可以是函式,大家看看下面程式碼的執行結果,請想一想 p1.get_grade 為什麼是函式而不是方法:

class Person(object):
    def __init__(self, name, score):
        self.name = name
        self.score = score
        self.get_grade = lambda: 'A'

p1 = Person('Bob', 90)
print p1.get_grade
print p1.get_grade()

分析
直接把 lambda 函式賦值給 self.get_grade 和繫結方法有所不同,函式呼叫不需要傳入 self,但是方法呼叫需要傳入 self。

9. 定義類方法

和屬性類似,方法也分例項方法類方法

在class中定義的全部是例項方法,例項方法第一個引數 self 是例項本身。

要在class中定義類方法,需要這麼寫:

class Person(object):
    count = 0
    @classmethod
    def how_many(cls):
        return cls.count
    def __init__(self, name):
        self.name = name
        Person.count = Person.count + 1

print Person.how_many()
p1 = Person('Bob')
print Person.how_many()

通過標記一個@classmethod,該方法將繫結到 Person 類上,而非類的例項。類方法的第一個引數將傳入類本身,通常將引數名命名為cls,上面的 cls.count 實際上相當於 Person.count。

因為是在類上呼叫,而非例項上呼叫,因此類方法無法獲得任何例項變數,只能獲得類的引用。

相關文章