8
\$\begingroup\$

I am coming from a Ruby background and I am learning Python. This is a method I've created to generate a URL safe unique key:

import random;

def generate_unique_key():
    array = []
    for letter in range(97,123):
        array.append(chr(letter))
    for letter in range(65,91):
        array.append(chr(letter))
    for number in range(0,10):
        array.append(number)
    for char in ["-", ".", "_", "~"]:
        array.append(char)

    random_values = random.sample(array, 15)
    random_values = map(lambda x: str(x), random_values)
    return "".join(random_values)

print(generate_unique_key())

Coming from a Ruby background I was certainly puzzled first at not being able call join directly on my list, but having to call it from a string instance.

Also in Ruby I would have written something similar to random.sample(array, 15).join("") directly without having to convert them all to string, but this I believe this is how join() works.

While this works, how can this function be more Pythonic?

\$\endgroup\$
  • \$\begingroup\$ @Peilonrayz Apologies, this is indeed python 3. I was testing on a python 2.7 interpreter which is why the print looks like that, but I will be using it in a python 3.7 environment \$\endgroup\$ – Ranhiru Jude Cooray Jul 26 '18 at 16:12
  • 3
    \$\begingroup\$ It generates a random key, but why would you say that it's unique? What are you using it for? \$\endgroup\$ – 200_success Jul 26 '18 at 16:18
  • \$\begingroup\$ lucumr.pocoo.org/2011/7/9/python-and-pola Not the best English, but a good read nonetheless than I think will help clear things up for you. \$\endgroup\$ – jpmc26 Jul 26 '18 at 20:42
  • \$\begingroup\$ @200_success: Alright, I posted an answer based on my earlier comment. Originally thought that my criticism was a little out of scope for CR but upon further consideration I came to a different conclusion. Thanks for the reminder! \$\endgroup\$ – David Foerster Jul 26 '18 at 21:31
15
\$\begingroup\$
  1. You can pass str to map rather than a lambda.
  2. You can remove the need for map if you change your third loop to append str(number).
  3. You can use string to remove the need for chr and int in the first three loops. By using:
    1. string.ascii_letters:
      abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
    2. string.digits:
      0123456789
  4. You can combine all your loops into one string concatenation.
  5. Don't use ; to end lines.
  6. You can define array as a constant outside the function.
  7. I recommend using Python 3, or changing print to be Python 3 compatible.
import random
import string

CHARACTERS = (
    string.ascii_letters
    + string.digits
    + '-._~'
)

def generate_unique_key():
    return ''.join(random.sample(CHARACTERS, 15))

print(generate_unique_key())

You can also use the secrets library (thanks @MathiasEttinger) which is a CSRNG version of random. It also contains the function secrets.token_urlsafe (thanks @Graipher) which generates a random string which is safe for use in URLs. Allowing:

def generate_unique_key(size=15):
    return secrets.token_urlsafe(size)[:size]
\$\endgroup\$
  • 10
    \$\begingroup\$ 8. Use secrets. \$\endgroup\$ – Mathias Ettinger Jul 26 '18 at 18:35
  • 6
    \$\begingroup\$ @MathiasEttinger And specifically, secrets.token_urlsafe. \$\endgroup\$ – Graipher Jul 26 '18 at 20:42
  • \$\begingroup\$ I recommend using Python 3, or changing print to be Python 3 compatible. I consider this bad advice. print(1, 2) has a very different meaning in python 2 and python 3. If you're going to give this advice - consider adding the future import to your example. \$\endgroup\$ – Shadow Jul 27 '18 at 5:17
  • 1
    \$\begingroup\$ @Shadow I'm unsure what part of that is bad advice. You can use future, or migrate away from the should be dead version. \$\endgroup\$ – Peilonrayz Jul 27 '18 at 8:13
  • \$\begingroup\$ If you want to make the print Python 3 compatible, you should use from __future__ import print_function. \$\endgroup\$ – jpmc26 Apr 25 at 14:18
9
\$\begingroup\$

Sampling method

sample() is the wrong kind of sampling here since it draws k random samples from the given population/alphabet without repetition. Instead you should should use choices() which allows repetition of the same items.

Normally you want as much entropy as possible in the given “space” (here: k = 15 items chosen from N = 66 Arabic digits, basic Latin letters and some punctuation). If you disallow repetitions you reduce the size of the key space (although not significantly1) and risk more (frequent) collisions at no benefit and more costly sample computation2.

Source of (pseudo-)randomness

You should also consider that the random module’s default (pseudo-)random number generator is not normally “safe”. See Gaipher’s comment for a possible safe alternative.

Avoid magic numbers, allow parameters if possible

Additionally you should avoid unexplained "magic numbers" (like 15) and allow parameters (with sensible defaults) for arbitrarily chosen values (like length).

Resulting code

def generate_unique_key(length=15):
    array = # ... construct character set
    return "".join(random.choices(array, k=length))

For Python versions prior to 3.6 you can provide a simple implementations of choices() yourself:

import random
try:
    random_choices = random.choices
except AttributeError:
    import itertools
    def random_choices(population, *, k=1):
        return map(random.choice, itertools.repeat(population, k))

1 Key space comparison:

  • with repetition: \$N^k = 66^{15} \approx 2.0 \cdot 10^{27}\$
  • without repetition: \$\prod^{N}_{i=N-k+1} i = \frac{!N}{!(N - k)} = \frac{!66}{!(66 - 15)} \approx 3.5 \cdot 10^{26}\$

2 An optimal algorithm for random samples without repetition is more complex than one with repetition and requires either \$O(k)\$ intermediate storage or \$O(k^2)\$ runtime (with k being the sample size) to keep track of previously drawn items.

\$\endgroup\$
2
\$\begingroup\$

most pythonic way to generate a URL safe unique key or token is to use secrets module.

Use secrets.token_urlsafe it will return a secure random URL-safe text string. The secrets module uses synchronization methods to ensure that no two processes can obtain the same data at the same time.

import secrets
print(secrets.token_urlsafe(32))

output:

GzWfp5xCcuFsnEUb9qqN_v94_XOx9hPwSGszdx4rNBk

you can change the byte size as per your requirement

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.