Python进阶(十一)装饰器

  装饰器是Python的语法糖,它可以将复杂的功能封装起来,在外部表现出特别简单的语法和语义。装饰器还可以实现AOP面向切面的编程。

  Python装饰器的本质,就是闭包。一般谈Python的闭包,都是指普通的入参,而谈装饰器的时候,入参一定有函数。闭包和装饰器,返回都是函数。函数是代码的最小封装单位,装饰器作用于函数,它不影响函数自身的执行,只是在函数的执行前后增加一些“装饰性”的动作。装饰器被称为Python的语法糖(Syntax Sugar),也被视为Python支持AOP编程(面向切面编程)的工具。

简单装饰器

  在函数执行前,增加一行print打印,代码如下:

def calling_trace(func):
    def wrapper():
        print('calling', func.__name__)
        func()
    return wrapper

@calling_trace
def test1():
    print('test1 is runing...')

test1()

  test1函数加上了calling_trace装饰器,相同于test1 = calling_trace(test1)。以上代码运行效果如下:

calling test1
test1 is runing...

  这就是装饰器的概念:不修改已有函数的代码,也不修改已有函数的调用处代码,却达到了丰富函数功能的效果

装饰带参数的函数

  如果被装饰的函数有参数,需要在装饰器内部原样复制函数的参数定义。

def calling_trace(func):
    def wrapper(*args):
        print('calling', func.__name__)
        a = func(*args)
        print('reture value:',a)
    return wrapper

@calling_trace
def test4(*args):
    print('test4 is runing...')
    return sum(args)

test4(1,2,3,4,5,6,7,8)
test4(23,34,45,56)

  *args表示一个tuple,在函数定义处出现,就是packing打包调用时的参数,在调用时出现,就是unpacking展开tuple。跟**kw(对应dict)用法一样。以上程序运行结果如下:

calling test4
test4 is runing...
reture value: 36
calling test4
test4 is runing...
reture value: 158

带参数的装饰器

  装饰器本身也可以带参数,通过在装饰器外再使用闭包,给装饰器封装其执行环境,可以使装饰器的功能更强大更灵活,也可以更好的控制函数的执行(比如在某些情况下不执行)。

  而带参数的装饰器,在语法上,也就跟闭包区分开来。(应该就是这个原因,闭包和装饰器在Python中是分成了两个概念)。

def calling_trace(run):
    def deco(func):
        def wrapper(*args, **kw):
            print('calling', func.__name__)
            if run == 1:
                a = func(*args, **kw)
                print('reture value:',a)
            else:
                print('not allow to run')
        return wrapper
    return deco

# test5 = calling_trace(run=1)(test5)
@calling_trace(run=1)
def test5(a,b,c=3):
    print('test5 is runing...')
    return a+b+c

@calling_trace(run=0)
def test6(*args):
    print('test6 is runing...')
    return sum(args)

test5(1,2)
test5(1,2,c=8)
test6(23,34,45,56)

  代码的运行结果:

calling test5
test5 is runing...
reture value: 6
calling test5
test5 is runing...
reture value: 11
calling test6
not allow to run

多重装饰器

  函数可以有多个装饰器!多个装饰器的效果,就相当于对函数进行了多层的封装包裹,而不同的装饰器对函数执行的功能影响,完全独立。比如有个装饰器用来控制函数是否能够被执行,另一个装饰器控制函数的所有raise出来的异常。

@a
@b
@c
def tt(): pass
# tt = a(b(c(tt)))

基于类的python装饰器

  Python装饰器函数其实是这样一个接口约束,它必须接受一个callable对象作为参数,然后返回一个callable对象。在Python中一般callable对象都是函数,但也有例外。只要某个对象重载了__call__()方法,那么这个对象就是callable的。

class call_class():
    def __call__(self):
        print('I am in call_class')

tt = call_class()
tt()

  执行结果如下:

I am in call_class

  我们判断一个对象(变量和函数都是对象)是否可调用,其实就是判断这个对象是否含有__call__函数。

  __call__这样前后都带下划线的方法在Python中被称为内置方法,有时候也被称为魔法方法。重载这些魔法方法一般会改变对象的内部行为。上面这个例子就让一个类对象拥有了被调用的行为。

  回到装饰器上的概念上来,装饰器要求接受一个callable对象,并返回一个callable对象,那么用类来实现也是也可以的。我们可以让类的构造函数__init__()接受一个函数,然后重载__call__()并返回一个函数,也可以达到装饰器函数的效果。

class calling_trace():
    def __init__(self, run):
        self.run = run

    def __call__(self, func):
        def wrapper(*args, **kw):
            print('calling', func.__name__)
            if self.run == 1:
                a = func(*args, **kw)
                print('return value:', a)
            else:
                print('not allow to run')
        return wrapper

#def calling_trace(run):
#    def deco(func):
#        def wrapper(*args, **kw):
#            print('calling', func.__name__)
#            if run == 1:
#                a = func(*args, **kw)
#                print('reture value:',a)
#            else:
#                print('not allow to run')
#        return wrapper
#    return deco

# test5 = calling_trace(run=1)(test5)
@calling_trace(run=1)
def test5(a,b,c=3):
    print('test5 is runing...')
    return a+b+c

@calling_trace(run=0)
def test6(*args):
    print('test6 is runing...')
    return sum(args)

test5(1,2)
test5(1,2,c=8)
test6(23,34,45,56)

  class calling_trace与注释掉的那个函数,效果是完全一样的。运行结果为:

calling test5
test5 is runing...
return value: 6
calling test5
test5 is runing...
return value: 11
calling test6
not allow to run

@staticmethod的使用

一般类中的函数,都是与对象绑定,其第1个参数习惯上都使用self这个名称(也可以使用别的名称),表示当此函数被调用的时候,对象自己会作为第1个参数(隐式地)传入。不过有趣的是,这些函数在被调用的时候,此对象并没有显式的出现在参数列表中。将对象自身作为第1个参数传入,是python解释器自动完成的动作。

>>> '-'.join('12345')
'1-2-3-4-5'

以上代码,调用了str对象的join函数,join函数的定义如下:

join(self, iterable, /)
    Concatenate any number of strings.

    The string whose method is called is inserted in between each given string.
    The result is returned as a new string.

    Example: '.'.join(['ab', 'pq', 'rs']) -> 'ab.pq.rs'

join函数的第1个参数是self,表示对象本身,按上文的示例代码,就是 ‘-’ 这个str对象。不过写出来的代码,参数只有’12345’,没有写出self参数。这里请仔细体会!

@staticmethod装饰过的函数,就不再是一般的成员函数了,而是“静态函数”。这类函数在调用的时候:

(1)Python解释器不会将对象作为第1个参数传入;
(2)不要通过对象来调用此类函数,要通过类来调用;
(3)因为没有对象传入,这类函数就无法获取与对象绑定的成员信息,也就是不能访问任何一个对象绑定的属性信息,只能通过类名来访问类变量class variable),这也是第2点的原因,要用类来调用静态函数;
(4)此类函数,被封装在类中,只能在此类以及继承此类的范围内访问,外界不可访问,也是一种封装技术。
它是静止的,不会随不同的类和对象而发生变动,只会出现在类以及继承于此类的空间中。

class group():

    num = 0

    def __init__(self, sn):
        self.sn = sn
        group.num += 1

    @staticmethod
    def getTotal():
        return group.num


g1 = group('123')
g2 = group('124')
g3 = group('125')
print(group.getTotal()) # >>> 3

  @staticmethod定义的函数,只存在此类或继承此类的空间(namespace)中,它不接受对象参数,因此它的最佳使用场景,就是为此类以及继承此类的类提供某种计算服务(比如某些属性值的检查),这类计算不需要具体对象的参与,也最好不要去访问类变量(在继承的情况下,类的名称会变),它的计算参数很可能均来自外部输入,但是计算结果为类和对象服务。或者,@staticmethod就是用一个类来封装一个功能块接口,如果没有继承,它们之间可以通过类名来相互调用。

@classmethod的使用

  @classmethod一样,也是应用于类中的函数,将这些函数转换为类函数。装饰器也是一个函数!内置的装饰器,当然也是内置函数。我们定义的Python类,内部可以有三种成员函数:

  1. 一般的对象函数,没有任何装饰器修饰的都是这类。
  2. @staticmethod装饰的静态函数,静态类的成员函数,只能通过明确的类名来访问类中的变量,使用方面受到限制。
  3. @classmethod装饰的类函数,使用比静态函数要广一些,因为类函数的第1个参数默认为当前类名。由于类函数可以访问类变量(class variable),使得类函数常常用来提供创建对象不同的接口。
class fruit():

    num = 0

    def __init__(self, price):
        self.price = price
        self.count()

    @classmethod
    def count(cls):
        cls.num += 1

    @classmethod
    def float_price(cls, price):
        if not isinstance(price, float):
            try:
                price = float(price)
            except: raise
        return cls(price)

f1 = fruit(1.234)
print(fruit.num)
f2 = fruit.float_price(1.2345)
print(fruit.num)
f3 = fruit.float_price('1.234')
print(fruit.num)
f4 = fruit.float_price('abcde')

  一般情况下,我们实用f1 = fruit(1.234)的方式创建对象。如果我们需要对入参进行一些限制和判断,甚至转换,就可以考虑实用float_price这个类函数。请看f2和f3的创建,确定参数没有问题之后,再调用cls(price)创建对象。

  clsclassmethod类函数的第1个默认参数,cls这个名字,与self一样,只是一个约定俗称的写法。cls(price),就跟fruit(price)一样,内部调用__init__创建对象。

  count函数也是类函数,用于累加类变量num,用来计数fruit对象的个数。上面代码执行效果如下:

1
2
3
Traceback (most recent call last):
  File "classmethod.py", line 30, in <module>
    f4 = fruit.float_price('abcde')
  File "classmethod.py", line 19, in float_price
    price = float(price)
ValueError: could not convert string to float: 'abcde'

  为什么这个简单的记录对象个数的函数,要用@classmethod来修饰呢?因为,在有继承关系的时候,可以让你少些很多代码,整体结构也更优雅。

class fruit():

    num = 0

    def __init__(self, price):
        self.price = price
        self.count()

    @classmethod
    def count(cls):
        cls.num += 1

    @classmethod
    def float_price(cls, price):
        if not isinstance(price, float):
            try:
                price = float(price)
            except: raise
        return cls(price)


class apple(fruit):

    @classmethod
    def int_price(cls, price):
        if not isinstance(price, int):
            try:
                price = int(price)
            except: raise
        return cls(price)


f1 = apple(12)
print(apple.num)
f2 = apple.float_price(1.2345)
print(apple.num)
f3 = apple.int_price('12')
print(apple.num)
f4 = apple.int_price('abcde')

  现在我们来创建apple对象,apple类继承fruit,只是增加了一个int_price类函数。同样,我们尝试创建4apple对象,预期输入'abcde'的时候,创建会失败,以上代码执行结果如下:

1
2
3
Traceback (most recent call last):
  File "classmethod.py", line 41, in <module>
    f4 = apple.int_price('abcde')
  File "classmethod.py", line 30, in int_price
    price = int(price)
ValueError: invalid literal for int() with base 10: 'abcde'

  结果符合我们的预期,apple对象的计数也在正常进行,这就要归功于fruit类中定义的那个count class method。因为classmethod函数可以使用第1个参数,cls,来访问类变量,因此继承之后,cls自动就指向了继承后的类。注意,这是@classmethod与@staticmethod不一样的地方

  这类函数是服务于类的,它可以访问类变量,因此在继承之后,依然可以保持对继承后的类的支持。

@property装饰器

  @property装饰器,它的作用,就是能够把一个类的成员函数,当成一个属性来访问,访问这个由函数装扮成的属性,表面上看是对属性的直接访问,实质上是在调用函数。

class fruit():

    def __init__(self, num, price):
        self.num = num
        self.price = price

    @property
    def totalcost(self):
        return round(self.num*self.price,2)


ft = fruit(100, 12.34)
print(ft.totalcost) #>>> 1234.0

  fruit类中有一个函数叫totalcost,被@property装饰了一下。与你你会看到,代码在访问(调用)totalcost的时候,并没有带括号,就像是一个普通的属性一下直接访问,但是实际上是在做函数调用。

  Python解释器在访问对象属性的时候,要先判断属性本身属于什么类型,内部会做一些转换,因此加上@property装饰器后,用访问普通属性的方式调用函数成为了可能。

  • @property装饰器访问私有成员

  Python对象本质上是没有私有成员的,用双下划线的方式定义的成员,也可以通过附加类名的方式来访问。不过这种访问方式应该要被禁止。

  Python提供了一个@property装饰器,可以更好的控制对对象成员的访问,也可以更好的控制对成员的修改。

class test():
    def __init__(self):
        self.__foo = 'foo'
        self.__bar = 'bar'
    
    @property
    def foo(self):
        # print('running in foo function')
        return self.__foo
        
        
t = test()
print(t.foo)

  用@property装饰的foo函数,就变成了foo成员变量,访问这个成员变量的时候,触发内部的一个函数执行。此时,foo这个成员变量只能读,不能写,如果尝试使用t.foo=123来赋值,会出现错误:AttributeError: can't set attribute。若想要实现修改内部__foo成员,可采用以下代码:

class test():
    def __init__(self):
        self.__foo = 'foo'
        self.__bar = 'bar'
    
    @property
    def foo(self):
        # print('running in foo function')
        return self.__foo
        
    @foo.setter
    def foo(self, foo_value):
        if foo_value < 100: 
            pass
        else:
            self.__foo = foo_value
        
t = test()
print(t.foo)
t.foo = 123
print(t.foo)

  注意@foo.setter这个装饰器的由来。在设置的时候,外部代码还是简简单单地使用等号实现,而触发的是内部的一段代码,这段代码可以实现设置前的检查!

  下面的代码,实现对某个成员的删除控制:

class test():
    def __init__(self):
        self.__foo = 'foo'
        self.__bar = 'bar'
    
    @property
    def foo(self):
        # print('running in foo function')
        return self.__foo
        
    @foo.setter
    def foo(self, foo_value):
        if foo_value < 100: 
            pass
        else:
            self.__foo = foo_value
            
    @foo.deleter
    def foo(self):
        if self.__foo > 200:
            self.__foo = 200
        
t = test()
print(t.foo)
t.foo = 123
print(t.foo)
t.foo = 234
print(t.foo)
del t.foo
print(t.foo)

  注意@foo.deleter这个装饰器。执行del t.foo语句的时候,同样触发一段自定义代码,上面的代码示例,并没有真正删除foo这个变量,只是限制它的值。

  通过@property装饰器,以及其衍生出来的一些其它装饰器,我们可以通过简单的语义实现复杂的功能,可以增加代码的可读性。

转载文章链接

  • https://www.pynote.net/archives/tag/decorator

你可能感兴趣的:(Python进阶(完结))