我是 sb!Python 都不会写!(大声)

背景

在一个奇怪的场景里触发了一个奇怪的问题。为了降低某个方法的 qps(?),写了一个摸鱼 decorator,使得该方法每 n 次调用只运行一次,其他时候返回上一次运行的结果。大概用法如下

1
2
3
4
@do_every(time=3)
def func():
.... # 当然,得是无副作用的
return ...

这个 decorator 也不难写,只是需要维护一个计数器和上一次的结果,也就是说是一个有状态的 decorator(不保证这么说是否合理,可以意会一下)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def do_every(time):
def decorator(func):
counter = -1
last_res = None

def wrapper(*args, **kw):
nonlocal counter, last_res
counter = (counter + 1) % time
if counter:
return last_res
last_res = func(*args, **kw)
return last_res
return wrapper
return decorator

因为其行为和摸鱼类似,因此命名为「摸鱼 decorator」

翻车

上面的代码看着写的很对(实际上也写的很对)

但实际上,遇到 class 后,可能产生和预期有出入的效果。

1
2
3
4
5
6
7
import random

class A:

@do_every(time = 3)
def func(self):
return random.random()

在上面的代码中,我们定义了class A,并给了一个成员方法func。那么,在使用do_every前,先思考一个问题

若存在两个A的实例,那他们应该各自摸鱼还是轮流摸鱼

事实上,由于没有思考过这个问题,因此直接导致了翻车。

1
2
3
4
5
6
7
a = A()
b = A()

for i in range(4):
print('a', a.func())
print('b', b.func())
# 输出啥?

摸鱼 decorator 最终起的作用是轮流摸鱼,参考如下输出

1
2
3
4
5
6
7
8
a 0.06733737591002142
b 0.06733737591002142
a 0.06733737591002142
b 0.6798565770897768
a 0.6798565770897768
b 0.6798565770897768
a 0.6623001317870929
b 0.6623001317870929

我们看到 4 轮中,a 干了两轮活,b 干了一轮活,加起来总共干了三次。实际上在使用上我们希望他们各自摸鱼,也就是 a 和 b 各需要干两轮活,总共加起来要干四次。资本家对此表达了强烈不满!

在实践中,我们发现 decorator 内部的变量,虽然在不同调用该 decorator 的函数间不共享(显然 decorator 的入参不同),但在 Python 中的成员方法,实际上应看做同一个函数(传入的 self 不同)

后续

再也不写有状态的 decorator 了!

由于没有想到好的修复方法,于是放弃了摸鱼(?),用别的办法绕过了这个问题。

有想到办法的牛逼网友,欢迎供稿。