如何使用静态方法、类方法或者抽象方法

Neil Zhu,简书ID Not_GOD,University AI 创始人 & Chief Scientist,致力于推进世界人工智能化进程。制定并实施 UAI 中长期增长战略和目标,带领团队快速成长为人工智能领域最专业的力量。
作为行业领导者,他和UAI一起在2014年创建了TASA(中国最早的人工智能社团), DL Center(深度学习知识中心全球价值网络),AI growth(行业智库培训)等,为中国的人工智能人才建设输送了大量的血液和养分。此外,他还参与或者举办过各类国际性的人工智能峰会和活动,产生了巨大的影响力,书写了60万字的人工智能精品技术内容,生产翻译了全球第一本深度学习入门书《神经网络与深度学习》,生产的内容被大量的专业垂直公众号和媒体转载与连载。曾经受邀为国内顶尖大学制定人工智能学习规划和教授人工智能前沿课程,均受学生和老师好评。

原文地址
代码检视是一种可以发现令程序员们头疼的事件的方法。近期检视OpenStack patches,我发现人们错误地试用了多个Python提供给方法的不同的声明(decorator)。所以,我尝试写一些可以在下次代码检视时可以寄给那些伙计的东西。
Python中方法的运作
=============
方法是作为类的属性(attribute)存储的函数。你可以以下面的方式声明和获取函数:

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

现在可以了!我们试用一个实例作为get_size方法的第一个参数调用了它,所以一切变得很美好。但是你很快会同意,这并不是一个很漂亮的调用方法的方式;因为每次我们想调用这个方法都必须使用到类。并且,如果我们不知道对象是哪个类的实例,这种方式就不方便了。
所以,Python为我们准备的是,它将类Pizza的所有的方法绑定到此类的任何实例上。这意味着类Pizza的任意实例的属性get_size是一个已绑定的方法:第一个参数是实例本身的方法

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

如我们预期,现在不需要提供任何参数给get_size,因为它已经被绑定(bound),它的self参数是自动地设为Pizza类的实例。下面是一个更好的证明:

>>> m = Pizza(42).get_size
>>> m
<bound method Pizza.get_size of <__main__.Pizza object at 0x10314b350>>
>>> m()
42

因此,你甚至不要保存一个对Pizza对象的饮用。它的方法已经被绑定在对象上,所以这个方法已经足够。
但是如何知道已绑定的方法被绑定在哪个对象上?技巧如下:

>>> m = Pizza(42).get_size
>>> m.__self__
<__main__.Pizza object at 0x10314b390>
>>> m == m.__self__.get_size
True

易见,我们仍然保存着一个对对象的引用,当需要知道时也可以找到。
在Python 3中,归属于一个类的函数不再被看成未绑定方法unbound method),但是作为一个简单的函数,如果要求可以绑定在对象上。所以,在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 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作为一个非静态的方法也可以work,但是给它一个self的参数将没有任何作用。这儿的decorator@staticmethod带来一些特别的东西:

>>> Pizza().cook is Pizza().cook
False
>>> Pizza().mix_ingredients is Pizza().mix_ingredients
True
>>> Pizza().mix_ingredients is Pizza.mix_ingredients
True
>>> Pizza()
<__main__.Pizza object at 0x10314b410>
>>> Pizza()
<__main__.Pizza object at 0x10314b510>
>>> 
  • Python不需要对每个实例化的Pizza对象实例化一个绑定的方法。绑定的方法同样是对象,创建它们需要付出代价。这里的静态方法避免了这样的情况:
  • 降低了阅读代码的难度:看到@staticmethod便知道这个方法不依赖与对象本身的状态;
  • 允许我们在子类中重载mix_ingredients方法。如果我们使用在模块最顶层定义的函数mix_ingredients,一个继承自Pizza的类若不重载cook,可能不可以改变混合成份(mix_ingredients)的方式。

类方法

什么是类方法?类方法是绑定在类而非对象上的方法!

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

此处有问题。原文中
>>> Pizza.get_radius is Pizza().get_radius True
还需要check一下。

不管你如何使用这个方法,它总会被绑定在其归属的类上,同时它第一个参数是类本身(记住:类同样是对象
何时使用这种方法?类方法一般用于下面两种:

  1. 工厂方法,被用来创建一个类的实例,完成一些预处理工作。如果我们使用一个@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())
  1. 静态方法调静态方法:如果你将一个静态方法分解为几个静态方法,你不需要硬编码类名但可以使用类方法。使用这种方式来声明我们的方法,Pizza这个名字不需要直接被引用,并且继承和方法重载将会完美运作。
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)

抽象方法

抽象方法在一个基类中定义,但是可能不会有任何的实现。在Java中,这被描述为一个接口的方法。
所以Python中最简单的抽象方法是:

class Pizza(object):
    def get_radius(self):
        raise NotImplementedError

任何继承自Pizza的类将实现和重载get_radius方法,否则会出现异常。这种独特的实现抽象方法的方式也有其缺点。如果你写一个继承自Pizza的类,忘记实现get_radius,错误将会在你使用这个方法的时候才会出现。

>>> Pizza()
<__main__.Pizza object at 0x106f381d0>
>>> 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

>>> BasePizza()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Can't instantiate abstract class BasePizza with abstract methods get_radius

混合静态方法、类方法和抽象方法

当我们构建类和继承关系时,终将会碰到要混合这些方法decorator的情况。下面提几个tip。
记住声明一个类为抽象类时,不要冷冻那个方法的prototype。这是指这个方法必须被实现,不过是可以使用任何参数列表来实现。

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中那样work了,见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()调用:

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,039评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,223评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,916评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,009评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,030评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,011评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,934评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,754评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,202评论 1 309
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,433评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,590评论 1 346
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,321评论 5 342
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,917评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,568评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,738评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,583评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,482评论 2 352

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,650评论 18 139
  • 静态方法 静态方法是一类特殊的方法。有时,需要写属于一个类的方法,但是不需要用到对象本身。例如: class Pi...
    chen_000阅读 260评论 0 0
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,621评论 18 399
  • 丙申九月,逢国庆,国人多出游,余恐长路漫堵,再三思忖,于3日,与三俩好友携家人东游至石岛一带。 石岛...
    陌岭先生阅读 603评论 0 0
  • 不奢于看你 怕目光无法转移 不想听见任何关于你 怕内心的波澜无法平静 不要想你 怕夜里的世界都是你 不要爱你了 可...
    钍素阅读 82评论 0 1