在阅读一些开源Python库的源码时,经常会看到在某个类的成员函数前,有类似于@staticmethod或@classmethod或@property的语法糖。本质上,它们都是函数装饰器,只不过通常被用来修饰类成员函数而已。
本笔记旨在说明这些语法糖的用途,关于普通函数装饰器语法的解释,可以参考这篇笔记。
在解释这些装饰器函数前,先来分析下普通成员函数。1. 类的普通成员函数
对于Python的类,其普通类成员函数的第一个参数默认为当前的类实例,通常的定义形式示例:
class C: def __init__(self): ## NOTICE: 不传入参数时创建实例会报错;"self"只是约定俗成的参数名而已,非语法的硬性规定 pass我们可以通过print C.__init__查看函数__init__():
>>> class C(): ... def __init__(self): ... pass ... >>> print C.__init__ <unbound method C.__init__> ## __init__()是类C的unbound method,即它此时未bound到任何类实例上 >>> c = C() >>> print c.__init__ <bound method C.__init__ of <__main__.C instance at 0x7fb4346d38c0>> ## 当类实例创建后,__init__()变成实例的bound method >>> print C.__init__.__get__ <method-wrapper '__get__' of instancemethod object at 0x7fb434772c80> ## 类成员函数默认实现了__get__()方法总之,对于类的普通成员函数来说,在创建类的具体实例前,它们是unbound method,通过类名调用时(如 C.fn()),会报错;类实例创建 后,它们都是实例的bound method,只能通过实例来调用(inst = C(); inst.fn())。
>>> class C(): ... def fn_classmethod(x): ... print x ... fn = classmethod(fn_classmethod) ... >>> C.fn <bound method classobj.fn_classmethod of <class __main__.C at 0x7fb4346b6bb0>> >>> C.fn() __main__.C可见,当调用classmethod()将fn_classmethod转换为class method后,我们可以直接通过C.fn来调用它,当然,通过C().fn()调用也可以。
>>> class C(): ... @classmethod ## Python的decorator语法会保证classmethod(fn)的自动调用 ... def fn(x): ... print x ... >>> C.fn <bound method classobj.fn of <class __main__.C at 0x7fb4346b6808>> >>> C.fn() __main__.C
classmethod的典型使用场合:
1) 直接用类来调用函数,而不用借助类实例
2) 更优雅地实现某个类的实例的构造(类似于Factory Pattern)
通常情况下,类实例是解释器自动调用类的__init__()来构造的,但借助classmethod可以在解释器调用__init__前实现一些预处理逻辑,然后将预处理后的参数传入类的构造函数来创建类实例。如dict类型支持的fromkeys()方法就是用classmethod实现的,它用dict实例当前的keys构造出一个新的dict实例。在文档Descriptor HowTo Guide最后部分给出了它对应的Python pseudo-code,感兴趣的话可以去研究一下。
关于实现类实例构造的另一个典型case,可以参考StackOverflow上的这篇问答帖。帖中Best Answer作者给出了一个典型场景,这个场景用非classmethod的方法也可以实现类实例的构造,但借助classmethod语法,可以实现的更优雅(a. 与__init__构造实例相比,classmethod方法也保证了构造逻辑代码复用度而且实现的更精简,如解析date_as_string为(year, month, day)的代码也可以被复用;b. 它不用通过类的实例调用,直接用类来调用即可构造新的实例;c. 与定义实现相同功能的全局函数相比,更符合OOP思想;d. 基类被继承时,基类中定义的classmethod也会被继承到继承类中)。
class C(object): @staticmethod def f(arg1, arg2, ...): ...与classmethod的装饰器语法糖类似,@staticmethod会自动调用staticmethod(f)。
4. @property
根据Python文档的说明,property([fget[, fset[, fdel[, doc]]]])为new-style类创建并返回property对象,该对象是根据传入的参数(fget/fset/fdel)创建的,它可以决定外部调用者对new-style类的某些属性是否具有读/写/删除权限。以官网文档给出的demo为例:
class C(object): ## NOTICE: property只对new style classes有效 def __init__(self): self._x = None def getx(self): return self._x def setx(self, value): self._x = value def delx(self): del self._x x = property(getx, setx, delx, "I'm the 'x' property.")上述示例中,x是类C的property对象,由于创建时传入了3个函数对象,故通过访问该属性可以实现对self._x的读/写/删除操作。具体而言 ,调用C().x时,解释器最终会调用getx;调用C().x = value时,解释器后最终调用setx;调用del C().x时,解释器会最终调用delx。
>>> class C(object): ... def __init__(self): ... self._name = 'name' ... @property ... def get_name(self): ... return self._name ... ... >>> c = C() >>> c.get_name 'name' >>> c.get_name = 'new name' Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: can't set attribute
当然,这里所说的"read-only",只是指这个属性名不会被赋值操作重新绑定新对象而已。如果这个属性名初始绑定的是个可变对象(如list或dict),则即使通过@property装饰,其绑定的对象的内容也可以通过属性名来修改。
如果想通过类的实例对象来修改或删除类实例的属性,则需用下面的代码来实现:
>>> class C(object): ... def __init__(self): ... self._name = 'name' ... @property ... def name(self): ... return self._name ... @name.setter ... def name(self, value): ... self._name = value ... @name.deleter ... def name(self): ... del self._name ... >>> c = C() >>> c.name 'name' >>> c.__dict__ {'_name': 'name'} >>> c.name = 'new name' >>> c.__dict__ {'_name': 'new name'} >>> c.name 'new name' >>> del c.name >>> c.__dict__ {}上述代码中,@property、@name.setter及@name.deleter均是装饰器语法糖,其中:@name.setter中的name指代的是经@property装饰后的对 象(即property(name)返回的名为name但类型为property object的对象),setter是这个名为name的property对象的built-in函数,其目的 是通过name.setter(name)为这个property对象提供修改其所属类的属性的功能。@name.deleter同理。
【参考资料】
1. Python的几个高级语法概念浅析:lambda表达式 && 闭包 && 装饰器
2. Descriptor HowTo Guide
3. StackOverflow: Python @classmethod and @staticmethod for beginner?
4. Python Docs: classmethod
5. Python Docs: staticmethod
6. The definitive guide on how to use static, class or abstract methods in Python
7. Newfound love of @staticmethod in Python
8. Objects and classes in Python: Decorators
9. Python Docs: property()
10. Python Property