Reputation: 29452
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?
Upvotes: 21
Views: 32463
Reputation: 63709
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
Upvotes: 10
Reputation: 18816
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:
list
against anything else
isinstance(input, list)
(so your example is correct)basestring
in Python 2.x, str
in Python 3.x)
isinstance(myvar, collections.Sequence) and not isinstance(myvar, str)
int
, str
, MyClass
isinstance(input, (int, str, MyClass))
.
try:
input = iter(input) if not isinstance(input, str) else [input]
except TypeError:
input = [input]
Upvotes: 4
Reputation: 46773
That is an ok way to do it (don't forget to include tuples).
However, you may also want to consider if the argument has a __iter__ method or __getitem__ method. (note that strings have __getitem__ instead of __iter__.)
hasattr(arg, '__iter__') or hasattr(arg, '__getitem__')
This is probably the most general requirement for a list-like type than only checking the type.
Upvotes: 0
Reputation: 881575
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!-)
Upvotes: 15
Reputation: 945
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.
Upvotes: 2
Reputation: 4842
Your aproach seems right to me.
It's similar to how you use atom?
in Lisp when you iterate over lists and check the current item to see if it is a list or not, because if it is a list you want to process its items, too.
So, yeah, don't see anything wrong with that.
Upvotes: 0
Reputation: 91535
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.
Upvotes: 1
Reputation: 132197
This seems like a reasonable way to do it. You're wanting to test if the element is a list, and this accomplishes that directly. It gets more complicated if you want to support other 'list-like' data types, too, for example:
isinstance(input, (list, tuple))
or more generally, abstract away the question:
def iterable(obj):
try:
len(obj)
return True
except TypeError:
return False
but again, in summary, your method is simple and correct, which sounds good to me!
Upvotes: -1