Почему оператор `is` ведет себя по-разному в сценарии против REPL? - PullRequest
4 голосов
/ 26 марта 2019

В python два кода имеют разные результаты:

a = 300
b = 300
print (a==b)
print (a is b)      ## print True
print ("id(a) = %d, id(b) = %d"%(id(a), id(b))) ## They have same address

Но в режиме оболочки (интерактивный режим):

>>> a = 300
>>> b = 300
>>> a is b
False
>>> id(a)
4501364368
>>> id(b)
4501362224

Оператор "is" имеет разные результаты.

Ответы [ 2 ]

7 голосов
/ 26 марта 2019

Когда вы запускаете код в сценарии .py, весь файл скомпилируется в объект кода перед его выполнением . В этом случае CPython может выполнять определенные оптимизации - например, повторно использовать один и тот же экземпляр для целого числа 300.

Вы также можете воспроизвести это в REPL, выполнив код в контексте, более похожем на выполнение скрипта:

>>> source = """\ 
... a = 300 
... b = 300 
... print (a==b) 
... print (a is b)## print True 
... print ("id(a) = %d, id(b) = %d"%(id(a), id(b))) ## They have same address 
... """
>>> code_obj = compile(source, filename="myscript.py", mode="exec")
>>> exec(code_obj) 
True
True
id(a) = 140736953597776, id(b) = 140736953597776

Некоторые из этих оптимизаций довольно агрессивны. Вы можете изменить строку сценария b = 300, изменив ее на b = 150 + 150, и CPython все равно "свернет" b в ту же самую константу. Если вас интересуют такие подробности реализации, посмотрите «1016 *peephole.c» и Ctrl + F для «таблицы констант».

Напротив, когда вы запускаете код построчно непосредственно в REPL, он выполняется в другом контексте. Каждая строка компилируется в «одиночном» режиме, и эта оптимизация недоступна.

>>> scope = {} 
>>> lines = source.splitlines()
>>> for line in lines: 
...     code_obj = compile(line, filename="<I'm in the REPL, yo!>", mode="single") 
...     exec(code_obj, scope) 
...
True
False
id(a) = 140737087176016, id(b) = 140737087176080
>>> scope['a'], scope['b']
(300, 300)
>>> id(scope['a']), id(scope['b'])
(140737087176016, 140737087176080)
4 голосов
/ 26 марта 2019

Здесь есть две вещи, которые нужно знать о CPython и его поведении.Во-первых, маленькие целые числа в диапазоне [- 5, 256] внутри.Таким образом, любое значение, попадающее в этот диапазон, будет иметь один и тот же идентификатор, даже при REPL:

>>> a = 100
>>> b = 100
>>> a is b
True

Поскольку 300> 256, оно не интернируется:

>>> a = 300
>>> b = 300
>>> a is b
False

Во-вторых, это то, чтов скрипте литералы помещаются в постоянный раздел скомпилированного кода.Python достаточно умен, чтобы понять, что и a, и b относятся к литералу 300 и что 300 является неизменным объектом, он может просто идти вперед и ссылаться на одно и то же постоянное местоположение.Если вы немного доработаете свой сценарий и напишите его так:

def foo():
    a = 300
    b = 300
    print(a==b)
    print(a is b)
    print("id(a) = %d, id(b) = %d" % (id(a), id(b)))


import dis
dis.disassemble(foo.__code__)

Начальная часть вывода выглядит следующим образом:

2           0 LOAD_CONST               1 (300)
            2 STORE_FAST               0 (a)

3           4 LOAD_CONST               1 (300)
            6 STORE_FAST               1 (b)

...

Как видите, CPython загружает a и b с использованием одного и того же постоянного слота.Это означает, что a и b теперь ссылаются на один и тот же объект (потому что они ссылаются на один и тот же слот), и именно поэтому a is b в сценарии True, но не в REPL.

Такое поведение можно увидеть и в REPL, если вы оберните свои операторы в функцию:

>>> import dis
>>> def foo():
...   a = 300
...   b = 300
...   print(a==b)
...   print(a is b)
...   print("id(a) = %d, id(b) = %d" % (id(a), id(b)))
...
>>> foo()
True
True
id(a) = 4369383056, id(b) = 4369383056
>>> dis.disassemble(foo.__code__)
  2           0 LOAD_CONST               1 (300)
              2 STORE_FAST               0 (a)

  3           4 LOAD_CONST               1 (300)
              6 STORE_FAST               1 (b)
# snipped...

Итог: хотя CPython время от времени проводит эти оптимизации, вы не должны действительно рассчитывать на это:это действительно детали реализации, и они со временем менялись (например, CPython делал это только для целых чисел до 100).Если вы сравниваете числа, используйте ==.: -)

...