欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

装饰器

程序员文章站 2022-03-16 11:06:32
...

第7章 函数装饰器与闭包

函数装饰器用于在源码中“标记”函数,以某种方式增强函数的行为。

基础知识

装饰器的一大特性是, 能把被装饰的函数替换成其他函数。 第二个特性是, 装饰器
在加载模块时立即执行

例如:

def deco(func):
    def inner():
        print('running inner()')

    return inner

# 等价于deco(target)
@deco
def target():
    print('running target()')


if __name__ == '__main__':
    print(target)
    target()

输出:

<function deco.<locals>.inner at 0x00000179D67E4708>
running inner()

分析:

​ 函数target被装饰函数deco替换成<function deco.<locals>.inner

装饰器的一个关键特性是, 它们在被装饰的函数定义之后立即运行。 这通常是在导入时
(即 Python 加载模块时)

例如:该文件文registration.py

registry = []


def register(func):
    print('running register(%s)' % func)
    registry.append(func)
    return func


@register
def f1():
    print('running f1()')


@register
def f2():
    print('running f2()')


def f3():
    print('running f3()')


def main():
    print('running main()')
    print('registry ->', registry)
    f1()
    f2()
    f3()


if __name__ == '__main__':
    main()

当该模块被导入时:import registration,会运行装饰器函数register。 很多 Python Web 框架使用这样的装饰器把函数添加到某种*注册处, 例如把URL模式映射到生成 HTTP 响应的函数上的注册处

闭包

闭包指延伸了作用域的函数, 其中包含函数定义体中引用、 但是不在定义体中定义

的非全局变量。 函数是不是匿名的没有关系, 关键是它能访问定义体之外定义的非全局变

例如:

class Averager():
    def __init__(self):
        self.series = []

    def __call__(self, new_value):
        self.series.append(new_value)
        total = sum(self.series)
        return total / len(self.series)


def make_averager():
    series = []

    def averager(new_value):
        series.append(new_value)
        total = sum(series)
        return total / len(series)

    return averager


if __name__ == '__main__':
    avg = Averager()
    print(avg(10))
    print(avg(11))
    print(avg(12))
    avg1 = make_averager()
    print(avg1)
    print(avg1(10))
    print(avg1(11))
    print(avg1(12))
    print(avg1.__code__.co_varnames)
    print(avg1.__code__.co_freevars)
    print(avg1.__closure__)
    print(avg1.__closure__[0].cell_contents)

输出:

10.0
10.5
11.0
<function make_averager.<locals>.averager at 0x000001B72FB2E708>
10.0
10.5
11.0
('new_value', 'total')
('series',)
(<cell at 0x000001B72FCB6D38: list object at 0x000001B72DE35448>,)
[10, 11, 12]

Process finished with exit code 0

分析:

​ avg是个可调用对象,每次调用时将参数new_value存放到self.series,根据输出结果avg1是个函数对象

<function make_averager.<locals>.averager, 注意, series 是 make_averager 函数的局部变量, 因为那个函数的定义体中初始化了series: series = []。 可是, 调用 avg1(10) 时, make_averager 函数已经返回了,
而它的本地作用域也一去不复返了,它从哪里获取 series,series不是在 函数averager作用域外吗?

avg1.__code__.co_varnames保存了变量名,而avg1.__code__.co_freevars保存了*变量,即make_averagerseries,如下图所示:

装饰器

series的绑定在返回的 avg1 函数的 closure 属性中。 avg1.__closure__ 中的各个元素对应于 avg1.__code__.co_freevars 中的一个名称。 这些元素是 cell 对象, 有个cell_contents属性, 保存着真正的值 。

nonlocal声明

Python 3 引入了 nonlocal 声明。 它的作用是把变量标记为*变量, 即使在函数中为变量赋予新值了, 也会变成*变量。 如果为 nonlocal 声明的变量赋予新值, 闭包中保存的绑定会更新。

例子:

def make_averager():
    count = 0
    total = 0

    def averager(new_value):
        nonlocal count, total
        count += 1
        total += new_value
        return total / count

    return averager


if __name__ == '__main__':
    avg = make_averager()
    print(avg(10))
    print(avg(11))
    print(avg(12))
    print(avg.__code__.co_varnames)
    print(avg.__code__.co_freevars)
    c = avg.__closure__
    print(avg.__closure__[0].cell_contents)
    pass

分析:

当 count 是数字或任何不可变类型时, count += 1 语句的作用其实与 count= count + 1 一样。 因此, 我们在 averager 的定义体中为 count 赋值了, 这会把count 变成局部变量。 total 变量也受这个问题影响。
但是对数字、 字符串、 元组等不可变类型来说, 只能读取, 不能更新。 如果尝试重新绑定, 例如 count = count + 1, 其实会隐式创建局部变量 count。 这样, count 就不是*变量了, 因此不会保存在闭包中。

实现一个简单的装饰器

import time
import functools

import requests


# 一个计算函数运行时间的装饰器
def clock(func):
    @functools.wraps(func)
    def clocked(url, *args, **kwargs):
        t0 = time.time()
        result = func(url, *args, **kwargs)
        elapsed = time.time() - t0
        name = func.__name__
        arg_lst = []
        if args:
            arg_lst.append(', '.join(repr(arg) for arg in args))
        if kwargs:
            pairs = ['%s=%r' % (k, w) for k, w in sorted(kwargs.items())]
            arg_lst.append(', '.join(pairs))
            arg_str = ', '.join(arg_lst)
            print('[%0.8fs] %s(%s)' % (elapsed, name, arg_str))
        return result

    return clocked


@clock
def download(url, *args, **kwargs):
    return requests.get(url).content


if __name__ == '__main__':
    download("https://home.firefoxchina.cn/", "arg1", "arg2", key1="value1")

使用functools.wraps 装饰器把相关的属性如__name____doc__func 复制到 clocked 中 ,而download

的参数会被复制到clocked中。

综合应用

下面是django一个编写视图函数的用法,通过叠放装饰器、参数化装饰器来让限定视图函数post_new必须满足要登录、

ajax请求且是POST请求

def login_required(function=None, redirect_field_name=REDIRECT_FIELD_NAME, login_url=None):
    """
    Decorator for views that checks that the user is logged in, redirecting
    to the log-in page if necessary.
    """
    actual_decorator = user_passes_test(
        lambda u: u.is_authenticated,
        login_url=login_url,
        redirect_field_name=redirect_field_name
    )
    if function:
        return actual_decorator(function)
    return actual_decorator

def ajax_required(func):
    """验证是否为AJAX请求"""
    @wraps(func)
    def wrap(request, *args, **kwargs):
        # request.is_ajax() 判断是否为ajax请求
        if not request.is_ajax():
            return HttpResponseBadRequest
        return func(request, *args, **kwargs)
    return wrap

def require_http_methods(request_method_list):
    """
    Decorator to make a view only accept particular request methods.  Usage::

        @require_http_methods(["GET", "POST"])
        def my_view(request):
            # I can assume now that only GET or POST requests make it this far
            # ...

    Note that request methods should be in uppercase.
    """
    def decorator(func):
        @wraps(func)
        def inner(request, *args, **kwargs):
            if request.method not in request_method_list:
                response = HttpResponseNotAllowed(request_method_list)
                log_response(
                    'Method Not Allowed (%s): %s', request.method, request.path,
                    response=response,
                    request=request,
                )
                return response
            return func(request, *args, **kwargs)
        return inner
    return decorator

@login_required
@ajax_required
@require_http_methods(["POST"])
def post_new(request):
    """发送动态, AJAX POST请求"""
    pass