平凡

python的装饰器

2018/10/01 Share

翻译自:
https://www.programiz.com/python-programming/decorator


装饰器接收一个函数,为其加入一些功能并返回它。
它又叫作元类编程,因为一部分程序在运行时尝试修改另外一部分程序。

简单说来,装饰器像一个包裹,不用更改这个函数自身,改变原函数代码的功能(增强了它原来的功能)。就像一包月饼卖10块钱,我们给它套上精致华丽骚气而浮夸的包装盒,瞬间能卖上千了有木有,但月饼还是原来的月饼,并没有任何改变。

前置知识

我们必须习惯,在python中,任何东西都是对象。名字只是指向这些对象的一个标识。函数也不例外,它们也是带有属性的对象,不同的名字可以指向同一个函数对象。
下面是一个例子:

def first(msg):
    print(msg)    

first("Hello")

second = first
second("Hello")

当你运行这段代码,firstsecond的输出都是一样的。它们都指向了同一个函数对象。

于是就出现了奇怪的东东。

函数可以当作参数,传给另一个函数。

如果在使用过pythonmap, filter,就应该理解这个(把lambda函数当作map的参数??)。

这些把其他函数当作参数的函数叫做高阶函数(higher order functions)。 下面是一个例子:

def inc(x):
    return x + 1

def dec(x):
    return x - 1

def operate(func, x):
    result = func(x)
    return result

我们这样调用函数:

>>> operate(inc,3)
4
>>> operate(dec,3)
2

甚至,一个函数可以返回另一个函数:

def is_called():
    def is_returned():
        print("Hello")
    return is_returned

new = is_called()

#Outputs "Hello"
new()

is_returned()是一个嵌套函数,每次我们调用is_called,都返回的是这个函数。

这是python中的闭包机制。

装饰器

def make_pretty(func):
    def inner():
        print("I got decorated")
        func()
    return inner

def ordinary():
    print("I am ordinary")

运行上面程序:

>>> ordinary()
I am ordinary

>>> # let's decorate this ordinary function
>>> pretty = make_pretty(ordinary)
>>> pretty()
I got decorated
I am ordinary

上面例子中,make_pretty()是一个装饰器,pretty = make_pretty(ordinary)这行,ordinary()函数被装饰,并且将新的函数以名字pretty返回。

可以看到,装饰器为原来的函数添加了新的功能。这就像为礼物套上包装一样。对象的表面看起来像是改变了,但里面的干货都没变,而且变得好看了。

我们可以使用@符号来装饰,它下面的函数是被装饰的函数,@后面跟的是装饰函数,例如:

@make_pretty
def ordinary():
    print("I am ordinary")

它等价于:

def ordinary():
    print("I am ordinary")
ordinary = make_pretty(ordinary)

使用@形式,本质上是语法糖。

装饰带有参数的函数

上面的装饰器非常简单,因为它装饰的是没有任何参数的函数。如果是带有参数的函数,例如下面这样:

def divide(a, b):
    return a/b

上面的函数有两个参数,如果将b设置为0,会报错。

>>> divide(2,5)
0.4
>>> divide(2,0)
Traceback (most recent call last):
...
ZeroDivisionError: division by zero

现在我们做一个装饰器,来检查错误

def smart_divide(func):
   def inner(a,b):
      print("I am going to divide",a,"and",b)
      if b == 0:
         print("Whoops! cannot divide")
         return

      return func(a,b)
   return inner

@smart_divide
def divide(a,b):
    return a/b

如果传入b=0,将会返回None。

>>> divide(2,5)
I am going to divide 2 and 5
0.4

>>> divide(2,0)
I am going to divide 2 and 0
Whoops! cannot divide

使用这种方式,可以装饰带参数的函数。

如果你眼尖,会发现装饰器中的inner(a,b)与原函数divide(a,b)参数是对应的。所以,我们可以让装饰器匹配任意数量的参数。

在python中,可以用function(*args, **kwargs)来解决这个问题。例如:

def works_for_all(func):
    def inner(*args, **kwargs):
        print("I can decorate any function")
        return func(*args, **kwargs)
    return inner

装饰器链

一个函数可以连续被多个装饰器装饰(就像月饼外面可以层层套包装)。

def star(func):
    def inner(*args, **kwargs):
        print("*" * 30)
        func(*args, **kwargs)
        print("*" * 30)
    return inner

def percent(func):
    def inner(*args, **kwargs):
        print("%" * 30)
        func(*args, **kwargs)
        print("%" * 30)
    return inner

@star
@percent
def printer(msg):
    print(msg)
printer("Hello")

上面代码的输出如下:

******************************
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
Hello
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
******************************

上面的代码:

@star
@percent
def printer(msg):
    print(msg)

等价于:

def printer(msg):
    print(msg)
printer = star(percent(printer))

所以,装饰器的顺序对结果是有影响的,如果我们这样写:

@percent
@star
def printer(msg):
    print(msg)

结果就会相反:

The execution would take place as,

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
******************************
Hello
******************************
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

带参数的装饰器

有时会往装饰器中传入参数。例如我们想写一个装饰器repeat,功能是执行N次被装饰的函数。在我们使用它时,只需要指定num_times,即@repeat(num_times),被装饰的函数就会执行相应的次数。

例如:

@repeat(num_times=4)
def greet(name):
    print(f"Hello {name}")

输出:

>>> greet("World")
Hello World
Hello World
Hello World
Hello World

应该怎么实现上述功能呢?

根据之前的代码,装饰器函数都是长这个样子:

def repeat(num_times):
    def decorator_repeat(func):
        ...  # Create and return a wrapper function
    return decorator_repeat

它创建并返回了一个内部的wrapper函数。

一般,实现带有参数的装饰器都是在内部函数里再嵌套一个内部函数。听着比较绕口,就像《盗梦空间》的剧情一样:

def repeat(num_times):
    def decorator_repeat(func):
        @functools.wraps(func)
        def wrapper_repeat(*args, **kwargs):
            for _ in range(num_times):
                value = func(*args, **kwargs)
            return value
        return wrapper_repeat
    return decorator_repeat

看起来有点乱,总共有三层def。但只不过是比之前的多添加了一层def处理传入装饰器的参数。让我们先来看最里面的一层:

def wrapper_repeat(*args, **kwargs):
    for _ in range(num_times):
        value = func(*args, **kwargs)
    return value

wrapper_repeat接收与被装饰函数相同数量的参数,并且返回这个函数(func)的执行结果。在内部,调用Nfunc。 这与之前的装饰器无本质区别,只是多传入了一个num_times参数。

最外层的repeat函数返回了装饰函数decorator_repeat的一个引用:

def repeat(num_times):
    def decorator_repeat(func):
        ...
    return decorator_repeat

repeat函数中,有些细节需要注意:

  • 定义decorator_repeat()作为内部函数意味着repeat()会调用这个函数对象。之前,装饰器调用这个函数时没有使用括号传入参数,但使用装饰器传入参数时,就需要加上括号。
  • num_timesrepeat函数中好像没有用到,但是它由python的闭包机制传入到wrapper_repeat函数中。

现在,函数就得到了我们想要的执行效果:

@repeat(num_times=4)
def greet(name):
    print(f"Hello {name}")

>>> greet("World")
Hello World
Hello World
Hello World
Hello World

例2

如果我们想方便的开启或关闭某些函数。可以使用这种带参数的装饰器:

import functools


def decorator_with_arguments(number):
    def my_decorator(func):
        @functools.wraps(func)
        def real_wrapper(*args, **kwargs):
            print("start to decorate!")
            if number == 233:
                print("Not running the function")
            else:
                func(*args, **kwargs)
            print("After the decoration!")

        return real_wrapper

    return my_decorator


@decorator_with_arguments(0)
def add(a, b):
    print(a + b)


add(1, 2)

执行结果:

  • start to decorate!
    3
    After the decoration!
    
  • 当传入装饰器的number != 233,执行add函数,否则执行add函数

  • 这种形式在python的一些框架如flask中经常见到。

喂丸待续

发表日期: October 1st 2018

版权声明: 本文采用知识共享署名-非商业性使用 4.0 国际许可协议进行许可

CATALOG
  1. 1. 前置知识
  2. 2. 装饰器
  3. 3. 装饰带有参数的函数
  4. 4. 装饰器链
  5. 5. 带参数的装饰器