By definition, a decorator is a function that takes another function and extends the behavior of the latter function without explicitly modifying it.
装饰器可以让我们在不修改一个函数的情况下,扩展函数的功能。让我们的代码更加优雅。
如何使用装饰器
我们的目标是,让函数每一次使用的时候都输出函数的使用时间。最简单的,我们可以这样:
import time
def print_time(func):
print(time.strftime("Time: %H:%M:%S",time.gmtime()))
func()
def foo():
print("I'm foo")
print_time(foo)
但是这样,我们每次调用 foo
的时候都要调用 print_time(foo)
。我们通过一个简单的装饰器来完成这个任务。我们用 wrapper 把函数包装了起来,并且这样还能很好的处理参数。
import time
def print_time(func):
def wrapper(num):
print(time.strftime("Time: %H:%M:%S",time.gmtime()))
func(num)
return wrapper
def foo(num):
print("We are {} foo".format(num))
foo = print_time(foo)
foo(2)
Python 提供了一个语法糖 @
,让我们用更简单的办法完成这件事情。@my_decorator
是 func = my_decorator(func)
的简单写法。
另外注意 wrapper(*args, **kwargs)
,这样就能处理任意多个参数的情况了。
import time
def print_time(func):
#注意这个参数的写法,可以处理任意多的参数
def wrapper(*args, **kwargs):
print(time.strftime("Time: %H:%M:%S",time.gmtime()))
#这样能处理有返回值的情况,虽然func没有
return func(*args, **kwargs)
return wrapper
#@语法糖
@print_time
def foo(num):
print("We are {} foo".format(num))
foo(2)
还可以包装上额外的参数。@print_time
等价于 foo = print_time(foo)
,注意到函数 print_time
返回的是一个函数,就不难理解了。
另外用包装器封装会失去函数的 metadata(比如type hint),我们使用 @wraps(func)
来解决这个问题。通过这个装饰器把 func 的 metadata 复制到 decorator 里去。来看一个例子 @debug
,提供调试信息的输出。注意 f-string 是 Python3.6 版本之后才有的新特性。
import functools
def debug(func):
"""Print the function signature and return value"""
@functools.wraps(func)
def wrapper_debug(*args, **kwargs):
args_repr = [repr(a) for a in args] # 1
kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()] # 2
signature = ", ".join(args_repr + kwargs_repr) # 3
print(f"Calling {func.__name__}({signature})")
value = func(*args, **kwargs)
print(f"{func.__name__!r} returned {value!r}") # 4
return value
return wrapper_debug
class A:
@debug
def func(self):
pass
如果装饰器还需要参数,就这么写。上面的理解成 func = debug(func)
,下面的理解成 func = debug("Warning")(func)
。
def debug(text):
def _debug(func):
"""Print the function signature and return value"""
@functools.wraps(func)
def wrapper_debug(*args, **kwargs):
args_repr = [repr(a) for a in args] # 1
kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()] # 2
signature = ", ".join(args_repr + kwargs_repr) # 3
print(text, f"Calling {func.__name__}({signature})")
value = func(*args, **kwargs)
print(text, f"{func.__name__!r} returned {value!r}") # 4
return value
return wrapper_debug
return _debug
class A:
@debug("Warning")
def func(self):
pass
常用装饰器 :star:
内置装饰器:@staticmathod
、@classmethod
、@property
。我们在介绍 python 的类的时候提到。
- @property:把类内方法当成属性来使用,必须要有返回值,相当于 getter;
- @staticmethod:静态方法,取消第一个参数 self。
- @classmethod:类方法,第一个参数转变成表示自身类的 cls 参数。
在面向对象(OOP)的设计模式中,decorator 被称为装饰模式。和这个长得比较像的 Rust 的过程宏,他更加简单。
# @property的常用方法如下
class A:
@property
def x(self):
"I am the 'x' property."
return self._x
@x.setter
def x(self, value):
self._x = value
@x.deleter
def x(self):
del self._x
a = A()
a.x = 1 # setter
print(a.x) # getter
del a.x # deleter
深入理解装饰器
Python 什么时候运行装饰器
答案是,装饰器在函数定义之后立即运行,而不是在函数第一次执行时运行。其实原因也是比较显然的,你写 def
的时候产生一个实例嘛。
闭包
多数装饰器会修改被装饰的函数。通常会像上面的例子一样,定义一个内部函数,然后把它返回替换被装饰的函数。使用内部函数的代码几乎都要依靠闭包才能正常运作。
首先我们要了解 python 中变量的 作用域。变量有全局变量和局部变量。下面的例子中,a,c 就只有局部作用域,而 b 是全局变量。要在函数中引用全局变量,应该用 global 关键字声明。在 Python 中没有变量声明这个东西。对一个变量进行赋值的动作就是声明。所以在下面的例子中,如果你把 global b
删除,那么最后 print(b)
得到的将会是 1,函数中的 b 是局部变量,和全局变量 b 没有关系。
def f(a):
global b
c=1
b=2
print(a)
print(b)
print(c)
b=1
f(1)
print(b)
我们再来看这样一个求平均数的例子。每次调用 avg 都可以求之前所有数字的平均。可是,series 是 make_average 的局部变量,因为在那个函数中初始化了 series 变量。可是在我们调用 avg 函数的时候,make_average 早就已经返回了,它的本地作用域也一起不复返了。
对于 average 函数来说,series 是 *变量 (free variable),指未在本地作用域中绑定的变量。函数 average 的闭包包括了 series 的声明。不过我们之前也提到了,对一个变量的赋值在 python 中起到了声明的效果。所以和上面的 global
关键字类似,为了把变量声明为*变量而不是局部变量,我们使用 nonlocal
关键字。
def make_average():
series = []
def average(new):
nonlocal series #在这个函数中不是必要的,因为我们没有给series赋值
series.append(new)
total = sum(series)
return total/len(series)
return average
avg = make_average()
print(avg(1))
print(avg(3))
如果你想查看一下*变量,可以执行一下 print(avg.__code__.co_freevars)
。
函数就是对象
在 python 中函数就是一个对象,或者说,函数是一个 callable 的对象,对象如果 callable,那么它也能表现的像一个函数。怎么让一个函数 callable 呢?定义一个 __call__
方法。
我们使用 dir
内置函数查看一下函数都有哪些属性,其中有一些属性是所有的对象共有的,有一些属性是所有的函数都有的:
>>> def a():
... '''demo'''
... return 1
...
>>> dir(a)
['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']
-
__name__
:函数的名字,str -
__globals__
:函数所在模块中的全局变量,dict -
__closure__
:函数闭包,即*变量的绑定,tuple -
__default__
:形式参数的默认值,tuple -
__annotations__
:参数和返回值的注解,dict -
__code__
:编译成字节码的函数 metadata 和函数定义体