V některých situacích můžeme při zpracování Django signálu narazit na problém rekurzivního volání jeho receiveru. Typicky se tak děje například u post_save
signálu, jehož receiver upraví a uloží instanci modelu (přidá timestamp, upraví data…).
V minulosti jsem často (špatně!) používal následující pattern:
from django.db.models.signals import post_save
from django.dispatch import receiver
@receiver(post_save, sender=SomeModel)
def do_something_with_instance(instance, **kwargs):
"""
Do not do this!
"""
post_save.disconnect(do_something_with_instance, sender=SomeModel)
instance.foo = bar()
instance.save()
post_save.connect(do_something_with_instance, sender=SomeModel)
Na první pohled se může zdát, že jde o elegantní řešení — přijmeme signál, dočasně odpojíme receiver, zpracujeme, co potřebujeme, a receiver znovu připojíme. Kód má však několik nedostatků:
- Zatímco je signál odpojený, nevolá se receiver pro ostatní instance (pro ilustraci si představme, že funkce
bar()
běží „dlouho“). - Pokud mezi odpojením a opětovným připojením nastane nějaká chyba, signál už zůstane navždy odpojený.
Toto téma je podrobně rozebráno na http://stackoverflow.com/a/28369908/752142. Ze stejného vlákna jsem se inspiroval a následující řešení s úspěchem používám ve svých projektech:
from functools import wraps
def prevent_receiver_recursion(receiver_function):
dirty_attr_name = '_dirty_{}'.format(receiver_function.__name__)
@wraps(receiver_function)
def wrapper(sender, instance=None, *args, **kwargs):
if not instance or hasattr(instance, dirty_attr_name):
# Do nothing
return
try:
setattr(instance, dirty_attr_name, True)
receiver_function(sender, instance, *args, **kwargs)
finally:
delattr(instance, dirty_attr_name)
return wrapper
Myšlenka je jednoduchá, po dobu vykonávání receiveru má instance nastavený atribut, který ji „zamkne“. Použití dekorátoru je následující:
from django.db.models.signals import post_save
from django.dispatch import receiver
@receiver(post_save, sender=SomeModel)
@prevent_receiver_recursion
def do_something_with_instance(instance, **kwargs):
instance.foo = bar()
instance.save()