Вот решение, которое очень похоже на решение Андрея Беренды, но работает, назначая атрибут функциональному объекту, а не используя нелокальную переменную. Практическое отличие состоит в том, что предыдущие аргументы функции становятся доступными извне, что может помочь в целях отладки.
from functools import wraps
def dont_run_twice(func):
@wraps(func)
def wrapper(*args, **kwargs):
if (args, kwargs) == wrapper._prev_args:
return None
wrapper._prev_args = args, kwargs
return func(*args, **kwargs)
wrapper._prev_args = None
return wrapper
Пример:
>>> @dont_run_twice
... def f(x, y):
... return x + y
...
>>> f(1, 2)
3
>>> f(3, 4)
7
>>> f(3, 4) # returns None
>>> f(1, 2)
3
>>> f._prev_args
((1, 2), {})
Обратите внимание, что оба решения имеют небольшой недостаток: вы можно вызывать с теми же значениями аргументов, если вы предоставляете их как позиционные аргументы, а затем ключевые аргументы (или наоборот):
>>> f(5, 6)
11
>>> f(x=5, y=6)
11
В качестве обходного пути вы можете объявить упакованную функцию только с позиционным (или ключевым словом) -only) аргументы:
# positional-only, requires Python 3.8+
@dont_run_twice
def f(x, y, /):
return x + y
# keyword-only
@dont_run_twice
def g(*, x, y):
return x + y
Обратите также внимание, что если предыдущие аргументы являются изменяемыми, то могут происходить странные вещи:
>>> a = [1, 2]
>>> b = [3, 4]
>>> f(a, b)
[1, 2, 3, 4]
>>> a[:] = [5, 6]
>>> b[:] = [7, 8]
>>> f([5, 6], [7, 8]) # returns None
Второй вызов функции здесь возвращает None
несмотря на новые аргументы, не совпадающие с исходными аргументами ни по значению, ни по идентичности; они равны текущим значениям исходных аргументов, которые были изменены после их использования в качестве аргументов. Это может привести к довольно тонким ошибкам, но, к сожалению, нет простого способа исправить это.