[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

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?
Building tension!
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, 
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).