Как реализовать счетчик с функцией инкремента в Python
Что проверяют
Простая задача служит входом в разговор о состоянии: классы, замыкания, инкапсуляция и потокобезопасность. Почти всегда следом спрашивают, что будет, если increment вызывать из нескольких потоков.
Как отвечать
Покажите два-три способа и объясните выбор:
- класс:
__init__задаётself._value = start, методincrement(self, step=1)увеличивает значение, свойствоvalueчерез@propertyотдаёт его без сеттера, чтобы снаружи счётчик нельзя было переписать. Уместно добавить__repr__иreset(); - замыкание: внешняя функция хранит
count = 0, внутренняя объявляетnonlocal countи увеличивает его. БезnonlocalполучитеUnboundLocalError, потому что присваивание делает имя локальным; itertools.count(start, step): готовый бесконечный итератор, значение берётся черезnext();collections.Counter: это не счётчик-переменная, а словарь частот элементов, стоит сразу развести эти понятия.
Затем главное, потокобезопасность. self._value += 1 это не одна операция, а чтение, сложение и запись, и поток может переключиться между ними даже при GIL. Под нагрузкой из нескольких потоков часть инкрементов теряется. Решение: threading.Lock и with self._lock: вокруг изменения. В сборке CPython без GIL, которая появилась в 3.13, такие гонки проявляются ещё чаще. Для нескольких процессов подойдёт multiprocessing.Value с get_lock(), для нескольких серверов нужен внешний атомарный счётчик, например INCR в Redis. В asyncio внутри одного потока += без await посередине безопасен.
Закончите тестом на pytest: начальное значение, шаг, сброс и отдельный тест, где десяток потоков делает по тысяче инкрементов, а итог сверяется с ожидаемым.
Где спотыкаются
Утверждают, что GIL делает += атомарным. Он защищает внутренности интерпретатора, а не вашу последовательность операций.
Хранят значение в атрибуте класса и увеличивают через Counter.count. Все экземпляры делят одно число, и счётчики начинают мешать друг другу.
Делают публичный атрибут и сеттер «на всякий случай». Тогда счётчик можно уменьшить или записать в него строку, и смысл инкапсуляции теряется.