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>類同樣是對象)
何時使用這種方法?類方法一般用于下面兩種:
- 工廠方法,被用來創建一個類的實例,完成一些預處理工作。如果我們使用一個
@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)
抽象方法
抽象方法在一個基類中定義,但是可能不會有任何的實現。在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_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列表