2

What is a way to avoid code repetition to loop if argument is a sequence (list, tuple) or to skip the loop but execute the action just once?

def foo(arg1,sequence=None):
    # If possible, loop, else do it once
        if isinstance(sequence,(list,tuple)):
            for value in sequence:
                do_something(arg1)
        else:
            do_something(arg1)

The actions I do in the for loop are way longer than this, and I was wondering what approach you normally use to avoid this, if you do avoid it. I often come across this issue and I haven't come up with something to 'solve' it.

EDIT: Question is not a duplicate of In Python, how do I determine if an object is iterable?, as suggested. I do not want to introduce a different condition. I want to avoid the repetition.

cap
  • 365
  • 2
  • 14

4 Answers4

6

Standardise on the loop being the default case and transform your single case into an iterable:

if not isinstance(sequence, (list, tuple)):
    sequence = [sequence]

for value in sequence:
    do_something(value)
deceze
  • 510,633
  • 85
  • 743
  • 889
3

The only other reasonable way I can think of is to convert everything to a sequence and then unconditionally iterate. This is relatively common to see in Python code, and it’s often extracted into a helper function.

if not isinstance(sequence, (list, tuple)):
    sequence = [sequence]
for value in sequence:
    ...
Dietrich Epp
  • 205,541
  • 37
  • 345
  • 415
3

From a software design perspective, it might be interesting to separate this in two functions:

  1. a function that makes something an iterable in case it is not a list or tuple (or something else), for example by wrapping it into a list or tuple; and
  2. a function that iterates over the items after we make it an iterable, and does the processing, like:
def make_iterable(iterable):
    if not isinstance(iteable, (list,tuple)):
        return (iterable, )
    return iterable

def foo(arg1 ,sequence=None):
    for item in make_iterable(sequence):
        do_something(arg1, item)

So now we can easily make more things iterable. We might for example return an empty tuple for a None, such that None is allowed as a way to avoid any iteration:

def make_iterable(iterable):
    if iterable is None:
        return ()
    if not isinstance(iteable, (list,tuple)):
        return (iterable, )
    return iterable

It is also easy to reuse the above method in all kinds of methods that allow to enter both a single object and an iterable.

Note however that there can be some problems with the above. Some types are iterable, but are not per se items you want to iterate over: for example a string is an iterable (you can iterate over its characters). In that case the In Python, how do I determine if an object is iterable? will succeed, but it might not be a good idea to "unpack" the string. But you might not want to iterate over the string when you call it as foo(4, 'foobar'), but see it as a single element.

Willem Van Onsem
  • 443,496
  • 30
  • 428
  • 555
1

A common python way is to "ask for forgiveness instead of permission". That is, try to treat it as an iterable, and if that fails, assume that it's not

try:
    for value in sequence:
         do_something(arg1)
except TypeError:
    do_something(arg1)

I know that seems counter-intuitive when you're coming from another language, but that's a common way to do it in python.

blue_note
  • 27,712
  • 9
  • 72
  • 90
  • If it leads to code repetition (and OP says there's more code there), then no, that's not very pythonic either. – deceze Sep 28 '18 at 12:38
  • @deceze: I've seen the pattern in standard python libraries, so it should be pythonic enough. If there's more code, the OP could make a function out of it (which should be done anyway, regardless of which approach he chooses) – blue_note Sep 28 '18 at 12:45