From ec3f168a09b54d8ee41e44f8fca70a582f9f0ddf Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Mon, 14 Jul 2025 07:22:10 +0000 Subject: [PATCH] Doc: match statement in style guide (#5187) * Test: add micro benchmark for match * Doc: add 'match' to python style guide --- docs/style.md | 4 +++ test/benchmark/match.py | 66 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 test/benchmark/match.py diff --git a/docs/style.md b/docs/style.md index 81853f41..5333155d 100644 --- a/docs/style.md +++ b/docs/style.md @@ -29,6 +29,10 @@ * New classes, attributes, and methods in core code should have docstrings that follow [reST style](https://peps.python.org/pep-0287/). * Worlds that do not follow PEP8 should still have a consistent style across its files to make reading easier. +* [Match statements](https://docs.python.org/3/tutorial/controlflow.html#tut-match) + may be used instead of `if`-`elif` if they result in nicer code, or they actually use pattern matching. + Beware of the performance: they are not `goto`s, but `if`-`elif` under the hood, and you may have less control. When + in doubt, just don't use it. ## Markdown diff --git a/test/benchmark/match.py b/test/benchmark/match.py new file mode 100644 index 00000000..ccb600c0 --- /dev/null +++ b/test/benchmark/match.py @@ -0,0 +1,66 @@ +"""Micro benchmark comparing match as "switch" with if-elif and dict access""" + +from timeit import timeit + + +def make_match(count: int) -> str: + code = f"for val in range({count}):\n match val:\n" + for n in range(count): + m = n + 1 + code += f" case {n}:\n" + code += f" res = {m}\n" + return code + + +def make_elif(count: int) -> str: + code = f"for val in range({count}):\n" + for n in range(count): + m = n + 1 + code += f" {'' if n == 0 else 'el'}if val == {n}:\n" + code += f" res = {m}\n" + return code + + +def make_dict(count: int, mode: str) -> str: + if mode == "value": + code = "dct = {\n" + for n in range(count): + m = n + 1 + code += f" {n}: {m},\n" + code += "}\n" + code += f"for val in range({count}):\n res = dct[val]" + return code + elif mode == "call": + code = "" + for n in range(count): + m = n + 1 + code += f"def func{n}():\n val = {m}\n\n" + code += "dct = {\n" + for n in range(count): + code += f" {n}: func{n},\n" + code += "}\n" + code += f"for val in range({count}):\n dct[val]()" + return code + return "" + + +def timeit_best_of_5(stmt: str, setup: str = "pass") -> float: + """ + Benchmark some code, returning the best of 5 runs. + :param stmt: Code to benchmark + :param setup: Optional code to set up environment + :return: Time taken in microseconds + """ + return min(timeit(stmt, setup, number=10000, globals={}) for _ in range(5)) * 100 + + +def main() -> None: + for count in (3, 5, 8, 10, 20, 30): + print(f"value of {count:-2} with match: {timeit_best_of_5(make_match(count)) / count:.3f} us") + print(f"value of {count:-2} with elif: {timeit_best_of_5(make_elif(count)) / count:.3f} us") + print(f"value of {count:-2} with dict: {timeit_best_of_5(make_dict(count, 'value')) / count:.3f} us") + print(f"call of {count:-2} with dict: {timeit_best_of_5(make_dict(count, 'call')) / count:.3f} us") + + +if __name__ == "__main__": + main()