Python 里的 random 模块

最近工作上遇到了一个比较有意思的问题:在生成邮箱验证码的时候,居然会出现重复,而且是可复现的重复,后来去另一个环境试了下,发现居然不同环境都会重复!

这种情况,粗略判断,应该是随机数生成的问题,但是奇怪的是,代码也重置了 seed,这个 seed 也是个随机的:

1
2
3
4
5
6
7
8
import random

random.seed(time.time() * 1000)


def get_capture(length=6):
all_letters = string.ascii_uppercase + string.digits
return ''.join(random.sample(all_letters, length))

经过调试,发现不论 seed 设置什么值,生成的随机字符串都是一样的,但是如果把 seed 设置放在函数内,就又都是随机的了。

最开始以为是 seed 设置会有作用域区分,但是经过查看代码,发现 random 居然是在内部生成了一个 Random 类的实例,从外部导入的 random.seedrandom.sample 都是在这个实例上调用的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# Create one instance, seeded from current time, and export its methods
# as module-level functions. The functions share state across all uses
#(both in the user's code and in the Python libraries), but that's fine
# for most programs and is easier for the casual user than making them
# instantiate their own Random() instance.

_inst = Random()
seed = _inst.seed
random = _inst.random
uniform = _inst.uniform
triangular = _inst.triangular
randint = _inst.randint
choice = _inst.choice
randrange = _inst.randrange
sample = _inst.sample
shuffle = _inst.shuffle
choices = _inst.choices
normalvariate = _inst.normalvariate
lognormvariate = _inst.lognormvariate
expovariate = _inst.expovariate
vonmisesvariate = _inst.vonmisesvariate
gammavariate = _inst.gammavariate
gauss = _inst.gauss
betavariate = _inst.betavariate
paretovariate = _inst.paretovariate
weibullvariate = _inst.weibullvariate
getstate = _inst.getstate
setstate = _inst.setstate
getrandbits = _inst.getrandbits

那既然不论在全局 seed,还是在函数内 seed,都基于同一个实例,那应该都起作用才对。现在的状态是全局调用 seed 函数,不论传入什么值产生的结果都是一样的。而在函数内调用 seed,如果输入是随机值,那么输出也是随机的,如果输入是个固定的,那么输出也会是固定的随机字符串。

那么这样看来,似乎只有一种可能————有其他地方在这个全局实例上也调用了 seed,于是我把断点打到这个 seed 函数里,然后重启 web,果然发现有很多次调用。

顺藤摸瓜,找到了其他在全局调用 seed 的地方,案子终于破了:其他地方调用 seed 的时机比这个地方迟,而且又都是在一个实例上调用的,那肯定都覆盖了,而且其他地方都是以固定数字调用 seed 的,所以最后产生的随机字符串都是以固定的顺序生成的。

那么如何修改代码呢?

在调试的时候,除了上面的那些全局调用 seed 的地方,还有一些其他写法,那就是新实例一个 Random 实例,让它和 random 模块里内置的那个默认实例区分开,然后后面调用 random 方法的地方,都基于这个新实例。妙啊!

那么 random 库使用的最佳实践应该是 在用到 random 的时候,搞一个自己的 Random 实例,这样就分开了。

代码修改如下:

1
2
3
4
5
6
7
8
import random

_random = random.Random(time.time() * 1000)


def get_capture(length=6):
all_letters = string.ascii_uppercase + string.digits
return ''.join(_random.sample(all_letters, length))
作者

skyrover

发布于

2022-11-03

更新于

2022-11-03

许可协议

评论