轉自 http://www.xdarui.com/archives/261.html
原文http://julien.danjou.info/blog/2013/guide-python-static-class-abstract-methods
代碼審查對發現人們很難理解的問題來說絕對是一種很讚的方式。我(Julien Danjou)最近在做OpenStack patches校對的時候發現人們不能夠正確的理解與使用Python提供的不同的函數修飾。所以我這裏提供一個鏈接以便下次review的時候帖出。
Python中方法是怎麼工作的
一個方法就是一個函數,它依付於一個類之上。你可以像這樣去定義和使用:
- >>> 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>
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
的實例中去,而方法的第一個參數希望接收一個實例。(Python2中實例必需是對應的那個類,而Python3中可以是任何東西)讓我這樣試試:
- >>> Pizza.get_size(Pizza(42))
- 42
所以Python幫我們做了這些事情,給所有Pizza
的實例綁定所有它的方法。這意味着get_size
是Pizza
實例的已綁定方法:方法的第一個參數是實
例它本身。
- >>> Pizza(42).get_size
- <bound method Pizza.get_size of <__main__.Pizza object at 0x7f3138827910>>
- >>> Pizza(42).get_size()
- 42
get_size()
方法傳入任何參數,因爲它已經綁定了,Pizza
實例自身會自動設置成方法的第一個參數。這可以證明:
- >>> m = Pizza(42).get_size
- >>> m()
- 42
Pizza
對象保存引用,因其方法與對象綁定了,所以其方法本身就夠了。但是如果我們想知道這個方法倒底綁定到了哪個對象上該怎麼做?這有一
個小技巧:
- >>> m = Pizza(42).get_size
- >>> m.__self__
- <__main__.Pizza object at 0x7f3138827910>
- >>> # You could guess, look at this:
- ...
- >>> m == m.__self__.get_size
- True
在Python3中,直接使用類中的方法不再視爲未綁定方法,而是一個簡單的函數,而只是在需要的時候才綁定到對象上。所以基本原則沒變只是簡化了模型。
- >>> 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
也能工作,但會傳遞一個並不會使用的參數self
。這裏個註解@staticmethod
可以幫我們做這些情況:
-
Python不會給每一個
Pizza
實例都增加一個綁定的方法。綁定方法也是一個函數,所以創建它們也有額外的開銷。通過靜態方法可以避免這些開銷:- >>> Pizza().cook is Pizza().cook
- False
- >>> Pizza().mix_ingredients is Pizza.mix_ingredients
- True
- >>> Pizza().mix_ingredients is Pizza().mix_ingredients
- True
類方法
說到這,倒底什麼是類方法?類方法是指不是綁定在對象上而是類上的方法!。
- >>> 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
- True
- >>> Pizza.get_radius()
- 42
-
工廠方法,用於創建一個類的實例比如一些預處理。如果我們用
@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())
-
靜態方法之前的調用:如果你把靜態方法分散成很多靜態方法,你不能直接硬編碼類名而應該用類方法。使用這種方式申明一個類方法,
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)
- class Pizza(object):
- def get_radius(self):
- raise NotImplementedError
- >>> Pizza()
- <__main__.Pizza object at 0x7fb747353d90>
- >>> Pizza().get_radius()
- Traceback (most recent call last):
- File "<stdin>", line 1, in <module>
- File "<stdin>", line 3, in get_radius
- NotImplementedError
- import abc
- class BasePizza(object):
- __metaclass__ = abc.ABCMeta
- @abc.abstractmethod
- def get_radius(self):
- """Method that should do something."""
- >>> BasePizza()
- Traceback (most recent call last):
- File "<stdin>", line 1, in <module>
- TypeError: Can't instantiate abstract class BasePizza with abstract methods get_radius
- 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
- 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
- import abc
- class BasePizza(object):
- __metaclass__ = abc.ABCMeta
- ingredient = ['cheese']
- @classmethod
- @abc.abstractmethod
- def get_ingredients(cls):
- """Returns the ingredient list."""
- return cls.ingredients
- 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()
抽象方法
抽象方法是基類中定義的方法,但卻沒有任何實現。在Java中,可以把方法申明成一個接口。而在Python中實現一個抽象方法的最簡便方法是:
Pizza
繼承下來的子類都必需實現get_radius
方法,否則就會產生一個錯誤。這種方式實現在抽象方法也有缺點,如果你寫了一個類繼承自Pizza
但卻忘了實現get_radius
時,只有當你用到了那個方法時纔會拋錯。abc
以及其特定的方法,一旦你嘗試去實例化BasePizza
類或任意從其繼承下來的類的時候都會得到一個錯誤。混搭使用靜態、類以及抽象方法
當創建類及繼承時,你就得使用混搭這些方式的方法了。這裏一些提示。
請記住,申明類時應先申明成抽象類,並且不要把類的原型寫死。這意味着,抽象方法必需得實現,但我可以在實現抽象方法時可以任意指定參數列表。
Calzone
繼承BasePizza
得按要求實現對應的接口,這非常有用。這意味着,我們可以實現成一個類或一個靜態方法,比如:BasePizza
所有要求。事實上一個實現細節是get_ingredients
方法返回結果時無需關心對象,不用關心有何約束。因此,你不能強 制要求抽象方法或類的實現是有規律的,或者說不應該這樣做。從Python3開始(Python2不會生效,見issue5867),可以在@abstractmethod
之上使用@staticmethod
和@classmethod
。get_ingredients
作爲類的方法,你就錯了。這只是意味着你在BasePizza
類中,實現get_ingredients
只是一個類方法。抽象類中實現抽象方法?是的!Python中抽象方法與Java相反。你可以在抽象方法中加入代碼並通過
super()
調用:BasePizza
類中繼承下來的pizza都得實現get_ingredients
方法,但是我們可以使用super()
通過默認機制獲取配料表。