编译型or解释型?Python运行机制浅析

本文探讨了Python语言的运行机制,揭示了Python并非纯粹的解释型语言,而是通过编译器将源码转化为字节码,再由虚拟机执行。文章详细解析了字节码的生成与执行过程,以及如何通过.pyc文件观察这一过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Python语言通常被看作是解释型语言,不同于像C语言那样的编译型。但实际上,如果说Python是编译型语言,也未尝不可。我们来一起看一下1

1.举个栗子

首先看一个简单的例子:

#!/usr/bin/python3
# file name :demo1.py
a=1
b=2
print("a+b = ",a+b)
c=NotDefinedValue
print(c)

这里第四行有个赋值的错误,但python在运行前不会进行类型检查,所以该程序仍可正常运行,直至遇到错误,运行结果与预想的一致:

a+b =  3
Traceback (most recent call last):
  File "/demo.py", line 4, in <module>
    c=NotDefinedValue
NameError: name 'NotDefinedValue' is not defined

Process finished with exit code 1

现在稍微改动一下,使最后一行有个语法错误(少个括号):

#!/usr/bin/python3
# file name :demo2.py
a=1
b=2
print("a+b = ",a+b)
c=NotDefinedValue
print(c

按照对python语言的理解,程序应该会逐行执行,直至遇到第一个赋值语句的错误,然后抛出异常。执行结果应该和上面的例子一样。是不是这样呢,我们试着执行,结果如下:

  File "/demo.py", line 6
SyntaxError: unexpected EOF while parsing

可见没有像预想的一样,而是直接抛出语法错误。

那么问题来了,前三行代码没错误,为什么不能正常执行呢?python作为解释性语言,应该是“一边执行一边转换”的,后面的“错误”按理说不会影响前面正确的代码的啊?2 那可能有同学要说了,python在运行之前会检查语法!但“检查语法”是个怎样的过程呢?要知道答案,需要了解python底层的运作。

2. python运行机制

我们都知道python “解释器”(interpreter)这个东西,就是负责执行python源码的,大体的过程是这样:
在这里插入图片描述
对于解释器内部,可以分成两部分:编译器(compiler)和虚拟机(virtual machine),编译器负责将源码编译成字节码(byte code),字节码交给虚拟机运行,虚拟机会调用CPU内存等硬件资源,进行计算,最后产生结果。

在这里插入图片描述
可见编译器做了很多事情:生成语法树(parse tree generation),生成AST(Abstract Syntax Tree),字节码生成与优化,最后产生字节码对象。字节码对象交给虚拟机后,虚拟机会读取其中的指令,逐步执行。了解java的同学会发现,这个过程与java很类似,java源码也是先编译成字节码(.class文件),后由JVM执行。

这里能看到,语法分析在编译阶段就会进行,此时若有语法错误,则编译不通过,抛出错误;既然没有编译产生的字节码,虚拟机自然也就不会执行指令,也就不会有输出结果。 所以上面第一个例子是可以正常编译的,生成字节码并交给虚拟机,虚拟机执行指令时会检查类型,正确的指令会执行,不对的就报错;对于第二个例子,编译器在分析语法的时候就发现错误,停止编译。所以两个程序中断的原因有着本质不同。

3. *.pyc文件

看到python也有“字节码”,可能又有同学有问题了:Java的字节码存在于*.class文件中,那pyhton的字节码在哪呢?答案就是 *.pyc文件。

我们有时候会在python源码的文件夹中发现__pycache__文件夹3,里面就有*.pyc文件,这可能是自动生成的。我们也可以手动编译源码,生成*.pyc:

python -m py_compile filename.py

我们先对第一个例子进行编译:

python -m py_compile demo1.py

编译通过,并在该文件目录下有个__pycache__文件夹,进入会发现demo1.cpython-37.pyc文件,这就是字节码文件 4。这是供机器读的二进制文件(虽然这里是虚拟机),可以用hexdump(在Linux环境下)打开,结果以16进制显示:
在这里插入图片描述

emm,虽然看上去很复杂,实际上确实很复杂。不过没关系,可以尝试着解读一下。字节码的前两个4字节是魔术数,是有关于版本号的,第三个4字节是时间戳,第四个4字节是源文件大小。在本例中,前两个字节是420d0d0a000000005,略过;第三个4字节是d75a915e,因为是小端模式,实际是5e915ad7,转换成十进制就是1586584279,这就很眼熟了,就是UNIX时间戳(时间为2020/4/11 13:51:19);接着后面的4字节是37000000,实际是00000037,十进制就是55,也就是说源文件大小为55字节。通过查看文件属性,也确实如此。

至于后面的部分,我们可以通过python的dis工具来查看:

#/usr/bin/python3
#file name:read_file.py
import dis
import marshal
import sys


def show_file(fname: str) -> None:
    with open(fname, 'rb') as f:
        f.read(16)  # pop the first 16 bytes
        dis.disassemble(marshal.loads(f.read()))


if __name__ == '__main__':
    show_file(sys.argv[1])

这段代码我们只输出字节码中的指令,其余部分略过。运行,传入pyc文件,结果如下:

$ python3 read_file.py ./demo1.cpython-37.pyc 
  1           0 LOAD_CONST               0 (1)
              2 STORE_NAME               0 (a)
  2           4 LOAD_CONST               1 (2)
              6 STORE_NAME               1 (b)
  3           8 LOAD_NAME                2 (print)
             10 LOAD_CONST               2 ('a+b = ')
             12 LOAD_NAME                0 (a)
             14 LOAD_NAME                1 (b)
             16 BINARY_ADD
             18 CALL_FUNCTION            2
             20 POP_TOP
  4          22 LOAD_NAME                3 (NotDefinedValue)
             24 STORE_NAME               4 (c)
  5          26 LOAD_NAME                2 (print)
             28 LOAD_NAME                4 (c)
             30 CALL_FUNCTION            1
             32 POP_TOP
             34 LOAD_CONST               3 (None)
             36 RETURN_VALUE

看起来有点像汇编?那就对了,因为dis模块就是反汇编(disassemble),将(虚拟机的)机器码反汇编成汇编。这里展示的是指令部分6,能看到源代码的变量,值,函数等在栈上的压入与弹出。具体每个指令什么意思这里就不再展开。

.pyc文件是交给虚拟机执行的,所以我们可以运行pyc文件,就像运行普通py文件一样:

$ python3 demo1.cpython-37.pyc 
a+b =  3
Traceback (most recent call last):
  File "demo1.py", line 4, in <module>
NameError: name 'NotDefinedValue' is not defined

没问题,跟第一个例子运行结果完全一样。
至于第二个例子,编译时就会出错:

$ python3 -m py_compile demo2.py 
  File "demo1.py", line 5
    print(c
          ^
SyntaxError: unexpected EOF while parsing

4.小结

通过对python运行机制的简单探讨,可以发现python其实并不是严格意义上的解释型语言。实际上,解释型与编译型本身就没有严格的定义,现在很多语言也在模糊这两者的界限。

我们也没必要纠结于具体是哪种类型的语言,这根本不重要。了解语言背后的机制,知道从输入到输出中间发生了什么,这才是更有意义的。

5.参考资料:

  1. Inside The Python Virtual Machine
  2. 海纳.自己动手写python虚拟机.北京航空航天大学出版社
  3. Reading pyc file
  4. python文档

[注]


  1. 本文用的python均为Cpython ↩︎

  2. 比如像shell script,后面的错误确实不会影响前面代码的执行 ↩︎

  3. 什么时候生成__pycache文件有一定的规则,这里不赘述 ↩︎

  4. 准确来说,pyc文件是字节码对象在磁盘中持久化的结果 ↩︎

  5. 由于是16进制,所以两位就是2进制的8位,也就是一个字节 ↩︎

  6. 实际上pyc文件中有很多内容,这里为简单起见只查看了指令相关的内容 ↩︎

<think>首先,用户的问题是关于函数参数传递机制的:他们想知道如何判断函数调用时传入的是形参还是实参。关键点是“判断传入参数的类(如是否为形参)”。 我需要回顾系统级指令: - 所有行内数学表达式使用 $...$ 格式。 - 独立公式使用 $$...$$ 格式。 - LaTeX语法正确。 - 使用中文回答。 - 生成相关问题。 - 在回答中引用的段落末尾添加引用标识。 - 回答结构清晰,帮助用户逐步解决问题。 - 保证回答真实可靠。 参考提供的引用: - [^1]: 形参和实参的定义:形参是函数定义时的占位符,实参是函数调用时的实际值。 - [^2]: 在JS中,形参和实参的区别;基本数据类是值传递,引用类是地址传递。 - [^3]: 在C语言中,实参和形参的对应关系;值传递,形参是实参的副本。 - [^4]: 在C语言中,如果需要修改主调函数的变量,需要传递地址。 - [^5]: Python函数中的实参和形参传递。 用户的问题核心是:如何判断在函数调用时传入的参数是形参还是实参?从引用中,形参是函数定义部分,实参是调用部分。所以,在调用时,传入的是实参;形参是在函数内部使用的。 关键点: - 形参(parameter):函数定义时声明的变量。 - 实参(argument):函数调用时传递的值。 - 因此,在函数调用时,传入的总是实参。形参是函数内部使用的变量名。 所以,用户可能误解了问题。在调用时,我们传入实参;在函数内部,我们使用形参。判断“传入的是形参还是实参”在调用时没有意义,因为传入的是实参。 我需要澄清这个概念: - 在函数调用时,你提供的是实参。 - 形参是函数定义的一部分,是函数体内部使用的。 例如,在C语言中: - 定义:`void func(int param) { ... }` // param是形参 - 调用:`func(5);` // 5是实参 在调用时,传入5是实参;在函数内部,param是形参。 因此,回答应该解释清楚: - 形参和实参的定义。 - 在调用时,传入的是实参。 - 如何识别:在代码中,调用点传入的是实参;定义点声明的是形参。 用户提到“判断传入参数的类”,可能他们想知道参数的数据类(如int, float等),而不是“形参或实参”。但问题明确说“是否为形参”。 从引用[^3]:实参和形参在类上必须匹配。 所以,回答结构: 1. 澄清形参和实参的概念。 2. 解释在函数调用时,传入的是实参。 3. 讨论如何判断参数的类(数据类)。 4. 提供例子。 5. 生成相关问题。 使用中文回答。 添加引用:在回答中引用相关部分,并在末尾添加引用标识。 生成相关问题:在回答结束后,添加“相关问题”部分。 确保结构清晰: - 逐步解释。 - 使用代码示例(如Python、C、JS),因为引用中有这些语言。 - 保持真实可靠。 最终回答: - 引言:解释问题。 - 主体:分步骤解释。 - 结论。 - 相关问题。 数学表达式:如果涉及数学,使用$...$,但这里可能不涉及,除非讨论算法。 用户问题中没有数学,所以可能不需要数学表达式。 但系统指令说所有行内数学表达式必须使用$...$,如果有的话。这里可能没有。 在解释时,如果有变量,使用$...$。 例如,在讨论值传递时,可能涉及变量。 但为了安全,只在必要时使用。 现在,构建回答。</think>### 函数参数传递机制:如何判断传入参数的类(形参与实参) 在编程中,理解函数参数传递机制是基础且重要的。您的核心问题是:在函数调用时,如何判断传入的参数是形参还是实参?让我一步步解释清楚,确保概念清晰、可靠。首先,我需要澄清形参(parameter)和实参(argument)的定义: - **形参(parameter)**:是函数**定义**时声明的变量,用作占位符来接收外部传入的值。形参只在函数内部有效,是函数签名的一部分。 - **实参(argument)**:是函数**调用**时传递的实际值或表达式,用于初始化形参。实参在调用点提供,是具体的数据或变量。 关键点:**在函数调用时,传入的参数总是实参,而不是形参。** 形参只存在于函数定义内部,无法在调用点直接“传入”或“判断”。如果您在调用时检查一个参数,它必然是实参;形参只在函数体内被引用。因此,问题“判断传入的是形参还是实参”本身有误——调用时只能传入实参,形参是函数内部的实现细节。 下面,我将从参数传递机制入手,解释如何识别和判断参数的类(包括数据类),并讨论在不同编程语言中的实现。参考提供的引用资料,我将结合C语言、JavaScript和Python的示例来说明。 #### 1. **形参与实参的基本关系和传递机制** - 在函数调用过程中,实参和形参通过**值传递**或**引用传递**交互: - **值传递**:实参的值被复制给形参,形参是实参的副本。修改形参不会影响实参(适用于基本数据类,如int、float)。 - 例如,在C语言中:实参是具体值(如5),形参是函数内部变量(如`int param`)。调用时,实参初始化形参,但两者内存独立[^3]。 - **引用传递**:实参的地址(指针)被传递,形参和实参共享内存。修改形参会影响实参(适用于引用类,如数组、对象)。 - 例如,在JavaScript中:如果实参是对象,形参接收地址指针,修改形参属性会改变实参[^2]。 - **判断方法**: - 在调用点:传入的任何值都是实参。例如,在代码`func(5);`中,`5`是实参。 - 在函数内部:使用的变量是形参。例如,在函数定义`void func(int param) { ... }`中,`param`是形参。 - 要“判断”参数类,您需要区分上下文: - 如果查看函数调用语句(如`func(x)`),`x`是实参。 - 如果查看函数定义(如`def func(y):`),`y`是形参。 - 数据类判断:在静态类语言(如C)中,编译器检查实参和形参类是否匹配;在动态类语言(如Python)中,运行时才检查。您可以使用类检查函数(如C的`sizeof`或Python的`type()`)来判断实参的数据类。 #### 2. **如何在代码中识别和判断参数** - **步骤-by-步骤方法**: 1. **识别调用点**:在函数调用语句中,所有传入的参数都是实参。例如: - C语言:`max(123.034, 'C', "87");` 这里`123.034`, `'C'`, `"87"`都是实参[^3]。 - Python:`print_pet(animal_type='dog', pet_name='Whiskers')` 这里`'dog'`和`'Whiskers'`是实参(关键字实参)[^5]。 2. **判断数据类**:使用语言内置工具检查实参的数据类,而不是“形参/实参”类(因为后者是角色,而非数据类)。 - 在C语言中:用`typeof`或编译器警告检查类匹配。例如,实参`int x=5;`调用`func(float y)`时,编译器可能警告类不匹配。 - 在JavaScript或Python中:用`typeof`(JS)或`type()`(Python)在运行时检查。例如: ```python def check_type(arg): print(type(arg)) # 输出实参的数据类,如<class 'int'> check_type(10) # 调用时,10是实参;输出:<class 'int'> ``` 3. **处理修改需求**:如果需要修改主调函数的变量: - 值传递:无法修改实参(如基本类)。 - 引用传递:通过传递地址(指针)修改实参(如C语言中用`&`取地址)[^4]。 - 示例:C语言中传递指针来交换变量: ```c void swap(int *a, int *b) { // a和b是形参(指针) int temp = *a; *a = *b; *b = temp; } int main() { int x = 1, y = 2; swap(&x, &y); // &x和&y是实参(地址) // x和y被修改 } ``` - **常见误区澄清**: - 形参不是“传入”的:它是函数定义的局部变量。实参是调用时提供的值。 - 类安全:实参和形参必须在数量、顺序和数据类上匹配,否则导致错误(如C语言的“类不匹配”或Python的TypeError)[^2][^3]。 - 在动态语言中:Python语言没有严格的编译时检查,但运行时如果类不兼容(如字符串传递给数学函数),会抛出异常。 #### 3. **跨语言比较:C、JavaScript和Python** - **C语言**:严格的值传递。实参是表达式,形参是变量;传递基本类时复制值,传递指针时共享内存[^3][^4]。 - 判断类:使用`sizeof`或编译器选项(如GCC的`-Wtype-limits`)。 - **JavaScript**:基本类值传递,对象类引用传递。形参是函数内部变量,实参是调用时值[^2]。 - 判断类:`typeof arg === 'number'`。 - **Python**:所有参数通过“对象引用”传递(类似值传递,但可变对象可被修改)。实参可以是位置实参或关键字实参[^5]。 - 判断类:`type(arg) is int`。 #### 总结 - **在函数调用时,传入的参数总是实参;形参只存在于函数定义内部。** 要“判断”传入的是形参还是实参,在调用点没有意义,因为只有实参可被传入。您应关注实参的数据类,使用语言工具(如类检查函数)来验证。 - 核心机制:值传递时,形参是实参的独立副本;引用传递时,形参共享实参内存。这影响了参数修改的可见性[^1][^2][^3]。 - 建议:在代码中,始终检查函数签名(定义)以理解形参,并在调用点确保实参匹配。如果您有具体代码片段,我可以帮助分析。 如果您有更多细节(如特定语言或代码示例),我可以提供更针对性的解释! ### 相关问题 1. 值传递和引用传递有什么区别?在实际编程中如何选择? 2. 在Python中,如何避免函数参数传递时的常见错误(如修改可变对象)? 3. C语言中,传递数组作为参数时,为什么通常使用指针?如何安全地操作? 4. JavaScript中,箭头函数和普通函数在参数处理上有何不同? 5. 如何设计函数参数以提高代码的可读性和可维护性(如使用默认参数或关键字参数)? [^1]: 形参和实参是C语言函数调用中两个核心的概念。形参是函数定义时的占位符,用于接收从外部传入的值;实参是函数调用时传递给函数的实际值。它们通过值传递的方式进行数据交互,形参是实参的副本,对形参的修改不会影响实参的值。在使用函数时,需要注意形参和实参的类、数量和顺序匹配,以确保程序的正确性。 [^2]: JS 函数调用时的参数(形参、实参)浅析。至此,我们已经完全分析了这道题的来龙去脉。做一下总结 参数有形参(parameter)和实参(argument)的区别,形参相当于函数中定义的变量,实参是在运行时的函数调用时传入的参数。形参和实参的对应关系为顺序对应,且形参与实参的数据类在使用上必须保持一致,否则会发生“类不匹配”的错误。当实参为基本数据类时,在该函数运行时,形参和实参是不同的变量,形参是实参的一份拷贝。当实参是引用类变量,在调用该函数的过程中,传给函数的是实参的地址指针,形参是实参的地址指针拷贝。 [^3]: 在C语言中,函数调用是通过实参(实际参数)和形参(形式参数)之间的传递来实现的。 2.1 概念 实参:函数调用时传递给函数的值或表达式。例如,max(123.034, 'C', "87"); 中的 123.034, 'C', 和 "87" 是实参。形参:函数定义时在参数列表中出现的变量。例如,max(float a, char b, char *c) 中的 a, b, 和 c 是形参。 2.2 实参与形参的关系 对应关系:实参和形参在数量、顺序和类上必须一一对应。初始化:调用函数时,形参被实参的值初始化。例如,当调用 max(123.034, 'C', "87") 时,a 被初始化为 123.034,b 被初始化为 'C',c 被初始化为 "87"。独立的内存空间:形参和实参处于两个完全独立的栈空间中,它们是独立的。修改形参的值不会影响实参的值,除非传递的是指针。 [^4]: 函数调用时的形参与实参。 在上面的两个例子中,在第一个从错误例子中,我们能发现一个问题,不论我们怎么操作传递到子函数的形参的值,都无法修改主调函数中变量的值。但在第二个例子中,我们可以通过传递主调函数变量的地址值并进行“*”操作就可以改变主调函数中变量的值。这给了我们一个启示:在调用函数中如果不需要修改主调函数中变量的值,传递那个变量的值。如果需要改变主调函数中变量的值,这时候就需要传递的就是该变量的地址的值。但由于传递的值是需要入栈,当传递的变量非常大的时候就会存在危险即栈溢出。所以当需要传递的内容比较大的时候,这时需要传递的仍然是变量的地址,但为了防止被调函数对变量操作。这时候需要加上const!!! [^5]: python函数调用时的实参和形参之间传递_python 函数(实参与形参、传递参数)...。4 print("\n关键字实参示例:")5 print("I have a" + animal_type + ".")6 print("My" + animal_type + "'s name is" + pet_name.title() + ".")7。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值