当对原始类型变量执行“=”时,CPython 实际上做了什么?
What does CPython actually do when "=" is performed on primitive type variables?
例如:
a = some_process_that_generates_integer_result()
b = a
有人告诉我 b 和 a 将指向同一块整数对象,因此 b 将修改该对象的引用计数。代码在函数PyObject* ast2obj_expr(void* _o)
in 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
我正在使用 Debian 提供的 python2.7-dbg 程序。我确定程序和源代码匹配,因为许多其他断点都可以正常工作。
那么,CPython 实际上对基本类型对象做了什么?
首先,Python中没有“原始对象”。一切都是对象,同类,在语言层面上它们都以相同的方式处理。因此,无论分配的值如何,以下分配都以相同的方式工作:
a = some_process_that_generates_integer_result()
b = a
在Python中,作业总是个参考副本。所以无论函数 returns 是什么,它的引用都会 复制 到变量 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 对象。您可以在使用 ast
module:
时了解这一点
>>> 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
module 查看 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
完成的。字节码解释器对该指令执行the following:
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 负责创建已解析程序的对象表示(仅语法),而字节码解释器 运行s 先前编译的字节码在 运行 时执行实际操作并处理实际对象。
例如:
a = some_process_that_generates_integer_result()
b = a
有人告诉我 b 和 a 将指向同一块整数对象,因此 b 将修改该对象的引用计数。代码在函数PyObject* ast2obj_expr(void* _o)
in 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
我正在使用 Debian 提供的 python2.7-dbg 程序。我确定程序和源代码匹配,因为许多其他断点都可以正常工作。
那么,CPython 实际上对基本类型对象做了什么?
首先,Python中没有“原始对象”。一切都是对象,同类,在语言层面上它们都以相同的方式处理。因此,无论分配的值如何,以下分配都以相同的方式工作:
a = some_process_that_generates_integer_result()
b = a
在Python中,作业总是个参考副本。所以无论函数 returns 是什么,它的引用都会 复制 到变量 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 对象。您可以在使用 ast
module:
>>> 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
module 查看 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
完成的。字节码解释器对该指令执行the following:
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 负责创建已解析程序的对象表示(仅语法),而字节码解释器 运行s 先前编译的字节码在 运行 时执行实际操作并处理实际对象。