☰
Python反射机制实例讲解
2026/10/13 7:09:27 网站建设 项目流程

前言


先说一个容易含糊的地方: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")) # False

getattr的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。这些都是防御侧的基本纪律,本文不给任何攻击示例。


常见坑点



  1. 用type判类型漏掉子类


❌if type(x) == int: ...→True是int的子类实例,却会被判为不匹配。 ✅if isinstance(x, int): ...(官方推荐,考虑继承)。



  1. hasattr吞不掉「非 AttributeError」异常


❌ 以为hasattr(obj, "x")一定安全——若x是 property 且 getter 内部抛ValueError,异常会直接冒出来。 ✅ 明确用try: ... except AttributeError:处理属性不存在;需要连其他异常一起兜住就显式写。



  1. 用getattr取双下划线私有属性取不到


❌getattr(obj, "__secret")→AttributeError,因为真实名字被编译期改写成_类名__secret。 ✅ 手动写全getattr(obj, "_类名__secret"),或者干脆提供公开访问接口,别去戳私有属性。



  1. 把用户输入直接当属性名/函数名调用


❌getattr(module, user_input)()→ 可被诱导调用任意可调用对象。 ✅ 先用白名单字典或集合校验,再取名字调用。



  1. 用eval/exec处理外部字符串


❌eval(user_expression)→ 能执行任意代码,是不可信输入下的严重风险。 ✅ 换成分发字典 +getattr白名单;确实要算表达式就用ast.literal_eval(只解析字面量)。



  1. 遍历vars(obj)时修改对象


❌for k in vars(obj): setattr(obj, k + "_x", 1)→RuntimeError: dictionary changed size during iteration。 ✅ 先做快照:for k in list(vars(obj)): ...。



  1. 以为dir()只给实例属性


❌"move" in vars(p)→False,于是判定实例没有这个方法。 ✅ 找方法用dir(p)或hasattr(p, "move");vars(p)只看实例自己的__dict__。



  1. 链式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处理不可信数据。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询