win10+Python 3.6.3

Python 装饰器,不再是一道坎,但你得有耐心。

python3没有pickle python3没有method_decorator_decorator


Python三大神器(装饰器、迭代器、生成器)之一:decorator,是用于扩展(增加)原来函数功能的一种函数,它的特殊之处在于:其返回值也是一个函数。就是一个返回函数的高阶函数。

如果没有装饰器,若想给一个函数增加新功能,最直接的办法是 修改(篡改)原函数代码。很多情况下,不允许或不便修改原函数代码,装饰器

一、在深刻理解、熟练应用decorator之前,先理解几个概念:

1、function(函数)

是组织好的、可重复使用的,用于实现单一、或相关联功能的代码段。语法如下:

def 函数标识符名称([,参数]):#参数是可选的
	函数体(若有return,则将返回一个值给调用者;若没有,将返回None)

示例:

>>> def func():
...     return "Life is short,I use Python."
...
>>> func()
'Life is short,I use Python.'
>>> func # 函数也是一种特殊的变量
<function func at 0x000001FE97E42E18>

2、scope(作用域)

作用域 即一个变量的命名空间。代码中变量被赋值的位置,就将决定哪些范围的对象可以访问这个对象,这个访问就是命名空间。 在Python中,创建一个函数,它将拥有自己的作用域(命名空间)。即 在函数内部碰到一个variable(变量)时,函数会优先在自己的命名空间中查找。

>>> a_string = "This is a global variable."
>>> def func():#该函数的功能是:打印【局部作用域】(即函数拥有自己独立的命名空间)。虽然目前是空的。
...     print(locals())
...
>>> print(globals())#打印【全局作用域】。包括变量a_string。
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, 'a_string': 'This is a global variable.', 'func': <function func at 0x000002430BC02E18>}
>>> func()
{}

修改一下:比较一下异同。对于func()函数而言,在函数体之内是局部作用域。之外是全局作用域。

>>> a_string = "This is a global variable."
>>> def func():
...     b_string ="This a local variable."
...     print(locals())
...
>>> print(globals())
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, 'a_string': 'This is a global variable.', 'func': <function func at 0x0000025AFFEE2E18>}
>>> func()
{'b_string': 'This a local variable.'}

3、variable resolution rules(变量解析规则)

Python作用域规则,创建变量一定会在当前作用域中创建一个变量,但是访问、或修改变量时会先在当前作用域查找变量,若未找到匹配变量,则将依次向上在闭合的作用域里进行查找。所以,若修改func()的实现,让其打印全局作用域中的变量是可以的:

>>> a_string = "This is a global variable."
>>> def func():
...     print(a_string)#1
...
>>> func()
This is a global variable.

#1处,Python解释器会优先在函数的局部作用域中查找a_string变量;当查找不到时,将在它的上层作用域查找。

但是,可在函数内部给全局变量赋值(本质是 新建一个跟全局变量同名的局部变量):

>>> a_string = "This is a global variable."
>>> def func():
...     a_string = "test"#1
...     print(locals())
...
>>> func()
{'a_string': 'test'}
>>> a_string#2
'This is a global variable.'

上述代码中,全局变量能被访问到(若是可变数据类型(如list、dict),甚至可被更改),但不能赋值。在函数内部#1处,实则是新建了一个局部变量。可见在#2处打印出的a_string变量(全局变量)的值并未改变。

4、variable lifetime(变量生存周期)

值得注意的是,变量不仅生存在一个命名空间内,它们还有自己的 生存周期。

>>> def func():
...     x = 1
...
>>> func()
>>> print(x)#1
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'x' is not defined

#1处引发的Error 不仅因为作用域规则导致的(尽管这 是抛出了NameError的原因),还因为与Python以及其他编程语言中函数调用实现机制有关。在上述这个位置、这个执行时间点 并没有有效的语法能够获取到变量x的值,因为它压根不存在。即 函数func()的命名空间随着函数调用开始而开始、结束而销毁。

5、Function parameters(函数参数)

Python允许向函数传递parameterparameter会变成局部变量存在于函数内部。

>>> def func(x):
...     print(locals())
...
>>> func(1)
{'x': 1}

在Python中有很多方式来定义、传递参数。在此简要地说明:函数的参数可以是必须的位置参数 或 可选的命名,默认参数

>>> def func(x, y=0):#1
...     return x - y
...
>>> func(3, 1)#2
2
>>> func(3)#3
3
>>> func()#4
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: func() missing 1 required positional argument: 'x'
>>> func(y=1, x=3)#5
2

上述代码中,#1定义了函数func(),有一个位置参数x、一个命名参数y
#2通过常规方式调用函数,尽管已有一个命名参数,但参数仍可通过位置传递给函数。

在调用函数时,对于命名参数y,可完全不管它,例如#3。若命名参数未接收到任何值,Python会自动使用声明的默认值(即 0)。

不过,不能忽略位置参数x,否则将引发像#4所示Error

Python支持函数调用时的命名参数(命名实参)。#5函数调用,传递的是两个命名实参,此时因为有名称标识,参数传递顺序可不必在意。

当然,#2函数第二个形参y,是通过位置方式传递值的。尽管它是一个命名参数。

函数的参数 可以有名称位置

6、Nested funcitons(嵌套函数/内嵌函数)

Python允许创建嵌套函数。即可在函数中定义函数、且现有的作用域和变量生存周期依旧适用。

>>> def outer():
...     x = 1
...     def inner():
...             print(x)#1
...     inner()#2
...
>>> outer()
1

#1 Python解释器需找一个名为x的局部变量,若查找失败则会继续在上层作用域查找,在此的上层作用域是定义在外头一个函数outer()里。
outer()函数来说,变量x是一个局部变量,inner()函数可访问封闭的作用域(读、修改)。

#2 调用inner()函数,inner也是一个遵循Python变量解析规则的变量名,Python解析器会优先在outer作用域中对变量名inner查找匹配的变量。如下可说明 innerouter()作用域中的一个变量:

>>> def outer():
...     x = 1
...     def inner():
...             print(x)
...     inner()#2
...     print(locals())
...
>>> outer()
1
{'inner': <function outer.<locals>.inner at 0x000001EC707B67B8>, 'x': 1}

7、Functions are first class objects in Python(Python世界中 函数是第一类对象)

在Python中,函数 与其他东西一样 都是对象、一切皆对象。解说:

>>> issubclass(int, object)
True
>>> def func():
...     pass
...
>>> func.__class__
<class 'function'>
>>> issubclass(func.__class__, object)
True

函数只是一些普通的值而已,跟其他值一样。即可以将函数像参数一样传递给其他函数 或从函数里返回函数。换句话说:在Python中,函数可看作是一个特殊变量。

>>> def add(x, y):
...     return x + y
...
>>> def sub(x, y):
...     return x - y
...
>>> def apply(func, x, y):#1
...     return func(x, y)#2
...
>>> apply(add, 2, 1)#3
3
>>> apply(sub, 2, 1)
1

上述代码中,addsub是两个普通的Python函数,用于实现简单的加减法功能,接收两个值,返回一个计算后的结果值。
#1接收的是一个函数的变量 只是一个普通的变量而已,和其他变量一样;
#2调用传递进来的函数func(),并调用变量包含的值;
#3可发现传递函数并没有特殊语法。函数名称只是像其他变量一样的标识符而已。

>>> def outer():
...     def inner():
...             print("Inside inner.")
...     return inner#1
...
>>> func = outer()#2
>>> func
<function outer.<locals>.inner at 0x0000024F27976950>
>>> func()
Inside inner.

在上述代码中:
#1正是将函数标识符的变量(名)inner作为返回值返回;如若不将函数inner return,它将根本不被调用到。每次outer()函数被调用时,innner()函数都将被重新定义。如果它(inner)不被当作变量返回,在每次执行之后,它将不复存在(生命周期)。
#2捕获返回值(即 函数inner),将它存放在一个新的变量func里。当对变量func进行求值时,它确实包含inner。还可对其进行调用。

8、Closures(闭包)

>>> def outer(x):
...     def inner():
...             print(x)#1
...     return inner
...
>>> print1 = outer(1)
>>> print2 = outer(2)
>>> print1()
1
>>> print2()
2

如果在一个内部函数(如 inner())里,对外部作用域(但不是在全局作用域)的变量(x)进行引用,那么内部函数(inner())就被认为是闭包(closure)。

9、Syntactic sugar(语法糖)

[语法]糖,一个术语,它意指那些没有给计算机语言添加新功能,而只是对人类来说更“甜蜜”的语法。

是一种便捷的写法,编译器会进行转换;可提高开发编码的效率,在性能上也不会带来损失。

加糖后的代码功能 与加糖前保持一致,且糖在不改变其所在位置的语法结构前提下,实现了运行时等价,即加糖后的代码编译后 跟加糖前一样。

语法糖,是为了避免编程者出现错误并提供效率的语法层面的一种优雅的解决方案。

实例是:Python中的 @装饰器符号、*args**kwargssuper()等。

PS:有个名词叫“语法盐”,与语法糖相反,是让编程者忧伤的语法!!

二、装饰器

真正的主角来了!前面的讲述都是为了更好地深刻理解装饰器。


1、装饰器,本质是一个Python函数,只不过它将一个函数当作参数,然后返回一个替代版函数。

它可让其他函数在不需要做任何代码变动的前提下增加额外功能,装饰器的返回值也是一个函数对象。

>>> def outer(some_func):
...     def inner():
...             print("before some_func")
...             ret = some_func()#1
...             return ret +1
...     return inner
...
>>> def func():
...     return 1
...
>>> decorated = outer(func)#2
>>> decorated()
before some_func
2

上述代码中,定义函数outer(),它只有一个参数some_func;接着定义了一个内嵌函数inner,它会打印一行字符串;然后调用some_func,在#1得到它(some_func())的返回值。在outer()每次调用时,some_func的值可能会不一样,但不管some_func的值如何,都会调用它。最后,inner返回some_func()+1的值。

调用在#2存储在变量decorated里的函数将打印那行字符串、返回值2,而不是期望中调用函数func得到的返回值1

python3没有pickle python3没有method_decorator_装饰器_02

在此,变量decorated是函数func的一个装饰版本、加强版本。如果打算编写一个有用的装饰器,可用装饰版本完全替代原先的函数func,如此将得到加强版func。想达到这个效果,完全不需要新的语法,简单赋值给变量func就行了:

>>> func = outer(func)
>>> func
<function outer.<locals>.inner at 0x000002BF19946730>

现在,任何调用都不会牵扯到原先的函数func,都会得到新的装饰版本func

综上所述,小结一下:装饰器的作用是为已存在的函数或对象添加额外的功能。
①、装饰器本质上是一个Python函数,它可让其他函数在不需要做任何代码变动的前提下增加额外的功能,它的返回值也是一个函数对象(注意用词:函数对象。不带圆括号)。
②、装饰器适用于有切面需求的场景,比如:插入日志、性能测试、事物处理、缓存、权限校验等场景。装饰器是解决这类问题的绝佳设计。有了它,我们可抽离出大量与函数功能本身无关的雷同代码、并继续重用。



下方编写一个有具体功能的装饰器。 假设有个类,提供类似坐标的对象,仅是一些x、y的坐标对。可惜的是( 假设)这些坐标对象不支持数学运算符,并且我们不能对源代码进行修改,即也不能直接加入运算符的支持。

>>> class Coordinate:
...     def __init__(self, x, y):
...             self.x = x
...             self.y = y
...     def __repr__(self):
...             return "Coord:"+str(self.__dict__)
...

接下来将做一系列数学运算,对两个坐标对象进行加减运算,这个方法很容易写出:

>>> def add(a, b):#a,b分别表示两个坐标对象,分别有x、y值。
...     return Coordinate(a.x +b.x, a.y +b.y)
...
>>> def sub(a,b):
...     return Coordinate(a.x -b.x, a.y -b.y)
...
>>> one = Coordinate(100, 200)
>>> two = Coordinate(300, 200)
>>> add(one, two)
Coord:{'x': 400, 'y': 400}

假设还需要增加边界检查的行为,怎么办??

>>> three = Coordinate(-100, -100)
>>> sub(one, two)
Coord:{'x': -200, 'y': 0}
>>> add(one, three)
Coord:{'x': 0, 'y': 100}

在不更改坐标对象onetwothree的前提下:one减去two的值是{'x':0, 'y':0}one加上three的值是{'x':100, 'y':200}。与其给每个方法都加上参数和返回值边界检查的逻辑,下方将编写一个边界检查的装饰器:

>>> def wrapper(func):
...     def checker(a, b):#1
...             if a.x <0 or a.y <0:
...                     a = Coordinate(a.x if a.x>0 else 0, a.y if a.y >0 else 0)
...             if b.x <0 or b.y <0:
...                     b = Coordinate(b.x if b.x>0 else 0, b.y if b.y >0 else 0)
...             ret = func(a, b)
...             if ret.x <0 or ret.y <0:
...                     ret = Coordinate(ret.x if ret.x >0 else 0, ret.y if ret.y >0 else 0)
...             return ret
...     return checker
...
>>> add = wrapper(add)
>>> sub = wrapper(sub)
>>> sub(one, two)
Coord:{'x': 0, 'y': 0}
>>> add(one, three)
Coord:{'x': 100, 'y': 200}

上述代码的装饰器 像之前的装饰器例子一样进行工作,返回一个经过修改的函数。但在此例中,它能够对函数输入的参数、返回值做一些有用的检查、格式化工作,将负值的xy替换成0

很明显,通过这样的方式,代码变得更加简洁:将边界检查的逻辑隔离到单独的方法中;然后通过装饰器包装的方式应用到我们需要进行检查的地方。另外一种方式是通过在计算方法的开始处、返回值之前调用边界检查的方法也能够达到同样的目的。但不可置否的是,使用装饰器能够让我们以最小的代码量达到坐标边界检查的目的。

使用@标识符将装饰器应用到函数

Python 2.4版本以上,支持使用标识符

@将装饰器应用在函数上,只需要在函数的定义前加上 @装饰器名称。上方例子中是将原本的方法用装饰后的方法代替:

>>> add = wrapper(add)

上述方法能在任何时候对任意方法进行包装。但是如果我们自定义一个方法,可以使用@进行装饰:

>>> @wrapper
... def add(a, b):
...     return Coordinate(a.x + b.x, a.y + b.y)

上述两种方法效果是一样的。Python只是加了一些语法糖(即 @) 将装饰的行为更加直接明确、优雅。

*args**kwargs可变参数

上述已完成一个实现具体功能的装饰器,但它只能应用在一类具体的方法上,如上例中方法接收两个参数,传递给闭包捕获的函数。 如果想实现一个能够应用在任何方法上的装饰器该如何实现??

实例:实现一个能应用在任何方法上的类似于计数器的装饰器,不需要更变原有方法的任何逻辑。即意味着装饰器能够接受拥有任何签名的函数作为自己的被装饰方法,同时能够用传递给它的参数对被装饰的方法进行调用。

正巧,Python有支持这个特性的语法(官方指导)。在定义函数时,若使用了*,那么对于通过位置传递的参数将会被放在带有*前缀的变量中。

>>> def one(*args):
...     print(args)#1
...
>>> one()
()
>>> one(1, 2, 3)
(1, 2, 3)
>>> def two(x, y, *args):#2
...     print(x, y, args)
...
>>> two("a", "b", "c")
a b ('c',)

函数one()只是简单地将任何传递进的位置参数全部打印出来。#1只是引用了函数内的变量args*args仅是用在函数定义时用于表示位置参数应该存储在变量args中。Python允许编程者制定一些参数并通过args捕获其他所有剩余的未被捕获的位置参数。如#2所示。

*操作符 在函数被调用时也能使用。意义是一样的。当调用一个函数时,一个用*标志的变量 意味着变量里内容需要被提取出来,然后当作位置参数被使用。举例:

>>> def add(x, y):
...     return x + y
...
>>> lst = [1, 2]
>>> add(lst[0], lst[1])#1
3
>>> add(*lst)#2
3

#1#2所做的事情其实是一样的。*args 要么是表示调用方法时额外的参数可从一个可迭代列表中取得;要么是定义方法时标志该方法可接受任意的位置参数。

**代表键值对的参数字典,和*所代表的意义相差无几。

>>> def func(**kwargs):
...     print(kwargs)
...
>>> func()
{}
>>> func(x=1, y=2)
{'x': 1, 'y': 2}

当我们定义一个函数时,可用**kwargs来表明,所有未被捕获的关键字参数都应该存储在kwargs的字典中。argskwargs并不是Python语法的一部分,但在定义函数时,使用这样的变量名是一个不成文的约定。和*一样,同样可在定义或者调用函数时候使用**

>>> dct = {"x":1, "y":2}
>>> def bar(x, y):
...     return x + y
...
>>> bar(**dct)
3

2、更通用的装饰器

现在可以很轻松地编写一个能 记录下传递给函数的参数的装饰器了,并将其输出到界面。

>>> def logger(func):
...     def inner(*args, **kwargs):#1
...             print("Arguments were:%s, %s" % (args, kwargs))
...             return func(*args, **kwargs)#2
...     return inner
...

函数inner()可接受任意数量、类型的参数,并将它们传递给被包装的方法,这使得我们能用这个装饰器来装饰任何方法。

>>> @logger
... def func1(x, y=1):
...     return x * y
...
>>> @logger
... def func2():
...     return 2
...
>>> func1(5, 4)
Arguments were:(5, 4), {}
20
>>> func1(1)
Arguments were:(1,), {}
1
>>> func2()
Arguments were:(), {}
2

3、重点:更高级的装饰器

带参数的装饰器、类装饰器,属于进阶内容。

3.1、带参数的装饰器

被装饰的函数带参数、带参数的装饰器 两者有区别的哦。

装饰器本身也支持参数。达到这个目的就需要多一层嵌套函数 即可。装饰器传参就相当于三层函数嵌套,在闭包的外面包裹一层函数用于处理传入的参数。

实例1:进入某函数后打印出log信息、且要指定log的级别。

def logging(level):
	def wrapper(func):
		def inner_wrapper(*args, **kwargs):
			print("[%s]:enter function %s" % (level, func.__name__))
			return func(*args, **kwargs)	
		return inner_wrapper
	return wrapper

@logging(level="INFO")
def say(something):
	print("Say %s" % something)

@logging(level="DEBUG")
def do(something):
	print("Do %s..." % something)

if __name__ == "__main__":
	say("hello!")
	do("my work.")

运行结果:

[INFO]:enter function say
Say hello!
[DEBUG]:enter function do
Do my work....

实例2:通过装饰器的参数实现是否使用计时功能。

import time

def timer(bool=True):
	if bool:
		def _timer(func):
			def wrapper(*args, **kwargs):
				startTime = time.time()
				func(*args, **kwargs)
				endTime = time.time()
				msecs = (endTime - startTime) *1000
				print('->elapsed time:%f ms.' % msecs)
			return wrapper
	else:
		def _timer(func):
			return func
	return _timer

@timer(False)
def myFunc():
	print('start myFunc.')
	time.sleep(1)
	print('end myFunc.')

@timer(True)
def add(a, b):
	print('start add.')
	time.sleep(1)
	print('result is %d.' % (a+b))
	print('end add.')

print('myFunc is %s.' % myFunc.__name__)
myFunc()
print('add is %s.' % add.__name__)
add(1, 2)

运行结果:

myFunc is myFunc.
start myFunc.
end myFunc.
add is wrapper.
start add.
result is 3.
end add.
->elapsed time:1000.015736 ms.

等价于add(1, 2) = timer(True)(add(1, 2))myFunc() = timer(False(myFunc())

其中,作为参数的func函数 是在返回函数wrapper()内执行的;然后在add()前面加上@timer(True)add()函数就相当于被注入了计时功能,今后只要调用add(),它就变身为“新的、功能更多的”函数了。

3.2、基于类实现的装饰器

装饰器函数 其实是这样一个接口约束:它必须接受一个callable对象作为参数,然后返回一个callable对象。Python中一般callable对象都是函数,但也有例外。只要某个对象重载了__call__()方法,那么这个对象就是callable

class Test:
	def __call__(self):
		print("call me!")

t = Test()
t()

运行结果:

call me!

如同上述代码中__call__前后带2个下划线的方法在Python中称为内置方法(魔法方法)。重载这些魔法方法一般会改变对象的内部行为,上述示例作用是:让一个对象拥有被调用的行为。

装饰器要求接受一个callable对象,并返回一个callable对象,也就说:用类来实现装饰器也是OK的。即让类的构造函数__init__()接受一个函数(需要装饰的函数),然后重载__call__()并返回一个函数,以达到装饰器函数的效果。

class Logging:
	def __init__(self, func):
		self.func = func

	def __call__(self, *args, **kwargs):
		print("[DEBUG]:enter function %s()." % self.func.__name__)
		return self.func(*args, **kwargs)

@Logging
def say(something):
	print("Say %s!" % something)

if __name__ == '__main__':
	say('hello')

运行结果:

[DEBUG]:enter function say().
Say hello!

带参数的类装饰器
通过类形式实现带参数的装饰器:在构造函数中接受传入的参数,通过类把参数保存起来;然后在重载__call__()方法,接受一个函数并返回一个函数。

class Logging:
	def __init__(self, level="INFO"):
		self.level = level

	def __call__(self, func):#接受函数
		def wrapper(*args, **kwargs):
			print("[%s]:enter function %s()." % (self.level, func.__name__))
			func(*args, **kwargs)
		return wrapper#返回函数

@Logging(level="INFO")
def say(something):
	print("Say %s!" % something)

if __name__ == '__main__':
	say('hello')

运行结果:

[INFO]:enter function say().
Say hello!

3.3、装饰器调用顺序,支持多个装饰器

装饰器是可以叠加使用的,这就涉及到装饰器调用顺序问题。对于Python @ 语法糖,装饰器的调用顺序 与使用 @ 语法糖声明的顺序相反。
即下方使用装饰器的地方等价于:add(1, 2) = deco_1(deco2(add(1, 2)))

  1. 代码的执行顺序是从上往下,当遇到@deco_1时,它的下方并非一个函数,无法进行装饰;
  2. 接着 @deco_2 会对add()进行装饰,返回的是 add = deco_2(add) = wrapper_2
  3. 第一次装饰结束,此时@deco_1下方是一个函数了,即已是wrapper_2的引用,同理add = deco_1(wrapper_2) = wrapper_1
  4. 此时,两个装饰器都已经装饰完成。add()进行调用时的顺序是 调用@deco_1中的wrapper_1,运行其中的func(),在func()中调用wrapper_2,运行其中的func()指向原先的add()

多个装饰器执行顺序是:从最后一个装饰器开始,执行到第一个装饰器,再执行函数本身。

def deco_1(func):
	print('enter into deco_1.')
	def wrapper_1(a, b):
		print('enter into deco_1_wrapper_1.')
		func(a, b)
	return wrapper_1

def deco_2(func):
	print('enter into deco_2.')
	def wrapper_2(a, b):
		print('enter into deco_2_wrapper_2.')
		func(a, b)
	return wrapper_2

@deco_1
@deco_2
def add(a, b):
	print('result is %d.' % (a+b))

add(1, 2)

运行结果:

enter into deco_2.
enter into deco_1.
enter into deco_1_wrapper_1.
enter into deco_2_wrapper_2.
result is 3.
@a
@b
@c
def func():
	pass

等价于 a(b(c(func)))

3.4、Python 内置装饰器

内置装饰器 和我们自定义的装饰器(普通装饰器)实现原理是一样的,不过,前者返回类对象,不是函数。

Python 内置装饰器有3个:@property@staticmethod@classmethod

3.4.1、@property

property,译作 属性。在Python中,表示可以通过类实例直接访问的信息。使调用类中的方法像引用类中的字段属性一样。被修饰的特性方法,内部可以实现处理逻辑,但对外提供统一的调用方式。遵循了统一访问的原则。

属性有3个装饰器:gettersetterdeleter,都是基于property()进行的封装,因为setterdeleterproperty()的第二、三个参数,不能直接套 用@ 语法。getettr装饰器 与不带getter的属性装饰器效果是一样的(为了凑数?本身没有任何存在的意义)。经过@property装饰过的函数返回的不再是一个函数,而是一个property对象。

参考官方文档:@property

场景1:定义一个屏幕对象(宽、高、分辨率)。在未接触到@property前,是这么编写的:

class Screen:
	def __init__(self):
		self.width = 1024
		self.height = 768
s = Screen()
print(s.width, s.height)

运行结果:

1024 768

但在实际编码中可能会产生一个严重的问题,__init__()中定义的属性是可变的,In other words,其他开发人员在知道属性名的情况下,可进行随意更改(如 无意识情况下),这可能造成严重后果。

class Screen:
	def __init__(self):
		self.width = 1024
		self.height = 768
s = Screen()
s.height = 7.68#无意中点了小数点
print(s.width, s.height)

上述代码打印的 s.height结果将是7.68。可能难以排查到此错误。

解决方案:将widthheight属性都设为私有,其他人不可随意更改。@property就起到此作用。

class Screen:
	@property
	def width(self):
		#变量名 不跟方法名相同,改为_width
		return self._width
	
	@property
	def height(self):
		return self._height
s = Screen()
s.width = 1024#与方法名一致
s.height = 768
print(s.width, s.height)

@property使方法 像属性一样调用,如同一种特殊的属性。而此时,若要在外部给widthheight重新直接赋值将报错 AtributeError,这样保证了属性的安全性。即 read only(只读属性)。

若真想对属性进行操作,则提供了封装方法的方式进行属性的修改:

class Screen:

	@property
	def width(self):
		#变量名 不跟方法名相同,改为_width。下同
		return self._width
	@width.setter
	def width(self, value):
		self._width = value
	
	@property
	def height(self):
		return self._height
	@height.setter
	def height(self, value):
		self._height = value
	@height.deleter
	def height(self):
		del self._height

s = Screen()
s.width = 1024#与方法名一致
s.height = 768
print(s.width, s.height)

上述代码中,就可以对属性进行赋值操作、删除属性操作。

场景2:在定义数据库字段类时,可能需要对其中的类属性做一些限制,一般用getset方法来写,那么在Python中该如何以尽量少的代码优雅地实现想要的限制、减少错误的发发生。
举例:定义一个学生成绩表

class Student:
	def get_score(self):
		return self._score
	def set_score(self, value):
		if not isinstance(value, int):
			raise ValueError('score must be an integer!')
		if value < 0 or value > 100:
			raise ValueError('score must between 0~100!')
		self._score = value

一般情况下,是这么调用的:

>>> s = Student()
>>> s.set_score(60)
>>> s.get_score()
60
>>> s.set_score(200)
Traceback (most recent call last):
  File "<pyshell#4>", line 1, in <module>
    s.set_score(200)
  File "D:\ATP\common\test.py", line 8, in set_score
    raise ValueError('score must between 0~100!')
ValueError: score must between 0~100!

懒人思维(哈哈),为了方便、节省时间,不想写s.set_score(200),直接写s.score = 200更快:@property出场!修改代码如下:

class Student:
    @property
	def score(self):
		return self._score

	@score.setter
	def score(self, value):
		if not isinstance(value, int):
			raise ValueError('score must be an integer!')
		if value < 0 or value > 100:
			raise ValueError('score must between 0~100!')
		self._score = value

根据上述代码,将get方法变为属性只需加上@property装饰器即可,此时,@property本身又会创建另外一个装饰器(@score.setter),它负责将set方法变成给属性赋值,这个操作后,调用将变得可控、方便:

>>> s = Student()
>>> s.score = 60#实际转化为s.set_score(60)
>>> s.score#实际转化为s.get_score()
60
>>> s.score = 200
Traceback (most recent call last):
  File "<pyshell#12>", line 1, in <module>
    s.score = 200
  File "D:\ATP\common\test.py", line 11, in score
    raise ValueError('score must between 0~100!')
ValueError: score must between 0~100!

综上,@property提供了可读、可写、可删除操作;若要实现只读属性,只需定义@property即可,不定义代表禁止其他操作。@property就是负责将一个方法变成属性调用的。

3.4.2、@classmethod、@staticmethod (类方法、静态方法)

@classmethod@staticmethod 两个装饰器的原理差不多:但具体使用上有细微差别。

  • 通常情况下,在类中定义的所有函数(注意用词,是“所有”,跟self没关系,self也只是一个普通的参数而已)都是对象的绑定方法,对象在调用绑定办法时会自动将自己作为参数传递给方法的第一个参数。除此之外有两种常见的方法:静态方法、类方法,二者皆为类量身定制,但实例非要使用(不建议),也不会报错。是一种普通函数,位于类定义的命名空间中,不会对任何实例类型进行操作。
  • @classmethod 返回一个classmethod类对象,与成员方法的区别在于其必须使用类对象作为第一个参数(即 所接收的第一个参数一定是一个类),是将类本身作为操作的方法。类方法被哪个类调用,就传入哪个类作为第一个参数进行操作。用于指定一个类的方法为类方法,而没有它时指定的类为实例方法。
  • @staticmethod 返回一个staticmethod类对象,类静态方法,其跟成员方法的区别是没有self参数(可以不传递任何参数),并且可以在类不进行实例化的情况下调用,可以通过类名直接引用到达将函数功能与实例解绑的效果。

两者调用的是各自的__init__()构造函数。

class classmethod:
	'''
	classmethod(function) ->method
	'''
	def __init__(self, function):#for @classmethod decorator
		pass

class staticmethod:
	'''
	staticmethod(function) ->method
	'''
	def __init__(self, function):#for @staticmethod decorator
		pass

装饰器的 @ 语法糖 等同于调用了这两个类的构造函数:

class Coordinate:
	@staticmethod#装饰器
	def coo(x, y, z):
		print(x, y, z)
	#等价于 coo = staticmethod(coo)
print(type(Coordinate))#类型本质就是函数
Coordinate.coo(1, 2, 3)#调用时 函数应该有几个参数就传递几个参数

运行结果:

<class 'function'>
1 2 3

上述装饰器接口定义可更加明确一些,装饰器必须接受一个callable对象,其实它并不关心你返回什么,可以是另外一个callable对象(大部分情况下),也可以是其他类对象(如 property)。

场景:一个处理日期信息的类。用于存储日期信息(不考虑时区,用UTC表示)

class Date:
	day, month, year = 0, 0, 0

	def __init__(self, day=0, month=0, year=0):
		self.day = day
		self.month = month
		self.year = year

上述代码使用了典型的类实例初始化方法__init__,它作为典型的instancemethod接受参数,其中第一个参数传递的必要参数是新建的实例本身。
场景递进:假如有很多('dd-mm-yyyy')格式字符串的日期信息 需要被创建成Date类实例。常规方法是不得不在项目的不同地方完成此事:

  1. 分析得到的年月日字符串,将它们转化成三个整型变量 或拥有三个元素的元组的变量。
  2. 通过传递这些值实例化Date

得到:

day, month, year = map(int, string_date.split('-'))
date = Date(day, month, year)

优雅的Python使用@classmethod,一种另类的构造函数:

class Date:
	day, month, year = 0, 0, 0

	def __init__(self, day=0, month=0, year=0):
		self.day = day
		self.month = month
		self.year = year

	@classmethod
	def from_string(cls, date_as_string):#第一个参数cls,表示调用当前类名
		day, month, year = map(int, date_as_string.split('-'))
		date = cls(day, month, year)
		return date#返回一个初始化后的类

	def print_date(self):
		print('Year:%s, Month:%s, Day:%s.' % (self.year, self.month, self.day))

date = Date.from_string('29-10-2018')#使用@classmethod修饰的方法是类专属的,可直接通过类名调用。【不需要实例化类!!!用起来方便。】
date.print_date()

运行结果:

Year:2018, Month:10, Day:29.

解析上述代码、以及其优势:

  1. 在一个地方解析日期字符串,并且重复使用它。 date = Date.from_string('29-10-2018')相当于先调用from_string()对字符串解析出来,然后再使用Date的构造函数初始化。
  2. 做到很好的封装。(相对于将执行字符串解析作为一个单独的函数在任何地方执行,这里使用的方法更符合OOP(面向对象编程)的范式)。重构类时,不用修改构造函数,只需额外添加我们需要处理的函数,并使用@classmethod
    弥补了Python不支持构造函数重载的不足,如果想从某种程度上实现构造函数重载,可使用@classmethod方法。这样直接调用这个方法,可以完成构造函数的工作。
  3. cls 表示类对象,而不是类实例。This is very cool,因为如果继承Date类,那么所有的子类也将拥有from_string()方法。
  4. 【不需要实例化类!!!用起来方便。】调用时加上类名即可。OOP思想。使类中的方法变成一个普通函数。

@staticmethod@classmethod非常相似,但不强制要求传递参数,不过做的事与类方法或实例方法一样。

场景再递进:以某种方式验证日期字符串。与之前一样要求定义在Date类内部,但不要求实例化它。

class Date:
	day, month, year = 0, 0, 0

	def __init__(self, day=0, month=0, year=0):
		self.day = day
		self.month = month
		self.year = year

	@classmethod
	def from_string(cls, date_as_string):#第一个参数cls,表示调用当前类名
		day, month, year = map(int, date_as_string.split('-'))
		date = cls(day, month, year)
		return date#返回一个初始化后的类

	def print_date(self):
		print('Year:%s, Month:%s, Day:%s.' % (self.year, self.month, self.day))

	@staticmethod
	def is_date_valid(date_as_string):#用Date.is_date_valid()的形式产生实例
		day, month, year = map(int, date_as_string.split('-'))
		return day <= 31 and month <=12 and year <=3999

date = Date.from_string('29-10-2018')
date.print_date()
is_date = Date.is_date_valid('29-10-2018')
print(is_date)

运行结果:

Year:2018, Month:10, Day:29.
True

对于@staticmethod的使用,不需要访问它所属的类,它本质上就是一个函数,调用方式跟调用函数一样,不同之处在于 它不关注对象和对象内部属性。

小结,使用场景:

  • @classmethod主要用途是作为构造函数,在工厂模式下使用较多,即OOP继承时使用。Python只有一个构造函数__new__,若想要多种构造函数时就不方便了,只能在new里写一堆if isinstance。使用@classmethod就可以方便地编写不同的构造函数。
  • @staticmethod主要用于限定namespace,有利于组织代码和命名空间整洁。一般情况下可替换为外部的函数,后者继承时不可更改。虽然这个静态方法是个普通的function,但它只有这个class会用到,不适合作为module level function。
  • 实质是 需要创建静态方法或类方法操作实例对象的公有属性时使用。

3.5、装饰器也有坑

至此,看到的都是讲述装饰器的优点:让代码更加优雅;减少重复。 但天下没有完美之物,使用不当,也会带来一些问题。


位置错误的代码

def html_tags(tag_name):
	print('begin outer function.')
	def wrapper_(func):
		print('begin of inner wrapper function.')
		def wrapper(*args, **kwargs):
			content = func(*args, **kwargs)
			print('<%s>%s</%s>' % (tag_name, content, tag_name))
		print('end of inner wrapper function.')
		return wrapper
	print('end of outer function.')
	return wrapper_

@html_tags('b')
def hello(name='David'):
	return 'Hello %s' % name

hello()
hello()

在装饰器中各个可能的位置上加上了print()语句,用于记录被调用的情况。能预计出打印顺序吗?如果不能,那么最好不要在装饰器函数之外添加逻辑功能,否则这个装饰器就不受我们控制了。输出结果:

begin outer function.
end of outer function.
begin of inner wrapper function.
end of inner wrapper function.
<b>Hello David</b>
<b>Hello David</b>

错误的函数签名和文档

PS:函数签名,表示调用函数的方式,即定义了函数的输入和输出(函数的声明信息,包括参数、返回值、调用约定之类。)。在Python中,可以使用标准库inspect的一些方法或类,来操作或创建函数签名。

装饰器装饰过的函数 看上去 表面名字没变,其实已经变了。

def logging(func):
	def wrapper(*args, **kwargs):
		"""print log before a function."""
		print('[DEBUG] %s:enter %s().' % (datetime.now(), func.__name__))
		return func(*args, **kwargs)
	return wrapper

@logging
def say(something):
	"""say something"""
	print('say %s!' % something)

print(say.__name__)#wrapper

输出结果:

wrapper

为何上述打印的是 wrapper??
其实回顾装饰器的 @ 语法糖 就明白了。@ 等价于 say = logging(say)logging返回的函数名字就是wrapper,上述语句正是把这个结果赋值say,因而say__name__自然是wrapper了。不仅是name,其他属性都来自wrapper,如docsource

解决方案:使用标准库中的functools.wraps

from functools import wraps

def logging(func):
	@wraps(func)
	def wrapper(*args, **kwargs):
		"""print log before a function."""
		print('[DEBUG] %s:enter %s().' % (datetime.now(), func.__name__))
		return func(*args, **kwargs)
	return wrapper

@logging
def say(something):
	"""say something"""
	print('say %s!' % something)

print(say.__name__)#wrapper
print(say.__doc__)

运行结果:

say
say something

顺利解决。

import inspect
print inspect.getargspec(say)  # failed
print inspect.getsource(say)  # failed

但是函数签名、源码是拿不到的,除非借助第三方包 wrapt

不能装饰 @staticmethod@classmethod 如若将装饰器用在一个静态方法 或 类方法中,则将报错:

class Car:
    def __init__(self, model):
        self.model = model

    @logging #装饰实例方法,OK
    def run(self):
        print("%s is running!" % self.model)

    @logging #装饰静态方法,Failed
    @staticmethod
    def check_model_for(obj):
        if isinstance(obj, Car):
            print("The model of your car is %s" % obj.model)
        else:
            print("%s is not a car!" % obj)

运行将报:AttributeError: 'staticmethod' object has no attribute '__module__'

回顾一下,@staticmethod装饰器返回的是一个staticmethod对象,而不是callable对象。这是不符合装饰器要求的(如 传入一个callable对象),自然而然不可在其上方添加其他装饰器。
解决方案:将@staticmethod置前,因为装饰返回一个正常的函数,然后再加上@staticmethod就没问题了。

class Car(object):
    def __init__(self, model):
        self.model = model

    @staticmethod
    @logging  # 在@staticmethod之前装饰,OK
    def check_model_for(obj):
        pass

3.6、优化装饰器

嵌套的装饰器函数不太直观,可使用第三方包改进,让装饰器函数可读性更好。

  1. decorator.py模块
  2. wrapt包

装饰器的理念是对原函数、对象的加强,相当于重新封装,所以一般装饰器函数都命名为wrapper(),译作 包装。函数只有在被调用时才会发挥其作用。如 @logging装饰器可在函数执行时额外输出日志;@cache装饰过的函数可计算缓存结果。