今天看啥  ›  专栏  ›  极客兔兔

深入理解Python-2.什么是装饰器(decorators)

极客兔兔  · 掘金  ·  · 2018-06-22 02:33

深入理解Python-2.什么是装饰器(decorators)

1)装饰器基础

Python中函数也是对象

为了理解装饰器,你必须先理解Python中函数也是对象,这很重要。接下用个简单的例子来解释。

def shout(word="yes"):
    return word.capitalize() + "!"

print(shout())
# 输出: 'Yes!'

# 作为一个对象,你也能像其他对象一样把一个函数赋值给一个变量。
scream = shout

# 注意,这里并没有使用括号:没有调用shout
# 仅仅是把shout赋值给scream,这意味着接下来可以调用scream

print(scream())
# 输出: 'Yes!'

# 不仅如此,你还可以删除shout,
# 这个函数仍然可以通过scream访问到。

del shout
try:
    print(shout())
except NameError, e:
    print(e)
    # 输出: "name 'shout' is not defined"

print(scream())
# 输出: 'Yes!'

记住这一点,我们很快会用到。

在Python中函数的另一个重要的特性是它们可以再另一个函数中声明。

def talk():

    # 在 "talk" 中可以随意定义一个函数...
    def whisper(word="yes"):
        return word.lower() + "..."

    # ... 而且可以理解调用
    print(whisper())

# 每次调用 “talk”,内部声明的 "whisper" 也会被调用。
talk()
# 输出: "yes..."

# 但是在 "talk" 之外,"whisper" 并不存在:

try:
    print(whisper())
except NameError, e:
    print(e)
    # 输出 : "name 'whisper' is not defined"*
    # Python的函数是对象,就像函数内部定义的其他普通对象一样。

函数引用

接下来就比较有趣了。

你已经知道了函数是对象,因此函数:

  • 可以被赋值给一个变量。
  • 可以在另一个函数中定义。

这也意味着,一个函数可以返回另一个函数。

def getTalk(kind="shout"):

    # 随意定义函数
    def shout(word="yes"):
        return word.capitalize()+"!"

    def whisper(word="yes") :
        return word.lower()+"...";

    # 我们返回其中之一。
    if kind == "shout":
        # 不使用 "()", 即不调用这个函数,仅返回函数对象。
        return shout  
    else:
        return whisper

# 返回一个函数,并将它赋值给一个变量。
talk = getTalk()

# 接下来你可以看到“talk”是一个函数对象。
print(talk)
# 输出: <function shout at 0xb7ea817c>

# 这个对象是被 “getTalk” 函数返回的。
print(talk())
# 输出: Yes!

# 你甚至可以直接在返回时调用。
print(getTalk("whisper")())
# 输出: yes...

不仅如此,如果你能返回一个函数,你也能将其作为一个参数传递:

def doSomethingBefore(func):
    print("I do something before then I call the function you gave me")
    print(func())

doSomethingBefore(scream)
# 输出:
# I do something before then I call the function you gave me
# Yes!

你已经掌握了需要理解装饰器一切基础概念了。正如你所见,装饰器就是函数的“包装”,这意味着装饰器 让你能够在它装饰的函数前后,去执行另外的一些代码,而不修改函数本身。

2)手工装饰

如何手工装饰函数呢?

# 一个装饰器接收另一个函数作为参数。
def my_shiny_new_decorator(a_function_to_decorate):

    # 函数内部,定义了另一个函数来包装(wrapper)原函数,
    # 因此能够在原函数前后执行一些代码。
    def the_wrapper_around_the_original_function():

        # 想在原函数前执行的代码
        print("Before the function runs")

        # 使用括号调用原函数
        a_function_to_decorate()

        # 想在原函数后执行的代码
        print("After the function runs")

    # 在这里,装饰函数并没有执行。我们仅仅是返回我们刚创建的包装函数。
    # 这个包装函数包含了原函数以及想在原函数前后执行的代码,一起准备就绪。
    return the_wrapper_around_the_original_function

# 现在创建一个普通的函数。
def a_stand_alone_function():
    print("I am a stand alone function, don't you dare modify me")

a_stand_alone_function()
# 输出: I am a stand alone function, don't you dare modify me

# 好了,你可以装饰它扩展它的行为
# 只需要把它传给装饰器,就可以用任何代码去动态地包装它,并返回一个等待调用的函数。

a_stand_alone_function_decorated = my_shiny_new_decorator(a_stand_alone_function)
a_stand_alone_function_decorated()

# 输出:
# Before the function runs
# I am a stand alone function, don't you dare modify me
# After the function runs

现在,你很可能想,调用 a_stand_alone_function_decorated来代替调用a_stand_alone_function。那很简单,仅需要使用 my_shiny_new_decorator返回的函数对象覆盖a_stand_alone_function的值即可。

a_stand_alone_function = my_shiny_new_decorator(a_stand_alone_function)
a_stand_alone_function()
# 输出:
# Before the function runs
# I am a stand alone function, don't you dare modify me
# After the function runs

# 这就是装饰器所做的事情!

3)解密装饰器

使用装饰器改写之前的例子。

@my_shiny_new_decorator
def another_stand_alone_function():
    print("Leave me alone")

another_stand_alone_function()  
# 输出:  
# Before the function runs
# Leave me alone
# After the function runs

这很简单,@decorator 相当于是这个赋值表达式的简写。

another_stand_alone_function = my_shiny_new_decorator(another_stand_alone_function)

装饰器仅仅是装饰器模式的Pythonic实现。Python中内置了很多经典的设计模式简化开发(例如迭代器模式)。

当前,你也可以叠加装饰器:

def bread(func):
    def wrapper():
        print("</''''''\>")
        func()
        print("<\______/>")
    return wrapper

def ingredients(func):
    def wrapper():
        print("#tomatoes#")
        func()
        print("~salad~")
    return wrapper

def sandwich(food="--ham--"):
    print(food)

sandwich()
# 输出: --ham--
sandwich = bread(ingredients(sandwich))
sandwich()
# 输出:
# </''''''\>
# #tomatoes#
# --ham--
# ~salad~
# <\______/>

使用Python的装饰器语法:

@bread
@ingredients
def sandwich(food="--ham--"):
    print(food)

sandwich()
#outputs:
# </''''''\>
# #tomatoes#
# --ham--
# ~salad~
# <\______/>

装饰器的顺序会影响最终的结果:

@ingredients
@bread
def strange_sandwich(food="--ham--"):
    print(food)

strange_sandwich()
# 输出:
# #tomatoes#
# </''''''\>
# --ham--
# <\______/>
# ~salad~

接下来介绍装饰器更高级的用法。

4)装饰器进阶

给被装饰函数传参数

# 给包装函数传递参数

def a_decorator_passing_arguments(function_to_decorate):
    def a_wrapper_accepting_arguments(arg1, arg2):
        print("I got args! Look: {0}, {1}".format(arg1, arg2))
        function_to_decorate(arg1, arg2)
    return a_wrapper_accepting_arguments

# 调用装饰器返回的函数, 其实就是在调用包装函数, 将参数传递给包装函数,包装函数再传递给被装饰的函数。

@a_decorator_passing_arguments
def print_full_name(first_name, last_name):
    print("My name is {0} {1}".format(first_name, last_name))

print_full_name("Peter", "Venkman")
# 输出:
# I got args! Look: Peter Venkman
# My name is Peter Venkman

装饰对象方法

在Python中方法和函数其实是一样的,唯一的区别在于对象方法期待第一个参数是对当前对象(self)的引用。

这就意味着,你也能用相同的方法给给方法添加装饰器,仅需要将self考虑在内。

def method_friendly_decorator(method_to_decorate):
    def wrapper(self, lie):
        lie = lie - 3 # very friendly, decrease age even more :-)
        return method_to_decorate(self, lie)
    return wrapper


class Lucy(object):

    def __init__(self):
        self.age = 32

    @method_friendly_decorator
    def sayYourAge(self, lie):
        print("I am {0}, what did you think?".format(self.age + lie))

l = Lucy()
l.sayYourAge(-3)
# 输出: I am 26, what did you think?

如果你想创建通用的装饰器,可以应用于任何函数或者方法,不管它的参数是什么,需要使用*args, **kwargs

def a_decorator_passing_arbitrary_arguments(function_to_decorate):
    # 包装函数接受任何参数
    def a_wrapper_accepting_arbitrary_arguments(*args, **kwargs):
        print("Do I have args?:")
        print(args)
        print(kwargs)
        # 不熟悉 *args 和 **kwargs,pack,unpack
        # 请查看 http://www.saltycrane.com/blog/2008/01/how-to-use-args-and-kwargs-in-python/
        function_to_decorate(*args, **kwargs)
    return a_wrapper_accepting_arbitrary_arguments

@a_decorator_passing_arbitrary_arguments
def function_with_no_argument():
    print("Python is cool, no argument here.")

function_with_no_argument()
# 输出:
# Do I have args?:
# ()
# {}
# Python is cool, no argument here.

@a_decorator_passing_arbitrary_arguments
def function_with_arguments(a, b, c):
    print(a, b, c)

function_with_arguments(1,2,3)
# 输出:
# Do I have args?:
# (1, 2, 3)
# {}
# 1 2 3

@a_decorator_passing_arbitrary_arguments
def function_with_named_arguments(a, b, c, platypus="Why not ?"):
    print("Do {0}, {1} and {2} like platypus? {3}".format(a, b, c, platypus))

function_with_named_arguments("Bill", "Linus", "Steve", platypus="Indeed!")
# 输出:
# Do I have args ? :
# ('Bill', 'Linus', 'Steve')
# {'platypus': 'Indeed!'}
# Do Bill, Linus and Steve like platypus? Indeed!

class Mary(object):

    def __init__(self):
        self.age = 31

    @a_decorator_passing_arbitrary_arguments
    def sayYourAge(self, lie=-3): # You can now add a default value
        print("I am {0}, what did you think?".format(self.age + lie))

m = Mary()
m.sayYourAge()
# outputs
# Do I have args?:
# (<__main__.Mary object at 0xb7d303ac>,)
# {}
# I am 28, what did you think?

给装饰器传参数

如何给装饰器本身传参数呢?

这可能有点别扭,因为一个装饰器必须接受一个函数作为参数。因此,你不能够将传递给被装饰函数的参数直接传给装饰器。

解决这个问题之前,我们看个小示例。

# 装饰器是普通的函数
def my_decorator(func):
    print("I am an ordinary function")
    def wrapper():
        print("I am function returned by the decorator")
        func()
    return wrapper

# 因此,你可以不需要“@”来调用它

def lazy_function():
    print("zzzzzzzz")

decorated_function = my_decorator(lazy_function)
# 输出: I am an ordinary function

# 输出: I am an ordinary function,仅仅是因为你调用了它,没啥特别的。

@my_decorator
def lazy_function():
    print("zzzzzzzz")

# 输出: I am an ordinary function

2种用法效果一致,“my_decorator”都被调用了。也就是说,当你@my_decorator,你是在告诉Python,调用@标志的这个函数。

这很重要,你给的这个标签能够直接指向装饰器。

更进一步:

def decorator_maker():

    print("由我来创建装饰器,我只会在调用我来创建装饰器时调用一次。")

    def my_decorator(func):

        print("我是一个装饰器,每次装饰函数时,我都会调用一次。")

        def wrapped():
            print("我是包装函数,当调用被装饰函数时,我也被调用了,作为包装函数,我返回被包装函数对象。")
            return func()

        print("作为一个装饰器,我返回包装函数。")

        return wrapped

    print("作为装饰器制造者,我返回一个装饰器。")
    return my_decorator

# 创建一个装饰器
new_decorator = decorator_maker()
# 输出
# 由我来创建装饰器,我只会在调用我来创建装饰器时调用一次。
# 作为装饰器制造者,我返回一个装饰器。

# 接下来装饰一个函数
def decorated_function():
    print("我是被装饰的函数")

decorated_function = new_decorator(decorated_function)
# 输出:
# 我是一个装饰器,每次装饰函数时,我都会调用一次。
# 作为一个装饰器,我返回包装函数。

# 调用被装饰函数:
decorated_function()
# 输出:
# 我是包装函数,当调用被装饰函数时,我也被调用了,作为包装函数,我返回被包装函数对象。
# 我是被装饰的函数

跳过中间变量,完成相同的事情。

def decorated_function():
    print("我是被装饰的函数")

decorated_function = decorator_maker()(decorated_function)
# 输出:
# 由我来创建装饰器,我只会在调用我来创建装饰器时调用一次。
# 作为装饰器制造者,我返回一个装饰器。
# 我是一个装饰器,每次装饰函数时,我都会调用一次。
# 作为一个装饰器,我返回包装函数。

# 最终:
decorated_function()
# 输出:
# 我是包装函数,当调用被装饰函数时,我也被调用了,作为包装函数,我返回被包装函数对象。
# 我是被装饰的函数

更简洁一点

@decorator_maker()
def decorated_function():
    print("I am the decorated function.")
# 输出:
# 由我来创建装饰器,我只会在调用我来创建装饰器时调用一次。
# 作为装饰器制造者,我返回一个装饰器。
# 我是一个装饰器,每次装饰函数时,我都会调用一次。
# 作为一个装饰器,我返回包装函数。

# 最终:
decorated_function()
# 输出:
# 我是包装函数,当调用被装饰函数时,我也被调用了,作为包装函数,我返回被包装函数对象。
# 我是被装饰的函数

看到了吗?我们使用“@”语法时,进行了函数调用。

回到如果给装饰器传参,如果我们使用函数去生成装饰器,我们可以给这个函数传参,不是吗?

def decorator_maker_with_arguments(decorator_arg1, decorator_arg2):

    print("我创建了装饰器,我接收参数: {0}, {1}".format(decorator_arg1, decorator_arg2))

    def my_decorator(func):
        # 了解闭包
        # 参考https://stackoverflow.com/questions/13857/can-you-explain-closures-as-they-relate-to-python
        print("我是装饰器,给我传了参数: {0}, {1}".format(decorator_arg1, decorator_arg2))

        # Don't confuse decorator arguments and function arguments!
        def wrapped(function_arg1, function_arg2) :
            print("我是包装函数,我能访问所有的变量\n"
                  "\t- 来自装饰器的: {0} {1}\n"
                  "\t- 来自函数调用的: {2} {3}\n"
                  "我能把它们装递给被装饰函数"
                  .format(decorator_arg1, decorator_arg2,
                        function_arg1, function_arg2))
            return func(function_arg1, function_arg2)

        return wrapped

    return my_decorator

@decorator_maker_with_arguments("Leonard", "Sheldon")
def decorated_function_with_arguments(function_arg1, function_arg2):
    print("我是被包装函数,仅仅知道自己的参数: {0}"
           " {1}".format(function_arg1, function_arg2))

decorated_function_with_arguments("Rajesh", "Howard")
# 输出:
# 我创建了装饰器,我接收参数:: Leonard, Sheldon
# 我是装饰器,给我传了参数: Leonard, Sheldon
# 我是包装函数,我能访问所有的变量
#    - 来自装饰器的: Leonard Sheldon
#    - 来自函数调用的: Rajesh Howard
# 我能把它们装递给被装饰函数
# 我是被包装函数,仅仅知道自己的参数: Rajesh Howard

这就是包含参数的装饰器,参数也能被设置为变量。

c1 = "Penny"
c2 = "Leslie"

@decorator_maker_with_arguments("Leonard", c1)
def decorated_function_with_arguments(function_arg1, function_arg2):
    print("我是被包装函数,仅仅知道自己的参数:"
           " {0} {1}".format(function_arg1, function_arg2))

decorated_function_with_arguments(c2, "Howard")

如你所见,用这种方式你能传递参数给装饰器。你甚至能使用*args**kwargs,但是记住,装饰器只被 调用一次,仅仅当Python导入这个脚本的时候。之后你不能动态地设置参数。当你"import x"时,这个函数已经被装饰了,因此不能变了。

装饰一个装饰器

  • 无参装饰器
def parent_decorator(child_decorator):
    """这个函数将被当作一个装饰器,用来装饰另一个装饰器: child_decorator。"""

    # 创建一个装饰器(装饰器的参数是一个函数)。
    def wrapper(func):
        print('我是父装饰器')
        # 返回传入装饰器的运行结果(装饰器的返回值是一个函数)
        return child_decorator(func)

    return wrapper

@parent_decorator
def child_decorator(func):
    def wrapper(args):
        print('我是子装饰器')
        func(args)

    return wrapper

@child_decorator
def print_hello(name):
    print('我是', name)


print_hello('极客兔兔 https://geektutu.com')

# 输出:
# 我是父装饰器
# 我是子装饰器
# 我是 极客兔兔 https://geektutu.com
  • 接受任意参数的装饰器

接下里将给出一些代码片段,允许传入任何参数来创建装饰器。为了接受函数,我们使用了另一个函数来创建装饰器。

如果我们使用另一个函数来包装这个装饰器,那么这个包装函数就成为了装饰了这个装饰器的新的装饰器。

接下来,举个例子来演示如何装饰一个装饰器。

def decorator_with_args(decorator_to_enhance):
    """这个函数将被当作一个装饰器,用来装饰另一个装饰器。允许接受任意的参数。"""

    # 使用和上面例子相同的方法来传递参数,创建装饰器
    def decorator_maker(*args, **kwargs):

        # 创建一个装饰器(装饰器的参数是一个函数),但是保留了maker传入的参数。
        def decorator_wrapper(func):

            # 返回传入装饰器的运行结果(装饰器的返回值是一个函数)
            # 传入的装饰器的声明必须是 decorator(func, *args, **kwargs)
            return decorator_to_enhance(func, *args, **kwargs)

        return decorator_wrapper

    return decorator_maker

这样使用


# 创建一个装饰器,并用 @decorator_with_args 装饰它
# 装饰器的声明是 "decorator(func, *args, **kwargs)",
# 需与decorator_with_args中保持一致。
@decorator_with_args
def decorated_decorator(func, *args, **kwargs):
    def wrapper(function_arg1, function_arg2):
        print("Decorated with {0} {1}".format(args, kwargs))
        return func(function_arg1, function_arg2)
    return wrapper

# 装饰目标函数
@decorated_decorator(42, 404, 1024)
def decorated_function(function_arg1, function_arg2):
    print("Hello {0} {1}".format(function_arg1, function_arg2))

decorated_function("Universe and", "everything")
# 输出:
# Decorated with (42, 404, 1024) {}
# Hello Universe and everything

5)装饰器的最佳实践

装饰器在Python2.4中被引入,装饰器会让函数调用变慢,你需要知道这一点。

你不能够取消对一个函数的装饰(有一些方法可以创建能被移除的装饰器,但是没人使用),因此,一旦一个函数被装饰了,那么所有使用这个函数的地方都被装饰了。

装饰器包装函数,可能会让调试变得困难。(对于Python >= 2.5的版本,会容易一些,原因如下)。

functools这个模块,在Python 2.5中引入,包含了functools.wraps()函数,这个函数将会把被装饰函数的名称、模块和docstring复制到它的装饰器中。(事实上,functools.wraps()也是一个装饰器,☺)

# 为了调试,堆栈将打印出函数名。
def foo():
    print("foo")

print(foo.__name__)

# 输出: foo

# 用了装饰后,变得复杂了
def bar(func):
    def wrapper():
        print("bar")
        return func()
    return wrapper

@bar
def foo():
    print("foo")

print(foo.__name__)
# 输出: wrapper

# "functools" 能解决这个问题

import functools

def bar(func):
    # "wrapper"包装了"func"
    # 神奇的事情在这~
    @functools.wraps(func)
    def wrapper():
        print("bar")
        return func()
    return wrapper

@bar
def foo():
    print("foo")

print(foo.__name__)
# 输出: foo

6)装饰器用来做什么?

现在最大的问题,装饰器用来做什么?

看起来很酷,很强大,但是实用才是王道。当然,这有无数种可能性。经典的用法是用来扩展不能修改的外部库导入的函数的行为,或者为了调试。

你可以扩展好几个函数的行为,像这样:

def benchmark(func):
    """
    打印花费多少时间的装饰器
    """
    import time
    def wrapper(*args, **kwargs):
        t = time.clock()
        res = func(*args, **kwargs)
        print("{0} {1}".format(func.__name__, time.clock()-t))
        return res
    return wrapper


def logging(func):
    """
    打印脚本活动的装饰器
    """
    def wrapper(*args, **kwargs):
        res = func(*args, **kwargs)
        print("{0} {1} {2}".format(func.__name__, args, kwargs))
        return res
    return wrapper


def counter(func):
    """
    打印函数被调用次数的装饰器
    """
    def wrapper(*args, **kwargs):
        wrapper.count = wrapper.count + 1
        res = func(*args, **kwargs)
        print("{0} has been used: {1}x".format(func.__name__, wrapper.count))
        return res
    wrapper.count = 0
    return wrapper

@counter
@benchmark
@logging
def reverse_string(string):
    return str(reversed(string))

print(reverse_string("Able was I ere I saw Elba"))
print(reverse_string("A man, a plan, a canoe, pasta, heros, rajahs, a coloratura, maps, snipe, percale, macaroni, a gag, a banana bag, a tan, a tag, a banana bag again (or a camel), a crepe, pins, Spam, a rut, a Rolo, cash, a jar, sore hats, a peon, a canal: Panama!"))

# 输出:
# reverse_string ('Able was I ere I saw Elba',) {}
# wrapper 0.0
# wrapper has been used: 1x
# ablE was I ere I saw elbA
# reverse_string ('A man, a plan, a canoe, pasta, heros, rajahs, a coloratura, maps, snipe, percale, macaroni, a gag, a banana bag, a tan, a tag, a banana bag again (or a camel), a crepe, pins, Spam, a rut, a Rolo, cash, a jar, sore hats, a peon, a canal: Panama!',) {}
# wrapper 0.0
# wrapper has been used: 2x
# !amanaP :lanac a ,noep a ,stah eros ,raj a ,hsac ,oloR a ,tur a ,mapS ,snip ,eperc a ,)lemac a ro( niaga gab ananab a ,gat a ,nat a ,gab ananab a ,gag a ,inoracam ,elacrep ,epins ,spam ,arutaroloc a ,shajar ,soreh ,atsap ,eonac a ,nalp a ,nam A

当前,装饰器的好处还在于如果能够正确使用,可避免重复代码。

@counter
@benchmark
@logging
def get_random_futurama_quote():
    from urllib import urlopen
    result = urlopen("http://subfusion.net/cgi-bin/quote.pl?quote=futurama").read()
    try:
        value = result.split("<br><b><hr><br>")[1].split("<br><br><hr>")[0]
        return value.strip()
    except:
        return "No, I'm ... doesn't!"


print(get_random_futurama_quote())
print(get_random_futurama_quote())

# outputs:
# get_random_futurama_quote () {}
# wrapper 0.02
# wrapper has been used: 1x
# The laws of science be a harsh mistress.
# get_random_futurama_quote () {}
# wrapper 0.01
# wrapper has been used: 2x
# Curse you, merciful Poseidon!

Python 内置了很多装饰器: property, staticmethod,等。

Django使用装饰器来管理缓存,视图权限。

集成内部异步调用。

装饰器的使用远不止这些。




原文地址:访问原文地址
快照地址: 访问文章快照