Python3-Cookbook(第九章) - 元编程Part1

一、函数装饰器入门

    对应Cookbook章节 9.1、9.2、9.3 和9.8

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2023/8/4 18:44
# @Author  : Maple
# @File    : 01-函数装饰器入门.py

#  Cookbook 9.1、9.2、9.3 和9.8


import time
from functools import wraps

def timethis(func):
    '''
    Decorator that reports the execution time.
    '''
    @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):
    '''
    Counts down
    '''
    print('hello')
    while n > 0:
      n -= 1

# 多层装饰器
def decorator1(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print('Decorator 1')
        return func(*args, **kwargs)
    return wrapper

def decorator2(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print('Decorator 2')
        return func(*args, **kwargs)
    return wrapper

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

# @wraps装饰func的底层 到底发生了什么?
# Part1
"""
# 1.wrapped是被装饰的函数
# 2.WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__qualname__', '__doc__','__annotations__')
# 3.WRAPPER_UPDATES = ('__dict__',)
# 4.返回一个 update_wrapper 的偏函数

def wraps(wrapped,
          assigned = WRAPPER_ASSIGNMENTS,
          updated = WRAPPER_UPDATES):
    Decorator factory to apply update_wrapper() to a wrapper function
 
    return partial(update_wrapper, wrapped=wrapped,
                   assigned=assigned, updated=updated)

"""

# Part2
"""
# 1. wrapper是装饰器被装饰后 返回的函数  
# 2. updated =  ('__dict__',)

def update_wrapper(wrapper,
                   wrapped,
                   assigned = WRAPPER_ASSIGNMENTS,
                   updated = WRAPPER_UPDATES):
    Update a wrapper function to look like the wrapped function

    
    for attr in assigned:
        try:
            # 1. 取出`被装饰`函数的属性
            value = getattr(wrapped, attr)
        except AttributeError:
            pass
        else:
            # 2.将第一步取出来的属性值 赋值给 wrapper,也就是说wrapper的元数据实际上替换成[被装饰的函数]了
            #    以此实现了被装饰函数 元数据的保留
            setattr(wrapper, attr, value)
    for attr in updated:
        # 1.update方法实现 字典值的更新,注意是 后面更新前面的(逻辑: 交叉的key,用后面的值更新前面的; 只存在于后面的key,添加到前面的字典 )
        # 2.如果wrapped 有 ('__dict__',)属性[Tips个人理解:类才有dict属性,方法没有这个属性],那么就把wrapper的dict属性也替换成wrapped的
        # 3.如果wrapped 没有 dict属性, getattr去默认值{},而空字典 对wrapper.__dict__'的值 不会有任何影响
        getattr(wrapper, attr).update(getattr(wrapped, attr, {}))

    return wrapper

"""



if __name__ == '__main__':

    # 1. 简单装饰器测试
    countdown(1000000)
    print('===================')



    # 2.解除装饰器
    # 2-1 方法之一
    """
    1.适用场景: 在函数已经有装饰器的情况下,但是又要单独测试函数本身的场景
    2.必要条件1: 有@wraps修饰
    2.必要条件2:只有一个装饰器,多个装饰器的情况解除之后的测试结果不可预测
    """
    # 通过__wrapped__直接访问被包装的函数:即countdown自身(不会走装饰器里面的逻辑)
    countdown.__wrapped__(1000)

    # 2-2 方法之二

    print('===================')
    countdown = countdown.__wrapped__
    countdown(100000)

    # 2-3 多个装饰器的解除测试
    print('===================')
    add =  add.__wrapped__ 
    result = add(2,3)
    print('result = {}'.format(result)) # result = 5
    """
    output: 
       Decorator 2 ,也就是说内层的装饰器并未解除
       result = 5
    """

    print(add.__module__) # __main__
    print(add.__name__) # add

二、函数装饰器传参

  对应cookbook章节 9.4 和 9.6

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2023/8/4 19:08
# @Author  : Maple
# @File    : 02-函数装饰器传参.py
import logging
from functools import wraps, partial

"""
对应cookbook 
9.4 和 9.6

"""

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

    def decorater(func):
        logname = name if name else func.__module__
        log = logging.getLogger(logname)
        logmsg = message if message else func.__name__

        @wraps(func)
        def wrapper(*args,**kwargs):
            log.log(level,logmsg)
            # print('{}:{}'.format(logname,logmsg))
            return func(*args,**kwargs)

        return wrapper

    return decorater


# 正常调用1
@logged(logging.DEBUG)
def add(x,y):
    return x + y

# 正常调用2
@logged(logging.CRITICAL,'example')
def spam():
    print('Spam')



# 正常调用3
# @logged()
def sub(x,y):
    return x - y

# 非正常调用,会报错
@logged
def sub(x,y):
    return x - y

# 因为如上方式等价于
sub = logged(sub) # 即第一个参数是被装饰函数本身,但是logged实际需要是接收的是level,name和message三个参数
# sub()

# 如何改进?

def logged2(func = None,*, level=logging.DEBUG,name=None,message=None):
    if func is None:
        return partial(logged2,level = level,name = name,message = message)

    logname = name if name else func.__module__
    log = logging.getLogger(logname)
    logmsg = message if message else func.__name__


    @wraps(func)
    def wrapper(*args,**kwargs):
        log.log(level,logmsg)
        print('{}:{}'.format(logname,logmsg))
        return func(*args,**kwargs)

    return wrapper

"""
如下方式等价于:
sub2 = logged2(sub2) # 即第一个参数是被装饰函数本身
# 此时sub2其实已经变成wrapper
sub2()

由于第一个参数不为空,所以直接走logger2 if后面的逻辑 
"""
# 1. 没有任何参数传递(注意这里并没有使用(),和装饰器的`普通用法`保持一致)
@logged2
def sub2(x,y):
    return x - y



"""
执行流程
1. 由于logger2中func参数未传入,所以为None,会返回一个偏函数partial(logged2,level = level,name = name,message = message),假设是s.所以第一句执行完成后,变成
@s
def add2(x,y)

2. add2 = s(add2),此时func参数不为None,所以直接走if后面的逻辑。最终返回的add2其实已经是 wrapper 了
3. add2() --> 本质上是执行wrapper()
"""
# 2. 如果传递参数
@logged2(level=logging.CRITICAL,name='add2',message='add2被执行了')
def add2(x,y):
    return x + y



if __name__ == '__main__':


    print('======logger 正常有参测试==========')
    result = add(1,2) #3
    print(result) #Spam

    spam()

    # result2 = sub(3,10)
    # print(result2)

    print('=====logger2 sub2无参测试===========')

    result2 = sub2(10,2)
    # __main__:sub2
    print('result2 = {}'.format(result2)) # result2 = 8

    print('=====logger2 add2有参测试===========')

    result3 = add2(10, 2)
    #add2:add2被执行了
    print('result3 = {}'.format(result3)) # result3 = 12

三、可自定义属性的装饰器

对应cookbook章节 9.5

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2023/8/16 21:17
# @Author  : Maple
# @File    : 05-可自定义属性的装饰器.py
import logging
from functools import partial, wraps


"""
cookbook 9.5
"""
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 ,msg = None):

    def decorate(func):
        log_name = name if name else func.__module__
        log = logging.getLogger(log_name)
        log_msg = msg if msg else func.__name__

        @wraps(func)
        def wrapper(*args,**kwargs):
            log.log(level, log_msg)
            print('level:',level,'log_msg',log_msg)
            func(*args,**kwargs)

        # 通过装饰器,给wrapper设置一个属性,属性名字就是被装饰函数:即set_level,然后属性值是 该函数的引用
        # 所以调用wrapper的set_level属性时,本质上就是在调用该方法。然后该方法会修改 level的值
        @attach_wrapper(wrapper)
        def set_level(new_level):
            # 申明该变量不是函数set_level中的局部变量,而是函数之外 logged函数中的参数
            nonlocal level
            level = new_level

        @attach_wrapper(wrapper)
        def set_message(new_msg):
            # 注意这里不要去改msg的值,而是要去修改log_msg
            # 1. 因为方法调用set_message的时候,首先跳到这里修改局部变量的值
            # 2. 然后跳到 wrapper方法,所以 如果修改msg的值, 由于[log_msg = msg if msg else func.__name__]这段代码(在wrapper上面)根本不会执行,所以msg的值也就无法改变
            nonlocal log_msg
            log_msg = new_msg

        return wrapper

    return decorate


@logged(level=logging.DEBUG,name = '罗二',msg="你也有今天")
def add(a,b):
    return a + b

if __name__ == '__main__':


    result = add(1,2)
    print('----------------')

    # 执行顺序
    ## 1. add此时是 wrapper
    ## 2. 而 set_level是wrapper中的一个实例属性,且对用的值是fun set_level
    ## 3. 因此以下语句会执行set_level逻辑,将level 修改为logging.INFO)

    add.set_level(logging.INFO)
    ## 4. 执行 wrapper中的逻辑:log.log(level, log_msg).....,注意此时level已经被修改成logging.INFO
    result = add(1, 2)

    print('----------------')
    add.set_message('好汉不提当年勇')
    result = add(1, 2)

四、强制参数类型检测

对应cookbook章节 9.7
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2023/8/5 0:53
# @Author  : Maple
# @File    : 03-强制参数类型检测.py

from inspect import signature
from functools import wraps

"""
对应cookbook 9.7
"""

def typeassert(*ty_args,**ty_kwargs):

    def decorate(func):
        # 如果是优化执行模式,不进行参数类型检查
        if not __debug__:
            return func

        sig = signature(func)

        # 自定义的参数类型限制条件,Sample:OrderedDict([('x', <class 'int'>), ('z', <class 'int'>)])
        bound_types = sig.bind_partial(*ty_args,**ty_kwargs).arguments
        print('bound_types',bound_types) # OrderedDict([('x', <class 'int'>), ('z', <class 'int'>)])

        @wraps(func)
        def wrapper(*args,**kwargs):
            # 被装饰函数所有实参的sig 字典,bound_values.arguments:OrderedDict([('x', 1), ('y', 2), ('z', 3)])
            bound_values = sig.bind(*args,**kwargs) # <BoundArguments (a=1, b=2, c=1)>

            for name,value in  bound_values.arguments.items():
                if name in bound_types:
                    if not isinstance(value,bound_types[name]):
                        raise TypeError('参数{} Expect type {}'.format(name, bound_types[name]))

            return func(*args,**kwargs)

        return wrapper

    return decorate

@typeassert(int,c=str)
def add(a,b,c='maple'):
    print(a,b,c)


if __name__ == '__main__':

    sig = signature(add)

    bound_values = sig.bind(1,2,c = 1)
    print(bound_values.arguments)


    add(1,2,'Maple')
    try:
        add('I', 2, 'Maple') # TypeError: 参数a Expect type <class 'int'>

    except TypeError as e:
        print(e)

    try:
        add(1, 3, 3)
    except TypeError as e:
        print(e) # 参数c Expect type <class 'str'>

五、类装饰器

对应cookbook章节:9.8 和 9.9
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2023/8/5 1:55
# @Author  : Maple
# @File    : 04-装饰器类.py

"""
对应cookbook章节:9.8 和 9.9


"""


# 1. 将装饰器定义为类的一部分
import types
from functools import wraps


class A:

    def decorator1(self,func):
        @wraps(func)
        def wrapper(*args,**kwargs): # 内部的wrapper方法通常不需要传入self 或者 cls
            print('decorator1')
            return func(*args,**kwargs)

        return wrapper

    @classmethod
    def decorator2(cls, func):
        def wrapper(*args, **kwargs):
            print('decorator2')
            return func(*args, *kwargs)

        return wrapper

a = A()

@a.decorator1
def add(a,b):
    return a + b

@A.decorator2
def sub(a,b):
    return a - b


"""
@property 的getter(), setter(), deleter(),其实就是在类中定义的三个装饰器
"""

class Person:

    def __init__(self,name):
        self._first_name = name

    # 创建一个类的实例
    first_name = property()

    # 通过类的实例中的getter方法,实现装饰器的功能
    @first_name.getter
    def first_name(self):
        return self._first_name

    @first_name.setter
    def first_name(self,value):
        if not isinstance(value,str):
            raise TypeError('Expect str type')


# 2. 将类定义为装饰器
# !!!get 方法:还是没有彻底理解
class Profiled:
    def __init__(self,func):
        wraps(func)(self)
        self.ncalls = 0

    def __call__(self, *args, **kwargs):
        self.ncalls += 1
        print('装饰器类被调用了{}次'.format(self.ncalls))
        return self.__wrapped__(*args,**kwargs)

    def __get__(self, instance, cls):
        if instance is None:
            return self
        else:
            # 该方法是为实例/类动态增加方法,第一个参数应该是方法,但这里是Profiled类的一个实例, 不懂~~
            ## 返回结果是
            print('get 方法被调用')
            print('self',self)
            print('instance:',instance)
            print('cls:',cls)
            return types.MethodType(self,instance)

@Profiled
def mul(a,b):
    return a * b


class Spam:
    @Profiled
    def div(self,a,b):
        return a / b

class Spam2:
    def div(self,a,b):
        return a / b


if __name__ == '__main__':

    result1 = add(1,2)
    print(result1)


    print('=================')
    result2 = sub(10,2)
    print(result2)

    print('=================')
    p = Person('Maple')
    print(p.first_name)
    # p.first_name = 12

    print('======将类定义为装饰器测试===========')
    mul(1,2) # 装饰器类被调用了1次
    mul(1,2) # 装饰器类被调用了2次
    print(mul.ncalls) # 2

    print('---------------------------')
    """
    执行步骤:
     1. 实例化Profiled,Profiled 装饰Spam,然后才实例化Spam,得到s
     2. 调用s.div(10,2),进到Profiled类的get方法,返回一个Spam的bound方法
     3. 调用Profiled的call 方法,执行self.__wrapped__(*args,**kwargs),其实self.__wrapped__为Spam的bound方法(Spam.div,就是被装饰的函数)
        ,执行spam.div()方法,得到最终结果
    
    """
    s = Spam()
    print('s',s)
    s.div(10,2) # 装饰器类被调用了1次
    s.div(15, 5) # 装饰器类被调用了1次
    print(s.div.ncalls)  # 2

    # 装饰器的用法等价于:
    print('======装饰器的用法等价测试===========')
    s2 = Spam2()
    s2  = Profiled(s2.div)
    print(s2(15,3))
  • 补充说明s.div(10,2)的详细执行过程

(1)前置知识-绑定方法

    先来看一个例子:

import types

class MyClass:
    def __init__(self, value):
        self.value = value

    def show_value(self, extra):
        print(f'Value: {self.value}, Extra: {extra}')

# 创建 MyClass 的实例
instance = MyClass(10)

# 创建一个普通的函数
def external_function(self, extra):
    print(f'External Function - Value: {self.value}, Extra: {extra}')

# 使用 types.MethodType 将 external_function 绑定到 instance 上
bound_method = types.MethodType(external_function, instance)

# 调用绑定方法
bound_method(5)

上例中bound_method就是一个绑定方法,当执行bound_method(5)时,instance会作为第一个参数传递到external_function中: 即self

(2)s.div(10,2)执行流程分析

   2.1 s.div执行流程

      ①首先,因为div被@Profiled装饰,所以div其实是profiled的一个实例

      ②同时,因为Profiled类实现了__get__方法,所以本质上是一个描述器

    因此当执行s.div时,会去执行Profiled类中的__get__方法,而该方法,会返回一个绑定方法:types.MethodType(self,instance)。 本质上就是将profiled实例绑定到Spam的实例s上。

假设该绑定方法就是bound_method

   2.2 s.div(2,10)执行流程--> bound_method(2,10)

① 上述绑定的对象时profiled实例,因此当执行绑定方法时,会执行profiled的__call__方法

需要结合以下2点理解:1.执行绑定方法时,实际就是执行被绑定对象();2.Profield对象实现了call方法,调用对象(),其实就会调用该对象的call方法

② __call__方法中的self是profiled实例对象本身-这是又类中定义的方法决定的,然后s作为一个参数会传递给args,同时2和10作为后续的参数,也会传递给args

   2.3 执行s.div本身的逻辑,比较好理解,不再赘述

   简单补充说明:s.div中的3个参数: self-s实例,10,2 即来自上一步的args参数

  • 6
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值