关于Python中如何使用静态、类、抽象方法的权威指南(译)
对于Python中静态、类、抽象方法的使用,我是一直很迷糊的。最近看到一篇技术文章对这方面解释的很好,在此翻译一下,加深印象,也为有需要的同学提供一个方便。
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属性的访问时没有绑定。这是什么意思呢?我们马上就会知道只要我们继续调用它一下:
>>> 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的实例。方法需要一个实例作为它的第一个参数(在Python 2中它必须是该类的一个实例,在Python 3中它可以是任何实例),让我们试一下:
>>> 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 0x00000000025B3E48>>
>>> 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 0x0000000002A95CF8>
>>>
>>> m == m.__self__.get_size
True
显然,我们依然有一个到对象的引用,如果有需要可以找回来。
在Python 3中,附加到类的方法不再视为绑定方法了,仅作为简单函数。如果有需要他们绑定到一个对象。原理依然保持不变,但是模型简化了。
>>> 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 0x0000000002907268>
静态方法:
静态方法是方法的一种特殊情况。有时候,你需要编写属于某个类的代码,但是从不使用对象本身。例如:
>>> class Pizza(object):
... @staticmethod
... def mix_ingredients(x,y):
... return x+y
... def cook(self):
... return self.mix_ingredient(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的类在没有重载cook本身的情况下,不能改变我们用于混合pizza的成分。
类方法:
说了这么多,那么什么是类方法?类方法是不绑定到对象但是绑定到类的方法。(注意我下面标红的部分,与原文有出入,我在Python 2.7.9和Python 3.4.3下运行得到的都是False)
>>> 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
False
>>> Pizza.get_radius()
42
不管你使用什么方式来访问这个方法,它总是绑定于它依附的类,而且它的第一个参数是类本身(记住类也是对象)。
那么,什么时候时候这种类型的方法呢?class方法常用于一下两种类型的方法中:
- 工厂方法,即用于创建一个类的实例用于某种预处理。如果我们使用@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中,这种方法被描述为接口。
在Python中最简单的写一个抽象方法的方式如下:
class Pizza(object):
def get_radius(self):
raise NotImplementedError
任何其他继承自Pizza的类应该实现并且重载get_radius方法。否则一个异常将会抛出。
这种特殊的实现抽闲方法的方式有一个缺点。如果你写一个继承自Pizza的类并且忘记实现get_radius了,错误仅在你打算试用这个方法的时候抛出。
>>> Pizza()
<__main__.Pizza object at 0x0000000002B9C208>
>>> Pizza().get_radius()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 3, in get_radius
NotImplementedError
有一种方法可以早点触发这种方式,当对象被实例化之后,使用Python提供的abc模块。
>>>
... class BasePizza(object):
... __metaclass__ = abc.ABCMeta
...
... @abc.abstractmethod
... def get_radius(self):
... """Method that should do something."""
...
利用abc和它特殊的类,只要你尝试实例化BasePizza或者任意继承自它的类,你都将得到一个类型错误。
>>> 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方法并不需要知道返回结果的对象其实是一个实现细节,不是一个让我们合约履行的标准。
因此,你不能强迫你的抽象方法的实现是一个普通的或者类或者静态方法。从Python 3(这在Python 2是行不通的,参照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实现为一个类的函数那就错了。这只是意味着你在BasePizza类中实现的get_ingredients是一个类方法。
在一个抽象方法中的实现?是的,在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()来获取成分列表。
原文地址:https://julien.danjou.info/blog/2013/guide-python-static-class-abstract-methods