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>
Python在這裡說明了什麼?Pizza類的屬性get_size是unbound(未繫結的),這代表什麼含義?我們呼叫一下就明白了:
>>> 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)
我們無法呼叫它(get_size),因為它沒有繫結到Pizza的任何例項上,而且一個方法需要一個例項作為它的第一個引數(Python2中必須是類的例項,Python3沒有這個強制要求),讓我們試一下:
>>> Pizza.get_size(Pizza(42))
42
我們使用一個例項作為這個方法的第一個引數來呼叫它,沒有出現任何問題。但是如果我說這不是一個方便的呼叫方法的方式,你將會同意我的觀點。我們每次呼叫方法都要涉及(這裡我理解是引用)類
來看Python打算為我們做些什麼,就是它從Pizza類中繫結所有的方法到這個類的任何例項上。意思就是Pizza例項化後get_size這個屬性是一個繫結方法,方法的第一個引數會是例項物件自己
>>> Pizza(42).get_size
<bound method Pizza.get_size of <__main__.Pizza object at 0x7f3138827910>>
>>> 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_size
True
明顯可以看出,我們仍然保持對我們物件的引用,而且如果需要我們可以找到它
在Python3中,類中的函式不再被認為是未繫結的方法(應該是作為函式存在),如果需要,會作為一個函式繫結到物件上,所以原理是一樣的(和Python2),只是模型被簡化了
>>> 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
簡化了程式碼的可讀性,看到@staticmethod我們就會知道這個方法不會依賴這個物件的狀態(一國兩制,高度自治)
允許在子類中重寫mix_ingredients方法。如果我們在頂級模型中定義了mix_ingredients函式,繼承自Pizza的類除了重寫,否則無法改變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 == Pizza().get_radius
True
>>> Pizza.get_radius()
42
無論以何種方式訪問這個方法,它都會被繫結到類中,它的第一個引數必須是類本身(記住類也是物件)
什麼時候使用類方法,類方法在以下兩種場合會有很好的效果:
1、工廠方法,為類建立例項,例如某種程度的預處理。如果我們使用@staticmethod代替,我們必須要在程式碼中硬編碼Pizza(寫死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())
2、使用靜態方法呼叫靜態方法,如果你需要將一個靜態方法拆分為多個,可以使用類方法來避免硬編碼類名。使用這種方法來宣告我們的方法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)
抽象方法
抽象方法是定義在基類中的,可以是不提供任何功能程式碼的方法
在Python中簡單的寫抽象方法的方式是:
class Pizza(object):
def get_radius(self):
raise NotImplementedError
繼承自Pizza的類都必須要實現並重寫get_redius,否則就會報錯
這種方式的抽象方法有一個問題,如果你忘記實現了get_radius,只有在你呼叫這個方法的時候才會報錯
>>> 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
使用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
備註:我使用Python3.6實現的程式碼
In [8]: import abc
...:
...: class BasePizza(abc.ABC):
...:
...: @abc.abstractmethod
...: def get_radius(self):
...: """:return"""
...:
In [9]: BasePizza()
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-9-70b53ea21e68> in <module>()
----> 1 BasePizza()
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_ingredioents方法不需要知道物件返回的結果,
因此,你不需要強制抽象方法實現成為常規方法、類或者靜態方法。在python3中,可以將@staticmethod和@classmethod裝飾器放在@abstractmethod上面
import abc
class BasePizza(object):
__metaclass__ = abc.ABCMeta
ingredient = [`cheese`]
@classmethod
@abc.abstractmethod
def get_ingredients(cls):
"""Returns the ingredient list."""
return cls.ingredients
和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()來獲取default_ingredients
本文翻譯自:https://julien.danjou.info/guide-python-static-class-abstract-methods/