len() should always return something

Steven D'Aprano steve at REMOVE-THIS-cybersource.com.au
Sat Jul 25 09:03:02 CEST 2009


On Fri, 24 Jul 2009 19:50:54 -0700, Dr. Phillip M. Feldman wrote:

> Here's a simple-minded example:
> 
> def dumbfunc(xs):
>    for x in xs:
>       print x
> 
> This function works fine if xs is a list of floats, but not if it is
> single float.  It can be made to work as follows:
> 
> def dumbfunc(xs):
>    if isinstance(xs,(int,float,complex)):
>       xs= [xs]
>    for x in xs:
>       print x
> 
> Having to put such extra logic into practically every function is one of
> the annoying things about Python.


But it's not "practically every function". It's hardly any function at 
all -- in my code, I don't think I've ever wanted this behavior. I would 
consider it an error for function(42) and function([42]) to behave the 
same way. One is a scalar, and the other is a vector -- they're different 
things, it's poor programming practice to treat them identically.

(If Matlab does this, so much the worse for Matlab, in my opinion.)

However, if you want this behaviour, it's easy enough to get it with a 
decorator:

from functools import wraps
def matlab(func):
    """Decorate func so that it behaves like Matlab, that is, 
    it considers a single scalar argument to be the same as a 
    vector of length one."""
    @wraps(func)
    def inner(arg, **kwargs):
        # If arg is already a vector, don't touch it.
        try:
            # If this succeeds, it's a vector of some type.
            iter(arg)
        except TypeError:
            # It's a scalar.
            arg = [arg]
        # Now call the decorated function (the original).
        return func(arg, **kwargs)
    return inner

With this decorator in hand, you can now easily get the behaviour you 
want with a single extra line per function:

>>> @matlab
... def mean(numbers):
...     return sum(numbers)/len(numbers)
...
>>> mean([4.5])
4.5
>>> mean(4.5)
4.5
>>> mean([4.5, 3.6])
4.0499999999999998


Decorators are extremely powerful constructs, and well worth learning. As 
an example, here's a similar decorator that will accept multiple 
arguments, like the built-in functions min() and max():

def one_or_many(func):
    """Decorate func so that it behaves like the built-ins
    min() and max()."""
    @wraps(func)
    def inner(*args, **kwargs):
        # If we're given a single argument, and it's a vector, 
        # use it as args.
        if len(args) == 1:
            try:
                iter(args[0])
            except TypeError:
                pass
            else:
                # No exception was raised, so it's a vector.
                args = args[0]
        # Now call the decorated function (the original).
        return func(args, **kwargs)
    return inner


And then use it:

>>> @one_or_many
... def minmax(numbers):
...     """Return the minimum and maximum element of numbers, 
...     making a single pass."""
...     # the following will fail if given no arguments
...     smallest = biggest = numbers[0]  
...     for x in numbers[1:]:
...             if x < smallest:
...                     smallest = x
...             elif x > biggest:
...                     biggest = x
...     return (smallest, biggest)
...
>>>
>>> minmax([2, 4, 6, 8, 1, 3, 5, 7])
(1, 8)
>>> minmax(2, 4, 6, 8, 1, 3, 5, 7)
(1, 8)




-- 
Steven



More information about the Python-list mailing list