The definitive guide on how to use static, class or abstract methods in Python
Doing code reviews is a great way to discover things that people might struggle to comprehend. While proof-reading OpenStack patches recently, I spotted that people were not using correctly the various decorators Python provides for methods. So here’s my attempt at providing me a link to send them to in my next code reviews.
1 How methods work in Python
方法是作为类的属性(attribute)存储的函数。你可以以下面的方式声明和获取函数:
In [1]: class Pizza(object):
...: def __init__(self, size):
...: self.size = size
...: def get_size(self):
...: return self.size
...:
In [2]: Pizza.get_size
Out[2]: <function __main__.Pizza.get_size(self)>
原参考资料中运行后提示: <unbound method Pizza.get_size>
也就是告诉我们类Pizza
的属性get_size
是一个非绑定的方法。这里我在Ipython和IDLE中实测结果:
<function __main__.Pizza.get_size(self)>
,
<function Pizza.get_size at 0x00000161A24E2EA0>
,
提示我Pizza.get_size
是一个函数,尝试调用一下方法get_size()
。
In [3]: Pizza.get_size()
Traceback (most recent call last):
File "<ipython-input-3-8f6eefb4f713>", line 1, in <module>
Pizza.get_size()
TypeError: get_size() missing 1 required positional argument: 'self'
不能调用,提示缺失一个位置参数’self’。其实质是因为它没有被绑定到任一Pizza
的实例上。一个方法需要一个实例作为它第一个参数(在Python 2中它必须是对应类的实例;在Python 3中可以是任何东西)。我们现在试试:
In [4]: Pizza.get_size(Pizza(42))
Out[4]: 42
现在可以了!我们试用一个实例作为get_size
方法的第一个参数调用了它,所以一切变得很美好。但是你很快会同意,这并不是一个很漂亮的调用方法的方式;因为每次我们想调用这个方法都必须使用到类。并且,如果我们不知道对象是哪个类的实例,这种方式就不方便了。
所以,Python为我们准备的是,它将类Pizza
的所有的方法绑定到此类的任何实例上。这意味着类Pizza
的任意实例的属性get_size
是一个已绑定的方法:第一个参数是实例本身的方法。
In [10]: Pizza(42).get_size
Out[10]: <bound method Pizza.get_size of <__main__.Pizza object at 0x0000015BD9C34EB8>>
In [11]: Pizza(42).get_size()
Out[11]: 42
如我们预期,现在不需要提供任何参数给get_size
,因为它已经被绑定(bound),它的self
参数是自动地设为Pizza
类的实例。下面是一个更好的证明:
In [5]: m = Pizza(42)
In [6]: m
Out[6]: <__main__.Pizza at 0x15bd9c34438>
In [7]: m = Pizza(42).get_size
In [8]: m
Out[8]: <bound method Pizza.get_size of <__main__.Pizza object at 0x0000015BD9C34E10>>
In [9]: m()
Out[9]: 42
因此,你甚至不要保存一个对Pizza对象的饮用。它的方法已经被绑定在对象上,所以这个方法已经足够。 但是如何知道已绑定的方法被绑定在哪个对象上?技巧如下:
In [12]: m = Pizza(42).get_size
In [13]: m.__self__
Out[13]: <__main__.Pizza at 0x15bd9c5e128>
In [14]: m == m.__self__.get_size
Out[14]: True
易见,我们仍然保存着一个对对象的引用,当需要知道时也可以找到。 在Python 3中,归属于一个类的函数不再被看成未绑定方法(unbound method),但是作为一个简单的函数,如果要求可以绑定在对象上。所以,在Python 3中原理是一样的,模型被简化了。
参考第一段代码
2 Static methods
静态方法是一类特殊的方法。有时,我们需要写属于一个类的方法,但是不需要用到对象本身。例如:
In [17]: 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
带来一些特别的东西:
In [18]: Pizza().cook is Pizza().cook
Out[18]: False
In [19]: Pizza().mix_ingredients is Pizza().mix_ingredients
Out[19]: True
In [20]: Pizza.mix_ingredients is Pizza.mix_ingredients
Out[20]: True
In [21]: Pizza()
Out[21]: <__main__.Pizza at 0x15bd9c5ea20>
- Python不需要对每个实例化的
Pizza
对象实例化一个绑定的方法。绑定的方法同样是对象,创建它们需要付出代价。这里的静态方法避免了这样的情况: - 降低了阅读代码的难度:看到
@staticmethod
便知道这个方法不依赖与对象本身的状态; - 允许我们在子类中重载
mix_ingredients
方法。如果我们使用在模块最顶层定义的函数mix_ingredients
,一个继承自Pizza
的类若不重载cook
,可能不可以改变混合成份(mix_ingredients)的方式。
3 Class methods
什么是类方法?类方法是绑定在类而非对象上的方法!
In [22]: class Pizza(object):
...: radius = 42
...: @classmethod
...: def get_radius(cls):
...: return cls.radius
...:
In [23]: Pizza.get_radius
Out[23]: <bound method Pizza.get_radius of <class '__main__.Pizza'>>
In [24]: Pizza.get_radius is Pizza().get_radius
Out[24]: False
In [25]: Pizza.get_radius()
Out[25]: 42
In [26]: Pizza().get_radius
Out[26]: <bound method Pizza.get_radius of <class '__main__.Pizza'>>
不管你如何使用这个方法,它总会被绑定在其归属的类上,同时它第一个参数是类本身(记住:类同样是对象) 何时使用这种方法?类方法一般用于下面两种:
- 工厂方法,被用来创建一个类的实例,完成一些预处理工作。如果我们使用一个
@staticmethod
静态方法,我们可能需要在函数中硬编码Pizza类的名称,使得任何继承自Pizza类的类不能使用我们的工厂用作自己的目的。
In [27]: 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
这个名字不需要直接被引用,并且继承和方法重载将会完美运作。In [28]: 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)
4 Abstract methods
抽象方法在一个基类中定义,但是可能不会有任何的实现。在Java中,这被描述为一个接口的方法。 所以Python中最简单的抽象方法是:
In [29]: class Pizza(object):
...: def get_radius(self):
...: raise NotImplementedError
任何继承自Pizza
的类将实现和重载get_radius
方法,否则会出现异常。这种独特的实现抽象方法的方式也有其缺点。如果你写一个继承自Pizza
的类,忘记实现get_radius
,错误将会在你使用这个方法的时候才会出现。
In [29]: class Pizza(object):
...: def get_radius(self):
...: raise NotImplementedError
...:
In [30]: Pizza()
Out[30]: <__main__.Pizza at 0x15bd9c72e80>
In [31]: Pizza().get_radius()
Traceback (most recent call last):
File "<ipython-input-31-00c16207f44e>", line 1, in <module>
Pizza().get_radius()
File "<ipython-input-29-eb9ab50a7580>", line 3, in get_radius
raise NotImplementedError
NotImplementedError
有种提前引起错误发生的方法,那就是当对象被实例化时,使用Python提供的abc
模块。
In [32]: import abc
...: class BasePizza(object):
...: __metaclass__ = abc.ABCMeta
...:
...: @abc.abstractmethod
...: def get_radius(self):
...: """Method that should do something."""
使用abc
和它的特类,一旦你试着实例化BasePizza
或者其他继承自它的类,就会得到TypeError
In [32]: import abc
...: class BasePizza(object):
...: __metaclass__ = abc.ABCMeta
...:
...: @abc.abstractmethod
...: def get_radius(self):
...: """Method that should do something."""
...:
In [33]: BasePizza()
Out[33]: <__main__.BasePizza at 0x15bd9c72fd0>
In [34]: BasePizza
Out[34]: __main__.BasePizza
In [35]: BasePizza().get_radius()
In [36]: BasePizza().get_radius
Out[36]: <bound method BasePizza.get_radius of <__main__.BasePizza object at 0x0000015BD9C727F0>>
5 Mixing static, class and abstract methods
当我们构建类和继承关系时,终将会碰到要混合这些方法decorator的情况。下面提几个tip。 记住声明一个类为抽象类时,不要冷冻那个方法的prototype。这是指这个方法必须被实现,不过是可以使用任何参数列表来实现。
In [37]: 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
类对象定义的接口需求。就是说,我们可以把它当作一个类方法或者静态方法来实现,例如:
In [38]: import abc
...: class BasePizza(object):
...: __metaclass__ = abc.ABCMeta
...: @abc.abstractmethod
...: def get_ingredients(self):
...: """Returns the ingredient list."""
...:
In [39]: class DietPizza(BasePizza):
...: @staticmethod
...: def get_ingredients():
...: return None
...:
这样做同样争取,并且完成了与BasePizza
抽象类达成一致的需求。get_ingredients
方法不需要知道对象,这是实现的细节,而非完成需求的评价指标。
因此,你不能强迫抽象方法的实现是正常的方法、类方法或者静态方法,并且可以这样说,你不能。从Python 3开始(这就不会像在Python 2中那样work了,见issue5867),现在可以在@abstractmethod
之上使用@staticmethod
和@classmethod
了。
In [40]: 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列表 。