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

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'>>

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

  1. 工廠方法,被用來創建一個類的實例,完成一些預處理工作。如果我們使用一個@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())
  1. 靜態方法調靜態方法:如果你將一個靜態方法分解爲幾個靜態方法,你不需要硬編碼類名但可以使用類方法。使用這種方式來聲明我們的方法,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_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列表 。

發佈了41 篇原創文章 · 獲贊 51 · 訪問量 6萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章