squirl
squirl

Reputation: 1784

Default values for iterable unpacking

I've often been frustrated by the lack of flexibility in Python's iterable unpacking.

Take the following example:

a, b = range(2)

Works fine. a contains 0 and b contains 1, just as expected. Now let's try this:

a, b = range(1)

Now, we get a ValueError:

ValueError: not enough values to unpack (expected 2, got 1)

Not ideal, when the desired result was 0 in a, and None in b.

There are a number of hacks to get around this. The most elegant I've seen is this:

a, *b = function_with_variable_number_of_return_values()
b = b[0] if b else None

Not pretty, and could be confusing to Python newcomers.

So what's the most Pythonic way to do this? Store the return value in a variable and use an if block? The *varname hack? Something else?

Upvotes: 8

Views: 1292

Answers (3)

yatsek
yatsek

Reputation: 1015

Shortest known to me version (thanks to @KellyBundy in comments below):

a, b, c, d, e, *_ = *my_list_or_iterable, *[None]*5

Obviously it's possible to use other default value than None if necessary.

Also there is one nice feature in Python 3.10 which comes handy here when we know upfront possible numbers of arguments - like when unpacking sys.argv

Previous method:

import sys.argv

_, x, y, z, *_ = *sys.argv, *[None]*3

New method:

import sys


match sys.argv[1:]: #slice needed to drop first value of sys.argv
    case [x]:
        print(f'x={x}')
    case [x,y]:
        print(f'x={x}, y={y}')
    case [x,y,z]:
        print(f'x={x}, y={y}, z={z}')
    case _:
        print('No arguments')

Upvotes: 0

squirl
squirl

Reputation: 1784

Here's an alternative version of the decorator solution by @supersam654, using iterators rather than lists for efficiency:

def variable_return(max_values, default=None):
    def decorator(f):
        def wrapper(*args, **kwargs):
            actual_values = f(*args, **kwargs)
            try:
                for count, value in enumerate(actual_values, 1):
                    yield value
            except TypeError:
                count = 1
                yield actual_values
            yield from [default] * (max_values - count)
        return wrapper
    return decorator

It's used in the same way:

@variable_return(3)
def ret_n(n):
    return tuple(range(n))

a, b, c = ret_n(2)

This could also be used with non-user-defined functions like so:

a, b, c = variable_return(3)(range)(2)

Upvotes: 1

supersam654
supersam654

Reputation: 3244

As mentioned in the comments, the best way to do this is to simply have your function return a constant number of values and if your use case is actually more complicated (like argument parsing), use a library for it.

However, your question explicitly asked for a Pythonic way of handling functions that return a variable number of arguments and I believe it can be cleanly accomplished with decorators. They're not super common and most people tend to use them more than create them so here's a down-to-earth tutorial on creating decorators to learn more about them.

Below is a decorated function that does what you're looking for. The function returns an iterator with a variable number of arguments and it is padded up to a certain length to better accommodate iterator unpacking.

def variable_return(max_values, default=None):
    # This decorator is somewhat more complicated because the decorator
    # itself needs to take arguments.
    def decorator(f):
        def wrapper(*args, **kwargs):
            actual_values = f(*args, **kwargs)
            try:
                # This will fail if `actual_values` is a single value.
                # Such as a single integer or just `None`.
                actual_values = list(actual_values)
            except:
                actual_values = [actual_values]
            extra = [default] * (max_values - len(actual_values))
            actual_values.extend(extra)
            return actual_values
        return wrapper
    return decorator

@variable_return(max_values=3)
# This would be a function that actually does something.
# It should not return more values than `max_values`.
def ret_n(n):
    return list(range(n))

a, b, c = ret_n(1)
print(a, b, c)
a, b, c = ret_n(2)
print(a, b, c)
a, b, c = ret_n(3)
print(a, b, c)

Which outputs what you're looking for:

0 None None
0 1 None
0 1 2

The decorator basically takes the decorated function and returns its output along with enough extra values to fill in max_values. The caller can then assume that the function always returns exactly max_values number of arguments and can use fancy unpacking like normal.

Upvotes: 2

Related Questions