Python權威指南之如何使用靜態類或抽象函數

轉自  http://www.xdarui.com/archives/261.html

發表時間:2013-09-04 22:24:30  閱讀:1670  標籤:Python  

原文http://julien.danjou.info/blog/2013/guide-python-static-class-abstract-methods

代碼審查對發現人們很難理解的問題來說絕對是一種很讚的方式。我(Julien Danjou)最近在做OpenStack patches校對的時候發現人們不能夠正確的理解與使用Python提供的不同的函數修飾。所以我這裏提供一個鏈接以便下次review的時候帖出。

Python中方法是怎麼工作的

一個方法就是一個函數,它依付於一個類之上。你可以像這樣去定義和使用:

  1. >>> class Pizza(object):
  2. ... def __init__(self, size):
  3. ... self.size = size
  4. ... def get_size(self):
  5. ... return self.size
  6. ...
  7. >>> Pizza.get_size
  8. <unbound method Pizza.get_size>
這裏Python告訴了我們Pizza這個類中的get_size方法沒綁定,這是什麼意思?我們嘗試去調用下就知道了:
  1. >>> Pizza.get_size()
  2. Traceback (most recent call last):
  3. File "<stdin>", line 1, in <module>
  4. TypeError: unbound method get_size() must be called with Pizza instance as first argument (got nothing instead)
因爲它並未綁定到任何一個Pizza的實例中去,而方法的第一個參數希望接收一個實例。(Python2中實例必需是對應的那個類,而Python3中可以是任何東西)讓我這樣試試:
  1. >>> Pizza.get_size(Pizza(42))
  2. 42
可以運行了!我們在調用的時候給第一個參數傳入了它的實例,所以一切都正常了。但是你不得不誠認,這樣調用不是很方便。每次調用這個方法的時候我們都得引用一個類,而且我們也不知 道這個對象是屬於哪個類,這樣肯定是不是長久之計。

所以Python幫我們做了這些事情,給所有Pizza的實例綁定所有它的方法。這意味着get_sizePizza實例的已綁定方法:方法的第一個參數是實 例它本身。

  1. >>> Pizza(42).get_size
  2. <bound method Pizza.get_size of <__main__.Pizza object at 0x7f3138827910>>
  3. >>> Pizza(42).get_size()
  4. 42
正如我們所期望,我們必沒有給get_size()方法傳入任何參數,因爲它已經綁定了,Pizza實例自身會自動設置成方法的第一個參數。這可以證明:
  1. >>> m = Pizza(42).get_size
  2. >>> m()
  3. 42
事實上,你甚至都不需要爲你的Pizza對象保存引用,因其方法與對象綁定了,所以其方法本身就夠了。但是如果我們想知道這個方法倒底綁定到了哪個對象上該怎麼做?這有一 個小技巧:
  1. >>> m = Pizza(42).get_size
  2. >>> m.__self__
  3. <__main__.Pizza object at 0x7f3138827910>
  4. >>> # You could guess, look at this:
  5. ...
  6. >>> m == m.__self__.get_size
  7. True
顯然,我仍有一個我們這個對象的引用,我們隨時可以通過它找回。

在Python3中,直接使用類中的方法不再視爲未綁定方法,而是一個簡單的函數,而只是在需要的時候才綁定到對象上。所以基本原則沒變只是簡化了模型。

  1. >>> class Pizza(object):
  2. ... def __init__(self, size):
  3. ... self.size = size
  4. ... def get_size(self):
  5. ... return self.size
  6. ...
  7. >>> Pizza.get_size
  8. <function Pizza.get_size at 0x7f307f984dd0>

靜態方法

靜態方法是方法中的一個特例。有時,你寫了一段屬於某個類的代碼,但是它並未使用這個類。比如:

  1. class Pizza(object):
  2. @staticmethod
  3. def mix_ingredients(x, y):
  4. return x + y
  5.  
  6. def cook(self):
  7. return self.mix_ingredients(self.cheese, self.vegetables)
這種情景下,寫了這樣的非靜態方法mix_ingredients也能工作,但會傳遞一個並不會使用的參數self。這裏個註解@staticmethod可以幫我們做這些情況:
  • Python不會給每一個Pizza實例都增加一個綁定的方法。綁定方法也是一個函數,所以創建它們也有額外的開銷。通過靜態方法可以避免這些開銷:
    1. >>> Pizza().cook is Pizza().cook
    2. False
    3. >>> Pizza().mix_ingredients is Pizza.mix_ingredients
    4. True
    5. >>> Pizza().mix_ingredients is Pizza().mix_ingredients
    6. True

類方法

說到這,倒底什麼是類方法?類方法是指不是綁定在對象上而是類上的方法!。

  1. >>> class Pizza(object):
  2. ... radius = 42
  3. ... @classmethod
  4. ... def get_radius(cls):
  5. ... return cls.radius
  6. ...
  7. >>>
  8. >>> Pizza.get_radius
  9. <bound method type.get_radius of <class '__main__.Pizza'>>
  10. >>> Pizza().get_radius
  11. <bound method type.get_radius of <class '__main__.Pizza'>>
  12. >>> Pizza.get_radius is Pizza().get_radius
  13. True
  14. >>> Pizza.get_radius()
  15. 42
無論你是如何使用類方法,它始終都是連接到類,而且第一個參數是類本身(記住類也是對象)。我們何時使用這種方法?類方法大多有這兩種用法:
  • 工廠方法,用於創建一個類的實例比如一些預處理。如果我們用@staticmethod,我們就得將類名Pizza硬編碼進函數,任何繼承自Pizza的類 都無法使用工廠方法的自身用處。
    1. class Pizza(object):
    2. def __init__(self, ingredients):
    3. self.ingredients = ingredients
    4.  
    5. @classmethod
    6. def from_fridge(cls, fridge):
    7. return cls(fridge.get_cheese() + fridge.get_vegetables())
  • 靜態方法之前的調用:如果你把靜態方法分散成很多靜態方法,你不能直接硬編碼類名而應該用類方法。使用這種方式申明一個類方法,Pizza類名不會被直接使用,而且 繼承的子類和重載父類都會很好的工作。
    1. class Pizza(object):
    2. def __init__(self, radius, height):
    3. self.radius = radius
    4. self.height = height
    5.  
    6. @staticmethod
    7. def compute_circumference(radius):
    8. return math.pi * (radius ** 2)
    9.  
    10. @classmethod
    11. def compute_volume(cls, height, radius):
    12. return height * cls.compute_circumference(radius)
    13.  
    14. def get_volume(self):
    15. return self.compute_volume(self.height, self.radius)

      抽象方法

      抽象方法是基類中定義的方法,但卻沒有任何實現。在Java中,可以把方法申明成一個接口。而在Python中實現一個抽象方法的最簡便方法是:

      1. class Pizza(object):
      2. def get_radius(self):
      3. raise NotImplementedError
      任何從Pizza繼承下來的子類都必需實現get_radius方法,否則就會產生一個錯誤。這種方式實現在抽象方法也有缺點,如果你寫了一個類繼承自Pizza但卻忘了實現get_radius時,只有當你用到了那個方法時纔會拋錯。
      1. >>> Pizza()
      2. <__main__.Pizza object at 0x7fb747353d90>
      3. >>> Pizza().get_radius()
      4. Traceback (most recent call last):
      5. File "<stdin>", line 1, in <module>
      6. File "<stdin>", line 3, in get_radius
      7. NotImplementedError
      這裏有一個簡單的辦法可以在類被實例化後觸發它,使用Python提供的abc模塊。
      1. import abc
      2.  
      3. class BasePizza(object):
      4. __metaclass__ = abc.ABCMeta
      5.  
      6. @abc.abstractmethod
      7. def get_radius(self):
      8. """Method that should do something."""
      使用abc以及其特定的方法,一旦你嘗試去實例化BasePizza類或任意從其繼承下來的類的時候都會得到一個錯誤。
      1. >>> BasePizza()
      2. Traceback (most recent call last):
      3. File "<stdin>", line 1, in <module>
      4. TypeError: Can't instantiate abstract class BasePizza with abstract methods get_radius

      混搭使用靜態、類以及抽象方法

      當創建類及繼承時,你就得使用混搭這些方式的方法了。這裏一些提示。

      請記住,申明類時應先申明成抽象類,並且不要把類的原型寫死。這意味着,抽象方法必需得實現,但我可以在實現抽象方法時可以任意指定參數列表。

      1. import abc
      2.  
      3. class BasePizza(object):
      4. __metaclass__ = abc.ABCMeta
      5.  
      6. @abc.abstractmethod
      7. def get_ingredients(self):
      8. """Returns the ingredient list."""
      9.  
      10. class Calzone(BasePizza):
      11. def get_ingredients(self, with_egg=False):
      12. egg = Egg() if with_egg else None
      13. return self.ingredients + egg
      Calzone繼承BasePizza得按要求實現對應的接口,這非常有用。這意味着,我們可以實現成一個類或一個靜態方法,比如:
      1. import abc
      2.  
      3. class BasePizza(object):
      4. __metaclass__ = abc.ABCMeta
      5.  
      6. @abc.abstractmethod
      7. def get_ingredients(self):
      8. """Returns the ingredient list."""
      9.  
      10. class DietPizza(BasePizza):
      11. @staticmethod
      12. def get_ingredients():
      13. return None
      這是正確的而且也實現了抽象類BasePizza所有要求。事實上一個實現細節是get_ingredients方法返回結果時無需關心對象,不用關心有何約束。因此,你不能強 制要求抽象方法或類的實現是有規律的,或者說不應該這樣做。從Python3開始(Python2不會生效,見issue5867),可以在@abstractmethod之上使用@staticmethod@classmethod
      1. import abc
      2.  
      3. class BasePizza(object):
      4. __metaclass__ = abc.ABCMeta
      5.  
      6. ingredient = ['cheese']
      7.  
      8. @classmethod
      9. @abc.abstractmethod
      10. def get_ingredients(cls):
      11. """Returns the ingredient list."""
      12. return cls.ingredients
      不要誤讀成這個:如果你嘗試去強迫子類實現get_ingredients作爲類的方法,你就錯了。這只是意味着你在BasePizza類中,實現get_ingredients只是一個類方法。

      抽象類中實現抽象方法?是的!Python中抽象方法與Java相反。你可以在抽象方法中加入代碼並通過super()調用:

      1. import abc
      2.  
      3. class BasePizza(object):
      4. __metaclass__ = abc.ABCMeta
      5.  
      6. default_ingredients = ['cheese']
      7.  
      8. @classmethod
      9. @abc.abstractmethod
      10. def get_ingredients(cls):
      11. """Returns the ingredient list."""
      12. return cls.default_ingredients
      13.  
      14. class DietPizza(BasePizza):
      15. def get_ingredients(self):
      16. return ['egg'] + super(DietPizza, self).get_ingredients()
      這種情況下,所有從BasePizza類中繼承下來的pizza都得實現get_ingredients方法,但是我們可以使用super()通過默認機制獲取配料表。
發佈了15 篇原創文章 · 獲贊 9 · 訪問量 35萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章