绑定完请刷新页面
取消
刷新

分享好友

×
取消 复制
Python进阶: 通过实例详解装饰器(附代码)
2020-06-19 10:12:12
Python中的装饰器有很多用处,比如输出日志、参数检查、代理设置、计数计时、结果缓存等等。本文就通过几个装饰器例子,详细解释一下Python中装饰器的用法。
  • 一步步从简到繁学习装饰器用法
  • 其他一些装饰器实例
  • Python中自带的装饰器

按照惯例,先上代码:GitHub - xianhu/LearnPython: 以撸代码的形式学习Python

一步步从简到繁学习装饰器用法

(1)简单的装饰器,实现日志输出功能:

# 构建装饰器
def logging(func):
    @functools.wraps(func)
    def decorator():
        print("%s called" % func.__name__)
        result = func()
        print("%s end" % func.__name__)
        return result
    return decorator

# 使用装饰器
@logging
def test01():
    return 1

# 测试用例
print(test01())
print(test01.__name__)

代码很简单,很容易看懂。这里注意"functools.wraps"用法,其目的是"test01.__name__"输出正确的"test01"。"@logging"相当于"test01 = logging(test01)",返回的是decorator函数,所以如果不加"functools.wraps",则"test01.__name__"返回为"decorator"。

注意,此时test01没有参数,对于带有参数的函数,logging装饰器则不再适用。那么如果想装饰带有参数的函数,装饰器该怎么写呢?

(2)装饰器传入函数参数,并正确返回结果:

# 构建装饰器
def logging(func):
    @functools.wraps(func)
    def decorator(a, b):
        print("%s called" % func.__name__)
        result = func(a, b)
        print("%s end" % func.__name__)
        return result
    return decorator

# 使用装饰器
@logging
def test01(a, b):
    print("in function test01, a=%s, b=%s" % (a, b))
    return 1

# 测试用例
print(test01(1, 2))

这里的test01函数带有参数a、b,那么decorator函数带有同样的参数即可。那么问题又来了,如何让logging装饰器更加通用,而不是只装饰参数为两个的函数呢?这时候自然想到Python中的 * 和 ** 的用法。

# 构建装饰器
def logging(func):
    @functools.wraps(func)
    def decorator(*args, **kwargs):
        print("%s called" % func.__name__)
        result = func(*args, **kwargs)
        print("%s end" % func.__name__)
        return result
    return decorator

# 使用装饰器
@logging
def test01(a, b):
    print("in function test01, a=%s, b=%s" % (a, b))
    return 1

# 使用装饰器
@logging
def test02(a, b, c=1):
    print("in function test02, a=%s, b=%s, c=%s" % (a, b, c))
    return 1

# 测试用例
print(test01(1, 2))
print(test02(1, 2, c=3, d=4))

此时对于任意参数的函数,logging都可以进行装饰。但是注意,logging装饰器是不带参数的,那么装饰器可以带参数吗?当然可以,我们换个例子:参数检查。

(3)构建带有参数的装饰器,并正确返回结果:

# 构建装饰器
def params_chack(a_type, b_type):
    def _outer(func):
        @functools.wraps(func)
        def _inner(a, b):
            assert isinstance(a, a_type) and isinstance(b, b_type)
            return func(a, b)
        return _inner
    return _outer

# 使用装饰器
@params_chack(int, (list, tuple))
def test03(a, b):
    print("in function test03, a=%s, b=%s" % (a, b))
    return 1

# 测试用例
print(test03(1, [2, 3]))   # 参数正确
print(test03(1, 2))        # 参数错误

从代码可以看出,实际上就是在原有装饰器的基础上,外层又加了一层包装。params_check装饰器的作用是限制个参数为a_type,第二个参数为b_type。类似于(2),这里如何让装饰器更加通用,而不是只装饰参数为两个的函数呢?这里又一次想到Python中的 * 和 **。


# 构建装饰器
def params_chack(**, **kw*):
    def _outer(func):
        @functools.wraps(func)
        def _inner(*args, **kwargs):
            result = [isinstance(_param, _type) for _param, _type in zip(args, *)]
            assert all(result), "params_chack: invalid parameters"
            result = [isinstance(kwargs[_param], kw*[_param]) for _param in kwargs if _param in kw*]
            assert all(result), "params_chack: invalid parameters"
            return func(*args, **kwargs)
        return _inner
    return _outer

# 使用装饰器
@params_chack(int, str, c=(int, str))
def test04(a, b, c):
    print("in function test04, a=%s, b=%s, c=%s" % (a, b, c))
    return 1

# 测试用例
print(test04(1, "str", 1))         # 参数正确
print(test04(1, "str", "abc"))     # 参数正确
print(test04("str", 1, "abc"))     # 参数错误

此时params_check装饰器不但能够传入任意个数的参数,而且支持K-V形式的参数传递。

(4)使用装饰器装饰类中的函数,比较简单,直接看代码。注意此时个参数为self本身:


# 使用装饰器
class ATest(object):
    @params_chack(object, int, str)
    def test(self, a, b):
        print("in function test of ATest, a=%s, b=%s" % (a, b))
        return 1

# 测试用例
a_test = ATest()
a_test.test(1, "str")    # 参数正确
a_test.test("str", 1)    # 参数错误

(5)多个装饰器同时装饰一个函数,也比较简单,直接看代码:

# 使用装饰器
@logging
@params_chack(int, str, (list, tuple))
def test05(a, b, c):
    print("in function test05, a=%s, b=%s, c=%s" % (a, b, c))
    return 1

# 测试用例
print(test05(1, "str", [1, 2]))        # 参数正确
print(test05(1, "str", (1, 2)))        # 参数正确
print(test05(1, "str", "str1str2"))    # 参数错误

(6)将装饰器写为类的形式,即“装饰器类”。此时对于装饰器类的要求是必须是可被调用的,即必须实现类的__call__方法。直接上代码:

# 构建装饰器类
class Decorator(object):
    def __init__(self, func):
        self.func = func
        return

    def __call__(self, *args, **kwargs):
        print("%s called" % self.func.__name__)
        result = self.func(*args, **kwargs)
        print("%s end" % self.func.__name__)
        return result

# 使用装饰器
@Decorator
def test06(a, b, c):
    print("in function test06, a=%s, b=%s, c=%s" % (a, b, c))
    return 1

# 测试用例
print(test06(1, 2, 3))
这里的装饰器类的构造函数中传入func,使其能在__call__方法中被调用。同时这里的装饰器类并没有带有参数,实现不了类似于参数检查的功能。类似于上边的思路,我们这里也可以构建带有参数的装饰器类,还是以参数检查为例:
# 构建装饰器类
class ParamCheck(object):

    def __init__(self, **, **kw*):
        self.* = *
        self.kw* = kw*
        return

    def __call__(self, func):
        @functools.wraps(func)
        def _inner(*args, **kwargs):
            result = [isinstance(_param, _type) for _param, _type in zip(args, self.*)]
            assert all(result), "params_chack: invalid parameters"
            result = [isinstance(kwargs[_param], self.kw*[_param]) for _param in kwargs if _param in self.kw*]
            assert all(result), "params_chack: invalid parameters"
            return func(*args, **kwargs)
        return _inner

# 使用装饰器
@ParamCheck(int, str, (list, tuple))
def test07(a, b, c):
    print("in function test06, a=%s, b=%s, c=%s" % (a, b, c))
    return 1

# 测试用例
print(test07(1, "str", [1, 2]))    # 参数正确
print(test07(1, "str", (1, 2)))    # 参数正确
print(test07(1, 2, (1, 2)))        # 参数错误

其他一些装饰器实例

函数缓存:一个函数的执行结果可以被缓存在内存中,下次再次调用时,可以先查看缓存中是否存在,如果存在则直接返回缓存中的结果,否则返回函数调用结果。这种装饰器比较适合装饰过程比较复杂或耗时的函数,比如数据库查询等。

# 实例: 函数缓存
def funccache(func):
    cache = {}

    @functools.wraps(func)
    def _inner(*args):
        if args not in cache:
            cache[args] = func(*args)
        return cache[args]
    return _inner

# 使用装饰器
@funccache
def test08(a, b, c):
    # 其他复杂或耗时计算
    return a + b + c

还有很多其他例子,比如函数调用计数、函数计时、函数自动重试等,思路都基本相同,这里就不一一列举了。



Python中自带的装饰器

Python中自带有三个和class相关的装饰器:@staticmethod、@classmethod 和@property。

(1)先看@property,可以将其理解为“将类方法转化为类属性的装饰器”。先看实例:

# 使用Python自带的装饰器
class People(object):

    def __init__(self):
        self._name = None
        self._age = None
        return

    @property
    def name(self):
        return self._name

    @name.setter
    def name(self, name):
        self._name = name
        return

    @property
    def age(self):
        return self._age

    @age.setter
    def age(self, age):
        assert  < age < 120
        self._age = age
        return

p = People()
p.name = "tom"          # 设置name
p.age = 12              # 设置age
print(p.name, p.age)    # 输出name和age
p.age = 120             # 设置age, 此时认为120为异常数据

这里定义一个People类,有两个属性name和age。当我们声明了实例p,使用p操作name和age时,实际上是调用的name、age方法,此时会做参数检查等工作。@property将name方法转化为属性,同时当对该属性进行赋值时,会自动调用@name.setter将下边的name方法。

@property有.setter、.getter和.deleter三中装饰器,分别对应赋值、取值和删除三种操作。

(2)@staticmethod 将类成员方法声明为类静态方法,类静态方法没有 self 参数,可以通过类名或类实例调用。

(3)@classmethod 将类成员方法声明为类方法,类方法所接收的个参数不是self,而是cls,即当前类的具体类型。

静态方法和类方法都比较简单,一个简单的例子解释静态方法和类方法:

# 类静态方法和类方法
class A(object):
    var = 1

    def func(self):
        print(self.var)
        return

    @staticmethod
    def static_func():
        print(A.var)
        return

    @classmethod
    def class_func(cls):
        print(cls.var)
        cls().func()
        return

=============================================================


作者主页:笑虎(Python爱好者,关注爬虫、数据分析、数据挖掘、数据可视化等)

作者专栏主页:撸代码,学知识 - 知乎专栏

作者GitHub主页:撸代码,学知识 - GitHub

欢迎大家拍砖、提意见。相互交流,共同进步!

==============================================================

分享好友

分享这个小栈给你的朋友们,一起进步吧。

人生苦短,不如学Python
创建时间:2020-06-18 16:48:21
Python是一种跨平台的计算机程序设计语言。 是一个高层次的结合了解释性、编译性、互动性和面向对象的脚本语言。初被设计用于编写自动化脚本(shell),随着版本的不断更新和语言新功能的添加,越多被用于独立的、大型项目的开发。
展开
订阅须知

• 所有用户可根据关注领域订阅专区或所有专区

• 付费订阅:虚拟交易,一经交易不退款;若特殊情况,可3日内客服咨询

• 专区发布评论属默认订阅所评论专区(除付费小栈外)

技术专家

查看更多
  • liuxuhui
    专家
戳我,来吐槽~