為什么 classmethod 比 staticmethod 更受寵?
我們知道,classmethod 和 staticmethod 都可以作為函數(shù)的裝飾器,都可用于不涉及類的成員變量的方法,但是你查一下 Python 標(biāo)準(zhǔn)庫就會知道 classmethod 使用的次數(shù)(1052)要遠(yuǎn)遠(yuǎn)多于 staticmethod(539),這是為什么呢?

這就要從 staticmethod 和 classmethod 的區(qū)別說起。
1、從調(diào)用形式上看,二者用法差不多
先說下什么是類,什么是實例,比如說 a = A(),那么 A 就是類,a 就是實例。
從定義形式上看,clasmethod 的第一個參數(shù)是 cls,代表類本身,普通方法的第一個參數(shù)是 self,代表實例本身,staticmethod 的參數(shù)和普通函數(shù)沒有區(qū)別。
從調(diào)用形式上看,staticmethod 和 classmethod 都支持類直接調(diào)用和實例調(diào)用。
class?MyClass:
????def?method(self):
????????"""
????????Instance?methods?need?a?class?instance?and
????????can?access?the?instance?through?`self`.
????????"""
????????return?'instance?method?called',?self
????@classmethod
????def?classmethod(cls):
????????"""
????????Class?methods?don't?need?a?class?instance.
????????They?can't?access?the?instance?(self)?but
????????they?have?access?to?the?class?itself?via?`cls`.
????????"""
????????return?'class?method?called',?cls
????@staticmethod
????def?staticmethod():
????????"""
????????Static?methods?don't?have?access?to?`cls`?or?`self`.
????????They?work?like?regular?functions?but?belong?to
????????the?class's?namespace.
????????"""
????????return?'static?method?called'
#?All?methods?types?can?be
#?called?on?a?class?instance:
>>>?obj?=?MyClass()
>>>?obj.method()
('instance?method?called',?0x1019381b8>)
>>>?obj.classmethod()
('class?method?called',?<class?MyClass?at?0x101a2f4c8>)
>>>?obj.staticmethod()
'static?method?called'
#?Calling?instance?methods?fails
#?if?we?only?have?the?class?object:
>>>?MyClass.classmethod()
('class?method?called',?<class?MyClass?at?0x101a2f4c8>)
>>>?MyClass.staticmethod()
'static?method?called'
2、先說說 staticmethod。
如果一個類的函數(shù)上面加上了 staticmethod,通常就表示這個函數(shù)的計算不涉及類的變量,不需要類的實例化就可以使用,也就是說該函數(shù)和這個類的關(guān)系不是很近,換句話說,使用 staticmethod 裝飾的函數(shù),也可以定義在類的外面。我有時候會糾結(jié)到底放在類里面使用 staticmethod,還是放在 utils.py 中單獨寫一個函數(shù)?比如下面的 Calendar 類:
class?Calendar:
????def?__init__(self):
????????self.events?=?[]
????def?add_event(self,?event):
????????self.events.append(event)
????@staticmethod
????def?is_weekend(dt:datetime):
????????return?dt.weekday()?>?4
if?__name__?==?'__main__':
????print(Calendar.is_weekend(datetime(2021,12,27)))
????#output:?False
里面的函數(shù) is_weekend?用來判斷某一天是否是周末,就可以定義在?Calendar 的外面作為公共方法,這樣在使用該函數(shù)時就不需要再加上 Calendar 這個類名。
但是有些情況最好定義在類的里面,那就是這個函數(shù)離開了類的上下文,就不知道該怎么調(diào)用了,比如說下面這個類,用來判斷矩陣是否可以相乘,更易讀的調(diào)用形式是 Matrix.can_multiply:
from?dataclasses?import?dataclass
@dataclass
class?Matrix:
????shape:?tuple[int,?int]?#?python3.9?之后支持這種類型聲明的寫法
????@staticmethod
????def?can_multiply(a,?b):
????????n,?m?=?a.shape
????????k,?l?=?b.shape
????????return?m?==?k
3、再說說 classmethod。
首先我們從 clasmethod 的形式上來理解,它的第一個參數(shù)是 cls,代表類本身,也就是說,我們可以在 classmethod 函數(shù)里面調(diào)用類的構(gòu)造函數(shù) cls(),從而生成一個新的實例。從這一點,可以推斷出它的使用場景:
當(dāng)我們需要再次調(diào)用構(gòu)造函數(shù)時,也就是創(chuàng)建新的實例對象時 需要不修改現(xiàn)有實例的情況下返回一個新的實例
比如下面的代碼:
class?Stream:
????def?extend(self,?other):
????????#?modify?self?using?other
????????...
????@classmethod
????def?from_file(cls,?file):
????????...
????@classmethod
????def?concatenate(cls,?*streams):
????????s?=?cls()
????????for?stream?in?streams:
????????????s.extend(stream)
????????return?s
steam?=?Steam()
當(dāng)我們調(diào)用 steam.extend 函數(shù)時候會修改 steam 本身,而調(diào)用 concatenate 時會返回一個新的實例對象,而不會修改 steam 本身。
4、本質(zhì)區(qū)別
我們可以嘗試自己實現(xiàn)一下 classmethod 和 staticmethod 這兩個裝飾器,來看看他們的本質(zhì)區(qū)別:
class?StaticMethod:
????def?__init__(self,?func):
????????self.func?=?func
????def?__get__(self,?instance,?owner):
????????return?self.func
????def?__call__(self,?*args,?**kwargs):??#?New?in?Python?3.10
????????return?self.func(*args,?**kwargs)
class?ClassMethod:
????def?__init__(self,?func):
????????self.func?=?func
????def?__get__(self,?instance,?owner):
????????return?self.func.__get__(owner,?type(owner))
class?A:
????def?normal(self,?*args,?**kwargs):
????????print(f"normal({self=},?{args=},?{kwargs=})")
????@staticmethod
????def?f1(*args,?**kwargs):
????????print(f"f1({args=},?{kwargs=})")
????@StaticMethod
????def?f2(*args,?**kwargs):
????????print(f"f2({args=},?{kwargs=})")
????@classmethod
????def?g1(cls,?*args,?**kwargs):
????????print(f"g1({cls=},?{args=},?{kwargs=})")
????@ClassMethod
????def?g2(cls,?*args,?**kwargs):
????????print(f"g2({cls=},?{args=},?{kwargs=})")
def?staticmethod_example():
????A.f1()
????A.f2()
????A().f1()
????A().f2()
????print(f'{A.f1=}')
????print(f'{A.f2=}')
????print(A().f1)
????print(A().f2)
????print(f'{type(A.f1)=}')
????print(f'{type(A.f2)=}')
def?main():
????A.f1()
????A.f2()
????A().f1()
????A().f2()
????A.g1()
????A.g2()
????A().g1()
????A().g2()
????print(f'{A.f1=}')
????print(f'{A.f2=}')
????print(f'{A().f1=}')
????print(f'{A().f2=}')
????print(f'{type(A.f1)=}')
????print(f'{type(A.f2)=}')
????print(f'{A.g1=}')
????print(f'{A.g2=}')
????print(f'{A().g1=}')
????print(f'{A().g2=}')
????print(f'{type(A.g1)=}')
????print(f'{type(A.g2)=}')
if?__name__?==?"__main__":
?main()
上面的類 StaticMethod?的作用相當(dāng)于裝飾器 staticmethod,類ClassMethod 相當(dāng)于裝飾器 classmethod。代碼的執(zhí)行結(jié)果如下:

可以看出,StaticMethod 和 ClassMethod 的作用和標(biāo)準(zhǔn)庫的效果是一樣的,也可以看出 classmethod 和 staticmethod 的區(qū)別就在于 classmethod 帶有類的信息,可以調(diào)用類的構(gòu)造函數(shù),在編程中具有更好的擴(kuò)展性。
最后的話
回答本文最初的問題,為什么 classmethod 更受標(biāo)準(zhǔn)庫的寵愛?是因為 classmethod 可以取代 staticmethod 的作用,而反過來卻不行。也就是說凡是使用 staticmethod 的地方,把 staticmethod 換成 classmethod,然后把函數(shù)增加第一個參數(shù) cls,后面調(diào)用的代碼可以不變,反過來卻不行,也就是說 classmethod 的兼容性更好。
另一方面,classmethod 可以在內(nèi)部再次調(diào)用類的構(gòu)造函數(shù),可以不修改現(xiàn)有實例生成新的實例,具有更強的靈活性和可擴(kuò)展性,因此更受寵愛,當(dāng)然這只是我的拙見,如果你有不同的想法,可以留言討論哈。
如果有幫助,還請點贊、在看、轉(zhuǎn)發(fā)、關(guān)注支持,感謝閱讀。
關(guān)注公眾號后,回復(fù)「送書」抽兩本《代碼大全2》,就這兩天了不要錯過哦。
