duck typing 在软件开发中意味着什么?
它是 dynamic languages 中使用的一个术语,没有 strong typing。
这个想法是您不需要类型来调用对象上的现有方法 - 如果在其上定义了方法,则可以调用它。
这个名字来源于“如果它看起来像鸭子,叫起来像鸭子,那就是鸭子”这句话。
Wikipedia 包含更多信息。
鸭子类型意味着一个操作没有正式指定其操作数必须满足的要求,而只是用给定的内容进行尝试。
与其他人所说的不同,这不一定与动态语言或继承问题有关。
示例任务:在对象上调用某个方法 Quack
。
如果不使用鸭子类型,执行此任务的函数 f
必须事先指定其参数必须支持某些方法 Quack
。一种常见的方式是使用接口
interface IQuack {
void Quack();
}
void f(IQuack x) {
x.Quack();
}
调用 f(42)
失败,但只要 donald
是 IQuack
子类型的实例,f(donald)
就可以工作。
另一种方法是 structural typing - 但同样,方法 Quack()
是正式指定的,任何无法提前证明它的 quack
都会导致编译器失败。
def f(x : { def Quack() : Unit }) = x.Quack()
我们甚至可以写
f :: Quackable a => a -> IO ()
f = quack
在 Haskell 中,Quackable
类型类确保我们的方法存在。
好吧,正如我所说,鸭子类型系统没有指定要求,而只是尝试是否可行。
因此,像 Python 那样的动态类型系统总是使用鸭子类型:
def f(x):
x.Quack()
如果 f
得到一个支持 Quack()
的 x
,那么一切都很好,如果没有,它将在运行时崩溃。
但是鸭子类型根本不意味着动态类型——事实上,有一种非常流行但完全静态的鸭子类型方法,它也没有给出任何要求:
template <typename T>
void f(T x) { x.Quack(); }
该函数不会以任何方式表明它需要一些可以Quack
的 x
,因此它只是在编译时尝试,如果一切正常,那就没问题了。
def f(x)
而不是 def f(IQuack x)
。
简单说明
什么是鸭子打字?
“如果它像鸭子一样走路,像……等等一样嘎嘎叫” - 是的,但这意味着什么??!
我们感兴趣的是“对象”可以做什么,而不是它们是什么。
让我们用一个例子来解压它:
https://i.stack.imgur.com/DNeRD.jpg
请参阅下文了解更多详情:
Duck Typing 功能示例:
想象一下,我有一根魔杖。它有特殊的权力。如果我挥动魔杖说“开车!”到一辆车,那么,它开车!
它适用于其他事情吗?不确定:所以我在卡车上试了一下。哇——它也能开车!然后我在飞机、火车和 1 Woods(它们是人们用来“驱动”高尔夫球的一种高尔夫球杆)上尝试它。他们都开车!
但它可以说,一个茶杯吗?错误:KAAAA-BOOOOOOM!结果不太好。 ====> 茶杯不能开车!!呃!?
这基本上是鸭子类型的概念。这是一个先试后买的系统。如果它有效,一切都很好。但如果它失败了,就像你手中的手榴弹一样,它会在你的脸上爆炸。
换句话说,我们感兴趣的是对象可以做什么,而不是对象是什么。
C# 或 Java 等语言呢?
如果我们关心对象实际上是什么,那么我们的魔术将仅适用于预设的授权类型——在这种情况下是汽车,但在其他可以驱动的对象上会失败:卡车、轻便摩托车、嘟嘟车等。它不适用于卡车,因为我们的魔杖预计它只适用于汽车。
换句话说,在这种情况下,魔杖非常仔细地查看对象是什么(是汽车吗?),而不是对象可以做什么(例如汽车、卡车等是否可以驾驶)。
让卡车开起来的唯一方法是,如果你能以某种方式让魔杖同时期待卡车和汽车(也许通过“实现一个通用界面”)。如果您不知道这意味着什么,请查看我的 cartoon on interfaces。
摘要:钥匙取出
在鸭子类型中重要的是对象实际上可以做什么,而不是对象是什么。
序幕
我试图保持简单,去掉迂腐的细微差别。
如果您想要更精确的定义,请查看 wikipedia article to duck typing,或 Matt Damon 对避免输入 Good Will Hunting 的解释
假设您正在设计一个简单的函数,该函数获取 Bird
类型的对象并调用其 walk()
方法。您可以想到两种方法:
这是我的函数,我必须确保它只接受 Bird 类型,否则代码将无法编译。如果有人想使用我的功能,他们必须知道我只接受鸟类。我的函数获取任何对象,我只是调用对象的 walk() 方法。因此,如果对象可以 walk() 则它是正确的。如果不能,我的功能将失败。因此,这里对象是 Bird 或其他任何东西并不重要,重要的是它可以 walk() (这是鸭子类型)。
必须考虑鸭类型在某些情况下可能有用。例如,Python 大量使用鸭子类型。
有用的阅读
在 https://en.wikipedia.org/wiki/Duck_typing 上有很好的 Java、Python、JavaScript 等的鸭子类型示例。
这也是一个很好的答案,它描述了动态类型的优点和缺点:动态类型的假设生产力增益是多少?
我看到很多重复旧习语的答案:
如果它长得像鸭子,叫起来像鸭子,那就是鸭子
然后深入解释你可以用鸭子打字做什么,或者一个似乎进一步混淆这个概念的例子。
我找不到那么多帮助。
这是我发现的关于鸭子类型的简单英语答案的最佳尝试:
Duck Typing 意味着一个对象是由它可以做什么来定义的,而不是由它是什么来定义的。
这意味着我们不太关心对象的类/类型,而更关心可以在其上调用哪些方法以及可以对其执行哪些操作。我们不关心它的类型,我们关心它可以做什么。
维基百科有一个相当详细的解释:
http://en.wikipedia.org/wiki/Duck_typing
鸭子类型是一种动态类型,其中对象的当前方法和属性集决定了有效的语义,而不是它从特定类或特定接口实现的继承。
重要的一点很可能是,对于鸭子类型,开发人员更关心对象中被消耗的部分,而不是实际的底层类型是什么。
不要做庸医;我支持你:
“鸭子打字”:=“尝试方法,不要检查类型”
注意::=
可以读作“被定义为”。
“鸭子类型”的意思是:只需在任何进入的对象上尝试方法(函数调用),而不是首先检查对象的类型以查看该方法是否甚至是对此类类型的有效调用。
让我们称之为“尝试方法,不检查类型”打字,“方法调用类型检查”,或者简称为“方法调用打字”。
在下面更长的解释中,我将更详细地解释这一点,并帮助您理解荒谬、深奥和混淆的术语“鸭子打字”。
更长的解释:
死🦆死! 🍗
Python 在上面做了这个概念。考虑这个示例函数:
def func(a):
a.method1()
a.method2()
当对象(输入参数 a
)进入函数 func()
时,该函数将尝试(在运行时)调用此对象上指定的任何方法(即:上例中的 method1()
和 method2()
) ,而不是首先检查 a
是否是具有这些方法的某种“有效类型”。
因此,这是在运行时基于操作的尝试,而不是在编译时或运行时基于类型的检查。
现在看看这个愚蠢的例子:
def func(duck_or_duck_like_object):
duck_or_duck_like_object.quack()
duck_or_duck_like_object.walk()
duck_or_duck_like_object.fly()
duck_or_duck_like_object.swim()
于是诞生了一句荒谬的句子:
如果它像鸭子一样走路,像鸭子一样嘎嘎叫,那么它就是鸭子。
使用“duck typing”的程序应该简单地尝试在对象上调用的任何方法(在上面的示例中:quack()
、walk()
、fly()
和 swim()
),甚至不知道 type< /em> 的对象!它只是尝试方法!如果它们有效,那太好了,因为所有“鸭子打字”语言都知道或关心,IT(传递给函数的对象)是一只鸭子!--因为所有(类似鸭子的)方法都在它上面工作。
(总结我自己的话):
“鸭子类型”语言不应检查其类型(无论是在编译时还是运行时)-它不在乎。它只会在运行时尝试这些方法。如果他们工作,那就太好了。如果他们不这样做,那么它将引发运行时错误。
那是鸭子打字。
我厌倦了这种荒谬的“鸭子”解释(因为没有这个完整的解释,它根本没有任何意义!),其他人听起来也是如此。示例:来自 BKSpurgeon's answer here(我用粗体强调):
(“如果它走路像鸭子,叫起来像鸭子,那它就是鸭子。”)——是的!但是,这是什么意思??!”
现在我明白了:只需在任何进入的对象上尝试该方法,而不是先检查对象的类型。
我将称之为“运行时检查程序只是在不知道对象是否具有这些方法的情况下尝试调用的方法,而不是首先检查对象的类型作为知道对象具有这些方法的手段”,因为那只是更有意义。但是……说起来太长了,所以人们宁愿多年来互相混淆,而不是说一些荒谬但朗朗上口的话,比如“鸭子打字”。
让我们称之为:“尝试方法,不检查类型”打字。或者,也许是:“方法调用类型检查”(简称“方法调用类型”)或“方法调用的间接类型检查”,因为它使用给定方法的调用作为“足够的证据”,即object 是正确的类型,而不是直接检查对象的类型。
请注意,这种“方法调用类型检查”(或者容易混淆地称为“鸭子类型”)是一种动态类型。但是,并非所有动态类型都必须是“方法调用类型检查”,因为动态类型或运行时的类型检查也可以通过实际检查对象的类型来完成,而不是简单地尝试调用在函数中的对象而不知道其类型。
另请阅读:
https://en.wikipedia.org/wiki/Duck_typing --> 在页面中搜索“run”、“run time”和“runtime”。
我知道我没有给出笼统的答案。在 Ruby 中,我们不声明变量或方法的类型——一切都只是某种对象。所以规则是“类不是类型”
在 Ruby 中,类从不(好吧,几乎从不)类型。相反,对象的类型更多地由该对象可以做什么来定义。在 Ruby 中,我们称之为鸭子类型。如果一个物体像鸭子一样走路和像鸭子一样说话,那么解释器很乐意把它当作鸭子来对待。
例如,您可能正在编写一个例程来将歌曲信息添加到字符串中。如果你有 C# 或 Java 背景,你可能会想写这样的:
def append_song(result, song)
# test we're given the right parameters
unless result.kind_of?(String)
fail TypeError.new("String expected") end
unless song.kind_of?(Song)
fail TypeError.new("Song expected")
end
result << song.title << " (" << song.artist << ")" end
result = ""
append_song(result, song) # => "I Got Rhythm (Gene Kelly)"
拥抱 Ruby 的鸭式打字,你会写出更简单的东西:
def append_song(result, song)
result << song.title << " (" << song.artist << ")"
end
result = ""
append_song(result, song) # => "I Got Rhythm (Gene Kelly)"
您不需要检查参数的类型。如果他们支持 <<(在结果的情况下)或标题和艺术家(在歌曲的情况下),一切都会正常工作。如果他们不这样做,您的方法无论如何都会抛出异常(就像您检查类型时所做的那样)。但是如果没有检查,您的方法会突然变得更加灵活。您可以将数组、字符串、文件或任何其他使用 << 附加的对象传递给它,它会正常工作。
查看语言本身可能会有所帮助;它经常帮助我(我不是以英语为母语的人)。
在 duck typing
中:
1) typing
这个词并不意味着在键盘上打字(就像我脑海中的持久图像),它意味着确定“那个东西是什么类型的东西?”
2) duck
一词表示确定是如何完成的;这是一种“松散”的决定,例如:“如果它像鸭子一样走路......那么它就是鸭子”。它是“松散的”,因为这东西可能是鸭子,也可能不是,但它是否真的是鸭子并不重要;重要的是我可以用它做我可以用鸭子做的事情,并期待鸭子表现出的行为。我可以喂它面包屑,这东西可能会朝我冲过来或冲向我或后退……但它不会像灰熊那样吞噬我。
鸭打字:
如果它说话和走路都像鸭子,那么它就是鸭子
这通常称为溯因(溯因推理或也称为追溯,我认为这是一个更清晰的定义):
从 C(结论,我们看到的)和 R(规则,我们知道的),我们接受/决定/假设 P(前提,属性),换句话说,一个给定的事实……鸭子医学诊断的基础:C = 走路,说话,R = 像一只鸭子,P = 它是一只鸭子
回到编程:
对象 o 具有方法/属性 mp1 和接口/类型 T 需要/定义 mp1
对象 o 具有方法/属性 mp2 和接口/类型 T 需要/定义 mp2
...
因此,不仅仅是简单地接受任何对象上的 mp1...,只要它符合 mp1... 的某些定义,编译器/运行时也应该可以接受断言 o 是类型 T
那么,上面的例子就是这样吗? Duck 打字本质上是不打字吗?或者我们应该称之为隐式类型?
Duck Typing 不是 Type Hinting!
基本上,为了使用“鸭子类型”,您不会针对特定类型,而是更广泛的子类型(不是在谈论继承,当我指的是子类型时,我是指适合相同配置文件的“事物”)通过使用通用接口.
你可以想象一个存储信息的系统。为了写入/读取信息,您需要某种存储和信息。
存储类型可能是:文件、数据库、会话等。
无论存储类型如何,该界面都会让您知道可用的选项(方法),这意味着此时什么都没有实现!换句话说,接口对如何存储信息一无所知。
每个存储系统都必须通过实现相同的方法来知道接口的存在。
interface StorageInterface
{
public function write(string $key, array $value): bool;
public function read(string $key): array;
}
class File implements StorageInterface
{
public function read(string $key): array {
//reading from a file
}
public function write(string $key, array $value): bool {
//writing in a file implementation
}
}
class Session implements StorageInterface
{
public function read(string $key): array {
//reading from a session
}
public function write(string $key, array $value): bool {
//writing in a session implementation
}
}
class Storage implements StorageInterface
{
private $_storage = null;
function __construct(StorageInterface $storage) {
$this->_storage = $storage;
}
public function read(string $key): array {
return $this->_storage->read($key);
}
public function write(string $key, array $value): bool {
return ($this->_storage->write($key, $value)) ? true : false;
}
}
所以现在,每次您需要写入/读取信息时:
$file = new Storage(new File());
$file->write('filename', ['information'] );
echo $file->read('filename');
$session = new Storage(new Session());
$session->write('filename', ['information'] );
echo $session->read('filename');
在此示例中,您最终在 Storage 构造函数中使用 Duck Typing:
function __construct(StorageInterface $storage) ...
希望它有所帮助;)
使用鸭子类型技术的树遍历
def traverse(t):
try:
t.label()
except AttributeError:
print(t, end=" ")
else:
# Now we know that t.node is defined
print('(', t.label(), end=" ")
for child in t:
traverse(child)
print(')', end=" ")
我认为将动态类型、静态类型和鸭子类型混为一谈会让人感到困惑。鸭子类型是一个独立的概念,甚至像 Go 这样的静态类型语言也可以有一个实现鸭子类型的类型检查系统。如果类型系统将检查(声明的)对象的方法而不检查类型,则可以将其称为鸭子类型语言。
鸭子打字这个词是一个谎言。
您会看到“如果它像鸭子一样走路,像鸭子一样嘎嘎叫,那么它就是鸭子。”这句话在这里一次又一次地重复。
但这不是鸭式打字(或我们通常所说的鸭式打字)的意义所在。我们正在讨论的所有 Duck Typing 都是关于试图对某些东西强制执行命令。看东西是不是嘎嘎,不管它说的是什么。但是没有推论该对象是否是鸭子。
有关真正的鸭子类型,请参阅类型类。现在遵循成语“如果它像鸭子一样走路,像鸭子一样嘎嘎叫,那么它就是鸭子。”。对于类型类,如果一个类型实现了一个类型类定义的所有方法,那么它可以被认为是一个成员该类型类(无需继承类型类)。因此,如果有一个类型类 Duck 定义了某些方法(quack 和 walk-like-duck),则任何实现这些相同方法的东西都可以被视为 Duck(没有需要继承鸭子)。
在鸭子类型中,对象的适用性(例如,在函数中使用)是基于是否实现了某些方法和/或属性而不是基于该对象的类型来确定的。
例如,在 Python 中,len
函数可用于任何实现 __len__
方法的对象。它不关心该对象是否属于某种类型,例如字符串、列表、字典或 MyAwesomeClass,只要这些对象实现 __len__
方法,len
就可以使用它们。
class MyAwesomeClass:
def __init__(self, str):
self.str = str
def __len__(self):
return len(self.str)
class MyNotSoAwesomeClass:
def __init__(self, str):
self.str = str
a = MyAwesomeClass("hey")
print(len(a)) # Prints 3
b = MyNotSoAwesomeClass("hey")
print(len(b)) # Raises a type error, object of type "MyNotSoAwesomeClass" has no len()
换句话说,MyAwesomeClass
看起来像鸭子,叫起来像鸭子,因此是鸭子,而 MyNotSoAwesomeClass
看起来不像鸭子,也不叫,因此不是鸭子!
鸭子打字:
let anAnimal
if (some condition)
anAnimal = getHorse()
else
anAnimal = getDog()
anAnimal.walk()
上面的函数调用在结构类型中不起作用
以下将适用于结构类型:
IAnimal anAnimal
if (some condition)
anAnimal = getHorse()
else
anAnimal = getDog()
anAnimal.walk()
就是这样,我们中的许多人已经直观地知道鸭式打字。
善意狩猎 - 马特达蒙鸭打字场景
CHUCKIE:好吧,我们会有问题吗?
克拉克:没问题。我只是希望你能给我一些关于什么是鸭子打字的见解?我的论点是打鸭式没有很好的定义,也不是很牢固
威尔:[打断]……强类型也不是。当然这是你的论点。你是一名一年级的研究生:你刚刚读完一些关于鸭子打字的文章,可能是在 StackOverflow 上,直到下个月当你到达四人帮时,你才会相信这一点,然后你就会将谈论 Google Go 和 Ocaml 如何成为具有结构子绑定结构的统计类型语言。这将持续到明年,直到你可能会在这里反刍 Matz,谈论你知道的 Pre-Ruby 3.0 乌托邦和子类型对 GC 的内存分配影响。
克拉克:[吃惊]事实上我不会,因为马茨严重低估了——
WILL:“Matz 大大低估了 Ruby 3.0 的 GC 对性能的影响。你从 Donald Knuth,计算机编程的艺术,第 98 页得到的,对吗?是的,我也读过。你会为我们剽窃整个事情吗? ——你对这件事有什么——你自己的想法吗?或者——那是你的事吗,你陷入了一个松弛的线程,你读了一些关于 r/ruby 的晦涩的段落,然后你假装,你把它当成你自己的——你自己的想法只是为了给一些女孩留下好印象,让我的朋友难堪?
[克拉克惊呆了]
威尔:看到像你这样的人的可悲之处在于,大约 50 年后你会开始自己思考,你会想出生活中有三个确定性的事实。一,不要那样做。第二,如果它像鸭子一样走路,那么它就是鸭子。第三,您通过 Ben Koshy 的堆栈溢出答案以零美分获得的教育投入了 150 美元。
克拉克:是的,但是我将获得学位,并且您将通过在我们去滑雪旅行的路上通过汽车的反应为我的孩子提供一些便宜的 html。
威尔:[微笑]是的,也许吧。但至少我不会是非原创的。
(一拍)
威尔:你对此有意见吗?我想我们可以走出去做一些代码练习吗?
克拉克:没问题
一段时间以后:
威尔:你喜欢苹果吗?
克拉克不知所措。嗯?
威尔:你喜欢他们的苹果吗? (Boom:Will 将一封信砸在窗户上。)我必须得到 Google 的报价! (给克拉克的录取通知书显示了他的面试答案:对冒泡排序算法的正确回答和鸭子走路的图片)
结束
(这是 old answer here 的脚注:)
我试图以我的方式理解这句著名的句子:“Python 不在乎一个对象是否是真正的鸭子。它只关心对象是否,首先是“嘎嘎”,其次是“像鸭子”。
有一个很好的网站。 http://www.voidspace.org.uk/python/articles/duck_typing.shtml#id14
作者指出,duck typing 让您可以创建自己的类,这些类具有自己的内部数据结构——但可以使用普通的 Python 语法进行访问。
不定期副业成功案例分享