在本章開始之前,我們需要先了解面向對象(OOP,Object Oriented Programming)的概念。如果學習過其他面向對象的編程語言,那麼可以跳過這部分。
面向對象是和面向過程相對的。以佩奇去吃飯為例:
面向過程是這樣的:
去食堂(...)
買飯(...)
吃飯(...)
面向對象是這樣的:
佩奇 = 豬()
佩奇.移動(食堂)
面條 =佩奇.購買(面條)
佩奇.吃(面條)
可以看到,面向對象的一個突出特點就是操作對象來實現功能。
類是一類事物的抽象,描述了一類事物應該有的屬性和功能。
我們可以把類看成是屬性和功能(函數)的結合。例如,狗具有年齡、體重、名字等屬性,有發出叫聲、吃飯、奔跑等功能。
而現實中的事物往往是具體的,例如一條名字為"旺旺",年齡3年,體重10kg的小狗。這種具體的東西我們叫做類的實例。
面向對象有一些特定的術語,如類、方法、數據成員等,Python3 面向對象 | 菜鳥教程 (runoob.com)。可以搜索面向對象等關鍵字找到這些概念。本文不去講解這些術語,盡量通過案例講解類的作用。
最簡單的定義類的語法:
class 類名:
一些函數、語句
通常,我們使用初始化函數定義類的屬性,並定義一些功能函數實現類的功能。
初始化函數是一個名稱為__init__()
特殊方法,可以有參數且第一個參數總是self
(約定俗成的,並不是語法規定)。如果設置了初始化方法,調用類名(參數)
時就會自動調用該方法。
self
參數代表實例,通過實例調用方法自動傳入實例對象作為self參數。帶有self參數的方法通常稱為實例方法。
class Dog:
def __init__(self, name="無名", age=0, weight=0):
self.name = name
self.age = age
self.weight = weight
def bark(self):
print("汪汪汪")
def growup(self):
self.age += 1
定義類之後,使用類一般是創建實例對象,通過實例對象進行操作。
ww = Dog("旺旺",0,1) # 使用類名(參數),創建實例對象
print(ww.age) # 使用 實例對象.屬性, 訪問屬性
ww.growup() # 使用實例對象.方法,調用方法。
# ww作為self參數,傳入growup(self)方法
ww.bark()
print(ww.age)
類可以繼承自其它類,被繼承的叫做基類(或父類),繼承者叫做派生類(或子類)。通過繼承子類可以擁有父類的方法和屬性。例如,上面的Dog
類是一種動物,那麼就可以通過繼承Animal
類獲得Animal
的屬性,重量,年齡等;擁有動物的方法,長大等。
這樣做的好處在類少的時候不那麼明顯,當類多了之後,例如我們繼續創建貓類,鳥類,魚類…之後,我們通過繼承就可以減少很多重復代碼。
繼承的語法:
class 派生類(基類):
一些語句
class Animal:
def __init__(self,name="無名", age=0, weight=0):
self.name = name
self.age = age
self.weight = weight
def growup(self):
self.age += 1
class Dog(Animal):
def __init__(self, name="無名", age=0, weight=0):
super().__init__(name,age,weight)#使用super().調用父類方法
def bark(self):
print("汪汪汪")
ww = Dog("旺旺",0,1)
print(ww.age)
ww.bark()
ww.growup() # growup方法 繼承自Animal
print(ww.age)
print("汪汪汪")
注釋:Python 3 可以使用直接使用 super().xxx
代替 super(Class, self).xxx
:
派生類的執行過程:
派生類定義的執行過程與基類相同。 當構造類對象時,基類會被記住。 此信息將被用來解析屬性引用:如果請求的屬性在類中找不到,搜索將轉往基類中進行查找。 如果基類本身也派生自其他某個類,則此規則將被遞歸地應用。
派生類的實例化沒有任何特殊之處: DerivedClassName()
會創建該類的一個新實例。 方法引用將按以下方式解析:搜索相應的類屬性,如有必要將按基類繼承鏈逐步向下查找,如果產生了一個函數對象則方法引用就生效。
派生類可能會重寫其基類的方法。 因為方法在調用同一對象的其他方法時沒有特殊權限,所以調用同一基類中定義的另一方法的基類方法最終可能會調用覆蓋它的派生類的方法。
在派生類中的重載方法實際上可能想要擴展而非簡單地替換同名的基類方法。 有一種方式可以簡單地直接調用基類方法:即調用 BaseClassName.methodname(self, arguments)
。 有時這對客戶端來說也是有用的。 (請注意僅當此基類可在全局作用域中以 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 派生類(基類1, 基類2, 基類3):
一些語句
#需要用 基類1.方法 來調用基類方法
例如:
class A:
def __init__(self):
self.aname = 'a'
class B:
def __init__(self):
self.bname = 'b'
class C(A, B):
def __init__(self):
#super().__init__()
# 如果用super().方法()來調用父類方法,將按照順序向上找到第一個符合條件的父類
A.__init__(self)
B.__init__(self)
cc = C()
print(cc.aname, cc.bname)
對於多數應用來說,在最簡單的情況下,你可以認為搜索從父類所繼承屬性的操作是深度優先、從左至右的,當層次結構中存在重疊時不會在同一個類中搜索兩次。 因此,如果某一屬性在 DerivedClassName
中未找到,則會到 Base1
中搜索它,然後(遞歸地)到 Base1
的基類中搜索,如果在那裡未找到,再到 Base2
中搜索,依此類推。
真實情況比這個更復雜一些;方法解析順序會動態改變以支持對 super()
的協同調用。 這種方式在某些其他多重繼承型語言中被稱為後續方法調用,它比單繼承型語言中的 super 調用更強大。
動態改變順序是有必要的,因為所有多重繼承的情況都會顯示出一個或更多的菱形關聯(即至少有一個父類可通過多條路徑被最底層類所訪問)。 例如,所有類都是繼承自 object
,因此任何多重繼承的情況都提供了一條以上的路徑可以通向 object
。 為了確保基類不會被訪問一次以上,動態算法會用一種特殊方式將搜索順序線性化, 保留每個類所指定的從左至右的順序,只調用每個父類一次,並且保持單調(即一個類可以被子類化而不影響其父類的優先順序)。 總而言之,這些特性使得設計具有多重繼承的可靠且可擴展的類成為可能。 要了解更多細節,請參閱 The Python 2.3 Method Resolution Order | Python.org。
實例變量屬於實例,每個實例單獨擁有,
類變量屬於類, 類的所有實例共享。
如果同樣的屬性名稱同時出現在實例和類中,則屬性查找會優先選擇實例屬性。
class Dog:
kind = 'canine' # 類變量,所有實例共享
def __init__(self, name):
self.name = name # 實例變量,每個實例單獨有自己的
>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.kind # 所有狗共享
'canine'
>>> e.kind # 所有狗共享
'canine'
>>> d.name # d獨有
'Fido'
>>> e.name # e獨有
'Buddy'
python中沒有類似java或C++那樣用private
限定的、只能從內部訪問的私有變量。
但是,大多數 Python 代碼都遵循這樣一個約定:帶有一個下劃線的名稱 (例如 _spam
) 應該被當作是 API 的非公有部分 (無論它是函數、方法或是數據成員)。
名稱改寫: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
語句的效果,因此這種效果僅限於同時經過字節碼編譯的代碼。 同樣的限制也適用於 getattr()
, setattr()
和 delattr()
,以及對於 __dict__
的直接引用。
有時會需要使用類似於 C 的“struct”這樣的數據類型,將一些命名數據項捆綁在一起。 這種情況適合定義一個空類:
class Employee:
pass
john = Employee() # Create an empty employee record
# Fill the fields of the record
john.name = 'John Doe'
john.dept = 'computer lab'
john.salary = 1000
大多數容器都可以使用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__()
方法將逐一訪問容器中的元素。 當元素用盡時,__next__()
將引發 StopIteration
異常來通知終止 for
循環。
你可以使用 next()
內置函數來調用 __next__()
方法;這個例子顯示了它的運作方式:
>>> s = 'abc'
>>> it = iter(s) # 返回迭代器對象
>>> it
<str_iterator object at 0x10c90e650>
>>> next(it) #使用next() 等價於 調用 it的__next__()方法
'a'
>>> next(it)
'b'
>>> next(it)
'c'
>>> next(it) # 元素用盡將引發 StopIteration異常
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
next(it)
StopIteration
因此,只要給類加上__iter__
方法返回迭代對象, 加上__next__
方法返回元素,就可以將自定義的類變為迭代器,然後就可以對其使用for
循環了。
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
生成器 是一個用於創建迭代器的簡單而強大的工具,看起來是帶yield
的函數,但是實際上創建了迭代器。
在調用生成器運行的過程中,每次遇到 yield 時函數會暫停並保存當前所有的運行信息,返回 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__()
方法。
另一個關鍵特性在於局部變量和執行狀態會在每次調用之間自動保存。
除了會自動創建方法和保存程序狀態,當生成器終結時,它們還會自動引發 StopIteration
。 這些特性結合在一起,使得創建迭代器能與編寫常規函數一樣容易。
某些簡單的生成器可以寫成簡潔的表達式代碼,所用語法類似列表推導式,但外層為圓括號而非方括號。 這種表達式被設計用於生成器將立即被外層函數所使用的情況。 生成器表達式相比完整的生成器更緊湊但較不靈活,相比等效的列表推導式則更為節省內存。
示例:
>>> 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']
yield
另外一個小眾的使用場景,是變相實現協程的效果,即在同一個線程內,實現不同任務交替執行:
def mytask1():
print('task1 開始執行')
'''
task code
'''
yield
def mytask2():
print('task2 開始執行')
'''
task code
'''
yield
gene1=mytask1()
gene2=mytask2()
# 實現mytask1 mytask2 交替執行
for i in range(100):
next(gene1)
next(gene2)