python方法调用名字不一样_从Python编译的角度看Python处理函数/方法定义与类定义时的不同行为...

1. 引言

最近在写Python程序时遇到了一个奇怪的问题。

本文中使用Python3(3.5.3)测试。

(1)定义两个类C1、C2,在C1的定义中使用后定义的C2的对象,会报错,因为C1定义时还没有定义C2:

class C1:

def __init__(self, ac=C2()):

print("Hello, I'm an object of C1")

class C2:

def __init__(self):

print("Hello, I'm an object of C2")

"""运行后报错:Traceback (most recent call last):File "test.py", line 1, in class C1:File "test.py", line 2, in C1def __init__(self, ac=C2()):NameError: name 'C2' is not defined"""

这比较容易理解,因为C2还没有定义,所以在C1的定义中使用时,会报未定义错误。

(2)然而,定义两个函数f1、f2,在f1的定义中使用后定义的f2,则顺利通过:

def f1():

f2()

def f2():

print("Hi, I'm f2!")

"""运行后程序顺利通过,未报告错误。"""

这是为什么呢?同样是引用未定义的对象(Python中函数是一等对象),类的定义中会报错,函数的定义中就不会。这里的原因主要在于Python在编译函数和编译类时的不同处理方式。

2. 我们都说Python是解释型语言,那么Python有编译过程吗?

答案是”当然有“。

(1)Python其实和Java/C#相似,也有一个虚拟机(解释器)。我们在运行.py文件时,Python虚拟机先把.py文件编译为PyCodeObject。而我们经常看到的.pyc文件就是内存中的PyCodeObject持久化到硬盘中的形式。

(2)我们平时写程序时发现有时候有.pyc文件有时候没有。因为Python解释器默认只是把我们可能重用到的模块持久化成pyc文件。比如说,我们写了一个模块,在别的Python文件引入(import)它的时候,就会将PyCodeObject持久化成.pyc文件,方便下次调用。

(3)加载模块时,如果同时存在.py和.pyc,Python会尝试使用.pyc,如果.pyc的编译时间早于.py的修改时间,则重新编译.py并更新.pyc。

(4)Python中有一个内置函数compile(),可以将源文件编译成codeobject,首先看这个函数的说明:

compile(...) compile(source, filename, mode[, flags[, dont_inherit]]) -> code object

参数1:源文件的内容字符串

参数2:源文件名称

参数3:exec-编译module,single-编译一个声明,eval-编译一个表达式 一般使用前三个参数就够了

使用示例:

# src.py

def f(a=0):

b = 1

print("Hi, I'm f!")

c = 2

d = 3

f()

# run.py

with open("src.py") as f:

str_src = f.read()

co = compile(str_src,'src','exec')

print(type(co))

print(co.co_names) # 所有的符号名称

print(co.co_name) # 模块名、函数名、类名

print(co.co_consts) # 常量集合、函数f和两个int常量a,b,d

print(co.co_consts[1].co_varnames) # 可以看到f函数也是一个PyCodeObject,打印f中的局部变量

print(co.co_consts[1].co_firstlineno) # 代码块在文件中的起始行号

print(co.co_stacksize) # 代码栈大小

print(co.co_filename) # 文件名

"""运行run.py的结果:('f', 'c', 'd')(0, , 'f', 2, 3, None)('a', 'b')13src"""

(5)PyCodeObject的co_code代表了字节码,这个字节码有什么含义?我们可以使用dis模块进行python的反编译:

# 续run.py

import dis

dis.dis(co)

"""输出结果:1 0 LOAD_CONST 0 (0)3 LOAD_CONST 1 ()6 LOAD_CONST 2 ('f')9 MAKE_FUNCTION 112 STORE_NAME 0 (f)5 15 LOAD_CONST 3 (2)18 STORE_NAME 1 (c)6 21 LOAD_CONST 4 (3)24 STORE_NAME 2 (d)7 27 LOAD_NAME 0 (f)30 CALL_FUNCTION 0 (0 positional, 0 keyword pair)33 POP_TOP34 LOAD_CONST 5 (None)37 RETURN_VALUE"""

由反编译结果来看,python字节码其实是模仿的x86的汇编。编译之后,解释器再对PyCodeObject解释执行。 输出编译结果各列含义:

第一列:行号

第二列:指令在代码块中的偏移量

第三列:指令

第四列:操作数

第五列:操作数说明

3. Python函数的定义与执行

当Python解释器遇到.py文件中的函数定义时,会将其编译为一个PyCodeObject。 在Python文档关于函数定义有:

函数定义是一个可执行语句。它的执行将当前本地命名空间中的函数名绑定到函数对象(函数可执行代码的包装器)。此函数对象包含对当前全局命名空间的引用,作为调用函数时要使用的全局命名空间。 函数定义不执行函数体;只有在调用函数时才会执行。

也就是说,Python在遇到函数定义时不会执行该函数,它只会将该函数编译成一个对象,该对象稍后可用于执行。python执行函数的唯一时刻是调用函数的时刻。 由于函数定义与执行的区别,在调用函数之前,Python无法验证函数中的变量是否已定义。因此,可以在函数体中使用当前不存在的名称。只要在调用函数时定义了名称,python就不会引发错误。 比如:

>>> def f():

... return a + b

...

我们可以看到,Python解释器没有报错。这是因为它只是简单地编译了f。它没有执行函数,所以不知道a和b没有定义。 我们反汇编f的PyCodeObject:

>>> from dis import dis

>>> dis(f)

2 0 LOAD_GLOBAL 0 (a)

3 LOAD_GLOBAL 1 (b)

6 BINARY_ADD

7 RETURN_VALUE

>>>

python在PyCodeObject中编码了两个LOAD_GLOBAL指令。指令的参数分别是变量名a和b。 这说明,python在编译函数时确实看到了我们试图引用两个变量,并创建了字节码指令来执行此操作。但在调用函数之前,它不会执行指令。 让我们看看当我们试图通过调用f来执行PyCodeObject时会发生什么:

>>> f()

Traceback (most recent call last):

File "", line 1, in

File "", line 2, in f

NameError: name 'a' is not defined

>>>

可以看到,python引发了一个NameError。这是因为它试图执行两个LOAD_GLOBAL指令,但发现全局范围内未定义的名称。 现在让我们看看如果在调用func之前定义a和b会发生什么:

>>> a = 1

>>> b = 5

>>> f()

6

>>>

可以看到,这时就不报错了。当python执行f的字节代码(PyCodeObject)时,它能够找到全局变量a和b,并使用这些变量来执行函数。

4. Python解释器对类定义的处理

python处理的类定义与函数定义稍有不同。 在Python文档关于类定义:

类的套件,使用新创建的本地命名空间和原始全局命名空间,在新的执行框架(参见命名和绑定)中执行。(通常,该套件主要包含函数定义。)当类的套件完成执行时,它的执行框架将被丢弃,但它的本地命名空间将被保存。

当Python遇到一个类定义时,它将为类创建一个与函数相似的PyCodeObject。但是,Python还允许类具有在类定义期间执行的名称空间(namespace)。Python立即执行类命名空间,因为类中已定义的任何变量都应属于该类。因此,类名称空间内使用的任何名称必须已定义,以便在类定义期间使用。

比如:

>>> class C1:

... print("Hello, I'm one of C1")

...

Hello, I'm one of C1

>>>

我们可以看到,Python解释器遇到类定义时,会在类定义期间执行的名称空间立即执行其中的可执行语句。 但是请注意,这并不适用于类方法。Python对待方法中未定义的名称和函数一样,允许在定义方法时使用未定义名称:

>>> class C2:

... def m1(self):

... return v

...

>>> v = 100

>>> c1 = C2()

>>> c1.

>>> c1.m1()

100

>>>

5. 结语

都说Python简单,其实若要是深究起来还是很有难度的。

6. 主要参考

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值