如何使用靜態方法、類方法或者抽象方法

Neil Zhu,簡書ID Not_GOD,University AI 創始人 & Chief Scientist,致力于推進世界人工智能化進程。制定并實施 UAI 中長期增長戰略和目標,帶領團隊快速成長為人工智能領域最專業的力量。
作為行業領導者,他和UAI一起在2014年創建了TASA(中國最早的人工智能社團), DL Center(深度學習知識中心全球價值網絡),AI growth(行業智庫培訓)等,為中國的人工智能人才建設輸送了大量的血液和養分。此外,他還參與或者舉辦過各類國際性的人工智能峰會和活動,產生了巨大的影響力,書寫了60萬字的人工智能精品技術內容,生產翻譯了全球第一本深度學習入門書《神經網絡與深度學習》,生產的內容被大量的專業垂直公眾號和媒體轉載與連載。曾經受邀為國內頂尖大學制定人工智能學習規劃和教授人工智能前沿課程,均受學生和老師好評。

原文地址
代碼檢視是一種可以發現令程序員們頭疼的事件的方法。近期檢視OpenStack patches,我發現人們錯誤地試用了多個Python提供給方法的不同的聲明(decorator)。所以,我嘗試寫一些可以在下次代碼檢視時可以寄給那些伙計的東西。
Python中方法的運作
=============
方法是作為類的屬性(attribute)存儲的函數。你可以以下面的方式聲明和獲取函數:

>>> class Pizza(object):
...     def __init__(self, size):
...         self.size = size
...     def get_size(self):
...         return self.size
...
>>> Pizza.get_size
<unbound method Pizza.get_size>

Python告訴你的是,類Pizza的屬性get_size是一個非綁定的方法。這又指什么呢?很快我們就會知道,試著調用一下:

>>> Pizza.get_size()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unbound method get_size() must be called with Pizza instance as first argument (got nothing instead)

這里我們不能調用這個方法是因為它沒有被綁定到任一Pizza的實例上。一個方法需要一個實例作為它第一個參數(在Python 2中它必須是對應類的實例;在Python 3中可以是任何東西)。我們現在試試:

>>> Pizza.get_size(Pizza(42))
42

現在可以了!我們試用一個實例作為get_size方法的第一個參數調用了它,所以一切變得很美好。但是你很快會同意,這并不是一個很漂亮的調用方法的方式;因為每次我們想調用這個方法都必須使用到類。并且,如果我們不知道對象是哪個類的實例,這種方式就不方便了。
所以,Python為我們準備的是,它將類Pizza的所有的方法綁定到此類的任何實例上。這意味著類Pizza的任意實例的屬性get_size是一個已綁定的方法:第一個參數是實例本身的方法。

>>> Pizza(42).get_size
<bound method Pizza.get_size of <__main__.Pizza object at 0x10314b310>>
>>> Pizza(42).get_size()
42

如我們預期,現在不需要提供任何參數給get_size,因為它已經被綁定(bound),它的self參數是自動地設為Pizza類的實例。下面是一個更好的證明:

>>> m = Pizza(42).get_size
>>> m
<bound method Pizza.get_size of <__main__.Pizza object at 0x10314b350>>
>>> m()
42

因此,你甚至不要保存一個對Pizza對象的飲用。它的方法已經被綁定在對象上,所以這個方法已經足夠。
但是如何知道已綁定的方法被綁定在哪個對象上?技巧如下:

>>> m = Pizza(42).get_size
>>> m.__self__
<__main__.Pizza object at 0x10314b390>
>>> m == m.__self__.get_size
True

易見,我們仍然保存著一個對對象的引用,當需要知道時也可以找到。
在Python 3中,歸屬于一個類的函數不再被看成未綁定方法unbound method),但是作為一個簡單的函數,如果要求可以綁定在對象上。所以,在Python 3中原理是一樣的,模型被簡化了。

>>> class Pizza(object):
...     def __init__(self, size):
...         self.size = size
...     def get_size(self):
...         return self.size
...
>>> Pizza.get_size
<function Pizza.get_size at 0x7f307f984dd0>

靜態方法

靜態方法是一類特殊的方法。有時,我們需要寫屬于一個類的方法,但是不需要用到對象本身。例如:

class Pizza(object):
    @staticmethod
    def mix_ingredients(x, y):
        return x + y
 
    def cook(self):
        return self.mix_ingredients(self.cheese, self.vegetables)

這里,將方法mix_ingredients作為一個非靜態的方法也可以work,但是給它一個self的參數將沒有任何作用。這兒的decorator@staticmethod帶來一些特別的東西:

>>> Pizza().cook is Pizza().cook
False
>>> Pizza().mix_ingredients is Pizza().mix_ingredients
True
>>> Pizza().mix_ingredients is Pizza.mix_ingredients
True
>>> Pizza()
<__main__.Pizza object at 0x10314b410>
>>> Pizza()
<__main__.Pizza object at 0x10314b510>
>>> 
  • Python不需要對每個實例化的Pizza對象實例化一個綁定的方法。綁定的方法同樣是對象,創建它們需要付出代價。這里的靜態方法避免了這樣的情況:
  • 降低了閱讀代碼的難度:看到@staticmethod便知道這個方法不依賴與對象本身的狀態;
  • 允許我們在子類中重載mix_ingredients方法。如果我們使用在模塊最頂層定義的函數mix_ingredients,一個繼承自Pizza的類若不重載cook,可能不可以改變混合成份(mix_ingredients)的方式。

類方法

什么是類方法?類方法是綁定在類而非對象上的方法!

>>> class Pizza(object):
...     radius = 42
...     @classmethod
...     def get_radius(cls):
...         return cls.radius
... 
>>> Pizza.get_radius
<bound method type.get_radius of <class '__main__.Pizza'>>
>>> Pizza().get_radius
<bound method type.get_radius of <class '__main__.Pizza'>>
>>> Pizza.get_radius is Pizza().get_radius
False
>>> Pizza.get_radius()
42

此處有問題。原文中
>>> Pizza.get_radius is Pizza().get_radius True
還需要check一下。

不管你如何使用這個方法,它總會被綁定在其歸屬的類上,同時它第一個參數是類本身(記?。?strong>類同樣是對象)
何時使用這種方法?類方法一般用于下面兩種:

  1. 工廠方法,被用來創建一個類的實例,完成一些預處理工作。如果我們使用一個@staticmethod靜態方法,我們可能需要在函數中硬編碼Pizza類的名稱,使得任何繼承自Pizza類的類不能使用我們的工廠用作自己的目的。
class Pizza(object):
    def __init__(self, ingredients):
        self.ingredients = ingredients
 
    @classmethod
    def from_fridge(cls, fridge):
        return cls(fridge.get_cheese() + fridge.get_vegetables())
  1. 靜態方法調靜態方法:如果你將一個靜態方法分解為幾個靜態方法,你不需要硬編碼類名但可以使用類方法。使用這種方式來聲明我們的方法,Pizza這個名字不需要直接被引用,并且繼承和方法重載將會完美運作。
class Pizza(object):
    def __init__(self, radius, height):
        self.radius = radius
        self.height = height
 
    @staticmethod
    def compute_circumference(radius):
         return math.pi * (radius ** 2)
 
    @classmethod
    def compute_volume(cls, height, radius):
         return height * cls.compute_circumference(radius)
 
    def get_volume(self):
        return self.compute_volume(self.height, self.radius)

抽象方法

抽象方法在一個基類中定義,但是可能不會有任何的實現。在Java中,這被描述為一個接口的方法。
所以Python中最簡單的抽象方法是:

class Pizza(object):
    def get_radius(self):
        raise NotImplementedError

任何繼承自Pizza的類將實現和重載get_radius方法,否則會出現異常。這種獨特的實現抽象方法的方式也有其缺點。如果你寫一個繼承自Pizza的類,忘記實現get_radius,錯誤將會在你使用這個方法的時候才會出現。

>>> Pizza()
<__main__.Pizza object at 0x106f381d0>
>>> Pizza().get_radius()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in get_radius
NotImplementedError

有種提前引起錯誤發生的方法,那就是當對象被實例化時,使用Python提供的abc模塊。

import abc
class BasePizza(object):
    __metaclass__ = abc.ABCMeta
    
    @abc.abstractmethod
    def get_radius(self):
        """Method that should do something."""

使用abc和它的特類,一旦你試著實例化BasePizza或者其他繼承自它的類,就會得到TypeError

>>> BasePizza()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Can't instantiate abstract class BasePizza with abstract methods get_radius

混合靜態方法、類方法和抽象方法

當我們構建類和繼承關系時,終將會碰到要混合這些方法decorator的情況。下面提幾個tip。
記住聲明一個類為抽象類時,不要冷凍那個方法的prototype。這是指這個方法必須被實現,不過是可以使用任何參數列表來實現。

import abc
class BasePizza(object):
    __metaclass__  = abc.ABCMeta
    @abc.abstractmethod
    def get_ingredients(self):
         """Returns the ingredient list."""
class Calzone(BasePizza):
    def get_ingredients(self, with_egg=False):
        egg = Egg() if with_egg else None
        return self.ingredients + egg

這個是合法的,因為Calzone完成了為BasePizza類對象定義的接口需求。就是說,我們可以把它當作一個類方法或者靜態方法來實現,例如:

import abc
class BasePizza(object):
    __metaclass__  = abc.ABCMeta
    @abc.abstractmethod
    def get_ingredients(self):
         """Returns the ingredient list."""
class DietPizza(BasePizza):
    @staticmethod
    def get_ingredients():
        return None

這樣做同樣爭取,并且完成了與BasePizza抽象類達成一致的需求。get_ingredients方法不需要知道對象,這是實現的細節,而非完成需求的評價指標。
因此,你不能強迫抽象方法的實現是正常的方法、類方法或者靜態方法,并且可以這樣說,你不能。從Python 3開始(這就不會像在Python 2中那樣work了,見issue5867),現在可以在@abstractmethod之上使用@staticmethod@classmethod了。

import abc
class BasePizza(object):
    __metaclass__  = abc.ABCMeta
 
    ingredient = ['cheese']
 
    @classmethod
    @abc.abstractmethod
    def get_ingredients(cls):
         """Returns the ingredient list."""
         return cls.ingredients

不要誤解:如果你認為這是強迫你的子類將get_ingredients實現為一個類方法,那就錯了。這個是表示你實現的get_ingredientsBasePizza類中是類方法而已。
在一個抽象方法的實現?是的!在Python中,對比與Java接口,你可以在抽象方法中寫代碼,并且使用super()調用:

import abc
class BasePizza(object):
    __metaclass__  = abc.ABCMeta
    default_ingredients = ['cheese']
    @classmethod
    @abc.abstractmethod
    def get_ingredients(cls):
         """Returns the ingredient list."""
         return cls.default_ingredients
class DietPizza(BasePizza):
    def get_ingredients(self):
        return ['egg'] + super(DietPizza, self).get_ingredients()

現在,每個你從BasePizza類繼承而來的pizza類將重載get_ingredients方法,但是可以使用默認機制來使用super()獲得ingredient列表

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,991評論 19 139
  • 靜態方法 靜態方法是一類特殊的方法。有時,需要寫屬于一個類的方法,但是不需要用到對象本身。例如: class Pi...
    chen_000閱讀 280評論 0 0
  • 1. Java基礎部分 基礎部分的順序:基本語法,類相關的語法,內部類的語法,繼承相關的語法,異常的語法,線程的語...
    子非魚_t_閱讀 31,769評論 18 399
  • 丙申九月,逢國慶,國人多出游,余恐長路漫堵,再三思忖,于3日,與三倆好友攜家人東游至石島一帶。 石島...
    陌嶺先生閱讀 632評論 0 0
  • 不奢于看你 怕目光無法轉移 不想聽見任何關于你 怕內心的波瀾無法平靜 不要想你 怕夜里的世界都是你 不要愛你了 可...
    釷素閱讀 92評論 0 1