用Python编写解释器。isinstance函数是否有害?

4
我正在将我创建的领域特定语言的解释器从Scala移植到Python。在这个过程中,我试图找到一种类似于Scala的case class功能的pythonic方法来模拟它,因为我经常使用它。最终,我采用了isinstance,但感觉可能错过了什么。
这篇文章那样批评isinstance的使用让我想知道是否有更好的解决方法,而不需要进行基本重写。
我已经建立了许多Python类,每个类都代表不同类型的抽象语法树节点,例如For、While、Break、Return、Statement等。
Scala允许处理操作符评估,如下所示:
case EOp("==",EInt(l),EInt(r)) => EBool(l==r)
case EOp("==",EBool(l),EBool(r)) => EBool(l==r)

到目前为止,我在将代码转换为Python时广泛使用elif块和isinstance调用来实现相同的效果,这样做更冗长且不符合Python编程规范。是否有更好的方法?


10
“被认为有害”的“被认为有害”也是有害的。即使是最受诟病的控制结构goto,在适当使用时也不是“被认为有害”的。以下解决方案很可能是更好的方法,但不要陷入isinstance或其他任何东西本质上是邪恶的,永远不能使用的心态。如果那是真的,它就不会出现在语言中。 - Glenn Maynard
1
@Glenn - 就我所知,“适当使用goto”只在C语言中存在。在任何更高级别的语言中,通常不应该以传统意义上使用goto。 - Chris Lutz
1
@Glenn Maynard:「有害」的问题在于:“程序员的质量是由他们所写程序中跳转语句密度递减函数所决定。” isinstance 的密度也遵循着同样的规律。零是最好的,大于零则意味着存在设计问题。许多事物都对优秀编程有害,其中包括 GOTO 语句。 - S.Lott
2
GOTO的需要表明了语言的局限性,但这并不是程序员的错。C语言具有非常有限的自动清理功能;这就产生了对goto的合理需求。另一种选择通常是深度嵌套的条件语句,这更加恶劣;人们编写了糟糕的代码来避免使用goto,仅仅因为他们被说服了它总是错误的,结果更糟糕。零确实是最好的,但当它需要更糟糕的代码扭曲时就不是了。 - Glenn Maynard
回到重点:如果你能找到一个干净的方法来避免使用isinstance,那通常是个好主意;但如果你不能,不要因为听说“isinstance有害”就做更糟糕的事情。语言特性并不是陷阱;至少在设计良好的语言中,它们都是有目的的。 - Glenn Maynard
7个回答

2

是的。

不用实例,只需使用多态性。这更简单。

class Node( object ):
    def eval( self, context ):
        raise NotImplementedError

class Add( object ):
    def eval( self, context ):
        return self.arg1.eval( context ) + self.arg2.eval( context )

这种情况非常简单,不需要使用isinstance


如果需要强制转换时该怎么办呢?

Add( Double(this), Integer(that) )

这仍然是一个多态性问题。
class MyType( object ):
    rank= None
    def coerce( self, another ):
        return NotImplemented

class Double( object ):
    rank = 2
    def coerce( self, another ):
        return another.toDouble()
    def toDouble( self ):
        return self
    def toInteger( self ):
        return int(self)

class Integer( object ):
    rank = 1
    def coerce( self, another ):
        return another.toInteger() 
    def toDouble( self ):
        return float(self)
    def toInteger( self ): 
        return self

 class Operation( Node ):
    def conform( self, another ):
        if self.rank > another.rank:
            this, that = self, self.coerce( another )
        else:
            this, that = another.coerce( self ), another
        return this, that
    def add( self, another ):
        this, that = self.coerce( another )
        return this + that

它确实需要所有涉及的类型都在您的控制之下,但这并非总是如此。大多数情况下,人们询问此问题时,他们正在处理内置类型或第三方模块。 - Lennart Regebro
@Lennart Regebro: "under your control"? 我不明白。你可以“包装”第三方类,以添加缺失的功能;你可以子类化第三方类以添加缺失的功能。而且——这是Python——你有源代码,可以修改以添加缺失的功能。我不确定你在暗示什么。 - S.Lott
但是如果添加时返回类型可以更改呢?例如,如果我有一个整数类和一个双精度类,并调用Add(Integer(i), Double(d))。有不同的可能行动来处理接下来会发生什么,但每个行动似乎都取决于知道代码正在将整数和双精度相加,以便在某种类中包装结果答案。 - Chris
@Chris:这是“双重分派”设计模式。如果你认为你有这种情况,那么你可能有一个相当严重的设计问题,需要重新考虑。通常,你可以通过分配转换责任来处理这个问题。 - S.Lott
Chris:如果你想要完整的模式匹配,最好使用像Scala或OCaml这样内置了它的语言。其次,可以在你选择的语言中实现模式匹配。您可以使用Python对象树在运行时构建模式,并在业务端使用任意可调用函数。为了提高效率,您可以使用标准的函数式编程语言编译器技术将它们转换为BDD。您甚至可以将其编译成Python代码并执行。但首先请尝试双重调度。 - Kragen Javier Sitaker

2

在Python中有一个经验法则,如果你发现自己正在编写大量的if/elif语句,并且条件相似(例如一堆isinstance(...)),那么你可能正在错误地解决问题。

更好的方法包括使用类和多态性、访问者模式、字典查找等。在您的情况下,创建一个Operators类并为不同类型重载它可以工作(如上所述),也可以使用(type, operator)项的字典。


这不是访问者模式的一种体现吗? - Paul Biggar

2
概括: 这是编写编译器的常见方式,在此处也完全适用。
在其他语言中,处理这个问题的一种非常常见的方法是“模式匹配”,这正是您所描述的。我想这就是 Scala 中该 case 语句的名称。这是一种编写编程语言实现和工具(编译器、解释器等)的非常常见的惯用法。为什么它如此出色?因为实现与数据完全分离(这通常是不好的,但在编译器中通常是理想的)。
然后问题在于,这种编程语言实现的常见惯用法在 Python 中是一种反模式。哦哦。正如您可能已经注意到的那样,这更多地是一个政治问题而不是语言问题。如果其他 Python 程序员看到了这段代码,他们会尖叫;如果其他语言实现者看到了它,他们会立即理解。
这在 Python 中成为反模式的原因是因为 Python 鼓励鸭子类型接口:您不应该根据类型来定义行为,而应该根据对象在运行时可用的方法来定义行为。如果您希望它成为符合 Python 惯例的代码,则 S. Lott 的答案 就可以胜任,但它并没有增加多少东西。
我怀疑你的设计并不是真正的鸭子类型 - 毕竟它是一个编译器,使用名称定义和静态结构定义类是非常常见的。如果你愿意,你可以将你的对象视为具有“类型”字段,并使用isinstance基于该类型进行模式匹配。
附注:
模式匹配可能是人们喜欢用函数式语言编写编译器等程序的头号原因。

1

这篇文章并没有攻击 isinstance。它攻击的是让你的代码测试特定类的想法。

而且,确实有更好的方法。或者说有几种方法。例如,你可以将类型处理成一个函数,然后通过按类型查找正确的函数来完成处理。就像这样:

def int_function(value):
   # Do what you mean to do here

def str_function(value):
   # Do what you mean to do here

type_function = {int: int_function, str: str_function, etc, etc}

def handle_value(value):
   function = type_function[type(value)]
   result = function(value)
   print "Oh, lovely", result

如果您不想自己处理这个注册表,可以看看Zope组件架构,它通过接口和适配器来处理,非常酷。但这可能有些过度。

更好的方法是尽量避免进行任何类型检查,但这可能有些棘手。


0
在我使用Python 3编写的DSL中,我使用了组合设计模式,使得节点在使用时都具有多态性,正如S. Lott所推荐的那样。
但是,在首次读取输入以创建这些节点时,我确实经常使用isinstance检查(针对抽象基类,例如collections.Iterable等,Python 3提供了这些类,我认为2.6也有),以及检查是否具有'__call__'属性,因为我的输入允许可调用对象。这是我发现的最清晰的方法(特别是涉及递归时),而不是仅尝试对输入执行操作并捕获异常,这是我想到的替代方法。当输入无效时,我自己引发自定义异常,以提供尽可能精确的失败信息。
对于此类测试,使用isinstance比使用type()更通用,因为isinstance将捕获子类 - 如果您可以针对抽象基类进行测试,那就更好了。有关抽象基类的信息,请参见http://www.python.org/dev/peps/pep-3119/

这里松散地定义了组合设计模式,因为我不必担心跳过静态类型的障碍 - 但与组合模式相同的关键点。我实现了不同的节点类型,实际上并没有使用不同的类,而是使用一个工厂来修改基本节点类以适应不同的节点类型,但这是另一个话题。 - Anon

0
在这种情况下,您似乎正在实现一种运算符重载系统,该系统使用对象的类型作为您要调用的运算符的选择机制。 您的节点类型恰好相当直接地对应于您的语言类型,但实际上您正在编写一个解释器。 节点的类型只是数据的一部分。
我不知道人们是否可以向您的特定领域语言添加自己的类型。 但无论如何,我都建议采用表驱动设计。
创建一个包含(二元运算符、类型1、类型2、结果类型、evalfunc)的数据表。 使用isinstance搜索匹配项,并具有某些标准以优先考虑某些匹配项。 可能可以使用比表格更复杂的数据结构来加快搜索速度,但现在您基本上正在使用长列表的ifelse语句进行线性搜索,因此我打赌普通的表格将比您现在所做的略微快一些。
我认为在这里使用isinstance并没有错,主要是因为类型只是您的解释器正在处理的数据之一,以便做出决策。 双重分派和其他类似技术只会掩盖程序正在执行的真正核心。
Python 中的一个很棒的特性是,由于操作函数和类型都是一等对象,所以你可以直接将它们放入表格(或任何你选择的数据结构)中。

-1
如果您需要在参数上使用多态(除了接收器),例如根据您的示例建议处理二元运算符的类型转换,您可以使用以下技巧:
class EValue(object):

    def __init__(self, v):
        self.value = v

    def __str__(self):
        return str(self.value)

    def opequal(self, r):
        r.opequal_value(self)

    def opequal_int(self, l):
        print "(int)", l, "==", "(value)", self

    def opequal_bool(self, l):
        print "(bool)", l, "==", "(value)", self

    def opequal_value(self, l):
        print "(value)", l, "==", "(value)", self


class EInt(EValue):

    def opequal(self, r):
        r.opequal_int(self)

    def opequal_int(self, l):
        print "(int)", l, "==", "(int)", self

    def opequal_bool(self, l):
        print "(bool)", l, "==", "(int)", self

    def opequal_value(self, l):
        print "(value)", l, "==", "(int)", self

class EBool(EValue):

    def opequal(self, r):
        r.opequal_bool(self)

    def opequal_int(self, l):
        print "(int)", l, "==", "(bool)", self

    def opequal_bool(self, l):
        print "(bool)", l, "==", "(bool)", self

    def opequal_value(self, l):
        print "(value)", l, "==", "(bool)", self


if __name__ == "__main__":

    v1 = EBool("true")
    v2 = EInt(5)
    v1.opequal(v2)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接