Python 如何以及何时确定变量的数据类型?
How and when does Python determine the data type of a variable?
我试图弄清楚 Python 3(使用 CPython 作为解释器)是如何执行它的程序的。我发现步骤是:
CPython 编译器将 Python 源代码(.py 文件)编译为 Python 字节码 (.pyc) 文件。在导入任何模块的情况下,.pyc 文件会被保存,在一个 main.py Python 脚本 运行ning 的情况下,它们不会被保存。
Python 虚拟机将字节码解释为硬件特定的机器码。
在这里找到的一个很好的答案 说 Python 虚拟机与 JVM 相比 运行 它的字节码需要更长的时间,因为 java 字节码包含有关数据的信息类型,而 Python 虚拟机逐行解释,并且必须确定数据类型。
我的问题是 Python 虚拟机如何确定数据类型,它是在解释机器代码期间还是在单独的进程(例如会产生另一个中间代码)期间发生?
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 解释器检查 x
和 y
是否定义了 __mul__
方法,并尝试 "make it work" 和 return结果。此外,Python 字节码永远不会被翻译成机器码。它在 CPython 解释器中执行。 JVM 和 CPython 虚拟机之间的一个主要区别是 JVM 可以随时将 Java 字节码编译为机器码以提高性能(JIT 编译),而 CPython VM 只按原样运行字节码。
CPython 的动态 运行 时间调度(与 Java 的静态编译时调度相比)只是原因之一,为什么 Java 比纯 CPython 更快:Java 中有 jit-compilation,不同的垃圾收集策略,int
、double
等原生类型与不可变数据的存在CPython 等中的结构。
我之前的 表明,动态调度只负责 运行ning 的大约 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
我们可以看到在字节码级别上,x
和 y
是整数、浮点数还是其他东西没有区别——解释器不关心。
Java的情况完全不同:
int add(int x, int y) {return x+y;}
和
float add(float x, float y) {return x+y;}
会导致完全不同的操作码,并且调用调度会在编译时发生 - 根据编译时已知的静态类型选择正确的版本。
CPython 解释器通常不需要知道参数的确切类型:内部有一个基数 "class/interface"(显然 C 中没有 类,所以它被称为 "protocol",但对于了解 C++/Java 的人来说,"interface" 可能是正确的心智模型),所有其他 "classes" 都是从中派生的。此基数 "class" 称为 PyObject
和 here is the description of its protocol.。因此,只要该函数是此 protocol/interface 的一部分,CPython 解释器就可以调用它,而无需知道确切的类型,并且调用将被分派到正确的实现(很像 "virtual" C++ 中的函数)。
在纯粹的 Python 方面,似乎变量没有类型:
a=1
a="1"
然而,在内部 a
有一个类型 - 它是 PyObject*
并且这个引用可以绑定到一个整数 (1
) 和一个 unicode 字符串 ("1"
) - 因为他们都 "inherit" 来自 PyObject
.
CPython 解释器有时会尝试找出引用的正确类型,对于上面的示例也是如此 - 当它看到 BINARY_ADD
-opcode 时,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 对象,请参阅此) 被使用,否则工作被分派到 PyNumber
-protocol.
显然,解释器还必须知道创建对象时的确切类型,例如 a="1"
或 a=1
使用不同的 "classes" - 但正如我们所见它不是唯一的一个地方。
因此解释器会在 运行 时间内干扰类型,但大多数时候它不必这样做 - 可以通过动态调度达到目标。
避免在 Python 中想到 "variables" 可能有助于您的理解。与必须将类型与变量、class 成员或函数参数相关联的静态类型语言相比,Python 仅处理 "labels" 或对象名称。
因此在代码段中,
a = "a string"
a = 5 # a number
a = MyClass() # an object of type MyClass
标签 a
从来没有类型。它只是一个名称,在不同的时间指向不同的对象(实际上非常类似于其他语言中的 "pointers")。另一方面,对象(字符串、数字)总是有一个类型。这种类型的性质可能会改变,因为您可以动态更改 class 的定义,但它始终是确定的,即由语言解释器知道。
所以要回答这个问题:Python 永远不会确定变量的类型 (label/name),它只用它来引用一个对象并且该对象有一个类型。
我试图弄清楚 Python 3(使用 CPython 作为解释器)是如何执行它的程序的。我发现步骤是:
CPython 编译器将 Python 源代码(.py 文件)编译为 Python 字节码 (.pyc) 文件。在导入任何模块的情况下,.pyc 文件会被保存,在一个 main.py Python 脚本 运行ning 的情况下,它们不会被保存。
Python 虚拟机将字节码解释为硬件特定的机器码。
在这里找到的一个很好的答案 说 Python 虚拟机与 JVM 相比 运行 它的字节码需要更长的时间,因为 java 字节码包含有关数据的信息类型,而 Python 虚拟机逐行解释,并且必须确定数据类型。
我的问题是 Python 虚拟机如何确定数据类型,它是在解释机器代码期间还是在单独的进程(例如会产生另一个中间代码)期间发生?
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 解释器检查 x
和 y
是否定义了 __mul__
方法,并尝试 "make it work" 和 return结果。此外,Python 字节码永远不会被翻译成机器码。它在 CPython 解释器中执行。 JVM 和 CPython 虚拟机之间的一个主要区别是 JVM 可以随时将 Java 字节码编译为机器码以提高性能(JIT 编译),而 CPython VM 只按原样运行字节码。
CPython 的动态 运行 时间调度(与 Java 的静态编译时调度相比)只是原因之一,为什么 Java 比纯 CPython 更快:Java 中有 jit-compilation,不同的垃圾收集策略,int
、double
等原生类型与不可变数据的存在CPython 等中的结构。
我之前的
为了让这个答案不那么抽象,我们来看一个例子:
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
我们可以看到在字节码级别上,x
和 y
是整数、浮点数还是其他东西没有区别——解释器不关心。
Java的情况完全不同:
int add(int x, int y) {return x+y;}
和
float add(float x, float y) {return x+y;}
会导致完全不同的操作码,并且调用调度会在编译时发生 - 根据编译时已知的静态类型选择正确的版本。
CPython 解释器通常不需要知道参数的确切类型:内部有一个基数 "class/interface"(显然 C 中没有 类,所以它被称为 "protocol",但对于了解 C++/Java 的人来说,"interface" 可能是正确的心智模型),所有其他 "classes" 都是从中派生的。此基数 "class" 称为 PyObject
和 here is the description of its protocol.。因此,只要该函数是此 protocol/interface 的一部分,CPython 解释器就可以调用它,而无需知道确切的类型,并且调用将被分派到正确的实现(很像 "virtual" C++ 中的函数)。
在纯粹的 Python 方面,似乎变量没有类型:
a=1
a="1"
然而,在内部 a
有一个类型 - 它是 PyObject*
并且这个引用可以绑定到一个整数 (1
) 和一个 unicode 字符串 ("1"
) - 因为他们都 "inherit" 来自 PyObject
.
CPython 解释器有时会尝试找出引用的正确类型,对于上面的示例也是如此 - 当它看到 BINARY_ADD
-opcode 时,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 对象,请参阅此PyNumber
-protocol.
显然,解释器还必须知道创建对象时的确切类型,例如 a="1"
或 a=1
使用不同的 "classes" - 但正如我们所见它不是唯一的一个地方。
因此解释器会在 运行 时间内干扰类型,但大多数时候它不必这样做 - 可以通过动态调度达到目标。
避免在 Python 中想到 "variables" 可能有助于您的理解。与必须将类型与变量、class 成员或函数参数相关联的静态类型语言相比,Python 仅处理 "labels" 或对象名称。
因此在代码段中,
a = "a string"
a = 5 # a number
a = MyClass() # an object of type MyClass
标签 a
从来没有类型。它只是一个名称,在不同的时间指向不同的对象(实际上非常类似于其他语言中的 "pointers")。另一方面,对象(字符串、数字)总是有一个类型。这种类型的性质可能会改变,因为您可以动态更改 class 的定义,但它始终是确定的,即由语言解释器知道。
所以要回答这个问题:Python 永远不会确定变量的类型 (label/name),它只用它来引用一个对象并且该对象有一个类型。