Python解释器在动态类型中是如何工作的?

9
我阅读了这个问题,但是它没有给我一个清晰的答案:Python 解释器如何查找类型? Python 解释器如何知道变量的类型?我不是在寻找如何获取类型。我想知道背后发生了什么。在下面的示例中,它如何将 int 或 string 类与我的变量关联起来。
它如何知道这是一个 int:
>>> i = 123
>>> type(i) 
<class 'int'>

或者那个字符串:
>>> i = "123"
>>> type(i)
<class 'str'>

@GreenAsJade:OP正在使用Python 3,其中' type'对象的表示方法使用“class”,而不是“type”;这是为了反映C定义的类型也只是类而已。 - Martijn Pieters
@MartijnPieters 或许需要一个 python3 标签吗? - GreenAsJade
@GreenAsJade:不,Python 2和3的答案是相同的。但是所提供的输出不需要更正。 - Martijn Pieters
啊,好的。但是字符串的例子确实可以 :) - GreenAsJade
3个回答

12
“Python 不会把类 int 或 string 与变量关联起来。变量没有类型,只有变量所引用的对象才有类型。变量只是指向对象的名称。例如,下面的代码也显示了对象的类型,但没有涉及任何变量:”
>>> type(1)
<class 'int'>
>>> type('foobar')
<class 'str'>

当你使用type(variable)时,表达式中的variable部分只是返回名称引用的对象,将该对象传递给type()函数。当使用1'foobar'时,该表达式是生成对象的字面值,然后将其传递给type()函数。
Python对象只是解释器内存中的数据结构;在CPython中使用C结构体。变量只是对这些结构的引用(指针)。在CPython中,基本类型结构称为PyObject,并且此结构具有告诉Python某个类型的ob_type。类型只是更多的C结构
如果您想在CPython源代码中跟踪,您需要从bltinmodule.c源代码开始(因为type是内置名称),该源代码定义typePyType_Type结构。调用typetype也是type)会调用它们的tp_new函数,而PyType_Type将其定义为type_new函数。此函数处理具有一个参数的调用如下:
/* Special case: type(x) should return x->ob_type */
{
    const Py_ssize_t nargs = PyTuple_GET_SIZE(args);
    const Py_ssize_t nkwds = kwds == NULL ? 0 : PyDict_Size(kwds);

    if (PyType_CheckExact(metatype) && nargs == 1 && nkwds == 0) {
        PyObject *x = PyTuple_GET_ITEM(args, 0);
        Py_INCREF(Py_TYPE(x));
        return (PyObject *) Py_TYPE(x);
    }

这里的x是您传入的PyObject对象;请注意,它不是一个变量,而是一个对象!因此,对于您的1整数对象或'foobar'字符串对象,将返回Py_TYPE()宏的结果。Py_TYPE是一个宏,它只是返回任何PyObject结构的ob_type值。

现在您已经有了1'foobar'的类型对象;为什么您在解释器会话中看到<class 'int'><class 'str'>呢?Python交互式解释器自动对任何表达式结果使用repr()函数。在PyType_Type定义的C结构中,PyType_Type结构被合并,因此该类型的所有插槽都直接可用;我将省略如何实现这一点。对于类型对象,使用repr()意味着调用type_repr函数,它返回以下内容:
rtn = PyUnicode_FromFormat("<class '%s'>", type->tp_name);

最终,type(1) 获取 ->ob_type 插槽(在 Python 3 中是 PyLong_Type 结构体,较长的故事),该结构具有将 tp_name 插槽设置为 "int" 的特性

TL;DR: Python 变量没有类型,它们只是指向对象的指针。 对象 有类型,如果您在解释器中回显对象,则 Python 解释器将遵循一系列间接引用以达到要打印的类型名称。


谢谢您的回答Martijn。只是想澄清一下,type_repr在什么时候被调用?在return (PyObject *) Py_TYPE(x);之后吗? - user51462
1
当你使用repr(object)时,会对object进行内省以查看是否有钩子来实现表示,这将导致type_reprreturn (PyObject *) Py_TYPE(x);部分不涉及其中,这是在调用type(object)时使用的。 - Martijn Pieters
啊,我明白了,所以它根本不是type(object)的一部分。非常感谢您及时的回复Martijn,我一直卡在这里。 - user51462

2

Python变量没有类型,它们只是对象的引用。无论引用什么,引用的大小都相同。在Python的C实现中,它是一个指针,并且有一种类型,它是指向Python对象的指针:PyObject *。无论对象的类别如何,指针的类型都是相同的。另一方面,对象知道它们属于哪个类。

有人认为Python没有变量,只有名称,尽管这对大多数人来说都太过分了。

在CPython实现中,引用具有id(标识符),它实际上是一个虚拟地址。这个地址的细节和值不值得追究-它可以(并且可能会)在版本之间改变,而且不应该被用于除了唯一标识对象的编号之外的任何事情。然而,它可以提供有趣的指针(请原谅双关语)以了解正在发生什么:

>>> x = 42
>>> y = x
>>> id(x)
4297539264
>>> id(y)
4297539264

请注意,xy的id(地址)是相同的 - 它们引用了同一个对象,一个值为42的int。那么,当我们改变x时,y也会改变吗?
>>> x = "hello"
>>> id(x)
4324832176
>>> id(y)
4297539264

感谢不用担心。现在x只是一个类为str的新对象,其值为“Hello”。
当我们:
>>> id(y)
4297539264
>>> y = 37
>>> id(y)
4297539104 
y的id已经改变了!这是因为它现在引用的是一个不同的对象。int是不可变的,所以赋值y = 37没有改变原始对象(42),它只是创建了一个新的对象。值为42的对象的引用计数被减少,现在可以(理论上)被删除。实际上,出于效率的原因,它可能会保留在内存中,但这是一些具体实现细节。
然而,让我们尝试对列表进行类似操作:
>>> a = [1,2,3,4]
>>> b = a
>>> id(a)
4324804808
>>> id(b)
4324804808
>>> a[0] = 99
>>> b
[99, 2, 3, 4]

所以改变列表a已经改变了b!这是因为Python中的列表(与R不同)是可变的,因此它们可以原地更改。赋值b = a仅复制了引用,从而节省了内存(实际上没有复制任何数据)。字典是另一个具有此类行为的对象。请参见标准库中的copy


0
变量的“类型”概念是通过使用特定类的对象来“实现”的。
因此,在
a=float()
中,float()返回由float类定义的类型float的对象。Python知道它是什么类型,因为这就是对象的工作方式:你知道它们是什么类型。a现在是一个值为0.0的float对象。
对于内置函数而言,也是一样的,只是它们有声明的快捷方式。
i=123

i=int(123)
是相同的。
int()返回一个值为123的整数类对象。
同样,
i="123"
等同于
i=str("123")
str("123")返回一个值为“123”的字符串类对象。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接