例如:

a = some_process_that_generates_integer_result()
b = a

有人告诉我B和A将指向同一整型对象块,因此B将修改该对象的引用计数。代码在python ast.c中的函数PyObject* ast2obj_expr(void* _o)中执行:
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中,赋值总是引用副本。所以无论函数返回什么,它的引用都会复制到变量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显示的代码实际上负责创建Numast对象。当使用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_CONSTSTORE_FASTLOAD_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负责创建解析程序(仅语法)的对象表示,而字节代码解释器运行先前编译的字节代码,以在运行时执行实际操作并处理真实对象。

关于python - 当对原始类型变量执行“=”时,CPython实际做什么?,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/37764401/

10-12 16:51