Python修饰器的简单理解

python 专栏收录该内容
7 篇文章 0 订阅

@abstractmethod

抽象方法,含abstractmethod方法的类不能实例化,继承了含abstractmethod方法的子类必须复写所有abstractmethod装饰的方法,未被修饰的可以不用重写

from abc import ABC, abstractmethod

class A(ABC):
    @abstractmethod
    def test(self):
        pass
    
class B(A):
    def test_1(self):
        print("未重写abstractmethod修饰的方法")

class C(A):
    def test(self):
        print("重写abstractmethod修饰的方法")

if __name__ == '__main__':
#     a = A()
#     b = B()
    c = C()
    

a = A()和b=B()会报错

@property

把一个方法变成属性调用。被修饰的特性方法,内部可以实现处理逻辑,但对外提供统一的调用方式,实现一个实例属性的get,set,delete三种方法的内部逻辑。

class student():
    def __init__(self, s):
        self._score = 90
        
    @property
    def score(self):
        return self._score

    @score.setter
    def score(self, value):
        self._score = value

s = student(90)
print(s.score)
s.score = 100
print(s.score)
90
100

@classmethod, staticmethod

类方法classmethod和静态方法staticmethod是为类操作准备的,是将类的实例化和其方法解耦,可以不在实例化的前提下调用某些方法。两者的区别可以这么理解:类方法是将类本身作为操作对象,而静态方法是独立于类的一个单独函数,只是寄存在一个类名下。

类方法与实例方法类似,但传递的不是类的实例,而是类本身,第一个参数是cls。我们可以用类的实例调用类方法,也可以直接用类名来调用。

class A:
    class_attr = 'attr'
    def __init__(self):
        pass
    
    @classmethod
    def class_foo(cls):
        print('running class_foo(%s)' % (cls.class_attr))

a = A()
a.class_foo()
A.class_foo()
running class_foo(attr)
running class_foo(attr)

静态方法类似普通方法,参数里面不用self。这些方法和类相关,但是又不需要类和实例中的任何信息、属性等。如果把这些方法写在类外边,这样就把和类相关的代码分散在类外,使得之后对于代码的理解和维护都是巨大的障碍。

class B:
    def __init__(self):
        pass
    @staticmethod
    def static_foo():
        print('staticmethod')

B.static_foo()
staticmethod

修饰器函数机制

闭包

首先了解一下,python的闭包。闭包理解:将闭包理解为一种特殊的函数,这种函数由两个函数嵌套组成,且称之为外函数和内函数,外函数返回值是内函数的引用,此时就构成闭包。

def 外层函数(参数):
    def 内层函数():
        print('内层函数执行', 参数)
    return 内层函数
def func(a, b):
    def line(x):
        return a * x - b
    return line
line = func(2, 3)
print(line(5))
7

一般函数结束,会释放临时变量,但在闭包中,由于外函数临时变量再内函数中用到,此时外函数会把临时变量与内函数绑定到一起,这样虽然外函数结束了,但调用内函数时依然能够使用临时变量,即闭包外层的参数可以在内存中进行保留

def func(a, b):
    a = 3
    def line(x):
        return a * x - b
    return line
line = func(2, 3)
print(line(5))
12

闭包函数中,可以随意修改外函数绑定的临时变量,但是如果想要修改外函数临时变量时会发现出错。在python语法当中,一个函数可以随意读取全局数据,但是要修改全局数据的时候有两种方法:

  1. global声明全局变量
  2. 全局变量时可变类型数据的时候进行修改
    在闭包函数中也类似。
  3. 在python3中,可以用nonlocal关键字声明一个变量,表示这个变量不是局部空间的变量,需要向上一层变量空间找这个变量。
  4. 在python2中,没有nonlocal关键字,可以把闭包变量改成可变数据类型。
def func(a):
    b = 10
    c = [a] # 可变数据类型
    def inner():
        nonlocal b
        b = b + 1
        
        c[0] += 1
        print(c[0])
        print(b)
    return inner

demo = func(5)
demo()
6
11

注意:闭包过程中,一旦外函数被调用一次返回内函数的引用,虽然每次调用内函数,是开启一个函数,执行后消亡,但是闭包变量实际只有一份,每次开始内函数都在使用同一份闭包变量。

def func(x):
    def inner(y):
        nonlocal x
        x += y
        return x
    return inner
a = func(10)
print(a(1))
print(a(2))
11
13

修饰器

修饰器是建立在闭包基础上,将被修饰函数传入闭包函数中执行。
简单的修饰器:

def w1(func):
    def inner():
        print('...验证权限...')
        func()
    return inner
@w1
def f1():
    print('f1 called')

@w1
def f2():
    print('f2 called')
f1()
f2()
...验证权限...
f1 called
...验证权限...
f2 called

在调用f1,f2函数时,成功进行了权限验证。这里是通过修饰器实现,通过定义一个闭包函数w1,在我们调用函数上通过关键词@w1,这样就完成了修饰。

修饰器原理

修饰函数w1,该函数接收参数func,其实就是接收一个方法名,w1内部定义一个inner函数,在inner函数中增加权限校验,并完成验证之后调用传进来的参数func,同时w1的返回值为内部函数inner,这其实就是一个闭包函数。

在f1上增加@w1,当python解释器执行到这句话的时候,会去调用w1函数,同时将被修饰的函数名作为参数传入,根据闭包原理,在执行w1函数的时候,此时直接把inner函数返回,同时把它赋值给f1,此时的f1已经不再是未加修饰的f1,而是指向w1.inner函数地址,相当于f1 = w1(f1)。

接下来调用f1的时候,其实就是调用w1.inner。

执行时机

def w1(fun):
    print('...装饰器开始装饰...')
    def inner():
        print('...验证权限...')
        fun()
    return inner
@w1
def test():
    print('test')
test()
...装饰器开始装饰...
...验证权限...
test

由此可以看出,当python执行到@w1时,就开始进行修饰了,相当于执行了test=w1(test)

两个修饰器执行流程和修饰结果

def makeBold(fun):
    print('----a----')

    def inner():
        print('----1----')
        return '<b>' + fun() + '</b>'
    return inner
def makeItalic(fun):
    print('----b----')

    def inner():
        print('----2----')
        return '<i>' + fun() + '</i>'
    return inner
@makeBold
@makeItalic
def test():
    print('----c----')
    print('----3----')
    return 'hello python decorator'
ret = test()
print(ret)
----b----
----a----
----1----
----2----
----c----
----3----
<b><i>hello python decorator</i></b>

可以看出,先用第二个修饰器进行修饰,接着再用第一个修饰器进行修饰,在调用过程中,先执行第一个修饰器,接着再执行第二个修饰器。

为什么?

  1. 通过上面修饰时机的介绍,我们可以知道,再执行到@makeBold的时候,需要对下面的函数进行修饰,此时解释器继续向下走,并发现不是一个函数名而是一个修饰器,这时候,@makeBold修饰器暂停运行,而接着执行@makeItalic,接着把test函数名传入修饰器函数,从而打印‘b’,在makeItalic修饰完之后,此时的test指向makeItalic.inner函数,这时候又返回执行@makeBold,接着把新test传入makeBold修饰器函数,因此打印’a’。

  2. 在调用test函数的时候,根据上述分析,此时test指向makeBold.inner函数,因此会先打印’1’,接下来,在调用fun()的时候,其实是调用makeItalic.inner,所以打印’2’,在makeItalic.inner中,调用的fun其实才是我们最先定义的test函数。

对有参函数进行修饰

上面两个例子中都是对无参函数进行的修饰,不再单独举例。下面将分析对有参函数的修饰。

def w_say(fun):
    """
    如果原函数有参数,那闭包函数必须保持参数个数一致,并且将参数传递给原方法
    """

    def inner(name):
        """
        如果被装饰的函数有行参,那么闭包函数必须有参数
        :param name:
        :return:
        """
        print('say inner called')
        fun(name)

    return inner


@w_say
def hello(name):
    print('hello ' + name)


hello('wangcai')
say inner called
hello wangcai

如果有多个参数。

def w_add(func):
    def inner(*args, **kwargs):
        print('add inner called')
        func(*args, **kwargs)
    return inner
@w_add
def add(a, b):
    print('%d + %d = %d' % (a, b, a + b))
@w_add
def add2(a, b, c):
    print('%d + %d + %d = %d' % (a, b, c, a + b + c))
add(2, 4)
add2(2, 4, 6)
add inner called
2 + 4 = 6
add inner called
2 + 4 + 6 = 12

利用python的可变参数实现带参函数的修饰。

对有返回值的函数进行修饰

def w_test(func):
    def inner():
        print('w_test inner called start')
        func()
        print('w_test inner called end')
    return inner
@w_test
def test():
    print('this is test fun')
    return 'hello'
ret = test()
print('ret value is %s' % ret)
w_test inner called start
this is test fun
w_test inner called end
ret value is None

可以看出,此时并没有输出test函数的返回值’hello’,而是None,为什么?可以发现,在inner函数中对test进行了调用,但是没有接受返回值,那么默认就是None。可以进行以下修改:

def w_test(func):
    def inner():
        print('w_test inner called start')
        str = func()
        print('w_test inner called end')
        return str
    return inner
@w_test
def test():
    print('this is test fun')
    return 'hello'
ret = test()
print('ret value is %s' % ret)
w_test inner called start
this is test fun
w_test inner called end
ret value is hello

这样就达到预期,完成对有返回参数的函数修饰。

带参数的修饰器

def func_args(pre='xiaoqiang'):
    def w_test_log(func):
        def inner():
            print('...记录日志...visitor is %s' % pre)
            func()
        return inner
    return w_test_log
# 带有参数的装饰器能够起到在运行时,有不同的功能
# 先执行func_args('wangcai'),返回w_test_log函数的引用
# @w_test_log
# 使用@w_test_log对test_log进行装饰
@func_args('wangcai')
def test_log():
    print('this is test log')
test_log()
...记录日志...visitor is wangcai
this is test log

简单的理解就是,带参数的修饰器在闭包的基础上又加了一层闭包。优点就是:在运行时,针对不同的参数作出不同的应用功能处理。

类修饰器

类修饰器其实是一个接口约束,它必须接受一个callable对象作为参数,然后返回一个callable对象。在python中,一般callable对象都是函数,但是又例外,比如只要某个对象重写了call方法,那么这个对象就是callable的。

当创建一个对象后,直接去执行这个对象,那么是会抛出异常,因为它不是callable,无法直接执行,进行修改后:

class Test(object):
    def __call__(self, *args, **kwargs):
        print('call called')


t = Test()
print(t())
call called
None

如何用类修饰函数。

class Test(object):
    def __init__(self, func):
        print('test init')
        print('func name is %s ' % func.__name__)
        self.__func = func
    def __call__(self, *args, **kwargs):
        print('装饰器中的功能')
        self.__func()
@Test
def test():
    print('this is test func')
test()
test init
func name is test 
装饰器中的功能
this is test func

当python解释器执行到@Test时,会把当前test函数作为参数传入Test对象,调用init方法,同时将test函数指向创建的Test对象,那么在接下来执行test()的时候,其实就是直接对创建的对象进行调用,执行其call方法。

warps修饰器

首先了解下partial和update_wrapper这两个函数。

partial

首先说partial函数,函数声明:functools.partial(func, *args, **keywords)。它的作用就是返回一个partial对象,当这个partial对象被调用的时候,就像通过func(*args, **kwargs)的形式来调用func函数一样。如果有额外的 位置参数(args) 或者 关键字参数(*kwargs) 被传给了这个partial对象,那它们也都会被传递给func函数,如果一个参数被多次传入,那么后面的值会覆盖前面的值。

这个函数很像C++中的bind函数,都是把某个函数的某个参数固定,从而构造出一个新的函数来。比如下面这个例子:

from functools import partial

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

# 这里创造了一个新的函数add2,只接受一个整型参数,然后将这个参数统一加上2
add2 = partial(add, y=2)

add2(3)  # 这里将会输出5
5

update_wrapper

这个函数是用来更新修饰器函数。作用就是从被修饰的函数中取出一些属性值,赋给修饰器函数。

def wrapper(f):
    def wrapper_function(*args, **kwargs):
        """这个是修饰函数"""
        return f(*args, **kwargs)
    return wrapper_function
    
@wrapper
def wrapped():
    """这个是被修饰的函数"""
    print('wrapped')

print(wrapped.__doc__)  # 输出`这个是修饰函数`
print(wrapped.__name__)  # 输出`wrapper_function`
这个是修饰函数
wrapper_function

上面可以看出,我们想要获取wrapped这个被修饰函数的文档字符串,但是却获取了wrapper_function的文档字符串,wrapped函数的名字也变成了wrapper_function函数的名字。这是因为加上@wrapper修饰器之后,相当于执行了wrapped=wrapper(wrapped)。

我们对上面定义的修饰器稍作修改。

from functools import update_wrapper

def wrapper(f):
    def wrapper_function(*args, **kwargs):
        """这个是修饰函数"""
        return f(*args, **kwargs)
    update_wrapper(wrapper_function, f)  # <<  添加了这条语句
    return wrapper_function
    
@wrapper
def wrapped():
    """这个是被修饰的函数"""
    print('wrapped')


print(wrapped.__doc__)  # 输出`这个是被修饰的函数`
print(wrapped.__name__)  # 输出`wrapped`
这个是被修饰的函数
wrapped

这时我们发现,_doc_ 和 __name__属性已经显示正确,除此之外update_wrapper函数也对__module__和__dict__等属性进行了更改和更新。

wraps修饰器

源码:

WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__qualname__', '__doc__',
                       '__annotations__')
WRAPPER_UPDATES = ('__dict__',)
def wraps(wrapped,
          assigned = WRAPPER_ASSIGNMENTS,
          updated = WRAPPER_UPDATES):
    return partial(update_wrapper, wrapped=wrapped,
                   assigned=assigned, updated=updated)

可以看出,wraps函数其实就是一个修饰器版的update_wrapper函数,它的功能和update_wrapper是一样的。我们可以修改我们上面定义修饰器例子。

from functools import wraps

def wrapper(f):
    @wraps(f)
    def wrapper_function(*args, **kwargs):
        """这个是修饰函数"""
        return f(*args, **kwargs)
    return wrapper_function
    
@wrapper
def wrapped():
    """这个是被修饰的函数
    """
    print('wrapped')

print(wrapped.__doc__)  # 输出`这个是被修饰的函数`
print(wrapped.__name__)  # 输出`wrapped`
这个是被修饰的函数
    
wrapped

简单理解:wraps这个修饰器,就是将被修饰的函数(wrapped)的一些属性值赋值给修饰器函数(wrapper)。

references:
https://www.cnblogs.com/slysky/p/9777424.html

  • 4
    点赞
  • 0
    评论
  • 6
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

相关推荐
©️2020 CSDN 皮肤主题: 像素格子 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值