Code Object
★ 该篇博客讲一个硬核的东西 - code object
这个东西对于你理解Python的底层机制非常重要!
# f.__code__
我们写的所有Python代码在运行的时候都会被编译成code object.
何为编译,简单理解:
将需用到的py源代码/需执行的py文件 --(进行编译)-- 字节码 --(通过python的PVM虚拟机逐行解释)
我们来看看下面这个例子:
def f():
pass
if __name__ == '__main__':
# <code object f at 0x10b08cbe0, file "/Users/dengchuan/Desktop/pythonProject/default_param.py", line 1>
print(f.__code__)
2
3
4
5
6
7
在装饰器那我们提到过:
当我们在python中所谓定义一个函数的时候,我们只是新建了一个变量,
然后这个变量里面保存了一个函数对象,也就是 function object
!
也就是说, 该程序中 我们 define(定义) 的空函数f, 它本身就是一个 function object.
每一个function object 都会有它对应的一个 code object.
我们通过打印 print(f.__code__)
, 就可以拿到 f这个function object里的code object.
# Code object里有什么
Q: 好, 那么, 这个code object中保存了什么呢?
我们可以看一下inspect模块的官方文档, 里面介绍了code object里有的attribute.
https://docs.python.org/zh-cn/3.10/library/inspect.html
当然, 除了文档以外, 你也可以在程序中, 通过 dir(f.__code__)
也能拿到.
['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__','__lt__', '__ne__', '__new__',
'__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__',
'co_argcount', 'co_cellvars', 'co_code', 'co_consts', 'co_filename', 'co_firstlineno', 'co_flags', 'co_freevars', 'co_kwonlyargcount', 'co_lnotab', 'co_name', 'co_names', 'co_nlocals', 'co_posonlyargcount', 'co_stacksize', 'co_varnames', 'replace']
2
3
可以看到 attribute 的部分是一样的, 我们将这些attribute分分类. 分别讨论下它们都是干什么的.
# co_code
首先是code object里的 co_code 这个 attribute.
属性 | 作用 |
---|---|
co_code | 保存着这一段代码 真正的, 用 binary(二进制) 表示的 byte code(字节码). |
def f():
pass
if __name__ == '__main__':
code = f.__code__
print(code.co_code) # b'd\x00S\x00'
2
3
4
5
6
7
这个attribute我们一般不会去直接接触, 通常我们会通过dis模块去查看它对应的 我们人类认识的byte code(字节码).
注: 现阶段, 不必纠结 这些字节码代表什么意思. 真的想了解的话, 去dis模块的官方文档查看.
只是想说, 当我们这段程序的运行超出了我们的认知的话, 可以看字节码的运行过程进行剖析. 这是一种解决途径.
# metadata相关
接下来的三个属性, 跟metadata相关. 也就是说, 它们跟真正的代码运行关系并不大, 它们是作为辅助数据出现的.
属性 | 作用 |
---|---|
co_name | 这段code的名字,一般就是你定义函数的名字. |
co_filename | 这段code是在哪个文件里被定义的. |
co_lnotab | 它保存的是一个mapping(映射),是一一对应的. 它将每一个byte code(字节码) 对应的cpython源代码 的行数 以二进制的形式进行了保存. |
def f():
pass
if __name__ == '__main__':
code = f.__code__
print(code.co_name) # f
print(code.co_filename) # /Users/dengchuan/Desktop/pythonProject/demo.py
print(code.co_lnotab) # b'\x00\x01'
2
3
4
5
6
7
8
9
10
# 运行时需要的
接下来, co_flags 和 co_stacksize 都是python 在 run time(运行时), Python virtual machine(虚拟机) 需要的一些数据.
属性 | 作用 |
---|---|
co_flags | 它是一个 bit map(位图) |
co_stacksize | 表明这段代码需要的栈的空间是多大 |
co_flags 简单来说, 就是在编译的时候, 会去判断下这个code有没有什么特别的属性.
比如有无 *args
、**kwargs
, 是不是一个generator(生成器)、coroutine(协程) 等.
通过这些flags呢, python在运行这些代码的时候, 就可能有不同的 behavior(行为)..
比如, python在运行一个生成器函数和普通函数的时候,肯定是不一样的.
def f():
pass
if __name__ == '__main__':
code = f.__code__
print(code.co_flags) # 67
print(code.co_stacksize) # 1
2
3
4
5
6
7
8
9
# 关于输入参数的
接下来这部分就比较重要了, 是关于 输入参数的.
这些属性决定了 这个python在往里面传入数据的时候, 怎么处理这些参数的. 它们也是python函数进行重载的一个基础.
属性 | 作用 |
---|---|
co_argcount | 参数的个数 但参数不包括 *args **kwargs keyword only argument |
co_posonlyargcount | positional only argument 的个数. / 斜杠前面的参数都是 positional only argument |
co_kwonlyargcount | keyword only argument 的个数* 星号后面的参数都是 keyword only argument |
那么我们理解的关键在于, 什么是 positional only 什么是 keyword only ?!
先回顾一点知识:
def f(a, b):
return a + b
2
针对上面程序的形参a和b, 你爱怎么传就怎么传, "但得保证 位置实参在关键字实参前面."
f(1, 2)
f(a=1, b=2)
f(1, b=2)
# f(a=1, 2) 报错: SyntaxError: positional argument follows keyword argument
2
3
4
再来看一段程序, 我们大部分人在写python程序的时候, argument(参数) 无非就写成这样.
def f(a, b=3, *args, **kwargs):
pass
if __name__ == '__main__':
code = f.__code__
print(code.co_argcount)
print(code.co_posonlyargcount)
print(code.co_kwonlyargcount)
2
3
4
5
6
7
8
9
10
上述程序中的f函数的参数 a, b=3, *args, **kwargs
, 既不是 positional only 也不是 keyword only !!
不信的话, 我们可以跑一下上面的程序瞅瞅.
2
0
0
2
3
那 positional only argument 到底是什么呢? 我们来看看下面这段代码.
def f(a, b=3, /, *args, **kwargs):
print(a + b)
if __name__ == '__main__':
code = f.__code__
print(code.co_argcount) # 2
print(code.co_posonlyargcount) # 2
print(code.co_kwonlyargcount) # 0
2
3
4
5
6
7
8
9
10
我们在 参数里加入了斜杠 /
, 它表明 斜杠 "前面" 的参数 必须都是通过 positional(位置) 传参的形式传递进来.
也就是说 斜杠前面的参数都是 positional only 的!
我们对上述程序, 进行测试
f(3, 2) # 5
f(3) # 6
# ★ f(3, b=1) ※虽然这样不报错,但你会发现最后结果是6 b=1是不生效的!哈哈哈哈 要特别注意哦~
# f(a=3) 报错: TypeError: f() missing 1 required positional argument:'a'`(缺少一个必要的位置参数a)
2
3
4
知道了 positional only, 那 keyword only 是什么呢? 我们再来看看下面这段代码.
def f(a, *, b=3, **kwargs):
print(a + b)
if __name__ == '__main__':
code = f.__code__
print(code.co_argcount) # 1
print(code.co_posonlyargcount) # 0
print(code.co_kwonlyargcount) # 1
2
3
4
5
6
7
8
9
10
我们在 参数里加入了星号 *
, 它表明 星号 "后面" 的变量 必须都是通过 keyword(关键字) 传参的形式传递进来.
也就是说 星号后面的参数都是 keyword only 的!
Ps: *
前面的, 爱怎么传就怎么传, 不管. 只要保证 位置实参在关键字实参前面 就行.
f(a=2)
f(2)
f(2, b=4)
# f(2, 4) 报错: TypeError: f() takes 1 positional argument but 2 were given
2
3
4
接下来, 到了最复杂, 最容易混淆的, 世界上没有几个人能明白的, 这一大串的name啦!(´・Д・)」
先思考一个东西, byte code 应该如何设计?
后面的我就听不懂了.. 暂略. ╮( ̄▽ ̄"")╭