使用Python的`isinstance`函数是否正确?

13

我有一个由不同方块组成的2D数组,它们都继承自Block类。我想检查我点击的方块是否是泥土类型的方块,如下所示:

clickedblock = getClickedBlock()

if isinstance(clickedblock, Dirt):
    place a block

else:
    don't place a block

我听说isinstance是不好的,应该避免使用,因为它会在代码中创建分支。什么时候使用isinstance会更好呢?

解决我的问题还有另一种更繁琐的方法,就是在Block中添加一个名为'id'的字段,然后检查它是否等于某个代表Dirt的常量。但那听起来很糟糕,而且比简单的isinstance更容易出错。


@DietrichEpp 这也是我在这里寻求确认的原因。 - Name McChange
@Keyser:这正是我的观点。你不同意,但你没有告诉我为什么不同意,因此你没有给其他人机会去选择自己的想法。 - Dietrich Epp
@DietrichEpp 我不说出我的原因并不会阻止人们自己下定决心。甚至可能相反。我之所以没有说是因为这不是正确的论坛 :) - keyser
3
我认为最符合面向对象的方式应该是像这样:if clickedblock.is_placeable(position): ... 或者 try: clickedblock.place(position) - Steven Rumbalski
@DietrichEpp,你的陈述开头有点傲慢。你并不知道他们可能是个孩子(实际上,看起来OP在提问时大约14岁)。这也意味着询问这样的问题是幼稚的(事实并非如此)。顺便说一句,无论别人告诉你某件事是好还是坏都不重要。程序员们关心的是什么情况下使用isinstance是合理的,什么情况下应该避免使用它。 - Dennis
@Dennis:我的抱怨是关于说“X很糟糕”这句话……我认为“X很糟糕”是一种居高临下的说法,省略了有用的信息,这样的陈述应该带着一些保留。这是你对孩子说的话,当你不想为你的建议提供任何理由时。 - Dietrich Epp
4个回答

14

你的例子看起来是一个合理使用 isinstance() 的情况。

并不是说 isinstance() 是坏的,通常多态性可以用于相同的目的(这导致在使用类的地方代码更加清晰)。

但有时候,isinstance() 就是你需要的。例如,检测变量是否为字符串的 Pythonic 方式是 isinstance(var, basestring)


3
我学习了一个教训,不要使用它。问题在于结果对导入类定义的方式非常敏感:
  • 对象被实例化的地方
  • isinstance测试执行的地方
如果一个是相对导入,另一个是绝对导入-则检查将失败。本质上,这就像在SomeClasssomepackage.SomeClass之间进行平等检查。这甚至无关乎它们来自同一文件等。此外,如果你在PYTHONPATH中同时拥有根目录和somepackage目录,则绝对样式导入可能表示从任意一个“源根”路径开始的路径,因此两个不同的绝对样式导入将导致失败的实例检查。
人们可以争论一些良好的做法是否会防止这种情况发生,但良好的做法也主要是关于不冒险。出于这个精神,我更喜欢在一个共同的祖先类中放置一些抽象方法,这样我以后可以依赖于事物如何操作而不是解释器认为它们是什么。
在Java中,每个类都解析为其完全限定类名。在程序内这些是唯一的,并且实例的测试非常简单。在Python中,这些可能会出现问题。

1
您所描述的问题是,当相同的模块以不同名称导入时,Python 无法确定这两个名称引用相同的文件。这是您代码中的错误,而不是isinstance的问题。这也正是为什么您不要将包的内部放入PYTHONPATH(或sys.path)的原因。 - Ethan Furman
天啊,我已经写了这一切都是关于导入的。如果你使用库或别人的代码,并且没有时间深入研究这种行为,那么“你有一个错误”这一点就变得不那么重要了。 - bawey
你可以说“你已经学会了不使用 if 的艰难方式”,因为某个库在他们的一些对象上搞砸了 __bool__ 方法。人们代码中的错误并不是一个彻底停止使用核心语言特性的好理由,而 isinstance 绝对是其中之一。 - Ethan Furman
说实话,我同意你的观点:通常最好明确检查某些东西是否为空,是否为None,或者具有LocalProxy实例(看看你,Flask和Werkzeug),直到你开始处理它,而不是仅仅依赖于if(my_var)。我的偏见可能不公平,措辞也不够好,但是对于来自不同语言背景的人来说,isinstance可能会带来很大的惊喜。如果这使我成为一个平庸的开发者-那就这样吧-我永远无法超越某些限制。与此同时,我只是更喜欢不会让我犯错的工具。干杯。 - bawey

2

如果您不想使用它,您还有其他选择。传统的鸭子类型解决方案:

try:
    clickedblock_place = clickedblock.place
except AttributeError:
    # don't place block
else:
    clickedblock_place()

或者您可以使用hasattr

if hasattr(clickedblock, 'place'):
    clickedblock.place()

我很少使用isinstance,除非是为了检查继承层次结构,比如说,如果你需要知道一个名称指向一个str还是一个unicode

if isinstance(str1, basestring):
    blah, blah, blah

4
请不要使用except:!它非常糟糕,会导致你的代码出现难以治愈的错误。使用except:会捕获所有异常,这将使得调试.place()中合法bug成为一场噩梦,还会捕获ctrl-c以及其他许多不想要的东西。 - David Wolever
2
如果你想使用 try/except(不一定是坏事),请使用:try: place_block = clickedblock.place; except AttributeError: dont_place(); else: place_block() - 这样你就可以看到 .place() 方法中的合法错误。 - David Wolever
2
感谢您的编辑,@David Wolever。您当然是完全正确的。我很匆忙。 - MikeHunter

1
我认为我会将其更改为更像这样:
PLACEABLE_TYPES = [ Dirt ]
if isinstance(clickedblock, PLACEABLE_TYPES):
   place the block
else:
   don't place the block

但是评论中的想法是:

if clickedblock.is_placeable(that_place):
    place the block
else:
    don't place the block

同样也有优点。


1
第一个例子几乎肯定是不正确的!如果 clickedblockDirt 的子类,它将失败。更好的方法是:if isinstance(clickedblock, PLACEABLE_TYPES)。但第二个例子 clickedblock.is_placeable(…) 可能要好得多。 - David Wolever

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