1

Lacking experience with maintaining dynamic-typed code, I'm looking for the best way to handle this kind of situations :

(Example in python, but could work with any dynamic-typed language)

def some_function(object_that_could_be_a_list):
     if isinstance(object_that_could_be_a_list, list):
          for element in object_that_could_be_a_list:
              some_function(element)
     else:
          # Do stuff that expects the object to have certain properties 
          # a list would not have

I'm quite uneasy with this, since I think a method should do only one thing, and I'm thinking that it is not as readable as it should be. So, I'd be tempted to make three functions : the first that'll take any object and "sort" between the two others, one for the lists, another for the "simple" objects. Then again, that'd add some complexity.

What is the most "sustainable" solution here, and the one that guarantee ease of maintenance ? Is there an idiom in python for those situations that I'm unaware of ? Thanks in advance.

Raveline
  • 2,660
  • 1
  • 24
  • 28

4 Answers4

6

Don't type check - do what you want to do, and if it won't work, it'll throw an exception which you can catch and manage.

The python mantra is 'ask for forgiveness, not permission'. Type checking takes extra time, when most of the time, it'll be pointless. It also doesn't make much sense in a duck-typed environment - if it works, who cares why type it is? Why limit yourself to lists when other iterables will work too?

E.g:

def some_function(object_that_could_be_a_list):
    try:
        for element in object_that_could_be_a_list:
            some_function(element)
    except TypeError:
        ...

This is more readable, will work in more cases (if I pass in any other iterable which isn't a list, there are a lot) and will often be faster.

Note you are getting terminology mixed up. Python is dynamically typed, but not weakly typed. Weak typing means objects change type as needed. For example, if you add a string and an int, it will convert the string to an int to do the addition. Python does not do this. Dynamic typing means you don't declare a type for a variable, and it may contain a string at some point, then an int later.

Duck typing is a term used to describe the use of an object without caring about it's type. If it walks like a duck, and quacks like a duck - it's probably a duck.

Now, this is a general thing, and if you think your code will get the 'wrong' type of object more often than the 'right', then you might want to type check for speed. Note that this is rare, and it's always best to avoid premature optimisation. Do it by catching exceptions, and then test - if you find it's a bottleneck, then optimise.

Gareth Latty
  • 86,389
  • 17
  • 178
  • 183
  • Also, using isinstance is fine, when you know before that you may get a list, for instance... – cfedermann Apr 14 '12 at 17:52
  • 2
    A common problem with that approach in the face of iteration is that strings are iterable, so you might need an explicit type check to catch strings. – Fred Foo Apr 14 '12 at 17:53
  • Type checking doesn't take very much time compared to exception handling. Also, you're catching `TypeError` but an incompatible type might just as well raise an `AttributeError`. – Fred Foo Apr 14 '12 at 17:56
  • Isn't the try/except also expensive in time ? – Raveline Apr 14 '12 at 17:56
  • Yes, it is, but the reality is that there is no point optimising until you know you need to, and in most cases, you will be doing a lot more iterations where you don't throw exceptions than you do. – Gareth Latty Apr 14 '12 at 18:03
  • @larsmans See [this question](http://stackoverflow.com/questions/9168904/suppressing-treatment-of-string-as-iterable/9168967#9168967) for more on that point. Generally, you should put the responsibility of that on the caller, rather than the function. As to the wrong exception, catch all the ones you need to in the given context. – Gareth Latty Apr 14 '12 at 18:08
  • (I knew about the distinction between weak and dynamically, I just got confused !) The method would be called quite frequently, which is why I'm reluctant to use exceptions. But I'm might be a bit too influenced by C++ / Java style of leaving exception to... exceptional cases. I guess I'm still uneasy with duck typing, at least for lists opposed to non-iterable objects. – Raveline Apr 14 '12 at 18:23
  • 2
    This is the thing - a lot of people struggle with using exceptions for flow control, as it's beaten into people with other languages that it's wrong. It actually makes a lot of sense, it can stop race conditions, it can give better performance (weirdly) in a lot of cases, and it reads a lot better. Give it a try, and if it does slow you down, optimise it out later, but I can say it probably won't be a problem 99% of the time. – Gareth Latty Apr 14 '12 at 18:26
2

A common practice is to implement the multiple interface by way of using different parameters for different kinds of input.

def foo(thing=None, thing_seq=None):
    if thing_seq is not None:
        for _thing in thing_seq:
            foo(thing=_thing)
    if thing is not None:
        print "did foo with", thing
SingleNegationElimination
  • 151,563
  • 33
  • 264
  • 304
  • Sure, I understand the idea, but what's the advantage compared to the isinstance ? Ok, it's probably a bit quicker. Not sure it's much more readable though. Though I know this practice and would not have thought about using it there. This is clever. – Raveline Apr 14 '12 at 18:20
  • 3
    the advantage is that there's no guesswork, the intent is unambigiously clear what both the caller meant, and how the callee should act. `import this` – SingleNegationElimination Apr 14 '12 at 19:27
2

Rather than doing it recursive I tend do it this way:

def foo(x):
    if not isinstance(x, list):
        x = [x]
    for y in x:
        do_something(y)
jrydberg
  • 579
  • 5
  • 14
1

You can use decorators in this case to make it more maintainable:

from mm import multimethod

@multimethod(int, int)
def foo(a, b):
    ...code for two ints...

@multimethod(float, float):
def foo(a, b):
    ...code for two floats...

@multimethod(str, str):
def foo(a, b):
    ...code for two strings...
Charles Menguy
  • 40,830
  • 17
  • 95
  • 117
  • 1
    It's also worth noting multimethod isn't in the standard library (IIRC there is a PEP currently being considered that would make @overload do a similar thing, however). – Gareth Latty Apr 14 '12 at 18:05
  • I didn't know about multimethod. I think I'd prefer something that is in the standard library, though. – Raveline Apr 14 '12 at 18:21