以动态语言键入类

时间:2014-03-19 08:37:36

标签: python clojure lisp

我必须承认我只掌握Python的基本知识,目前正在学习Haskell。

我想知道类型类的概念是否存在/在Python或Clojure(或其他动态强类型语言)中是否有意义?

换句话说,如果我有一个函数名f,那么根据提供给f的运行时参数,将调用一个不同的函数实现(如类型的==函数属于Haskell中的Eq类型类。这样的概念是否存在于动态语言中,例如Clojure / Python?

6 个答案:

答案 0 :(得分:4)

多个分派(example in Julia language)与类型类似。多分派提供编译时多态(就像类型类一样),而典型动态语言(即Python)中的对象接口通常仅限于运行时多态。多个调度提供了比动态面向对象语言中常见接口更好的性能,因此它在动态语言中非常有用。

Python有一些multiple dispatch implementations,但我不确定它们是否提供编译时多态性。

答案 1 :(得分:4)

Multimethods似乎在Clojure中做到了。例如,让我们定义一个plus函数来添加数字,但是连接其他任何东西的字符串表示。

(defmulti plus (fn [& xs] (every? number? xs)))

(defmethod plus true [& xs] (apply + xs))
(defmethod plus false [& xs] (apply str xs))

(plus 1 8) ;9
(plus 1 \8) ;"18"

多方法是函数((ifn? plus)true),所以你想要的是第一类:

(let [z (partial plus 5)] (z \3)) ;"53"

答案 2 :(得分:4)

你可以使用clojure中的多方法或协议,或者使用python中的简单成员函数(类方法)来接近这一点。但是,haskell中存在的每个缺失都有一个重要特征:返回型多态。

编译器知道你"期望"一个返回的函数,可以相应地调度到不同的实现。这意味着,在相同参数上调用的相同函数可以完全不同,具体取决于对其返回值的处理方式。例如:

Prelude> read "[5,6,7]" :: [Int]
[5,6,7]
Prelude> read "[5,6,7]" :: [Double]
[5.0,6.0,7.0]

同样,你甚至可以拥有多态常量,它们对每个类型类实例都有不同的值:

Prelude Data.Word> (minBound, minBound, minBound) :: (Int, Bool, Word8)
(-9223372036854775808,False,0)

你不能用动态语言真正做到这一点,因为没有类型推断。您可以通过传递代表"我想要的结果类型的对象来假装它,并将它们用作您的调度程序,但它并不完全相同。

答案 3 :(得分:2)

您无法在Python中定义多个具有相同名称的函数(在同一范围内)。如果这样做,第二个定义将覆盖第一个并且是唯一一个被调用的(至少当两者都在同一个范围内时 - 显然你可以在不同的类中拥有共享名称的类方法)。参数列表也是类型无知的,因此即使你可以定义两次函数,解释器也只能根据参数的数量来区分它们,而不是类型。你需要做的是编写一个可以处理几个不同参数列表的函数,然后在必要时检查它们在该函数中的类型。

最简单的方法是使用默认参数和关键字参数。

默认参数

假设你有这样的功能:

def BakePie(crust, filling="apple", oventemp=(375,F), universe=1):
    ...

您可以使用位置参数调用此函数,如下所示:

BakePie("graham cracker")
BakePie("buttery", "cherry")
BakePie("fluffy", "lemon meringue", (400,F))
BakePie("fluffy", "key lime", (350,F), 7)

关键字参数

这些都会奏效,但您可能并不总是希望更改每个默认值。如果您想在不同的宇宙中烘烤标准的苹果派怎么办?好吧,你可以使用关键字参数调用它:

BakePie("buttery", universe=42)

在这种情况下,将使用fill和oventemp的默认参数,并且只会更改Universe(和crust,因为没有默认值而必须始终给出)的参数。这里的一条规则是,在调用函数时,任何关键字参数都必须位于任何位置参数的右侧。关键字参数的顺序无关紧要,例如这也有效:

BakePie("fluffy", oventemp=(200, C), filling="pecan")

但这不会:

BakePie(filling="boysenberry", "crumb")

更多关键字参数

现在,如果函数的行为完全不同,具体取决于传递的参数,该怎么办?例如,你有一个乘法函数,它接受两个整数,或者它取一个整数和一个整数列表,或者两个整数列表,并将它们相乘。这种情况是调用者只想使用关键字参数的情况。您可以像这样设置函数定义:

def GenericMultiply(int1=False, int2=False, ilist1=False, ilist2=False):
    # check which parameters have values, then do stuff

(或使用None代替False。)

当你需要乘以两个整数时,请这样调用:

GenericMultiply(int1=6, int2=7)

注意:您也可以只使用两个位置参数执行上述操作,并通过使用type()函数或使用try:except:blocks等手动检查函数内部的类型。调用仅适用于每个参数的列表或整数的方法。

进一步阅读

在Python中还有很多其他方法可以解决这个问题,我只是想描述一个最简单的方法。如果你想了解更多,我推荐官方Python教程的Defining Functions部分和下一部分,更多关于定义函数(这将详细介绍位置和关键字参数,以及* args和** kwargs语法,允许您使用可变长度参数列表定义函数,而无需使用默认值。)

答案 4 :(得分:1)

在某种程度上,此功能由类方法复制。例如,__repr__方法与Haskell中的Show类型类大致相同:

$ ghci
>>> x = 1
>>> show x
"1"
>>> x = [1,2,3]
>>> show x
"[1,2,3]"

或在Python中

$ python
>>> x = 1
>>> x.__repr__()
'1'
>>> x = [1,2,3]
>>> x.__repr__()
'[1,2,3]'

显然,在每种情况下都会调用一个不同的函数,具体取决于show / __repr__的类型(在Haskell的情况下)或类(在Python的情况下)被应用于/被召唤。

对于支持它们的语言,更接近的是接口 - 抽象类及其所有方法也是抽象的(它们在Java中称为接口,在C ++中称为虚拟类)。您不会倾向于在动态类型语言中看到它们,因为接口的要点是声明一组实现类必须符合的方法及其相关类型。如果没有静态类型,那么就没有必要声明类型应该是什么。

答案 5 :(得分:0)

是的,Python 中存在类型类。您可以使用 dry-python/classes 为它们建模。

示例:

from classes import AssociatedType, Supports, typeclass

class Greet(AssociatedType):
    """Special type to represent that some instance can `greet`."""

@typeclass(Greet)
def greet(instance) -> str:
    """No implementation needed."""

@greet.instance(str)
def _greet_str(instance: str) -> str:
    return 'Hello, {0}!'.format(instance)

def greet_and_print(instance: Supports[Greet]) -> None:
    print(greet(instance))

greet_and_print('world')
# Hello, world!

它还具有完整的 mypy 支持。