Python 装饰器

最近在学Python的进阶知识,然后看到了装饰器,整理总结一下装饰器中的知识点以作备忘。

在讲装饰器之前,有必要先提及一下Python中的闭包,因为装饰器的本质就是闭包,并且当我们在编写参数化的装饰器时,往往都会出现闭包。

闭包

维基百科中对于闭包的解释是这样的,在一些语言中,在函数中可以(嵌套)定义另一个函数时,如果内部的函数引用了外部的函数的变量,则可能产生闭包。闭包可以用来在一个函数与一组“私有”变量之间创建关联关系。在给定函数被多次调用的过程中,这些私有变量能够保持其持久性。

我们可以看下面一个简单的例子,

def make_func(x):
    def func():
        return x ** x
    return func

f = make_func(5)
f()   # 输出为 3125

在上例中xmake_func的变量,对于func而言是外部变量,并对其使用并计算返回,这样一来我们便实现了一个简单的闭包。因此闭包简单来说就是内层函数引用了外层函数的变量,然后返回内层函数的情况。

装饰器

装饰器是可调用对象,其参数是另外一个函数,它可以让其他函数在不需要做任何代码变动的前提下增加额外功能(如:插入日志、性能测试等),装饰器可能会处理被装饰的函数,然后把他返回,或者将其替换成另一个函数或可调用对象。当我们定义了一个函数,想在运行时动态的增加功能,又不想改动函数本身的代码的时候,就可以使用装饰器。

我们现在有如下的代码:

def target():
    print('running target()')

但是我们想在运行时打印出函数的调用,最简单的方法就是直接在函数中增加print('call function', target.__name__)即可,当然也可以去新定义一个调用函数

def call_func(f):
    print('call function', f.__name__)
    return f()

但是,上述方法都不是我们所要考虑的(虽然很容易看懂,毕竟我们要讲装饰器啊)。我们都知道Python是支持高阶函数的,所谓高阶函数就是接受函数为参数,或者把函数作为结果返回的函数,我们所遇到的map,filter,sorted等都是高阶函数,大家可能在刚学习Python的时候就有用到过,比如sorted(lists, key = len),其根据列表中字符串的长短进行排序。所以,要打印出函数调用,我们也可以通过一个高阶函数来实现,完整的代码如下

def call_func(f):  # 装饰器函数,f是被装饰的函数
    def fn():
        print('call function', f.__name__)
        return f()
    return fn
    
def target():
    print('running target()')
    
target = call_func(target)
target()

该代码便实现了一个装饰器的功能,原来的函数target通过call_func来对其进行装饰。

不过当我们编写的代码较多时,会觉得写target = call_func(target)这样一条指令过于繁琐,所以Python中内置的@语法糖可以简化装饰器调用,对于上述代码的后半段,我们可以改写为:

@call_func
def target():
    print('running target()')
target()

其输出为:

call function target
running target()

在这里我们可以看下target的输出(也就是不使用调用运算符‘()’),其输出内容为

<function __main__.call_func.<locals>.fn()>

从结果中可以看出target现在是fn的引用,我们再来看一下target__name__还是不是target,执行target.__name__,可以得到fn,

target.__name__

不难看出,target函数已被替换成函数fn

因此,装饰器的一大特性是,能把被装饰的函数替换成其他函数

大多数装饰器会在内部定义一个函数然后将其返回(如上例中的fn),但是也有时候为了减少代码量的编写,不在内部定义函数,而是直接让装饰器返回原来的函数。当我们定义的函数中有参数计算时,我们需在内部定义一个函数然后将其返回;若定义的函数不需要参数时,便可以将定义函数的环节省去,如下:

# 在内部定义函数的示例
def call_func(f): 
    def fn(x):
        print('call function', f.__name__)
        return f(x)
    return fn
    
@call_func   
def target(x):
    print(x ** x)
target(3)
# 输出结果为
# call function target
# 27

# 在内部不定义函数
def call_func(f):
    print('call function', f.__name__)
    return f

@call_func
def target():
    print('running target()')  
target()
# 输出结果为
# call function target
# running target()
# 这里需要注意的是,查看target.__name__结果时输出仍为target,因为在装饰器函数中,其返回的是之前传入的函数

不过个人感觉还是在装饰器里面再定义一个函数稍好。

之前说到装饰器的一大特性是,能把被装饰的函数替换成其他函数。当然,装饰器还有另一大特性,装饰器在加载模块时立即执行,也就是说装饰器在被装饰的函数定义之后立即运行,我们可以看下面一个简单的例子,

def deco(f):
    print('running deco(%s)' % f)
    return f

@deco
def f1():
    print('running f1()')

结果为:

running deco(<function f1 at 0x00000218294FE048>)

可以看出我们并没有去执行调用函数的语句(装饰器除外),它却将装饰器函数中的print语句输出,而f1中的语句只有在明确调用时才会去执行,由此可见装饰器将会在被装饰的函数定义之后立即运行。

在这里,需要指出的是,若在上述装饰器函数中内建一个函数,将看不出这样的效果,这是因为我们返回的内建函数并没有被调用,只有等到调用被装饰的函数时才会去调用它。

def deco(f):
    def inner():
        print('running deco(%s)' % f)
        return f
    return inner

@deco
def f1():
    print('running f1()')

我们可以将装饰器对函数的装饰用一个过程来表示,@deco 等价于 f1 = deco(f1) -> f1 = inner,从该过程我们可以明显的看出,inner并没有被调用。

装饰具有单个参数或无参数的函数

对于我们所定义的一个函数,当前无参数时,我们可直接在装饰器内建一个不带参数的函数;当具有一个参数时,需给在装饰器内建的函数中给定,如下:

def call_func(f): 
    def fn(x):    # 定义内部函数fn,它接收一个参数x
        print('call function', f.__name__)
        return f(x)
    return fn
    
@call_func   
def target(x):
    return x ** x
target(3)

装饰具有多个参数的函数

对于上例,我们对target函数稍加修改,使其接收两个参数,并对其进行计算,

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

当对上述代码使用我们之前定义的装饰器,将会报如下错误

TypeError: fn() takes 1 positional argument but 2 were given

聪明的同学肯定会说,既然报错说少给了参数,那我们就根据它的要求多给它一个参数呗。没错,我们可以直接在装饰器函数call_func中添加一个参数,但是,当我们需要添加的参数上百个时,我们也要一个个去添加嘛?这当然时不行的,因此,我们在传递参数时可考虑仅限关键字参数,也就是我们通常所见到的‘*’,‘**’的标记。对于该类标记,做个简单的介绍。

*kw是以元组的形式存放的,其对于无具名参数使用
**kw是以字典的形式存放的,其对于具名参数使用

def test(*kw, **kws):
    print('kw:', kw)
    print('kws:', kws)
test(1, 2, name = 'xiaoming', age = '12')

结果为

kw: (1, 2)
kws: {'name': 'xiaoming', 'age': '12'}

需指出的时,对于‘**’有另外一种用法,如下

def test(name, *kw, **kws):
    print('kws:', kws)

tag = {'name': 'xiaoming', 'age': 12, 'gender': 'male'}
test(**tag)

结果为:

kws: {'age': 12, 'gender': 'male'}

发现kws中的输出无name键了,这是因为在tag前加了’**’,使得字典tag中的所有元素作为单个参数传入,同名键会被绑定到对应的具名参数上,余下的则被**kws捕获。

好了,还回到我们装饰器的问题上了,当我们所定义的函数具有多个参数时,我们便可以在装饰器中使用仅限关键字参数以使其 自适应任何参数定义的函数。如下:

def call_func(f): 
    def fn(*args, **kwargs):    # 定义内部函数fn,它接收一个参数x
        print('call function', f.__name__)
        print('args中参数:',', '.join(repr(arg) for arg in args))
        print('kwargs中参数:',', '.join('%s=%r' % (k, w) for k, w in kwargs.items()))
        return f(*args, **kwargs)  # 将全部参数再传给原函数进行计算打印
    return fn

@call_func
def target(x, y, name, age):
    print(x + y)

target(3, 4, name = 'xiaoming', age = 12)

结果为:

call function target
args中参数: 3, 4
kwargs中参数: name='xiaoming', age=12
7

参数化装饰器

在上面的有关装饰器的例子中,我们都是直接写出@***的形式,其Python是把被装饰的函数作为第一个参数传给装饰器函数,那怎么让装饰器接收其他的参数呢?答案是:创建一个装饰器工厂函数,把参数传给它,返回一个装饰器,然后再把它应用到要装饰的函数上?没看懂?我刚看这句话的时候也没有看懂,后来经过一番研究才了解少许,换句话说,就是在我们原本定义的装饰器A的外围再去定义一个函数func,让他来为装饰器A接收除被装饰的函数外的其他参数,并让函数func返回装饰器A的函数,装饰器A视情况决定是否使用该参数。可能大多数同学还是不能明白,没关系,我们上代码:

registry = set()   # 定义一个set对象,用于存放被装饰的函数
def register(active = True):   # 装饰器工厂函数
    def deco(func):    # 真正的装饰器函数
        def fn():
            if active:   # 若active = True,则将该被装饰的函数放入registry当中
                registry.add(func)
            else:
                registry.discard(func)
            return func
        return fn
    return deco

@register(active = False)
def f1():
    print('running f1()')
    
@register(active = True)
def f2():
    print('running f2()')

@register()   # 默认为active = True
def f3():
    print('running f3()')

f1()
f2()
f3()
registry

控制台的显示结果为:

{<function __main__.f2()>, <function __main__.f3()>}

从结果中我们可以看出,registry中最后只存放了f2,f3,而f1却没有,这就是装饰器中参数在做怪,f1的装饰器函数中的参数active被置为False,从而使得其无法和它的伙伴们一起加入到registry中。

上例中的register函数就是一个装饰器工厂函数,其用来接收我们所传给装饰器的其他参数。因此对于上述代码我们可以将其用伪代码简述一下:

定义(def)一个装饰器工厂函数,带有我们所要传递的参数
	定义(def)一个实际应用的装饰器函数
		装饰器函数内部,进行对参数判断等操作
	返回(return)我们之前所定义的装饰器函数

这里需要注意的时,由于我们定义了一个装饰器工厂函数,所以在使用@语法糖时,一定要传入变量,或者直接使用默认的变量(使用默认的变量也需要加上’()’),否则会报错

有的同学可能会问,为什么我们这里调用了被装饰后的函数,可是却没有输出类似running f1()的语句,这是因为我们在return func中并没有调用func,而在本文刚开始的是return func(),故有输出,大家可以观察一下。

为了更好的理解这一过程,我们用等价的语言来进行描述,以f2为例, @register(active = True) ==> f2 = register(active = True)(f2) ==> f2 = deco(f2) ==> f2 = fn,由此可见,fn并没有被调用,也就无法对registry进行操作。

叠放装饰器

有时候我们会编写多个装饰器,并且需要将这多个装饰器都应用到某个函数上时,这就需要我们对装饰器进行叠放,在进行叠放时,需根据自己编写的代码注意一定的顺序,以免装饰器执行错误。例如,把@d1和@d2两个装饰器按顺序应用到f函数上,作用相当于f = d1(d2(f)),代码如下:

@d1
@d2
def f():
	print('f')

完善装饰器

虽然装饰器可以让我们的代码更加优雅,减少重复,不需要对代码进行修改,但是也会带来一些问题,最常见的就是错误的函数签名和文档,我们在一开始的装饰器的例子中,对target.__name__的结果进行了查看,其输出结果不再是target,而是fn(大家可以拉到上面再去看一下),这就是使用装饰器所带来的问题,装饰器会遮盖被装饰函数的__name__,__doc__等属性,也就是对原函数会输出替换后的函数的__name__,__doc__等属性,这对于我们去查看函数说明时会带来不少的麻烦。我们想看target函数的名称或说明,你却给我输出fn函数的名称或说明,这可不行,不能给你糖吃了你就听它的话了,就跟着它走了,我们还得把你给拽回来,所以我们可以将装饰器中的函数的__name__,__doc__等属性替换为被装饰函数的属性,见如下代码

def call_func(f):  # 装饰器函数,f是被装饰的函数
    def fn():
        return f
    fn.__name__ = f.__name__
    fn.__doc__ = f.__doc__
    return fn
    
def target():
    print('running target()')
    
target = call_func(target)
target.__name__

输出为:

'target'

可见我们将target.__name__原来的属性还给拽了回来,在这里面我们需要知道被装饰的函数装饰后的函数名是什么,并对其进行修改(在该层函数的外围进行修改,return之前)。

但是每次都通过写这样的语句来进行属性的修改,未免有些过于麻烦,当然Python也考虑到了这样,因此Python提供了functools.wraps装饰器把被装饰函数的相关属性从原来的函数复制到装饰器中定义的函数中,上述代码可修改为:

import functools
def call_func(f):  # 装饰器函数,f是被装饰的函数
    @functools.wraps(f)
    def fn():
        return f
    return fn
    
@call_func
def target():
    print('running target()')
    
target.__name__

内置的一些装饰器

Python中内置了三个用于装饰方法的函数:property、classmethod、staticmethod

@classmethod

在类class中定义的全部是实例方法,实例方法的第一个参数self是实例本身,要想在class中定义类方法,需要用到@classmethod,从而将该方法绑定到我们定义的类上,而非类的实例,即定义操作类,而不是操作实例的方法,也就是说类可以直接操作该方法,见下例:

class Person(object):
    count = 0
    @classmethod
    def how_many(cls):
        return cls.count
    def __init__(self, name):
        self.name = name
        Person.count = Person.count + 1
 
Person.how_many()  # 类直接调用,输出为 0
Person.how_many()  # 由于没有实例化,所以例中的属性不会加1,输出仍然为 0
p1 = Person('Bob')        
Person.how_many()  # 由于实例化了,故count属性加1,输出为 1

@classmethod改变了调用方法的方式,因此类构造的第一个参数是类本身,而不是实例,通常将参数名命名为cls(cls不可省略!!!),上面的cls.count实际上相当于Person.count因为是在类上调用,而非实例上调用,因此类方法无法获得任何实例变量,只能获得类的引用,也就是说类方法只能使用类中的属性,实例化传入的参数无法使用。

通常在爬虫框架Scrapy中数据库的操作中会用到该装饰方法:

class MongoPipeline(object):
    def __init__(self, mongo_uri, mongo_db):
        self.mongo_uri = mongo_uri
        self.mongo_db = mongo_db

    @classmethod
    def from_crawler(cls, crawler):
        return cls( 
              mongo_uri = crawler.settings.get('MONGO_URI'), mongo_db = crawler.settings.get('MONGO_DB')
            )

@staticmethod

@staticmethod装饰器也会改变方法的调用方式,但是第一个参数不是特殊的值,其实staticmethod是一种普通函数,位于类定义的命名空间中,不会对任何实例类型进行操作,见如下例子:

class Static(object):
    @staticmethod
    def sta():
        print('running staticmethod')
    def __init__(self, name):
        pass

Static.sta()  # 输出为 'running staticmethod'

当我们编写类时需要采用很多不同的方式来创建实例,而我们只有一个__init__函数,此时staticmethod就派上用场了,如下(参考文章):

import time
class Date:
    def __init__(self, year, month, day):
        self.year = year
        self.month = month
        self.day = day
        
    @staticmethod
    def now(): # 用Date.now()的形式去产生实例,该实例用的是当前时间
        t = time.localtime() #获取结构化的时间格式
        return Date(t.tm_year, t.tm_mon, t.tm_mday) #新建实例并且返回
        
    @staticmethod
    def tomorrow(): # 用Date.tomorrow()的形式去产生实例,该实例用的是明天的时间
        t = time.localtime(time.time() + 86400)
        return Date(t.tm_year, t.tm_mon, t.tm_mday)

a = Date('1987',11,27)    # 自己定义时间
b = Date.now()            # 采用当前时间
c = Date.tomorrow()       # 采用明天的时间

我们来比较一下classmethodstaticmethod的区别,见如下代码:

class Compare:
    def __init__(self, name):
        self.name = name
        
    @classmethod
    def clsmethod(cls):
        name = 'compare function'
        return cls(name)
    
    @staticmethod
    def stamethod():
        name = 'compare function'
        return Compare(name)
        
class Son_compare(Compare):
    def __str__(self):
        return '%s' % self.name

print(Son_compare.stamethod())
print(Son_compare.clsmethod())

<__main__.Compare object at 0x000002182952BDD8>
compare function

Son_compare.stamethod()输出的是目标地址,并没有得到我们预期的输出的字符串,这是因为我们虽然在Son_compare中定义了__str__,但是在调用Son_compare.stamethod()时,又去创建了Compare对象,所以Son_compare中的__str__不会被触发;而Son_compare.clsmethod()中创建的时Son_compare对象,因此能够输出我们想要的结果。

由此我们可以发现使用stamethod会带来一定的缺点,就是其采用硬编码的方式,当我们修改一个类的名称时,其相应的stamethod的类名称也需要修改,而不同于clsmethod中用cls来代替了我们所修改的类名。对于上例,若需要stamethod的输出与clsmethod的输出相同,只需要将stamethod()中的Compare修改为Son_compare即可。

@property

@property用来装饰读值方法
首先见如下代码:

class Student(object):
    def __init__(self, name, score):
        self.name = name
        self.__score = score
        
    def get_score(self):
        return self.__score
        
    def set_score(self, score):
        if score < 0 or score > 100:
            raise ValueError('invalid score')
        self.__score = score

上述代码可以实现学生分数的修改,并且当输入的分数小于0或大于100时,报错‘无效分数’,我们对上述代码进行如下操作:

s = Student('Bob', 59)
s.get_score()   # 输出为 59
# 将该学生的成绩进行修改
s.set_score(99)
s.get_score()   # 输出为 99
# 将该学生的成绩修改为1000,将会报错
s.set_score(1000)
s.get_score()
# ValueError: invalid score

但是通过这样调用函数的方式来得到和修改学生的score属性,显得比较麻烦,所以这个时候可以使用property装饰器来把get/set方法装饰成属性调用,也就是我们可以直接使用s.score来进行读取和设置,上述代码使用property装饰器后为:

class Student(object):
    def __init__(self, name, score):
        self.name = name
        self.__score = score
        
    @property
    def score(self):
        return self.__score
        
    @score.setter
    def score(self, score):
        if score < 0 or score > 100:
            raise ValueError('invalid score')
        self.__score = score
    
s = Student('xiaoming', 59)
s.score   # 输出为 59
s.score = 99   # 将学生成绩修改为 99
s.score   # 输出为 99
s.score = 1000   # 将学生成绩修改为 99
s.score   # 输出为 ValueError: invalid score

上述代码明显比第一种方法方便的多。第一个score(self)get方法,用@property装饰,第二个score(self, score)set方法,用@score.setter装饰,@score.setter是前一个@property装饰后的副产品。

关于Python装饰器的学习暂时学到这么多,以后学到新内容的再过来补充。

博客新人,码Python知识点比之前的实例代码烦多了,敲了6h(泪奔::>_<::),不过值得的,把装饰器自己梳理了一遍,掌握的更加牢固了。

得滚去看论文了,再不看的话导师就要约谈了。

编程快乐!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值