python 装饰器语法糖(@classmethod @staticmethod @property @name.)原理剖析和运用场景

引用:http://blog.csdn.net/slvher/article/details/42497781

这篇文章系统的介绍这几者之间的关系和区别。有兴趣的朋友可以到上面的链接查看原文,这里我把原文拷贝如下(如有侵权,通知马上删除)

====================================================================

在阅读一些开源Python库的源码时,经常会看到在某个类的成员函数前,有类似于@staticmethod或@classmethod或@property的语法糖。本质上,它们都是函数装饰器,只不过通常被用来修饰类成员函数而已。

本笔记旨在说明这些语法糖的用途,关于普通函数装饰器语法的解释,可以参考这篇笔记

在解释这些装饰器函数前,先来分析下普通成员函数。

1. 类的普通成员函数
对于Python的类,其普通类成员函数的第一个参数默认为当前的类实例,通常的定义形式示例:

[python]

class C:
def __init__(self): ## NOTICE: 不传入参数时创建实例会报错;"self"只是约定俗成的参数名而已,非语法的硬性规定

pass

我们可以通过print C.__init__查看函数__init__():

[python]

>>> class C():
...     def __init__(self):
...         pass
...
>>> print C.__init__

总之,对于类的普通成员函数来说,在创建类的具体实例前,它们是unbound method,通过类名调用时(如 C.fn()),会报错;类实例创建后,它们都是实例的bound method,只能通过实例来调用(inst = C(); inst.fn())。
关于unbound method函数调用报错的原因,与Python底层实现时对函数名的查找规则有关。从上面示例代码可看到,__init__()函数默认实现了__get__()方法,而根据Python底层规则,当某个对象(Python中万物皆对象)实现了_get__()/__set__()/__delete__()这3个method中任何一个时,它就成了一个支持descriptor protocal的descriptor。当调用c.__init__时(当然,这个是Python解释器帮我们调用的,但这并不改变__init__实际上是个普通类函数的事实),根据descriptor invoking规则,其将被转化为type(c).__dict__[‘__init__‘].__get__(c, type(c))的形式,可见,实际上发生的调用类似于C.__init__(c),即类C的实例c会被当作第1个参数传给普通类成员函数。所以,在定义类普通成员函数时,至少需要有self参数,且类的普通成员函数必须通过类实例来调用,而不能通过类名直接调用。
关于上面提到的Descriptor Protocol及其对obj attribute查找规则的影响,强烈建议读懂这篇文档Descriptor HowTo Guide。不夸张的说,理解Descriptor对我们理解Python代码的底层行为有巨大帮助。

2. @classmethod
根据Python文档的说明,classmethod(fn)表明函数fn是类的函数而非类实例的函数,在语法上,它要求fn的函数签名至少要有1个参数,函数被调用时,解释器会将类作为第1个参数传给fn。示例如下:

[python]

>>> class C():
...     def fn_classmethod(x):
...         print x
...     fn = classmethod(fn_classmethod)
...
>>> C.fn

可见,当调用classmethod()将fn_classmethod转换为class method后,我们可以直接通过C.fn来调用它,当然,通过C().fn()调用也可以。
更需要注意的是,在调用时C.fn()时,fn_classmethod()唯一参数x的实参确实是类C(即示例中print出来的__main__.C),该参数是解释器自动传入的。
在Python语法中,@classmethod是一种实现自动调用classsmethod(fn_classmethod)的语法糖,它实现的功能与上述示例代码一致,只是看起来更精简且更pythonic而已:

[python]

>>> class C():

...    @classmethod ## Python的decorator语法会保证classmethod(fn)的自动调用

...     def fn(x):

...         print x

...

>>> C.fn

>>> 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也会被继承到继承类中)。

3. @staticmethod
根据Python文档的说明,staticmethod(fn)表明函数fn是类的静态方法。具体到类定义体内某个函数的定义上,如果该函数想声明称静态成员,则只需在其定义体前加上"@staticmethod"这行,利用装饰器语法糖来实现staticmethod(cls.fun)的目的。示例如下:

[python]

class C(object):

@staticmethod

def f(arg1, arg2, ...):

...

与classmethod的装饰器语法糖类似,@staticmethod会自动调用staticmethod(f)。
Python中类静态方法的语义跟C++/Java类似,即类的静态成员属于类本身,不属于类的实例,它无法访问实例的属性(数据成员或成员函数)。定义为staticmethod的函数被调用时,解释器不会自动为其隐式传入类或类实例的参数,它的实际参数列表与调用时显式传入的参数列表保持一致。
staticmethod的典型应用场景:
若类的某个函数确认不会涉及到与类实例有关的操作时,可以考虑将该函数定义为类的staticmethod。比如,根据业务逻辑,可将全局函数封装到一个类中并声明为staticmethod,这样看起来更符合OOP思想,具体的例子可以参考这篇Blog。当然,这只是一种符合OOP的封装思路,并非意味着碰到全局函数就一定要这样做,需要看个人习惯或业务需求。
再次强调:定义为staticmethod类型的函数,其函数体中最好不要涉及与类实例有关的操作(包括创建类实例或访问实例的属性),因为一旦涉及到类实例就意味着这些实例名是硬编码的,在类被继承的场景下,调用这些staticmethod类型的函数会创建基类或访问基类属性,而这通常不是业务预期的行为。具体的case可以参考StackOverflow这篇问答帖的第2个高票答案。

4. @property
根据Python文档的说明,property([fget[, fset[, fdel[, doc]]]])为new-style类创建并返回property对象,该对象是根据传入的参数(fget/fset/fdel)创建的,它可以决定外部调用者对new-style类的某些属性是否具有读/写/删除权限。以官网文档给出的demo为例:

[python] view plain copy print?

  1. class C(object): ## NOTICE: property只对new style classes有效
  2. def __init__(self):
  3. self._x = None
  4. def getx(self):
  5. return self._x
  6. def setx(self, value):
  7. self._x = value
  8. def delx(self):
  9. del self._x
  10. 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。
由于property()的第1个参数是fget,利用这一点,可以很容易实现一个只有read-only权限的类属性:

[python] view plain copy print?

  1. >>> class C(object):
  2. ...     def __init__(self):
  3. ...         self._name = ‘name‘
  4. ...     @property
  5. ...     def get_name(self):
  6. ...         return self._name
  7. ...
  8. ...
  9. >>> c = C()
  10. >>> c.get_name
  11. ‘name‘
  12. >>> c.get_name = ‘new name‘
  13. Traceback (most recent call last):
  14. File "
  15. AttributeError: can‘t set attribute

当然,这里所说的"read-only",只是指这个属性名不会被赋值操作重新绑定新对象而已。如果这个属性名初始绑定的是个可变对象(如list或dict),则即使通过@property装饰,其绑定的对象的内容也可以通过属性名来修改。
如果想通过类的实例对象来修改或删除类实例的属性,则需用下面的代码来实现:

[python] view plain copy print?

  1. >>> class C(object):
  2. ...     def __init__(self):
  3. ...         self._name = ‘name‘
  4. ...     @property
  5. ...     def name(self):
  6. ...         return self._name
  7. ...     @name.setter
  8. ...     def name(self, value):
  9. ...         self._name = value
  10. ...     @name.deleter
  11. ...     def name(self):
  12. ...         del self._name
  13. ...
  14. >>> c = C()
  15. >>> c.name
  16. ‘name‘
  17. >>> c.__dict__
  18. {‘_name‘: ‘name‘}
  19. >>> c.name = ‘new name‘
  20. >>> c.__dict__
  21. {‘_name‘: ‘new name‘}
  22. >>> c.name
  23. ‘new name‘
  24. >>> del c.name
  25. >>> c.__dict__
  26. {}

上述代码中,@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同理。
至于property对象支持的函数setter()和deleter()的来历,文档Descriptor HowTo Guide在介绍property原理时给出了property类底层实现的Python伪码,值得精读。
从伪码还可以看到,property类实现了__get__()、__set__()和__delete__()方法,这意味着property类是个遵循descriptor protocol的data descriptor,根据文档Descriptor HowTo Guide关于Invoking Descriptors的说明,data descriptor会影响解释器对属性名的查找链,具体而言,当上面的代码中调用c.name时,解释器会将其转化成type(c).__dict__[‘name‘].__get__(c, type(c))(备注:通过print type(c).__dict__可以验证,name确实存在于dict中),故这个转化后的调用链会调用到property对象的__get__()方法,而根据property的实现伪码,在__get__()中最终会调用到类C对应的name()函数。
上面这段话所描述的流程正是property魔法背后的原理。
当然,要想真正理解还需要仔细研究property的python伪码逻辑。

@property除可以实现属性的只读权限功能外,还可以用在这种场景下:
类属性已经暴露给外部调用者,但由于业务需求,需要针对这个属性进行业务逻辑的修改(如增加边界判定或修改属性计算方法,等等),则此时引入property()或@property语法糖可以在修改逻辑的同时保证代码的后向兼容,外部调用者无需修改调用方式。具体的case可以参考这篇Blog中提到的场景。

【参考资料】
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

====================== EOF =====================

时间: 2024-10-24 13:29:22

python 装饰器语法糖(@classmethod @staticmethod @property @name.)原理剖析和运用场景的相关文章

python装饰器&语法糖

装饰器: 1 >>> def a(func): 2 ... def b(*argv): 3 ... print("in b") 4 ... return func(*argv) 5 ... return b 6 ... 7 >>> def c(a,b): 8 ... print(a**2,b**2) 9 ... 10 >>> c = a(c) 11 >>> c(2,3) 12 in b 13 4 9 14 >

python装饰器 语法糖

简介: 装饰器(Decorators)是 Python 的一个重要部分.简单地说:他们是修改其他函数的功能的函数. 比如说我们写flask,路由就是用装饰器定义的.如果写权限控制,那么权限控制一般也是由装饰器来实现的.日志记录,一般也可以通过装饰器来实现. 简单说,就是为了给某些函数增加一种或几种功能的做法. 下面举例实现. 一:基本函数 1.源码 from time import sleep def watch_movie(): print('看电影') sleep(3) print('The

python 装饰器(语法糖)

def  login(func):    def testlogin():        for  i in range(3):            _username="abc"            _passwrod="123456"            user_status = False            iuput = input("请输入账号:")            input1 = input("请输入密码

python学习day07 高阶函数 装饰器 语法糖

语法糖对于计算机的运行并没有任何的好处,但是对于程序员的好处是很大的,方便我们写代码,所以称为糖 #******************************装饰器************************* # 装饰器本质上就是一个python函数,他可以让其他函数在不需要做任何代码变动的前提下,增加额外的功能,装饰器的返回值也是一个函数对象. # 装饰器的应用场景:比如插入日志,性能测试,事务处理,缓存等等场景 import time def func1(): print('in f

装饰器语法糖运用

装饰器语法糖运用 前言:函数名是一个特性的变量,可以作为容器的元素,也可以作为函数的参数,也可以当做返回值. 闭包定义: 内层函数对外层函数(非全局)变量的引用,这个内层函数就可以成为闭包 在Python中我们用__closure__来检查函数是否是闭包 def func1(): name = '张三' def func2(): print(name) # 能够访问到外层作用域的变量 func2() print(func2.__closure__) # (<cell at 0x1036c7438

python装饰器 语法与解读

import time #加载time模块 from functools import wraps #加载functools模块中的wraps函数 def cost_time(old_fn): # 注:cost_time是装饰器名 ,形参old_fn是要装饰的函数 装饰器最外层没有return,也就是不用返回的 @wraps(old_fn) #它主要作用就是接收到原函数中的注析,所以括号内也是上面形参一样的 def inner(*args, **kwargs): # inner它是cost_ti

Python装饰器完全解读

1 引言 装饰器(Decorators)可能是Python中最难掌握的概念之一了,也是最具Pythonic特色的技巧,深入理解并应用装饰器,你会更加感慨——人生苦短,我用Python. 2 初步理解装饰器 2.1 什么是装饰器 在解释什么是装饰器之前,我们有必要回顾一下Python中的一些思想和概念.我们都知道,Python是一门面向对象的语言,Python基本思想就是一些皆对象,数据类型是对象.类是对象.类实例也是对象……对于接下来我们要说的装饰器而言,最重要的是,函数也是对象! 你没看错,函

理解Python装饰器

装饰器本质上是一个Python函数,它可以让其他函数在不需要做任何代码变动的前提下增加额外功能,装饰器的返回值也是一个函数对象.它经常用于有切面需求的场景,比如:插入日志.性能测试.事务处理.缓存.权限校验等场景.装饰器是解决这类问题的绝佳设计,有了装饰器,我们就可以抽离出大量与函数功能本身无关的雷同代码并继续重用.概括的讲,装饰器的作用就是为已经存在的对象添加额外的功能. 先来看一个简单例子: def foo(): print('i am foo') 现在有一个新的需求,希望可以记录下函数的执

Python装饰器AOP 不定长参数 鸭子类型 重载(三)

1 可变长参数与关键字参数 *args代表任意长度可变参数 **kwargs代表关键字参数 用*args和**kwargs只是为了方便并没有强制使用它们. 缺省参数即是调用该函数时,缺省参数的值若未被传入,则传入默认预设的值. 注意 : 须将所有带有默认值的参数置于参数列表的末尾. def print_info(name, age = 18,gender = True ) print_info("zhan", gender = False ) def demo(num, *nums ,