方法在Python中是如何工作的
方法就是一個函數,它作為一個類屬性而存在,你可以用如下方式來聲明、訪問一個函數:
>>> class Pizza(object):... def __init__(self, size):... self.size = size... def get_size(self):... return self.size...>>> Pizza.get_size
Python在告訴你,屬性_get_size是類Pizza的一個未Binder 方法。這是什麼意思呢?很快我們就會知道答案:
>>> Pizza.get_size()Traceback (most recent call last): File "", line 1, in 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))
太棒了,現在用一個執行個體作為它的的第一個參數來調用,整個世界都清靜了,如果我說這種調用方式還不是最方便的,你也會這麼認為的;沒錯,現在每次調用這個方法的時候我們都不得不引用這個類,如果不知道哪個類是我們的對象,長期看來這種方式是行不通的。
那麼Python為我們做了什麼呢,它綁定了所有來自類_Pizza的方法以及該類的任何一個執行個體的方法。也就意味著現在屬性get_size是Pizza的一個執行個體對象的Binder 方法,這個方法的第一個參數就是該執行個體本身。
>>> Pizza(42).get_size>>>> Pizza(42).get_size()42
和我們預期的一樣,現在不再需要提供任何參數給_get_size,因為它已經是綁定的,它的self參數會自動地設定給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_sizeTrue
顯然,該對象仍然有一個引用存在,只要你願意你還是可以把它找回來。
在Python3中,依附在類上的函數不再當作是未綁定的方法,而是把它當作一個簡單地函數,如果有必要它會綁定到一個對象身上去,原則依然和Python2保持一致,但是模組更簡潔:
>>> class Pizza(object):... def __init__(self, size):... self.size = size... def get_size(self):... return self.size...>>> Pizza.get_size
靜態方法
靜態方法是一類特殊的方法,有時你可能需要寫一個屬於這個類的方法,但是這些代碼完全不會使用到執行個體對象本身,例如:
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對象執行個體初始化一個Binder 方法,Binder 方法同樣是對象,但是建立他們需要成本,而靜態方法就可以避免這些。
>>> Pizza().cook is Pizza().cookFalse>>> Pizza().mix_ingredients is Pizza.mix_ingredientsTrue>>> Pizza().mix_ingredients is Pizza().mix_ingredientsTrue
可讀性更好的代碼,看到@staticmethod我們就知道這個方法並不需要依賴對象本身的狀態。
可以在子類中被覆蓋,如果是把mix_ingredients作為模組的頂層函數,那麼繼承自Pizza的子類就沒法改變pizza的mix_ingredients了如果不覆蓋cook的話。
類方法
話雖如此,什麼是類方法呢?類方法不是綁定到對象上,而是綁定在類上的方法。
>>> class Pizza(object):... radius = 42... @classmethod... def get_radius(cls):... return cls.radius... >>> >>> Pizza.get_radius>>>> Pizza().get_radius>>>> Pizza.get_radius is Pizza().get_radiusTrue>>> Pizza.get_radius()42
無論你用哪種方式訪問這個方法,它總是綁定到了這個類身上,它的第一個參數是這個類本身(記住:類也是對象)。
什麼時候使用這種方法呢?類方法通常在以下兩種情境是非常有用的:
Factory 方法:它用於建立類的執行個體,例如一些預先處理。如果使用@staticmethod代替,那我們不得不寫入程式碼Pizza類名在函數中,這使得任何繼承Pizza的類都不能使用我們這個Factory 方法給它自己用。
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_area(radius): return math.pi * (radius ** 2) @classmethod def compute_volume(cls, height, radius): return height * cls.compute_area(radius) def get_volume(self): return self.compute_volume(self.height, self.radius)
抽象方法
抽象方法是定義在基類中的一種方法,它沒有提供任何實現,類似於Java中介面(Interface)裡面的方法。
在Python中實現抽象方法最簡單地方式是:
class Pizza(object): def get_radius(self): raise NotImplementedError
任何繼承自_Pizza的類必須覆蓋實現方法get_radius,否則會拋出異常。
這種抽象方法的實現有它的弊端,如果你寫一個類繼承Pizza,但是忘記實現get_radius,異常只有在你真正使用的時候才會拋出來。
>>> Pizza()<__main__.Pizza object at 0x7fb747353d90>>>> Pizza().get_radius()Traceback (most recent call last): File "", line 1, in File "", line 3, in get_radiusNotImplementedError
還有一種方式可以讓錯誤更早的觸發,使用Python提供的abc模組,對象被初始化之後就可以拋出異常:
import abc class BasePizza(object): __metaclass__ = abc.ABCMeta @abc.abstractmethod def get_radius(self): """Method that should do something."""
使用abc後,當你嘗試初始化BasePizza或者任何子類的時候立馬就會得到一個TypeError,而無需等到真正調用get_radius的時候才發現異常。
>>> BasePizza()Traceback (most recent call last): File "", line 1, in 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
這樣是允許的,因為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方法並不需要知道返回結果是什麼,結果是實現細節,不是契約條件。
因此,你不能強制抽象方法的實現是一個常規方法、或者是類方法還是靜態方法,也沒什麼可爭論的。從Python3開始(在Python2中不能如你期待的運行,見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()來調用它。(譯註:在Java8中,介面也提供的預設方法,允許在介面中寫方法的實現)
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()
這個例子中,你構建的每個pizza都通過繼承BasePizza的方式,你不得不覆蓋get_ingredients方法,但是能夠使用預設機制通過super()來擷取ingredient列表。