Proposal: named return values through dict initialization and unpacking

Steven D'Aprano steve at pearwood.info
Tue Jun 21 21:35:05 EDT 2016


On Tue, 21 Jun 2016 05:34 pm, Ari Freund wrote:

> I'd like to run this idea by the community to see if it's PEP worthy and
> hasn't been already rejected.
> 
> Background
> Just as keyword arguments enhance code readability and diminish the risk
> of bugs, so too would named
> return values.

I don't think that follows. Keyword arguments work well when a function has
many optional arguments, say, ten or so, but it's rare for functions to
have optional return values, and typically they only return a few values:
usually one, sometimes two, rarely three.

If your function has more than three return values, you really need to
rethink the function. Chances are good that it is either doing too much, or
you should be encapsulating some of the detail of what it returns into
objects:

# Ugly:
fe, fi, fo, fum, spam, eggs, cheese, tomato, foo, bar, baz = func(arg)

# Probably better:
obj = func(arg)
assert hasattr(obj, 'fe')
assert hasattr(obj, 'fi')  # etc.


Of course, returning a dict of {key:value} pairs is a kind of encapsulation.


> Currently, we can write 
>    val1, val2, val3 = myfunc()
> but we must take care to order the variables correctly, i.e., in the same
> order as the corresponding
> values in myfunc's return statement.  Getting it wrong may result in
> difficult-to-detect bugs.

Most languages deal with that with type checking, which can eliminate many
(but not all) of such bugs. Python 3 will encourage optional type checking
too. Alternatively, this is one of the issues that TDD and unit-testing can
solve. And in practice, help(func) should never be more than a few key
presses away, so I just don't see this as being a sufficiently large
problem in practice to deserve new syntax.

Yes, I found it annoying for the longest time that I couldn't remember which
order enumerate returned its values: index, item or item, index. But I had
many solutions to turn to, and eventually I memorised it.


> This 
> problem would be alleviated if we could name the return values and
> disregard their order.
> 
> My proposal consists of two parts.
> 
> 1.  Add a new form of assignment by dictionary unpacking.
>       Suppose d is a dict whose keys are the strings 'var1', 'var2',
> 'var3'.  Then the assignment
>          var1, var2, var3 = **d
>       would set the variables to the corresponding values.  Order would
>       not
> be important, so
>          var3, var1, var2 = **d
>       would have the same effect.


But I don't want to use the horrible key names your function uses. I want to
use names which makes sense for my application:

width, counter, aardvark = **d

but d is {'var1': x, 'var2': y, 'var3': z}. What am I to do?

Seriously, this is a real problem for your proposal. It's one thing to be
forced to use a function's ugly parameter names:

result = somefunction(var1=width, var2=counter, var3=aardvark)

It's another to be forced to use variables that match something the function
sets. What if we're already using a variable with that name? What if we
want to use a different name?

Realistically, we would hope that functions will choose informative names,
but informative names tend to be long. Or if they are short, they might be
excessively generic. Your function might return:

number_of_pages, number_of_sections, number_of_chapters

but I might prefer to use:

pagecount, sectioncount, chaptercount

or even:

npage, nsect, nchapt


It is unacceptable if I have to do this:

number_of_pages, number_of_sections, number_of_chapters = **func(args)
npage, nsect, nchapt = (number_of_pages, number_of_sections,
        number_of_chapters)



Whatever names your function uses, whether they are overly abstract, generic
names like a, b, c or var1, var2, var3, or painfully detailed long names
like number_of_pages_containing_requested_section_titles, the caller is
generally going to want to use *something else* for the assignment target.


By the way, we can already get pretty close to this today:

d = func(args)
npage, nsect, nchapt = (d[key] for key in 
        'number_of_pages number_of_sections number_of_chapters'.split())



>       Also, not all keys would need to participate; for example,
>          var2 = **d
>       would work just fine.
>       It would be an error for the same name to appear more than once on
> the LHS of the assignment.

Is that necessary? I can currently do this:

spam, eggs, spam, spam, cheese = func(args)


>       It would be an error for a name on the LHS to not be a valid
>       variable
> name or for there not to
>       be a corresponding key in the dict.

So I can't assign directly to legal assignment targets?

# forbidden
mylist[2], obj.foo, mydict['key'] = **func(args)


This is looking less and less useful...


>       It would (possibly) be an error if the dict contained keys that are
> not strings (or are strings
>       that cannot serve as variable names).
> 
> 2.  Extend the dict() form of dictionary initialization to allow an
> element to be a variable name, e.g.,
>          dict(var1, var2, var3, var4=33)
>       would be equivalent to
>          dict(var1=var1, var2=var2, var3=var3, var4=33)

How is dict supposed to know what var1 etc. are? The dict constructor has no
more magical insight into the caller's locals as any other function.

Also, this is legal syntax now:

d = {'a': 1}
mydict = dict(d, spam=999)


giving {'spam': 999, 'a': 1}. Your proposal would return:

{'spam': 999, 'd': {'a': 1}}


or would have an ambiguous case where the caller would never be sure which
behaviour takes precedence.



-- 
Steven




More information about the Python-list mailing list