Python Q&A: 装饰器
第一期的Python Q&A。
Question.
@ 在Python上是装饰器标识符。迄今为止我已经遇到了三个装饰器:
@dataclass,让需要接受大量初始化参数的类看上去更简洁;1 2 3 4 5 6 7 8 9 10 11
from dataclasses import dataclass @dataclass class Custom: # 这些没有默认值的属性需要实例化的时候提供 prop1: str prop2: str prop3: str # 之后照常实例化就好 custom = Custom(prop1="hello", prop2="from", prop3="python!")
@classmethod,定义类方法,以在实例化之前先定义其行为;1 2 3 4 5 6 7 8
class Custom: # ...... # 不同于实例方法,类方法可由实例化自该类(或实例化自继承它的类)的对象调用 # 你可以轻松地让不同对象实现同一名称的方法,还能确保互不干扰 @classmethod def fun_cls(cls, param1, param2): pass
@staticmethod,在类里定义一个不依赖于类的一个方法。1 2 3 4 5 6 7 8
class Custom: # ...... # 静态方法可以不依赖于类/对象本身 # 也就是说,你甚至可以不将 `cls`/`self` 作为必需参数 @staticmethod def fun_static(param1, param2): pass
由于我学的很零散,我并不清楚工程上是如何利用这些装饰器的,以及除了这些还有没有其他的,目的完全不同的装饰器。
Answer.
装饰器是 Python 非常棒的特性之一,个人感觉比那些“魔术方法”更“魔术”(因为“魔术方法”看上去既不直观也不魔术,但以后或许会说。或许吧……)
对于不太明白装饰器定义的人,我简单说一下:装饰器装饰的是函数,它能在不改动函数的基础上赋予函数特殊的超能力。(抛瓦!!!)
以后如果面对了这些工程问题,那么装饰器将非常有用。
例如给一个外部应用程序写一个“Warpper”(差不多就是“API”),某些程序可能需要十几个,二十几个参数,将它们堆到 __init__ 里,场面将极其壮观:
1
2
3
class Warpper:
def __init__(self, param1, param2, param3, param4, param5, param6,):
pass
而如果使用 *args(按位置传值)或者 **kwargs(按键值对传值),使用 IDE 的人会很头疼,因为这样做就不知道它可以接受哪些参数,以及这些参数具有哪些类型限制。此时使用 @dataclass,就是非常棒的选择。
当然,Python 的装饰器类型很多,你也可以自己定义几个,从而方便你的程序设计。我大概举几个例子吧。
第一个案例,假设你有这样一个类:
1
2
3
class Circle:
def __init__(self, radius):
self._radius = radius
它具有半径 _radius 这一个属性。以_开头的都是受保护的对象——当然,如果你想访问,这个障碍仍然挡不住你。即便如此你还是想上锁,那该怎么办?
@property就可以做到这一点。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def area(self):
# 调用时不需要括号,直接 c.area 即可
return 3.14 * (self._radius ** 2)
@property
def radius(self):
return self._radius
# 尝试实例化一个 Circle 对象
circle = Circle(radius = 5)
# 访问其半径(注意这里用的是 radius,而不是 _radius,更不是 radius())
print(circle.radius) # 返回 5
# 访问其面积
print(circle.area) # 返回 78.5
用函数伪装成属性,就是它最大的功用——这后面两个函数(方法)如同活板门,它们只能将值传出去,而无法接受一个值以进行改动。更妙的是,调用它们不需要带(),让它们看上去更像属性,而不是函数(方法)!
虽然还是防不了有人用 circle._radius 强行“Hack”,但对于路人来说足够了。算了,至少 circle.radius 已经变成只读了,就这样吧。(笑)
另一个案例与性能有关。学习动态规划时,“计算斐波那契数列”是一个经典的入门问题。传统的“递归”(自顶向下)会反复对同一个输入求值(例如计算 F(45),其内部仍然要反复计算 F(1) 的值),所以我们引入了动态规划,引入了“剪枝”,引入了“自底向上”的方法,经过了无穷多的麻烦才赢得了 \(O(N)\) 的时间复杂度,而不是以往的 \(O(2^{n})\)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 一个经典的斐波那契数列通项函数,采用了滚动数组算法
# 如果你学过动态规划或者刷过 Leetcode,你会觉得相当眼熟
def fib(n: int) -> int:
if n <= 1:
return n
# a 代表 f(0), b 代表 f(1)
a, b = 0, 1
# 从第 2 级开始计算,直到第 n 级
for _ in range(2, n + 1):
# 新的结果 = 前两个结果之和
# 更新 a 和 b,并向后滚动
a, b = b, a + b
return b
不过在 Python,想避免重复计算,只需要这个装饰器:
1
2
3
4
5
6
from functools import lru_cache
@lru_cache(maxsize=128)
def fib(n):
if n < 2: return n
return fib(n-1) + fib(n-2)
它让函数具备自动缓存的能力,从而在下次获得同样输入时,通过查表直接返回结果。令人欣慰的是,它的时间复杂度同样是线性阶 \(O(N)\);但空间复杂度……或者说需要消耗多少内存,取决于你给了多少空间用来缓存结果。算是以空间换时间了。
最后一个案例,是自定义装饰器。假设某个用户(user)想要动数据库,想必第一道坎就是要检查他的权限。给每个操作都加个 if 检查其角色(role)也不是不行,但加上了装饰器之后,代码就更加简洁,也更直观了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
def admin_required(func):
# 这又是一个装饰器,但作用于`warpper`,
# 目的是防止`func`原本的名字(`__name__`)、文档(`__doc__`)等被下面的这个函数覆盖掉
@functools.wraps(func)
def wrapper(user, *args, **kwargs):
if user.role != 'admin':
raise PermissionError("权限不足")
return func(user, *args, **kwargs)
return wrapper
@admin_required
def delete_database(user):
# 此处无需 if 检查用户权限,而专心于实际的业务逻辑
print("数据库已删除")