30

I would like to iterate over the outputs of an unknown function. Unfortunately I do not know whether the function returns a single item or a tuple. This must be a standard problem and there must be a standard way of dealing with this -- what I have now is quite ugly.

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
Edward
  • 305
  • 1
  • 3
  • 7

8 Answers8

34

The most general solution to this problem is to use isinstance with the abstract base class collections.Iterable.

import collections

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

You might also want to test for basestring as well, as Kindall suggests.

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

Now some people might think, as I once did, "isn't isinstance considered harmful? Doesn't it lock you into using one kind of type? Wouldn't using hasattr(x, '__iter__') be better?"

The answer is: not when it comes to abstract base classes. In fact, you can define your own class with an __iter__ method and it will be recognized as an instance of collections.Iterable, even if you do not subclass collections.Iterable. This works because collections.Iterable defines a __subclasshook__ that determines whether a type passed to it is an Iterable by whatever definition it implements.

>>> class MyIter(object):
...     def __iter__(self):
...         return iter(range(10))
... 
>>> i = MyIter()
>>> isinstance(i, collections.Iterable)
True
>>> collections.Iterable.__subclasshook__(type(i))
True
Community
  • 1
  • 1
senderle
  • 145,869
  • 36
  • 209
  • 233
  • 2
    Note also that you could test for `collections.Sequence` if you want to only include list- and tuple- like types. – senderle Jul 15 '11 at 18:07
  • I also get `isinstance('aa',collections.Iterable)` as `True` which is not what I was expecting. Is there a tweak? – Dinesh Dec 22 '15 at 02:59
  • @Dinesh, True is the correct thing to return -- a string is an Iterable of characters! If you want your code to behave differently for strings, you have to add a test. Use isinstance(x, basestring) to capture both strings and unicode objects. – senderle Dec 25 '15 at 15:16
  • Using or importing the ABCs (like `Iterable`) from `collections` instead of from `collections.abc` is deprecated since Python 3.3,and in 3.9 it will stop working. Use `from collections.abc import Iterable` instead. – Michele Sep 03 '20 at 17:44
  • @Michele quite so. This is out-of-date in several ways — `basestring` doesn't exist in Python 3 at all! I'll update it when I can but feel free to edit if you like. – senderle Sep 03 '20 at 18:29
7

It's not particularly elegant to include the code everywhere you need it. So write a function that does the massaging. Here's a suggestion I came up with for a similar previous question. It special-cases strings (which would usually be iterable) as single items, which is what I find I usually want.

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

Usage:

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

Update Here's a generator version that uses the new-ish (Python 3.3) yield from statement.

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
kindall
  • 178,883
  • 35
  • 278
  • 309
2

Perhaps better to use collections.Iterable to find out whether the output is an iterable or not.

import collections

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

for ii in x:
    #do stuff

This will work if type of x is either of these - list, tuple, dict, str, any class derived from these.

Pushpak Dagade
  • 6,280
  • 7
  • 28
  • 41
1

You'll want to do the following:

iterator = (x,) if not isinstance(x, (tuple, list)) else x

then

for i in iterator:
    #do stuff
TorelTwiddler
  • 5,996
  • 2
  • 32
  • 39
0

You might be able to get better performance if you use generators. This should work on python 3.3 and above.

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
Aaron de Windt
  • 16,794
  • 13
  • 47
  • 62
0

I like the approach with Itreable suggested by someone else. Though maybe in some cases the following would be better. It is more more EAFP (https://docs.python.org/3.5/glossary.html#term-eafp) way:

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>

This doesn't bothers with checking what we are dealing with. We just try to get an iterator and if it fails, we assume it failed because we were dealing with something that cannot be iterated over (well, it really seems like it cannot be). So we just make a tuple and make an iterator from that.

jary
  • 1,161
  • 6
  • 9
0

You could also try using the operator.isSequenceType function

import operator
x = unknown_function()
if not operator.isSequenceType(x) and not isinstance(x, basestring):
    x = (x,)
for item in x:
    do_something(item)
RajS
  • 1
0

You might define a function that ensures the returned value supports iteration (str, dict, tuple, etc --including user-defined sequence types that don't directly inherit from these classes) rather than checking if it is a tuple or list directly.

def ensure_iterable(x):
    return (x,) if not hasattr(x, '__iter__') else x

x = ensure_iterable(UnknownFunction())
for i in x:
    do_something(i)
multipleinterfaces
  • 8,913
  • 4
  • 30
  • 34