Python教程第八章 類

本系列文章是我學(xué)習(xí)Python3.9的官方tutorial的筆記,大部分來源于官網(wǎng)的中文翻譯,但由于該翻譯有些部分實(shí)在太差和啰嗦,我做了很多刪除和修改,還有部分原文講不明白的,我參考其他資料增加了進(jìn)一步闡述說明。

類提供了一種組合數(shù)據(jù)和功能的方法。 創(chuàng)建一個(gè)新類意味著創(chuàng)建一個(gè)新的對象類型,從而允許創(chuàng)建一個(gè)該類型的新實(shí)例。 每個(gè)類的實(shí)例可以擁有保存自己狀態(tài)的屬性和改變自己狀態(tài)的方法(定義在類中的)。

8.1 Python的作用域


學(xué)習(xí)類之前,先來了解一下Python的作用域,Python的作用域分四層,由內(nèi)到外分別是:

  • local:當(dāng)前函數(shù)局部作用域,函數(shù)范圍內(nèi)定義的局部變量僅在函數(shù)范圍內(nèi)可見
  • nonlocal:非本地作用域,即當(dāng)前函數(shù)的所有外層函數(shù)(可以是很多層嵌套)的局部變量
  • global:當(dāng)前模塊的全局作用域,執(zhí)行腳本或交互式執(zhí)行的當(dāng)前模塊是_main_
  • built-in:內(nèi)置作用域,即Python解釋器的內(nèi)置變量
    內(nèi)層作用域的變量外場不可見,外層作用域的變量在內(nèi)層默認(rèn)是readonly的,如果需要修改外層作用域的變量,需要對變量進(jìn)行g(shù)lobal或者nonlocal的聲明,如果沒有聲明,賦值效果僅僅是創(chuàng)建一個(gè)內(nèi)層局部變量而已并不會修改外層變量。
    用例子來說明這幾個(gè)作用域的區(qū)別以及global和nolocal關(guān)鍵字的用法會更加清晰。
>>> gcount = 0
>>> def global_test():
...     gcount+=1
...     print (gcount)
>>> global_test()
Traceback (most recent call last):
  File "<pyshell#6>", line 1, in <module>
    global_test()
  File "<pyshell#5>", line 2, in global_test
    gcount+=1
UnboundLocalError: local variable 'gcount' referenced before assignment

上面程序報(bào)錯(cuò),是因?yàn)榈谝恍械膅count是全局變量 (此處可以省略global關(guān)鍵字),在global_test()函數(shù)內(nèi)部對它只能讀,程序中顯然對gcount進(jìn)行了賦值修改,所以解釋器函數(shù)內(nèi)的gcount是local變量,因?yàn)?=是一個(gè)先讀后加的操作,因此會報(bào)local變量在賦值前先被引用的錯(cuò)誤。
在函數(shù)內(nèi)加上global聲明即可正常運(yùn)行:

>>> gcount = 0
>>> def global_test():
...     global gcount
...     gcount +=1
...     print (gcount)
>>> global_test()
1

在如果只是讀全局變量,則不需要global聲明也可以正常使用:

>>> gcount = 0
>>> def global_test():
...     print (gcount)
>>> global_test()
0

nonlocal關(guān)鍵字用來在函數(shù)中使用外層但非全局的變量:

def scope_test():
    spam = "test spam" # 此處聲明的spam對下面幾個(gè)函數(shù)來說屬于nonlocal變量
    def do_local():
        spam = "local spam"
    def do_nonlocal():
        nonlocal  spam
        spam = "nonlocal spam"
    def do_global():
        global spam
        spam = "global spam"
    do_local()
    print("After local assignmane:", spam)
    do_nonlocal()
    print("After nonlocal assignment:",spam)
    do_global()
    print("After global assignment:",spam)

scope_test()
print("In global scope:",spam)


outputs: 
After local assignmane: test spam # local改不了nonlocal變量
After nonlocal assignment: nonlocal spam # nonlocal聲明可以改nonlocal變量
After global assignment: nonlocal spam # global聲明只能改global變量改不了nonlocal變量
In global scope: global spam # global變量賦值成功

8.2 初探類


8.2.1 類定義語法

最簡單的類定義看起來像這樣:

class ClassName:
    <statement-1>
    .
    .
    .
    <statement-N>

當(dāng)進(jìn)入類定義時(shí),將創(chuàng)建一個(gè)新的命名空間,并將其用作局部作用域 --- 因此,所有對局部變量的賦值都是在這個(gè)作用域之內(nèi)。 包括函數(shù)定義也在這個(gè)這個(gè)作用域內(nèi)綁定函數(shù)名稱。
類定義被執(zhí)行后,會創(chuàng)建一個(gè)類對象,即這個(gè)類定義的命名空間或者作用域的一個(gè)包裝。

8.2.2 類對象

類對象支持兩種操作:屬性引用和實(shí)例化。
屬性引用 使用的語法為: obj.name。 有效的屬性名稱是類對象被創(chuàng)建時(shí)存在于類命名空間中的所有名稱。 因此,如果類定義是這樣的:

class MyClass:
    """A simple example class"""
    i = 12345

    def f(self):
        return 'hello world'

那么 MyClass.i 和 MyClass.f 就是有效的屬性引用,將分別返回一個(gè)整數(shù)和一個(gè)函數(shù)對象。 類屬性也可以被賦值,因此可以通過賦值來更改 MyClass.i 的值。 doc 也是一個(gè)有效的屬性,將返回所屬類的文檔字符串: "A simple example class"。
類的 實(shí)例化 使用函數(shù)表示法。 可以把類對象視為是返回該類的一個(gè)新實(shí)例的不帶參數(shù)的函數(shù)。 舉例來說(假設(shè)使用上述的類):

x = MyClass()

創(chuàng)建類的新 實(shí)例 并將此對象分配給局部變量 x。
實(shí)例化操作(“調(diào)用”類對象)會創(chuàng)建一個(gè)空對象。 如果需要?jiǎng)?chuàng)建帶有特定初始狀態(tài)的自定義實(shí)例,可以在類定義中包含一個(gè)名為 __init__() 的特殊方法,就像這樣:

def __init__(self):
    self.data = []

當(dāng)一個(gè)類定義了 __init__() 方法時(shí),類的實(shí)例化操作會自動為新創(chuàng)建的類實(shí)例發(fā)起調(diào)用 __init__()。
當(dāng)然,__init__() 方法還可以有額外參數(shù)以實(shí)現(xiàn)更高靈活性。 在這種情況下,提供給類實(shí)例化運(yùn)算符的參數(shù)將被傳遞給 __init__()。 例如,:

>>> class Complex:
...     def __init__(self, realpart, imagpart):
...         self.r = realpart
...         self.i = imagpart
...
>>> x = Complex(3.0, -4.5)
>>> x.r, x.i
(3.0, -4.5)

8.2.3 函數(shù)對象和方法對象

繼續(xù)用這個(gè)例子來說明:

class MyClass:
    """A simple example class"""
    i = 12345

    def f(self):
        return 'hello world'

MyClass是一個(gè)類對象,而x = MyClass()即利用類對象創(chuàng)建了一個(gè)MyClass的實(shí)例對象并賦值給x。對于類對象MyClass來說,里面定義了屬性f是一個(gè)函數(shù)對象,可以通過MyClass.f(x)的方式來調(diào)用;對于x這個(gè)實(shí)例對象,我們定義里面的屬性f為方法對象,可以直接通過x.f()來調(diào)用。注意區(qū)別,x.f()調(diào)用時(shí)并沒有傳遞第一個(gè)參數(shù)self,實(shí)際上對于每個(gè)實(shí)例方法的調(diào)用,Python都會自動將調(diào)用者(即.前面的x)作為第一個(gè)參數(shù)自動傳入,底層調(diào)用的依然是類對象MyClass.f(x)這個(gè)函數(shù)。有點(diǎn)繞,簡單的說就是x.f()等價(jià)與MyClass.f(x),其中x是MyClass的實(shí)例。

8.2.4 類變量和實(shí)例變量

一般來說,實(shí)例變量是每個(gè)實(shí)例獨(dú)有的數(shù)據(jù),而類變量則是該類所有實(shí)例共享的,看例子:

class Dog:

    kind = 'canine'         # 類變量

    def __init__(self, name):
        self.name = name    # 實(shí)例變量

>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.kind                  # shared by all dogs
'canine'
>>> e.kind                  # shared by all dogs
'canine'
>>> d.name                  # unique to d
'Fido'
>>> e.name                  # unique to e
'Buddy'

共享數(shù)據(jù)可能在涉及mutable對象的時(shí)候?qū)е铝钊梭@訝的結(jié)果。 例如以下代碼中的 tricks 列表不應(yīng)該被用作類變量,因?yàn)樗械?Dog 實(shí)例將只共享一個(gè)單獨(dú)的列表:

class Dog:

    tricks = []             # mistaken use of a class variable

    def __init__(self, name):
        self.name = name

    def add_trick(self, trick):
        self.tricks.append(trick)

>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.add_trick('roll over')
>>> e.add_trick('play dead')
>>> d.tricks                # unexpectedly shared by all dogs
['roll over', 'play dead']

正確的類設(shè)計(jì)應(yīng)該使用實(shí)例變量:

class Dog:

    def __init__(self, name):
        self.name = name
        self.tricks = []    # creates a new empty list for each dog

    def add_trick(self, trick):
        self.tricks.append(trick)

>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.add_trick('roll over')
>>> e.add_trick('play dead')
>>> d.tricks
['roll over']
>>> e.tricks
['play dead']

如果同樣的屬性名稱同時(shí)出現(xiàn)在實(shí)例和類中,則屬性查找會優(yōu)先選擇實(shí)例:

>>> class Warehouse:
        purpose = 'storage'
        region = 'west'

>>> w1 = Warehouse()
>>> print(w1.purpose, w1.region)
storage west
>>> w2 = Warehouse()
>>> w2.region = 'east'
>>> print(w2.purpose, w2.region)
storage east

另外要注意,每個(gè)值都是一個(gè)對象,因此具有相應(yīng)的類或者說類型,存儲在object._class_中 。

8.3 繼承


派生類或者叫子類的定義語法如下:

class DerivedClassName(BaseClassName):
    <statement-1>
    .
    .
    .
    <statement-N>

派生類定義的執(zhí)行過程與基類相同。 當(dāng)構(gòu)造類對象時(shí),基類會被記住。 此信息將被用來解析屬性引用:如果請求的屬性在派生類中找不到,搜索將轉(zhuǎn)往基類中進(jìn)行查找。 如果基類本身也派生自其他某個(gè)類,則此規(guī)則將被遞歸地應(yīng)用。
派生類的實(shí)例化沒有任何特殊之處: DerivedClassName() 會創(chuàng)建該類的一個(gè)新實(shí)例。
派生類可能會重寫其基類的方法,將覆蓋基類的方法,盡管是基類的方法在調(diào)用該對象的其他基類方法時(shí),最終也可能會調(diào)用覆蓋它的派生類的方法。

def Animal:
    def name(self):
        return 'animal'
    def talk(self):
        print('I am ', self.name())

def Cat(Animal):
    def name(self):
        return 'cat'

c = Cat()
c.talk()

output:
I am cat

在派生類中的重寫基類方法可能仍需要調(diào)用基類的方法時(shí),可以通過BaseClassName.methodname(self, arguments)類對象的方式調(diào)用。

Python有兩個(gè)內(nèi)置函數(shù)可被用于繼承機(jī)制:

  • 使用 isinstance() 來檢查一個(gè)實(shí)例的類型: isinstance(obj, int) 僅會在 obj.__class__int 或某個(gè)派生自 int 的類時(shí)為 True
  • 使用 issubclass() 來檢查類的繼承關(guān)系: issubclass(bool, int)True,因?yàn)?boolint 的子類。 但是,issubclass(float, int)False,因?yàn)?float 不是 int 的子類。

8.3.1 多重繼承

Python 也支持多重繼承。 帶有多個(gè)基類的類定義語句如下所示:

class DerivedClassName(Base1, Base2, Base3):
    <statement-1>
    .
    .
    .
    <statement-N>

大多數(shù)情況下,你可以認(rèn)為搜索從父類所繼承屬性的操作是深度優(yōu)先、從左至右的。因此,如果某一屬性在 DerivedClassName 中未找到,則會到 Base1 中搜索它,然后(遞歸地)到 Base1 的基類中搜索,如果在那里未找到,再到 Base2 中搜索,依此類推。
而實(shí)際情況比這個(gè)更復(fù)雜一些,方法解析順序會動態(tài)改變以支持對 super() 的協(xié)同調(diào)用。
動態(tài)改變順序是有必要的,因?yàn)樗卸嘀乩^承的情況都會顯示出一個(gè)或更多的菱形關(guān)聯(lián)(即至少有一個(gè)父類可通過多條路徑被最底層類所訪問)。 例如,所有類都是繼承自 object,因此任何多重繼承的情況都提供了一條以上的路徑可以通向 object。 為了確?;惒粫辉L問一次以上,動態(tài)算法會用一種特殊方式將搜索順序線性化, 保留每個(gè)類所指定的從左至右的順序,只調(diào)用每個(gè)父類一次,并且保持單調(diào)(即一個(gè)類可以被子類化而不影響其父類的優(yōu)先順序)。

8.4 私有變量


原則上僅限實(shí)例內(nèi)部訪問的‘私有變量’在Python中時(shí)不存在的,但大多數(shù)Python代碼遵循這樣一個(gè)規(guī)則:帶有一個(gè)下劃線的名稱 (例如 _spam) 應(yīng)該被當(dāng)作是 API 的非公有部分 (無論它是函數(shù)、方法或是數(shù)據(jù)成員)。
由于存在對于類私有成員的有效使用場景(例如避免名稱與子類所定義的名稱相沖突),因此存在對此種機(jī)制的有限支持,稱為 名稱改寫。 任何形式為 __spam 的標(biāo)識符(帶有兩個(gè)前綴下劃線)將被替換為 _classname__spam,其中 classname 為的當(dāng)前類名稱。
名稱改寫有助于讓子類重載方法而不破壞類內(nèi)方法調(diào)用。例如:

class Mapping:
    def __init__(self, iterable):
        self.items_list = []
        self.__update(iterable)

    def update(self, iterable):
        for item in iterable:
            self.items_list.append(item)

    __update = update   # private copy of original update() method

class MappingSubclass(Mapping):

    def update(self, keys, values):
        # provides new signature for update()
        # but does not break __init__()
        for item in zip(keys, values):
            self.items_list.append(item)

上面的示例即使在 MappingSubclass 引入了一個(gè) __update 標(biāo)識符的情況下也不會出錯(cuò),因?yàn)樗鼤?Mapping 類中被替換為 _Mapping__update 而在 MappingSubclass 類中被替換為 _MappingSubclass__update。
請注意,改寫規(guī)則的設(shè)計(jì)主要是為了避免意外沖突;訪問或修改私有變量仍然是可能的。這在特殊情況下甚至?xí)苡杏?,例如在調(diào)試器中。

8.5 迭代器


您可能已經(jīng)注意到大多數(shù)容器對象都可以使用 for 語句:

for element in [1, 2, 3]:
    print(element)
for element in (1, 2, 3):
    print(element)
for key in {'one':1, 'two':2}:
    print(key)
for char in "123":
    print(char)
for line in open("myfile.txt"):
    print(line, end='')

在背后,for 語句會對容器對象調(diào)用 iter()。 該函數(shù)返回一個(gè)定義了 __next__() 方法的迭代器對象,此方法將逐一訪問容器中的元素。 當(dāng)元素用盡時(shí),__next__() 將引發(fā) StopIteration 異常來通知終止 for 循環(huán)。 你可以使用 next() 內(nèi)置函數(shù)來調(diào)用 __next__() 方法;這個(gè)例子顯示了它的運(yùn)作方式:

>>> s = 'abc'
>>> it = iter(s)
>>> it
<iterator object at 0x00A1DB50>
>>> next(it)
'a'
>>> next(it)
'b'
>>> next(it)
'c'
>>> next(it)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
    next(it)
StopIteration

看過迭代器協(xié)議的幕后機(jī)制,給你的類添加迭代器行為就很容易了。 定義一個(gè) __iter__() 方法來返回一個(gè)帶有 __next__() 方法的對象。 如果類已定義了 __next__(),則 __iter__() 可以簡單地返回 self:

class Reverse:
    """Iterator for looping over a sequence backwards."""
    def __init__(self, data):
        self.data = data
        self.index = len(data)

    def __iter__(self):
        return self

    def __next__(self):
        if self.index == 0:
            raise StopIteration
        self.index = self.index - 1
        return self.data[self.index]
>>> rev = Reverse('spam')
>>> iter(rev)
<__main__.Reverse object at 0x00A1DB50>
>>> for char in rev:
...     print(char)
...
m
a
p
s

8.6 生成器


生成器 是一個(gè)用于創(chuàng)建迭代器的簡單而強(qiáng)大的工具。 它們的寫法類似于標(biāo)準(zhǔn)的函數(shù),但當(dāng)它們要返回?cái)?shù)據(jù)時(shí)會使用 yield 語句。 每次在生成器上調(diào)用 next() 時(shí),它會從上次離開的位置恢復(fù)執(zhí)行(它會記住上次執(zhí)行語句時(shí)的所有數(shù)據(jù)值)。 示例如下:

def reverse(data):
    for index in range(len(data)-1, -1, -1):
        yield data[index]
>>> for char in reverse('golf'):
...     print(char)
...
f
l
o
g

生成器完成的操作同樣可以用前一節(jié)所描述的基于類的迭代器來完成。 但生成器的寫法更為緊湊,因?yàn)樗鼤詣觿?chuàng)建 __iter__()__next__() 方法。
另一個(gè)關(guān)鍵特性在于局部變量和執(zhí)行狀態(tài)會在每次調(diào)用之間自動保存。 這使得該函數(shù)相比使用 self.index 和 self.data 這種實(shí)例變量的方式更易編寫且更為清晰。
除了會自動創(chuàng)建方法和保存程序狀態(tài),當(dāng)生成器終結(jié)時(shí),它們還會自動引發(fā) StopIteration。
這些特性結(jié)合在一起,使得創(chuàng)建迭代器能與編寫常規(guī)函數(shù)一樣容易。

8.7 生成器表達(dá)式


某些簡單的生成器可以寫成簡潔的表達(dá)式代碼,所用語法類似列表推導(dǎo)式,但外層為圓括號而非方括號。 這種表達(dá)式被設(shè)計(jì)用于生成器將立即被外層函數(shù)所使用的情況。 生成器表達(dá)式相比完整的生成器更緊湊但較不靈活,相比等效的列表推導(dǎo)式則更為節(jié)省內(nèi)存。

>>> sum(i*i for i in range(10))                 # sum of squares
285

>>> xvec = [10, 20, 30]
>>> yvec = [7, 5, 3]
>>> sum(x*y for x,y in zip(xvec, yvec))         # dot product
260

>>> unique_words = set(word for line in page  for word in line.split())

>>> valedictorian = max((student.gpa, student.name) for student in graduates)

>>> data = 'golf'
>>> list(data[i] for i in range(len(data)-1, -1, -1))
['f', 'l', 'o', 'g']
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容