Caustic
Caustic

Reputation: 969

length of Python list when list has a single value

Okay I concede that I didn't ask the question very well. I will update my question to be more precise.

I am writing a function that takes a list as an argument. I want to check the length of the list so I can loop through the list.

The problem that I have is when the list has only one entry, len(myList) returns the length of that entry (the length of the string) and not the length of the list which should be == 1.

I can fix this if I force the argument to be parsed as a single value list ['val']. But I would prefer my API to allow the user to parse either a value or a list of values.

example:

    def myMethod(self,dataHandle, data,**kwargs):

        comment = kwargs.get('comment','')

        _dataHandle= list()
        _data = list()

        _dataHandle.append(dataHandle)
        _data.append(data)

        for i in range(_dataHandle):
            # do stuff.

I would like to be able to call my method either by

myMethod('ed', ed.spectra,comment='down welling irradiance')

or by

myMethod(['ed','lu'] , [ed.spectra,lu.spectra] , comments = ['downwelling', upwelling radiance'])

Any help would be greatly appreciated. Might not seem like a big deal to parse ['ed'], but it breaks the consistency of my API so far.

Upvotes: 1

Views: 21470

Answers (5)

odorousbeast
odorousbeast

Reputation: 1

I run into this same problem frequently. Building a list from an empty list, as you are doing with the "_dataHandle= list()" line, is common in Python because we don't reserve memory in advance. Therefore, it is often the case that the state of the list will transition from empty, to one element, to multiple elements. As you found, Python treats the indexing different for one element vs. multiple elements. If you can use list comprehension, then the solution can be simple. Instead of:

for i in range(_dataHandle):

use:

for myvar in _dataHandle:

In this case, if there is only one element, the loop only iterates once as you would expect.

Upvotes: 0

juliomalegria
juliomalegria

Reputation: 24921

You can ask if your variable is a list:

def my_method(my_var):
    if isinstance(my_var, list):
        for my_elem in my_var:
            # do stuff with my_elem
    else:  # my_var is not iterable
        # do stuff with my_var

EDIT: Another option is to try iterating over it, and if it fails (raises and exception) you assume is a single element:

def my_method(my_var):
    try:
        for my_elem in my_var:
            # do stuff with my_elem
    except TypeError:  # my_var is not iterable
        # do_stuff with my_var

The good thing about this second options is that it will work not only for lists, as the first one, but with anything that is iterable (strings, sets, dicts, etc.)

Upvotes: 3

Ben
Ben

Reputation: 71450

list('ed') does not create a list containing a single element, 'ed'. list(x) in general does not create a list containing a single element, x. In fact, if you had been using numbers rather than strings (or anything else non-iterable), this would have been blindingly obvious to you:

>>> list('ed')
['e', 'd']
>>> list(3)
Traceback (most recent call last):
  File "<pyshell#0>", line 1, in <module>
    list(3)
TypeError: 'int' object is not iterable
>>

So you are in fact passing a list with multiple elements to your method, which is why len is returning greater than 1. It's not returning the length of the first element of the list.

For your method to allow passing either a single item or a list, you'd have to do some checking to see if it's a single item first, and if it is create a list containing it with myVar = [myVar], then run your loop.

However this sort of API is tricky to implement and use, and I would not recommend it. The most natural way to check if you've been given a collection or an item is see if myVar is iterable. However this fails for strings, which are iterable. Strings unfortunately straddle the boundry between a collection and an individual data item; we very very often use them as data items containing a "chunk of text", but they are also collections of characters, and Python allows them to be used as such.

Such an API also is likely to cause you to one day accidentally pass a list that you're thinking of as a single thing and expecting the method to treat it as a single thing. But it's a list, so suddenly the code will behave differently.

It also raises questions about what you do with other data types. A dictionary is not a list, but it can be iterated. If you pass a dictionary as myVar, will it be treated as a list containing a single dictionary, or will it iterate over the keys of the dictionary? How about a tuple? What about a custom class implementing __iter__? What if the custom class implementing __iter__ is trying to be "string-like" rather than "list-like"?

All these questions lead to surprises if the caller guesses/remembers wrongly. Surprises when programming lead to bugs. IMHO, it's better to just live with the extra two characters of typing ([ and ]), and have your API be clean and simple.

Upvotes: 0

Benson
Benson

Reputation: 22847

The proper python syntax for a list consisting of a single item is [ 'ed' ].

What you're doing with list('ed') is asking python to convert 'ed' to a list. This is a consistent metaphor in python: when you want to convert something to a string, you say str(some_thing). Any hack you'd use to make list('ed') return a list with just the string 'ed' would break python's internal metaphors.

When python sees list(x), it will try to convert x to a list. If x is iterable, it does something more or less equivalent to this:

def make_list(x):
  ret_val = []
  for item in x:
    ret_val.append(item)
  return ret_val

Because your string 'ed' is iterable, python will convert it to a list of length two: [ 'e', 'd' ].

The cleanest idiomatic python in this case might be to have your function accept a variable number of arguments, so instead of this

def my_func(itemList):
  ...

you'd do this

def my_func(*items):
  ...

And instead of calling it like this

my_func(['ed','lu','lsky'])

You'd call it like this:

my_func('ed', 'lu', 'lsky')

In this way you can accept any number of arguments, and your API will be nice and clean.

Upvotes: 5

Chris Eberle
Chris Eberle

Reputation: 48775

You do actually need to put your string in a list if you want it to be treated like a list

EDIT

I see that at some point there was a list in front of the string. list, contrary to what you may think, doesn't create a list of one item. It calls __iter__ on the string object and iterates over each item. Thus it makes a list of characters.

Hopefully this makes it clearer:

>>> print(list('abc'))
['a', 'b', 'c']
>>> print(list(('abc',)))
['abc']

Upvotes: 1

Related Questions