检测序列参数的正确方法是什么?

18
我想编写一个函数,它可以接受参数,该参数既可以是序列也可以是单个值。值的类型可以是字符串,整数等等,但我不想将其限制为硬编码列表。
换句话说,我想知道参数X是序列还是我需要将其转换为序列以避免后续特殊情况处理。我可以这样做: type(X) in (list, tuple) 但是可能存在其他我不知道的序列类型,并且没有通用的基类。
-N. 编辑:请参见下面的“答案”,了解为什么大多数答案都无法帮助我。也许您有更好的建议。

1
请注意,类型为str的对象也是序列类型! - pi.
2
@pi:没错,这才是真正的问题所在。下面所有“好”的答案都没有考虑到这一点。 - noamtm
1
嗯,我实际上会说是因为你没有真正定义问题。如果你最初就说明了任务是什么,你可能会立即得到更有用的答案。 - Matthew Schinckel
1
麻烦的地方在于,OP所指的“sequence”并非Python中所定义的严格意义上的“序列”,而是更为宽泛的“多个事物”与“一个事物”的区别。 - Gregg Lind
12个回答

19

从2.6开始,请使用抽象基类

>>> import collections
>>> isinstance([], collections.Sequence)
True
>>> isinstance(0, collections.Sequence)
False

此外,ABC可以定制以考虑异常情况,例如不将字符串视为序列。这里是一个例子:
import abc
import collections

class Atomic(object):
    __metaclass__ = abc.ABCMeta
    @classmethod
    def __subclasshook__(cls, other):
        return not issubclass(other, collections.Sequence) or NotImplemented

Atomic.register(basestring)

注册后,可以使用isinstanceissubclass来使用Atomic类:
assert isinstance("hello", Atomic) == True

这比硬编码列表要好得多,因为您只需要注册规则的例外情况,代码的外部用户可以注册他们自己的例外情况。
请注意,在Python 3中,指定元类的语法已更改,并且删除了basestring抽象超类,需要使用以下内容:
class Atomic(metaclass=abc.ABCMeta):
    @classmethod
    def __subclasshook__(cls, other):
        return not issubclass(other, collections.Sequence) or NotImplemented

Atomic.register(str)

如果需要的话,可以编写同时兼容Python 2.6+和3.x的代码,但这样做需要使用稍微复杂一些的技术,动态创建所需的抽象基类,从而避免由于元类语法差异导致的语法错误。这本质上与Benjamin Peterson的six模块的with_metaclass()函数相同。
class _AtomicBase(object):
    @classmethod
    def __subclasshook__(cls, other):
        return not issubclass(other, collections.Sequence) or NotImplemented

class Atomic(abc.ABCMeta("NewMeta", (_AtomicBase,), {})):
    pass

try:
    unicode = unicode
except NameError:  # 'unicode' is undefined, assume Python >= 3
    Atomic.register(str)  # str includes unicode in Py3, make both Atomic
    Atomic.register(bytes)  # bytes will also be considered Atomic (optional)
else:
    # basestring is the abstract superclass of both str and unicode types
    Atomic.register(basestring)  # make both types of strings Atomic

在2.6版本之前,operator模块中有类型检查器。

>>> import operator
>>> operator.isSequenceType([])
True
>>> operator.isSequenceType(0)
False

2
assert operator.isSequenceType("hello") == True (正如其他地方指出的那样,字符串是序列,我相信Coady也知道...原始问题规格不足。)
- Gregg Lind

5
以上提到的所有方式的问题在于,str被视为一个序列(它是可迭代的,有getitem等),但通常被视为单个项目。例如,函数可以接受一个参数,该参数可以是文件名或文件名列表。函数如何从中检测出第一个?最Pythonic的方法是什么?
根据修改后的问题,看起来你想要的是更像这样的东西:
def to_sequence(arg):
    ''' 
    determine whether an arg should be treated as a "unit" or a "sequence"
    if it's a unit, return a 1-tuple with the arg
    '''
    def _multiple(x):  
        return hasattr(x,"__iter__")
    if _multiple(arg):  
        return arg
    else:
        return (arg,)

>>> to_sequence("a string")
('a string',)
>>> to_sequence( (1,2,3) )
(1, 2, 3)
>>> to_sequence( xrange(5) )
xrange(5)

这并不能保证处理所有类型,但它可以很好地处理你提到的情况,并且对大多数内置类型应该能做到正确处理。

使用时,请确保接收此输出的任何内容都可以处理可迭代对象。


8
正如@Jim Brissom在有关问题的此答案的评论中指出的那样,字符串没有__iter__属性是Python 3中已修复的一个差异,这使得该方法不兼容于较新的版本。 - martineau

4

这里介绍了序列的概念:https://docs.python.org/2/library/stdtypes.html#sequence-types-str-unicode-list-tuple-bytearray-buffer-xrange

所以序列不同于可迭代对象。我认为序列必须实现__getitem__,而可迭代对象必须实现__iter__。例如,字符串是序列,不实现__iter__,xrange对象是序列,不实现__getslice__

但从您想要做的事情来看,我不确定您是否想要序列,而更多地是想要可迭代对象。因此,如果您不想使用字符串等,请使用hasattr("__iter__", X),而不是hasattr("__getitem__", X)


hasattr不足以检查它是否可迭代。列表type也将具有__iter__成员,但本身不可迭代。即hasattr(list,'iter')==True,但iter(list)->“TypeError:'type' object is not iterable”。它还会错过实现__getitem__但不实现__iter__的对象。 - Brian
好的,针对这个特定情况:它是一个内置函数,并且__iter__是一个静态方法(1个参数)。所以基本上,是的,鸭子类型失败了!但是iter()可以从不可迭代对象创建一个可迭代对象(因此不是解决方案)。唯一“确定”的解决方案是像你和noamtm一样排除/包含所需的类型。 - Piotr Lesnicki

4

在我看来,Python 的做法是将列表作为 *list 传递。例如:

myfunc(item)
myfunc(*items)

实际上,myfunc(*items) 会传递一个 items 列表的元组。 - martineau

3
在这种情况下,我更喜欢始终使用序列类型或始终使用标量。字符串不是唯一在此设置中表现不良的类型;任何具有聚合用途并允许迭代其部分的类型都可能会出现问题。

3
最简单的方法是检查是否可以将其转换为迭代器。即
try:
    it = iter(X)
    # Iterable
except TypeError:
    # Not iterable

如果您需要确保它是可重启或随机访问序列(即不是生成器等),那么这种方法将不足够。

正如其他人所指出的,字符串也是可迭代的,因此如果您需要排除它们(特别是在通过项进行递归时很重要,因为list(iter('a'))再次给出['a']),则可能需要使用以下方法来明确地排除它们:

 if not isinstance(X, basestring)

这并没有帮助我,因为一个 str 对象是可迭代的,但我想将其视为单个项目。 - noamtm
这就是为什么我提到要检查isinstance。由于字符串是完全可迭代和可索引的,没有办法基于它们的“顺序性”来排除它们,而不会同时排除其他某些序列类型。你唯一的选择就是添加一个显式的isinstance检查,将它们作为异常情况处理。 - Brian

2
我是一个新手,不知道正确的做法。我想回答我的答案:
所有上述方法的问题在于 str 被视为序列(可迭代,具有 __getitem__ 等特性),但通常被视为单个项。
例如,函数可能接受一个参数,该参数可以是文件名或文件名列表。函数检测第一个还是后者的最Pythonic方式是什么?
我应该发表这个问题作为一个新问题吗?编辑原始问题?

我认为Coady的回答是最好的,因为它允许我们挑选哪些类被视为序列而不考虑它们是否具有特殊方法。 - martineau

1

我认为我会检查对象是否具有某些指示它是序列的方法。不确定是否有官方定义可以说明什么构成序列。我能想到最好的是,它必须支持切片。因此,你可以这样说:

is_sequence = '__getslice__' in dir(X)

你可能还需要检查一下你将要使用的特定功能。

正如 pi 在评论中指出的那样,一个字符串是一个序列,但你可能不想把它当作一个序列来处理。你可以添加一个显式测试,检查类型是否不是 str。


1
hasattr(X, 'getslice') 是做相同事情更有效的方法。 - Moe
1
__getslice__实际上已经过时了 - 用户类型中的切片最好通过在__getitem__方法中接受切片对象来处理,这样就可以在没有__getslice__的情况下进行切片。我认为,即使不可切片也应该被视为序列。 - Brian

1
如果字符串是问题所在,检测序列并过滤掉字符串的特殊情况:
def is_iterable(x):
  if type(x) == str:
    return False
  try:
    iter(x)
    return True
  except TypeError:
    return False

0

修订答案:

我不知道你所说的“序列”是否与 Python 手册中所称的“序列类型”相匹配,但如果是的话,你应该寻找 __Contains__ 方法。这是 Python 用来实现检查“if something in object:”的方法。

if hasattr(X, '__contains__'):
    print "X is a sequence"

我的原始回答:

我会检查你收到的对象是否实现了迭代器接口:

if hasattr(X, '__iter__'):
    print "X is a sequence"

对我来说,这是最接近你对序列定义的匹配,因为它允许你做像这样的事情:

for each in X:
    print each

文件对象具有迭代器接口。可能还有其他不是真正序列的对象也具有它。 - Dave Costa
@Dave:你说得对,文件对象具有迭代器接口。我会添加一个新的修订答案。 - Ricardo Reyes
我想重申我在另一篇帖子中发表的评论 - 在dir(X)中,您应该使用hasattr(X,'contains')而不是'contains'。 - Moe
@Moe:可以合理地预期,dir()永远不会返回非常长的列表,因此“in”和“hasattr”的性能差异是微不足道的。我认为,“in”更清晰地表明了检查的意图,尽管当然这是主观的。 - Ricardo Reyes
__contains__ 适用于容器类型,序列类型必须实现 __len____getitem__,请参见此处:https://docs.python.org/zh-cn/3/library/collections.abc.html。 - Stan
显示剩余5条评论

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