Python 程式設計/類
類是將類似資料和函式聚合在一起的一種方式。類基本上是一個範圍,在該範圍內執行各種程式碼(尤其是函式定義),並且該範圍的區域性變數成為類的屬性,以及該類構建的任何物件的屬性。由類構建的物件稱為該類的例項。
一瞥 Python 中的類
import math
class MyComplex:
"""A complex number""" # Class documentation
classvar = 0.0 # A class attribute, not an instance one
def phase(self): # A method
return math.atan2(self.imaginary, self.real)
def __init__(self): # A constructor
"""A constructor"""
self.real = 0.0 # An instance attribute
self.imaginary = 0.0
c1 = MyComplex()
c1.real = 3.14 # No access protection
c1.imaginary = 2.71
phase = c1.phase() # Method call
c1.undeclared = 9.99 # Add an instance attribute
del c1.undeclared # Delete an instance attribute
print(vars(c1)) # Attributes as a dictionary
vars(c1)["undeclared2"] = 7.77 # Write access to an attribute
print(c1.undeclared2) # 7.77, indeed
MyComplex.classvar = 1 # Class attribute access
print(c1.classvar == 1) # True; class attribute access, not an instance one
print("classvar" in vars(c1)) # False
c1.classvar = -1 # An instance attribute overshadowing the class one
MyComplex.classvar = 2 # Class attribute access
print(c1.classvar == -1) # True; instance attribute access
print("classvar" in vars(c1)) # True
class MyComplex2(MyComplex): # Class derivation or inheritance
def __init__(self, re = 0, im = 0):
self.real = re # A constructor with multiple arguments with defaults
self.imaginary = im
def phase(self):
print("Derived phase")
return MyComplex.phase(self) # Call to a base class; "super"
c3 = MyComplex2()
c4 = MyComplex2(1, 1)
c4.phase() # Call to the method in the derived class
class Record: pass # Class as a record/struct with arbitrary attributes
record = Record()
record.name = "Joe"
record.surname = "Hoe"
要定義類,請使用以下格式
class ClassName:
"Here is an explanation about your class"
pass
此類定義中的大寫是約定,但語言不要求。通常最好至少新增一個簡短的說明,說明你的類應該做什麼。上面的程式碼中的 pass 語句只是告訴 Python 直譯器繼續執行,什麼也不做。你可以在新增第一個語句後將其刪除。
該類是一個可呼叫物件,當被呼叫時,它會構建該類的例項。假設我們建立一個類 Foo。
class Foo:
"Foo is our new toy."
pass
要構建類 Foo 的例項,請“呼叫”類物件
f = Foo()
這將構建類 Foo 的一個例項,並在 f 中建立一個對它的引用。
要訪問類例項的成員,請使用語法 <class instance>.<member>。也可以使用 <class name>.<member> 訪問類定義的成員。
方法是類中的函式。第一個引數(方法必須始終至少帶一個引數)始終是呼叫函式的類例項。例如
>>> class Foo:
... def setx(self, x):
... self.x = x
... def bar(self):
... print(self.x)
如果執行此程式碼,將不會發生任何事情,至少在構建 Foo 的例項之前,然後在該例項上呼叫 bar 之前不會發生。
在普通函式中,如果你要設定一個變數,例如 test = 23,你將無法訪問 test 變數。鍵入 test 會說它未定義。這在類函式中也是如此,除非它們使用 self 變數。
基本上,在前面的示例中,如果我們要刪除 self.x,函式 bar 將無法執行任何操作,因為它無法訪問 x。setx() 中的 x 會消失。self 引數將變數儲存到類的“共享變數”資料庫中。
你不需要使用 self。但是,使用 self 是一個規範。
呼叫方法與呼叫函式非常相似,但是不要像形式引數列表所暗示的那樣將例項作為第一個引數傳遞,而是將函式用作例項的屬性。
>>> f = Foo()
>>> f.setx(5)
>>> f.bar()
這將輸出
5
可以透過使用它作為定義類的屬性而不是該類的例項,在任意物件上呼叫方法,如下所示
>>> Foo.setx(f,5)
>>> Foo.bar(f)
這將產生相同的輸出。
正如 setx 方法所示,Python 類的成員可以在執行時更改,而不僅僅是它們的價值,這與 C++ 或 Java 等語言中的類不同。我們甚至可以在執行上面的程式碼後刪除 f.x。
>>> del f.x
>>> f.bar()
Traceback (most recent call last): File "<stdin>", line 1, in ? File "<stdin>", line 5, in bar AttributeError: Foo instance has no attribute 'x'
這帶來的另一個影響是,我們可以在程式執行期間更改 Foo 類的定義。在下面的程式碼中,我們建立了一個名為 y 的 Foo 類定義的成員。如果我們再建立 Foo 的新例項,它現在將擁有這個新成員。
>>> Foo.y = 10
>>> g = Foo()
>>> g.y
10
這一切的核心是一個 字典,可以透過“vars(ClassName)”訪問
>>> vars(g)
{}
起初,這個輸出毫無意義。我們剛剛看到 g 擁有成員 y,那麼為什麼它不在成員字典中?但是,如果你還記得,我們將 y 放入了類定義 Foo 中,而不是 g 中。
>>> vars(Foo)
{'y': 10, 'bar': <function bar at 0x4d6a3c>, '__module__': '__main__',
'setx': <function setx at 0x4d6a04>, '__doc__': None}
在那裡,我們擁有 Foo 類定義的所有成員。當 Python 檢查 g.member 時,它首先在 g 的 vars 字典中檢查“member”,然後檢查 Foo。如果我們建立 g 的新成員,它將被新增到 g 的字典中,但不會被新增到 Foo 的字典中。
>>> g.setx(5)
>>> vars(g)
{'x': 5}
請注意,如果我們現在為 g.y 分配一個值,我們不是將該值分配給 Foo.y。Foo.y 將仍然是 10,但 g.y 現在將覆蓋 Foo.y
>>> g.y = 9
>>> vars(g)
{'y': 9, 'x': 5}
>>> vars(Foo)
{'y': 10, 'bar': <function bar at 0x4d6a3c>, '__module__': '__main__',
'setx': <function setx at 0x4d6a04>, '__doc__': None}
果然,如果我們檢查這些值
>>> g.y
9
>>> Foo.y
10
請注意,f.y 也將是 10,因為 Python 不會在 vars(f) 中找到“y”,因此它將從 vars(Foo) 獲取“y”的值。
有些人可能還注意到,Foo 中的方法出現在類字典中,與 x 和 y 位於同一位置。如果你還記得關於 lambda 函式 的部分,我們可以像變數一樣對待函式。這意味著我們可以像分配變數一樣在執行時將方法分配給類。但是,如果你這樣做,請記住,如果我們呼叫類例項的方法,傳遞給該方法的第一個引數將始終是類例項本身。
我們還可以使用類的 __dict__ 成員訪問類的成員字典。
>>> g.__dict__
{'y': 9, 'x': 5}
如果我們從 g.__dict__ 中新增、刪除或更改鍵值對,這與我們對 g 的成員進行了那些更改的效果相同。
>>> g.__dict__['z'] = -4
>>> g.z
-4
類很特別,因為一旦建立了一個例項,該例項就獨立於所有其他例項。我可以有兩個例項,每個例項都有不同的 x 值,它們不會影響彼此的 x。
f = Foo()
f.setx(324)
f.boo()
g = Foo()
g.setx(100)
g.boo()
f.boo() 和 g.boo() 將列印不同的值。
新式類是在 python 2.2 中引入的。新式類是指以內建型別為基類的類,最常見的是 object。從底層來看,舊式類和新式類之間的一個主要區別是它們的型別。舊式類的例項都是 instance 型別。新式類的例項將返回與 x.__class__ 相同的值作為它們的型別。這使得使用者定義的類與內建型別處於同一級別。舊式/傳統類將在 Python 3 中消失。考慮到這一點,所有開發都應該使用新式類。新式類還添加了類似於 Java 程式設計師所熟悉的屬性和靜態方法等構造。
舊式/傳統類
>>> class ClassicFoo:
... def __init__(self):
... pass
新式類
>>> class NewStyleFoo(object):
... def __init__(self):
... pass
屬性是帶有 getter 和 setter 方法的屬性。
>>> class SpamWithProperties(object):
... def __init__(self):
... self.__egg = "MyEgg"
... def get_egg(self):
... return self.__egg
... def set_egg(self, egg):
... self.__egg = egg
... egg = property(get_egg, set_egg)
>>> sp = SpamWithProperties()
>>> sp.egg
'MyEgg'
>>> sp.egg = "Eggs With Spam"
>>> sp.egg
'Eggs With Spam'
>>>
從 Python 2.6 開始,可以使用 @property 裝飾器
>>> class SpamWithProperties(object):
... def __init__(self):
... self.__egg = "MyEgg"
... @property
... def egg(self):
... return self.__egg
... @egg.setter
... def egg(self, egg):
... self.__egg = egg
Python 中的靜態方法就像 C++ 或 Java 中的靜態方法一樣。靜態方法沒有 "self" 引數,也不需要在使用它們之前例項化類。它們可以使用 staticmethod() 定義。
>>> class StaticSpam(object):
... def StaticNoSpam():
... print("You can't have have the spam, spam, eggs and spam without any spam... that's disgusting")
... NoSpam = staticmethod(StaticNoSpam)
>>> StaticSpam.NoSpam()
You can't have have the spam, spam, eggs and spam without any spam... that's disgusting
它們也可以使用函式裝飾器 @staticmethod 定義。
>>> class StaticSpam(object):
... @staticmethod
... def StaticNoSpam():
... print("You can't have have the spam, spam, eggs and spam without any spam... that's disgusting")
與所有面向物件語言一樣,Python 也支援繼承。繼承是一個簡單的概念,透過它,一個類可以擴充套件另一個類的功能,或者在 Python 的情況下,可以擴充套件多個其他類的功能。使用以下格式:
class ClassName(BaseClass1, BaseClass2, BaseClass3,...):
...
ClassName 是所謂的派生類,即從基類派生而來。然後派生類將擁有其所有基類的所有成員。如果在派生類和基類中定義了一個方法,那麼派生類中的成員將覆蓋基類中的成員。為了使用基類中定義的方法,有必要將該方法作為定義類的屬性呼叫,如上面的 Foo.setx(f,5) 中所示。
>>> class Foo:
... def bar(self):
... print("I'm doing Foo.bar()")
... x = 10
...
>>> class Bar(Foo):
... def bar(self):
... print("I'm doing Bar.bar()")
... Foo.bar(self)
... y = 9
...
>>> g = Bar()
>>> Bar.bar(g)
I'm doing Bar.bar()
I'm doing Foo.bar()
>>> g.y
9
>>> g.x
10
再一次,我們可以透過檢視類字典來了解幕後發生了什麼。
>>> vars(g)
{}
>>> vars(Bar)
{'y': 9, '__module__': '__main__', 'bar': <function bar at 0x4d6a04>,
'__doc__': None}
>>> vars(Foo)
{'x': 10, '__module__': '__main__', 'bar': <function bar at 0x4d6994>,
'__doc__': None}
當我們呼叫 g.x 時,它首先像往常一樣在 vars(g) 字典中查詢。與上面一樣,它接下來檢查 vars(Bar),因為 g 是 Bar 的例項。然而,由於繼承,如果它沒有在 vars(Bar) 中找到 x,Python 將檢查 vars(Foo)。
如#繼承節所示,一個類可以從多個類派生
class ClassName(BaseClass1, BaseClass2, BaseClass3):
pass
多重繼承中一個棘手的地方是方法解析:在方法呼叫時,如果方法名在多個基類或其基類中可用,那麼應該呼叫哪個基類方法。
方法解析順序取決於類是舊式類還是新式類。對於舊式類,派生類是從左到右考慮的,基類的基類在移至右側之前被考慮。因此,在上面,BaseClass1 首先被考慮,如果在那裡沒有找到方法,就會考慮 BaseClass1 的基類。如果失敗,則考慮 BaseClass2,然後是其基類,依此類推。對於新式類,請參閱 Python 線上文件。
連結
- 9.5.1. 多重繼承,docs.python.org
- Python 2.3 方法解析順序,python.org
有許多方法具有保留名稱,這些名稱用於特殊目的,例如模擬數值或容器操作等等。所有這些名稱都以兩個下劃線開頭和結尾。約定俗成,以單個下劃線開頭的函式是它們被引入的範圍內的 "私有" 的。
這些目的之一是構造一個例項,它的特殊名稱是 "__init__"。__init__() 在例項返回之前被呼叫(沒有必要手動返回例項)。例如,
class A:
def __init__(self):
print('A.__init__()')
a = A()
輸出
A.__init__()
__init__() 可以接受引數,在這種情況下,需要將引數傳遞給類才能建立例項。例如,
class Foo:
def __init__ (self, printme):
print(printme)
foo = Foo('Hi!')
輸出
Hi!
以下示例展示了使用 __init__() 和不使用 __init__() 之間的區別
class Foo:
def __init__ (self, x):
print(x)
foo = Foo('Hi!')
class Foo2:
def setx(self, x):
print(x)
f = Foo2()
Foo2.setx(f,'Hi!')
輸出
Hi! Hi!
類似地,當一個例項被銷燬時,會呼叫 "__del__";例如,當它不再被引用時。
這些方法也是建構函式和解構函式,但它們只在使用 with 例項化類時執行。示例
class ConstructorsDestructors:
def __init__(self):
print('init')
def __del__(self):
print('del')
def __enter__(self):
print('enter')
def __exit__(self, exc_type, exc_value, traceback):
print('exit')
with ConstructorsDestructors():
pass
init enter exit del
元類 建構函式。
|
透過覆蓋 __str__ 可以覆蓋將物件轉換為字串(如使用 print 語句或使用 str() 轉換函式)的行為。通常,__str__ 返回物件內容的格式化版本。這通常不會是可執行的程式碼。 例如 class Bar:
def __init__ (self, iamthis):
self.iamthis = iamthis
def __str__ (self):
return self.iamthis
bar = Bar('apple')
print(bar)
輸出 apple 這個函式很像 __str__()。如果 __str__ 不存在,但這個函式存在,則使用該函式的輸出作為列印的替代。__repr__ 用於以字串形式返回物件的表示形式。通常,它可以被執行以獲取回原始物件。 例如 class Bar:
def __init__ (self, iamthis):
self.iamthis = iamthis
def __repr__(self):
return "Bar('%s')" % self.iamthis
bar = Bar('apple')
bar
輸出(注意區別:可能沒有必要將其放在列印語句中,但在 Python 2.7 中確實需要這樣做) Bar('apple')
|
|
|
這是負責設定類屬性的函式。 它被提供變數被賦值的名稱和值。 當然,每個類都帶有一個預設的 __setattr__,它只是設定變數的值,但我們可以覆蓋它。 >>> class Unchangable:
... def __setattr__(self, name, value):
... print("Nice try")
...
>>> u = Unchangable()
>>> u.x = 9
Nice try
>>> u.x
Traceback (most recent call last): File "<stdin>", line 1, in ? AttributeError: Unchangable instance has no attribute 'x' 類似於 __setattr__,除了這個函式在嘗試訪問類成員時被呼叫,而預設函式只是返回該值。 >>> class HiddenMembers:
... def __getattr__(self, name):
... return "You don't get to see " + name
...
>>> h = HiddenMembers()
>>> h.anything
"You don't get to see anything"
這個函式被呼叫來刪除屬性。 >>> class Permanent:
... def __delattr__(self, name):
... print(name, "cannot be deleted")
...
>>> p = Permanent()
>>> p.x = 9
>>> del p.x
x cannot be deleted
>>> p.x
9
|
|
運算子過載允許我們使用內建的 Python 語法和運算子來呼叫我們定義的函式。
|
如果一個類具有 __add__ 函式,我們可以使用 '+' 運算子來新增類的例項。 這將使用作為引數傳遞的類的兩個例項呼叫 __add__,返回值將是加法的結果。 >>> class FakeNumber:
... n = 5
... def __add__(A,B):
... return A.n + B.n
...
>>> c = FakeNumber()
>>> d = FakeNumber()
>>> d.n = 7
>>> c + d
12
要覆蓋 增量賦值 運算子,只需在正常的二元運算子前面新增 'i',例如,對於 '+=' 使用 '__iadd__' 而不是 '__add__'。 該函式將獲得一個引數,該引數將是增量賦值運算子右側的物件。 然後,該函式的返回值將被分配給運算子左側的物件。 >>> c.__imul__ = lambda B: B.n - 6
>>> c *= d
>>> c
1
重要的是要注意,如果增量運算子函式尚未直接設定,則 增量賦值 運算子也將使用正常的運算子函式。 這將按預期工作,'__add__' 將被呼叫用於 '+=' 等等。 >>> c = FakeNumber()
>>> c += d
>>> c
12
|
|
|
一元運算子將只傳遞它們被呼叫的類的例項。 >>> FakeNumber.__neg__ = lambda A : A.n + 6
>>> -d
13
|
|
|
在 Python 中還可以覆蓋 索引和切片 運算子。 這允許我們在自己的物件上使用 class[i] 和 class[a:b] 語法。 最簡單的專案運算子是 __getitem__。 它將類的例項作為引數,然後是索引的值。 >>> class FakeList:
... def __getitem__(self,index):
... return index * 2
...
>>> f = FakeList()
>>> f['a']
'aa'
我們還可以為與將值分配給專案相關的語法定義一個函式。 此函式的引數包括被分配的值,以及來自 __getitem__ 的引數。 >>> class FakeList:
... def __setitem__(self,index,value):
... self.string = index + " is now " + value
...
>>> f = FakeList()
>>> f['a'] = 'gone'
>>> f.string
'a is now gone'
我們可以對切片做同樣的事情。 同樣,每個語法都有與之關聯的不同引數列表。 >>> class FakeList:
... def __getslice___(self,start,end):
... return str(start) + " to " + str(end)
...
>>> f = FakeList()
>>> f[1:4]
'1 to 4'
請記住,切片語法中的起始和結束引數可能為空。 在這裡,Python 對起始和結束都有預設值,如下所示。 >> f[:]
'0 to 2147483647'
請注意,這裡顯示的切片結束的預設值只是 32 位系統上的最大可能的帶符號整數,並且可能因系統和 C 編譯器而異。
我們還有用於刪除專案和切片的運算子。
請注意,這些與 __getitem__ 和 __getslice__ 相同。 |
|
|
Python 類的靈活性意味著類可以採用各種行為。 但是,為了易於理解,最好謹慎使用 Python 的許多工具。 嘗試在類定義中宣告所有方法,並且儘可能始終使用 <class>.<member> 語法而不是 __dict__。 看看 C++ 和 Java 中的類,看看大多數程式設計師對類的期望。
由於 Python 類中的所有成員都可以被類外部的函式/方法訪問,因此沒有辦法強制執行封裝,除非覆蓋 __getattr__、__setattr__ 和 __delattr__。然而,一般的做法是,類的建立者或模組的建立者僅僅相信使用者只會使用預期的介面,並避免為了那些需要訪問模組內部工作機制的使用者而限制訪問。當使用類或模組中除了預期介面之外的部分時,請記住,這些部分可能會在模組的後續版本中發生改變,甚至可能導致模組出現錯誤或未定義的行為,因為封裝是私有的。
在定義類時,通常使用類定義開始處的字串字面量來記錄類。然後,該字串將被放置在類定義的 __doc__ 屬性中。
>>> class Documented:
... """This is a docstring"""
... def explode(self):
... """
... This method is documented, too! The coder is really serious about
... making this class usable by others who don't know the code as well
... as he does.
...
... """
... print("boom")
>>> d = Documented()
>>> d.__doc__
'This is a docstring'
文件字串是記錄程式碼的一種非常有用的方式。即使你從未編寫過任何單獨的文件(並且讓我們承認,這樣做對許多程式設計師來說優先順序最低),在你的類中包含資訊豐富的文件字串將極大地提高它們的可用性。
存在多種工具可以將 Python 程式碼中的文件字串轉換為可讀的 API 文件,例如,EpyDoc。
不要僅僅停留在記錄類定義上。類中的每個方法也應該有它自己的文件字串。請注意,上面示例類 Documented 中的方法 explode 的文件字串有一個相當長的文件字串,跨越了多行。它的格式與 Python 建立者 Guido van Rossum 在PEP 8中的風格建議相符。
在執行時向類新增方法相當容易。假設我們有一個名為 Spam 的類和一個名為 cook 的函式。我們希望能夠在 Spam 類的所有例項上使用 cook 函式。
class Spam:
def __init__(self):
self.myeggs = 5
def cook(self):
print("cooking %s eggs" % self.myeggs)
Spam.cook = cook #add the function to the class Spam
eggs = Spam() #NOW create a new instance of Spam
eggs.cook() #and we are ready to cook!
這將輸出
cooking 5 eggs
向已建立的類例項新增方法有點棘手。再次假設我們有一個名為 Spam 的類,並且我們已經建立了 eggs。但後來我們注意到我們想烹飪這些 eggs,但我們不想建立一個新的例項,而是使用已經建立的例項。
class Spam:
def __init__(self):
self.myeggs = 5
eggs = Spam()
def cook(self):
print("cooking %s eggs" % self.myeggs)
import types
f = types.MethodType(cook, eggs, Spam)
eggs.cook = f
eggs.cook()
現在我們可以烹飪我們的 eggs,最後一條語句將輸出
cooking 5 eggs
我們也可以編寫一個函式來簡化向類例項新增方法的過程。
def attach_method(fxn, instance, myclass):
f = types.MethodType(fxn, instance, myclass)
setattr(instance, fxn.__name__, f)
現在我們只需要用我們想要附加的函式、我們要附加它的例項以及例項所衍生的類作為引數來呼叫 attach_method 即可。因此,我們的函式呼叫可能看起來像這樣
attach_method(cook, eggs, Spam)
注意,在 add_method 函式中,我們不能寫instance.fxn = f因為這會向例項新增一個名為 fxn 的函式。
- 9. 類,docs.python.org
- 2. 內建函式 # vars,docs.python.org