Что на самом деле делает CPython, когда "=" выполняется для переменных примитивного типа?
Например:
a = some_process_that_generates_integer_result()
b = a
Кто-то сказал мне, что b и a будут указывать на один и тот же кусок целочисленного объекта, поэтому b будет изменять счетчик ссылок этого объекта. Код выполнен в функции PyObject* ast2obj_expr(void* _o)
в Python-ast.c:
static PyObject* ast2obj_object(void *o)
{
if (!o)
o = Py_None;
Py_INCREF((PyObject*)o);
return (PyObject*)o;
}
......
case Num_kind:
result = PyType_GenericNew(Num_type, NULL, NULL);
if (!result) goto failed;
value = ast2obj_object(o->v.Num.n);
if (!value) goto failed;
if (PyObject_SetAttrString(result, "n", value) == -1)
goto failed;
Py_DECREF(value);
break;
Однако я думаю, что изменение количества ссылок без смены владельца действительно бесполезно. Я ожидаю, что каждая переменная, содержащая примитивные значения (числа с плавающей точкой, целые числа и т. Д.), Всегда имеет свое собственное значение, а не ссылается на один и тот же объект.
И при выполнении моего простого тестового кода я нашел точку останова в приведенном выше Num_kind
ветка никогда не достигается:
def some_function(x, y):
return (x+y)*(x-y)
a = some_function(666666,66666)
print a
b = a
print a
print b
b = a + 999999
print a
print b
b = a
print a
print b
Я использую программу python2.7-dbg, предоставленную Debian. Я уверен, что программа и исходный код совпадают, потому что многие другие точки останова работают правильно.
Итак, что же на самом деле делает CPython с объектами примитивного типа?
1 ответ
Во-первых, в Python нет "примитивных объектов". Все является одним и тем же объектом, и все они обрабатываются одинаково на уровне языка. Таким образом, следующие назначения работают одинаково независимо от назначенных значений:
a = some_process_that_generates_integer_result()
b = a
В Python назначения всегда являются ссылочными копиями. Поэтому, что бы функция не возвращала, ее ссылка копируется в переменную a
, И затем во второй строке ссылка снова копируется в переменную b
, Таким образом, обе переменные будут ссылаться на один и тот же объект.
Вы можете легко проверить это, используя id()
функция, которая сообщит вам личность объекта:
print id(a)
print id(b)
Это напечатает один и тот же идентификационный номер дважды. Обратите внимание, что, сделав именно это, вы скопировали ссылку еще два раза: это не переменные, которые передаются в функции, а копии ссылок.
Это отличается от других языков, где вы часто различаете "вызов по значению" и "вызов по ссылке". Первый означает, что вы создаете копию значения и передаете ее функции, что означает, что для этого значения выделяется новая память; последнее означает, что фактическая ссылка передается и изменения этой ссылки также влияют на исходную переменную.
То, что делает Python, часто называют "вызовом по присваиванию": каждый вызов функции, когда вы передаете аргументы, по сути является присваиванием новым переменным (которые затем становятся доступны функции). И задание копирует ссылку.
Когда все является объектом, это на самом деле очень простая стратегия. И, как я сказал выше, то, что происходит с целыми числами, ничем не отличается от того, что происходит с другими объектами. Единственное "особенное" в целых числах - это то, что они неизменны, поэтому вы не можете изменить их значения. Это означает, что целочисленный объект всегда ссылается на одно и то же значение. Это позволяет легко обмениваться объектом (в памяти) с несколькими значениями. Каждая операция, которая дает новый результат, дает вам другой объект, поэтому, когда вы выполняете серию арифметических операций, вы фактически меняете объект, на который указывает переменная все время.
То же самое происходит и с другими неизменяемыми объектами, например со строками. Каждая операция, которая возвращает измененную строку, дает вам другой строковый объект.
Назначения с изменяемыми объектами также одинаковы. Просто возможно изменение значения этих объектов, поэтому они выглядят по-разному. Рассмотрим этот пример:
a = [1] # creates a new list object
b = a # copies the reference to that same list object
c = [2] # creates a new list object
b = a + c # concats the two lists and creates a new list object
d = b
# at this point, we have *three* list objects
d.append(3) # mutates the list object
print(d)
print(b) # same result since b and d reference the same list object
Теперь, возвращаясь к вашему вопросу и коду C, который вы там приводите, вы на самом деле смотрите на неправильную часть CPython, чтобы получить там объяснение. AST - это абстрактное синтаксическое дерево, которое анализатор создает при разборе файла. Он отражает синтаксическую структуру программы, но пока ничего не говорит о реальном поведении во время выполнения.
Код, который вы показали для Num_kind
на самом деле несет ответственность за создание Num
АСТ объекты. Вы можете получить представление об этом при использовании ast
модуль:
>>> import ast
>>> doc = ast.parse('foo = 5')
# the document contains an assignment
>>> doc.body[0]
<_ast.Assign object at 0x0000000002322278>
# the target of that assignment has the id `foo`
>>> doc.body[0].targets[0].id
'foo'
# and the value of that assignment is the `Num` object that was
# created in that C code, with that `n` property containing the value
>>> doc.body[0].value
<_ast.Num object at 0x00000000023224E0>
>>> doc.body[0].value.n
5
Если вы хотите получить представление о реальной оценке кода Python, вы должны сначала взглянуть на байт-код. Байт-код - это то, что исполняется виртуальной машиной во время выполнения. Вы можете использовать dis
Модуль для просмотра байтового кода для кода Python:
>>> def test():
foo = 5
>>> import dis
>>> dis.dis(test)
2 0 LOAD_CONST 1 (5)
3 STORE_FAST 0 (foo)
6 LOAD_CONST 0 (None)
9 RETURN_VALUE
Как видите, здесь есть две основные инструкции для байт-кода: LOAD_CONST
а также STORE_FAST
, LOAD_CONST
просто загрузит постоянное значение в стек оценки. В этом примере мы просто загружаем постоянное число, но вместо этого мы можем также загрузить значение из вызова функции (попробуйте поиграть с dis
модуль, чтобы понять, как это работает).
Само назначение выполняется с использованием STORE_FAST
, Интерпретатор байтового кода выполняет следующие действия для этой инструкции:
TARGET(STORE_FAST)
{
v = POP();
SETLOCAL(oparg, v);
FAST_DISPATCH();
}
Таким образом, он по существу получает значение (ссылку на целочисленный объект) из стека, а затем вызывает SETLOCAL
который, по сути, просто присвоит значение локальной переменной.
Обратите внимание, что это не увеличивает счетчик ссылок этого значения. Вот что происходит с LOAD_CONST
или любая другая инструкция байт-кода, которая получает значение откуда-то:
TARGET(LOAD_CONST)
{
x = GETITEM(consts, oparg);
Py_INCREF(x);
PUSH(x);
FAST_DISPATCH();
}
Так что tl; dr: присвоения в Python всегда являются ссылочными копиями. Ссылки также копируются всякий раз, когда используется значение (но во многих других ситуациях эта скопированная ссылка существует только в течение короткого времени). AST отвечает за создание объектного представления анализируемых программ (только синтаксис), в то время как интерпретатор байтового кода запускает предварительно скомпилированный байтовый код для выполнения реальных действий во время выполнения и работы с реальными объектами.