views:

300

answers:

7

(Title and contents updated after reading Alex's answer)

In general I believe that it's considered bad form (un-Pythonic) for a function to sometimes return an iterable and sometimes a single item depending on its parameters.

For example struct.unpack always returns a tuple even if it contains only one item.

I'm trying to finalise the API for a module and I have a few functions that can take one or more parameters (via *args) like this:

a = s.read(10)        # reads 10 bits and returns a single item
b, c = s.read(5, 5)   # reads 5 bits twice and returns a list of two items.

So it returns a single item if there's only one parameter, otherwise it returns a list. Now I think this is fine and not at all confusing, but I suspect that others may disagree.

The most common use-case for these functions would be to only want a single item returned, so always returning a list (or tuple) feels wrong:

a, = s.read(10)       # Prone to bugs when people forget to unpack the object
a = s.read(10)[0]     # Ugly and it's not clear only one item is being returned

Another option is to have two functions:

a = s.read(10)
b, c = s.read_list(5, 5)

which is OK, but it clutters up the API and requires the user to remember twice as many functions without adding any value.

So my question is: Is sometimes returning an iterable and sometimes a single item confusing and un-Pythonic? If so what's the best option?


Update: I think the general consensus is that it's very naughty to only return an iterable sometimes. I think that the best option for most cases would be to always return the iterable, even if it contained only one item.

Having said that, for my particular case I think I'll go for the splitting into two functions (read(item) / readlist(*items)), the reasoning being that I think the single item case will happen much more often than the multiple item case, so it makes it easier to use and the API change less problematic for users.

Thanks everyone.

A: 

In python lists are objects :) So no type mismatch

AlberT
True enough! I've edited the question to avoid confusion.
Scott Griffiths
+11  A: 

If you are going to be returning iterators sometimes, and single objects on others, I'd say return always an iterator, so you don't have to think about it.

Generaly, you would use that function in a context that expects an iterator, so if you'd have to check if it where a list to iterate or an object to do just one time the work, then its easier to just return an iterator and iterate always, even if its one time.

If you need to do something different if you are returned one element, just use if len(var):.

Remember, consistency is a valuable good.

I lean towards returning a consistent object, not necesarily the same type, but if I return ever an iterable, I return always an iterable.

voyager
+1. Sometimes being a thing and sometimes being a list-of-thing is usually a mistake. Python did that for %-formatting and this is widely considered a mistake and nasty trap.
bobince
I was afraid people would say this - it just feels ugly to get a list back when you've clearly only asked for a single item!
Scott Griffiths
@Scot Griffiths: IMHO the potential bugs caused by being too clever outwight the problems a simple variable could cause. Why dont use a method like ´def read(a_tuple):´ instead of using ´*args´?
voyager
You're probably right overall, but using `def read(a_tuple)` for a single value you'd have to call `a, = read((10,))` which isn't an improvement. Or am I missing something?
Scott Griffiths
I agree that using a tuple or list would be much better. I don't often find myself passing hard-coded numbers into api's and lists have great functionality in python.
Adam
@Scott: It only feels ugly to get a list back when you've asked for one item because you've given the method the wrong name...If you're reading bits, why not have `read_bits` plus `read_one` and `read_many` based on `read_bits`?
wbg
+1  A: 

The only situation where I would do this is with a parameterized function or method, where one or more of the parameters the caller gives determines the type returned; for example, a "factory" function that returns one of a logically similar family of objects:

newCharacter = characterFactory("human", "male", "warrior")

In the general case, where the caller doesn't get to specify, I'd avoid the "box of chocolates" behavior. :)

Kevin Little
In my particular case the number of items returned equals the number of items given in the function call, so I don't think that the user will be surprised by what gets returned.
Scott Griffiths
+2  A: 

In general, I would have to say that returning two different types is bad practice.

Imagine the next developer coming to read and maintain your code. At first he/she will read a method using your function and think "Ah, read() returns a single item."

Later they will see code treating read()'s result as a list. At best this will simply confuse them and force them to examine read()'s usage. At worst they might think there is a bug in the implementation using read() and attempt to fix it.

Finally, once they understand read() returns two possible types they will have to ask themselves "is there possibly a third return type I need to be ready for?"

This reminds me of the saying: "Code as if the next guy to maintain your code is a homicidal maniac who knows where you live."

Michael Groner
+1  A: 

It may not be a matter of "pythonic" but rather a matter of "good design". If you returnd different things AND nobody has to do typechecks on them, then it's probably okay. That's polymorphism for you. OTOH, if the caller has to "pierce the veil" then you have a design problem, known as a violation of the Liskov Substitution Principle. Pythonic or not, it is clearly not an OO design, which means it will be prone to bugs and programming inconveniences.

tottinge
+1  A: 

I would have read(integer) and read_list(iterable).

That way you could do read(10) and get back a single result and read_list([5, 5, 10, 5]) and get back a list of results. This is both more flexible and explicit.

Jason Christa
+2  A: 

Returning either a single object, or an iterable of objects, depending on arguments, is definitely hard to deal with. But the question in your title is much more general and the assertion that the standard library's function avoid (or "mostly avoid") returning different types based on the argument(s) is quite incorrect. There are many counter-examples.

Functions copy.copy and copy.deepcopy return the same type as their argument, so of course they're "returning different types depending on" the argument. "Return same type as the input" is actually VERY common -- you could class here, also, the "fetch an object back from a container where it was put", though normally that's done with a method rather than a function;-). But also, in the same vein, consider itertools.repeat (once you iterate on its returned iterator), or, say, filter...:

>>> filter(lambda x: x>'f', 'zaplepidop')
'zplpiop'
>>> filter(lambda x: x>'f', list('zaplepidop'))
['z', 'p', 'l', 'p', 'i', 'o', 'p']

filtering a string returns a string, filtering a list returns a list.

But wait, there's more!-) Functions pickle.loads and its friends (e.g. in module marshal &c) return objects of types entirely dependent on the value you're passing as an argument. So does built-in function eval (and similarly input, in Python 2.*). This is the second common pattern: construct or reconstruct an object as dictated by the value of the argument(s), of a wide (or even ubbounded) variety of possible types, and return it.

I know no good example of the specific anti-pattern you've observed (and I do believe it's an anti-pattern, mildly -- not for any high-falutin' reason, just because it's pesky and inconvenient to deal with;-). Note that these cases I have exemplified ARE handy and convenient -- that's the real design discriminant in most standard library issue!-)

Alex Martelli
You're right that the question is phrased too generally, and it really comes down to just an interable vs. non-interable problem. I guess if you've called it an anti-pattern then that's the death knell for it!-)
Scott Griffiths