Dig Deeper
Mass Name Generation
Mass Name Generation
We first generate all the possible names, shuffle them, and then either use next (the simplest way) or maintain a current_index to get the name.
Note that selecting randomly from the list of all names would be incorrect, as there is a possibility of the name being repeated.
One possible way to do it:
from itertools import product
from random import shuffle
from string import ascii_uppercase
letter_pairs = (''.join(p) for p in product(ascii_uppercase, ascii_uppercase))
numbers = (str(i).zfill(3) for i in range(1000))
names = [l + n for l, n in product(letter_pairs, numbers)]
shuffle(names)
NAMES = iter(names)
class Robot(object):
def __init__(self):
self.reset()
def reset(self):
self.name = next(NAMES)
The first few lines of the mass name generation uses itertools.product.
The resultant code is a simplification of:
letter_pairs = (''.join((l1, l2)) for l1 in ascii_uppercase for l2 in ascii_uppercase)
numbers = (str(i).zfill(3) for i in range(1000))
names = [l + n for l in letter_pairs for n in numbers]
After the name generation, the names are shuffled - using the default seed in the random module (the current timestamp).
When the tests reseed random, this has no effect as the names were shuffled before that.
We then set NAMES to the iterable of names, and in reset, set the robot’s name to the next(name).
If you are interested, you can read more on iter and next.
Unlike the on the fly approach, this has a relatively short “generation” time, because we are merely giving the next name instead of generating it.
However, this has a huge startup memory and time cost, as 676,000 strings have to be calculated and stored.
For an approximate calculation, 676,000 strings * 5 characters / string * 1 byte / character gives 3380000 bytes or 3.38 MB of RAM - and that’s just the memory aspect of it.
Sounds small, but this might be a relatively significant startup cost.
Thus, this approach is inefficient in cases where only a small number of names are needed and the time to set/reset the robot isn’t crucial.
Find name on the fly
Find name on the fly
We generate the name on the fly and add it to a cache or a store, checking to make sure that the generated name has not been used previously.
A possible way to implement this:
from string import ascii_uppercase, digits
from random import choices
cache = set()
class Robot:
def __get_name(self):
return ''.join(choices(ascii_uppercase, k=2) + choices(digits, k=3))
def reset(self):
while (name := self.__get_name()) in cache:
pass
cache.add(name)
self.name = name
def __init__(self):
self.reset()
We use a set for the cache as it has a low access time, and because we do not need the preservation of order or the ability to access by index.
Using choices is one of the many ways to generate the name.
Another way might be to use randrange along with zfill for the number part, and a double random.choice / random.choice on itertools.product to generate the letter part.
The first is shorter, and best utilizes the Python standard library.
As we are using a while loop to check for the name generation, it is convenient to store the local name using the walrus operator.
It’s also possible to find the name once before the loop, and then find it again inside the loop, but that would be an unnecessary repetition:
def reset(self):
name = self.__get_name()
while name in cache:
name = self.__get_name()
cache.add(name)
self.name = name
A helper method (private in this case) makes your code cleaner, but it’s equally valid to have the code in the loop itself:
def reset(self):
while (name := ''.join(choices(ascii_uppercase, k=2) + choices(digits, k=3))) in cache:
pass
cache.add(name)
self.name = name
We call reset from __init__ - it is syntactically valid to do it the other way around, but it is not considered good practice to call dunder methods directly.
This has almost no startup time and memory, apart from declaring an empty set.
Note that the generation time is the same as the mass generation approach, as a similar method is used.
However, as the name is generated at the time of setting/resetting, the method time itself is higher.
In the long run, if many names are generated, this is inefficient, since collisions will start being generated more often than unique names.
Source: Exercism python/robot-name