logo

Thursday 24th of May 2012

廣告

廣告贊助商

首頁 Python教學 Twisted教學 Twisted的Callback機制 : Deferred
Twisted的Callback機制 : Deferred PDF 列印 E-mail
作者是 Victor   
週三, 11 二月 2009 21:10

什麼是Deferred

簡單的一句話來說,它是Twisted的Callback機制,或許會令人覺得很奇怪,Callback不就是函數當參數傳遞,執行完成後再呼叫如此而以,何以需要一個特別的機制,事實上除此之外還有更多事情要處理,而它的Deferred帶來很大的好處,我們直接參考 官方文件,來說明什麼是Deferred

Deferred的使用方式

以下這張圖說明Deferred物件是這樣被使用的

  1. 送出資料需求
  2. 回傳Deferred物件
  3. 綁定Callback到Deferred物件

這樣或許有些難懂,我們用一個實際一點的例子來說明

from twisted.internet import reactor
from twisted.web.client import getPage

def printPage(html):
    print html

def printError(error):
    print error

# 1. request google page
# 2. return deferred object
deferred = getPage('http://www.google.com')

# 3. add callbacks, printPage for print page content
# print error for print error message
deferred.addCallbacks(printPage, printError)

reactor.run()

這個程式會下載Google的首頁並且印出來,我們的程式兩個Callbacks函數就是圖形中的Data sink,也就是最後資料的去處,而Data source就是getPage函數,所以對應的步驟是

  1. 呼叫getPage函數,它會送出http的request,但是並不會立刻完成
  2. 因為getPage不會立刻完成,是非同步的任務,所以這個函數不會Blocked,而是立刻回傳一個deferred物件,我們就可以增加callback到deferred物件
  3. 當網頁下載成功或失敗了,它就會呼叫deferred裡的callback函數

 Deferred的運作方式

以下這張圖是當非同步事件完成時,Deferred物件會呼叫Callback的流程

  1. 當結果(非同步的任務完成的資料、或發生錯誤)已經準備好時,如果成功則呼叫.callback(結果),如果失敗則呼叫.errback(失敗) (所謂的失敗是一個twisted.python.failure.Failure的實體,裡面夾帶了失敗的原因)
  2. Deferred物件會以上一個callback或errback的回傳值傳給下一層,並且根據下面的規責執行
  • callback的回傳值都會當做下一個callback的第一個參數
  • 如果callback raise一個例外,則呼叫下一層的errback
  • 一個沒有被處理的失敗會往下面的errback傳,就像except:那樣一個一個的處理
  • 如果errback沒有回傳失敗或raise一個例外(表示被處理掉了),則呼叫下一層的callback

為什麼要有這樣的機制?

這樣的機制雖然沒有太複雜,但是也是要花一點心思去了解,那為何要有這樣的機制? 因為在這樣非同步的情況下,沒有地方去攔截例外,因為所有事件的處理都是reactor.run()在做,例外會從那裡跑出來,但是那並不是我們想要的,我們只想針對某個非同步的事件做處理,因此有了這套機制我們就可以針對我們想要的例外或失敗做處理,除此之外,它本身也是責任鏈的設計樣式(Design pattern),這表示你可以增加一串的函數來處理資料,舉個例子

取得網頁資料 -> 解析網頁資料 -> 計算統計分析 -> 儲存到資料庫

你可以有這樣一串的責任各負責不同的事情,它們都不與前後要做的事情無關,所以重覆使用性就大大提升,而且就算其中一個環節出錯,它並不是丟出例外,而是向下傳播,如果你在某一層上不想處理那樣的錯誤,就不要加上errback,錯誤還是會往下傳播,而且它們又無知於上層和下層,達到降低藕合度的效果。

Deferred使用的技巧

Deferred在回傳的時候,就是增加callback的好時機,舉個例子你可以這樣封裝你非同步函數

import re

from twisted.internet import reactor
from twisted.web.client import getPage

def parsePage(html):
    result = re.match('(.*)<title>(.*?)</title>(.*)', html, re.IGNORECASE)
    return result.groups()[1]

def getTitle(url):
    d = getPage(url)
    d.addCallback(parsePage)
    return d

def printTitle(title):
    print title

def printError(error):
    print error

# get page's title
deferred = getTitle('http://www.google.com')

deferred.addCallbacks(printTitle, printError)

reactor.run()

在這個例子中我們封裝了getPage,取得它回傳的Deferred然後增加上我們的parsePage callback,如此一來外界看來這個getTitle就是取得某個網頁然後解析title的行為,當然,你也可以選擇不封裝成getTitle,而只是使用parsePage當做第一個callback去處理html

import re

from twisted.internet import reactor
from twisted.web.client import getPage

def parsePage(html):
    result = re.match('(.*)<title>(.*?)</title>(.*)', html, re.IGNORECASE)
    return result.groups()[1]

def printTitle(title):
    print title

def printError(error):
    print error

# get page's title
deferred = getPage('http://www.google.com')
deferred.addCallbacks(parsePage, printError)
deferred.addCallbacks(printTitle, printError)

reactor.run()

你有你的選擇,這就是它的彈性

Callback的回呼參數

callback函數的第一個參數是回傳的資料,在讀完上面的說明相信都已經很清楚,但是事實上很多時候,我們會有很多非同步事件同時進行,而完成時光靠第一個參數的資料,我們並不能知道當初我們呼叫這函數的目的為何,所以很多時候我們都想要在事情完成時,順便附帶一些當初這任務執行時的訊息,這時候就可以用到回呼參數了,下面這個例子示範如何使用回呼參數

from twisted.internet import reactor
from twisted.web.client import getPage

def printPage(html, siteName):
    print siteName, 'page length', len(html)

def printError(error):
    print error

deferred = getPage('http://www.google.com')
deferred.addCallbacks(printPage, printError, callbackArgs=('Google',))

deferred = getPage('http://yahoo.com')
deferred.addCallbacks(printPage, printError, callbackArgs=('Yahoo',))

reactor.run()

回呼參數會當做第二個參數傳給callback,如此一來就可以夾代一些有用的資訊給回呼函數去處理,errback一樣可以使用這樣的參數,關於詳細情況請查詢Twisted的API參考手冊

為什麼我的錯誤不會被印出來?

有個很常見的問題就是,如果你的某個callback函數丟出某個例外,可是你卻發現錯誤訊息沒有被補捉並且印出來,以下例子示範這樣的情況

from twisted.internet import reactor
from twisted.web.client import getPage

def printPage(html):
    # raise a exception
    a = 10 / 0
    print html

def printError(error):
    print error
    
deferred = getPage('http://www.google.com')
deferred.addCallbacks(printPage, printError)

reactor.run()

你會發現你一直等,但是就是不見錯誤訊息出現,或是結果印出,原因出在於,未被補捉的錯誤,會在deferred物件被摧毀時才會印出,所以你看不到它被印出來,解決的方法就是,在下面再加一層的errback,如此一來就能補捉到上一層的錯誤,不用等到deferred被摧毀

from twisted.internet import reactor
from twisted.web.client import getPage

def printPage(html):
    # raise a exception
    a = 10 / 0
    print html

def printError(error):
    print error
    
deferred = getPage('http://www.google.com')
deferred.addCallbacks(printPage, printError)
deferred.addErrback(printError)

reactor.run()

確保callback都有回傳資料

以上範例都因為目的只為印出資料程式便結束了,如果說你的程式想要被重覆使用,可以和其它callback串在一起,最好在每個callback後面都回傳資料,否則Python預設沒回傳的回傳值為None,下面的callback就不能處理上面的資料,以下範例示範正確的做法

from twisted.internet import reactor
from twisted.web.client import getPage

def printPage(html):
    print html
    # we pass it to chain, so other callback can handle it
    return html

def printError(error):
    print error
    # pass it
    return error
    
deferred = getPage('http://www.google.com')
deferred.addCallbacks(printPage, printError)

reactor.run()
請注意,如果errback沒有回傳值的話,依照Deferred的規則,回傳值不是失敗或是沒有丟出例外的話,會轉到callback繼續執行

 

 

核心是 Joomla!. Designed by: Free Joomla Theme, whois protect. Valid XHTML and CSS.