Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What is the difference between destructured assignment and "normal" assignment? [duplicate]

Tags:

I was playing around in the python 2.7.6 REPL and came across this behavior.

>>> x = -10
>>> y = -10
>>> x is y
False
>>> x, y = [-10, -10]
>>> x is y
True

It seems that destructured assignment returns the same reference for equivalent values. Why is this?

like image 433
Evan Rose Avatar asked Dec 24 '17 04:12

Evan Rose


2 Answers

I know nothing about Python but I was curious.

First, this happens when assigning an array too:

x = [-10,-10]
x[0] is x[1]  # True

It also happens with strings, which are immutable.

x = ['foo', 'foo']
x[0] is x[1]  # True

Disassembly of the first function:

         0 LOAD_CONST               1 (-10)
         3 LOAD_CONST               1 (-10)
         6 BUILD_LIST               2
         9 STORE_FAST               0 (x)

The LOAD_CONST (consti) op pushes constant co_consts[consti] onto the stack. But both ops here have consti=1, so the same object is being pushed to the stack twice. If the numbers in the array were different, it would disassemble to this:

         0 LOAD_CONST               1 (-10)
         3 LOAD_CONST               2 (-20)
         6 BUILD_LIST               2
         9 STORE_FAST               0 (x)

Here, constants of index 1 and 2 are pushed.

co_consts is a tuple of constants used by a Python script. Evidently literals with the same value are only stored once.

As for why 'normal' assignment works - you're using the REPL so I assume each line is compiled seperately. If you put

x = -10
y = -10
print(x is y)

into a test script, you'll get True. So normal assignment and destructured assignment both work the same in this regard :)

like image 129
rjh Avatar answered Sep 30 '22 23:09

rjh


What happens is that the interactive Python interpreter compiles every statement separately. Compilation not only produces bytecode, it also produces constants, for any built-in immutable type, including integers. Those constants are stored with the code object as the co_consts attribute.

Your x = -10 is compiled separately from the y = -10 assignment, and you end up with entirely separate co_consts structures. Your x, y = [-10, -10] iterable assignment on the other hand, is a single assignment statement, passed to the compiler all at once, so the compiler can re-use constants.

You can put simple statements (like assignments) on a single line with a semicolon between them, at which point, in Python 2.7, you get the same -10 object again:

>>> x = -10; y = -10
>>> x is y
True

Here we compiled a single statement again, so the compiler can decide it that it only needs a single object to represent the -10 value:

>>> compile('x = -10; y = -10', '', 'single').co_consts
(-10, None)

'single' is the compilation mode the interactive interpreter uses. The compiled bytecode loads the -10 value from those constants.

You'd get the same thing if you put everything in a function, compiled as a single compound statement:

>>> def foo():
...     x = -10
...     y = -10
...     return x is y
...
>>> foo()
True
>>> foo.__code__.co_consts
(None, -10)

Modules are also compiled in one pass, so globals in a module can share constants.

All this is an implementation detail. You should never, ever count on this.

For example, in Python 3.6, the unary minus operator is handled separately (rather than -10 being seen as a single integer literal), and the -10 value is reached after constant folding during the peephole optimisation. This gets you get two separate -10 values:

>>> import sys
>>> sys.version_info
sys.version_info(major=3, minor=6, micro=3, releaselevel='final', serial=0)
>>> compile('x = -10; y = -10', '', 'single').co_consts
(10, None, -10, -10)

Other Python implementations (PyPy, Jython, IronPython, etc.) are free to handle constants differently again.

like image 30
Martijn Pieters Avatar answered Sep 30 '22 23:09

Martijn Pieters