代码之家  ›  专栏  ›  技术社区  ›  Chris

用python编写解释程序。IsInstance是否被认为有害?

  •  4
  • Chris  · 技术社区  · 15 年前

    我正在通过解释器将我从scala创建的特定于域的语言移植到python。在这个过程中,我试图找到一种方法,通过这种方法,蟒蛇可以模拟我广泛使用的scala的case类特性。最后,我使用了IsInstance,但感觉自己可能遗漏了一些东西。

    this one 攻击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调用,以达到同样的效果,更加冗长和非pythonic。有更好的方法吗?

    7 回复  |  直到 15 年前
        1
  •  2
  •   Community CDub    7 年前

    总结 :这是编写编译器的一种常见方法,这里就可以了。

    在其他语言中处理这个问题的一个非常常见的方法是“模式匹配”,这正是您所描述的。我想这就是它的名字 case scala中的语句。它是编写编程语言实现和工具的一个非常常见的习语:编译器、解释器等。为什么它这么好?因为实现与数据完全分离(通常是坏的,但在编译器中通常是可取的)。

    问题是,这种编程语言实现的常见习惯用法是Python中的反模式。哦,哦。正如你可能知道的,这更像是一个政治问题,而不是一个语言问题。如果其他的pythonistas看到代码,他们会尖叫;如果其他语言实现者看到代码,他们会立即理解。

    这在Python中是反模式的原因是因为Python鼓励使用duck类型的接口:您不应该基于类型拥有行为,而是应该由对象在运行时可用的方法来定义行为。 S. Lott's answer 如果您希望它是惯用的Python,那么它可以很好地工作,但它添加的很少。

    我怀疑您的设计并不是真正的duck类型化的——毕竟它是一个编译器,并且使用一个名字定义的具有静态结构的类非常常见。如果愿意,可以将对象视为具有“类型”字段,并且 isinstance 用于基于该类型进行模式匹配。

    Addenum:

    模式匹配可能是人们喜欢用函数语言编写编译器等的首要原因。

        2
  •  2
  •   Eloff    15 年前

    在python中有一个经验法则,如果你发现自己编写了大量的if/elif语句,并且有类似的条件(例如,一堆isinstance(…)那么你可能是以错误的方式解决了这个问题。

    更好的方法包括使用类和多态性、访问者模式、dict查找等。在您的案例中,使具有不同类型重载的operator类可以工作(如上所述),具有(类型、运算符)项的dict也可以工作。

        3
  •  2
  •   S.Lott    15 年前

    对。

    不用实例,只要使用 Polymorphism . 这更简单。

    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
    
        4
  •  1
  •   Lennart Regebro    15 年前

    文章没有攻击 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组件体系结构,它通过接口和适配器来处理这个问题,这真的很酷。但这可能太过分了。

    更妙的是,如果您能以某种方式避免进行任何类型的类型检查,但这可能很棘手。

        5
  •  0
  •   Anon    15 年前

    在我使用python 3编写的DSL中,我使用了复合设计模式,因此节点的使用都是多态的,正如S.lott推荐的那样。

    但是,当我首先在输入中读取以创建这些节点时,我确实使用了很多IsInstance检查(针对诸如Collections.Iterable等抽象基类,Python3提供了这些基本类,我相信它们在2.6中),以及对hasattr的检查。 '__call__' 因为我的输入中允许使用可调用文件。这是我发现的最干净的方法(特别是涉及递归的方法),而不仅仅是针对输入尝试操作并捕获异常,这是我想到的另一种选择。当输入无效时,我自己提出了自定义异常,以尽可能提供精确的故障信息。

    将isInstance用于此类测试比使用type()更为普遍,因为isInstance将捕获子类——如果可以对抽象基类进行测试,那就更好了。参见 http://www.python.org/dev/peps/pep-3119/ 有关抽象基类的信息。

        6
  •  0
  •   Omnifarious    15 年前

    在这种特殊情况下,您似乎正在实现的是一个运算符重载系统,它使用对象类型作为您要调用的运算符的选择机制。您的节点类型恰好与您的语言类型相当直接地对应,但实际上您正在编写一个解释器。节点的类型只是一段数据。

    我不知道人们是否可以将他们自己的类型添加到特定于域的语言中。但我还是建议采用表驱动的设计。

    创建一个包含(binary_operator、type1、type2、result_type、evalfunc)的数据表。使用isInstance在该表中搜索匹配项,并有一些条件来优先选择某些匹配项。也许可以使用比表更复杂的数据结构来加快搜索速度,但现在基本上,您使用的是ifelse语句的长列表来进行线性搜索,所以我敢打赌,一个普通的旧表将比您现在所做的略快。

    我不认为IsInstance在很大程度上是错误的选择,因为类型只是您的解释器用来做决定的一部分数据。双重分派和其他类似的技术只会掩盖你的程序正在做的真正的工作。

    在Python中,一个简洁的地方是,由于运算符函数和类型都是第一类对象,所以您可以直接将它们填充到表中(或者您选择的任何数据结构)。

        7
  •  -1
  •   Aoife    15 年前

    如果您需要对参数(除了接收器)进行多态性,例如,要使用示例中建议的二进制运算符处理类型转换,可以使用以下技巧:

    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)
    
    推荐文章