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

时间: 2024-10-07 20:33:24

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

22.python中的面向对象和类的基本语法

当我发现要写python的面向对象的时候,我是踌躇满面,坐立不安呀.我一直在想:这个坑应该怎么爬?因为python中关于面向对象的内容很多,如果要讲透,最好是用面向对象的思想重新学一遍前面的内容.这个坑是如此之大,犹豫再三,还是只捡一下重要的内容来讲吧,不足的内容只能靠大家自己去补充了. 惯例声明一下,我使用的版本是 python2.7,版本之间可能存在差异. 好,在开讲之前,我们先思考一个问题,看代码: 为什么我只创建是为 a 赋值,就可以使用一些我没写过的方法? 可能会有小伙伴说:因为 a

Python 中的引用和类属性的初步理解

最近对Python 的对象引用机制稍微研究了一下,留下笔记,以供查阅. 首先有一点是明确的:「Python 中一切皆对象」. 那么,这到底意味着什么呢? 如下代码: #!/usr/bin/env python a = [0, 1, 2] # 来个简单的list # 最初,list 和其中各个元素的id 是这样的. print 'origin' print id(a),a for x in a: print id(x), x print '----------------------' # 我们把

如何在Python中实现这五类强大的概率分布

R编程语言已经成为统计分析中的事实标准.但在这篇文章中,我将告诉你在Python中实现统计学概念会是如此容易.我要使用Python实现一些离散和连续的概率分布.虽然我不会讨论这些分布的数学细节,但我会以链接的方式给你一些学习这些统计学概念的好资料.在讨论这些概率分布之前,我想简单说说什么是随机变量(random variable).随机变量是对一次试验结果的量化. 举个例子,一个表示抛硬币结果的随机变量可以表示成 Python 1 2 X = {1 如果正面朝上, 2 如果反面朝上} 随机变量是

Python中QuerySet和Objects类

一.1.queryset是查询集,就是传到服务器上的url里面的查询内容.Django会对查询返回的结果集QuerySet进行缓存,这是为了提高查询效率.也就是说,在你创建一个QuerySet对象的时候,Django并不会立即向数据库发出查询命令,只有在你需要用到这个QuerySet的时候才会这样做.2.Objects是django实现的mvc中的m,Django中的模型类都有一个objects对象,它是一个Django中定义的QuerySet类型的对象,它包含了模型对象的实例.3.不能,因为g

Python中的静态方法与类成员方法区分

学习python过程中对类声明中的"静态方法"与"类成员方法"有过那么一丝混淆,简略总结了一下两者声明方式和功能上的异同,如下: # -*- coding:utf-8 -*- class Test(object): value1 = 'value1' #定义类变量value1 def __init__(self): self.value2 = 'value2' #在构造函数中定义实例变量value2 @staticmethod def method1() print

关于python中的 object基类

参考:[1] stackoverflow: what is the difference between old style and new style classes in Python? [2] The Inside Story on New-style Classes

Python中怎样初始化一个类类class?

1 # 12-1 FP树的类定义 2 class treeNode: 3 def _init_(self,nameValue,numOccur,parentNode): 4 self.name=nameValue; # 节点的名字 5 self.count=numOccur; # 出现次数 6 self.nodeLink=None; # 链接相似的元素项 7 self.parent=parentNode; # 父节点 8 self.children={}; # 子节点 9 def inc(sel

Java中抽象类和接口中均不能定义静态的抽象方法

1.Java抽象类中不能有静态的抽象方法. 抽象类是不能实例化的,即不能被分配内存:而static修饰的方法在类实例化之前就已经别分配了内存,这样一来矛盾就出现了:抽象类不能被分配内存,而static方法必须被分配内存.所以抽象类中不能有静态的抽象方法. 定义抽象方法的目的是重写此方法,但如果定义成静态方法就不能被重写. 2.接口中不能有静态的抽象方法 接口中的方法也只能是 public abstract修饰的,不能加上static.接口是不能实例化的,即不能被分配内存,而static修饰的方法