基于 Python 动态执行的特性,一个类的实例应当有哪些属性 ( field ) 和方法 ( 这些定义被称之元信息 ),并不像其它编译型语言那样在程序运行之前就确定不变了,而可能是随着脚本的运行而被临时修改甚至创建。换句话说,Python 可以在运行时随时修改类或实例的元信息,简称元编程。
元编程极大地拓展了脚本语言的灵活性,你或许还可以从另一门 Groovy 语言的 MOP 元对象协议中获取一些有意思的灵感。见:一文通读 Groovy 元对象协议 MOP - 掘金 (juejin.cn)。
元信息检查
首先从元信息检查开始说起。这可以通过两个内置函数进行:
vars()
函数能以字典形式打印出某个实例所具有的属性。dir()
函数能以列表形式打印出某个实例的所有属性以及方法 ( 包括从类 object
获取的魔法函数 ) 的标识符。比如:class Foo:
def __init__(self,v_):
self.v = v_
def f(self):pass
@staticmethod
def g():pass
foo = Foo(1)
print(dir(foo)) # ['__class__', '__delattr__', ... , 'f', 'g', 'v']
print(vars(foo)) # {'v': 1}
除此之外,Python 对象内部通用内置了属性或方法来反射元信息:
foo.__dict__
属性,相当于调用 vars(foo)
。foo.__dir__()
方法,相当于调用 dir(foo)
。使用 hasattr()
函数可以检测查看某个对象的元信息内是否包含某个标识符。若要动态获取标识符,可以使用 getattr()
函数。
print(hasattr(foo,"f"))
print(getattr(foo,"f"))
如果 getattr()
函数返回的标识符是属性,那么该方法会直接返回它的值或引用。若返回的标识符是方法,则可以将它看作一个可调用对象 ( 它是 method
类型 ) 调用,如:
foo = Foo(1)
invokable = getattr(foo,"f")
invokable()
Python 的 inspect
模块提供了内置的 ismethod()
函数,用于检测该标识符是否为方法。比如:
mthd = inspect.ismethod(getattr(foo,"f"))
print(mthd) # True
mthd()
元信息注入
使用 setattr()
函数可以向已构造的对象内部插入新的属性值。比如:
foo = Foo(1)
setattr(foo, "a", 2)
print(getattr(foo,"a")) # 2
或者直接以硬编码的方式注入属性。比如:
class Goo: pass
goo1 = Goo()
goo1.i = 20
print(goo1.i)
通过 MethodType
可以直接将表达式注入对象内作为方法。比如:
goo1 = Goo()
# 保留第一个参数作为方法接收者。
def method_(this): print(this.i)
# lambda 表达式也需要保留第一个参数作为方法接收者
lambda_ = lambda this: print(this.i)
goo1.invocable = MethodType(lambda_, goo1)
goo1.invocable()
不难理解,如果方法直接注入到 Goo
类型内,则该方法将作为类方法。此时,表达式的 this
将指代 cls
而非 self
。
访问拦截器
Python 对象内置了 __getattr__()
,__getattribute__()
和 __setattr__()
三个魔法函数:
__getattr__()
方法拦截。__getattribute__()
方法拦截。在 __getattribute__()
和 __getattr__()
同时被重写的场合,优先调用前者。当前者抛出 AttributeError
时,再尝试访问后者。__setattr__()
方法拦截。通过合理利用这三个魔法函数,我们可以创建出一个安全的,可被安全访问的动态对象。这里举一个简单的例子:
class Foo:
def __init__(self):
self.v_ = 10
def __getattribute__(self, item):
try:
v = object.__getattribute__(self, item)
except AttributeError:
return None
return v
foo = Foo()
foo.a = 100
print(foo.a) # 100
print(foo.b) # None
基于这种实现,当访问未定义的属性时,Foo 实例将以返回 None
的形式代替抛出 AttributeError
。
作者:花花子 来源:稀土掘金