Python 中的代码对象 code object 与 code 属性**
代码对象 code object
是一段可执行的 Python
代码在 CPython
中的内部表示。
可执行的 Python
代码包括:
- 函数
- 模块
- 类
- 生成器表达式
当你运行一段代码时,它被解析并编译成代码对象,随后被 CPython 虚拟机执行。代码对象包含一系列直接操作虚拟机内部状态的指令。这跟你在用 C 语言编程时是类似的,你写出人类可读的文本,然后用编译器转换成二进制形式,二进制代码(C 的机器码或者是 Python 的字节码)被 CPU(对于 C 语言来说)或者 CPython 虚拟机虚拟的 CPU 直接执行。代码对象除了包含指令,还提供了虚拟机运行代码所需要的一些额外信息。
以下的内容是在Python 3.7
中实验的,而且主要是针对于函数来讲。至于模块和类虽然也是通过代码对象实现的(实际上,.pyc
文件里面就存放着序列化的模块代码对象),但是代码对象的大多数特性主要和函数相关。
关于版本需要注意两点:
在Python 2
中,函数的代码对象通过 函数.func_code
来访问;而 Python 3
中,则需要通过 函数.__code__
来访问。
Python 3
的代码对象增加了一个新属性co_kwonlyargcount
,对应强制关键字参数 keyword-only argument
。
首先在控制台找出属于 函数.__code__
的所有不以双下划线开头的属性,一共有 15
个。
>>> li = [i for i in dir((lambda: 0).__code__) if not i.startswith('__')]
>>> print(li)
['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_stacksize', 'co_varnames']
>>> len(li)
15
co_argcount
:函数接收参数的个数,不包括*args
和**kwargs
以及强制关键字参数。
>>> def test(a, b, c, d=1, e=2, *args, f=3, g, h=4, **kwargs):
... print(a, b, c, d, e, f, g, h, args, kwargs)
...
>>> code_obj = test.__code__
>>> code_obj.co_argcount
5
-
co_code
:二进制格式的字节码bytecode
,以字节串bytes
的形式存储(在Python 2
中以str
类型存储)。它为虚拟机提供一系列的指令。函数从第一条指令开始执行,在碰到RETURN_VALUE
指令的时候停止执行。 -
co_cellvars
和co_freevars
:这两个属性用来实现嵌套函数的作用域。co_cellvars
元组里面存储着所有被嵌套函数用到的变量名。co_freevars
元组里面存储着所有被函数使用的在闭包作用域中定义的变量名。 -
co_consts
:在函数中用到的所有常量,比如整数、字符串、布尔值等等。它会被LOAD_CONST
操作码使用,该操作码需要一个索引值作为参数,指明需要从co_consts
元组中加载哪一个元素。
我们看一个例子:
def f(a, b, *d, e):
c = 3
def g():
inner_x = x
return a + c
return g
f1 = f(1, 2, 3, 4, 5, e = 6)
print('闭包:', f1.__code__.co_cellvars)
print('闭包:', f1.__code__.co_freevars)
print('闭包:', f1.__code__.co_varnames)
print('闭包:', f1.__code__.co_consts)
print('外部:', f.__code__.co_cellvars)
print('外部:', f.__code__.co_freevars)
print('外部:', f.__code__.co_varnames)
print('外部:', f.__code__.co_consts)
print('外部:', f.__code__.co_consts[2].co_freevars)
运行结果:
闭包: ()
闭包: ('a', 'c')
闭包: ('inner_x',)
闭包: (None,)
外部: ('a', 'c')
外部: ()
外部: ('a', 'b', 'e', 'd', 'g')
外部: (None, 3, <code object g at 0x7fb3e2329780, file "<ipython-input-103-269e55cda931>", line 3>, 'f.<locals>.g')
外部: ('a', 'c')
co_filename
:代码对象所在的文件名。co_firstlineno
:代码对象的第一行位于所在文件的行号。co_flags
:这是一个整数,存放着函数的组合布尔标志位。可以在inspect
模块的文档中查看这些标志位的具体含义:Code Objects Bit Flagsco_lnotab
:这个属性是line number table
行号表的缩写。它以字节串bytes
的形式存储,每两个字节是一对,分别是co_code
字节串的偏移量和Python
行号的偏移量。具体参阅:lnotab_notes.txt
co_kwonlyargcount
:存放强制关键字参数的个数。co_name
:是与代码对象关联的对象的名字。co_names
:该属性是由字符串组成的元组,里面按照使用顺序存放了全局变量和被导入的名字。co_nlocals
:函数中局部变量的个数,相当于是co_varnames
的长度。co_stacksize
:一个整数,代表函数会使用的最大栈空间。co_varnames
:函数所有的局部变量名称(包括函数参数)组成的元组。
我们看一下应用:
global_A = 1
def test_func(a, b, c, d = 1, e = 2, *args, f = 3, g, h = 4, **kwargs):
a = global_A
def inner_finc(b, c):
return b + c
print(a, b, c, d, e, f, g, h, args, kwargs)
print(test_func.__code__.co_kwonlyargcount)
print(test_func.__code__.co_filename)
print(test_func.__code__.co_firstlineno)
print(test_func.__code__.co_lnotab)
print(test_func.__code__.co_flags)
print(test_func.__code__.co_name)
print(test_func.__code__.co_names)
print(test_func.__code__.co_varnames)
print(test_func.__code__.co_nlocals)
print(test_func.__code__.co_stacksize)
运行结果:
3
test.py
10
b'\x00\x01\x04\x01\x08\x02'
79
test_func
('global_A', 'print')
('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'args', 'kwargs', 'inner_finc')
11
11
python闭包
闭包并不只是一个python中的概念,在函数式编程语言中应用较为广泛。理解python中的闭包一方面是能够正确的使用闭包,另一方面可以好好体会和思考闭包的设计思想。
首先看一下维基上对闭包的解释:
在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。
根据我们对编程语言中函数的理解,大概印象中的函数是这样的:
程序被加载到内存执行时,函数定义的代码被存放在代码段中。函数被调用时,会在栈上创建其执行环境,也就是初始化其中定义的变量和外部传入的形参以便函数进行下一步的执行操作。当函数执行完成并返回函数结果后,函数栈帧便会被销毁掉。函数中的临时变量以及存储的中间计算结果都不会保留。下次调用时唯一发生变化的就是函数传入的形参可能会不一样。函数栈帧会重新初始化函数的执行环境。
C++中有static关键字,函数中的static关键字定义的变量独立于函数之外,而且会保留函数中值的变化。函数中使用的全局变量也有类似的性质。
但是闭包中引用的函数定义之外的变量是否可以这么理解呢?但是如果函数中引用的变量既不是全局的,也不是静态的(python中没有这个概念)。应该怎么正确的理解呢?
我们可以看一下以下代码:
def outer_func():
loc_list = []
def inner_func(name):
loc_list.append(len(loc_list) + 1)
print '%s loc_list = %s' %(name, loc_list)
return inner_func
clo_func_0 = outer_func()
clo_func_0('clo_func_0')
clo_func_0('clo_func_0')
clo_func_0('clo_func_0')
clo_func_1 = outer_func()
clo_func_1('clo_func_1')
clo_func_0('clo_func_0')
clo_func_1('clo_func_1')
运行结果:
clo_func_0 loc_list = [1]
clo_func_0 loc_list = [1, 2]
clo_func_0 loc_list = [1, 2, 3]
clo_func_1 loc_list = [1]
clo_func_0 loc_list = [1, 2, 3, 4]
clo_func_1 loc_list = [1, 2]
闭包的作用
自由变元可以记录闭包函数被调用的信息,以及闭包函数的一些计算结果中间值。而且被自由变量记录的值,在下次调用闭包函数时依旧有效。
根据闭包函数中引用的自由变量的一些特性,闭包的应用场景还是比较广泛的。后面会有文章介绍其应用场景之一——单例模式,限于篇幅,此处以装饰器为例介绍一下闭包的应用。
如果我们想对一个函数或者类进行修改重定义,最简单的方法就是直接修改其定义。但是这种做法的缺点也是显而易见的:
- 可能看不到函数或者类的定义
- 会破坏原来的定义,导致原来对类的引用不兼容
- 如果多人想在原来的基础上定制自己函数,很容易冲突
使用闭包可以相对简单的解决上面的问题,下面看一个例子:
def func_dec(func):
def wrapper(*args):
if len(args) == 2:
func(*args)
else:
print 'Error! Arguments = %s'%list(args)
return wrapper
@func_dec
def add_sum(*args):
print sum(args)
# add_sum = func_dec(add_sum)
args = range(1,3)
add_sum(*args)
对于上面的这个例子,并没有破坏add_sum函数的定义,只不过是对其进行了一层简单的封装。如果看不到函数的定义,也可以对函数对象进行封装,达到相同的效果(即上面注释掉的13行),而且装饰器是可以叠加使用的。
@functools.wraps的作用
闭包的缺点也是很明显的,那就是经过装饰器装饰的函数或者类不再是原来的函数或者类了。这也是使用装饰器改变函数或者类的行为与直接修改定义最根本的差别。
def counter(cls):
obj_list = []
def wrapper(*args, **kwargs):
new_obj = cls(*args, **kwargs)
obj_list.append(new_obj)
print("class:%s'object number is %d" % (cls.__name__, len(obj_list)))
return new_obj
return wrapper
@counter
class my_cls(object):
STATIC_MEM = 'This is a static member of my_cls'
def __init__(self, *args, **kwargs):
print(self, args, kwargs)
print(my_cls.__name__)
print(type(my_cls))
print(my_cls.STATIC_MEM)
my_cls(1, 2, key = 'lijinze')
运行结果:
<__main__.my_cls object at 0x7fb3e4e26048> (1, 2) {'key': 'lijinze'}
wrapper
<class 'function'>
-----------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-121-a1198eb8b12b> in <module>
17 print(my_cls.STATIC_MEM)
18
---> 19 my_cls(1, 2, key = 'lijinze')
<ipython-input-121-a1198eb8b12b> in wrapper(*args, **kwargs)
2 obj_list = []
3 def wrapper(*args, **kwargs):
----> 4 new_obj = cls(*args, **kwargs)
5 obj_list.append(new_obj)
6 print("class:%s'object number is %d" % (cls.__name__, len(obj_list)))
<ipython-input-121-a1198eb8b12b> in __init__(self, *args, **kwargs)
15 print(my_cls.__name__)
16 print(type(my_cls))
---> 17 print(my_cls.STATIC_MEM)
18
19 my_cls(1, 2, key = 'lijinze')
AttributeError: 'function' object has no attribute 'STATIC_MEM'
经过装饰器修饰后,我们定义的类my_cls已经成为一个函数。 my_cls被装饰器counter修饰,等价于 my_cls = counter(my_cls)
。显然在上面的例子中,my_cls.STATIC_MEM
是错误的,正确的用法是self.STATIC_MEM
。对象中找不到属性的话,会到类空间中寻找,因此被装饰器修饰的类的静态属性是可以通过其对象进行访问的。虽然my_cls已经不是类,但是其调用返回的值却是被装饰之前的类的对象。
解决方法:
Python装饰器(decorator)在实现的时候,被装饰后的函数其实已经是另外一个函数了(函数名等函数属性会发生改变),为了不影响,Python的functools包中提供了一个叫wraps的decorator来消除这样的副作用。写一个decorator的时候,最好在实现之前加上functools的wrap,它能保留原有函数的名称和函数属性。我们加上@functools.wraps()
正确代码:
import functools
def counter(cls):
obj_list = []
@functools.wraps(cls)
def wrapper(*args, **kwargs):
new_obj = cls(*args, **kwargs)
obj_list.append(new_obj)
print("class:%s'object number is %d" % (cls.__name__, len(obj_list)))
return new_obj
return wrapper
@counter
class my_cls(object):
STATIC_MEM = 'This is a static member of my_cls'
def __init__(self, *args, **kwargs):
print(self, args, kwargs)
print(my_cls.STATIC_MEM)
print(my_cls.__name__)
print(type(my_cls))
my_cls(1,2, key = 'ljz')
运行结果:
<__main__.my_cls object at 0x7fb3e4e57c88> (1, 2) {'key': 'ljz'}
This is a static member of my_cls
my_cls
<class 'function'>
class:my_cls'object number is 1
将闭包函数应用于统计一个类创建的对象数量代码:
def counter(cls):
obj_list = []
@functools.wraps(cls)
def wrapper(*args, **kwargs):
new_obj = cls(*args, **kwargs)
obj_list.append(new_obj)
print("class:%s'object number is %d" % (cls.__name__, len(obj_list)))
return new_obj
return wrapper
@counter
class my_cls(object):
STATIC_MEM = 'This is a static member of my_cls'
def __init__(self, *args, **kwargs):
self.a = args[0]
self.b = args[1]
def add(self):
return self.a + self.b
def main(self):
print(self.add())
def print_static(self):
print(self.STATIC_MEM)
o1 = my_cls(1, 2, key = 'shijun')
o2 = my_cls(3, 4, key = 'ljz')
o3 = my_cls(5, 6, key = 'lijinze')
o1.main()
o2.print_static()
运行结果:
class:my_cls'object number is 1
class:my_cls'object number is 2
class:my_cls'object number is 3
3
This is a static member of my_cls
将闭包应用于异常处理中
我们可以将报警加入到装饰器中构成一个成熟的异常处理报警模块
utils.exception_tool.py
#!/usr/bin/env python
# encoding: utf-8
import functools
import sys
from loguru import logger
from utils.alarm_tool import Alarm
def exception_warpper(func):
@functools.wraps(func)
def inner(*args, **kwargs):
exp_msg = ''
ret = None
try:
try:
func_name = func.__name__
file_name = func.__code__.co_filename
line_num = func.__code__.co_firstlineno
exp_msg = '{}:{},f:{}'.format(
file_name, line_num, func_name
)
except:
pass
ret = func(*args, **kwargs)
except Exception as e:
exc_type, exc_value, exc_traceback = sys.exc_info()
logger.exception(e)
Alarm.msg('exception, {}, e:{}'.format(exp_msg, exc_value))
return ret
return inner
将以上异常处理模块应用于多线程业务中
def _crawl(self):
crawler_dir = os.path.join(spider_dir, 'crawler', 'crawler_*.py')
with ThreadPoolExecutor(max_workers=self.max_worker) as pool:
futures = set()
for cls_path in glob.glob(crawler_dir):
imp_cls = self._load_class(
cls_path,
'spider.crawler',
'CrawlerImpl'
)
# 加载方法对象结束
if imp_cls is not None:
cls_obj = imp_cls()
job = pool.submit(exception_warpper(cls_obj.crawl))
futures.add(job)
else:
logger.error('spider:{} load fail!', cls_path)
for job in as_completed(futures):
result = job.result()
if result:
self.crawler_result_list.append(result)
logger.info('crawl:{}, {}', result.name, result.url)
else:
logger.warning('crawl_result is null')