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

时间:2018-11-23 15:13:29

标签: python python-3.x virtual-machine cpython

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

  1. CPython编译器将Python源代码(.py文件)编译为Python字节码(.pyc)文件。在导入任何模块的情况下,将保存.pyc文件,在运行一个main.py Python脚本的情况下,将不保存它们。

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

这里https://stackoverflow.com/a/1732383/8640077的一个很好的回答是,与JVM相比,Python虚拟机运行其字节码需要更长的时间,因为Java字节码包含有关数据类型的信息,而Python虚拟机则逐行解释行,并且必须确定数据类型。

我的问题是Python虚拟机如何确定数据类型,它是在对机器代码的解释期间还是在单独的过程中发生的(例如,将产生另一个中间代码)?

3 个答案:

答案 0 :(得分:3)

CPython的动态,运行时分派(与Java的静态,编译时分派相比)只是Java比纯CPython更快的原因之一:Java中存在jit编译,不同的垃圾回收策略,intdouble之类的本机类型与CPython中不可变数据结构的存在等等。

我以前的superficial experiments曾表明,动态调度仅负责运行的大约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“接口”可能是正确的思维模型),就可以从中得出所有其他“类”。此基本“类”称为PyObjecthere is the description of its protocol.。因此,只要该函数是该协议/接口的一部分,CPython解释器就可以调用该函数,而无需知道确切的类型,并且该调用将被分派到正确的实现(很像C ++中的“虚拟”函数)。

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

a=1
a="1"

但是,内部a的类型为PyObject*,此引用可以绑定到整数(1)和unicode字符串("1" )-因为它们都从PyObject“继承”。

对于上面的示例,CPython解释器有时会尝试找出正确的引用类型-当它看到BINARY_ADD-操作码时,就会执行following C-code

    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-object,请参见{{3 }}),否则工作将分派到PyNumber-协议。

很显然,在创建对象时,解释器还必须知道确切的类型,例如,对于a="1"a=1使用了不同的“类”-但是我们已经知道,它不是只有一个地方。

因此,解释器会在运行时干预类型,但是大多数时候不必这样做-可以通过动态调度来达到目标​​。

答案 1 :(得分:0)

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仅按原样运行字节码。

答案 2 :(得分:0)

避免理解Python中的“变量”可能对您的理解很有用。与必须将类型与变量,类成员或函数参数关联的静态类型语言相比,Python仅处理对象的“标签”或名称。

在摘要中,

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

标签a从来没有类型。它只是在不同时间指向不同对象的名称(实际上,非常相似,也就是其他语言中的“指针”)。另一方面,对象(字符串,数字)始终具有类型。这种类型的性质可能会改变,因为您可以动态更改类的定义,但是它总是可以确定的,即由语言解释器知道。

所以要回答这个问题:Python永远不会确定变量的类型(标签/名称),它仅使用它来引用对象,并且该对象具有类型。