python装饰函数

装饰器(Decorators)是 Python 的一个重要部分。简单地说:他们是修改其他函数的功能的函数。他们有助于让我们的代码更简短,也更Pythonic(Python范儿)。

每个人都有的内裤主要功能是用来遮羞,但是到了冬天它没法为我们防风御寒,咋办?我们想到的一个办法就是把内裤改造一下,让它变得更厚更长,这样一来,它不仅有遮羞功能,还能提供保暖,不过有个问题,这个内裤被我们改造成了长裤后,虽然还有遮羞功能,但本质上它不再是一条真正的内裤了。于是聪明的人们发明长裤,在不影响内裤的前提下,直接把长裤套在了内裤外面,这样内裤还是内裤,有了长裤后宝宝再也不冷了。装饰器就像我们这里说的长裤,在不影响内裤作用的前提下,给我们的身子提供了保暖的功效。

一切皆对象

首先我们来理解下 Python 中的函数:

  1. def hi(name="yasoob"):
  2. return "hi " + name
  3. print(hi())
  4. # output: 'hi yasoob'
  5. # 我们甚至可以将一个函数赋值给一个变量,比如
  6. greet = hi
  7. # 我们这里没有在使用小括号,因为我们并不是在调用hi函数
  8. # 而是在将它放在greet变量里头。我们尝试运行下这个
  9. print(greet())
  10. # output: 'hi yasoob'
  11. # 如果我们删掉旧的hi函数,看看会发生什么!
  12. del hi
  13. print(hi())
  14. #outputs: NameError
  15. print(greet())
  16. #outputs: 'hi yasoob'

在函数中定义函数

刚才那些就是函数的基本知识了。我们来让你的知识更进一步。在 Python 中我们可以在一个函数中定义另一个函数:

  1. def hi(name="yasoob"):
  2. print("now you are inside the hi() function")
  3. def greet():
  4. return "now you are in the greet() function"
  5. def welcome():
  6. return "now you are in the welcome() function"
  7. print(greet())
  8. print(welcome())
  9. print("now you are back in the hi() function")
  10. hi()
  11. #output:now you are inside the hi() function
  12. # now you are in the greet() function
  13. # now you are in the welcome() function
  14. # now you are back in the hi() function
  15. # 上面展示了无论何时你调用hi(), greet()和welcome()将会同时被调用。
  16. # 然后greet()和welcome()函数在hi()函数之外是不能访问的,比如:
  17. greet()
  18. #outputs: NameError: name 'greet' is not defined

那现在我们知道了可以在函数中定义另外的函数。也就是说:我们可以创建嵌套的函数。现在你需要再多学一点,就是函数也能返回函数。

从函数中返回函数

其实并不需要在一个函数里去执行另一个函数,我们也可以将其作为输出返回出来:

  1. def hi(name="yasoob"):
  2. def greet():
  3. return "now you are in the greet() function"
  4. def welcome():
  5. return "now you are in the welcome() function"
  6. if name == "yasoob":
  7. return greet
  8. else:
  9. return welcome
  10. a = hi()
  11. print(a)
  12. #outputs: <function greet at 0x7f2143c01500>
  13. #上面清晰地展示了`a`现在指向到hi()函数中的greet()函数
  14. #现在试试这个
  15. print(a())
  16. #outputs: now you are in the greet() function

再次看看这个代码。在 if/else 语句中我们返回 greet 和 welcome,而不是 greet() 和 welcome()。为什么那样?这是因为当你把一对小括号放在后面,这个函数就会执行;然而如果你不放括号在它后面,那它可以被到处传递,并且可以赋值给别的变量而不去执行它。 你明白了吗?让我再稍微多解释点细节。

当我们写下 a = hi(),hi() 会被执行,而由于 name 参数默认是 yasoob,所以函数 greet 被返回了。如果我们把语句改为 a = hi(name = “ali”),那么 welcome 函数将被返回。我们还可以打印出 hi()(),这会输出 now you are in the greet() function

将函数作为参数传给另一个函数

  1. def hi():
  2. return "hi yasoob!"
  3. def doSomethingBeforeHi(func):
  4. print("I am doing some boring work before executing hi()")
  5. print(func())
  6. doSomethingBeforeHi(hi)
  7. #outputs:I am doing some boring work before executing hi()
  8. # hi yasoob!

现在你已经具备所有必需知识,来进一步学习装饰器真正是什么了。装饰器让你在一个函数的前后去执行代码。

你的第一个装饰器

在上一个例子里,其实我们已经创建了一个装饰器!现在我们修改下上一个装饰器,并编写一个稍微更有用点的程序:

  1. def a_new_decorator(a_func):
  2. def wrapTheFunction():
  3. print("I am doing some boring work before executing a_func()")
  4. a_func()
  5. print("I am doing some boring work after executing a_func()")
  6. return wrapTheFunction
  7. def a_function_requiring_decoration():
  8. print("I am the function which needs some decoration to remove my foul smell")
  9. a_function_requiring_decoration()
  10. #outputs: "I am the function which needs some decoration to remove my foul smell"
  11. a_function_requiring_decoration = a_new_decorator(a_function_requiring_decoration)
  12. #now a_function_requiring_decoration is wrapped by wrapTheFunction()
  13. a_function_requiring_decoration()
  14. #outputs:I am doing some boring work before executing a_func()
  15. # I am the function which needs some decoration to remove my foul smell
  16. # I am doing some boring work after executing a_func()

你看明白了吗?我们刚刚应用了之前学习到的原理。这正是 python 中装饰器做的事情!它们封装一个函数,并且用这样或者那样的方式来修改它的行为。

@ 语法糖

如果你接触 Python 有一段时间了的话,想必你对 @ 符号一定不陌生了,没错 @ 符号就是装饰器的语法糖,它放在函数开始定义的地方,这样就可以省略最后一步再次赋值的操作。

这里是我们如何使用 @ 来运行之前的代码:

  1. @a_new_decorator
  2. def a_function_requiring_decoration():
  3. """Hey you! Decorate me!"""
  4. print("I am the function which needs some decoration to "
  5. "remove my foul smell")
  6. a_function_requiring_decoration()
  7. #outputs: I am doing some boring work before executing a_func()
  8. # I am the function which needs some decoration to remove my foul smell
  9. # I am doing some boring work after executing a_func()
  10. #the @a_new_decorator is just a short way of saying:
  11. a_function_requiring_decoration = a_new_decorator(a_function_requiring_decoration)

希望你现在对 Python 装饰器的工作原理有一个基本的理解。如果我们运行如下代码会存在一个问题:

  1. print(a_function_requiring_decoration.__name__)
  2. # Output: wrapTheFunction

这并不是我们想要的!Ouput输出应该是”a_function_requiring_decoration”。这里的函数被warpTheFunction替代了。它重写了我们函数的名字和注释文档(docstring)。幸运的是Python提供给我们一个简单的函数来解决这个问题,那就是functools.wraps。我们修改上一个例子来使用functools.wraps:

  1. from functools import wraps
  2. def a_new_decorator(a_func):
  3. @wraps(a_func)
  4. def wrapTheFunction():
  5. print("I am doing some boring work before executing a_func()")
  6. a_func()
  7. print("I am doing some boring work after executing a_func()")
  8. return wrapTheFunction
  9. @a_new_decorator
  10. def a_function_requiring_decoration():
  11. """Hey yo! Decorate me!"""
  12. print("I am the function which needs some decoration to "
  13. "remove my foul smell")
  14. print(a_function_requiring_decoration.__name__)
  15. # Output: a_function_requiring_decoration

args、*kwargs

可能有人问,如果我的业务逻辑函数 foo 需要参数怎么办?比如:

  1. def foo(name):
  2. print("i am %s" % name)

我们可以在定义 wrapper 函数的时候指定参数:

def wrapper(name):
        logging.warn("%s is running" % func.__name__)
        return func(name)
    return wrapper

这样 foo 函数定义的参数就可以定义在 wrapper 函数中。这时,又有人要问了,如果 foo 函数接收两个参数呢?三个参数呢?更有甚者,我可能传很多个。当装饰器不知道 foo 到底有多少个参数时,我们可以用 *args 来代替:

def wrapper(*args):
        logging.warn("%s is running" % func.__name__)
        return func(*args)
    return wrapper

如此一来,甭管 foo 定义了多少个参数,我都可以完整地传递到 func 中去。这样就不影响 foo 的业务逻辑了。这时还有读者会问,如果 foo 函数还定义了一些关键字参数呢?比如:

def foo(name, age=None, height=None):
    print("I am %s, age %s, height %s" % (name, age, height))

这时,你就可以把 wrapper 函数指定关键字函数:

def wrapper(*args, **kwargs):
        # args是一个数组,kwargs一个字典
        logging.warn("%s is running" % func.__name__)
        return func(*args, **kwargs)
    return wrapper

带参数的装饰器

装饰器还有更大的灵活性,例如带参数的装饰器,在上面的装饰器调用中,该装饰器接收唯一的参数就是执行业务的函数 foo 。装饰器的语法允许我们在调用时,提供其它参数,比如@decorator(a)。这样,就为装饰器的编写和使用提供了更大的灵活性。比如,我们可以在装饰器中指定日志的等级,因为不同业务函数可能需要的日志级别是不一样的。

def use_logging(level):
    def decorator(func):
        def wrapper(*args, **kwargs):
            if level == "warn":
                logging.warn("%s is running" % func.__name__)
            elif level == "info":
                logging.info("%s is running" % func.__name__)
            return func(*args)
        return wrapper

    return decorator

@use_logging(level="warn")
def foo(name='foo'):
    print("i am %s" % name)

foo()

上面的 use_logging 是允许带参数的装饰器。它实际上是对原有装饰器的一个函数封装,并返回一个装饰器。我们可以将它理解为一个含有参数的闭包。当我 们使用@use_logging(level="warn")调用的时候,Python 能够发现这一层的封装,并把参数传递到装饰器的环境中。

@use_logging(level="warn")`等价于`@decorator

类装饰器

没错,装饰器不仅可以是函数,还可以是类,相比函数装饰器,类装饰器具有灵活度大、高内聚、封装性等优点。使用类装饰器主要依靠类的__call__方法,当使用 @ 形式将装饰器附加到函数上时,就会调用此方法。

class Foo(object):
    def __init__(self, func):
        self._func = func

    def __call__(self):
        print ('class decorator runing')
        self._func()
        print ('class decorator ending')

@Foo
def bar():
    print ('bar')

bar()

functools.wraps

使用装饰器极大地复用了代码,但是他有一个缺点就是原函数的元信息不见了,比如函数的docstring__name__、参数列表,先看例子:

# 装饰器
def logged(func):
    def with_logging(*args, **kwargs):
        print func.__name__      # 输出 'with_logging'
        print func.__doc__       # 输出 None
        return func(*args, **kwargs)
    return with_logging

# 函数
@logged
def f(x):
   """does some math"""
   return x + x * x

logged(f)

不难发现,函数 f 被with_logging取代了,当然它的docstring__name__就是变成了with_logging函数的信息了。好在我们有functools.wrapswraps本身也是一个装饰器,它能把原函数的元信息拷贝到装饰器里面的 func 函数中,这使得装饰器里面的 func 函数也有和原函数 foo 一样的元信息了。

from functools import wraps
def logged(func):
    @wraps(func)
    def with_logging(*args, **kwargs):
        print func.__name__      # 输出 'f'
        print func.__doc__       # 输出 'does some math'
        return func(*args, **kwargs)
    return with_logging

@logged
def f(x):
   """does some math"""
   return x + x * x

装饰器顺序

一个函数还可以同时定义多个装饰器,比如:

@a
@b
@c
def f ():
    pass

它的执行顺序是从里到外,最先调用最里层的装饰器,最后调用最外层的装饰器,它等效于

f = a(b(c(f)))