Python 运行环境的自省机制

有时候我们会碰到这样的需求,需要执行对象的某个方法,或是需要对对象的某个字段赋值,而方法名或是字段名在编码代码时并不能确定,需要通过参数传递字符串的形式输入。

这个机制被称为反射(反过来让对象告诉我们他是什么),或是自省,用于实现在运行时获取未知对象的信息。

反射是个很吓唬人的名词,听起来高深莫测,在一般的编程语言里反射相对其他概念来说稍显复杂,一般来说都是作为高级主题来讲;但在Python中反射非常简单,用起来几乎感觉不到与其他的代码有区别,使用反射获取到的函数和方法可以像平常一样加上括号直接调用,获取到类后可以直接构造实例;不过获取到的字段不能直接赋值,因为拿到的其实是另一个指向同一个地方的引用,赋值只能改变当前的这个引用而已。


In [1]:
#coding: UTF-8


import sys #  模块,sys指向这个模块对象
def foo(): pass # 函数,foo指向这个函数对象 
class Cat(object): # 类,Cat指向这个类对象
    def __init__(self, name='kitty'):
        self.name = name
    def sayHi(self): #  实例方法,sayHi指向这个方法对象,使用类或实例.sayHi访问
        print self.name, 'says Hi!' # 访问名为name的字段,使用实例.name访问

cat = Cat() # cat是Cat类的实例对象

print Cat.sayHi # 使用类名访问实例方法时,方法是未绑定的(unbound)
print cat.sayHi # 使用实例访问实例方法时,方法是绑定的(bound)


<unbound method Cat.sayHi>
<bound method Cat.sayHi of <__main__.Cat object at 0x7fc4f47b1790>>

访问对象的属性

以下列出了几个内建方法,可以用来检查或是访问对象的属性。这些方法可以用于任意对象而不仅仅是例子中的Cat实例对象;Python中一切都是对象。


In [2]:
cat = Cat('kitty')

print cat.name # 访问实例属性
cat.sayHi() # 调用实例方法

print dir(cat) # 获取实例的属性名,以列表形式返回
if hasattr(cat, 'name'): # 检查实例是否有这个属性
    setattr(cat, 'name', 'tiger') # same as: a.name = 'tiger'
print getattr(cat, 'name') # same as: print a.name

getattr(cat, 'sayHi')() # same as: cat.sayHi()


kitty
kitty says Hi!
['__class__', '__delattr__', '__dict__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'name', 'sayHi']
tiger
tiger says Hi!

代码块(func_code)

代码块可以由类源代码、函数源代码或是一个简单的语句代码编译得到。
co_argcount: 普通参数的总数,不包括*参数和**参数。
co_names: 所有的参数名(包括*参数和**参数)和局部变量名的元组。
co_varnames: 所有的局部变量名的元组。
co_filename: 源代码所在的文件名。
co_flags:  这是一个数值,每一个二进制位都包含了特定信息。较关注的是0b100(0x4)和0b1000(0x8),如果co_flags & 0b100 != 0,说明使用了*args参数;如果co_flags & 0b1000 != 0,说明使用了**kwargs参数。另外,如果co_flags & 0b100000(0x20) != 0,则说明这是一个生成器函数(generator function)。

In [3]:
co = cat.sayHi.func_code
print co
print co.co_argcount        # 1
print co.co_names           # ('name',)
print co.co_varnames        # ('self',)
print co.co_flags & 0b100   # 0


<code object sayHi at 0x7fc4f480c930, file "<ipython-input-1-54c83e5cf426>", line 7>
1
('name',)
('self',)
0

栈帧(frame)

栈帧表示程序运行时函数调用栈中的某一帧。函数没有属性可以获取它,因为它在函数调用时才会产生,而生成器则是由函数调用返回的,所以有属性指向栈帧。想要获得某个函数相关的栈帧,则必须在调用这个函数且这个函数尚未返回时获取。你可以使用sys模块的_getframe()函数、或inspect模块的currentframe()函数获取当前栈帧。这里列出来的属性全部是只读的。

  1. f_back: 调用栈的前一帧。
  2. f_code: 栈帧对应的code对象。
  3. f_locals: 用在当前栈帧时与内建函数locals()相同,但你可以先获取其他帧然后使用这个属性获取那个帧的locals()。
  4. f_globals: 用在当前栈帧时与内建函数globals()相同,但你可以先获取其他帧。

In [4]:
def add(x, y=1):
    f = sys._getframe() # same as inspect.currentframe()
    print locals()
    print f.f_locals    # same as locals()
    print f.f_back      # <frame object at 0x...>
    return x+y
add(2)


{'y': 1, 'x': 2, 'f': <frame object at 0x7fc4f47bd238>}
{'y': 1, 'x': 2, 'f': <frame object at 0x7fc4f47bd238>}
<frame object at 0x7fc4f47bd420>
Out[4]:
3

追踪(traceback)

追踪是在出现异常时用于回溯的对象,与栈帧相反。由于异常时才会构建,而异常未捕获时会一直向外层栈帧抛出,所以需要使用try才能见到这个对象。你可以使用sys模块的exc_info()函数获得它,这个函数返回一个元组,元素分别是异常类型、异常对象、追踪。traceback的属性全部是只读的。

  1. tb_next: 追踪的下一个追踪对象。
  2. tb_frame: 当前追踪对应的栈帧。
  3. tb_lineno: 当前追踪的行号。

In [5]:
def div(x, y):
    try:
        return x/y
    except:
        print sys.exc_info()
        tb = sys.exc_info()[2]  # return (exc_type, exc_value, traceback)
        print tb
        print tb.tb_lineno      # "return x/y" 的行号
div(1, 0)


(<type 'exceptions.ZeroDivisionError'>, ZeroDivisionError('integer division or modulo by zero',), <traceback object at 0x7fc4f47bf7e8>)
<traceback object at 0x7fc4f47bf7e8>
3

Inspect 模块在栈帧检查时的使用

除了代码对象的自省外, inspect 模块包括了一些函数以检查函数执行时的运行时环境。这些函数中的多数是处理调用栈,操作对象是调用帧。

栈中的每个帧记录包含:

  1. 帧对象
  2. 代码所在文件的文件名
  3. 该文件中当前运行的行号
  4. 所调用的函数名
  5. 源码中上下文代码行的一个列表
  6. 该列表中当前行的索引

这些信息会在运行异常时产生 traceback. 它对于记录日志或者调试程序也很用,可以通过查看栈帧发现函数的参数值。

currentframe() 会返回位于栈顶的帧,对应当前函数。 getargvalues() 返回一个 tuple, 包含:

  1. 参数名
  2. 变参名
  3. 帧中局部值构成的 dict, 名为locals, 每对 key:value变量名:变量值

结合它们,可以显示调用栈中不同的函数参数和局部变量。


In [6]:
import inspect
def add(x, y=1, *z):
    print inspect.getargvalues(inspect.currentframe())
    return x + y + sum(z)
add(2)


ArgInfo(args=['x', 'y'], varargs='z', keywords=None, locals={'y': 1, 'x': 2, 'z': ()})
Out[6]:
3

In [7]:
def recurse(limit):
    local_variable = '.' * limit
    
    # locals 包含了并非 recurse() 参数的 local_variable: '.'
    print(limit, inspect.getargvalues(inspect.currentframe()))
    
    if limit <= 0:
        return
    recurse(limit - 1)
    return local_variable

if __name__ == '__main__':
    recurse(2)


(2, ArgInfo(args=['limit'], varargs=None, keywords=None, locals={'local_variable': '..', 'limit': 2}))
(1, ArgInfo(args=['limit'], varargs=None, keywords=None, locals={'local_variable': '.', 'limit': 1}))
(0, ArgInfo(args=['limit'], varargs=None, keywords=None, locals={'local_variable': '', 'limit': 0}))

In [ ]: