---
title: '《Python有什么好学的》之修饰器'
pubDate: 2018-11-07
tags:
  - 'python'
description: '《Python有什么好学的》之修饰器'
---

“Python有什么好学的”这句话可不是反问句，而是问句哦。

主要是煎鱼觉得太多的人觉得Python的语法较为简单，写出来的代码只要符合逻辑，不需要太多的学习即可，即可从一门其他语言跳来用Python写（当然这样是好事，谁都希望入门简单）。

于是我便记录一下，如果要学Python的话，到底有什么好学的。记录一下Python有什么值得学的，对比其他语言有什么特别的地方，有什么样的代码写出来更Pythonic。一路回味，一路学习。

## 什么是修饰器，为什么叫修饰器

修饰器英文是Decorator，

我们假设这样一种场景：古老的代码中有几个很是复杂的函数F1、F2、F3...，复杂到看都不想看，反正我们就是不想改这些函数，但是我们需要改造加功能，在这个函数的前后加功能，这个时候我们很容易就实现这个需求：

```python
def hi():
    """hi func，假装是很复杂的函数"""
    return 'hi'

def aop(func):
    """aop func"""
    print('before func')
    print(func())
    print('after func')

if __name__ == '__main__':
    aop(hi)
```

![](https://source.mkshell.com/15413391330336.jpg)

以上是很是简单的实现，利用Python参数可以传函数引用的特性，就可以实现了这种类似AOP的效果。

这段代码目前没有什么问题，接下来煎鱼加需求：需求为几十个函数都加上这样的前后的功能，而所有调用这些函数地方也要相应地升级。

看起来这个需求比较扯，偏偏这个需求却是较为广泛：在调用函数的前后加上log输出、在调用函数的前后计算调用时间、在调用函数的前后占用和释放资源等等。

一种比较笨的方法就是，为这几十个函数逐一添加一个入口函数，针对a函数添加一个a_aop函数，针对b函数添加一个b_aop函数...如此这样。问题也很明显：

1. 工作量大
2. 代码变得臃肿复杂
3. 原代码有多处调用了这些函数，可以会升级不完全

于是接下来有请修饰器出场，修饰器可以统一地给这些函数加这样的功能：

```python
def aop(func):
    """aop func"""
    def wrapper():
        """wrapper func"""
        print('before func')
        func()
        print('after func')
    return wrapper

@aop
def hi():
    """hi func"""
    print('hi')

@aop
def hello():
    """hello func"""
    print('hello')

if __name__ == '__main__':
    hi()
    hello()
```

![](https://source.mkshell.com/15413396962142.jpg)

以上aop函数就是修饰器的函数，使用该修饰器时只要在待加函数上一行加`@修饰器函数名`即可，如实例代码中就是`@aop`。

加上了`@aop`后，调用新功能的hi函数就喝原来的调用一样：就是`hi()`而不是`aop(hi)`，也意味着所有调用这些函数的地方不需要修改就可以升级。

简单地来说，大概修饰器就是以上的这样子。

## @是个什么

对于新手来说，上面例子中，`@`就是一样奇怪的东西：为什么这样子用就可以实现煎鱼需求的功能了。

其实我们还可以不用`@`，煎鱼换一种写法：

```python
def hi():
    """hi func"""
    print('hi')

def aop(func):
    """aop func"""
    def wrapper():
        """wrapper func"""
        print('before func')
        func()
        print('after func')
    return wrapper

if __name__ == '__main__':
    hi()

    print('')

    hi = aop(hi)
    hi()
```

![](https://source.mkshell.com/15413411757415.jpg)

上面的例子中的aop函数就是之前说过的修饰器函数。

如例子main函数中第一次调用hi函数时，由于hi函数没叫修饰器，因此我们可以从输出结果中看到程序只输出了一个hi而没有前后功能。

然后煎鱼加了一个`hi = aop(hi)`后再调用hi函数，得到的输出结果和加修饰器的一样，换言之：

`@aop` 等效于`hi = aop(hi)`

因此，我们对于`@`，可以理解是，它通过闭包的方式把新函数的引用赋值给了原来函数的引用。

有点拗口。`aop(hi)`是新函数的引用，至于返回了引用的原因是aop函数中运用闭包返回了函数引用。而`hi`这个函数的引用，本来是指向旧函数的，通过`hi = aop(hi)`赋值后，就指向新函数了。

## 被调函数加参数

以上的例子中，我们都假设被调函数是无参的，如hi、hello函数都是无参的，我们再看一眼煎鱼刚才的写的修饰器函数：

```python
def aop(func):
    """aop func"""
    def wrapper():
        """wrapper func"""
        print('before func')
        func()
        print('after func')
    return wrapper
```

很明显，闭包函数wrapper中，调用被调函数用的是`func()`，是无参的。同时就意味着，如果func是一个带参数的函数，再用这个修饰器就会报错。

```python
@aop
def hi_with_deco(a):
    """hi func"""
    print('hi' + str(a))

if __name__ == '__main__':
    # hi()
    hi_with_deco(1)
```

![](https://source.mkshell.com/15413426217736.jpg)

就是参数的问题。这个时候，我们把修饰器函数改得通用一点即可，其中import了一个函数（也是修饰器函数）：

```python
from functools import wraps

def aop(func):
    """aop func"""
    @wraps(func)
    def wrap(*args, **kwargs):
        print('before')
        func(*args, **kwargs)
        print('after')

    return wrap

@aop
def hi(a, b, c):
    """hi func"""
    print('test hi: %s, %s, %s' % (a, b, c))

@aop
def hello(a, b):
    """hello func"""
    print('test hello: %s, %s' % (a, b))

if __name__ == '__main__':
    hi(1, 2, 3)
    hello('a', 'b')
```

![](https://source.mkshell.com/15413427947406.jpg)

这是一种很奇妙的东西，就是在写修饰器函数的时候，还用了别的修饰器函数。那也没什么，毕竟修饰器函数也是函数啊，有什么所谓。

## 带参数的修饰器

思路到了这里，煎鱼不禁思考一个问题：修饰器函数也是函数，那函数也是应该能传参的。函数传参的话，不同的参数可以输出不同的结果，那么，修饰器函数传参的话，不同的参数会怎么样呢？

其实很简单，修饰器函数不同的参数，能生成不同的修饰器啊。

如，我这次用这个修饰器是把时间日志打到`test.log`，而下次用修饰器的时候煎鱼希望是能打到`test2.log`。这样的需求，除了写两个修饰器函数外，还可以给修饰器加参数选项：

```python
from functools import wraps

def aop_with_param(aop_test_str):
    def aop(func):
        """aop func"""
        @wraps(func)
        def wrap(*args, **kwargs):
            print('before ' + str(aop_test_str))
            func(*args, **kwargs)
            print('after ' + str(aop_test_str))
        return wrap
    return aop

@aop_with_param('abc')
def hi(a, b, c):
    """hi func"""
    print('test hi: %s, %s, %s' % (a, b, c))

@aop_with_param('pppppp')
def hi2(a, b, c):
    """hi func"""
    print('test hi: %s, %s, %s' % (a, b, c))

if __name__ == '__main__':
    hi(1, 2, 3)
    print('')
    hi2(2, 3, 4)
```

![](https://source.mkshell.com/15413437338294.jpg)

同样的，可以加一个参数，也可以加多个参数，这里就不说了。

## 修饰器类

大道同归，逻辑复杂了之后，人们都喜欢将**函数**的思维层面抽象上升到**对象**的层面。原因往往是对象能拥有多个函数，对象往往能管理更复杂的业务逻辑。

显然，修饰器函数也有对应的修饰器类。写起来也没什么难度，和之前的生成器一样简单：

```python
from functools import wraps

class aop(object):
    def __init__(self, aop_test_str):
        self.aop_test_str = aop_test_str

    def __call__(self, func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            print('before ' + self.aop_test_str)
            func()
            print('after ' + self.aop_test_str)

        return wrapper

@aop('pppppp')
def hi():
    print('hi')
```

看得出来，这个修饰器类也不过是多了个`__call__`函数，而这个`__call__`函数的内容和之前写的修饰器函数一个样！而使用这个修饰器的方法，和之前也一样，一样的如例子中的`@aop('pppppp')`。

甚至，煎鱼过于无聊，还试了一下继承的修饰器类：

```python
class sub_aop(aop):
    def __init__(self, sub_aop_str, *args, **kwargs):
        self.sub_aop_str = sub_aop_str
        super(sub_aop, self).__init__(*args, **kwargs)

    def __call__(self, func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            print('before ' + self.sub_aop_str)
            super(sub_aop, self).__call__(func)()
            print('after ' + self.sub_aop_str)
        return wrapper

@sub_aop('ssssss', 'pppppp')
def hello():
    print('hello')

if __name__ == '__main__':
    hello()
```

你们猜猜结果怎么样？

_先这样吧_

若有错误之处请指出，更多地请关注[造壳](https://www.mkshell.com)。
