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()
-
\$\begingroup\$ As long as you're using random, you should also make use of random.choices. \$\endgroup\$Teepeemm– Teepeemm2021年12月09日 20:47:26 +00:00Commented Dec 9, 2021 at 20:47
1 Answer 1
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'))
-
\$\begingroup\$
make_float = random.uniform
is incorrect. OP wantsstart <= returns < stop
so that means a multiple ofrandom.random()
. \$\endgroup\$Reinderien– Reinderien2021年12月09日 20:03:21 +00:00Commented Dec 9, 2021 at 20:03 -
\$\begingroup\$ @Reinderien Thanks for pointing that out. Edited my answer accordingly. \$\endgroup\$FMc– FMc2021年12月09日 20:20:45 +00:00Commented Dec 9, 2021 at 20:20