python-装饰器详解

1. 什么是装饰器

首先要说明的是,装饰器本身是一个函数,它接收其他函数作为参数,并将其以一个新的修改后的函数进行替换.
在汉语词典中,对装饰的解释是在身体或物体的表面加些附属的东西,使美观.在 python 中,装饰器顾名思义,就是为 python 中的函数附加一些附属的东西,比如说插入日志,性能测试,参数检查,权限校验等等.这些附属的东西由装饰器进行实现,而 Python 中的其他函数只关心它自身的核心逻辑就好, 不会被装饰器所影响.

2. 创建一个装饰器

首先我们实现一个最简单的装饰器,它什么都不做,只返回原函数.

# -*- coding: utf-8 -*-
"""
create time : 2018-11-08 21:44:52
author : sk
"""

def identity(f):
    return f

@identity
def foo():
    return "hello world"

if __name__ == "__main__":
    print(foo())

在这里,我们定义了一个函数 foo(), 它很简单,返回伟大的 “hello world” 字符串.然后我们定义了一个装饰器identity(), 它也很简单,接受一个函数,然后返回这个函数.注意,装饰器接受的是一个函数. @identity 这个东西,是 python 给到我们的语法糖,让我们便于书写.如果将这个语法糖翻译成我们习惯的样子,会是如下的形式.

# -*- coding: utf-8 -*-
"""
create time : 2018-11-08 21:44:52
author : sk
"""


def identity(f):
    return f

def foo():
    return "hello world"

if __name__ == "__main__":
    foo = identity(foo)
    print(foo())

注意,我们把 identity() 这个东东转换成了代码 foo = identity(foo),很好理解,就是将 foo函数作为参数传递给装饰器(就是一个函数)identity,然后再指向foo,相当于装饰器identity将函数 foo 包装了一层,但是 foo 函数的核心逻辑不变,仍然返回伟大的 “hello world”.

3. 装饰器示例-参数检查

在第一节中,我们提到了装饰器的一些功能,比如插入日志,性能测试,参数检查,权限校验等等.现在,我们以参数检查为例,说明一下装饰器的作用.
考虑一个场景,你往银行卡里面取钱和存钱.一般的逻辑是,首先检查你输入的账号和密码是否正确,正确之后,才能进行取钱和存钱的操作.转换成 python 代码如下.

# -*- coding: utf-8 -*-
"""
create time : 2018-11-08 22:25:01
author : sk
"""

class Store(object):
    def __init__(self):
        self.your_money = 10000
        
    def get_money(self, user, passwd, money):
        if user != "your-name" and passwd != "your-passwd":
            raise Exception("账号密码不对,无法取钱")
        self.your_money -= money
        return money
    
    def put_money(self, user, passwd, money):
        if user != "your-name" and passwd != "your-passwd":
            raise Exception("账号密码不对,无法取钱")
        self.your_money += money
        return "Success"

上面的代码中,我们创建了一个类 Store.如果你实例化一个 Store(即 store = Store()),会创建一个你的银行卡,里面有 10000 块钱.然后你取钱和存钱的时候,有两行检测参数的代码.这样看的话,代码很冗余拖沓,一点也不美观.我们如何进行优化呢?
首先,很容易想到,我们将这两行代码抽离出来,做成一个通用的检测参数的逻辑函数,这样,会美观一点.

# -*- coding: utf-8 -*-
"""
create time : 2018-11-08 22:25:01
author : sk


"""

def check_user(user, passwd):
    if user != "your-name" and passwd != "your-passwd":
        raise Exception("账号密码不对,无法取钱")

class Store(object):
    def __init__(self):
        self.your_money = 10000
        
    def get_money(self, user, passwd, money):
        check_user(user, passwd)
        self.your_money -= money
        return money

    def put_money(self, user, passwd, money):
        check_user(user, passwd)
        self.your_money += money
        return "Success"

这样看起来美观一点了.
如果让它更简洁美观呢?
我们可以让装饰器同学登场了.

# -*- coding: utf-8 -*-
"""
create time : 2018-11-08 22:25:01
author : sk


"""

def check_user(f):
    def wrapper(*args, **kwargs):
        if kwargs.get("user") != "your-name" and kwargs.get("passwd") != "your-passwd":
            raise Exception("账号密码不对,无法取钱")
        return f(*args, **kwargs)
    return wrapper

class Store(object):
    def __init__(self):
        self.your_money = 10000

    @check_user
    def get_money(self, user, passwd, money):
        self.your_money -= money
        return money

    @check_user
    def put_money(self, user, passwd, money):
        self.your_money += money
        return "Success"

if __name__ == "__main__":
    store = Store()
    # 下面两行会 raise Exception
    # money = store.get_money(user="jia-de", passwd="jia-de", money=1000)
    # print(money)
    # 下面两行会返回 1000
    money = store.get_money(user="your-name", passwd="your-passwd", money=1000)
    print(money)

这样看起来代码整洁规范了很多.
有两点需要说明一下.

  1. 装饰器里面又包裹了一个函数.这个是叫做 函数柯里化.懵逼中…
  2. *args, **kwargs 是python里面的传递参数的两种方式. *args 用来将参数打包成tuple给函数体调用. **kwargs 打包关键字参数成dict给函数体调用.

4. 上述装饰器的缺陷以及改进

4.1 缺陷

正如前面所提到的,装饰器会用一个动态创建的新函数替换原来的。然而,新函数缺少很多原函数的属性,如 docstring 和 名字。

"""
create time : 2018-11-08 21:44:52
author : sk
"""

def identity(f):
    print("hello identity")
    def wrapper(*args, **kwargs):
        return f(*args, **kwargs)
    return wrapper

@identity
def foo(name="python"):
    """ docstring test
    this is a test string
    """
    return name

if __name__ == "__main__":
    print(foo.__doc__)
    print(foo.__name__)

上述代码中,返回的结果是

hello identity
None
wrapper

4.2 改进措施

幸好,python 内置的 functools模块通过装饰器 wraps 解决了这个问题。它会复制这些属性给到这个包装器本身。

"""
create time : 2018-11-08 21:44:52
author : sk
"""

import functools

def identity(f):
    print("hello identity")
    @functools.wraps(f)
    def wrapper(*args, **kwargs):
        return f(*args, **kwargs)
    return wrapper

@identity
def foo(name="python"):
    """ docstring test
    this is a test string
    """
    return name

if __name__ == "__main__":
    print(foo.__doc__)
    print(foo.__name__)

此时,程序的输出是我们预想的那样。

hello identity
 docstring test
    this is a test string
    
foo
  • ps: functools 还提供了一个 update_wrapper 的函数来解决这个问题,但是我这边验证下来有错误。可能是俺调用的姿势不对。

5. 装饰器类

我们也可以将装饰器做成一个类的形式。如下所示。

"""
create time : 2018-11-08 21:44:52
author : sk
"""

import functools


class Identitf(object):
    def __init__(self):
        pass
    
    def __call__(self, f):
        print("hello identity")
        @functools.wraps(f)
        def wrapper(*args, **kwargs):
            return f(*args, **kwargs)
        return wrapper

@Identitf()
def foo(name="python"):
    """ docstring test
    this is a test string
    """
    return name

if __name__ == "__main__":
    print(foo.__doc__)
    print(foo.__name__)

使用类来实现一个装饰器的好处是 : 可以扩展功能

  1. 我们可以在类里面实现各种方法,来扩展装饰器的功能。
  2. 我们可以实现一个子类来继承父类,从而扩展装饰器的功能。

6. python 中的一些装饰器

6.1 静态方法 @ staticmethod

静态方法是属于类的方法,但是实际上并非运行在类的实例上。静态方法是指类中无需实例参与即可调用的方法(不需要self参数),在调用过程中,无需将类实例化,直接在类之后使用.号运算符调用方法。在一个类中实现静态方法需要用 @staticmethod 装饰器。

"""
create time : 2018-11-08 21:44:52
author : sk
"""
class MathTest(object):
    @staticmethod
    def add(x,y):
        return x+y
    
    def test(self, info):
        return info
    
if __name__ == "__main__":
    print(MathTest.add(2,3))
    print(MathTest().add(2,4))
    print(MathTest().test("hello"))
    print(MathTest.test("hello"))

输出的结果如下:

5
6
hello
Traceback (most recent call last):
... ...
TypeError: test() missing 1 required positional argument: 'info'

前面三个结果正常输入。而最后一个 print , 由于没有实例化对象,所以报错。而 add 是静态方法,所以第二个 print 没有报错。

6.2 类方法 @classmethod

类方法是直接绑定到类而非它的实例的方法。类方法在Python中使用比较少,类方法传入的第一个参数为cls,是类本身。并且,类方法可以通过类直接调用,或通过实例直接调用。但无论哪种调用方式,最左侧传入的参数一定是类本身。

"""
create time : 2018-11-08 21:44:52
author : sk
"""


class MathTest(object):
    @classmethod
    def add(cls, x,y):
        return x+y
    
    def test(self, info):
        return info
    
if __name__ == "__main__":
    print(MathTest.add)
    print(MathTest().add)
    print(MathTest.add(2,3))

返回的结果是

<bound method MathTest.add of <class '__main__.MathTest'>>
<bound method MathTest.add of <class '__main__.MathTest'>>
5

6.3 抽象方法 @abc.abstractmethod

抽象方法是定义在基类中可能有或没有实现的方法

class Pizza(object):
	@staticmethod
	def test():
		raise NotImplementedError

也可以使用 abc 模块来实现抽象方法。

import abc


class Pizza(object):
	@abc.abstractmethod
	def test():
		pass
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值