在Python中迭代列表或单个元素

30

我希望能够遍历一个未知函数的输出结果。不幸的是,我并不知道这个函数是返回单个项目还是元组。这应该是一个普遍存在的问题,必须有一种标准的方法来处理它 - 目前我的解决方案相当丑陋。

x = UnknownFunction()
if islist(x):
    iterator = x
else:
    iterator = [x]

def islist(s):
    try:
        len(s)
        return True
    except TypeError:
        return False

for ii in iterator:
    #do stuff

6
标准的方法是让函数返回一个包含一个元素的元组。 - Ignacio Vazquez-Abrams
6
当然,如果你可以控制那个功能。问题似乎是在说他没有控制权。 - Fred Larson
1
@Fred Larson:你总是至少有这么多的控制权:lambda *a, **kw: (f(*a, **kw), ) - Rosh Oxymoron
1
@senderle:希望在某个层面上,该函数是已知的并且可以被包装。我还没有见过不可预测的API。;) - Rosh Oxymoron
@Rosh 用户提供要调用的函数名称,因此结果的形式在运行时是未知的。 - Edward
显示剩余3条评论
8个回答

34
这个问题的最通用解决方案是使用抽象基类 collections.Iterableisinstance
import collections

def get_iterable(x):
    if isinstance(x, collections.Iterable):
        return x
    else:
        return (x,)

正如Kindall所建议的那样,您可能还想测试basestring

    if isinstance(x, collections.Iterable) and not isinstance(x, basestring):

现在有些人可能会认为,就像我曾经想过的那样,"isinstance不是被认为是有害的吗?它不会把你锁定到使用一种类型吗?使用hasattr(x, '__iter__')不是更好吗?" 答案是:当涉及到抽象基类时,情况并非如此。事实上,您可以定义自己的类,并具有一个__iter__方法,即使您不是collections.Iterable的子类,它也将被识别为collections.Iterable的一个实例。这是因为collections.Iterable定义了一个__subclasshook__,通过实现其定义的任何定义来确定传递给它的类型是否为可迭代类型。
>>> class MyIter(object):
...     def __iter__(self):
...         return iter(range(10))
... 
>>> i = MyIter()
>>> isinstance(i, collections.Iterable)
True
>>> collections.Iterable.__subclasshook__(type(i))
True

2
请注意,如果您只想包括类似于列表和元组的类型,则可以测试collections.Sequence - senderle
我还得到了isinstance('aa',collections.Iterable)的结果为True,这不是我期望的。有什么调整方法吗? - Dinesh
@Dinesh,True是正确的返回值--字符串是字符的可迭代对象!如果你想让你的代码对字符串有不同的行为,你需要添加一个测试。使用isinstance(x, basestring)来捕获字符串和Unicode对象。 - senderle
从 Python 3.3 开始,使用或导入 collections 中的 ABC(如 Iterable)而不是 collections.abc 已被弃用,在 3.9 版本中将停止工作。请改为使用 from collections.abc import Iterable - Michele
@Michele 非常正确。这个代码在几个方面都已经过时了——在Python 3中根本不存在basestring!我会尽快更新它,但如果你愿意,可以随意编辑。 - senderle

7

在需要使用代码的各个地方都包含它并不是特别优雅。因此,编写一个执行调整的函数。这里是我为类似先前问题提出的建议。它将字符串(通常可迭代)作为单个项目进行特殊处理,这正是我通常想要的。

def iterfy(iterable):
    if isinstance(iterable, basestring):
        iterable = [iterable]
    try:
        iter(iterable)
    except TypeError:
        iterable = [iterable]
    return iterable

使用方法:

for item in iterfy(unknownfunction()):
     # do something

更新 这里有一个生成器版本,使用新的(Python 3.3)yield from语句。

def iterfy(iterable):
    if isinstance(iterable, str):
        yield iterable
    else:
        try:
            # need "iter()" here to force TypeError on non-iterable
            # as e.g. "yield from 1" doesn't throw until "next()"
            yield from iter(iterable)
        except TypeError:
            yield iterable

6
我更喜欢iterrify这个名字。;) - Rosh Oxymoron

2

也许更好的方法是使用 collections.Iterable 来判断输出是否可迭代。

import collections

x = UnknownFunction()
if not isinstance(x, collections.Iterable): x = [x]

for ii in x:
    #do stuff

如果x的类型是以下之一,则此方法适用于 - list, tuple, dict, str或从这些类派生的任何类。


1
你需要做以下事情:
iterator = (x,) if not isinstance(x, (tuple, list)) else x

那么

for i in iterator:
    #do stuff

1
这在元组中不起作用(在问题中提到)。它也不适用于列表的子类。 - Rosh Oxymoron
你可能需要这样写:iterator = (x,) if type(x) != tuple else x - dgrant
3
not isinstance(x, cls)type(x) != cls 更可取。 - Rosh Oxymoron
哦,不知道呢。怎么回事?(我会在答案中修改它。) - TorelTwiddler
通过直接与类进行比较,您将排除所有子类。例如,这在命名元组中是行不通的。 - Rosh Oxymoron
显示剩余2条评论

0
你也可以尝试使用 operator.isSequenceType 函数。
import operator
x = unknown_function()
if not operator.isSequenceType(x) and not isinstance(x, basestring):
    x = (x,)
for item in x:
    do_something(item)

0
你可以定义一个函数,确保返回的值支持迭代(例如strdicttuple等,包括不直接继承这些类的用户自定义序列类型),而不是直接检查它是否为tuplelist
def ensure_iterable(x):
    return (x,) if not hasattr(x, '__iter__') else x

x = ensure_iterable(UnknownFunction())
for i in x:
    do_something(i)

0

如果您使用生成器,可能会获得更好的性能。这应该适用于 Python 3.3 及以上版本。

from collections import Iterable

def iterrify(obj):
    """
    Generator yielding the passed object if it's a single element or
    yield all elements in the object if the object is an iterable.

    :param obj: Single element or iterable.
    """
    if isinstance(obj, (str, bytes)):  # Add any iterables you want to threat as single elements here
        yield obj
    elif isinstance(obj, Iterable):  # Yield from the iterables.
        yield from obj
    else:  # yield single elements as is.
        yield obj

0

我喜欢别人提出的使用可迭代对象 suggested 的方法。不过在某些情况下,以下方法可能更好。这是一种更加 EAFP (https://docs.python.org/3.5/glossary.html#term-eafp) 的方式:

In [10]: def make_iter(x): 
    ...:         try: 
    ...:             return iter(x) 
    ...:         except TypeError: 
    ...:             # We seem to be dealing with something that cannot be itereated over. 
    ...:             return iter((x,)) 
    ...:              

In [11]: make_iter(3)                                                                                                                                                                         
Out[11]: <tuple_iterator at 0x7fa367b29590>

In [13]: make_iter((3,))                                                                                                                                                                      
Out[13]: <tuple_iterator at 0x7fa367b4cad0>

In [14]: make_iter([3])                                                                                                                                                                       
Out[14]: <list_iterator at 0x7fa367b29c90>

这不涉及检查我们正在处理什么。我们只是尝试获取一个迭代器,如果失败了,我们就假设失败是因为我们正在处理某些无法迭代的东西(好吧,它似乎真的不能)。所以我们只需制作一个元组并从中制作一个迭代器。


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