• 博客园logo
  • 会员
  • 周边
  • 新闻
  • 博问
  • 闪存
  • 众包
  • 赞助商
  • Chat2DB
    • 搜索
      所有博客
    • 搜索
      当前博客
  • 写随笔 我的博客 短消息 简洁模式
    用户头像
    我的博客 我的园子 账号设置 会员中心 简洁模式 ... 退出登录
    注册 登录
LOFLY
终其一生,编织快乐
博客园    首页    新随笔    联系   管理    订阅  订阅

函数装饰器、类装饰器和对象装饰器

函数装饰器、类装饰器和对象装饰器

原文地址: https://zhuanlan.zhihu.com/p/93846887

要理清装饰器原理和作用,还得从函数闭包说起。

1 闭包


def outer():
     x = 1
     def inner():
          print(x)
     return inner
 
foo = outer()
foo()
print(foo.__closure__)
 # 打印结果为
 # 1
# (<cell at 0x00000000004AF2E8: int object at 0x000000006F14B440>,)

但从生命周期的角度,foo的值为outer()函数的返回值,当执行foo()时,outer()函数已经执行完毕了,此时其作用域内定义的变量x也应该已经销毁,因此执行foo()时,当执行到print()语句应该会出错。但实际执行过程中却没有。

参考: __closure__属性参考: https://deepinout.com/python/python-top-articles/1695828428_tr_closure-magic-function-in-python.html

在Python中,函数对象具有一个__closure__属性,该属性存储有关从包含函数作用域中捕获的变量的信息。__closure__属性是一个包含单元对象的元组,其中每个单元对象代表一个捕获的变量。如果函数没有从其包含函数中捕获任何变量,则其__closure__属性将为None。

def outer(x):
     def inner():
         print("The value of x is %d " % x)
     return inner

foo1 = outer(1)
foo2 = outer(2)

foo1() # 1
foo2() # 2

可以看到当传递给outer()不同的参数时,得到的inner()打印结果是不同的,这证明了闭包函数inner()记录了其外层命名空间。
我们可以利用闭包的特性得到一个对已有函数运行行为进行扩充或修改的新函数,而同时保留已有函数,不用对已有函数的代码进行修改。

2 装饰器

装饰器其实就是一个以函数作为参数并返回一个替换函数的可执行函数,即装饰器是一个函数,它以函数作为参数,返回另一个函数。

def outer(some_func):
    def inner():
        print("before some_func")
        ret = some_func()

        return ret + 1
    return inner

def foo():
    return 1

decorated = outer(foo)
print(decorated()) # 2

before some_func
2             

示例中decorated是foo的装饰版,即给foo加上了一些东西。在实际使用中实现装饰器后可能就想用装饰器替换原来的函数了,这只需要给foo重新赋值即可:

foo = outer(foo)

之后调用foo()就都是调用装饰器,而不是原来的foo。根据闭包的原理,原来的foo()在作为参数传递进outer()时,inner()就已经对其作了 “记录” 。

假设有一个提供坐标对象的库。它们主要由一对对(x, y)坐标组成。但当前这些坐标对象不支持数学运算,并且无法修改源码。如果有一个需求需要对这些坐标对象做很多数学运算,例如要构造能够接收两个坐标对象的 add 和 sub 函数,并且做适当的数学运算。这些函数很容易实现(为方便演示,提供一个简单的Coordinate类)。

但是如果add()和sub()函数必须有边界检测功能呢?例如只能对正坐标进行加或减,并且返回值也限制为正坐标,此时add()和sub()函数不能满足需求:

class Coordinate(object):
    def __init__(self,x ,y) -> None:
        self.x = x
        self.y = y
    
    def __repr__(self) -> str:
        return "Coord: " + str(self.__dict__)
    
def add(a,b):
        return Coordinate(a.x+b.x, a.y + b.y)
    
def sub(a,b):
   return Coordinate(a.x - b.x, a.y - b.y)


one = Coordinate(100,100)
two = Coordinate(300,200)
three = Coordinate(-100,-100)
print(sub(one,two))
print(add(one,three))

输出结果:

Coord: {'x': -200, 'y': -100}
Coord: {'x': 0, 'y': 0}

如果希望在不修改one、two和three的基础上,使得one和two的差值为{x: 0, y: 0},one和three的和为{x: 100, y: 200},那么可以使用装饰器的方法。接下来用一个边界检测装饰器来实现这一点,而不用对每个函数里的输入参数和返回值添加边界检测。


def wrapper(func):
     def checker(a:Coordinate, b:Coordinate): # 1
         if a.x < 0 or a.y < 0:
             a = Coordinate(a.x if a.x > 0 else 0, a.y if a.y > 0 else 0)
         if b.x < 0 or b.y < 0:
             b = Coordinate(b.x if b.x > 0 else 0, b.y if b.y > 0 else 0)
         ret = func(a, b)
         if ret.x < 0 or ret.y < 0:
             ret = Coordinate(ret.x if ret.x > 0 else 0, ret.y if ret.y > 0 else 0)
         return ret
     return checker

add = wrapper(add)
sub = wrapper(sub)

print(sub(one, two))
print(add(one, three))


# 打印结果
# Coord: {'y': 0, 'x': 0}
# Coord: {'y': 200, 'x': 100}

这种方案不是唯一的解决方案,但它让代码更加简洁: 通过将边界检测从函数本身分离,使用装饰器包装它们,并应用到所有需要的函数。 可替换的方案是:在每个数学运算函数返回前,对每个输入参数和输出结果调用一个函数。不过就对函数应用边界检测的代码量而言,使用装饰器至少是较少重复的。

3 函数装饰器@符号的应用

Python 2.4通过在函数定义前添加一个装饰器名和@符号,来实现对函数的包装。在上面代码示例中,用了一个包装的函数来替换包含函数的变量来实现了装饰函数。

add = wrapper(add)

这种模式可以随时用来包装任意函数。但是如果定义了一个函数,可以用@符号来装饰函数,如下:


@wrapper
def add(a,b):
    return Coordinate(a.x + b.x, a.y + b.y)

print(add(one,two)) #  Coord: {'x': 400, 'y': 300}


值得注意的是,这种方式和简单的使用wrapper()函数的返回值来替换原始变量的做法没有什么不同—— Python只是添加了一些语法糖来使之看起来更加明确。

4 更通用的函数装饰器

Python的函数定义中,允许有一个接收所有多余的位置参数的参数,以及一个接收所有多余的关键字参数的参数,这两个参数的参数名前要分别用*和**修饰。(不定参数)


def test(arg1, arg2, arg3, arg4, *arg_others, **kwarg_others):
     print(arg1, arg2, arg3, arg4)
     print(type(arg_others), arg_others)
     print(type(kwarg_others), kwarg_others)
test(1, 3, 5, 7, 9, 11, arg5 = 'A', arg6 = 'B')


image

函数参数列表只有“任意位置参数”和“任意关键字参数”也是允许的:


def test(*args, **kwargs):
     print(args)
     print(kwargs)

test(1, 3, 5, 7, arg1 = 'A', arg2 = 'B')
 # (1, 3, 5, 7)
# {'arg1': 'A', 'arg2': 'B'}


另外在给函数传参时,用*修饰的参数将会被认为是列表,而将所有元素取出作为位置参数;用**修饰的参数会被认为是字典,而将所有键和值取出作为关键字参数。


def test(arg1, arg2, arg3, arg4, arg5):
     print(arg1, arg2, arg3, arg4, arg5)
     
arg_list = [1, 3, 5]
kwarg_dict = {'arg4': 2, 'arg5': 4}

test(*arg_list, **kwarg_dict)
# 1 3 5 2 4

利用这种语法特性可以编写可以装饰不同参数数量的函数的函数装饰器。


def wrapper(func):
     def inner(*args, **kwargs):
         func(*args, **kwargs)
     return inner
@wrapper
def func1(a, b, c):
     print('a=', a, 'b=', b , 'c=', c)
func1(1, b = 'boy', c = 'cat')
# a= 1 b= c= cat
@wrapper
def func2(v1, v2):
    print(v1, v2)
     
func2(100, 200)
# 100 200

5 使用类作为装饰器

类也可以作为装饰器,使用起来可能比函数装饰器更方便。首先看下面一个简单的例子:


class myDecorator(object):
     def __init__(self, f):
         print("inside myDecorator.__init__()")
         f() # Prove that function definition has completed
     def __call__(self):
         print("inside myDecorator.__call__()")

@myDecorator
def aFunction():
     print("inside aFunction()")

print("Finished decorating aFunction()")
aFunction()


输出结果:
inside myDecorator.__init__()
inside aFunction()
Finished decorating aFunction()
inside myDecorator.__call__()


可以看出,在aFunction()函数声明处进入了类myDecorator的__init__()方法,但要注意,从第2个输出可以看出,此时函数aFunction()的定义已经完成了,在__init__()中调用的输入参数f(),实际上是调用了aFunction()函数。至此aFunction()函数的声明完成,包括装饰器声明的部分,然后输出了第3个输出。最后执行aFunction()时,可以看出实际上是执行了类myDecorator的__call__()方法(定义了__call__()方法的类的对象可以像函数一样被调用,此时调用的是对象的__call__()方法)。


@myDecorator
 def aFunction():
     # 。。。


等价于

def aFunction():
   # 。。。     
aFunction = myDecorator(aFunction)

因此被装饰后的函数aFunction()实际上已经是类myDecorator的对象。当再调用aFunction()函数时,实际上就是调用类myDecorator的对象,因此会调用到类myDecorator的__call__()方法。


class entryExit(object):
     def __init__(self, f):
         self.f = f
         
     def __call__(self):
         print("Entering", self.f.__name__)
         self.f()
         print("Exited", self.f.__name__)

@entryExit
def func1():
    print("inside func1()")
 
@entryExit
def func2():
     print("inside func2()")

func1()
func2()

输出:
Entering func1
inside func1()
Exited func1
Entering func2
inside func2()
Exited func2

6 使用对象作为装饰器

根据装饰器的语法,对象当然也可以作为装饰器使用,对比使用类作为装饰器,使用对象作为装饰器有些时候更有灵活性,例如能够方便定制和添加参数。


class Decorator:
     def __init__(self, arg1, arg2):
         print('执行类Decorator的__init__()方法')
         self.arg1 = arg1
         self.arg2 = arg2
         
     def __call__(self, f):
         print('执行类Decorator的__call__()方法')
         def wrap(*args):
             print('执行wrap()')
             print('装饰器参数:', self.arg1, self.arg2)
             print('执行' + f.__name__ + '()')
             f(*args)
             print(f.__name__ + '()执行完毕')
         return wrap
     
@Decorator('Hello', 'World')
def example(a1, a2, a3):
     print('传入example()的参数:', a1, a2, a3)
     
print('装饰完毕')
print('准备调用example()')
example('Wish', 'Happy', 'EveryDay')
print('测试代码执行完毕')



输出:
执行类Decorator的__init__()方法
执行类Decorator的__call__()方法
装饰完毕
准备调用example()
执行wrap()
装饰器参数: Hello World
执行example()
传入example()的参数: Wish Happy EveryDay
example()执行完毕
测试代码执行完毕


根据装饰器的语法,下面的代码:


@Decorator('Hello', 'World')
def example(a1, a2, a3):
#...


等价于:


def example(a1, a2, a3):
#...   


这时就不难理解例子中的输出了,@Decorator('Hello', 'World')实际上生成了一个类Decorator的对象,然后该对象作为装饰器修饰example()函数,修饰过程就是调用了Decorator对象的__call__()方法来“封装”exmaple(),最后example()函数的实际上是闭包后,call()方法中定义的wrap()函数。

posted @ 2024-05-15 08:46  编织快乐  阅读(56)  评论(0)    收藏  举报
刷新页面返回顶部
博客园  ©  2004-2026
浙公网安备 33010602011771号 浙ICP备2021040463号-3