目录

​问题引出​

​使用方法​

​闭包是什么​

​装饰器的实现​


看一段 mido 这个库的源代码 mido/midifiles/tracks.py

class MidiTrack(list):
@property
def name(self):
"""
doc...
"""
for message in self:
if message.type == 'track_name':
return message.name
else:
return ''

@name.setter
def name(self, name):
# Find the first track_name message and modify it.
for message in self:
if message.type == 'track_name':
message.name = name
return
else:
# No track name found, add one.
self.insert(0, MetaMessage('track_name', name=name, time=0))

这些 @ 符号是什么意思?

问题引出

定义类函数时会有大量相同的需求,比如给某个函数记录运行日志,设置某个变量只读,限制函数输入参数范围并在范围外抛出异常;

为了使得代码更简洁,把这种与函数本身的功能无关的功能集合起来,叫做装饰器,用@表示,把@叫做装饰器的语法糖;

装饰器把大量相同的工作省去了,使得程序更整洁(关注核心功能)

使用方法

在想要装饰的函数前写上 @xxx 即可

如上文中用 property 装饰了 name 这个函数

常用的装饰器有

@property

@xxx.name

@staticmethod

@classmethod

@abstractmethod

@wraps

下面分别讲上述接种常见装饰器的常见用法,​​这篇博客​​讲的也不错,可以参考看看

  • @property 与 @xxx.name 一起

将一个方法伪装成属性,被修饰的特性方法,内部可以实现处理逻辑,但对外提供统一的调用方式,实现一个实例属性的get,set,delete三种方法的内部逻辑。看一个例子

class Screen(object):

@property
def width(self):
return self._width

@width.setter
def width(self, value):
if value < 0:
raise ValueError('width must be a positive number.')
self._width = value

@property
def height(self):
return self._height

@height.setter
def height(self, value):
if value < 0:
raise ValueError('height must be a positive number.')
self._height = value

@property
def resolution(self):
return self.width * self.height


s = Screen()
s.height = 20
s.width = 40
print(s.resolution)

正确输出 height*width=800

  • 只有 @property

设置某个只读属性,通常用在类内成员变量赋值防止修改

在上面的例子运行后,加一句 s.resolution = 1

报错 AttributeError: can't set attribute

  • @staticmethod
class c:
@staticmethod
def f(*args, **kwargs):
pass

在类函数的前面加上@staticmethod就是定义了一个静态方法,不用实例化类 c 也可以调用函数 f,即 c.f() 

  • classmethod

类似于@staticmethod,也可以在不实例化类的情况下调用类内函数,区别是

  • abstracmethod

翻译过来就是抽象方法,用在抽象类(虚类)的虚函数上;含abstractmethod方法的类不能实例化,继承了含abstractmethod方法的子类必须复写所有abstractmethod装饰的方法,未被装饰的可以不重写;下面这个示例代码来自上面提到的CSDN博客链接

# -*- coding:utf-8 -*-
from abc import ABC, abstractmethod

class **A**(**ABC**):
@abstractmethod
def **test**(self):
pass

class **B**(**A**):
def **test_1**(self):
print("未覆盖父类abstractmethod")

class **C**(**A**):
def **test**(self):
print("覆盖父类abstractmethod")

if __name__ == '__main__':
a = A()
b = B()
c = C()

"""
前两个分别报错如下:
a = A()
TypeError: Can't instantiate abstract class A with abstract methods test
b = B()
TypeError: Can't instantiate abstract class **B** **with** **abstract** **methods** **test**
第三个实例化是正确的
"""
  • @wraps

见下方,说明装饰器原理后叙述

以下的内容需要一些时间消化,需要耐心理解

笔者水平有限,推荐一个讲的比较通俗易懂的视频 ​​Python的闭包与装饰器_哔哩哔哩_bilibili​


闭包是什么

闭包是一个普遍的概念,许多语言里都有

闭包:在作用域外(以特定方式)使用局部变量

复习一些概念

  • 变量有生命周期(作用域),C++中的作用域是{ },pyhton中的作用域是tab之间(比如 def 和 return 之间)
  • 离开作用域后无法访问到变量
def fun(*args, **kwargs):
a = 1

print(a)

# output: NameError: name 'a' is not defined

加入一些新概念

  • 函数也是一种变量
  • 所以函数里也能定义函数,叫嵌套函数
  • 函数的返回值可以是函数
def fun():
print("fun")
def fun_in():
print("fun_in")
return fun_in
return fun_in

var = fun() # 调用fun,初始化fun_in
var() # 调用fun_in

# output:
# fun
# fun_in

看到,在函数 fun 的外部实现了内部函数 fun_in 的调用,也就是在变量作用域外“访问”了 fun_in;

这是因为在调用 fun 的时候初始化了 fun_in ,并将该内部变量保存在 var 里;

此时 var = fun_in,所以调用 var 效果与 fun_in( ) 相同

再看一个例子

def fun():
a = 1
def fun_in(num):
return num + a
return fun_in

var = fun()
var(3)

# output: 4

正确输出了 a+3 = 4

在作用域外依然能使用局部变量 a ,也是因为这个 a 保存在 var 里了

但是直接 print(a) 会报错,因为 a 这个变量只存在于 var 中,只有 var 能(通过调用 fun_in 的方式)使用

装饰器的实现

那闭包和装饰器有什么关系?

再看下文章最开始的代码

class MidiTrack(list):
@property
def name(self):
...

在 pycharm 中 Ctrl + 鼠标点击 来到 @property 的声明处

class property(object):
"""
Property attribute. ...
"""
def __init__(self, fget=None, fset=None, fdel=None, doc=None):
# known special case of property.__init__
...

也就是说 property 也是一个类

@property 实际上就是初始化 property 这个类,也就是调用 __init__ 函数

装饰器 @xxx 的实质就是调用函数

在函数 def fun 前使用 @decorator 等同于 decorator(fun)

看下面一段例子,想给函数 real_fun 记录一段日志

import logging

def decorator(fun):
logging.basicConfig(level=logging.DEBUG,
format='%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s',
datefmt='%a, %d %b %Y %H:%M:%S',
filename='./test.log',
filemode='w')
def fun_in(*args, **kwargs):
logging.debug("{} will run.".format(fun.__name__))
fun(*args, **kwargs)
logging.debug("{} finished.".format(fun.__name__))

return fun_in

def real_fun():
print("real_fun")


real_fun = decorator(real_fun)
real_fun()
# output: real fun

当前文件夹下生成的日志文件 test.log

Thu, 19 Mar 2020 13:15:51 property_test.py[line:88] DEBUG real_fun will run.
Thu, 19 Mar 2020 13:15:51 property_test.py[line:90] DEBUG real_fun finished.

为了简洁美观,关注有更有价值的 real_fun 的功能

在函数 real_fun 前使用@decorator  与  real_fun = decorator(real_fun)   是一样的

把上例写成

import logging

def decorator(fun):
logging.basicConfig(level=logging.DEBUG,
format='%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s',
datefmt='%a, %d %b %Y %H:%M:%S',
filename='./test.log',
filemode='w')

def fun_in(*args, **kwargs):
logging.debug("{} will run.".format(fun.__name__))
fun(*args, **kwargs)
logging.debug("{} finished.".format(fun.__name__))

return fun_in

@decorator
def real_fun():
print("real_fun")


real_fun()

输出与日志记录内容相同

现在我们可以把 @decorator 之前所有内容放到另一个 .py 文件中,在 real_fun 所在文件调用装饰器,优雅地完成日志记录

最后来说前文提到的 @wraps

在完成上面的工作后,还有个问题

print(real_fun.__name__)
# output:fun_in

因为实际上调用是在 fun_in 内部发生的

为了 real_fun 的 __name__ 仍是 real_fun ,在内部函数 fun_in 之前使用 @wraps

现在代码是这样

import logging
from functools import wraps


def decorator(fun):
logging.basicConfig(level=logging.DEBUG,
format='%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s',
datefmt='%a, %d %b %Y %H:%M:%S',
filename='./test.log',
filemode='w')

@wraps(fun)
def fun_in():
logging.debug("{} will run.".format(fun.__name__))
fun()
logging.debug("{} finished.".format(fun.__name__))

return fun_in


@decorator
def real_fun():
print("real_fun")


real_fun()
print(real_fun.__name__)
# output: real_fun
# output: real_fun