2
\$\begingroup\$

I created a small library Python library to randomly create primitive types and collections. The primitive types are int, float, and string. The collections are tuples of 2, tuples of 3, a list, a 2d list, a set, and a dictionary. The odds are all equal in the random library. Notably, the collections take a function to call which allows a random function of primitive types inside. Below is the code.

import random


def make_int(start, stop):
   """
   :param start: int, start is not None
   :param stop: int, stop is not None and stop > start
   :return: int, return is not None and start <= returns < stop
   """
   return random.randrange(start, stop)


def test_make_int():
   start = 0
   stop = 10
   assert start <= make_int(start, stop) < stop


def make_float(start, stop):
   """
   :param start: float, start is not None
   :param stop: float, stop is not None and stop >= 0
   :return: float, returns is not None and start <= returns < stop
   """
   return random.randrange(start, stop)


def test_make_float():
   start = 0.0
   stop = 10.0
   assert start <= make_float(start, stop) < stop


def make_str(rows, string):
   """
   :param rows: int, rows is not None and rows >= 0
   :param string: str, string is not None
   :return: str, returns is not None and returns[row] in string
   """
   return [random.choice(string) for _ in range(rows)]


def test_make_str():
   letters = "abcd"
   rows =  4
   answer = make_str(rows, letters)
   for char in answer:
       assert char in letters


def make_tuple2(random_inside):
   """
   :param random_inside: Callable[[], Any], random_inside is not None
   :return: Tuple[Any, Any], returns is not None
   """
   return random_inside(), random_inside()


def test_make_tuple2():
   def dice():
       return 4
   assert make_tuple2(dice) == (4, 4)


def make_tuple3(random_inside):
   """
   :param random_inside: Callable[[], Any], random_inside is not None
   :return: Tuple[Any, Any, Any], returns is not None
   """
   return random_inside(), random_inside(), random_inside()


def test_make_tuple3():
   def dice():
       return 4
   assert make_tuple3(dice) == (4, 4, 4)


def make_1d_list(rows, random_inside):
   """
   :param rows: int,rows is not None and rows >= 0
   :param random_inside: Callable[[], Any], random_inside is not None
   :return: List[Any], returns is not None and len(returns) == None
   """
   return [random_inside() for _ in range(rows)]


def test_make_1d_list():
   def random_str():
       return "a"
   rows = 2
   assert make_1d_list(rows, random_str) == ["a", "a"]


def make_2d_list(rows, cols, random_inside):
   return [[random_inside() for _ in range(rows)] for _ in range(cols)]


def test_make_2d_list():
   def number():
       return 9
   rows = 2
   cols = 2
   assert make_2d_list(rows, cols, number) == [[9, 9], [9, 9]]





def make_set(rows, random_inside):
   """
   :param rows: int, rows is not None and rows >= 0
   :param random_inside: Callable[[], Any], random_inside is not None
   :return: Set[Any], returns is not None and len(set) == rows
   """
   answer = set()
   col = 0
   for _ in range(rows):
       col = random.randrange(0, rows)
       answer.add(random_inside())
   return answer


def test_make_set():
   def random_letter():
       return "c"
   rows = 1
   assert make_set(rows, random_letter) == {"c"}


def make_random_dict(number_of_keys, random_key_function, random_value_function):
   """
   :param number_of_keys: int, number_of_keys is not None and number_of_keys >= 0
   :param random_key_function: Callable[[], Any], random_key_function is not None
   :param random_value_function: Callable[[], Any], random_value_function is not None
   :return: Set[Any], returns is not None and len(returns) == number_of_keys
   """
   answer = dict()
   for _ in range(number_of_keys):
       answer[random_key_function()] = random_value_function()
   return answer


def test_make_random_dict():
   def random_key():
       return "a"

   def random_value():
       return 4

   number_of_keys = 3
   make_random_dict(number_of_keys, random_key, random_value) == {"a": 4}


def tests():
   test_make_int()
   test_make_float()
   test_make_str()
   test_make_tuple2()
   test_make_tuple3()
   test_make_1d_list()
   test_make_2d_list()
   test_make_set()
   test_make_random_dict()


if __name__ == "__main__":
   tests()

\$\endgroup\$
1
  • \$\begingroup\$ As long as you're using random, you should also make use of random.choices. \$\endgroup\$
    – Teepeemm
    Commented Dec 9, 2021 at 20:47

1 Answer 1

3
\$\begingroup\$

Indent 4 spaces. Almost no Python programmers indent their code 3 spaces. Four is the most common, in my experimence.

A function should return its advertised type. Your tests are not very thorough. For example, when testing make_float() you merely assert than the return value is within the start-stop range. You do not check whether the function actually returned a float -- and, in fact, it does not. If you want a float, you should be using random.random rather than random.randrange. Similarly, your make_str() function is oddly named. It returns a list of random characters. It would seem more intuitive to return an actual string by joining those characters together.

Don't write code if you merely need an alias. Your make_int() function is a trivial wrapper around functionality in the standard library. Rather than reimplementing it yourself, just create the necessary alias.

import random

make_int = random.randrange

Static language limitations do not apply to Python. Functions like make_tuple2() and make_tuple3() might be appropriate for languages that have one arm tied behind their back, but not for Python, which can easily support the making of random tuples of any size. Also, the usage pattern for these functions is awkward since the caller must pass in a zero-argument function to generate the random values -- even though most of the other functions in your library are not zero-argument functions. All of those limitations are unneeded. You could make similar enhancements to make_1d_list(), make_2d_list(), and make_set().

# A general purpose function to make random tuples.

def make_tuple(n, func, *xs, **kws):
    return tuple(func(*xs, **kws) for _ in range(n))

# Usage examples.

tup = make_tuple(5, make_float, 5.5, 9)
s = make_tuple(3, make_str, 3, 'abce')

Don't rely on trivial tests. Some of your other tests were weak because they did not check enough things -- particularly the data type of the returned value. Your tests for make_random_dict() are even worse: they give the illusion of testing without any real substance. The function is supposed to return a random dict of the requested size, but you've arranged things in the tests so that the only possible return value is {"a": 4}. A nonsense function that returned a constant could pass that test. Either skip testing entirely (a legitimate option under a variety of circumstances) or write actual tests. How does one test a random function? Don't rely on naive equality checks. Instead, probe the other characteristics of the returned data: is it the correct data type; does it have the correct length; do the returned values (or in your case, keys and values) fall within the expected ranges of allowed values? And if you truly need the assurance of an equality check, implement a simple class that defines a __call__() method and use that state-carrying object to emit a predictable sequence of values as it is called by your random-data-generating functions. Here's one illustration (note that the usage illustration relies on suggestions further down regarding the dict-generating function):

class Incrementer:

    def __init__(self, val = 0):
        self.val = val

    def __call__(self, step = 1):
        val = self.val
        self.val += step
        return val

# Usage illustration.
got = make_random_dict(4, Incrementer(3), (2,), Incrementer('a'), ('b',))
exp = {3: 'a', 5: 'ab', 7: 'abb', 9: 'abbb'}
assert got == exp

Naming consistency is important in a library. Why does the dict-generating function have "random" in the name when none of the other functions do? Whenever feasible, choose a naming convention and stick with it.

A few notes on generalizing the dict-generating function. My suggestions for generalizing the tuple and list functions are trickier to apply to make_random_dict(). The complexity comes from the fact that we need to support arbitrary arguments for both key-generation and value-generation. You could require users to pass such arguments explicitly rather than via Python's * and ** mechanisms. You could also limit the usage to positional arguments only, since the rest of the library doesn't need anything else. I'll leave it to you to decide how far to pursue this.

def make_random_dict(n, key_func, key_xs, key_kws, val_func, val_xs, val_kws):
    # But this is a pretty awkward function signature.
    ...

def make_random_dict(n, key_func, key_xs, val_func, val_xs):
    # A bit less awkward.
    return {
        key_func(*key_xs) : val_func(*val_xs)
        for _ in range(n)
    }

# Usage illustration.
d = make_random_dict(4, make_int, (1, 15), make_str, (2, 'abc'))
\$\endgroup\$
2
  • \$\begingroup\$ make_float = random.uniform is incorrect. OP wants start <= returns < stop so that means a multiple of random.random(). \$\endgroup\$
    – Reinderien
    Commented Dec 9, 2021 at 20:03
  • \$\begingroup\$ @Reinderien Thanks for pointing that out. Edited my answer accordingly. \$\endgroup\$
    – FMc
    Commented Dec 9, 2021 at 20:20

Your Answer

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

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