Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions exercises/practice/robot-name/.approaches/introduction.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
# Introduction
Robot Name in Python is an interesting exercise for practising randomness.

Robot Name in Python is an interesting exercise for practicing randomness.

## General Guidance
Two ways immedietely come to mind: generate all the possible names and then return them sequentially, or generate a random name and ensure that it's not been previously used.

Two ways immediately come to mind: generate all the possible names and then return them sequentially, or generate a random name and ensure that it has not been previously used.

Randomness can be a little, well, random, so **it's very easy to have an incorrect solution and still pass the tests**. It's strongly recommended to submit your solution for Code Review.

## Approach: mass name generation

We'd first have to generate all the possible names, shuffle them, and then use `next` (the simplest way) or maintain a `current_index` and get the name.
Here's a possible way to do it:

Expand All @@ -26,14 +29,17 @@ class Robot(object):
def reset(self):
self.name = next(NAMES)
```

Note that selecting randomly from the list of all names would be incorrect, as there's a possibility of the name being repeated.
For more detail and explanation of the code, [read here][approach-mass-name-generation].

## Approach: name on the fly
Another approach is to generate the name on the fly and add it to a cache or a store, and checking if the generated name hasn't been used previously.

Another approach is to generate the name on the fly and add it to a cache or a store, checking if the generated name hasn't been used previously.


A possible way to implement this:

```python
from string import ascii_uppercase, digits
from random import choices
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# Mass Name Generation
We'd first have to generate all the possible names, shuffle them, and then use `next` (the simplest way) or maintain a `current_index` and get the name.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style suggestion:

Blank line after headings (currently missing).

Tend to trailing white space, there are two occurrences where this happens... I will mention in review here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be OK if I also added blank lines before/after the code snippets in the markdowns? It should not make a difference when rendered, but I find it easier to read and edit the markdown that way -- is that something that works for everybody or just my personal quirk?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be OK if I also added blank lines before/after the code snippets in the markdowns?

Please do. Not just your quirk: mine as well. Didn't do it on my first pass, because I didn't want to bog things down. But absolutely!

Note that selecting randomly from the list of all names would be incorrect, as there's a possibility of the name being repeated.

Here's a possible way to do it:
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:

```python
from itertools import product
Expand All @@ -25,25 +26,27 @@ class Robot(object):

The first few lines of the mass name generation uses [`itertools.product`][itertools-product].
The resultant code is a simplification of:

```python
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`][random-seed] in the `random` module (the current timestamp).
After the name generation, the names are shuffled - using the [default `seed`][random-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'd like, read more on [`iter` and `next`][iter-and-next].
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`][iter-and-next].

Unlike the on the fly approach, this has a relatively short "generation" time, because we're 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.
Unlike the [on the fly approach][approach-name-on-the-fly], 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 it's relatively very expensive at the beginning.
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.

[random-seed]: https://docs.python.org/3/library/random.html#random.seed
[iter-and-next]: https://www.programiz.com/python-programming/methods/built-in/iter
[itertools-product]: https://www.hackerrank.com/challenges/itertools-product/problem
[approach-name-on-the-fly]: https://exercism.org/tracks/python/exercises/robot-name/approaches/name-on-the-fly
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
# Find name on the fly
We generate the name on the fly and add it to a cache or a store, and checking if the generated name hasn't been used previously.

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:

```python
from string import ascii_uppercase, digits
from random import choices
Expand All @@ -10,7 +12,7 @@ cache = set()


class Robot:
def __get_name(self):
def __get_name(self):
return ''.join(choices(ascii_uppercase, k=2) + choices(digits, k=3))

def reset(self):
Expand All @@ -19,18 +21,30 @@ class Robot:
cache.add(name)
self.name = name

def __init__(self):
def __init__(self):
self.reset()
```
We use a `set` for the cache as it has a low access time, and we don't need the preservation of order or the ability to be indexed.

This way is merely one of the many to generate the name.
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.
This is the shortest way, and best utilizes the Python standard library.
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][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:

```python
def reset(self):
name = self.__get_name()
while name in cache:
name = self.__get_name()
cache.add(name)
self.name = name
```

As we're using a `while` loop to check for the name generation, it's convenient to store the local `name` using the [walrus operator][walrus-operator].
It's also possible to find the name before the loop and find it again inside the loop, but that would unnecessary repetition.
A helper method ([private][private-helper-methods] in this case) makes your code cleaner, but it's equally valid to have the code in the loop itself:

```python
def reset(self):
while (name := ''.join(choices(ascii_uppercase, k=2) + choices(digits, k=3))) in cache:
Expand All @@ -39,14 +53,15 @@ def reset(self):
self.name = name
```

We call `reset` from `__init__` - it's syntactically valid to do it the other way round, but it's not considered good practice to call [dunder methods][dunder-methods] directly.
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][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.
Note that the _generation_ time is the same as the [mass generation approach][approach-mass-name-generation], 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.
In the long run, if many names are generated, this is inefficient, since collisions will start being generated more often than unique names.

[walrus-operator]: https://realpython.com/python-walrus-operator/
[private-helper-methods]: https://www.geeksforgeeks.org/private-methods-in-python/
[dunder-methods]: https://dbader.org/blog/python-dunder-methods
[dunder-methods]: https://dbader.org/blog/python-dunder-methods
[approach-mass-name-generation]: https://exercism.org/tracks/python/exercises/robot-name/approaches/mass-name-generation
Loading