發(fā)布于:2021-02-01 13:56:20
0
84
0
在本教程中,我將幫助解開(kāi)類(lèi)方法、靜態(tài)方法和常規(guī)實(shí)例方法背后的秘密。
如果您對(duì)它們之間的差異有了直觀(guān)的理解,您將能夠編寫(xiě)面向?qū)ο蟮腜ython,它可以更清楚地傳達(dá)其意圖,并且從長(zhǎng)遠(yuǎn)來(lái)看更易于維護(hù)。
實(shí)例,類(lèi),靜態(tài)方法-概述
讓我們從編寫(xiě)一個(gè)(Python 3)類(lèi)開(kāi)始,該類(lèi)包含所有三種方法類(lèi)型的簡(jiǎn)單示例:
class MyClass: def method(self): return 'instance method called', self @classmethod def classmethod(cls): return 'class method called', cls @staticmethod def staticmethod(): return 'static method called'
注意:對(duì)于Python 2用戶(hù):@staticmethod
和@classmethod
修飾符從Python 2.4開(kāi)始就可以使用了,這個(gè)示例將起作用照原樣。您可以選擇使用class MyClass(object):
語(yǔ)法聲明從object
繼承的新樣式類(lèi),而不是使用普通的class MyClass:
聲明。除此之外,您還可以繼續(xù)使用
實(shí)例方法
MyClass調(diào)用的第一個(gè)方法method是常規(guī)實(shí)例方法。這是您大多數(shù)時(shí)候會(huì)使用的基本,簡(jiǎn)潔的方法類(lèi)型。您可以看到該方法self帶有一個(gè)參數(shù),它指向MyClass該方法何時(shí)被調(diào)用的實(shí)例(但當(dāng)然實(shí)例方法可以接受多個(gè)參數(shù))。
通過(guò)該self參數(shù),實(shí)例方法可以自由訪(fǎng)問(wèn)同一對(duì)象上的屬性和其他方法。在修改對(duì)象的狀態(tài)時(shí),這賦予了他們很多功能。
實(shí)例方法不僅可以修改對(duì)象狀態(tài),而且還可以通過(guò)self.__class__屬性訪(fǎng)問(wèn)類(lèi)本身。這意味著實(shí)例方法也可以修改類(lèi)狀態(tài)。
類(lèi)方法
讓我們將其與第二種方法進(jìn)行比較MyClass.classmethod。我用@classmethod裝飾器標(biāo)記了此方法,以將其標(biāo)記為類(lèi)方法。
當(dāng)self調(diào)用方法時(shí),類(lèi)方法不接受參數(shù),而是采用cls指向類(lèi)的參數(shù),而不是對(duì)象實(shí)例。
因?yàn)轭?lèi)方法只能訪(fǎng)問(wèn)此cls參數(shù),所以它不能修改對(duì)象實(shí)例狀態(tài)。那將需要訪(fǎng)問(wèn)self。但是,類(lèi)方法仍然可以修改適用于該類(lèi)所有實(shí)例的類(lèi)狀態(tài)。
靜態(tài)方法
第三種方法,MyClass.staticmethod
被標(biāo)記為一個(gè)@staticmethod
裝飾器,以將其標(biāo)記為一個(gè)靜態(tài)方法。
這種類(lèi)型的方法既不接受self
也不接受cls
參數(shù)(當(dāng)然它可以自由接受任意數(shù)量的其他參數(shù))。
因此靜態(tài)方法既不能修改對(duì)象狀態(tài),也不能修改類(lèi)狀態(tài)。靜態(tài)方法在它們可以訪(fǎng)問(wèn)的數(shù)據(jù)方面受到限制,它們主要是為方法命名命名空間的一種方法。
讓我們看看他們的行動(dòng)!
我知道到目前為止,這一討論還只是理論上的。而且,我相信您必須對(duì)這些方法類(lèi)型在實(shí)踐中的差異有一個(gè)直觀(guān)的了解?,F(xiàn)在,我們將討論一些具體示例。
讓我們看一下這些方法在調(diào)用時(shí)的行為。我們將從創(chuàng)建該類(lèi)的實(shí)例開(kāi)始,然后在其上調(diào)用三個(gè)不同的方法。
MyClass 的設(shè)置方式是,每個(gè)方法的實(shí)現(xiàn)都返回一個(gè)元組,其中包含供我們跟蹤發(fā)生了什么的信息以及該方法可以訪(fǎng)問(wèn)的類(lèi)或?qū)ο蟮哪男┎糠帧?/span>
當(dāng)我們調(diào)用實(shí)例方法時(shí),會(huì)發(fā)生以下情況:
>>> obj = MyClass() >>> obj.method() ('instance method called', <MyClass instance at 0x10205d190>)
這證實(shí)了method(實(shí)例方法)可以<MyClass instance>通過(guò)self參數(shù)訪(fǎng)問(wèn)對(duì)象實(shí)例(打印為)。
調(diào)用該方法時(shí),Python會(huì)將self參數(shù)替換為實(shí)例對(duì)象obj。我們可以忽略點(diǎn)調(diào)用語(yǔ)法(obj.method())的語(yǔ)法糖,并手動(dòng)傳遞實(shí)例對(duì)象以獲得相同的結(jié)果:
>>> MyClass.method(obj) ('instance method called', <MyClass instance at 0x10205d190>)
您能猜出如果不先創(chuàng)建實(shí)例就嘗試調(diào)用該方法會(huì)發(fā)生什么情況嗎?
順便說(shuō)一句,實(shí)例方法還可以通過(guò)屬性訪(fǎng)問(wèn)類(lèi)本身self.__class__。這使實(shí)例方法在訪(fǎng)問(wèn)限制方面功能強(qiáng)大-它們可以修改對(duì)象實(shí)例和類(lèi)本身的狀態(tài)。
接下來(lái)讓我們嘗試類(lèi)方法:
>>> obj.classmethod() ('class method called', <class MyClass at 0x101a2f4c8>)
調(diào)用classmethod()顯示了它無(wú)權(quán)訪(fǎng)問(wèn)該<MyClass instance>對(duì)象,而只能訪(fǎng)問(wèn)<class MyClass>代表該類(lèi)本身的對(duì)象(Python中的所有內(nèi)容都是一個(gè)對(duì)象,甚至是類(lèi)本身)。
請(qǐng)注意,當(dāng)我們調(diào)用時(shí),Python如何自動(dòng)將類(lèi)作為第一個(gè)參數(shù)傳遞給函數(shù)MyClass.classmethod()。通過(guò)點(diǎn)語(yǔ)法在Python中調(diào)用方法會(huì)觸發(fā)此行為。self實(shí)例方法上的參數(shù)以相同的方式工作。
請(qǐng)注意,命名這些參數(shù)self而cls僅僅是一個(gè)慣例。你可以很容易地為它們命名the_object和the_class和得到同樣的結(jié)果。重要的是它們?cè)谠摲椒ǖ膮?shù)列表中排在第一位。
現(xiàn)在該調(diào)用靜態(tài)方法了:
>>> obj.staticmethod() 'static method called'
您是否看到我們?nèi)绾握{(diào)用staticmethod()對(duì)象并能夠成功完成調(diào)用?當(dāng)一些開(kāi)發(fā)人員得知可以在對(duì)象實(shí)例上調(diào)用靜態(tài)方法時(shí),他們會(huì)感到驚訝。
在幕后,Python只是通過(guò)使用點(diǎn)語(yǔ)法調(diào)用靜態(tài)方法時(shí)不傳遞self或cls參數(shù)來(lái)簡(jiǎn)單地強(qiáng)制執(zhí)行訪(fǎng)問(wèn)限制。
這證實(shí)了靜態(tài)方法既不能訪(fǎng)問(wèn)對(duì)象實(shí)例狀態(tài)也不能訪(fǎng)問(wèn)類(lèi)狀態(tài)。它們像常規(guī)函數(shù)一樣工作,但屬于類(lèi)(和每個(gè)實(shí)例的)名稱(chēng)空間。
現(xiàn)在,讓我們看看嘗試在類(lèi)本身上調(diào)用這些方法時(shí)發(fā)生的情況-無(wú)需事先創(chuàng)建對(duì)象實(shí)例:
>>> MyClass.classmethod() ('class method called', <class MyClass at 0x101a2f4c8>) >>> MyClass.staticmethod() 'static method called' >>> MyClass.method() TypeError: unbound method method() must be called with MyClass instance as first argument (got nothing instead)
我們能夠調(diào)用classmethod()并staticmethod()很好,但是嘗試調(diào)用實(shí)例方法method()失敗,并帶有TypeError。
這是可以預(yù)期的-這次我們沒(méi)有創(chuàng)建對(duì)象實(shí)例,而是嘗試直接在類(lèi)藍(lán)圖本身上調(diào)用實(shí)例函數(shù)。這意味著Python無(wú)法填充self參數(shù),因此調(diào)用失敗。
這應(yīng)該使這三種方法類(lèi)型之間的區(qū)別更加清晰。但是我不會(huì)再這樣了。在接下來(lái)的兩節(jié)中,我將介紹兩個(gè)更實(shí)際的示例,說(shuō)明何時(shí)使用這些特殊方法類(lèi)型。
我將基于這個(gè)簡(jiǎn)單的Pizza類(lèi)來(lái)舉例:
class Pizza: def __init__(self, ingredients): self.ingredients = ingredients def __repr__(self): return f'Pizza({self.ingredients!r})'
>>> Pizza(['cheese', 'tomatoes']) Pizza(['cheese', 'tomatoes'])
注意:此代碼示例以及本教程中的后續(xù)代碼示例使用Python 3.6 f-strings構(gòu)造由返回的字符串__repr__。在Python 2和3.6之前的Python 3版本上,您將使用不同的字符串格式表達(dá)式,例如:
def __repr__(self): return 'Pizza(%r)' % self.ingredients
美味比薩餅工廠(chǎng)與@classmethod
如果你在現(xiàn)實(shí)世界中接觸過(guò)比薩餅,你會(huì)知道有許多美味的變化可用:
Pizza(['mozzarella', 'tomatoes'])
Pizza(['mozzarella', 'tomatoes', 'ham', 'mushrooms'])
Pizza(['mozzarella'] * 4)
意大利人幾個(gè)世紀(jì)前就想出了他們的比薩餅分類(lèi)法,所以這些美味的比薩餅都有自己的名字。我們最好利用這個(gè)優(yōu)勢(shì),為Pizza
類(lèi)的用戶(hù)提供一個(gè)更好的界面來(lái)創(chuàng)建他們想要的pizza對(duì)象。
一個(gè)好的、干凈的方法是使用類(lèi)方法作為工廠(chǎng)函數(shù)來(lái)創(chuàng)建不同種類(lèi)的pizza:
class Pizza:
def __init__(self, ingredients):
self.ingredients = ingredients
def __repr__(self):
return f'Pizza({self.ingredients!r})'
@classmethod
def margherita(cls):
return cls(['mozzarella', 'tomatoes'])
@classmethod
def prosciutto(cls):
return cls(['mozzarella', 'tomatoes', 'ham'])
注意我是如何使用在margherita
和prosciutto
工廠(chǎng)方法中使用cls
參數(shù),而不是直接調(diào)用Pizza
構(gòu)造函數(shù)。
這是一個(gè)技巧,您可以用來(lái)遵循“不要重蹈覆轍”(DRY)的原則。如果我們決定在某個(gè)時(shí)候重命名該類(lèi),則不必記住在所有類(lèi)方法工廠(chǎng)函數(shù)中都更新構(gòu)造函數(shù)名稱(chēng)。
現(xiàn)在,我們可以用這些工廠(chǎng)方法做什么?讓我們?cè)囉靡幌拢?/span>
>>> Pizza.margherita()
Pizza(['mozzarella', 'tomatoes'])
>>> Pizza.prosciutto()
Pizza(['mozzarella', 'tomatoes', 'ham'])
正如您所看到的,我們可以使用工廠(chǎng)函數(shù)創(chuàng)建新的Pizza
對(duì)象,這些對(duì)象按照我們想要的方式配置。它們都在內(nèi)部使用相同的__init__
構(gòu)造函數(shù),只是為記住所有不同的成分提供了一個(gè)快捷方式。
另一種看待類(lèi)方法用法的方法是,它們?cè)试S您為類(lèi)定義替代構(gòu)造函數(shù)。
Python只允許每個(gè)類(lèi)使用一個(gè)__init__
方法。使用類(lèi)方法可以根據(jù)需要添加任意多的可選構(gòu)造函數(shù)。這可以使類(lèi)的接口自文檔化(在一定程度上),并簡(jiǎn)化它們的使用。
什么時(shí)候使用靜態(tài)方法
這里要想出一個(gè)好的例子有點(diǎn)困難。但是告訴你,我會(huì)繼續(xù)把比薩餅拉得越來(lái)越薄…(好吃?。┻@是我想到的:
import math
class Pizza:
def __init__(self, radius, ingredients):
self.radius = radius
self.ingredients = ingredients
def __repr__(self):
return (f'Pizza({self.radius!r}, '
f'{self.ingredients!r})')
def area(self):
return self.circle_area(self.radius)
@staticmethod
def circle_area(r):
return r ** 2 * math.pi
現(xiàn)在我改變了什么?首先,我修改了構(gòu)造函數(shù)和__repr__
以接受一個(gè)額外的radius
參數(shù)。
我還添加了一個(gè)area()
實(shí)例方法來(lái)計(jì)算并返回比薩餅的面積(這也是@property
的一個(gè)很好的候選者-但是,這只是一個(gè)玩具示例)。
而不是計(jì)算面積直接在area()
中,使用眾所周知的圓面積公式,我將其分解為一個(gè)單獨(dú)的circle_area()
靜態(tài)方法。
讓我們?cè)囋嚕?/span>
>>> p = Pizza(4, ['mozzarella', 'tomatoes'])
>>> p
Pizza(4, ['mozzarella', 'tomatoes'])
>>> p.area()
50.26548245743669
>>> Pizza.circle_area(4)
50.26548245743669
當(dāng)然,這是一個(gè)有點(diǎn)簡(jiǎn)單的例子,但它可以幫助解釋靜態(tài)方法提供的一些好處。
正如我們所了解的,靜態(tài)方法不能訪(fǎng)問(wèn)類(lèi)或?qū)嵗隣顟B(tài),因?yàn)樗鼈儾唤邮?/span>cls
或self
參數(shù)。這是一個(gè)很大的限制-但它也是一個(gè)很好的信號(hào),表明一個(gè)特定的方法獨(dú)立于它周?chē)乃衅渌椒ā?/span>
在上面的示例中,很明顯circle_area()
不能以任何方式修改類(lèi)或類(lèi)實(shí)例。(當(dāng)然,你可以用一個(gè)全局變量來(lái)解決這個(gè)問(wèn)題,但這不是重點(diǎn)。)
現(xiàn)在,為什么這很有用?
將一個(gè)方法標(biāo)記為靜態(tài)方法不僅僅意味著一個(gè)方法不會(huì)修改類(lèi)或?qū)嵗隣顟B(tài)-Python運(yùn)行時(shí)也會(huì)強(qiáng)制執(zhí)行此限制。
類(lèi)似的技術(shù)允許您清楚地交流類(lèi)體系結(jié)構(gòu)的各個(gè)部分,以便新的開(kāi)發(fā)工作自然地在這些體系結(jié)構(gòu)中進(jìn)行設(shè)置邊界。當(dāng)然,挑戰(zhàn)這些限制是很容易的。但在實(shí)踐中,它們通常有助于避免意外的修改違背原始設(shè)計(jì)。
換言之,使用靜態(tài)方法和類(lèi)方法是傳達(dá)開(kāi)發(fā)人員意圖的方法,同時(shí)強(qiáng)制執(zhí)行該意圖,以避免大多數(shù)會(huì)破壞設(shè)計(jì)的思維失誤和錯(cuò)誤。
謹(jǐn)慎地應(yīng)用用這種方式編寫(xiě)某些方法可以提供維護(hù)好處,減少其他開(kāi)發(fā)人員錯(cuò)誤使用您的類(lèi)的可能性,這是有道理的。
在編寫(xiě)測(cè)試代碼時(shí),靜態(tài)方法也有好處。
因?yàn)?/span>circle_area()
方法完全獨(dú)立于類(lèi)的其他部分,所以它非常重要更容易測(cè)試。
在單元測(cè)試中測(cè)試方法之前,我們不必?fù)?dān)心設(shè)置一個(gè)完整的類(lèi)實(shí)例。我們可以像測(cè)試常規(guī)函數(shù)一樣發(fā)射出去。同樣,這使以后的維護(hù)更容易。
關(guān)鍵要點(diǎn)
實(shí)例方法需要一個(gè)類(lèi)實(shí)例,并且可以通過(guò)訪(fǎng)問(wèn)該實(shí)例self。
類(lèi)方法不需要類(lèi)實(shí)例。他們無(wú)法訪(fǎng)問(wèn)實(shí)例(self),但可以通過(guò)訪(fǎng)問(wèn)類(lèi)本身cls。
靜態(tài)方法無(wú)權(quán)訪(fǎng)問(wèn)cls或self。它們像常規(guī)函數(shù)一樣工作,但屬于類(lèi)的名稱(chēng)空間。
靜態(tài)方法和類(lèi)方法進(jìn)行通信,并(在一定程度上)強(qiáng)制開(kāi)發(fā)人員進(jìn)行有關(guān)類(lèi)設(shè)計(jì)的意圖。這可以帶來(lái)維護(hù)優(yōu)勢(shì)。
作者介紹
熱門(mén)博客推薦