目录
问题引出
使用方法
闭包是什么
装饰器的实现
看一段 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
下面分别讲上述接种常见装饰器的常见用法,这篇博客讲的也不错,可以参考看看
将一个方法伪装成属性,被修饰的特性方法,内部可以实现处理逻辑,但对外提供统一的调用方式,实现一个实例属性的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
设置某个只读属性,通常用在类内成员变量赋值防止修改
在上面的例子运行后,加一句 s.resolution = 1
报错 AttributeError: can't set attribute
class c:
@staticmethod
def f(*args, **kwargs):
pass
在类函数的前面加上@staticmethod就是定义了一个静态方法,不用实例化类 c 也可以调用函数 f,即 c.f()
类似于@staticmethod,也可以在不实例化类的情况下调用类内函数,区别是
翻译过来就是抽象方法,用在抽象类(虚类)的虚函数上;含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**
第三个实例化是正确的
"""
见下方,说明装饰器原理后叙述
以下的内容需要一些时间消化,需要耐心理解
笔者水平有限,推荐一个讲的比较通俗易懂的视频 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