Python基礎之:Python中的類

flydean 發表於 2021-04-02
Python

簡介

class是物件導向程式設計的一個非常重要的概念,python中也有class,並且支援物件導向程式設計的所有標準特性:繼承,多型等。

本文將會詳細講解Python中class的資訊。

作用域和名稱空間

在詳細講解class之前,我們來看一下作用域和名稱空間的概念。

名稱空間(Namespace)是從名稱到物件的對映,大部分的名稱空間都是通過 Python 字典來實現的。

名稱空間主要是為了避免程式中的名字衝突。只要名字在同一個名稱空間中保持唯一即可,不同的命令空間中的名字互不影響。

Python中有三種名稱空間:

  • 內建名稱(built-in names), Python 語言內建的名稱,比如函式名 abs、char 和異常名稱 BaseException、Exception 等等。
  • 全域性名稱(global names),模組中定義的名稱,記錄了模組的變數,包括函式、類、其它匯入的模組、模組級的變數和常量。
  • 區域性名稱(local names),函式中定義的名稱,記錄了函式的變數,包括函式的引數和區域性定義的變數。(類中定義的也是)

名稱空間的搜尋順序是 區域性名稱-》全域性名稱-》內建名稱。

在不同時刻建立的名稱空間擁有不同的生存期。包含內建名稱的名稱空間是在 Python 直譯器啟動時建立的,永遠不會被刪除。模組的全域性名稱空間是在在模組定義被讀入時建立.

通常,模組名稱空間也會持續到直譯器退出。

被直譯器的頂層呼叫執行的語句,比如從一個指令碼檔案讀取的程式或互動式地讀取的程式,被認為是 __main__ 模組呼叫的一部分,因此它們也擁有自己的全域性名稱空間。(內建名稱實際上也存在於一個模組中;這個模組稱作 builtins 。)

一個 作用域 是一個名稱空間可直接訪問的 Python 程式的文字區域。

Python中有四種作用域:

  • Local:最內層,包含區域性變數,比如一個函式/方法內部。
  • Enclosing:包含了非區域性(non-local)也非全域性(non-global)的變數。比如兩個巢狀函式,一個函式(或類) A 裡面又包含了一個函式 B ,那麼對於 B 中的名稱來說 A 中的作用域就為 nonlocal。
  • Global:當前指令碼的最外層,比如當前模組的全域性變數。
  • Built-in: 包含了內建的變數/關鍵字等。,最後被搜尋

作用域的搜尋順序是 Local -> Enclosing -> Global -> Built-in

Python中用nonlocal關鍵字宣告為Enclosing範圍,用global關鍵字宣告為全域性範圍。

我們來看一個global 和 nonlocal 會如何影響變數繫結的例子:

def scope_test():
    def do_local():
        spam = "local spam"

    def do_nonlocal():
        nonlocal spam
        spam = "nonlocal spam"

    def do_global():
        global spam
        spam = "global spam"

    spam = "test spam"
    do_local()
    print("After local assignment:", spam)
    do_nonlocal()
    print("After nonlocal assignment:", spam)
    do_global()
    print("After global assignment:", spam)

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

上面程式輸出:

After local assignment: test spam
After nonlocal assignment: nonlocal spam
After global assignment: nonlocal spam
In global scope: global spam

函式內的變數預設是local作用域,如果要在函式的函式中修改外部函式的變數,那麼需要將這個變數宣告為nonlocal, 最後在模組頂層或者程式檔案頂層的變數是全域性作用域,如果需要引用修改的話需要宣告為global作用域。

class

Python中的類是用class來定義的,我們看一個最簡單的class定義:

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

類定義中的程式碼將建立一個新的名稱空間,裡面的變數都被看做是區域性作用域。所有對區域性變數的賦值都是在這個新名稱空間之內。

類物件

class定義類之後,就會生成一個類物件。我們可以通過這個類物件來訪問類中定義的屬性和方法。

比如我們定義了下面的類:

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

    def f(self):
        return 'hello world'

類中定義了一個屬性 i 和一個方法 f。那麼我們可以通過 MyClass.iMyClass.f 來訪問他們。

注意,Python中沒有像java中的private,public這一種變數訪問範圍控制。你可以把Python class中的變數和方法都看做是public的。

我們可以直接通過給 MyClass.i 賦值來改變 i 變數的值。

In [2]: MyClass.__doc__
Out[2]: 'A simple example class'

In [3]: MyClass.i=100

In [4]: MyClass
Out[4]: __main__.MyClass

In [5]: MyClass.i
Out[5]: 100

Class中,我們還定義了class的文件,可以直接通過 __doc__ 來訪問。

類的例項

例項化一個類物件,可以將類看做是無參的函式即可。

In [6]: x = MyClass()

In [7]: x.i
Out[7]: 100

上面我們建立了一個MyClass的例項,並且賦值給x。

通過訪問x中的i值,我們可以發現這個i值是和MyClass類變數中的i值是一致的。

例項化操作(“呼叫”類物件)會建立一個空物件。 如果你想在例項化的時候做一些自定義操作,那麼可以在類中定義一個 __init__() 方法時,類的例項化操作會自動為新建立的類例項發起呼叫 __init__()

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

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

例項物件的屬性

還是上面class,我們定義了一個i屬性和一個f方法:

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

    def f(self):
        return 'hello world'

我們可以通過例項物件來訪問這個屬性:

In [6]: x = MyClass()

In [7]: x.i
Out[7]: 100

甚至我們可以在例項物件中建立一個不屬於類物件的屬性:

In [8]: x.y=200

In [9]: x.y
Out[9]: 200

甚至使用完之後,不保留任何記錄:

x.counter = 1
while x.counter < 10:
    x.counter = x.counter * 2
print(x.counter)
del x.counter

方法物件

我們有兩種方式來訪問函式中定義的方法,一種是通過類物件,一種是通過例項物件,看下兩者有什麼不同:

In [10]: x.f
Out[10]: <bound method MyClass.f of <__main__.MyClass object at 0x7fb69fc5f438>>

In [11]: x.f()
Out[11]: 'hello world'

In [12]:  MyClass.f
Out[12]: <function __main__.MyClass.f>

In [13]:  MyClass.f()
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-13-e50d25278077> in <module>()
----> 1 MyClass.f()

TypeError: f() missing 1 required positional argument: 'self'

從上面的輸出我們可以看出,MyClass.f 是一個函式,而x.f 是一個object物件。

還記得f方法的定義嗎?f方法有一個self引數,如果作為函式來呼叫的話,一定要傳入所有需要的引數才可以,這也就是為什麼直接呼叫MyClass.f() 報錯,而 x.f() 可以直接執行的原因。

雖然方法的第一個引數常常被命名為 self。 這也不過就是一個約定: self 這一名稱在 Python 中絕對沒有特殊含義。

方法物件的特殊之處就在於例項物件會作為函式的第一個引數被傳入。 在我們的示例中,呼叫 x.f() 其實就相當於 MyClass.f(x)。 總之,呼叫一個具有 n 個引數的方法就相當於呼叫再多一個引數的對應函式,這個引數值為方法所屬例項物件,位置在其他引數之前。

為什麼方法物件不需要傳入self這個引數呢?從 x.f的輸出我們可以看出,這個方法已經繫結到了一個例項物件,所以self引數會被自動傳入。

方法可以通過使用 self 引數的方法屬性呼叫其他方法:

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

    def add(self, x):
        self.data.append(x)

    def addtwice(self, x):
        self.add(x)
        self.add(x)

類變數和例項變數

在類變數和例項變數的使用中,我們需要注意哪些問題呢?

一般來說,例項變數用於每個例項的唯一資料,而類變數用於類的所有例項共享的屬性和方法。

class Dog:

    kind = 'canine'         # class variable shared by all instances

    def __init__(self, name):
        self.name = name    # instance variable unique to each instance

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

所以,如果是例項變數,那麼需要在初始化方法中進行賦值和初始化。如果是類變數,可以直接定義在類的結構體中。

舉個正確使用例項變數的例子:

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

如果同樣的屬性名稱同時出現在例項和類中,則屬性查詢會優先選擇例項:

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

繼承

看下Python中繼承的語法:

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

如果基類定義在另一個模組中的時候:

class DerivedClassName(modname.BaseClassName):

如果請求的屬性在類中找不到,搜尋將轉往基類中進行查詢。 如果基類本身也派生自其他某個類,則此規則將被遞迴地應用。

派生類可能會重寫其基類的方法。 因為方法在呼叫同一物件的其他方法時沒有特殊許可權,所以呼叫同一基類中定義的另一方法的基類方法最終可能會呼叫覆蓋它的派生類的方法。

Python中有兩個內建函式可以用來方便的判斷是繼承還是例項:

  • 使用 isinstance() 來檢查一個例項的型別:

    例如:isinstance(obj, int) 僅會在 obj.__class__ 為 int 或某個派生自 int 的類時為 True。

  • 使用 issubclass() 來檢查類的繼承關係:

    例如: issubclass(bool, int) 為 True,因為 bool 是 int 的子類。 但是,issubclass(float, int) 為 False,因為 float 不是 int 的子類。

Python也支援多重繼承:

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

如果某一屬性在 DerivedClassName 中未找到,則會到 Base1 中搜尋它,然後(遞迴地)到 Base1 的基類中搜尋,如果在那裡未找到,再到 Base2 中搜尋,依此類推。

私有變數

雖然Python中並沒有強制的語法規定私有變數,但是大多數 Python 程式碼都遵循這樣一個約定:帶有一個下劃線的名稱 (例如 _spam) 應該被當作是 API 的非公有部分 (無論它是函式、方法或是資料成員)。

這只是我們在寫Python程式時候的一個實現細節,並不是語法的強制規範。

既然有私有變數,那麼在繼承的情況下就有可能出現私有變數覆蓋的情況,Python是怎麼解決的呢?

Python中可以通過變數名改寫的方式來避免私有變數的覆蓋。

任何形式為 __spam 的識別符號(至少帶有兩個字首下劃線,至多一個字尾下劃線)的文字將被替換為 _classname__spam,其中 classname 為去除了字首下劃線的當前類名稱。 這種改寫不考慮識別符號的句法位置,只要它出現在類定義內部就會進行。

舉個例子:


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 引入了一個 __update 識別符號的情況下也不會出錯,因為它會在 Mapping 類中被替換為 _Mapping__update 而在 MappingSubclass 類中被替換為 _MappingSubclass__update

請注意傳遞給 exec()eval() 的程式碼不會將發起呼叫類的類名視作當前類;這類似於 global 語句的效果,因此這種效果僅限於同時經過位元組碼編譯的程式碼。

迭代器

對於大多數容器物件來說,可以使用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 語句會在容器物件上呼叫 iter()方法。 該函式返回一個定義了 __next__() 方法的迭代器物件,此方法將逐一訪問容器中的元素。 當元素用盡時,__next__() 將引發 StopIteration 異常來通知終止 for 迴圈。

你可以使用 next() 內建函式來呼叫 __next__() 方法;下面的例子展示瞭如何使用:

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

知道了迭代器的原理之後,我們就可以為自定義的class新增迭代器物件了,我們需要定義一個 __iter__() 方法來返回一個帶有 __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]

生成器

生成器 是一個用於建立迭代器的簡單而強大的工具。 它們的寫法類似於標準的函式,但當它們要返回資料時會使用 yield 語句。 每次在生成器上呼叫 next() 時,它會從上次離開的位置恢復執行(它會記住上次執行語句時的所有資料值)。

看一個生成器的例子:


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

可以用生成器來完成的操作同樣可以用前一節所描述的基於類的迭代器來完成。 但生成器的寫法更為緊湊,因為它會自動建立 __iter__()__next__() 方法。

生成器還可以用表示式程式碼的方式來執行,這樣的寫法和列表推導式類似,但外層為圓括號而非方括號。

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

本文已收錄於 http://www.flydean.com/10-python-class/

最通俗的解讀,最深刻的乾貨,最簡潔的教程,眾多你不知道的小技巧等你來發現!

歡迎關注我的公眾號:「程式那些事」,懂技術,更懂你!