Python何时以及如何确定变量的数据类型?

12

我试图弄清楚Python 3(使用CPython作为解释器)如何执行其程序。我发现其步骤如下:

  1. 通过CPython编译器将Python源代码(.py文件)编译成Python字节码(.pyc)文件。在导入任何模块的情况下,.pyc文件都会被保存,在运行一个主.py Python脚本的情况下,则不保存。

  2. Python虚拟机将字节码解释为硬件特定的机器码。

这里发现一个很好的答案(https://dev59.com/jXRC5IYBdhLWcg3wAcM3#1732383)指出,与JVM相比,Python虚拟机花费更长时间来运行其字节码,因为java字节码包含有关数据类型的信息,而Python虚拟机逐行解释并必须确定数据类型。

我的问题是Python虚拟机是如何确定数据类型的?这个过程是在解释为机器码的过程中进行还是另一个单独的过程(例如产生另一个中间代码)?


6
你认为Python为什么需要“确定数据类型”?Python是一种动态类型语言;只有在明确要求时才会检查类型,并且变量的类型可能在其生命周期内发生连续变化。我非常怀疑Python和Java之间执行时间差别是由于运行时类型检查引起的。 - Daniel Roseman
那么即使在从Bytecode翻译成Machine Code的过程中,Python也不知道变量的类型吗? AD 2:那么Python和Java之间执行时间最大的区别是什么呢? - PyFox
1
我在寻找答案时发现了这个资源。也许你会觉得它有用。 - user2201041
1
我还没有完全理解 这篇文章,但它比较简短,我认为它回答了你的问题。 - user2201041
在这里发现了一个很棒的答案,如下链接所示:[请添加答案链接] - Martin Thoma
显示剩余4条评论
3个回答

4
CPython的动态运行时分发(与Java的静态编译时分发相比)只是Java比纯CPython更快的原因之一:Java中有jit编译,不同的垃圾回收策略,原生类型如int、double,而CPython中是不可变数据结构等等。早期的表面实验已经表明,动态分发只占运行速度的约30% - 你不能用它来解释几个数量级的速度差异。为了让这个答案更具体化,让我们看一个例子:
def add(x,y):
   return x+y

查看字节码:

import dis
dis.dis(add)

这将会给出:

2         0 LOAD_FAST                0 (x)
          2 LOAD_FAST                1 (y)
          4 BINARY_ADD
          6 RETURN_VALUE

在字节码级别上,无论xy是整数、浮点数还是其他类型,解释器都不会关心。

在Java中情况完全不同:

int add(int x, int y) {return x+y;}

float add(float x, float y) {return x+y;}

这将导致完全不同的操作码,并且调用分派将在编译时发生 - 根据编译时已知的静态类型选择正确的版本。

很多时候,CPython解释器不必知道参数的确切类型:内部有一个基本的“类/接口”(显然在C中没有类,因此称为“协议”,但对于了解C++ / Java的人来说,“接口”可能是正确的心理模型),从中派生出所有其他“类”。这个基本“类”被称为PyObject这里是其协议的描述。。因此,只要函数是该协议/接口的一部分,CPython解释器就可以调用它,而无需知道确切的类型,调用将被分派到正确的实现(非常像C ++中的“虚拟”函数)。

在纯Python方面,似乎变量没有类型:

a=1
a="1"

然而,在内部,a 有一种类型 - 它是 PyObject*,这个引用可以绑定到一个整数 (1) 和一个 Unicode 字符串 ("1") - 因为它们都“继承”自 PyObject

时不时地,CPython 解释器会尝试找出引用的正确类型,对于上面的示例 - 当它看到 BINARY_ADD 操作码时,将执行 以下 C 代码

    case TARGET(BINARY_ADD): {
        PyObject *right = POP();
        PyObject *left = TOP();
        PyObject *sum;
        ...
        if (PyUnicode_CheckExact(left) &&
                 PyUnicode_CheckExact(right)) {
            sum = unicode_concatenate(left, right, f, next_instr);
            /* unicode_concatenate consumed the ref to left */
        }
        else {
            sum = PyNumber_Add(left, right);
            Py_DECREF(left);
        }
        Py_DECREF(right);
        SET_TOP(sum);
        if (sum == NULL)
            goto error;
        DISPATCH();
    }

在这里,解释器查询是否两个对象都是Unicode字符串,如果是这种情况,则使用特殊方法(可能更有效,事实上它尝试原地更改不可变的Unicode对象,请参见此SO-answer),否则工作将分派给PyNumber协议。

显然,在创建对象时解释器也必须知道确切的类型,例如对于a="1"a=1,使用不同的“类”- 但正如我们所看到的,这并不是唯一的地方。

因此,解释器在运行时干涉类型,但大多数时候它不必这样做-可以通过动态分派达到目标。


2

为了更好地理解Python中的“变量”,最好不要这样想。与静态类型语言不同,静态类型语言必须将类型与变量、类成员或函数参数关联起来,而Python仅处理对象的“标签”或名称。

因此,在以下代码片段中,

a = "a string"
a = 5 # a number
a = MyClass() # an object of type MyClass

1
Python是围绕鸭子类型哲学构建的。没有显式的类型检查,甚至在运行时也没有。例如,
>>> x = 5
>>> y = "5"
>>> '__mul__' in dir(x)
>>> True
>>> '__mul__' in dir(y)
>>> True
>>> type(x)
>>> <class 'int'>
>>> type(y)
>>> <class 'str'>
>>> type(x*y)
>>> <class 'str'>

CPython解释器会检查xy是否定义了__mul__方法,并尝试“使其工作”并返回结果。此外,Python字节码永远不会被翻译成机器码。它在CPython解释器内部执行。JVM和CPython虚拟机之间的一个主要区别是,JVM可以将Java字节码编译为机器码以获得性能提升(即JIT编译),而CPython VM只能像原样运行字节码。

你说的“Python字节码永远不会被翻译成机器码。它在CPython解释器内执行。”是什么意思?你能详细说明一下吗? - PyFox
机器码通常指可以在计算机上执行的代码。例如,当您在计算机上编译C++程序时,它会被编译为特定于您计算机CPU架构的机器码。您的CPU可以理解这些指令并运行它们。因此,在某种程度上,您的CPU是解释器。将Python字节码视为CPython虚拟机的机器码。它们只是CPython虚拟机的指令。CPython虚拟机可以运行这些指令,而无需将它们转换为其他内容。 - prithajnath

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