Problem 537762 · medium · Level 05 Advanced Algorithms & Graphs

A Sampler You Cannot Call Wrongly

py-api-design · keyword-only arguments · validation · random.Random · sampling

A survey toolkit has this sampling helper, and every caller gets it wrong somehow:

def sample(rows, k, rep, w, s):
    import random
    if s: random.seed(s)                   # reseeds the whole program's generator
    if rep:
        return random.choices(rows, w, k=k)
    rows.sort(key=lambda r: str(r))        # changes the caller's list
    return random.sample(rows, k) if k <= len(rows) else None

What does sample(data, 5, True, None, 0) do? Write its replacement, sample_rows(rows, k, *, replace=False, weights=None, seed=None):

  • rows is any iterable (a list, a tuple, a generator); it is never changed. The result is always a new list of k rows.
  • Without replacement (the default), the result is rng.sample(pool, k), where pool is list(rows) in its original order. With replace=True it is rng.choices(pool, weights=weights, k=k).
  • rng is the global random module when seed is None, and a new random.Random(seed) otherwise, so a seeded call is reproducible and does not disturb the global generator.
  • These calls raise ValueError with a clear message: k < 0; k > len(pool) without replacement; k > 0 with an empty pool and replacement; weights given without replace=True; weights whose length differs from the number of rows.
  • replace, weights and seed are keyword-only, so sample_rows(data, 5, True) raises TypeError.

Setup helpers, available with Run: households(n, seed) generates survey rows, raises(fn, *args, **kwargs) returns the name of the exception a call raises (or None), and untouched(data) calls your function several ways and reports whether data and the global generator were left alone.

Examples

Input:  sample_rows(["a", "b", "c", "d", "e"], 3, seed=7)
Output: ["c", "b", "d"]

Input:  sample_rows((r for r in "xyz"), 4, replace=True, weights=[0, 0, 1], seed=1)
Output: ["z", "z", "z", "z"]

Input:  raises(sample_rows, [1, 2], 3), raises(sample_rows, [1, 2], 1, True)
Output: ("ValueError", "TypeError")

Constraints

  • Up to 10**5 rows; seed is None or a whole number (including 0).

Goals

  • Replace positional flags with keyword-only options and sensible defaults
  • Validate combinations of options and fail with clear errors
  • Never change the caller's data, and always return the same kind of value
Starting Python…