Python装饰器的理解和应用


装饰器的作用
  • 打印日志: @log
  • 检测性能:@performance
  • 数据库事务: @transaction
  • URL路由:@post(’/register’)

装饰器的目的:

  • 使代码可读性更高,感觉高大上;
  • 代码结构更加清晰,代码冗余度更低;
无参数decorator

Python的 decorator 本质上就是一个高阶函数,它接收一个函数作为参数,然后,返回一个新函数。

使用 decorator 用Python提供的 @ 语法,这样可以避免手动编写f = decorate(f) 这样的代码。

考察一个@log的定义:

def testlog(func):
    def inner(arg):
        print("print log:" + func.__name__)
        func(arg)
    return inner

# 不使用@
def test(x):
    print("this is test function:",x)

test = testlog(test)
test("11")

# 使用@
@testlog
def test(x):
    print("this is test function:",x)

test("11")

# print log:test
# this is test function:11

例子:阶乘

from functools import reduce

def testlog(func):
    def inner(*args,**kwargs):
      # 要让 @testlog 自适应任何参数定义的函数,可以利用Python的 *args 和 **kw,保证任意个数的参数总是能正常调用
        print("print log:" + func.__name__)
        func(*args,**kwargs)
    return inner

@testlog
def factorial(x, y):
    print("this is factorial function:")
    res = reduce(lambda x, y: x * y, range(x, y))
    print(res)
    return res

factorial(1, 11)
# print log:factorial
# this is factorial function:
# 3628800

例子:写一个@performance,作为一个函数计时器。

import time

def performance(func):
    def inner(*args, **kwargs):
        time1 = time.time()
        res = func(*args, **kwargs)
        time2 = time.time()
        print(f"call %s%s%s in %fs" % (func.__name__, args,kwargs, time2 - time1))
        return res
    return inner

@performance
def factorial(x, y):
    return reduce(lambda x, y: x * y, range(x, y))

print(factorial(1, 11))
# call factorial(1, 11){} in 0.000999s
# 3628800
有参数decorator

在之前没有带参数的装饰器上添加参数:

from functools import reduce
import time

def performance(unit):
    def decorator_(func):
        def wrap(*args, **kwargs):
            time1 = time.time()
            res = func(*args, **kwargs)
            time.sleep(1)
            time2 = time.time()
            t = 1000 * (time2 - time1) if unit == 'ms' else (time2 - time1)
            print(f"call %s in %s %s"%(func.__name__, t, unit))
            return res
        return wrap
    return decorator_

@performance('ms')
def factorial_(n):
    return reduce(lambda x,y: x*y, range(1, n+1))

print(factorial_(6))
# call factorial_ in 1000.087261199951 ms
# 720
装饰器的优化

@decorator可以动态实现函数功能的增加,但是,经过@decorator“改造”后的函数,和原函数相比,除了功能多一点外,还有其它不同的地方。

在没有decorator的情况下,打印函数名:

def f1(x):
    pass
print(f1.__name__)  # f1

有decorator的情况下,再打印函数名:

def log(func):
    def wrapper(*args, **kw):
        return func(*args, **kw)
    return wrapper
@log
def f2(x):
    pass
print(f2.__name__)  # wrapper

由于decorator返回的新函数函数名已经不是’f2’,而是@log内部定义的’wrapper’。这对于那些依赖函数名的代码就会失效。decorator还改变了函数的__doc__等其它属性。如果要让调用者看不出一个函数经过了@decorator的“改造”,就需要把原函数的一些属性复制到新函数中:

def log(func):
    def wrapper(*args, **kw):
        return func(*args, **kw)
    wrapper.__name__ = func.__name__
    wrapper.__doc__ = func.__doc__
    return wrapper
@log
def f2(x):
    pass
print(f2.__name__)  # f2

这样写decorator很不方便,也很难把原函数的所有必要属性都一个一个复制到新函数上,所以Python内置的functools可以用来自动化完成这个“复制”的任务:

import functools
def log(func):
    @functools.wraps(func)
    def wrapper(*args, **kw):
        return func(*args, **kw)
    return wrapper
@log
def f2(x):
    pass
print(f2.__name__)  # f2
# 带参数的装饰器
# 注意@functools.wraps应该作用在返回的新函数上。
def performance(prefix):
    def log(func):
        @functools.wraps(func)
        def wrapper(*args, **kw):
            return func(*args, **kw)
        return wrapper
    return log
@performance("debug")
def f2(x):
    pass
print(f2.__name__)  # f2
类装饰器(不带参数)

基于类装饰器的实现,必须实现 __call____init__两个内置函数。
__init__ :接收被装饰函数
__call__ :实现装饰逻辑。

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

    def __call__(self, *args, **kwargs):
        print(f"logger:{self.func.__name__} in decorator")
        return self.func(*args, **kwargs)

@logger
def test(para):
    print(f"test func({para})")

test("123")
# logger:test in decorator
# test func(123)
类装饰器(带参数)

若还需要打印DEBUG WARNING等级别的日志。这就需要给类装饰器传入参数,给这个函数指定级别了。

带参数和不带参数的类装饰器有很大的不同。

__init__ :不再接收被装饰函数,而是接收传入参数。
__call__ :接收被装饰函数,实现装饰逻辑。

class logger(object):
    def __init__(self, level='info'):
        self.level = level
        
    def __call__(self, func):
        def wrapper(*args, **kwargs):
            print(f"logger[{self.level}]:{func.__name__} in decorator")
            func(*args, **kwargs)
        return wrapper

@logger(level='warning')
def test_funct(para):
    print(f'test test_funct({para})')

test_funct("321")
# logger[warning]:test_funct in decorator
# test test_funct(321)
偏函数

当一个函数有很多参数时,调用者就需要提供多个参数。如果减少参数个数,就可以简化写代码的负担。

比如,int()函数可以把字符串转换为整数,当仅传入字符串时,int()函数默认按十进制转换:

>>> int('12345')
12345

但int()函数还提供额外的base参数,默认值为10。如果传入base参数,就可以做 N 进制的转换:

>>> int('12345', base=8)
5349
>>> int('12345', 16)
74565

假设要转换大量的二进制字符串,每次都传入int(x, base=2)非常麻烦,于是,我们想到,可以定义一个int2()的函数,默认把base=2传进去:

def int2(x, base=2):
    return int(x, base)

这样,我们转换二进制就非常方便了:

>>> int2('1000000')
64
>>> int2('1010101')
85

functools.partial就是帮助我们创建一个偏函数的,不需要我们自己定义int2(),可以直接使用下面的代码创建一个新的函数int2:

>>> import functools
>>> int2 = functools.partial(int, base=2)
>>> int2('1000000')
64
>>> int2("11",base=2)  # 临时需要可以更改
3

所以,functools.partial可以把一个多参数的函数变成一个参数少的新函数,少的参数需要在创建时指定默认值,这样,新函数调用的难度就降低了。

偏函数与类实现装饰器

大多数装饰器都是基于函数和闭包实现的,但这并非构造装饰器的唯一方式。

Python 对某个对象是否能通过装饰器( @decorator)形式使用只有一个要求:decorator 必须是一个“可被调用(callable)的对象

对于callable 对象,最熟悉的就是函数了;

除函数之外,类也可以是 callable 对象,只要实现了__call__ 函数(上面几个例子)。

还有容易被人忽略的偏函数其实也是 callable 对象。

import time
import functools

class DelayFunc:
    def __init__(self, duration, func):
        self.duration = duration
        self.func = func

    def __call__(self, *args, **kwargs):
        print(f'Wait for {self.duration} seconds...')
        time.sleep(self.duration)
        return self.func(*args, **kwargs)

    def eager_call(self, *args, **kwargs):
        print('Call without delay')
        return self.func(*args, **kwargs)

def delay(duration):
    """
    装饰器:推迟某个函数的执行。
    同时提供 .eager_call 方法立即执行
    """
    # 此处为了避免定义额外函数,
    # 直接使用 functools.partial 帮助构造 DelayFunc 实例
    return functools.partial(DelayFunc, duration)
 
@delay(duration=2)
def add(a, b):
    return a+b
  
print(add)    # add 变成了类DelayFunc的实例
# <__main__.DelayFunc object at 0x000001EED91FA3C8>
print(add(3, 5))  #  直接调用实例,进入 __call__
# Wait for 2 seconds...
# 8
print(add.eager_call(1,2))  # 实现实例方法
# Call without delay
# 3
装饰类的装饰器

用 Python 写单例模式的时候,常用的有三种写法。其中一种,是用装饰器来实现的。

instances = {}

def singleton(cls):
    def get_instance(*args, **kwargs):
        cls_name = cls.__name__
        print("cls_name:",cls_name)
        if not cls_name in instances:
            print(f"cle_name[{cls_name}] not in instances")
            instance = cls(*args, **kwargs)
            instances[cls_name] = instance
        return instances[cls_name]
    return get_instance

@singleton
class User:
    _instance = None

    def __init__(self, name):
        self.name = name
        print("name=",self.name)

u1 =User("usr1")
print(u1,u1.name)

u2 =User("usr2")
print(u2,u2.name)

print(u1 == u2)
# cls_name: User
# cle_name[User] not in instances
# name= usr1
# <__main__.User object at 0x000001B2D33BB2B0> usr1
# cls_name: User
# <__main__.User object at 0x000001B2D33BB2B0> usr1
# True
wraps再理解

了解完偏函数后再回看functools.wraps()的作用

wraps 其实是一个偏函数对象(partial),源码如下:

def wraps(wrapped,
          assigned = WRAPPER_ASSIGNMENTS,
          updated = WRAPPER_UPDATES):
    return partial(update_wrapper, wrapped=wrapped,
                   assigned=assigned, updated=updated)

wraps其实就是调用了一个函数update_wrapper,知道原理后,我们改写上面的代码,在不使用 wraps的情况下,也可以让wrapped.__name__ 打印出 wrapped,代码如下:

from functools import update_wrapper

WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__qualname__', '__doc__',
                       '__annotations__')

def decorator(func):
    def inner():
        pass
    update_wrapper(inner, func, assigned=WRAPPER_ASSIGNMENTS)
    return inner

@decorator
def testfunc():
    pass

print(testfunc.__name__)  # testfunc

一般最常使用的就是:

from functools import wraps

def decorator(func):
    @wraps(func)
    def inner():
        pass
    return inner

@decorator
def testfunc():
    pass

print(testfunc.__name__)  # testfunc
python内置装饰器:property

property通常存在于类中,可以将一个函数定义成一个属性,属性的值就是该函数return的内容。

初学Python时这样给实例绑定属性:

class Person(object): 
    def __init__(self, name, age= None):
        self.name = name
        self.age = age

# instantiate
p1 = Person("person1")

# add attribute
p1.age = 25

# query attrbute
print(p1.name)

# delete attribute
del p1.age

但是这样存在问题:直接把属性暴露出去,虽然写起来很简单,但是并不能对属性的值做合法性限制。合理的写法如下:

class Person(object):
    def __init__(self, name, age= None):
        self.name = name
        self.__age = age

    def set_age(self, age):
        if not isinstance(age, int):
            raise ValueError('input illegal: age must be int.')
        if not 0 < age < 100:
            raise ValueError('input illegal: age must between 0 and 150.')
        self.__age = age

    def get_age(self):
        return self.__age

    def del_age(self):
        self.__age = None


# instantiate
p1 = Person("tiny")

# add attribute
p1.set_age(25)

# query attrbute
print(p1.get_age())

# delete attribute
p1.del_age()

上面的代码设计虽然可以变量的定义,但是可以发现不管是获取还是赋值(通过函数)都和我们平时想要的的不同。我们想要的是这样形式的:赋值p1.age = 25, 获取p1.age

使用@property :

class Person(object):
    def __init__(self, name, age=None):
        self.name = name
        self.__age = age

    @property
    def age(self):
        return self.__age

    @age.setter
    def age(self, value):
        if not isinstance(value, int):
            raise ValueError('age must be int.')
        if not 0 < value < 150:
            raise ValueError('age must between 1 and 150.')
        self.__age = value

    @age.deleter
    def age(self):
        del self.__age


# instantiate
p1 = Person("tiny")

# modify attribute
p1.age = 25

# query attrbute
print(p1.age)

# delete attribute
del p1.age

@property装饰过的函数,会将一个函数定义成一个属性,属性的值就是该函数return的内容。同时,会将这个函数变成另外一个装饰器。就像@age.setter@age.deleter

@age.setter 使得我们可以使用p1.age = 25这样的方式直接赋值;
@age.deleter 使得我们可以使用del p1.age这样的方式来删除属性。

参考来源:
慕课网python进阶教程
微信公号:Python学习开发

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值