訊號¶
Django 包含一個「訊號分發器」,可協助解耦的應用程式在框架中其他地方發生動作時收到通知。簡而言之,訊號允許特定的發送者通知一組接收者,某些動作已發生。當多個程式碼片段可能對相同的事件感興趣時,它們特別有用。
例如,第三方應用程式可以註冊以在設定變更時收到通知
from django.apps import AppConfig
from django.core.signals import setting_changed
def my_callback(sender, **kwargs):
print("Setting changed!")
class MyAppConfig(AppConfig):
...
def ready(self):
setting_changed.connect(my_callback)
Django 的內建訊號讓使用者程式碼可以在特定動作發生時收到通知。
您也可以定義和發送您自己的自訂訊號。請參閱下方的定義和發送訊號。
警告
訊號提供了鬆散耦合的外觀,但它們可能會快速導致程式碼難以理解、調整和除錯。
在可能的情況下,您應該選擇直接呼叫處理程式碼,而不是透過訊號分發。
監聽訊號¶
若要接收訊號,請使用 Signal.connect()
方法註冊一個接收者函數。當訊號發送時,會呼叫接收者函數。所有訊號的接收者函數會依註冊順序一次一個地被呼叫。
- Signal.connect(receiver, sender=None, weak=True, dispatch_uid=None)[原始碼]¶
- 參數:
receiver – 將連接到此訊號的回呼函數。請參閱接收者函數以取得更多資訊。
sender – 指定要接收訊號的特定發送者。請參閱連接到由特定發送者發送的訊號以取得更多資訊。
weak – Django 預設將訊號處理程式儲存為弱參考。因此,如果您的接收者是本機函數,則可能會被垃圾回收。若要防止這種情況,請在呼叫訊號的
connect()
方法時傳遞weak=False
。dispatch_uid – 在可能發送重複訊號的情況下,訊號接收者的唯一識別碼。請參閱防止重複訊號以取得更多資訊。
讓我們看看這是如何運作的,方法是註冊一個在每個 HTTP 請求完成後被呼叫的訊號。我們將連接到 request_finished
訊號。
接收者函數¶
首先,我們需要定義一個接收者函數。接收者可以是任何 Python 函數或方法
def my_callback(sender, **kwargs):
print("Request finished!")
請注意,該函數會接收一個 sender
引數,以及萬用字元關鍵字引數(**kwargs
);所有訊號處理程式都必須接收這些引數。
我們將在稍後查看發送者,但現在請查看 **kwargs
引數。所有訊號都會發送關鍵字引數,並且可以隨時變更這些關鍵字引數。在 request_finished
的情況下,它被記錄為不發送任何引數,這表示我們可能會想要將訊號處理程式撰寫為 my_callback(sender)
。
這會是錯誤的 – 事實上,如果您這樣做,Django 會拋出錯誤。這是因為任何時候都可能將引數新增到訊號,而您的接收者必須能夠處理這些新的引數。
接收者也可以是異步函數,具有相同的簽名,但使用 async def
宣告
async def my_callback(sender, **kwargs):
await asyncio.sleep(5)
print("Request finished!")
訊號可以同步或異步發送,而接收者會自動調整為正確的呼叫樣式。請參閱發送訊號以取得更多資訊。
新增了對異步接收者的支援。
連接接收者函數¶
有兩種方式可以將接收者連接到訊號。您可以採取手動連接路線
from django.core.signals import request_finished
request_finished.connect(my_callback)
或者,您可以使用 receiver()
裝飾器
以下是如何使用裝飾器連接
from django.core.signals import request_finished
from django.dispatch import receiver
@receiver(request_finished)
def my_callback(sender, **kwargs):
print("Request finished!")
現在,每當請求完成時,都會呼叫我們的 my_callback
函數。
此程式碼應放在何處?
嚴格來說,訊號處理和註冊程式碼可以放在您喜歡的任何地方,但建議避免應用程式的根模組及其 models
模組,以盡量減少匯入程式碼的副作用。
實際上,訊號處理程式通常定義在它們相關的應用程式的 signals
子模組中。訊號接收器會連接在應用程式ready()
方法中的組態類別。如果您正在使用 receiver()
裝飾器,請在 ready()
中匯入 signals
子模組,這將隱式連接訊號處理程式
from django.apps import AppConfig
from django.core.signals import request_finished
class MyAppConfig(AppConfig):
...
def ready(self):
# Implicitly connect signal handlers decorated with @receiver.
from . import signals
# Explicitly connect a signal handler.
request_finished.connect(signals.my_callback)
注意
在測試期間,可能會多次執行 ready()
方法,因此您可能會想要保護您的訊號免於重複,尤其是在您計劃在測試中發送它們的情況下。
連接到由特定發送者發送的訊號¶
某些訊號會發送多次,但您只會對接收這些訊號的特定子集感興趣。例如,請考慮在儲存模型之前發送的 django.db.models.signals.pre_save
訊號。大多數時候,您不需要知道任何模型何時被儲存 – 只需要知道何時儲存特定模型。
在這些情況下,您可以註冊以接收僅由特定發送者發送的訊號。在 django.db.models.signals.pre_save
的情況下,發送者將是被儲存的模型類別,因此您可以指出您只想要由某些模型發送的訊號
from django.db.models.signals import pre_save
from django.dispatch import receiver
from myapp.models import MyModel
@receiver(pre_save, sender=MyModel)
def my_handler(sender, **kwargs): ...
只有在儲存 MyModel
的實例時,才會呼叫 my_handler
函數。
不同的訊號會使用不同的物件作為其發送者;您需要查閱內建訊號文件以了解每個特定訊號的詳細資訊。
防止重複訊號¶
在某些情況下,將接收者連接到訊號的程式碼可能會執行多次。這可能會導致您的接收者函數被註冊多次,因此會針對訊號事件被呼叫多次。例如,在測試期間,可能會多次執行 ready()
方法。更廣泛地說,這發生在您的專案匯入您定義訊號的模組的任何地方,因為訊號註冊會執行多次,因為它被匯入多次。
如果這個行為造成問題(例如,當模型儲存時使用信號發送電子郵件),請傳遞一個獨特的識別符作為 dispatch_uid
參數,以識別您的接收器函式。這個識別符通常會是一個字串,儘管任何可雜湊的物件都可以。最終結果是,您的接收器函式只會針對每個獨特的 dispatch_uid
值綁定到信號一次。
from django.core.signals import request_finished
request_finished.connect(my_callback, dispatch_uid="my_unique_identifier")
定義和發送信號¶
您的應用程式可以利用信號基礎架構,並提供自己的信號。
何時使用自訂信號
信號是隱式函式呼叫,這會使除錯更加困難。如果您的自訂信號的發送者和接收者都在您的專案中,您最好使用顯式函式呼叫。
定義信號¶
所有信號都是 django.dispatch.Signal
的實例。
例如
import django.dispatch
pizza_done = django.dispatch.Signal()
這宣告一個 pizza_done
信號。
發送信號¶
在 Django 中,有兩種同步發送信號的方式。
信號也可以非同步發送。
- Signal.asend(sender, **kwargs)¶
- Signal.asend_robust(sender, **kwargs)¶
要發送信號,請呼叫 Signal.send()
、 Signal.send_robust()
、await Signal.asend()
或 await Signal.asend_robust()
。您必須提供 sender
參數(通常是類別),並且可以提供任意數量的其他關鍵字參數。
例如,以下是發送我們的 pizza_done
信號可能的情況
class PizzaStore:
...
def send_pizza(self, toppings, size):
pizza_done.send(sender=self.__class__, toppings=toppings, size=size)
...
所有四個方法都會回傳一個元組對列表 [(接收器, 回應), ...]
,代表呼叫的接收器函式及其回應值的列表。
send()
與 send_robust()
的區別在於如何處理接收器函式引發的例外。 send()
*不會* 捕捉接收器引發的任何例外;它只是允許錯誤傳播。因此,並非所有接收器都可能會在發生錯誤時收到信號通知。
send_robust()
會捕捉所有衍生自 Python 的 Exception
類別的錯誤,並確保所有接收器都收到信號通知。如果發生錯誤,則錯誤實例會在引發錯誤的接收器的元組對中回傳。
追溯資訊會出現在呼叫 send_robust()
時回傳的錯誤的 __traceback__
屬性上。
asend()
與 send()
類似,但它是一個必須等待的協程
async def asend_pizza(self, toppings, size):
await pizza_done.asend(sender=self.__class__, toppings=toppings, size=size)
...
無論是同步還是非同步,接收器都會正確地調整為使用 send()
或 asend()
。當透過 asend()
呼叫時,同步接收器將使用 sync_to_async()
呼叫。當透過 sync()
呼叫時,非同步接收器將使用 async_to_sync()
呼叫。類似於 中介軟體的案例,以這種方式調整接收器會有一些小的效能成本。請注意,為了減少 send()
或 asend()
呼叫中同步/非同步呼叫樣式切換的次數,接收器會先按是否為非同步分組,然後再呼叫。這表示在同步接收器之前註冊的非同步接收器可能會在同步接收器之後執行。此外,非同步接收器會使用 asyncio.gather()
同時執行。
所有內建信號(除了非同步請求-回應週期中的信號)都會使用 Signal.send()
進行分派。
新增了對非同步信號的支援。
中斷信號連線¶
要中斷接收器與信號的連線,請呼叫 Signal.disconnect()
。參數如 Signal.connect()
中所述。如果已中斷接收器的連線,則此方法會回傳 True
,如果沒有,則回傳 False
。當 sender
作為 <應用程式 標籤>.<模型>
的惰性參考傳遞時,此方法一律回傳 None
。
receiver
參數表示要中斷連線的已註冊接收器。如果使用 dispatch_uid
來識別接收器,則可以為 None
。