The definitive guide on how to use static, class or abstract methods in Python
Doing code reviews is a great way to discover things that people might struggle to comprehend. While proof-reading OpenStack patches recently, I spotted that people were not using correctly the various decorators Python provides for methods. So here’s my attempt at providing me a link to send them to in my next code reviews.
1 How methods work in Python
方法是作爲類的屬性(attribute)存儲的函數。你可以以下面的方式聲明和獲取函數:
In [1]: class Pizza(object):
...: def __init__(self, size):
...: self.size = size
...: def get_size(self):
...: return self.size
...:
In [2]: Pizza.get_size
Out[2]: <function __main__.Pizza.get_size(self)>
原參考資料中運行後提示: <unbound method Pizza.get_size>
也就是告訴我們類Pizza
的屬性get_size
是一個非綁定的方法。這裏我在Ipython和IDLE中實測結果:
<function __main__.Pizza.get_size(self)>
,
<function Pizza.get_size at 0x00000161A24E2EA0>
,
提示我Pizza.get_size
是一個函數,嘗試調用一下方法get_size()
。
In [3]: Pizza.get_size()
Traceback (most recent call last):
File "<ipython-input-3-8f6eefb4f713>", line 1, in <module>
Pizza.get_size()
TypeError: get_size() missing 1 required positional argument: 'self'
不能調用,提示缺失一個位置參數’self’。其實質是因爲它沒有被綁定到任一Pizza
的實例上。一個方法需要一個實例作爲它第一個參數(在Python 2中它必須是對應類的實例;在Python 3中可以是任何東西)。我們現在試試:
In [4]: Pizza.get_size(Pizza(42))
Out[4]: 42
現在可以了!我們試用一個實例作爲get_size
方法的第一個參數調用了它,所以一切變得很美好。但是你很快會同意,這並不是一個很漂亮的調用方法的方式;因爲每次我們想調用這個方法都必須使用到類。並且,如果我們不知道對象是哪個類的實例,這種方式就不方便了。
所以,Python爲我們準備的是,它將類Pizza
的所有的方法綁定到此類的任何實例上。這意味着類Pizza
的任意實例的屬性get_size
是一個已綁定的方法:第一個參數是實例本身的方法。
In [10]: Pizza(42).get_size
Out[10]: <bound method Pizza.get_size of <__main__.Pizza object at 0x0000015BD9C34EB8>>
In [11]: Pizza(42).get_size()
Out[11]: 42
如我們預期,現在不需要提供任何參數給get_size
,因爲它已經被綁定(bound),它的self
參數是自動地設爲Pizza
類的實例。下面是一個更好的證明:
In [5]: m = Pizza(42)
In [6]: m
Out[6]: <__main__.Pizza at 0x15bd9c34438>
In [7]: m = Pizza(42).get_size
In [8]: m
Out[8]: <bound method Pizza.get_size of <__main__.Pizza object at 0x0000015BD9C34E10>>
In [9]: m()
Out[9]: 42
因此,你甚至不要保存一個對Pizza對象的飲用。它的方法已經被綁定在對象上,所以這個方法已經足夠。 但是如何知道已綁定的方法被綁定在哪個對象上?技巧如下:
In [12]: m = Pizza(42).get_size
In [13]: m.__self__
Out[13]: <__main__.Pizza at 0x15bd9c5e128>
In [14]: m == m.__self__.get_size
Out[14]: True
易見,我們仍然保存着一個對對象的引用,當需要知道時也可以找到。 在Python 3中,歸屬於一個類的函數不再被看成未綁定方法(unbound method),但是作爲一個簡單的函數,如果要求可以綁定在對象上。所以,在Python 3中原理是一樣的,模型被簡化了。
參考第一段代碼
2 Static methods
靜態方法是一類特殊的方法。有時,我們需要寫屬於一個類的方法,但是不需要用到對象本身。例如:
In [17]: 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
帶來一些特別的東西:
In [18]: Pizza().cook is Pizza().cook
Out[18]: False
In [19]: Pizza().mix_ingredients is Pizza().mix_ingredients
Out[19]: True
In [20]: Pizza.mix_ingredients is Pizza.mix_ingredients
Out[20]: True
In [21]: Pizza()
Out[21]: <__main__.Pizza at 0x15bd9c5ea20>
- Python不需要對每個實例化的
Pizza
對象實例化一個綁定的方法。綁定的方法同樣是對象,創建它們需要付出代價。這裏的靜態方法避免了這樣的情況: - 降低了閱讀代碼的難度:看到
@staticmethod
便知道這個方法不依賴與對象本身的狀態; - 允許我們在子類中重載
mix_ingredients
方法。如果我們使用在模塊最頂層定義的函數mix_ingredients
,一個繼承自Pizza
的類若不重載cook
,可能不可以改變混合成份(mix_ingredients)的方式。
3 Class methods
什麼是類方法?類方法是綁定在類而非對象上的方法!
In [22]: class Pizza(object):
...: radius = 42
...: @classmethod
...: def get_radius(cls):
...: return cls.radius
...:
In [23]: Pizza.get_radius
Out[23]: <bound method Pizza.get_radius of <class '__main__.Pizza'>>
In [24]: Pizza.get_radius is Pizza().get_radius
Out[24]: False
In [25]: Pizza.get_radius()
Out[25]: 42
In [26]: Pizza().get_radius
Out[26]: <bound method Pizza.get_radius of <class '__main__.Pizza'>>
不管你如何使用這個方法,它總會被綁定在其歸屬的類上,同時它第一個參數是類本身(記住:類同樣是對象) 何時使用這種方法?類方法一般用於下面兩種:
- 工廠方法,被用來創建一個類的實例,完成一些預處理工作。如果我們使用一個
@staticmethod
靜態方法,我們可能需要在函數中硬編碼Pizza類的名稱,使得任何繼承自Pizza類的類不能使用我們的工廠用作自己的目的。
In [27]: class Pizza(object):
...: def __init__(self, ingredients):
...: self.ingredients = ingredients
...: @classmethod
...: def from_fridge(cls, fridge):
...: return cls(fridge.get_cheese() + fridge.get_vegetables())
靜態方法調靜態方法:如果你將一個靜態方法分解爲幾個靜態方法,你不需要硬編碼類名但可以使用類方法。使用這種方式來聲明我們的方法,
Pizza
這個名字不需要直接被引用,並且繼承和方法重載將會完美運作。In [28]: 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)
4 Abstract methods
抽象方法在一個基類中定義,但是可能不會有任何的實現。在Java中,這被描述爲一個接口的方法。 所以Python中最簡單的抽象方法是:
In [29]: class Pizza(object):
...: def get_radius(self):
...: raise NotImplementedError
任何繼承自Pizza
的類將實現和重載get_radius
方法,否則會出現異常。這種獨特的實現抽象方法的方式也有其缺點。如果你寫一個繼承自Pizza
的類,忘記實現get_radius
,錯誤將會在你使用這個方法的時候纔會出現。
In [29]: class Pizza(object):
...: def get_radius(self):
...: raise NotImplementedError
...:
In [30]: Pizza()
Out[30]: <__main__.Pizza at 0x15bd9c72e80>
In [31]: Pizza().get_radius()
Traceback (most recent call last):
File "<ipython-input-31-00c16207f44e>", line 1, in <module>
Pizza().get_radius()
File "<ipython-input-29-eb9ab50a7580>", line 3, in get_radius
raise NotImplementedError
NotImplementedError
有種提前引起錯誤發生的方法,那就是當對象被實例化時,使用Python提供的abc
模塊。
In [32]: import abc
...: class BasePizza(object):
...: __metaclass__ = abc.ABCMeta
...:
...: @abc.abstractmethod
...: def get_radius(self):
...: """Method that should do something."""
使用abc
和它的特類,一旦你試着實例化BasePizza
或者其他繼承自它的類,就會得到TypeError
In [32]: import abc
...: class BasePizza(object):
...: __metaclass__ = abc.ABCMeta
...:
...: @abc.abstractmethod
...: def get_radius(self):
...: """Method that should do something."""
...:
In [33]: BasePizza()
Out[33]: <__main__.BasePizza at 0x15bd9c72fd0>
In [34]: BasePizza
Out[34]: __main__.BasePizza
In [35]: BasePizza().get_radius()
In [36]: BasePizza().get_radius
Out[36]: <bound method BasePizza.get_radius of <__main__.BasePizza object at 0x0000015BD9C727F0>>
5 Mixing static, class and abstract methods
當我們構建類和繼承關係時,終將會碰到要混合這些方法decorator的情況。下面提幾個tip。 記住聲明一個類爲抽象類時,不要冷凍那個方法的prototype。這是指這個方法必須被實現,不過是可以使用任何參數列表來實現。
In [37]: 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
類對象定義的接口需求。就是說,我們可以把它當作一個類方法或者靜態方法來實現,例如:
In [38]: import abc
...: class BasePizza(object):
...: __metaclass__ = abc.ABCMeta
...: @abc.abstractmethod
...: def get_ingredients(self):
...: """Returns the ingredient list."""
...:
In [39]: class DietPizza(BasePizza):
...: @staticmethod
...: def get_ingredients():
...: return None
...:
這樣做同樣爭取,並且完成了與BasePizza
抽象類達成一致的需求。get_ingredients
方法不需要知道對象,這是實現的細節,而非完成需求的評價指標。
因此,你不能強迫抽象方法的實現是正常的方法、類方法或者靜態方法,並且可以這樣說,你不能。從Python 3開始(這就不會像在Python 2中那樣work了,見issue5867),現在可以在@abstractmethod
之上使用@staticmethod
和@classmethod
了。
In [40]: 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_ingredients
在BasePizza
類中是類方法而已。
在一個抽象方法的實現?是的!在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列表 。