Questioning the effects of multiple assignment
TLDR; if you are a Python 'Master' then feel free to skim the first part
(which you should know hands-down), until the excerpts from 'the manual'
and from there I'll be interested in learning from you...
Yesterday I asked a junior prog to expand an __init__() to accept either
a series of (>1) scalars (as it does now), or to take similar values but
presented as a tuple. He was a bit concerned that I didn't want to
introduce a (separate) keyword-argument, until 'we' remembered
starred-parameters. He then set about experimenting. Here's a dichotomy
that surfaced as part of our 'play':-
(my problem is: I can't (reasonably) answer his question...)
If you read this code:
NB The print-ing does not follow the input-sequence, because that's the
point to be made...
>>> def f( a, *b, c=0 ):
... print( a, type( a ) )
... print( c, type( c ) )
... print( b )
>>> f( 1, 'two', 3, 'four' )
[I had to force "c" to become a keyword argument, but other than that,
we'll be using these three parameters and four argument-values, again]
Question 1: did you correctly predict the output?
1 <class 'int'>
0 <class 'int'>
('two', 3, 'four')
Ahah, "c" went to default because there was no way to identify when the
"*b" 'stopped' and "c" started - so all the values 'went' to become "b"
(were all "consumed by"...).
Why did I also print "b" differently?
Please read on, gentle reader...
Let's make two small changes:
- amend the last line of the function to be similar:
... print( b, type( b ) )
- make proper use of the function's API:
>>> f( 1, 'two', 3, c='four' )
Question 2: can you predict the output of "a"? Well duh!
(same as before)
1 <class 'int'>
Question 3: has your predicted output of "c" changed? Yes? Good!
(Web.Refs below, explain; should you wish...)
four <class 'str'>
Question 4: can you correctly predict the content of "b" and its type?
('two', 3) <class 'tuple'>
That makes sense, doesn't it? The arguments were presented to the
function as a tuple, and those not assigned to a scalar value ("a" and
"c") were kept as a tuple when assigned to "b".
Jolly good show, chaps!
(which made my young(er) colleague very happy, because now he could see
that by checking the length of the parameter, such would reveal if the
arguments were being passed as scalars or as a tuple.
Aside: however, it made me think how handy it would be if the
newly-drafted PEP 622 -- Structural Pattern Matching were available
today (proposed for v3.10, https://www.python.org/dev/peps/pep-0622/)
because (YAGNI-aside) we could then more-easily empower the API to
accept other/more collections!
Why am I writing then?
Because during the same conversations I was
'demonstrating'/training/playing with some code that is (apparently)
very similar - and yet, it's not. Oops!
Sticking with the same, toy-data, let's code:
>>> a, *b, c = 1, 'two', 3, 'four'
>>> a, type( a )
>>> c, type( c )
>>> b, type( b )
Question 5: what do you expect "a" and "c" to deliver in this context?
(1, <class 'int'>)
('four', <class 'str'>)
Happy so far?
Question 6: (for maximum effect, re-read snippets from above, then) what
do you expect from "b"?
(['two', 3], <class 'list'>)
List? A list? What's this "list" stuff???
When "b" was a parameter (above) it was assigned a tuple!
Are you as shocked as I?
Have you learned something?
(will it ever be useful?)
Has the world stopped turning?
Can you explain why these two (apparently) logical assignment processes
have been designed to realise different result-objects?
NB The list cf tuple difference is 'legal' - at least in the sense that
it is documented/expected behavior:-
Python Reference Manual: 7.2. Assignment statements
Assignment statements are used to (re)bind names to values and to modify
attributes or items of mutable objects:
An assignment statement evaluates the expression list (remember that
this can be a single expression or a comma-separated list, the latter
yielding a tuple) and assigns the single resulting object to each of the
target lists, from left to right.
A list of the remaining items in the iterable is then assigned to the
starred target (the list can be empty).
Python Reference Manual: 6.3.4. Calls
A call calls a callable object (e.g., a function) with a possibly empty
series of arguments:
If there are more positional arguments than there are formal parameter
slots, a TypeError exception is raised, unless a formal parameter using
the syntax *identifier is present; in this case, that formal parameter
receives a tuple containing the excess positional arguments (or an empty
tuple if there were no excess positional arguments).