【问题标题】:find out if the `in` operator can be used找出是否可以使用 `in` 运算符
【发布时间】:2015-08-02 11:00:14
【问题描述】:

找出in 运算符是否可以在python 中使用的最简单(也是最优雅)的方法是什么? 如果我打开一个 python shell 并输入:

"" in 2

打印出来:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: argument of type 'int' is not iterable

根据python-docs,一个可迭代对象是:

容器。iter()

返回一个迭代器对象。该对象需要支持下面描述的迭代器协议。如果一个容器支持不同的 迭代类型,可以提供额外的方法来专门 为这些迭代类型请求迭代器。 (一个对象的例子 支持多种形式的迭代将是一个树结构,它 支持广度优先和深度优先遍历。)这种方法 对应于 Python 的类型结构的 tp_iter 槽 Python/C API 中的对象。

所以

hasattr([], "__iter__") and hasattr({}, "__iter__")

按预期返回true,但是

hasattr("test_string", "__iter__")

返回假。但我可以使用

"test" in "test_string"

没有任何问题。

优雅我指的是不使用 try-except 解决方案

【问题讨论】:

  • 旁白:如果您认为 try-except 不优雅,您会发现 Python 非常令人沮丧。 (您的代码也可能到处都有竞争条件,但这是另一个问题。)
  • 动态类型语言的一个问题确实是编译器无法在编译时检查这个(当然你买东西作为回报)。
  • 你也可以检查__contains__
  • hasattr() 只是 getattr() 在 try/except 后面。试图避免异常真的不值得。

标签: python iterator containers


【解决方案1】:

你可以检查'__contains__' in dir(theobject)是否,但我并不认为try不雅。

【讨论】:

  • 当然,如果theobject 的类型定义了__iter__ 而不是__contains__,这将不起作用。或有效的__getitem__,但都不是。或者如果theobject.__contains__ 存储在实例字典而不是类型字典中。或者,如果该类型不使用__dict__,或者确实使用了__contains__,则通过__getattr____getattribute__ 方法动态生成。
  • 它在某些情况下可以工作,但当然try..except 是要走的路。
  • @abarnert:您错过了一个:该对象定义了一个实现不佳的__dir__,并且没有告诉您它正确实现了什么。此外,我有 85% 的把握即使通过元类也无法动态生成特殊方法。
  • @Kevin:哦,我什至没有想到那个,但你是对的,你总是可以伪造__dir__,并且恶意或不恰当地这样做。 :)
  • 这里还有一个有趣的,调用__contains__本身可以修改实例或类并删除__contains__方法:)
【解决方案2】:

in(和not in)运算符使用__contains__() 来检查成员资格(通常)。检查这一点的最简单和最优雅的方法是__contains__ 属性的直接存在,或者Container 抽象基类:

hasattr(obj, '__contains__')

# Python 3: Container
import collections.abc
isinstance(obj, collections.abc.Container)

# Python 2: Container
import collections
isinstance(obj, collections.Container)

但是,文档确实提到:

对于未定义__contains__() 的对象,成员资格测试首先尝试通过__iter__() 进行迭代,然后通过__getitem__() 尝试旧的序列迭代协议,请参阅this section in the language reference

所以,如果你想绝对确定 in 可以在不依赖 try-except 块的情况下使用,你应该使用:

hasattr(obj, '__contains__') or hasattr(obj, '__iter__') or hasattr(obj, '__getitem__')

如果你只期望container-like 对象,我会忽略__iter__(),甚至可能忽略__getitem__(),并坚持使用__contains__()

【讨论】:

  • 即使同时检查IterableContainer(或者hasattring 同时检查__iter____contains__)是不够的;一个带有__getitem__ 的类在调用0 时返回或引发IndexError 将失败这两个测试,但仍然会通过in(和iter)成功。
  • 最新编辑:最简单、最优雅的检查方法是使用try:/except:。它具有正确的好处,而不仅仅是大致/经常正确。
  • @abarnert:对于 3.x,我不同意你的看法。 AFAIK 所有内置类型现在都支持所有预期的方法。用户定义的类型将(或应该)从collections.abc 中的类继承,因为它省去了定义所有mixin 方法的麻烦(例如.index() 用于序列)。
  • @cpburnz:你不是在用 Python 术语思考。仅应在“真正的错误”上引发异常的想法不适合即使 for 循环在封面下也是 try 的语言。此外,如果这是一个真正的错误,那正是您想要except的时候,所以异常可以渗透到您可以适当处理事情的水平(甚至可能一直如此)直到中止程序,如果它是您的代码中的意外逻辑错误)。
  • @cpburnz:啊,我想我看错了。您是说您应该始终使用异常,这只是您是否使用(本地)try/except 还是只是让异常成为异常的问题?在这种情况下,我 100% 同意。
【解决方案3】:

试试正确而优雅的方法。

首先,a in b 是否会引发异常取决于 a 和 b,而不仅仅是 b。

另一个问题是in 有多种工作方式。这是一个支持in但不支持迭代的对象示例:

>>> class EvenNumbers(object):
...     def __contains__(self, n):
...         return n % 2 == 0
...     
>>> even_numbers = EvenNumbers()
>>> 4 in even_numbers
True
>>> 5 in even_numbers
False
>>> for even_number in even_numbers:
...     pass
... 
TypeError: 'EvenNumbers' object is not iterable

这是一个支持迭代但没有定义__contains__的对象示例:

>>> import itertools as it
>>> even_numbers = (2*n for n in it.count())
>>> 4 in even_numbers
True
>>> even_numbers.__contains__
AttributeError: 'generator' object has no attribute '__contains__'

因此,要实现有效的 LBYL,您必须考虑a in b 可以工作(或不工作)的所有可能方法。我这里只列出了几个,还有几个。你会发现你的代码变得又长又丑,最终会意识到 try/except 一直是阻力最小的路径!

【讨论】:

  • 如果我可以只为最后一段给你+2,我会的。不幸的是,当我尝试时,StackOverflow 似乎引发了异常。 :)
【解决方案4】:

迭代器协议实际上不需要类型来支持__iter__。它需要一个类型支持__iter____getitem__,从0 开始连续整数参数。请参阅iter 函数以获得文档中对此的最佳解释。

所以,hasattr(x, "__iter__") 会在测试某些东西是否可迭代时给出假阴性。

那么,您如何做到这一点?好吧,即使你不喜欢它,正确的方法是:

try:
    i = iter(x)
except TypeError:
    # not iterable

另外,请注意,正如 hasattr 的文档所解释的那样:

这是通过调用getattr(object, name) 并查看它是否引发异常来实现的。

所以,实际上,您根本没有避免异常;您只是想出了一种更复杂的方法来引发异常并将该事实隐藏起来。


但与此同时,迭代首先是一个红鲱鱼。 in 运算符是使用 __contains__ 方法实现的。未定义 __contains__ 方法的容器类型将退回迭代和比较,但以这种方式实现它不需要类型。您可以拥有一个比迭代快得多的__contains__(如dictset);你甚至可以成为一个容器而不是一个可迭代的。 (请注意,collections module ABCs 有单独的 ContainerIterable 基;两者都不依赖于另一个。)


那么,如果你真的想在没有任何异常处理的情况下这样做,你怎么能?

好吧,您必须检查以下至少一项是否属实:

  • x 有一个 __contains__ 方法。
  • x 有一个 __iter__ 方法。
  • x 有一个 __getitem__ 方法,当使用数字 0 调用该方法时,要么成功返回,要么引发 IndexError

即使您接受如果不实际尝试使用号码0 调用它就无法测试最后一个,并且只是假设拥有__getitem__ 是“足够接近”,您如何在没有依赖异常?

你真的不能。例如,您可以迭代 dir(x),但这不适用于动态定义 __contains__ 的类,例如,在委托给 self.real_sequence__getattr__ 方法中。

而且,即使你可以,如果你有一个类将__contains__ 定义为不带参数,会发生什么?该属性存在,但in 仍将引发TypeError

所有这些都忽略了which special methods are looked up on the object and which on the type itself 上的(依赖于实现的)规则。例如,在 CPython 2.7 中:

>>> class C(object): pass
>>> c = C()
>>> c.__contains__ = lambda self, x: return True
>>> hasattr(c, '__contains__')
True
>>> c.__contains__(2)
True
>>> 2 in c
TypeError: argument of type 'C' is not iterable

【讨论】:

  • 最后一个很有趣。不考虑 cpython,python 是否指定它是否会/不会查找实例或类?
  • @wim:IIRC,Python 2.x 只是说“可能”在类中查找“特殊方法”。我相信 3.x 将其限制为特殊方法的特定列表,但无论是否以这种方式查找它们,它仍然由实现决定。
  • @wim:我们开始吧,3.4.12 Special method lookup for new-style classes:“对于新型类,特殊方法的隐式调用只有在对象类型上定义时才能保证正确工作,而不是在对象的实例字典中。”
  • @wim:这是一个特定于实现的优化,他们不想束缚其他实现的手。
  • @wim:我对 3.x 的看法是错误的; 3.3.9 Special method lookup 仍然只是说“隐式调用特殊方法”,没有具体的列表。
猜你喜欢
  • 1970-01-01
  • 2010-12-18
  • 2023-01-23
  • 2022-06-29
  • 2018-01-04
  • 2015-05-05
  • 2011-07-25
  • 2014-09-28
  • 2011-02-28
相关资源
最近更新 更多