Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Concise way to create single member list in Python?

Tags:

python

list

Suppose I have some object x that may be a single instance of a data type (say float) or it may be a list of float types.

Is there any what I can ensure that x is wrapped as a list, perhaps being a singleton list if necessary, without checking its type or doing anything like that.

I'd like for something like list(x) to just always work, whether x was a singleton or not, but this doesn't work since a singleton isn't iterable.

Edit: see my answer below for some elaboration.

At the same time, I don't want to define my own function to construct lists from singeltons, and I don't want to do anything inline like this either:

from collections import Iterable
y = [x] if not isinstance(x, Iterable) else list(x)

If nothing more concise than this exists, that's OK. I am looking for some clean way of doing this that's already built into the language.

I'm sure some may think the isinstance approach is clean and good, but I'm specifically looking for something more concise that doesn't require me to write anything new before hand. I just can't find anything when searching doc pages and I'm not sure how to ask this question to a search engine.

like image 949
ely Avatar asked Aug 09 '12 22:08

ely


2 Answers

Here's an answer the question you didn't ask, but perhaps should have. I can only guess though because you failed to give the relevant context.

How can I cope with an argument passed to me that violates my interface contract?

You shouldn't try to. If I call sum(2) I properly get

TypeError: 'int' object is not iterable

because sum was expecting an iterable and I didn't give it one. If you are concerned that someone will call both

my_function(2.2)
my_function([2.2, 4.4])

The proper response is to raise a TypeError, not try to patch things up for the caller because as soon as you fix the first case, someone will then call

my_function([2.2, 'loretta'])

Do you cover that case also? What if they hand you a self-referential list? If you come from a strongly-typed language background, there is strong temptation to abuse Python so that it acts like Java. This only yields bad Python code: you've piled a hack on top of a defect.

And "type safety" is a bit of a canard too or else double sqrt(float) would not have to return domain errors, but it does. Should sqrt take the absolute value of its argument? Masking errors in call signatures only ensures that the defects go undiscovered, and then break later when you get an invalid argument that you didn't anticipate.

like image 94
msw Avatar answered Sep 21 '22 10:09

msw


In revisiting this question after much more experience with Python, one solution occurred to me, but it violates my then-required property of not defining a function.

def make_list_containing(*args):
    return list(args)

Then make_list_containing(3) or make_list_containing('foo') behave as one would expect (as opposed to the list constructor, which you have to alter your intuitions to learn how to expect it to act).

It still is surprising to me that list as a constructor is synonymous with "convert to list type" rather than "place into a list object instance".

I can see how the polymorphic behavior of the constructor could be ambiguous in the case of singletons that are also iterables, like strings. But still, why not support the "place into list" behavior for non-iterable singletons?

Another thing is that, at least to me, the approach I am describing seems more consistent and more adherent to least astonishment.

With make_list_containing you know you are always getting a list back, and that it will contain the elements that were passed in. As long as you pass them in with the format you desire, then that's exactly what you'll get out. Consider:

In [34]: make_list_containing(*'foo')
Out[34]: ['f', 'o', 'o']

In [35]: make_list_containing('foo')
Out[35]: ['foo']

or

In [40]: make_list_containing(*enumerate(range(3)))
Out[40]: [(0, 0), (1, 1), (2, 2)]

In [41]: make_list_containing(enumerate(range(3)))
Out[41]: [<enumerate at 0x7f61a7f0fd70>]

Now that seems much more intuitive to me. Preprending the * is exactly how I am supposed to "de-iterate" the items within something as arguments. If I don't go out of my way to choose that, then I must be treating 'foo' as a singleton and plan to handle its elements or iterability myself interior to the function call.

One thing that is frustrating within this conversation is that just because this convention of Python is old people seem to conflate that with meaning it is not surprising. But that's a narrow definition of surprise. Yes, when I fire up the interpreter, I am no longer "surprised" when list('foo') give me ['f', 'o', 'o']. But I still am surprised that that is the interface choice of the language for a constructor as important as list. It surprises me because it seems to have more design weaknesses than strengths, and that what was used to justify it originally was some fixation of slickness of syntax, rather than clarity of thought.

At this point it strays too far into the opinion zone, and I'm obviously OK with the fact that, due to overwhelming historical momentum, this will not be changed in Python. But the spirit of my question is still about this very legitimate point. It's not a matter of "laziness" or "just write a wrapper function and move on" as most of the comments seem to want it to be.

like image 23
ely Avatar answered Sep 20 '22 10:09

ely