pythonic way to convert variable to list

28,307

Solution 1

I like Andrei Vajna's suggestion of hasattr(var,'__iter__'). Note these results from some typical Python types:

>>> hasattr("abc","__iter__")
False
>>> hasattr((0,),"__iter__")
True
>>> hasattr({},"__iter__")
True
>>> hasattr(set(),"__iter__")
True

This has the added advantage of treating a string as a non-iterable - strings are a grey area, as sometimes you want to treat them as an element, other times as a sequence of characters.

Note that in Python 3 the str type does have the __iter__ attribute and this does not work:

>>> hasattr("abc", "__iter__")
True

Solution 2

Typically, strings (plain and unicode) are the only iterables that you want to nevertheless consider as "single elements" -- the basestring builtin exists SPECIFICALLY to let you test for either kind of strings with isinstance, so it's very UN-grotty for that special case;-).

So my suggested approach for the most general case is:

  if isinstance(input, basestring): input = [input]
  else:
    try: iter(input)
    except TypeError: input = [input]
    else: input = list(input)

This is THE way to treat EVERY iterable EXCEPT strings as a list directly, strings and numbers and other non-iterables as scalars (to be normalized into single-item lists).

I'm explicitly making a list out of every kind of iterable so you KNOW you can further on perform EVERY kind of list trick - sorting, iterating more than once, adding or removing items to facilitate iteration, etc, all without altering the ACTUAL input list (if list indeed it was;-). If all you need is a single plain for loop then that last step is unnecessary (and indeed unhelpful if e.g. input is a huge open file) and I'd suggest an auxiliary generator instead:

def justLoopOn(input):
  if isinstance(input, basestring):
    yield input
  else:
    try:
      for item in input:
        yield item
    except TypeError:
      yield input

now in every single one of your functions needing such argument normalization, you just use:

 for item in justLoopOn(input):

You can use an auxiliary normalizing-function even in the other case (where you need a real list for further nefarious purposes); actually, in such (rarer) cases, you can just do:

 thelistforme = list(justLoopOn(input))

so that the (inevitably) somewhat-hairy normalization logic is just in ONE place, just as it should be!-)

Solution 3

First, there is no general method that could tell a "single element" from "list of elements" since by definition list can be an element of another list.

I would say you need to define what kinds of data you might have, so that you might have:

  • any descendant of list against anything else
    • Test with isinstance(input, list) (so your example is correct)
  • any sequence type except strings (basestring in Python 2.x, str in Python 3.x)
    • Use sequence metaclass: isinstance(myvar, collections.Sequence) and not isinstance(myvar, str)
  • some sequence type against known cases, like int, str, MyClass
    • Test with isinstance(input, (int, str, MyClass))
  • any iterable except strings:
    • Test with

.

    try: 
        input = iter(input) if not isinstance(input, str) else [input]
    except TypeError:
        input = [input]

Solution 4

You can put * before your argument, this way you'll always get a tuple:

def a(*p):
  print type(p)
  print p

a(4)
>>> <type 'tuple'>
>>> (4,)

a(4, 5)
>>> <type 'tuple'>
>>> (4,5,)

But that will force you to call your function with variable parameters, I don't know if that 's acceptable for you.

Solution 5

You can do direct type comparisons using type().

def my_func(input):
    if not type(input) is list:
        input = [input]
    for e in input:
        # do something

However, the way you have it will allow any type derived from the list type to be passed through. Thus preventing the any derived types from accidentally being wrapped.

Share:
28,307
hoju
Author by

hoju

nothing to see here, move along now

Updated on February 27, 2020

Comments

  • hoju
    hoju over 4 years

    I have a function whose input argument can either be an element or a list of elements. If this argument is a single element then I put it in a list so I can iterate over the input in a consistent manner.

    Currently I have this:

    def my_func(input):
        if not isinstance(input, list): input = [input]
        for e in input:
            ...
    

    I am working with an existing API so I can't change the input parameters. Using isinstance() feels hacky, so is there a proper way to do this?

  • Andrei Vajna II
    Andrei Vajna II almost 15 years
    I think you can use something like hasattr(a, '__iter__') to see if it's a 'list-like' data type.
  • Peter
    Peter almost 15 years
    this kind of direct comparison isn't a great idea - if a user subclasses list, for example, your type() based comparison will break immediately. isinstance() is closer to what the OP wants.
  • Unknown
    Unknown almost 15 years
    This doesn't work for him because he said he can't change the arguments and someone could enter [1,2] and it would be doubly wrapped as ([1,2],)
  • attwad
    attwad almost 15 years
    Ho yeah sorry I didn't saw the part where he said he was working on an api. my fault.
  • Kenan Banks
    Kenan Banks almost 15 years
    -1. Iterables do not necessarily have a len, nor are they necessarily finite.
  • Cristian Ciupitu
    Cristian Ciupitu almost 15 years
    Calling iter(input) and ignoring its result isn't a waste of resources? Wouldn't be better if you would only check the existence of the __iter__ attribute?
  • Nandhini
    Nandhini almost 15 years
    @Cristian, i don't think that is expensive, may be one function call which will be doing almost same which you suggest and also it doesn't rely on checking magic attribute __iter__
  • Unknown
    Unknown almost 15 years
    Don't you mean my suggestion?
  • Sami Ahmed Siddiqui
    Sami Ahmed Siddiqui almost 15 years
    Exactly, I was giving alternatives, but my "however" remark tells him that his way is still better.
  • Andrei Vajna II
    Andrei Vajna II almost 15 years
    My comment on Peter's answer was first. Ha! :P
  • Dana the Sane
    Dana the Sane almost 14 years
    This is so useful that I wish it was part of the standard library.