13

I have run into this several times. I'm dealing with a lot of methods that can accept a list of strings. Several times I have accidentally passed a single string and it gets broken apart into a list and each character is used, which isn't the desired behavior.

def test(a,b):
    x = []
    x.extend(a)
    x.extend(b)
    return x

x = [1,2,3,4]

What I don't want to happen:

test(x,'test')
[1, 2, 3, 4, 't', 'e', 's', 't']

I have to resort to a strange syntax:

test(x,['list']) 

I would like these to work implicitly:

test(x,'list')
[1, 2, 3, 4, 'test']

test(x,['one', 'two', 'three'])
[1, 2, 3, 4, 'one', 'two', 'three']

I really feel like there's a "pythonic" way to do this or something involving duck typing, but I don't see it. I know I could use isinstance() to check if it's a string, but I feel like there's a better way.

Edit: I'm using python 2.4.3

shadowland
  • 757
  • 1
  • 6
  • 20
  • It's probably not a good idea to re-interpret the expected behavior of `list.extend()` to overcome problems related to *accidental* input. – Austin Marshall Oct 20 '11 at 17:13

6 Answers6

13

Use this

def test( x, *args ):

Now you can do

test( x, 'one' )

and

test( x, 'one', 'two' )

and

test( x, *['one', 'two',] )
NullUserException
  • 83,810
  • 28
  • 209
  • 234
S.Lott
  • 384,516
  • 81
  • 508
  • 779
  • @NullUserExceptionఠ_ఠ: You're right. Unit testing is more important than this level of syntatic nonsense. However, I figured I'd go with the "no unit testing" approach, since that seemed to be implied by the question. – S.Lott Oct 20 '11 at 15:17
  • 1
    @NullUserException: Downvotes mean "this is wrong" or "this isn't helpful in the given context". They don't mean "I stylistically disagree". This answer is certainly helpful. – Sven Marnach Oct 20 '11 at 15:21
  • @SvenMarnach Yeah, I removed the downvote. But I am still not a fan of this approach. – NullUserException Oct 20 '11 at 15:22
  • +1 Using API design to remove ambiguity is a very reasonable and clean solution. – Raymond Hettinger Oct 20 '11 at 16:55
9

I'm sorry to say it but this really is one of the few cases where using isinstance() makes sense. However one of the other answers that suggests testing for list is doing it backwards: str (and for Python 2.x unicode) are the anomalous types so those are the ones you test for:

def test(a,b):
    x = []
    if isinstance(b, str): b = [b]
    x.extend(a)
    x.extend(b)
    return x

or for Python 2.x test isinstance(b, basestring).

Duncan
  • 92,073
  • 11
  • 122
  • 156
  • I have several instances where I've passed an argument where the list is a list of dictionary keys (as strings). Passing 'test' would try a lookup for mydict['t'], mydict['e'], etc. Maybe isinstance is best for the above example. It's certainly the easiest way to see the intent. – shadowland Oct 20 '11 at 15:32
  • 1
    Going further, the API should offer a default argument so that the caller can decide which iterable types are to be considered atomic. That avoids hardwiring the behavior. def test(a, b, atomic=(basestring, buffer)): ... – Raymond Hettinger Oct 20 '11 at 16:58
3

From The Zen of Python:

Explicit is better than implicit.

In my opinion, your first example is explicit. It takes two values and processes them in well-understood ways. Although it "feels" a little strange to new Python programmers, it behaves just like expect. list.extend accepts lists, so it treats strings like a list of chars.

Your proposal alters the semantics of list.extend to be equivalent to list.append, but only when dealing with strings. That would be a real surprise to someone who wants your function to treat your strings as lists of chars and who would then have to call it like test(x,list('test')).

You can do what you're asking, but please don't. :-)

Kirk Strauser
  • 30,189
  • 5
  • 49
  • 65
2

Hm, may this is easy way:

>>> def a(*args):
...     x=[]
...     for i in args:
...             if '__iter__' in dir(i):
...                     x+=list(i)
...             else:
...                     x.append(i)
...     return x
... 
>>> a(1,2,3,4)
[1, 2, 3, 4]
>>> a(1,2,[3,4])
[1, 2, 3, 4]
>>> a(1,2,[3,4],'123')
[1, 2, 3, 4, '123']
>>> a(1,2,[3,4],'123', 1231, (1, 2, 3, 4))
[1, 2, 3, 4, '123', 1231, 1, 2, 3, 4]
pod2metra
  • 256
  • 1
  • 6
1

A little tweak to pod2metra's answer enables the function to handle all levels of annidation:

>>> def b(*args):
...     x=[]
...     for i in args:
...             if '__iter__' in dir(i):
...                     x += b(*i)
...             else:
...                     x.append(i)
...     return x
... 
>>> b(1, (2, (3, 4)))
[1, 2, 3, 4]

Instead with the function 'a' you had:

>>> a(1, (2, (3, 4)))
[1, 2, (3, 4)]

I know it was not in the intentions of the original question, but maybe this generalization can be useful.

Apperò
  • 41
  • 7
0

Simple test the type of the parameter:

def test(a,b):
    x = a if isinstance(a, list) else [a]
    x += b if isinstance(b, list) else [b]
    return x
gecco
  • 17,969
  • 11
  • 51
  • 68