10

I want to have a function that wraps and object in an iterable one in order to allow the clients of the function treat the same way collections and single objects, i did the following:

 def to_iter(obj):
     try:
         iter(obj)
         return obj
     except TypeError:
         return [obj]

Is there a pythonic way to do this?, what if obj is a string and i want to treat strings as single objects?, should i use isinstance instead iter?

angvillar
  • 1,074
  • 3
  • 10
  • 25
  • "it's better to ask forgiveness than permission". Your code is okay unless you don't want to treat strings as iterables – JBernardo May 29 '13 at 01:48
  • so i have to use isinstace to check string objects? cause strings are iterables i can't have a list with a string in it with this code – angvillar May 29 '13 at 01:50
  • 2
    This is a bad idea. The API for your function should just iterables. Callers can wrap single objects in a tuple or list if they need to. What if they want to treat an iterable other than a string as a single object — something you might not even know about? – detly May 29 '13 at 01:54
  • @Twissell Yes, you may special case the iterable you want to treat as scalar. – JBernardo May 29 '13 at 01:55
  • @detly i feel is a good idea just cause the client dont have to check wheter the object is a collection or a single instance, so it can use the same interface for both. – angvillar May 29 '13 at 02:03
  • 1
    @Twissell - They *should* have to know, though. They should certainly know what their own code is producing. Your own API should be documented. (Do you really think it's a good idea for people to just call your function with random stuff until it stops throwing exceptions?) – detly May 29 '13 at 02:39
  • i see your point and feels right, but i am using this as a context for a template engine, so if i use a for loop in the template to render some object data i can render one or more objects without any additional change. – angvillar May 29 '13 at 02:46
  • @Twissell - You know your own situation best, the only point I'm trying to make is that the extra complexity introduces edge cases you may not have thought about, and you might not be able to solve them once other code depends on this behaviour. – detly May 29 '13 at 02:51

3 Answers3

9

Your approach is good: It would cast a string object to an iterable though

try:
    iter(obj)
except TypeError, te:
    obj = list(obj)

Another thing you can check for is:

if not hasattr(obj, "__iter__"): #returns True if type of iterable - same problem with strings
    obj = list(obj)
return obj

To check for string types:

import types
if not isinstance(obj, types.StringTypes) and hasattr(obj, "__iter__"):
    obj = list(obj)
return obj
karthikr
  • 97,368
  • 26
  • 197
  • 188
  • hassattr and isinstance have the same behavior?, i can see that one raises and exception and the other is a boolean right? – angvillar May 29 '13 at 01:57
  • both return a bool, and both are a fail-silently lookup. – karthikr May 29 '13 at 01:58
  • 5
    `hasattr(obj, "__iter__")` **does not mean** iterable (you may add any stuff as this attribute). Also, if `iter` fails, `list` will also fail... And don't use the deprecated except syntax... – JBernardo May 29 '13 at 01:59
  • i know, but in this case, the sample code of karthikr is using hasattr in a boolean expresion instead using exceptions – angvillar May 29 '13 at 02:00
  • is there any reason your storing the exception? and not using `as`? – Ryan Haining May 29 '13 at 02:00
  • nope.. no particular reason. Just a coding style, since it was not being used anyways @RyanHaining – karthikr May 29 '13 at 02:01
  • @karthikr for your last code sample i just remove "not" for isinstance and added "or not" instead "and" for hasattr, now not iterable objects and strings are wrapped into a list – angvillar May 29 '13 at 02:20
  • ok.. so dont make the changes you made, and it will work fine – karthikr May 29 '13 at 02:21
3

Here's a general solution with some doctests to demonstrate:

def is_sequence(arg):
    """
SYNOPSIS
    Test if input is iterable but not a string.

DESCRIPTION
    Type checker. True for any object that behaves like a list, tuple or dict
    in terms of ability to iterate over its elements; but excludes strings.

    See http://stackoverflow.com/questions/1835018

PARAMETERS
    arg         Object to test.

RETURNS
    True or False

EXAMPLES
    ## string
    >>> is_sequence('string')
    False

    ## list
    >>> is_sequence([1, 2, 3,])
    True

    ## set
    >>> is_sequence(set([1, 2, 3,]))
    True

    ## tuple
    >>> is_sequence((1, 2, 3,))
    True

    ## dict
    >>> is_sequence(dict(a=1, b=2))
    True

    ## int
    >>> is_sequence(123)
    False

LIMITATIONS
    TBD
    """
    return (not hasattr(arg, "strip") and
            hasattr(arg, "__iteritems__") or
            hasattr(arg, "__iter__"))


def listify(arg):
    """
SYNOPSIS
    Wraps scalar objects in a list; passes through lists without alteration.

DESCRIPTION
    Normalizes input to always be a list or tuple.  If already iterable and
    not a string, pass through.  If a scalar, make into a one-element list.

    If a scalar is wrapped, the same scalar (not a copy) is preserved.

PARAMETERS
    arg         Object to listify.

RETURNS
    list

EXAMPLES
    >>> listify(1)
    [1]

    >>> listify('string')
    ['string']

    >>> listify(1, 2)
    Traceback (most recent call last):
        ...
    TypeError: listify() takes exactly 1 argument (2 given)

    >>> listify([3, 4,])
    [3, 4]

    ## scalar is preserved, not copied
    >>> x = 555
    >>> y = listify(x)
    >>> y[0] is x
    True

    ## dict is not considered a sequence for this function
    >>> d = dict(a=1,b=2)
    >>> listify(d)
    [{'a': 1, 'b': 2}]

    >>> listify(None)
    [None]

LIMITATIONS
    TBD
    """
    if is_sequence(arg) and not isinstance(arg, dict):
        return arg
    return [arg,]
Chris Johnson
  • 20,650
  • 6
  • 81
  • 80
  • Perfect, but pretty much useless without is_sequence being defined. I specifically needed to exclude strings, so my is_sequence looks like this: ```def is_sequence(arg): if isinstance(arg, str): return False if hasattr(arg, "__iter__"): return True``` – Will Sep 08 '17 at 16:38
  • 1
    You will want to use `basestr` instead of `str` if your code could be run in Python 2.x. `str` doesn't include Unicode. – Chris Johnson Sep 08 '17 at 16:45
1

My portable pythonic hammer:

def isstr(o):
  try:
    basestring
  except NameError:
    basestring = (str, bytes)
  return isinstance(o, basestring)

def toiter(o):
  if not isstr(o):
    try:
      return iter(o)
    except TypeError:
      pass
  return iter([o])

def tolist(o):
  if not isstr(o):
    try:
      return list(o)
    except TypeError:
      pass
  return [o]
Alexander Shukaev
  • 16,674
  • 8
  • 70
  • 85