英文官方文件: https://docs.python.org/3.8/tutorial/classes.html
中文官方文件: https://docs.python.org/zh-cn/3.8/tutorial/classes.html
類提供了一種組合資料和功能的方法。
建立一個新類意味著建立一個新的物件型別
,從而允許建立一個該型別的新例項。
1、類定義
最簡單的類定義看起來像這樣:
class ClassName:
<statement-1>
.
.
.
<statement-N>
類定義與函式定義 (def 語句) 一樣必須被執行才會起作用。
當進入類定義時,將建立一個新的名稱空間,並將其用作區域性作用域。因此,所有對區域性變數的賦值都是在這個新名稱空間之內。 特別的,函式定義會繫結到這裡的新函式名稱。
2、類物件
類物件支援兩種操作:屬性引用和例項化。
(1)屬性引用
屬性引用的標準語法: obj.name
。
有效的屬性名稱是,類物件被建立時,存在於類名稱空間中的所有名稱。 因此,如果類定義是這樣的:
class MyClass:
"""A simple example class"""
i = 12345
def f(self):
return 'hello world'
那麼 MyClass.i
和 MyClass.f
就是有效的屬性引用,將分別返回一個整數和一個函式物件。
類屬性也可以被賦值,因此可以通過賦值來更改 MyClass.i
的值。
__doc__
也是一個有效的屬性,將返回所屬類的文件字串: "A simple example class"。
(2)例項化
類的例項化使用函式表示法。
可以把類物件視為是返回該類的一個新例項的不帶引數的函式。 舉例來說(假設使用上述的類):
x = MyClass()
建立類的新例項並將此物件分配給區域性變數 x。
例項化操作(“呼叫”類物件)會建立一個空物件,也可以使用__init__()
建立帶有特定初始狀態的自定義例項。例如:
def __init__(self):
self.data = []
此時,類的例項化操作會自動為新建立的類例項呼叫 __init__()
。 因此在這個示例中,可以通過 x = MyClass()
語句獲得一個經初始化的新例項:
__init__()
方法還可以有額外引數以實現更高靈活性。在這種情況下,提供給類例項化運算子的引數將被傳遞給 __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)
3、例項物件
例項物件唯一操作是屬性引用。 有兩種有效的屬性名稱:資料屬性和方法。
(1)資料屬性
資料屬性不需要宣告,像區域性變數一樣,它們將在第一次被賦值時產生。
例如,如果 x 是上面建立的 MyClass 的例項,則以下程式碼段將列印數值 16,且不保留任何追蹤資訊:
x.counter = 1
while x.counter < 10:
x.counter = x.counter * 2
print(x.counter)
del x.counter
(2)方法
方法是“從屬於”物件的函式。 【注:方法是針對物件來說的,函式是針對類來說的】
(在 Python 中,方法這個術語並不是類例項所特有的:其他物件也可以有方法。例如,列表物件具有 append, insert, remove, sort 等方法。然而,在以下討論中,我們使用方法一詞將專指類例項物件的方法,除非另外顯式地說明。)
例項物件的有效方法名稱依賴於其所屬的類。 【注:這裡說的是方法名稱】
根據定義,一個類中所有是函式物件的屬性都是定義了其例項的相應方法。
因此在我們的示例中,x.f 是有效的方法引用,因為 MyClass.f 是一個函式,而 x.i 不是方法,因為 MyClass.i 不是一個函式。 但是x.f 與 MyClass.f 並不是一回事,它是一個方法物件,不是函式物件。
4、方法物件
通常,方法在繫結後立即被呼叫,在 MyClass 示例中,這將返回字串 'hello world'。
x.f()
但是,立即呼叫一個方法並不是必須的: x.f 是一個方法物件,它可以被儲存起來以後再呼叫。 例如:
xf = x.f
while True:
print(xf())
將繼續列印 hello world,直到結束。
雖然 f()
的函式定義指定了一個引數,但在上面呼叫 x.f()
時並沒有帶引數。 當不帶引數地呼叫一個需要引數的函式時 Python 肯定會引發異常,即使引數實際未被使用。
方法的特殊之處就在於例項物件會作為函式的第一個引數被傳入。 在我們的示例中,呼叫 x.f() 其實就相當於 MyClass.f(x)。 【注:也就是方法引數列表中的self】
總之,呼叫一個具有 n 個引數的方法就相當於呼叫再多一個引數的對應函式,這個引數值為方法所屬例項物件,位置在其他引數之前。
當一個例項的非資料屬性【注:即方法】被引用時,將搜尋例項所屬的類。
如果被引用的屬性名稱表示一個有效的類屬性中的函式物件,會通過打包(指向)查詢到的例項物件和函式物件 到一個抽象物件的方式來建立方法物件:這個抽象物件就是方法物件。 【注:xf = x.f,x.f 是一個方法物件】
當附帶引數列表呼叫方法物件時,將基於例項物件和引數列表構建一個新的引數列表【注:self和引數列表】,並使用這個新引數列表呼叫相應的函式物件。
5、類和例項變數
一般來說,例項變數用於每個例項的唯一資料,而類變數用於類的所有例項共享的屬性和方法:
【注:下例中kind
是類變數,name
是例項變數】
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'
正如 名稱和物件 中已討論過的,共享資料可能在涉及可變物件的時候,例如列表和字典,導致令人驚訝的結果。
例如以下程式碼中的 tricks 列表不應該被用作類變數,因為所有的 Dog 例項將只共享一個單獨的列表:
【注:類變數是所有例項所共享的,以下程式碼中的 tricks 列表不應該被用作類變數,例項呼叫 add_trick 時,就改變了 tricks 列表】
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']
正確的類設計應該使用例項變數:
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']
6、補充說明
如果同樣的屬性名稱同時出現在例項和類中,則屬性查詢會優先選擇例項:
>>>
>>> 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
方法的第一個引數常常被命名為 self。 這也不過就是一個約定: self 這一名稱在 Python 中絕對沒有特殊含義。但是要注意,不遵循此約定會使得你的程式碼對其他 Python 程式設計師來說缺乏可讀性,而且也可以想像一個 類瀏覽器 程式的編寫可能會依賴於這樣的約定。
任何一個作為類屬性的函式都為該類的例項定義了一個相應方法。 函式定義的文字並非必須包含於類定義之內:將一個函式物件賦值給一個區域性變數也是可以的。 例如:
# Function defined outside the class
def f1(self, x, y):
return min(x, x+y)
class C:
f = f1
def g(self):
return 'hello world'
h = g
現在 f, g 和 h 都是 C 類的引用函式物件的屬性,因而它們就都是 C 的例項的方法,其中 h 完全等同於 g。 但請注意,本示例的做法通常只會令程式的閱讀者感到迷惑。
方法可以通過使用 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)
方法可以通過與普通函式相同的方式引用全域性名稱。與方法相關聯的全域性作用域就是包含其定義的模組。(類永遠不會被作為全域性作用域。)
雖然我們很少會有充分的理由在方法中使用全域性作用域,但全域性作用域存在許多合法的使用場景:舉個例子,匯入到全域性作用域的函式和模組可以被方法所使用,在其中定義的函式和類也一樣。
通常,包含該方法的類本身是在全域性作用域中定義的,而在下一節中我們將會發現為何方法需要引用其所屬類的很好的理由。
每個值都是一個物件,因此具有 類(也稱為 型別),並儲存為 object.__class__
。