天天看點

鴨子類型 Duck typing

在看ruby的時候,發現ruby有一種 duck typing 程式設計風格,這屬于語言思想,科普一下

在程式設計中,鴨子類型(英語:duck typing)是動态類型的一種風格。在這種風格中,一個對象有效的語義,不是由繼承自特定的類或實作特定的接口,而是由目前方法和屬性的集合決定。這個概念的名字來源于由James Whitcomb Riley提出的鴨子測試(見下面的“曆史”章節),“鴨子測試”可以這樣表述:

“當看到一隻鳥走起來像鴨子、遊泳起來像鴨子、叫起來也像鴨子,那麼這隻鳥就可以被稱為鴨子。” [1] [2]

在鴨子類型中,關注的不是對象的類型本身,而是它是如何使用的。例如,在不使用鴨子類型的語言中,我們可以編寫一個函數,它接受一個類型為鴨的對象,并調用它的走和叫方法。在使用鴨子類型的語言中,這樣的一個函數可以接受一個任意類型的對象,并調用它的走和叫方法。如果這些需要被調用的方法不存在,那麼将引發一個運作時錯誤。任何擁有這樣的正确的走和叫方法的對象都可被函數接受的這種行為引出了以上表述,這種決定類型的方式是以得名。

鴨子類型通常得益于不測試方法和函數中參數的類型,而是依賴文檔、清晰的代碼和測試來確定正确使用。從靜态類型語言轉向動态類型語言的使用者通常試圖添加一些靜态的(在運作之前的)類型檢查,進而影響了鴨子類型的益處和可伸縮性,并限制了語言的動态特性。

目錄

  [隐藏]
  • 1 概念樣例
  • 2 靜态語言中的鴨子類型
  • 3 與其他類型系統的比較
    • 3.1 結構類型系統
    • 3.2 接口
    • 3.3 模闆或泛型
  • 4 批評
  • 5 曆史
  • 6 實作
    • 6.1 在ColdFusion中
    • 6.2 在Objective-C中
    • 6.3 在Python中
    • 6.4 在Common Lisp中
  • 7 參見條目
  • 8 參考
  • 9 外部連結

[編輯]概念樣例

考慮用于一個使用鴨子類型的語言的以下僞代碼:

function calculate(a, b, c) => return (a+b)*c

example1 = calculate (1, 2, 3)
example2 = calculate ([1, 2, 3], [4, 5, 6], 2)
example3 = calculate ('apples ', 'and oranges, ', 3)

print to_string example1
print to_string example2
print to_string example3
           

在樣例中,每次對

calculate

的調用都使用的對象(數字、清單和字元串)在繼承關系中沒有聯系。隻要對象支援“+”和“*”方法,操作就能成功。例如,翻譯成Ruby或Python語言,運作結果應該是:

9
[1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6]
apples and oranges, apples and oranges, apples and oranges, 
           

這樣,鴨子類型在不使用繼承的情況下使用了多态。唯一的要求是

calculate

函數需要作為參數的對象擁有“+”和“*”方法。以下樣例(python語言)展現了鴨子測試。就

in_the_forest

函數而言,對象是一個鴨子:

class Duck:
    def quack(self): 
        print "Quaaaaaack!"
    def feathers(self): 
        print "The duck has white and gray feathers."
 
class Person:
    def quack(self):
        print "The person imitates a duck."
    def feathers(self): 
        print "The person takes a feather from the ground and shows it."
 
def in_the_forest(duck):
    duck.quack()
    duck.feathers()
 
def game():
    donald = Duck()
    john = Person()
    in_the_forest(donald)
    in_the_forest(john)
 
game()
           

[編輯]靜态語言中的鴨子類型

一些通常的靜态,語言如Boo和C#第四版,有一些額外的類型注解,它們訓示編譯器将類的類型檢查安排在運作時而不是編譯時,并在編譯器的輸出中包含用于運作時類型檢查的代碼[3][4]。這些附加的内容允許這些語言享受鴨子類型的大多數益處,僅有的缺點是需要在編譯時識别和指定這些動态類。

[編輯]與其他類型系統的比較

[編輯]結構類型系統

鴨子類型和結構類型相似但與之不同。結構類型由類型的結構決定類型的相容性和等價性,而鴨子類型隻由結構中在運作時所通路的部分決定類型的相容性。Objective Caml語言使用結構類型系統。

[編輯]接口

接口可以提供鴨子類型的一些益處,但鴨子類型與之不同的是沒有顯式定義任何接口。例如,如果一個第三方Java庫實作了一個使用者不允許修改的類,使用者就無法把這個類的執行個體用作一個自己定義的接口的實作,而鴨子類型允許這樣做。

[編輯]模闆或泛型

模闆函數或方法在一個靜态類型上下文中應用鴨子測試;這同時帶來了靜态和動态類型檢查的一般優點和缺點。同時,由于在鴨子類型中,隻有在運作時被實際調用的方法需要被實作,而模闆要求實作在編譯時不能證明不可到達的所有方法,是以鴨子類型更具有可伸縮性。

執行個體包括帶有模闆的C++語言和Java語言的泛型。

[編輯]批評

常常被引用的一個批評是:

關于鴨子類型的一個問題是它要求程式員在任何時候都必須很好地了解他/她正在編寫的代碼。在一個強靜态類型的、使用了類型繼承樹和參數類型檢查的語言中,給一個類提供未預測的對象類型更為困難。例如,在Python中,你可以建立一個稱為Wine的類,并在其中需要實作press方法。然而,一個稱為Trousers的類可能也實作press()方法。為了避免奇怪的、難以檢測的錯誤,開發者在使用鴨子類型時需要意識到每一個“press”方法的可能使用,即使在語義上和他/她所正在編寫工作的代碼沒有任何關系。
本質上,問題是:“如果它走起來像鴨子并且叫起來像鴨子”,它也可以是一隻正在模仿鴨子的龍。盡管它們可以模仿鴨子,但也許你不總是想讓龍進入池塘。

鴨子類型的提倡者,如Guido van Rossum,認為這個問題可以通過在測試和維護代碼庫前擁有足夠的了解來解決[5][6]。

對鴨子類型的批評傾向于成為關于動态類型和靜态類型的争論的更廣闊的觀點的特殊情形。

[編輯]曆史

Alex Martelli很早(2000年)就在釋出到comp.lang.python新聞討論區上的一則消息中使用了這一術語。他同時對鴨子測試的錯誤的字面了解提出了提醒,以避免人們錯誤認為這個術語已經被使用。

換言之,不要檢查它是不是一個鴨子:檢查它像不像一個鴨子地叫,等等。取決于你需要哪個像鴨子的行為的子集來使用語言。

[編輯]實作

[編輯]在ColdFusion中

web應用程式腳本語言ColdFusion允許函數參數被指定為類型為any。對于這種參數,任意對象都可被傳入,函數調用在運作時被動态綁定。如果對象沒有實作一個被調用的函數,一個可被捕獲并優雅地處理的運作時例外将被抛出。在ColdFusion 8中,這也可以被一個已定義的事件onMissingMethod()而不是例外處理器處理。另一個可替代的參數類型WEB-INF.cftags.component限制傳入參數是一個ColdFusion元件(CFC),在一個不正确的對象傳入時它提供了更好的錯誤消息。

[編輯]在Objective-C中

Objective-C,C和Smalltalk的一個交錯,像Smalltalk一樣,允許使用者聲明一個對象的類型為“id”并向它發送任何資訊。發送者可以測試一個對象以了解它能不能對一個消息響應,對象可以在收到消息的時候決定響應與否,如果發送者發送了一個接收者不能響應的消息,一個例外會被抛出。是以,鴨子類型在Objective-C中被完全支援。

[編輯]在Python中

鴨子類型在Python中被廣泛使用。Python術語表這樣定義鴨子類型:

Pythonic programming style that determines an object's type by inspection of its method or attribute signature rather than by explicit relationship to some type object ("If it looks like a duck and quacks like a duck, it must be a duck.") By emphasizing interfaces rather than specific types, well-designed code improves its flexibility by allowing polymorphic substitution. Duck-typing avoids tests using 

type()

 or 

isinstance()

. Instead, it typically employs the EAFP (Easier to Ask Forgiveness than Permission) style of programming.

在Python中,鴨子類型的最典型例子就是類似file的類。這些類可以實作

file

的一些或全部方法,并可以用于

file

通常使用的地方。例如,

GzipFile

實作了一個用于通路gzip壓縮的資料的類似file的對象。

cStringIO

允許把一個Python字元串視作一個檔案。套接字(socket)也和檔案共同擁有許多相同的方法。然而套接字缺少

tell()

方法,不能用于

GzipFile

可以使用的所有地方。這展現了鴨子類型的可伸縮性:一個類似file的對象可以實作它有能力實作的方法,且隻能被用于它有意義的情形下。

EAFP原則描述了例外處理的使用。例如相對于檢查一個自稱為類似Duck的對象是否擁有一個quack()方法(使用

if hasattr(mallard, "quack"): ...

),人們通常更傾向于用例外處理把對quack的調用嘗試包裹起來:

try:
    mallard.quack()
except (AttributeError, TypeError):
    print "mallard can't quack()"
      

這個寫法的優勢在于它鼓勵結構化處理其他來自類的錯誤(這樣的話,例如,一個不能完成quack的Duck子類可以抛出一個“QuackException”,這個例外可以簡單地添加到包裹它的代碼,并不需要影響更多的代碼的邏輯。同時,對于其他不同類的對象存在不相容的成員而造成的命名沖突,它也能夠處理(例如,假設有一個醫學專家Mallard有一個布爾屬性将他分類為“quack=True”,試圖執行Mallard.quack()将抛出一個TypeError)。

在更實際的實作類似file的行為的例子中,人們更傾向于使用Python的異常處理機制來處理各種各樣的可能因為各種程式員無法控制的環境和operating system問題而發生的I/O錯誤。在這裡,“鴨子類型”産生的例外可以在它們自己的子句中捕獲,與作業系統、I/O和其他可能的錯誤分别處理,進而避開複雜的檢測和錯誤檢查邏輯。

[編輯]在Common Lisp中

Common Lisp提供了一個面向對象的擴充(Common Lisp對象系統,簡寫為CLOS)。在Common Lisp中,CLOS和Lisp的動态類型使鴨子類型成為一種通用的程式設計風格。

使用Common Lisp,使用者通常不需要查詢類型,因為如果一個函數不适用,系統會抛出一個運作時錯誤。這個錯誤可以被Common Lisp的條件系統處理。在類外定義的方法也可以為指定的對象定義。

(defclass duck () ())
 
(defmethod quack ((a-duck duck))
  (print "Quaaaaaack!"))
 
(defmethod feathers ((a-duck duck))
  (print "The duck has white and gray feathers."))
 
(defclass person () ())
 
(defmethod quack ((a-person person))
  (print "The person imitates a duck."))
 
(defmethod feathers ((a-person person))
  (print "The person takes a feather from the ground and shows it."))
 
(defmethod in-the-forest (duck)
  (quack duck)
  (feathers duck))
 
(defmethod game ()
  (let ((donald (make-instance 'duck))
        (john (make-instance 'person)))
    (in-the-forest donald)
    (in-the-forest john)))
 
(game)
      

Common Lisp通常的開發風格(像SLIME一樣使用Lisp REPL)也允許互動式修複:

? (defclass cat () ())
#<STANDARD-CLASS CAT>
? (quack (make-instance 'cat))
> Error: There is no applicable method for the generic function:
>          #<STANDARD-GENERIC-FUNCTION QUACK #x300041C2371F>
>        when called with arguments:
>          (#<CAT #x300041C7EEFD>)
> If continued: Try calling it again
1 > (defmethod quack ((a-cat cat))
        (print "The cat imitates a duck."))
 
#<STANDARD-METHOD QUACK (CAT)>
1 > (continue)
 
"The cat imitates a duck."
      

通過這種方法,軟體可以通過擴充隻有部分工作的使用鴨子類型的代碼來開發。