基本改进、集合和本地名称
使用set,而不是列表,测试唯一性要快得多;集合成员测试需要与集合大小无关的恒定时间,而列表需要O(N)线性时间.使用集合理解一次生成一系列键,以避免在循环中查找和调用set.add()
方法;如果是随机的,较大的键无论如何产生副本的可能性很小.
因为这是在一个紧密的循环中完成的,所以在尽可能优化所有名称查找的同时,这是值得的:
import secrets
import numpy as np
from functools import partial
def produce_amount_keys(amount_of_keys, _randint=np.random.randint):
keys = set()
pickchar = partial(secrets.choice, string.ascii_uppercase + string.digits)
while len(keys) < amount_of_keys:
keys |= {''.join([pickchar() for _ in range(_randint(12, 20))]) for _ in range(amount_of_keys - len(keys))}
return keys
_randint
关键字参数将np.random.randint
名称绑定到函数中的一个局部名称,这比全局名称引用更快,尤其是在涉及属性查找时.
pickchar()
部分避免查找模块或更多局部变量的属性;它是一个具有所有引用的可调用函数,因此执行速度更快,尤其是在循环中执行时.
只有在产生了重复项时,while
循环才会继续迭代.如果没有重复的话,我们在一个集合中产生足够的键来填充剩余的键.
第一次改进的时间安排
对于100个项目,差异并没有那么大:
>>> timeit('p(100)', 'from __main__ import produce_amount_keys_list as p', number=1000)
8.720592894009314
>>> timeit('p(100)', 'from __main__ import produce_amount_keys_set as p', number=1000)
7.680242831003852
但是,当你开始扩大这个范围时,你会注意到O(N)成员资格测试成本与列表的对比确实会拖累你的版本:
>>> timeit('p(10000)', 'from __main__ import produce_amount_keys_list as p', number=10)
15.46253142200294
>>> timeit('p(10000)', 'from __main__ import produce_amount_keys_set as p', number=10)
8.047800761007238
我的版本速度几乎是10k物品的两倍;40k个项目可以在大约32秒内运行10次:
>>> timeit('p(40000)', 'from __main__ import produce_amount_keys_list as p', number=10)
138.84072386901244
>>> timeit('p(40000)', 'from __main__ import produce_amount_keys_set as p', number=10)
32.40720253501786
列表版本耗时超过2分钟,是原来的10倍多.
Numpy是随机的. Select 功能,加密功能不强
你可以通过放弃secrets
模块而改用np.random.choice()
来加快速度;不过,这不会产生加密级别的随机性,但拾取随机字符的速度是随机字符的两倍:
def produce_amount_keys(amount_of_keys, _randint=np.random.randint):
keys = set()
pickchar = partial(
np.random.choice,
np.array(list(string.ascii_uppercase + string.digits)))
while len(keys) < amount_of_keys:
keys |= {''.join([pickchar() for _ in range(_randint(12, 20))]) for _ in range(amount_of_keys - len(keys))}
return keys
这带来了巨大的不同,现在仅需16秒就可以生成10乘以40k的密钥:
>>> timeit('p(40000)', 'from __main__ import produce_amount_keys_npchoice as p', number=10)
15.632006907981122
进一步调整了itertools模块和发电机
我们还可以从itertools
模块Recipes部分中选取unique_everseen()
function,让其考虑唯一性,然后使用无限生成器和itertools.islice()
function将结果限制为我们想要的数字:
# additional imports
from itertools import islice, repeat
# assumption: unique_everseen defined or imported
def produce_amount_keys(amount_of_keys):
pickchar = partial(
np.random.choice,
np.array(list(string.ascii_uppercase + string.digits)))
def gen_keys(_range=range, _randint=np.random.randint):
while True:
yield ''.join([pickchar() for _ in _range(_randint(12, 20))])
return list(islice(unique_everseen(gen_keys()), amount_of_keys))
这一速度稍快,但仅略快:
>>> timeit('p(40000)', 'from __main__ import produce_amount_keys_itertools as p', number=10)
14.698191125993617
操作系统.Uradom()字节和另一种生成字符串的方法
接下来,我们可以继续使用UUID4(基本上只是os.urandom()
的包装器)和Base64.但是,通过大小写折叠Base64并用随机选取的字符替换2个字符,他的方法严重限制了这些字符串中的熵(你不会产生可能的全部唯一值,一个20个字符的字符串每99437位熵中只使用(256 ** 15) / (36 ** 20)
==1!).
Base64编码既使用大小写字符和数字,也使用-
和/
字符(或+
和_
表示URL安全变体).对于仅大写字母和数字,您必须将输出大写,并将额外的两个字符映射到其他随机字符,这一过程会从os.urandom()
提供的随机数据中丢弃大量熵.除了使用Base64,您还可以使用Base32编码,它使用大写字母和数字2到8,因此生成的字符串可能为32**n,而不是36**n.然而,这可以进一步加快上述try 的速度:
import os
import base64
import math
def produce_amount_keys(amount_of_keys):
def gen_keys(_urandom=os.urandom, _encode=base64.b32encode, _randint=np.random.randint):
# (count / math.log(256, 32)), rounded up, gives us the number of bytes
# needed to produce *at least* count encoded characters
factor = math.log(256, 32)
input_length = [None] * 12 + [math.ceil(l / factor) for l in range(12, 20)]
while True:
count = _randint(12, 20)
yield _encode(_urandom(input_length[count]))[:count].decode('ascii')
return list(islice(unique_everseen(gen_keys()), amount_of_keys))
这是really快:
>>> timeit('p(40000)', 'from __main__ import produce_amount_keys_b32 as p', number=10)
4.572628145979252
40k键,10次,仅用4秒多.所以速度大约是75倍;使用os.urandom()
作为源的速度是不可否认的.
这是,cryptographically strong again;os.urandom()
生成用于加密的字节.另一方面,我们将可能产生的字符串数量减少了90%以上(((36 ** 20) - (32 ** 20)) / (36 ** 20) * 100
是90.5),我们不再使用输出中的0
、1
、8
和9
位数.
所以也许我们应该使用urandom()
技巧来产生一个正确的Base36编码;我们必须生产自己的b36encode()
功能:
import string
import math
def b36encode(b,
_range=range, _ceil=math.ceil, _log=math.log, _fb=int.from_bytes, _len=len, _b=bytes,
_c=(string.ascii_uppercase + string.digits).encode()):
"""Encode a bytes value to Base36 (uppercase ASCII and digits)
This isn't too friendly on memory because we convert the whole bytes
object to an int, but for smaller inputs this should be fine.
"""
b_int = _fb(b, 'big')
length = _len(b) and _ceil(_log((256 ** _len(b)) - 1, 36))
return _b(_c[(b_int // 36 ** i) % 36] for i in _range(length - 1, -1, -1))
用这个:
def produce_amount_keys(amount_of_keys):
def gen_keys(_urandom=os.urandom, _encode=b36encode, _randint=np.random.randint):
# (count / math.log(256, 36)), rounded up, gives us the number of bytes
# needed to produce *at least* count encoded characters
factor = math.log(256, 36)
input_length = [None] * 12 + [math.ceil(l / factor) for l in range(12, 20)]
while True:
count = _randint(12, 20)
yield _encode(_urandom(input_length[count]))[-count:].decode('ascii')
return list(islice(unique_everseen(gen_keys()), amount_of_keys))
这相当快,而且最重要的是产生了36个大写字母和数字的完整范围:
>>> timeit('p(40000)', 'from __main__ import produce_amount_keys_b36 as p', number=10)
8.099918447987875
诚然,base32版本的速度几乎是这个版本的两倍(这要归功于使用表的高效Python实现),但使用自定义Base36编码器的速度仍然是非加密安全的numpy.random.choice()
版本的两倍.
然而,再次使用os.urandom()
produces bias;我们必须产生比12到19个36位"数字"所需的更多的熵位.例如,对于17位数字,我们无法使用字节生成36**17个不同的值,只能生成最接近的256**11字节的等效值,这大约是高出的1.08倍,因此我们最终会偏向A
、B
,并在较小程度上偏向C
(感谢Stefan Pochmann指出这一点).
Picking an integer below (36 ** length)
and mapping integers to base36
因此,我们需要找到一种安全的随机方法,它可以为我们提供均匀分布在0
(含)和36 ** (desired length)
(不含)之间的值.然后我们可以将数字直接映射到所需的字符串.
首先,将整数映射为字符串;为了以最快的速度生成输出字符串,对以下内容进行了调整:
def b36number(n, length, _range=range, _c=string.ascii_uppercase + string.digits):
"""Convert an integer to Base36 (uppercase ASCII and digits)"""
chars = [_c[0]] * length
while n:
length -= 1
chars[length] = _c[n % 36]
n //= 36
return ''.join(chars)
接下来,我们需要一种快速、准确的方法来 Select 一个范围内的数字.你仍然可以使用os.urandom()
,但是你必须将字节屏蔽到最大位数,然后循环直到你的实际值低于限制.这实际上已经由secrets.randbelow()
function人实施.在Python版本中<;3.6您可以使用random.SystemRandom().randrange()
,它使用完全相同的方法和一些额外的包装来支持大于0的下限和步长.
使用secrets.randbelow()
,功能变为:
import secrets
def produce_amount_keys(amount_of_keys):
def gen_keys(_below=secrets.randbelow, _encode=b36number, _randint=np.random.randint):
limit = [None] * 12 + [36 ** l for l in range(12, 20)]
while True:
count = _randint(12, 20)
yield _encode(_below(limit[count]), count)
return list(islice(unique_everseen(gen_keys()), amount_of_keys))
这与base64解决方案非常接近(可能有偏差):
>>> timeit('p(40000)', 'from __main__ import produce_amount_keys_below as p', number=10)
5.135716405988205
这几乎与Base32方法一样快,但会产生全范围的关键点!