PythonCookbook第八章(元编程)

元编程的主要目标是创建函数和类,并用它们来操纵代码(比如修改、生成或者包装已有的代码)。Python中基于这个目的的主要特性包括装饰器、类装饰器以及元类。

9.1 给函数添加一个包装

问题

我们想给函数添加一个包装以添加额外的处理。

解决方案

写一个简单的装饰器

import time
from functools import wraps


def timethis(func):
    '''
    :param func:  Decorator that reports the execution time
    :return: func
    '''
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(func.__name__, end - start)
        return result
    # 返回一个函数
    return wrapper

@timethis
def countdown(n):

    while n > 0:
        n -= 1

if __name__ == '__main__':
    print(countdown(10000))
    print(countdown(10000000))

 讨论:

装饰器是一个函数,它可以接收一个函数作为输入并返回一个新的函数作为输出。

@timethis
def countdown(n):

这个的意思就是countdown = timethis(countdown)

类里面的内置的@staticmethod, @classmethos, @property都是一样的逻辑

9.2编写装饰器时如何保存函数的元数据。

问题:

我们已经编写好一个装饰器,但是当将它用在一个函数上时,一些重要的元数据比如函数名、文档字符串、函数注释以及调用签名都丢失了。

解决方案:

functools.wraps

import time
from functools import wraps


def timethis(func):
    '''
    :param func:  Decorator that reports the execution time
    :return: func
    '''
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(func.__name__, end - start)
        return result
    # 返回一个函数
    return wrapper

@timethis
def countdown(n: int) -> int:
    '''this is countdown'''
    while n > 0:
        n -= 1
    return n

if __name__ == '__main__':
    print(countdown(10000))
    print(countdown(10000000))
    # 函数数据类型
    print(countdown.__annotations__)
    # 函数名字
    print(countdown.__name__)
    # 函数文档解释
    print(countdown.__doc__)
countdown 0.0006299018859863281
0
countdown 0.5314240455627441
0
{'n': <class 'int'>, 'return': <class 'int'>}
countdown
this is countdown

 讨论:

如果取消@wraps

函数的特性都没有了

countdown 0.0007231235504150391
0
countdown 0.5646867752075195
0
{}
wrapper
None
 # 取回原函数
    print(countdown.__wrapped__)
    from inspect import signature
    print(signature(countdown))
    print(signature(countdown.__wrapped__))

 可以通过被装饰函数的__wrapped__取回没有被装饰的函数

9.3 对装饰器进行解包装

问题:

取回没有包装过的函数

解决方案:

通过__wrapped__属性取回

讨论:

只要在装饰器利用了functools.wraps(func)对元数据进行了适当的拷贝,才能用__wrapped__属性取出。

多个装饰器的时候,看__wrapped__的效果。

from functools import wraps

def decorator1(func):
    @wraps(func)
    def wrapper(*args):
        print('Decorator1')
        return func(*args)
    return wrapper

def decorator2(func):
    @wraps(func)
    def wrapper(*args):
        print('Decorator2')
        return func(*args)

    return wrapper

@decorator1
@decorator2
def add(x, y):
    return x+y

if __name__ == '__main__':
    print(add(2,3))
    print('=' * 10 )
    # 取回的是decorator2函数
    print(add.__wrapped__(3,4))
    print('=' * 10)
    # 取回的是原来的函数
    print(add.__wrapped__.__wrapped__(3, 4))
/usr/local/bin/python3.7 /Users/shijianzhong/study/PythonCookbook/chapter_9/t3_3.py
Decorator1
Decorator2
5
==========
Decorator2
7
==========
7

Process finished with exit code 0

 Python3.7已经修改了这个漏洞,不会直接穿越到元素的函数了。

但请注意并不是所有的装饰器都使用了@wraps,因此有些装饰器的行为可能与我们预期的有所区别,特别是,由内建的装饰器@staitcmethod和@classmethod创建的描述符对象并不遵循这个约定(相反,它们会把原始函数保存在__func__属性中)。

9.4定义一个可接收参数的装饰器

问题:

编写一个可接收掺乎的装饰器

解决方案:

编写一个装饰器工厂,书中用了logging模块,编写装饰器工厂,刚好我也重新复习下logging模块

from functools import wraps
import logging
import time

def logged(level, name=None, message=None):

    def decorate(func):
        logname = name if name else func.__name__
        # 获取一个log输出对象流
        log = logging.getLogger(logname)
        logmsg = message if message else  func.__name__

        @wraps(func)
        def wrapper(*args, **kwargs):
            # 输出log信息
            # log.setLevel(logging.ERROR)
            # print(log.level)
            log.log(level, logmsg)
            return func(*args, **kwargs)
        return wrapper
    return decorate

@logged(logging.DEBUG)
def add(x, y):
    return x+y

@logged(logging.CRITICAL, 'example')
def spam():
    print('Spam!')

if __name__ == '__main__':
    print(add(1, 2))
    print('=' * 10)
    time.sleep(1)
    print(spam())
/usr/local/bin/python3.7 /Users/shijianzhong/study/PythonCookbook/chapter_9/t4_2.py
3
==========
spam
Spam!
None

Process finished with exit code 0

 通过装饰器工厂的主要作用就是可以传递参数给内部函数调用,这里传入的是logging的等级

讨论

@decorator(x, y, z)

def func(a, b):

  ...

其实底层运行的是

func = decorator(x,y,z)(func)

decorator(x,y,z)返回的必须是一个可调用对象。

9.5 定义一个属性可由用户修改的装饰器

问题

我们想编写一个装饰器来包装函数,但是可以让用户调整装饰器的属性,这样在运行时能够控制装饰器的行为

解决方案:

编写一个访问器函数,通过nonlocal关键字变量来修改装饰器内部的属性。之后把访问器函数作为函数属性附加到包装函数上。

我自己写的测试,根本不需要访问器函数,直接在包装函数外面定义包装函数的属性为函数。

from functools import wraps, partial
import logging
import time


logging.basicConfig(level=logging.DEBUG)

# 访问起函数,是一个简化版的装饰器工厂函数,使用了partial技巧
def attach_wrapper(obj, func=None):
    if func is None:
        return partial(attach_wrapper, obj)
    setattr(obj, func.__name__, func)
    return func

def logged(level, name=None, message=None):

    def decorate(func):
        # print(func.__wrapped__)
        # print(func.__name__)
        logname = name if name else func.__name__
        # 获取一个log输出对象流
        log = logging.getLogger(logname)
        logmsg = message if message else  func.__name__

        @wraps(func)
        def wrapper(*args, **kwargs):
            # 输出log信息
            # log.setLevel(logging.ERROR)
            # print(log.level)
            log.log(level, logmsg)
            return func(*args, **kwargs)

        # 装饰器工厂会直接装饰器工厂函数调用两次,第一次传wrapper参数,第二次调用传func参数
        # 通过这个装饰器工厂给函数赋值属性
        @attach_wrapper(wrapper)
        def set_level(newlevel):
            nonlocal level
            level = newlevel

        @attach_wrapper(wrapper)
        def set_message(newmsg):
            nonlocal logmsg
            logmsg = newmsg

        wrapper.get_level = lambda :level
        wrapper.name = 'sidian'

        return wrapper
    return decorate

@logged(logging.DEBUG)
def add(x, y):
    return x+y

@logged(logging.CRITICAL, 'example')
def spam():
    print('Spam!')

if __name__ == '__main__':
    print(add.set_message('Hello World'))
    print(add(1, 2))
    print('=' * 10)
    time.sleep(1)
    print(spam())
/usr/local/bin/python3.7 /Users/shijianzhong/study/PythonCookbook/chapter_9/t5_2.py
DEBUG:add:Hello World
None
3
==========
Spam!
None
CRITICAL:example:spam

 其实我自己在写的过程中发现,不写访问起函数,

wrapper.get_level = lambda :level
wrapper.name = 'sidian'

 定义内部函数,然后给内部函数添加属性为函数也可以,相对来说就是多一步手工添加,但可以避免写访问器函数。

讨论:

装饰器都使用了@functoos.wrap的话,内层函数可以括约多个装饰器层进行传播。

9.6定义一个能接收可选参数的装饰器

问题:

我们想编写一个单独的装饰器,使其即可以像@decorator这样不带参数使用,也可以像@decorator(x,y,z)使用装饰器工厂这么用。

解决方案:

定义装饰器函数的传参方式里面有*,通过functools.partail返回一个函数。

from functools import wraps
import logging
import functools
import time

def logged(func=None, *, level=logging.WARNING, name=None, message=None):
    # 如果没有传入func,就返回这个partial定义好的函数,第二次执行这个函数,并自动传参被装饰的函数
    if func is None:
        return functools.partial(logged, level=level, name=name, message=message)


    logname = name if name else func.__name__
    # 获取一个log输出对象流
    log = logging.getLogger(logname)
    logmsg = message if message else  func.__name__

    @wraps(func)
    def wrapper(*args, **kwargs):
        # 输出log信息
        # log.setLevel(logging.ERROR)
        # print(log.level)
        log.log(level, logmsg)
        return func(*args, **kwargs)
    return wrapper


@logged
def add(x, y):
    return x+y

@logged(level=logging.CRITICAL, name='example')
def spam():
    print('Spam!')

if __name__ == '__main__':
    print(add(1, 2))
    print('=' * 10)
    time.sleep(1)
    print(spam())

 讨论:

整个重点一点要了解到

@decorator(x, y, z)

def func(a, b):

  ...

其实底层运行的是

func = decorator(x,y,z)(func)           这个是重中之重

decorator(x,y,z)返回的必须是一个可调用对象。

猜你喜欢

转载自www.cnblogs.com/sidianok/p/12315501.html