1. 程式人生 > >如何在Python中正確使用static、class、abstract方法

如何在Python中正確使用static、class、abstract方法

在Python面向物件程式設計中,類中的方法有多種形式:例項方法、靜態方法、類方法、抽象方法,本文來說說如何正確地使用這些方法。

例項方法

例項方法的定義只需要把第一個引數指定為 self,儘管這個名字可以是任意取的,但約定俗成為 self,該引數就是該類的一個例項物件,你可以用如下方式來宣告、訪問這個方法(不是呼叫):

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

屬性 get_size 是類 Pizza 的一個未繫結方法。這是什麼意思呢?很快我們就會有答案:

>>> 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類的任何例項物件上,它需要一個 Pizza 的例項物件作為引數傳遞進去(Python2必須是該類的例項,Python3中可以是任何東西),嘗試一下:

>>> Pizza.get_size(Pizza(42))
42

現在用一個例項物件(Pizza(42))作為 get_size 的第一個引數傳遞進去,程式正常執行了,如果我說這種呼叫方式還不是最方便的,你也會這麼認為的;沒錯,現在每次呼叫這個方法的時候我們都不得不引用這個類,看起來非常蹩腳,長期看來這種方式是行不通的。

get_sizePizza 的一個例項方法,呼叫的時候必須先例項化出一個物件出來,該例項方法的第一個引數就是該例項本身。

>>> Pizza(42).get_size
<bound method Pizza.get_size of <__main__.Pizza object at 0x7f3138827910>>
>>> Pizza(42).get_size()
42

和我們預期的一樣,現在不再需要提供任何引數給 get_size,因為它已經是繫結的,Python 會自動把例項物件傳遞給 self引數,下面程式碼是最好的證明:

>>> 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作為模組的頂層函式,那麼繼承自Pizza的子類就沒法改變pizza的mix_ingredients瞭如果不覆蓋cook的話。

類方法

什麼是類方法呢?類方法不是繫結到例項物件上,而是繫結在類物件上的方法。

>>> 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_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 "<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,而無需等到真正呼叫get_radius的時候才發現異常。

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

這樣是允許的,因為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_ingredientsBasePizza中是一個類方法。

可以在抽象方法中做程式碼的實現?沒錯,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列表。


關注公眾號「Python之禪」(id:vttalk)獲取最新文章 python之禪