4
\$\begingroup\$

I want to implement a random word generator that would not use Import statements. The solution is working for a *nix system. The solution does yield a random word, but please suggest:

  1. How to make the function optimally/safely return words with a higher position in the list(e.g. now we're almost guaranteed to return an 'A' word)
  2. How to get a word with min/max length instead of length
  3. Should I break this into smaller functions?
  4. How to make the code more readable/higher performance
  5. Is there a better approach to this problem? How to tell which modules are better off imported instead of implemented from scratch(except for the built-ins)?
def generate_random_word(length=8):
    """
    Return a random string from /usr/share/dict/words on a *nix system
    """
    with open('/usr/share/dict/words', 'r') as words_file, open("/dev/random", 'rb') as random_file:
        random_words = words_file.read().split('\n')
        size_words = [x for x in random_words if len(x) == length]
        for _ in range(0, 100):
            while True:
                try:
                    random_word = size_words[(int.from_bytes(random_file.read(1), 'big'))]
                except IndexError:
                    continue
                break


    return random_word
\$\endgroup\$

2 Answers 2

4
\$\begingroup\$

I am surprised that you chose to use /dev/random although I can understand that for some purposes like generating private keys a source of strong entropy is preferred and there are even devices for that. Then this discussion could interest you. But the built-in random module in python should be sufficient here.

To read the file to a list of items you can just do this:

with open('/usr/share/dict/words', 'r') as f:
    lines = f.read().splitlines()

Good thing is that you are using the context manager for the open function.

Try to avoid IndexError rather than handle it and ignore it.

If your file is small (check size before opening) you can be lazy, load all items to a list, then filter it, and return one item at random:

def generate_random_word(min_length=8, max_length=13):
    with open('/usr/share/dict/words', 'r') as f:
        lines = f.read().splitlines()

    # select words matching desired length
    # selection = [line for line in lines if len(line) <= max_length and len(line) >= min_length ]
    selection = [line for line in lines if min_length <= len(line) <= max_length]
    # no match found
    if len(selection) == 0:
        return None

    return random.choice(selection)

If no matching item is found (or the file is empty) then I chose to return None.

If you want to filter lines at the source the implementation could be like this:

def generate_random_word(min_length=8, max_length=13):
    with open('/usr/share/dict/words', 'r') as f:
        selection = [line for line in f.read().splitlines() if min_length <= len(line) <= max_length]

    # no match found
    if len(selection) == 0:
        return None

    return random.choice(selection)

The file has to exist but it can be empty, then there is no error but the function will return None. Use os.exists to test that the file is present.

Yes, there is an import but it is a built-in module, does not require installation of a third-party module with PIP. It is also portable and not just Unix.

However, if you insist on /dev/random and want no import one thing you could do is retrieve a random integer like what you are doing now and use it in a modulo-type fashion against the list of matching items, to pick one word at random. Be careful with implementation as you may introduce unwanted bias in the selection. Random functions exist for a reason.

While it is possible to rely solely on /dev/random reimplementing the functionality with decent randomization will result in more code and reinventing the wheel.

\$\endgroup\$
2
  • \$\begingroup\$ Is there a better way to determine if a module is a built-in than what's described here stackoverflow.com/questions/4922520/… ? \$\endgroup\$ Jun 10, 2020 at 15:45
  • 1
    \$\begingroup\$ @KateVelasquez: Check out docs.python.org/3/library for a non-programmatic way. It just lists all modules in the standard library, which are included in all Python installations. \$\endgroup\$
    – Graipher
    Jun 10, 2020 at 15:54
1
\$\begingroup\$

Even if you don't want to use the standard library random module, you might want to take a look at their implementation and do something similar. This way you can always replace your random class with the standard library one. Here I directly copied the relevant methods:

def _random(numbytes):
    with open('/dev/random', 'rb') as f:
        return f.read(numbytes)

class Random:
    def getrandbits(self, k):
        """getrandbits(k) -> x.  Generates an int with k random bits."""
        if k <= 0:
            raise ValueError('number of bits must be greater than zero')
        numbytes = (k + 7) // 8                       # bits / 8 and rounded up
        x = int.from_bytes(_random(numbytes), 'big')
        return x >> (numbytes * 8 - k)                # trim excess bits

    def _randbelow(self, n):
        "Return a random int in the range [0,n).  Raises ValueError if n==0."

        getrandbits = self.getrandbits
        k = n.bit_length()  # don't use (n-1) here because n can be 1
        r = getrandbits(k)          # 0 <= r < 2**k
        while r >= n:
            r = getrandbits(k)
        return r

    def choice(self, seq):
        """Choose a random element from a non-empty sequence."""
        try:
            i = self._randbelow(len(seq))
        except ValueError:
            raise IndexError('Cannot choose from an empty sequence') from None
        return seq[i]

You can then use it like this:

random = Random()

def generate_random_word(length=8):
    with open('/usr/share/dict/words') as file:
        words = [word for line in file if len(word := line.strip()) == length]
    return random.choice(words)

If you need to do this more than once, then you might want to read the list of words only once and not every time you run the function. You can achieve this by using a nested function (you could also use a global variable or a class):

def random_word_generator(length=8):
    with open('/usr/share/dict/words') as file:
        words = [word for line in file if len(word := line.strip()) == length]
    def _generate():
        return random.choice(words)
    return _generate

Which you can use like this:

random = Random()
generate_random_word = random_word_generator()
random_word = generate_random_word()
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

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