天天看點

Python基礎之:Python中的類

文章目錄

  • 簡介
  • 作用域和命名空間
  • class
    • 執行個體對象的屬性
    • 方法對象
    • 類對象
    • 類的執行個體
    • 類變量和執行個體變量
    • 繼承
    • 私有變量
  • 疊代器
  • 生成器

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作用域。

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

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

類定義中的代碼将建立一個新的命名空間,裡面的變量都被看做是局部作用域。所有對局部變量的指派都是在這個新命名空間之内。

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

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

class MyClass:"""A simple example class"""i = 12345def f(self):return 'hello world'      

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

MyClass.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=100In [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 = 12345def f(self):return 'hello world'      

我們可以通過執行個體對象來通路這個屬性:

In [6]: x = MyClass()In [7]: x.i
Out[7]: 100      

甚至我們可以在執行個體對象中建立一個不屬于類對象的屬性:

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

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

x.counter = 1while x.counter < 10:x.counter = x.counter * 2print(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 instancesdef __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 dogdef 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 selfdef __next__(self):if self.index == 0:raise StopIteration
        self.index = self.index - 1return 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 squares285>>> xvec = [10, 20, 30]>>> yvec = [7, 5, 3]>>> sum(x*y for x,y in zip(xvec, yvec))         # dot product260>>> 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']      

繼續閱讀