如何正確理解@classmethod與@staticmethod
Python面向物件程式設計中,類中定義的方法可以是 @classmethod 裝飾的類方法,也可以是 @staticmethod 裝飾的靜態方法,用的最多的還是不帶裝飾器的例項方法,如果把這幾個方法放一塊,對初學者來說無疑是一頭霧水,那我們該如何正確地使用它們呢?
先來看一個簡單示例:
class A(object): def m1(self, n): print("self:", self) @classmethod def m2(cls, n): print("cls:", cls) @staticmethoddef m3(n): pass a = A() a.m1(1) # self: <__main__.A object at 0x000001E596E41A90> A.m2(1) # cls: <class '__main__.A'> A.m3(1)
我在類中一共定義了3個方法,m1 是例項方法,第一個引數必須是 self(約定俗成的)。m2 是類方法,第一個引數必須是cls(同樣是約定俗成),m3 是靜態方法,引數根據業務需求定,可有可無。當程式執行時,大概發生了這麼幾件事(結合下面的圖來看)。
- 第一步:程式碼從第一行開始執行
class
命令,此時會建立一個類 A 物件(沒錯,類也是物件,一切皆物件嘛)同時初始化類裡面的屬性和方法,記住,此刻例項物件還沒創建出來。 - 第二、三步:接著執行
a=A()
,系統自動呼叫類的構造器,構造出例項物件 a - 第四步:接著呼叫
a.m1(1)
,m1 是例項方法,內部會自動把例項物件傳遞給 self 引數進行繫結,也就是說, self 和 a 指向的都是同一個例項物件。 - 第五步:呼叫
A.m2(1)
時,python內部隱式地把類物件傳遞給 cls 引數,cls 和 A 都指向類物件。
嚴格意義上來說,左邊的都是變數名,是物件的引用,右邊才是真正的對像,為了描述方便,我直接把 a 稱為物件,你應該明白我說物件其實是它所引用右邊的那個真正的物件。
再來看看每個方法各有什麼特性
例項方法
print(A.m1) # A.m1在py2中顯示為<unbound method A.m1><function A.m1 at 0x000002BF7FF9A488> print(a.m1) <bound method A.m1 of <__main__.A object at 0x000002BF7FFA2BE0>>
A.m1是一個還沒有繫結例項物件的方法,對於未繫結方法,呼叫 A.m1 時必須顯示地傳入一個例項物件進去,而 a.m1是已經綁定了例項的方法,python隱式地把物件傳遞給了self引數,所以不再手動傳遞引數,這是呼叫例項方法的過程。
A.m1(a, 1) # 等價 a.m1(1)
如果未繫結的方法 A.m1 不傳例項物件給 self 時,就會報引數缺失錯誤,在 py3 與 py2 中,兩者報的錯誤不一致,python2 要求第一個引數self是例項物件,而python3中可以是任意物件。
A.m1(1) TypeError: m1() missing 1 required positional argument: 'n'
類方法
print(A.m2) <bound method A.m2 of <class '__main__.A'>> print(a.m2) <bound method A.m2 of <class '__main__.A'>>
m2是類方法,不管是 A.m2 還是 a.m2,都是已經自動綁定了類物件A的方法,對於後者,因為python可以通過例項物件a找到它所屬的類是A,找到A之後自動繫結到 cls。
A.m2(1) # 等價 a.m2(1)
這使得我們可以在例項方法中通過使用 self.m2()這種方式來呼叫類方法和靜態方法。
def m1(self, n): print("self:", self) self.m2(n)
靜態方法
print(A.m3) <function A.m3 at 0x000002BF7FF9A840> print(a.m3) <function A.m3 at 0x000002BF7FF9A840>
m3是類裡面的一個靜態方法,跟普通函式沒什麼區別,與類和例項都沒有所謂的繫結關係,它只不過是碰巧存在類中的一個函式而已。不論是通過類還是例項都可以引用該方法。
A.m3(1) # 等價 a.m3(1) ``` 以上就是幾個方法的基本介紹。現在把幾個基本的概念理清楚了,那麼現在來說說幾個方法之間的使用場景以及他們之間的優缺點。 ### 應用場景 靜態方法的使用場景: 如果在方法中不需要訪問任何例項方法和屬性,純粹地通過傳入引數並返回資料的功能性方法,那麼它就適合用靜態方法來定義,它節省了例項化物件的開銷成本,往往這種方法放在類外面的模組層作為一個函式存在也是沒問題的,而放在類中,僅為這個類服務。例如下面是微信公眾號開發中驗證微信簽名的一個例子,它沒有引用任何類或者例項相關的屬性和方法。 ```python from hashlib import sha1 import tornado.web class SignatureHandler(tornado.web.RequestHandler): def get(self): """ 根據簽名判斷請求是否來自微信 """ signature = self.get_query_argument("signature", None) echostr = self.get_query_argument("echostr", None) timestamp = self.get_query_argument("timestamp", None) nonce = self.get_query_argument("nonce", None) if self._check_sign(TOKEN, timestamp, nonce, signature): logger.info("微信簽名校驗成功") self.write(echostr) else: self.write("你不是微信發過來的請求") @staticmethod def _check_sign(token, timestamp, nonce, signature): sign = [token, timestamp, nonce] sign.sort() sign = "".join(sign) sign = sha1(sign).hexdigest() return sign == signature
類方法的使用場景有:
作為工廠方法建立例項物件,例如內建模組 datetime.date 類中就有大量使用類方法作為工廠方法,以此來建立date物件。
class date: def __new__(cls, year, month=None, day=None): self = object.__new__(cls) self._year = year self._month = month self._day = day return self @classmethod def fromtimestamp(cls, t): y, m, d, hh, mm, ss, weekday, jday, dst = _time.localtime(t) return cls(y, m, d) @classmethod def today(cls): t = _time.time() return cls.fromtimestamp(t)
如果希望在方法裡面呼叫靜態類,那麼把方法定義成類方法是合適的,因為要是定義成靜態方法,那麼你就要顯示地引用類A,這對繼承來說可不是一件好事情。
class A: @staticmethod def m1() pass @staticmethod def m2(): A.m1() # bad @classmethod def m3(cls): cls.m1() # good
其實也不算是什麼深入理解吧,最多算是明白怎麼用,真要深入理解恐怕還要另寫一篇文章,有興趣的可以去了解一下Python的描述符。
關注公眾號「Python之禪」(id:vttalk)獲取最新文章