python - guia - qgis español
La forma más rápida de generar una cadena única de estilo aleatorio con longitud aleatoria en Python 3 (5)
Enfoque alternativo: singularidad en la creación en lugar de por prueba
El enfoque obvio para su pregunta sería generar una salida aleatoria y luego verificar si es única. Aunque no ofrezco una implementación, aquí hay un enfoque alternativo:
- Genera una salida que se vea lo más aleatoria posible
- Genere una salida que se garantiza que sea única y que se vea algo aleatoria
- Combínalos
Ahora tiene una salida que se garantiza que es única, y parece ser aleatoria.
Ejemplo
Supongamos que desea generar 999999 cadenas con longitudes de 12 y 20. El enfoque funcionará, por supuesto, para todos los conjuntos de caracteres, pero sea sencillo y supongamos que desea utilizar solo 0-9.
- Genera salidas aleatorias con longitudes de 6 a 14.
- Permuta aleatoriamente los números 000000 a 999999 (sí, 6 dígitos es bastante para ''sacrificar'' en aparente aleatoriedad, pero con un conjunto de caracteres más grande no necesitará tantos caracteres)
- Ahora combínelos de tal manera que la singularidad debe ser preservada. La forma más trivial sería la simple concatenación de las entidades, pero, por supuesto, puede pensar en soluciones menos obvias.
Ejemplo a pequeña escala
Generar aleatoriedad:
sdfdsf xxer ver
Generar singularidad
xd ae bd
Combinar
xdsdfdsf aexxer bdver
Tenga en cuenta que este método supone que tiene un número mínimo de caracteres por entrada, lo que parece ser el caso en su pregunta.
Sé cómo crear una cadena aleatoria, como:
''''.join(secrets.choice(string.ascii_uppercase + string.digits) for _ in range(N))
Sin embargo, no debería haber duplicados, así que en este momento solo estoy comprobando si la clave ya existe en una lista, como se muestra en el siguiente código:
import secrets
import string
import numpy as np
amount_of_keys = 40000
keys = []
for i in range(0,amount_of_keys):
N = np.random.randint(12,20)
n_key = ''''.join(secrets.choice(string.ascii_uppercase + string.digits) for _ in range(N))
if not n_key in keys:
keys.append(n_key)
Lo cual está bien para una pequeña cantidad de claves como 40000
, sin embargo, el problema no se adapta bien, cuantas más claves haya. Así que me pregunto si hay una manera más rápida de llegar al resultado para obtener aún más claves, como 999999
Mejoras básicas, sets y nombres locales.
Use un conjunto , no una lista, y las pruebas de singularidad son mucho más rápidas; establecer las pruebas de membresía toma tiempo constante independientemente del tamaño establecido, mientras que las listas toman tiempo O (N) lineal. Use una comprensión de set para producir una serie de claves a la vez para evitar tener que buscar y llamar al método set.add()
en un bucle; apropiadamente aleatorias, las claves más grandes tienen una pequeña posibilidad de producir duplicados de todos modos.
Debido a que esto se hace en un circuito cerrado, vale la pena optimizar al máximo todas las búsquedas de nombres:
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
El argumento de la palabra clave _randint
vincula el nombre np.random.randint
a un local en la función, que es más rápido de referencia que los globales, especialmente cuando se trata de búsquedas de atributos.
El pickchar()
parcial evita buscar atributos en módulos o más locales; es un solo invocable que tiene todas las referencias en su lugar, por lo que es más rápido en la ejecución, especialmente cuando se realiza en un bucle.
El bucle while se mantiene iterando solo si se produjeron duplicados. Producimos suficientes claves en un solo conjunto de comprensión para completar el resto si no hay duplicados.
Tiempos para esa primera mejora.
Para 100 artículos, la diferencia no es tan grande:
>>> 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
pero cuando empiece a ampliar esto, notará que el costo de la prueba de membresía O (N) en una lista realmente reduce su versió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
Mi versión ya es casi el doble de rápida que 10k artículos; Los artículos de 40k se pueden ejecutar 10 veces en unos 32 segundos:
>>> 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
La versión de la lista tomó más de 2 minutos, más de diez veces más.
Función random.choice de Numpy, no criptográficamente fuerte
Puede hacer esto aún más rápido si abandona el módulo de secrets
y usa np.random.choice()
lugar; sin embargo, esto no producirá una aleatoriedad de nivel criptográfico, pero elegir un carácter aleatorio es dos veces más rápido:
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
Esto hace una gran diferencia, ahora se pueden producir 10 claves de 40k en solo 16 segundos:
>>> timeit(''p(40000)'', ''from __main__ import produce_amount_keys_npchoice as p'', number=10)
15.632006907981122
Más ajustes con el módulo de itertools y un generador.
También podemos tomar la función unique_everseen()
de la sección Recetas del módulo itertools
para que se encargue de la singularidad, luego usar un generador infinito y la función itertools.islice()
para limitar los resultados solo al número que deseamos:
# 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))
Esto es un poco más rápido aún, pero solo marginalmente:
>>> timeit(''p(40000)'', ''from __main__ import produce_amount_keys_itertools as p'', number=10)
14.698191125993617
Os.urandom () bytes y un método diferente para producir cadenas
A continuación, podemos seguir con las ideas de Adam Barnes para usar UUID4 (que básicamente es solo una envoltura alrededor de os.urandom()
) y Base64. Pero al plegar la Base64 y reemplazar 2 caracteres por otros elegidos al azar, su método limita severamente la entropía en esas cadenas (no producirá el rango completo de valores únicos posibles, una cadena de 20 caracteres que solo se usa (256 ** 15) / (36 ** 20)
== ¡1 en cada 99437 bits de entropía!).
La codificación Base64 utiliza caracteres y dígitos en mayúsculas y minúsculas, pero también agrega los caracteres -
y /
(o +
y _
para la variante de URL segura). Solo para letras y dígitos en mayúsculas, tendría que escribir en mayúsculas y asignar esos dos caracteres adicionales a otros caracteres aleatorios, un proceso que elimina una gran cantidad de entropía de los datos aleatorios proporcionados por os.urandom()
. En lugar de usar Base64, también puede usar la codificación Base32, que usa letras mayúsculas y los dígitos del 2 al 8, por lo que produce cadenas con 32 ** n posibilidades en comparación con 36 ** n. Sin embargo, esto puede acelerar las cosas más allá de los intentos anteriores:
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))
Esto es realmente rápido:
>>> timeit(''p(40000)'', ''from __main__ import produce_amount_keys_b32 as p'', number=10)
4.572628145979252
40k teclas, 10 veces, en poco más de 4 segundos. Tan cerca de 75 veces más rápido; la velocidad de usar os.urandom()
como fuente es innegable.
Esto es, criptográficamente fuerte otra vez ; os.urandom()
produce bytes para uso criptográfico. Por otro lado, redujimos el número de cadenas posibles producidas en más del 90% ( ((36 ** 20) - (32 ** 20)) / (36 ** 20) * 100
es 90.5), no somos más tiempo usando los 0
, 1
, 8
y 9
dígitos en las salidas.
Así que quizás deberíamos usar el truco urandom()
para producir una codificación Base36 adecuada; tendremos que producir nuestra propia función 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))
y usa eso:
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))
Esto es razonablemente rápido, y sobre todo produce el rango completo de 36 letras y dígitos en mayúsculas:
>>> timeit(''p(40000)'', ''from __main__ import produce_amount_keys_b36 as p'', number=10)
8.099918447987875
Por supuesto, la versión base32 es casi el doble de rápida que esta (gracias a una implementación eficiente de Python utilizando una tabla) pero el uso de un codificador Base36 personalizado sigue siendo el doble de la velocidad de la versión numpy.random.choice()
no segura desde el punto de numpy.random.choice()
criptográfico.
Sin embargo, el uso de os.urandom()
produce un sesgo de nuevo; tenemos que producir más bits de entropía de lo que se requiere para entre 12 y 19 "dígitos 36 base". Para 17 dígitos, por ejemplo, no podemos producir 36 ** 17 valores diferentes usando bytes, solo el equivalente más cercano a 256 ** 11 bytes, que es aproximadamente 1.08 veces demasiado alto, por lo que terminaremos con un sesgo hacia A
, B
, y en menor medida, C
(gracias Stefan Pochmann por señalarlo).
Escoger un entero debajo (36 ** length)
y mapear enteros a base36
Por lo tanto, debemos llegar a un método seguro y aleatorio que nos pueda dar valores distribuidos uniformemente entre 0
(inclusive) y 36 ** (desired length)
(exclusivo). Luego podemos asignar el número directamente a la cadena deseada.
Primero, mapeando el entero a una cadena; se ha modificado lo siguiente para producir la cadena de salida más rápido:
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)
A continuación, necesitamos un método rápido y criptográficamente seguro para seleccionar nuestro número en un rango. Aún puede usar os.urandom()
para esto, pero luego tendría que enmascarar los bytes hasta un número máximo de bits, y luego hacer un bucle hasta que su valor real esté por debajo del límite. Esto ya está implementado por la función secrets.randbelow()
. En las versiones de Python <3.6 puede usar random.SystemRandom().randrange()
, que usa el mismo método exacto con un random.SystemRandom().randrange()
adicional para admitir un límite inferior mayor que 0 y un tamaño de paso.
Usando secrets.randbelow()
la función se convierte en:
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))
y esto es bastante parecido a la solución base64 (probablemente sesgada):
>>> timeit(''p(40000)'', ''from __main__ import produce_amount_keys_below as p'', number=10)
5.135716405988205
Esto es casi tan rápido como el enfoque de Base32, ¡pero produce toda la gama de teclas!
Así que es una carrera de velocidad, ¿verdad?
Sobre la base del trabajo de Martijn Pieters, tengo una solución que aprovecha inteligentemente otra biblioteca para generar cadenas aleatorias: uuid
.
Mi solución es generar un uuid4
, base64, codificarlo y mayúsculas, para obtener solo los caracteres que buscamos y luego dividirlos en una longitud aleatoria.
Esto funciona para este caso porque la longitud de las salidas que buscamos, (12-20), es más corta que la codificación base64 más corta de un uuid4. También es muy rápido, porque uuid
es muy rápido.
También lo hice un generador en lugar de una función regular, porque pueden ser más eficientes.
Curiosamente, el uso de la función randint
la biblioteca estándar fue más rápido que el de numpy
.
Aquí está la salida de prueba:
Timing 40k keys 10 times with produce_amount_keys
20.899942063027993
Timing 40k keys 10 times with produce_amount_keys, stdlib randint
20.85920040300698
Timing 40k keys 10 times with uuidgen
3.852462349983398
Timing 40k keys 10 times with uuidgen, stdlib randint
3.136272903997451
Aquí está el código para uuidgen()
:
def uuidgen(count, _randint=np.random.randint):
generated = set()
while True:
if len(generated) == count:
return
candidate = b64encode(uuid4().hex.encode()).upper()[:_randint(12, 20)]
if candidate not in generated:
generated.add(candidate)
yield candidate
Y here está todo el proyecto. (En commit d9925d en el momento de escribir).
Gracias a los comentarios de Martijn Pieters, he mejorado un poco el método, aumentando la entropía y acelerándolo en un factor de aproximadamente 1/6.
Todavía hay mucha entropía perdida al convertir todas las letras en minúsculas en mayúsculas. Si eso es importante, entonces es posiblemente recomendable utilizar b32encode()
, que tiene los caracteres que queremos, menos 0
, 1
, 8
y 9
.
La nueva solución dice lo siguiente:
def urandomgen(count):
generated = set()
while True:
if len(generated) == count:
return
desired_length = randint(12, 20)
# # Faster than math.ceil
# urandom_bytes = urandom(((desired_length + 1) * 3) // 4)
#
# candidate = b64encode(urandom_bytes, b''//'').upper()
#
# The above is rolled into one line to cut down on execution
# time stemming from locals() dictionary access.
candidate = b64encode(
urandom(((desired_length + 1) * 3) // 4),
b''//'',
).upper()[:desired_length]
while b''/'' in candidate:
candidate = candidate.replace(b''/'', choice(ALLOWED_CHARS), 1)
if candidate not in generated:
generated.add(candidate)
yield candidate.decode()
Y la salida de prueba:
Timing 40k keys 10 times with produce_amount_keys, stdlib randint
19.64966493297834
Timing 40k keys 10 times with uuidgen, stdlib randint
4.063803717988776
Timing 40k keys 10 times with urandomgen, stdlib randint
2.4056471119984053
El nuevo commit en mi repositorio es 5625fd .
Los comentarios de Martijn sobre la entropía me hicieron pensar. El método que utilicé con base64
y .upper()
hace que las letras sean mucho más comunes que los números. Revisé el problema con una mente más binaria.
La idea era tomar la salida de os.urandom()
, interpretarla como una cadena larga de números sin signo de 6 bits, y usar esos números como un índice para una matriz de los caracteres permitidos. El primer número de 6 bits seleccionaría un carácter del rango A..Z0..9A..Z01
, el segundo número de 6 bits seleccionaría un carácter del rango 2..9A..Z0..9A..T
, y así.
Esto tiene un ligero aplastamiento de la entropía, ya que el primer carácter tendrá un poco menos de probabilidad de contener 2..9
, el segundo carácter U..Z0
menos probable que contenga U..Z0
, y así sucesivamente, pero es mucho mejor que antes.
Es un poco más rápido que uuidgen()
, y un poco más lento que urandomgen()
, como se muestra a continuación:
Timing 40k keys 10 times with produce_amount_keys, stdlib randint
20.440480664998177
Timing 40k keys 10 times with uuidgen, stdlib randint
3.430628580001212
Timing 40k keys 10 times with urandomgen, stdlib randint
2.0875444510020316
Timing 40k keys 10 times with bytegen, stdlib randint
2.8740892770001665
No estoy completamente seguro de cómo eliminar el último trozo de trituración de entropía; compensar el punto de inicio para que los personajes solo muevan el patrón un poco, al azar la compensación será lenta, barajar el mapa aún tendrá un período ... Estoy abierto a las ideas.
El nuevo código es el siguiente:
from os import urandom
from random import randint
from string import ascii_uppercase, digits
# Masks for extracting the numbers we want from the maximum possible
# length of `urandom_bytes`.
bitmasks = [(0b111111 << (i * 6), i) for i in range(20)]
allowed_chars = (ascii_uppercase + digits) * 16 # 576 chars long
def bytegen(count):
generated = set()
while True:
if len(generated) == count:
return
# Generate 9 characters from 9x6 bits
desired_length = randint(12, 20)
bytes_needed = (((desired_length * 6) - 1) // 8) + 1
# Endianness doesn''t matter.
urandom_bytes = int.from_bytes(urandom(bytes_needed), ''big'')
chars = [
allowed_chars[
(((urandom_bytes & bitmask) >> (i * 6)) + (0b111111 * i)) % 576
]
for bitmask, i in bitmasks
][:desired_length]
candidate = ''''.join(chars)
if candidate not in generated:
generated.add(candidate)
yield candidate
Y el código completo, junto con un README más profundo sobre la implementación, se termina en de0db8 .
Intenté varias cosas para acelerar la implementación, como se ve en el repositorio. Algo que definitivamente ayudaría es una codificación de caracteres donde los números y las letras mayúsculas ASCII son secuenciales.
Una simple y rápida:
def b36(n, N, chars=string.ascii_uppercase + string.digits):
s = ''''
for _ in range(N):
s += chars[n % 36]
n //= 36
return s
def produce_amount_keys(amount_of_keys):
keys = set()
while len(keys) < amount_of_keys:
N = np.random.randint(12, 20)
keys.add(b36(secrets.randbelow(36**N), N))
return keys
- Edición: lo que sigue se refiere a una revisión previa de la respuesta de Martijn. Después de nuestra discusión, le agregó otra solución, que es esencialmente la misma que la mía pero con algunas optimizaciones. Sin embargo, no ayudan mucho, es solo un 3,4% más rápido que el mío en mis pruebas, por lo que, en mi opinión, en su mayoría solo complican las cosas. -
En comparación con la solución final de Martijn en su respuesta aceptada, la mía es mucho más simple, alrededor del factor 1.7 más rápido y sin sesgo:
Stefan
8.246490597876106 seconds.
8 different lengths from 12 to 19
Least common length 19 appeared 124357 times.
Most common length 16 appeared 125424 times.
36 different characters from 0 to Z
Least common character Q appeared 429324 times.
Most common character Y appeared 431433 times.
36 different first characters from 0 to Z
Least common first character C appeared 27381 times.
Most common first character Q appeared 28139 times.
36 different last characters from 0 to Z
Least common last character Q appeared 27301 times.
Most common last character E appeared 28109 times.
Martijn
14.253227412021943 seconds.
8 different lengths from 12 to 19
Least common length 13 appeared 124753 times.
Most common length 15 appeared 125339 times.
36 different characters from 0 to Z
Least common character 9 appeared 428176 times.
Most common character C appeared 434029 times.
36 different first characters from 0 to Z
Least common first character 8 appeared 25774 times.
Most common first character A appeared 31620 times.
36 different last characters from 0 to Z
Least common last character Y appeared 27440 times.
Most common last character X appeared 28168 times.
Martijn tiene un sesgo en el primer carácter, A
aparece con demasiada frecuencia y 8
muy rara vez. Realicé mi prueba diez veces, su primer personaje más común siempre fue A
o B
(cinco veces cada uno), y su carácter menos común siempre fue 7
, 8
o 9
(dos, tres y cinco veces, respectivamente). También verifiqué las longitudes por separado, la longitud 17 fue particularmente mala, su primer personaje más común siempre apareció unas 51500 veces, mientras que su primer personaje menos común apareció unas 25400 veces.
Nota divertida: estoy usando el módulo de secrets
que Martijn desestimó :-)
Todo mi guión:
import string
import secrets
import numpy as np
import os
from itertools import islice, filterfalse
import math
#------------------------------------------------------------------------------------
# Stefan
#------------------------------------------------------------------------------------
def b36(n, N, chars=string.ascii_uppercase + string.digits):
s = ''''
for _ in range(N):
s += chars[n % 36]
n //= 36
return s
def produce_amount_keys_stefan(amount_of_keys):
keys = set()
while len(keys) < amount_of_keys:
N = np.random.randint(12, 20)
keys.add(b36(secrets.randbelow(36**N), N))
return keys
#------------------------------------------------------------------------------------
# Martijn
#------------------------------------------------------------------------------------
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()):
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_martijn(amount_of_keys):
def gen_keys(_urandom=os.urandom, _encode=b36encode, _randint=np.random.randint, _factor=math.log(256, 36)):
while True:
count = _randint(12, 20)
yield _encode(_urandom(math.ceil(count / _factor)))[-count:].decode(''ascii'')
return list(islice(unique_everseen(gen_keys()), amount_of_keys))
#------------------------------------------------------------------------------------
# Needed for Martijn
#------------------------------------------------------------------------------------
def unique_everseen(iterable, key=None):
seen = set()
seen_add = seen.add
if key is None:
for element in filterfalse(seen.__contains__, iterable):
seen_add(element)
yield element
else:
for element in iterable:
k = key(element)
if k not in seen:
seen_add(k)
yield element
#------------------------------------------------------------------------------------
# Benchmark and quality check
#------------------------------------------------------------------------------------
from timeit import timeit
from collections import Counter
def check(name, func):
print()
print(name)
# Get 999999 keys and report the time.
keys = None
def getkeys():
nonlocal keys
keys = func(999999)
t = timeit(getkeys, number=1)
print(t, ''seconds.'')
# Report statistics about lengths and characters
def statistics(label, values):
ctr = Counter(values)
least = min(ctr, key=ctr.get)
most = max(ctr, key=ctr.get)
print(len(ctr), f''different {label}s from'', min(ctr), ''to'', max(ctr))
print(f'' Least common {label}'', least, ''appeared'', ctr[least], ''times.'')
print(f'' Most common {label}'', most, ''appeared'', ctr[most], ''times.'')
statistics(''length'', map(len, keys))
statistics(''character'', ''''.join(keys))
statistics(''first character'', (k[0] for k in keys))
statistics(''last character'', (k[-1] for k in keys))
for _ in range(2):
check(''Stefan'', produce_amount_keys_stefan)
check(''Martijn'', produce_amount_keys_martijn)
Advertencia: Esto no es criptográficamente seguro . Quiero dar un enfoque numpy
alternativo al de la gran respuesta de Martijn.
numpy
funciones numpy
no están realmente optimizadas para ser llamadas repetidamente en un bucle para tareas pequeñas; más bien, es mejor realizar cada operación a granel. Este enfoque proporciona más claves de las que necesita (de manera masiva, en este caso porque exageré demasiado la necesidad de sobreestimar) y, por lo tanto, es menos eficiente en cuanto a memoria, pero sigue siendo muy rápido.
Sabemos que todas sus longitudes de cadena están entre 12 y 20. Simplemente genere todas las longitudes de cadena de una vez. Sabemos que el
set
final tiene la posibilidad de recortar la lista final de cadenas, por lo que deberíamos anticiparnos y hacer más "longitudes de cadena" de las que necesitamos. 20,000 extra es excesivo, pero es para hacer un punto:string_lengths = np.random.randint(12, 20, 60000)
En lugar de crear todas nuestras secuencias en un bucle
for
, cree una lista 1D de caracteres que sea lo suficientemente larga para ser cortada en 40,000 listas. En el peor de los casos, todas las longitudes de cadena aleatorias en (1) fueron la longitud máxima de 20. Eso significa que necesitamos 800,000 caracteres.pool = list(string.ascii_letters + string.digits)
random_letters = np.random.choice(pool, size=800000)
Ahora solo necesitamos cortar esa lista de caracteres aleatorios. Usando
np.cumsum()
podemos obtener índices de inicio secuenciales para las sublistas, ynp.roll()
desplazará esa matriz de índices en 1, para dar una matriz correspondiente de índices finales.starts = string_lengths.cumsum()
ends = np.roll(string_lengths.cumsum(), -1)
Cortar la lista de caracteres aleatorios por los índices.
final = [''''.join(random_letters[starts[x]:ends[x]]) for x, _ in enumerate(starts)]
Poniendolo todo junto:
def numpy_approach():
pool = list(string.ascii_letters + string.digits)
string_lengths = np.random.randint(12, 20, 60000)
ends = np.roll(string_lengths.cumsum(), -1)
starts = string_lengths.cumsum()
random_letters = np.random.choice(pool, size=800000)
final = [''''.join(random_letters[starts[x]:ends[x]]) for x, _ in enumerate(starts)]
return final
Y los resultados de timeit
:
322 ms ± 7.97 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)