3

Is there a better way in Python to determine whether a function argument is a single number or a list of numbers. Currently I'm using the side effect of an exception to channel control flow, but it doesn't seem elegant:

def buildPattern( NumberOrList, Framework ):
    FinalPattern = '-->'
    try:
        ListLen = len(NumberOrList)
        #if this is a list, program flow will reach this point
        I = 0
        for Char in Framework:
            if Char == 'x':
                FinalPattern = FinalPattern + ' ' + str(NumberOrList[I])
                I = (I+1) % ListLen
            else:
                FinalPattern = FinalPattern + '  '
    except:
        #if we reach this point, we don't have a list... (or some other problem)
        for Char in Framework:
            if Char == 'x':
                FinalPattern = FinalPattern + ' ' + str(NumberOrList)
            else:
                FinalPattern = FinalPattern + '  '
    return FinalPattern

print buildPattern( 3,'x x x x x x x x ' ) #single number call
print buildPattern( [1,3,5], 'x x x x x x x x ' ) #list call
binarybarbarian
  • 65
  • 1
  • 2
  • 10
  • 1
    just put it in a list if it's a single int: `if type(NumberOrList) == 'int': NumberOrList = [NumberOrList]` – jordanm Oct 03 '17 at 05:57
  • 1
    @jordanm What you wrote is not pythonic and wouldn't even work the way you wrote it. – MegaIng Oct 03 '17 at 06:07

3 Answers3

6

You can use exception handling to do this, but as you say, it's not very elegant. Also, it's not so efficient if the exception gets raised often, it's faster to use an explicit test.

Actually, it would be even more elegant to redesign your code. ;) Just because Python allows you to define a function where the arg can be a number or a list that doesn't mean it's a Good Idea. As you've discovered it tends to make the internal logic of the function more complicated, and it often leads to duplicated code in the different execution paths. But anyway...

The clean way to test the arg is to see whether or not it's an iterable, and you can use the collections.Iterable class to do that. In modern versions of Python, that class has been moved to the collections.abc module, but it's currently still available in collections, to make it easier to write code that runs correctly on Python 2 & Python 3.

BTW, it's generally not a good idea to use "naked" except. You should always name the exception(s) that you want to catch, otherwise you may end up catching things that you don't expect, and that your code won't handle correctly.

Also, it's a good idea to follow the PEP-0008 style guide. It makes it easier for others to read your code.

Here's a more compact version of your function, written using PEP-0008 style names. It uses itertools.cycle to simplify cycling over the source data. It also gathers the output strings into a list & joins them together in a single step. This is more efficient than doing string concatenation in a loop. This code runs correctly on both Python 2 and Python 3.

from __future__ import print_function
from collections import Iterable
from itertools import cycle

def build_pattern(source, framework):
    if not isinstance(source, Iterable):
        source = [source]
    source = cycle(map(str, source))
    final_pattern = ['-->']
    for char in framework:
        final_pattern.append(next(source) if char == 'x' else ' ')
    return ' '.join(final_pattern)

print(build_pattern(3, 'x x x x x x x x ')) #single number call
print(build_pattern([1, 3, 5], 'x x x x x x x x ')) #list call
print(build_pattern('abcde', 'x x x x x x x x ')) #string call

output

--> 3   3   3   3   3   3   3   3  
--> 1   3   5   1   3   5   1   3  
--> a   b   c   d   e   a   b   c  

As VPfB mentions in the comments, a string is an iterable, so if you pass a string to my build_pattern it will get passed directly to cycle(map(str, source)), it won't get wrapped in a list. That's quite ok here, but there are situations where this behaviour can cause problems, the classic case being the flattening of an arbitrarily-nested list. The answers here show how to handle that situation.

PM 2Ring
  • 54,345
  • 6
  • 82
  • 182
  • There is a catch everybody using `isinstance(arg, Iterable)` should be aware of. It is fine for numbers and many other types, but it does not work for strings. A single string is iterable! This is to support the "not a good idea" view. – VPfB Oct 03 '17 at 06:58
  • @VPfB Good point, and I guess I should mention that in my answer. However, in this particular situation it doesn't matter, since if we pass a string to `build_pattern ` it will still do the right thing, as the last line in my code demonstrates. – PM 2Ring Oct 03 '17 at 07:12
3

Why don't you try this

def buildPattern(NumberOrList, Framework):
    if isinstance(NumberOrList, list):
       # This is list
    else:
       # This is number

This is recommended by pep-8 python style guide instead of type(obj)

Reference:

https://www.python.org/dev/peps/pep-0008/#programming-recommendations

theBuzzyCoder
  • 2,652
  • 2
  • 31
  • 26
0

Just use a if statment with isinstance at the top of the function:

if isinstance(NumberOrList, int):
    # Its a number
elif isinstance(NumberOrList,list):
    # Its a list
else:
    # Its something you dont want
MegaIng
  • 7,361
  • 1
  • 22
  • 35