21

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?

Evan
  • 1,348
  • 2
  • 10
  • 20
  • 1
    Huh. And if you use positive integers (`x=10`; `y=10`) you see different behavior. – larsks Dec 24 '17 at 04:52
  • `is` checks the memory address of the object...so why would you expect two variables to share the same address like you've done? – pstatix Dec 24 '17 at 04:55
  • Any two variables that are assigned to values between -5 and 256 (including -5 and 256) would have the same ids. – Akavall Dec 24 '17 at 04:57
  • 1
    It's not the assignment so much as the list literal is reusing the same object. Try `z = [-20, -20]; z[0] is z[1]`. – chepner Dec 24 '17 at 05:01
  • @Akavall Elaborate? – pstatix Dec 24 '17 at 05:04
  • 3
    This might also help https://stackoverflow.com/questions/15171695/whats-with-the-integer-cache-inside-python/15172182#15172182 – Bharath M Shetty Dec 24 '17 at 05:05
  • @Dark, your first link is differentiating between small numbers and lists or tuples of small numbers. – Evan Dec 24 '17 at 05:08
  • @EvanRose The first links answer did cover this portion, so I thought of it as dupe. But the second link is much better to answer this case. – Bharath M Shetty Dec 24 '17 at 05:09
  • 1
    I might be missing it, but where does that first link use destructured assignment? – Evan Dec 24 '17 at 05:11
  • 1
    @chepner Your explanation makes sense to me. The interpreter uses the same object for repeating values in a list to save memory. – Evan Dec 24 '17 at 05:20
  • This behaviour is different in `python3.x`. You will get both of them as `False`. – Sohaib Farooqi Dec 24 '17 at 05:32
  • Python 2 is dead, so don't worry about it. Version 3 behaves as expected. –  Dec 24 '17 at 10:23
  • Related ["is" operator behaves unexpectedly with integers](https://stackoverflow.com/questions/306313/is-operator-behaves-unexpectedly-with-integers) – Ilja Everilä Dec 24 '17 at 12:27
  • @gecko As something that is almost certainly an implementation-dependent optimization, I wouldn't depend on any consistent behavior even in Python 2. – chepner Dec 24 '17 at 13:29
  • More importantly, why are you testing integers for identity? –  Dec 24 '17 at 14:06
  • Just for curiosity and educational purposes. – Evan Dec 24 '17 at 18:46
  • @gecko The OP posted about this behavior [here](http://9tabs.com/random/2017/12/23/evil-coding-incantations.html) – miracle173 Dec 25 '17 at 07:44
  • @chepner you are right, but I think you wanted to write "python 3" instead of "python 2" – miracle173 Dec 25 '17 at 07:47
  • @miracle173 I was referring to gecko's implication that the behavior is specific to a version of Python. It isn't; whether two literals will refer to the same object in memory or not is not defined by the language; it is an implementation-dependent optimization. The only time you can rely on two names to refer to the same object is if you explicitly assign one to the other: `x = y` guarantees that `x is y` will subsequently be true (assuming neither name is modified in between). – chepner Dec 26 '17 at 16:44

2 Answers2

8

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 :)

rjh
  • 49,276
  • 4
  • 56
  • 63
6

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.

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343