前言
先说一个容易含糊的地方:Python 官方文档里没有「反射(reflection)」这个概念,也没有对应的专门章节。Java 有java.lang.reflect包和明确的反射 API,Python 里承担同类职责的能力叫内省(introspection)——在运行时查看对象的类型、属性和方法,并按名字去读写、调用它们。中文技术圈习惯借用「反射」这个词来指代这套能力,说「Python 的反射」时,指的其实就是内省加动态调用。
第二个常见误解是「反射就是eval」。eval/exec能执行任意代码,是最不该用于这项任务的手段;真正的主力是getattr/setattr/hasattr/delattr这四个内置函数,加上inspect模块。本文讲的是安全、可控的那条路。
本文分四部分:四个基础动作、结构探查(type/isinstance/vars/dir)、inspect模块、以及一个按名字动态调用的实战。所有 API 名称与签名以 Python 官方文档为准。本机没有 Python 解释器,示例仅作人工推演,实际运行以你的解释器为准。
一、四个基础动作
Python 里访问属性有两种写法:点号obj.name是编译期就固定的名字,函数getattr(obj, "name")则是运行期用字符串拼出来的名字。后者才是「按名字访问」的关键。
| 动作 | 函数写法 | 等价的点号语法 |
|---|
| 取属性 | getattr(obj, "x", default) | obj.x(缺省值除外) |
| 设属性 | setattr(obj, "x", v) | obj.x = v |
| 判断存在 | hasattr(obj, "x") | 无直接等价写法 |
| 删属性 | delattr(obj, "x") | del obj.x |
# 适用于 Python 3.0+
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def move(self, dx, dy):
self.x += dx
self.y += dy
p = Point(1, 2)
print(getattr(p, "x")) # 1
print(getattr(p, "z", "无此属性")) # 无此属性 ← 第三个参数是缺省值
print(hasattr(p, "y")) # True
setattr(p, "y", 20) # 等价于 p.y = 20
print(p.y) # 20
delattr(p, "y") # 等价于 del p.y
print(hasattr(p, "y")) # Falsegetattr的name必须是字符串;当属性不存在且没给缺省值时抛AttributeError。注意官方文档的一条提醒:name不必是合法标识符(setattr也一样),但私有名字改写(name mangling)发生在编译期,所以想用getattr取self.__x这类双下划线开头的属性,必须自己把它改写成_类名__x。
再补一条版本常识:Python 2.7 已于2020 年 1 月 1 日停止维护。内省时若在旧代码里看到obj.iteritems()/iterkeys()/itervalues(),这些方法在 Python 3 里已移除,应统一改成.items()/.keys()/.values()(返回的是视图,不是列表);xrange也已被删掉,range本身就是惰性的。另外 Python 3 的hasattr只把AttributeError当作「属性不存在」,不像 Python 2 那样吞掉所有异常,这一点在写通用探测代码时要留意。
二、结构探查:type、isinstance、vars、dir
看一个对象「是什么、有什么」:
# 适用于 Python 3.0+
p = Point(1, 2)
print(type(p)) # <class '__main__.Point'>
print(isinstance(p, Point)) # True
print(sorted(vars(p))) # ['x', 'y'] ← 实例属性名
print(sorted(vars(Point).keys())) # 含 '__init__'、'move' 等
print("move" in dir(p)) # True ← dir 含类里继承来的成员
print(callable(getattr(p, "move"))) # True四个函数的分工:
type(x)返回对象精确的类型对象;用type(x) is int判等会漏掉子类。isinstance(x, T)会考虑继承关系,是判断类型时官方推荐的方式。特别地,bool是int的子类,所以isinstance(True, int)为True,而type(True) is int为False。vars(obj)返回对象的__dict__(实例属性),vars()不传参时等价于locals()。dir(obj)返回一个排序后的名字列表,它合并了实例属性、类及其 MRO 上所有基类的属性,还会经过__dir__钩子定制——远不止实例__dict__那点内容。
判断「是不是某个类」还可以用issubclass(A, B),它同样考虑继承链。
三、inspect 模块:把函数看透
inspect提供更高层的查询能力,常见的几个:
# 适用于 Python 3.0+
import inspect
def greet(name, *, loud=False):
return f"hi {name}"
print(inspect.isfunction(greet)) # True
print(inspect.isclass(Point)) # True
sig = inspect.signature(greet)
print(str(sig)) # (name, *, loud=False)
print(list(sig.parameters)) # ['name', 'loud']
print(sig.parameters["loud"].kind) # 说明 loud 是「仅关键字参数」inspect.signature()接受范围很广:普通函数、类、functools.partial对象等都能解析出签名。inspect.getmembers(obj, predicate)返回按名字排序的(名称, 值)列表,第二个参数是筛选函数:
# 适用于 Python 3.0+
for name, func in inspect.getmembers(Point, inspect.isfunction):
print(name)
# 会看到 __init__、move 这样定义在类里的函数inspect.ismethod()用于判断绑定方法:inspect.ismethod(p.move)为True,而inspect.ismethod(Point.move)为False(后者是普通函数)。这一点在写通用调用器时很容易搞混。
四、实战:按名字动态调用
一个常见需求是「根据字符串找到并调用相应函数」,例如做简易的命令分发。正确做法是查白名单表,而不是拼字符串去eval:
# 适用于 Python 3.0+
import importlib
def add(a, b):
return a + b
def sub(a, b):
return a - b
# 白名单:只允许这些名字被外部触发
ACTIONS = {"add": add, "sub": sub}
def dispatch(action, *args):
func = ACTIONS.get(action)
if func is None:
raise ValueError(f"未知动作: {action}")
return func(*args)
print(dispatch("add", 2, 3)) # 5
print(dispatch("sub", 5, 1)) # 4如果目标在别的模块里,用importlib.import_module拿到模块对象,再getattr取出需要的类或函数:
# 适用于 Python 3.0+
def load(module_name, attr_name):
module = importlib.import_module(module_name)
return getattr(module, attr_name)
DateTime = load("datetime", "datetime")
print(DateTime.now()) # 打印当前日期时间这里同样建议在调用前做白名单校验,不要把用户输入直接当成模块名或属性名拼进去。
顺带说清eval/exec的定位:它们会执行任意代码,只要字符串可控,就等同于把程序控制权交出去,绝不能用于不可信输入。类似的还有pickle反序列化——它能构造任意对象,处理不可信数据时应改用json。这些都是防御侧的基本纪律,本文不给任何攻击示例。
常见坑点
- 用
type判类型漏掉子类
❌if type(x) == int: ...→True是int的子类实例,却会被判为不匹配。 ✅if isinstance(x, int): ...(官方推荐,考虑继承)。
hasattr吞不掉「非 AttributeError」异常
❌ 以为hasattr(obj, "x")一定安全——若x是 property 且 getter 内部抛ValueError,异常会直接冒出来。 ✅ 明确用try: ... except AttributeError:处理属性不存在;需要连其他异常一起兜住就显式写。
- 用
getattr取双下划线私有属性取不到
❌getattr(obj, "__secret")→AttributeError,因为真实名字被编译期改写成_类名__secret。 ✅ 手动写全getattr(obj, "_类名__secret"),或者干脆提供公开访问接口,别去戳私有属性。
- 把用户输入直接当属性名/函数名调用
❌getattr(module, user_input)()→ 可被诱导调用任意可调用对象。 ✅ 先用白名单字典或集合校验,再取名字调用。
- 用
eval/exec处理外部字符串
❌eval(user_expression)→ 能执行任意代码,是不可信输入下的严重风险。 ✅ 换成分发字典 +getattr白名单;确实要算表达式就用ast.literal_eval(只解析字面量)。
- 遍历
vars(obj)时修改对象
❌for k in vars(obj): setattr(obj, k + "_x", 1)→RuntimeError: dictionary changed size during iteration。 ✅ 先做快照:for k in list(vars(obj)): ...。
- 以为
dir()只给实例属性
❌"move" in vars(p)→False,于是判定实例没有这个方法。 ✅ 找方法用dir(p)或hasattr(p, "move");vars(p)只看实例自己的__dict__。
- 链式
getattr忘了逐层兜底
❌getattr(getattr(cfg, "db"), "host", None)→ 若db不存在,内层先抛AttributeError,外层缺省值救不了。 ✅ 逐层写getattr(cfg, "db", None),判空后再取下一层。
总结
| 需求 | 用什么 |
|---|
| 按字符串读写属性 | getattr/setattr/hasattr/delattr |
| 判断类型 | isinstance/issubclass(不用type判等) |
| 看实例属性 | vars(obj)/obj.__dict__ |
| 看全部可用名字 | dir(obj)/inspect.getmembers() |
| 看函数签名 | inspect.signature() |
| 按名字取模块成员 | importlib.import_module()+getattr |
| 按名字分发调用 | 白名单字典 +getattr,不要eval/exec |
把「反射」翻译成「内省加getattr家族」之后,整件事就清晰了:它的价值在于让代码在运行期按名字组织行为,做插件、命令分发、序列化都靠它。但它的能力边界也是安全风险边界——凡是名字来自外部的地方,都要先过白名单,绝不eval,绝不用pickle处理不可信数据。