Python 反射入门实践到精通
反射就是程序在运行过程中,动态获取对象信息、调用属性/方法、创建/删除成员的能力------不用提前硬编码对象细节,运行时再决定"操作谁、怎么操作"。
一、反射的核心:4个内置函数
Python 实现反射无需依赖第三方库,核心就是4个内置函数,适用于类、实例、模块等几乎所有对象。搭配 __dict__ 属性辅助查看成员。
| 函数 | 作用 | 适用对象 | 返回值 |
|---|---|---|---|
getattr(obj, name, default) |
动态获取对象的属性/方法 | 类、实例、模块、内置对象 | 成功返回属性值/方法对象;失败时,有default则返回default,无则抛AttributeError |
setattr(obj, name, value) |
动态为对象添加/修改属性/方法 | 类、实例、模块 | 无返回值,直接修改原对象 |
hasattr(obj, name) |
动态判断对象是否存在某个属性/方法 | 类、实例、模块 | 存在返回True,不存在返回False |
delattr(obj, name) |
动态删除对象的属性/方法 | 类、实例、模块 | 无返回值,直接删除原对象成员 |
name 参数必须是字符串!用字符串映射对象成员,从而实现"动态操作", |
二、入门实践:对「类/实例」的反射
反射,先从最基础、最易上手的「类和实例」操作开始。我定义一个简单的 Student 类,结合可直接复制运行的代码示例:
python
class Student:
# 类属性
school = "北京大学"
# 构造方法
def __init__(self, name, age):
# 实例属性
self.name = name
self.age = age
# 实例方法
def study(self, course):
print(f"{self.name}正在学习{course},就读于{self.school}")
# 创建实例
stu1 = Student("张三", 20)
1. hasattr:判断成员是否存在
在动态操作对象前,先用 hasattr 判断成员是否存在,能有效避免AttributeError 报错,"防御性操作"。
python
# 判断实例是否有name属性
print(hasattr(stu1, "name")) # True
# 判断类是否有school属性
print(hasattr(Student, "school")) # True
# 判断实例是否有study方法(方法也是对象的成员)
print(hasattr(stu1, "study")) # True
# 判断不存在的成员
print(hasattr(stu1, "gender")) # False
2. getattr:动态获取成员
获取属性时,直接返回属性值;获取方法时,返回方法对象,需要加 () 并传入对应参数才能调用。如果不确定成员是否存在,建议加上 default 默认值。
python
# 获取实例属性
name = getattr(stu1, "name")
print(name) # 张三
# 获取类属性
school = getattr(Student, "school")
print(school) # 北京大学
# 获取实例方法并调用(注意传参)
study_func = getattr(stu1, "study")
study_func("Python编程") # 张三正在学习Python编程,就读于北京大学
# 获取不存在的成员,设置默认值(避免报错)
gender = getattr(stu1, "gender", "未知")
print(gender) # 未知
3. setattr:动态添加/修改成员
无论是实例还是类,都能通过 setattr 动态添加新的属性/方法,或者修改已有的成员,极大提升了代码的灵活性,适合运行时动态扩展对象功能。
python
# 为实例**修改**已有属性
setattr(stu1, "age", 21)
print(stu1.age) # 21
# 为实例**添加**新属性
setattr(stu1, "gender", "男")
print(stu1.gender) # 男
# 为类**添加**新类属性
setattr(Student, "address", "北京市海淀区")
print(Student.address) # 北京市海淀区
print(stu1.address) # 北京市海淀区(实例继承类属性)
# 为实例**动态添加方法**(实用:运行时为对象绑定函数)
def run(self):
print(f"{self.name}正在跑步,年龄{self.age}")
# 把函数绑定为实例方法(注意:第一个参数必须是self,和实例方法一致)
setattr(stu1, "run", run)
stu1.run(stu1) # 张三正在跑步,年龄21
4. delattr:动态删除成员
用于动态删除对象的属性/方法,删除后再访问该成员会报错,适合运行时清理无用的对象资源(谨慎使用,避免误删必要成员)。
python
# 删除实例属性
delattr(stu1, "gender")
# print(stu1.gender) # 报错:AttributeError
# 删除类属性
delattr(Student, "address")
# print(Student.address) # 报错:AttributeError
三、对「模块」的反射
场景:动态导入并调用模块的函数/类
自定义模块 my_module.py,里面包含常用的函数和类,我们反射动态调用它的成员,无需硬编码模块成员的调用路径。
python
# my_module.py
def add(a, b):
return a + b
def mul(a, b):
return a * b
class Calculator:
def div(self, a, b):
if b == 0:
return "除数不能为0"
return a / b
通过反射动态调用该模块的成员,无需修改代码,就能切换调用的函数/类:
python
# 主程序 main.py
import my_module # 导入自定义模块
# 1. 动态调用模块中的函数
func_name = "add" # 可从配置文件读取、用户输入(字符串格式)
if hasattr(my_module, func_name):
add_func = getattr(my_module, func_name)
print(add_func(3, 5)) # 8
# 2. 动态获取模块中的类,并创建实例、调用方法
class_name = "Calculator"
if hasattr(my_module, class_name):
CalClass = getattr(my_module, class_name)
cal = CalClass() # 创建实例
# 再对实例反射调用方法
res = getattr(cal, "div")(10, 2)
print(res) # 5.0
如果模块名也是动态的(比如从配置文件读取),可以用 __import__() 函数动态导入模块(替代 import 关键字),实现"模块名+成员名"的双重动态切换:
python
# 动态导入模块(module_name是字符串)
module_name = "my_module"
my_module = __import__(module_name)
# 后续操作和上面一致
add_func = getattr(my_module, "add")
print(add_func(2, 4)) # 6
四、反射的实际应用场景
场景1:配置化开发,避免硬编码
开发数据处理、任务调度等程序时,经常需要支持多种处理方式(如Excel/CSV/JSON数据处理、多种任务执行逻辑)。如果用硬编码,需要写大量 if/elif 判断,后续新增功能还要修改主程序;用反射结合配置文件,就能实现"新增功能不修改主程序",符合软件设计的"开闭原则"。
- 定义不同的处理类(统一接口,如都有
handle方法),存放在handlers.py中:
python
# handlers.py
class ExcelHandler:
def handle(self):
print("处理Excel数据")
class CsvHandler:
def handle(self):
print("处理CSV数据")
class JsonHandler:
def handle(self):
print("处理JSON数据")
- 编写配置文件(
config.json),用字符串指定需要使用的处理类,无需修改代码:
json
{
"handler": "ExcelHandler"
}
- 读取配置文件,通过反射动态获取处理类并执行,新增处理方式只需加类+改配置:
python
import json
import handlers # 导入处理类模块
# 读取配置文件
with open("config.json", "r", encoding="utf-8") as f:
config = json.load(f)
handler_name = config["handler"]
# 反射动态获取类并执行
if hasattr(handlers, handler_name):
# 获取处理类
HandlerClass = getattr(handlers, handler_name)
# 创建实例并调用方法
handler = HandlerClass()
getattr(handler, "handle")() # 输出:处理Excel数据
场景2:框架入门必备
比如 Flask 的路由映射,底层就是用反射将 URL 字符串映射到视图函数,我们用反射实现一个极简版路由:
python
# 极简版 Flask 路由映射(反射实现,框架入门简化版)
class MiniFlask:
def __init__(self):
# 存储 URL 和视图函数的映射(键:URL字符串,值:函数名字符串)
self.url_map = {}
# 路由装饰器(模拟 Flask @app.route)
def route(self, url):
def decorator(func):
# 将 URL 和函数名(字符串)存入映射表
self.url_map[url] = func.__name__
return func
return decorator
# 模拟请求分发(核心:用反射动态调用视图函数)
def run(self, url):
# 判断 URL 是否在映射表中
if url in self.url_map:
# 获取函数名(字符串)
func_name = self.url_map[url]
# 用反射动态获取当前模块中的视图函数并调用
if hasattr(__main__, func_name):
view_func = getattr(__main__, func_name)
return view_func()
return "404 Not Found"
# 实例化极简框架
app = MiniFlask()
# 定义视图函数(模拟框架开发)
@app.route("/index")
def index():
return "欢迎访问首页(反射实现路由调度)"
@app.route("/about")
def about():
return "关于我们(框架入门实践)"
# 模拟用户请求
print(app.run("/index")) # 输出:欢迎访问首页(反射实现路由调度)
print(app.run("/about")) # 输出:关于我们(框架入门实践)
用反射将 URL 字符串映射到视图函数,无需硬编码 URL 和函数的对应关系,这就是反射在框架中的核心作用,也是框架入门必须掌握的反射实践。
场景3:插件化程序开发
开发主程序时,如果需要支持"插件扩展"(比如 IDE 的插件、工具类软件的功能插件),可以用反射实现插件的热插拔。主程序无需知道插件的具体实现,只需约定插件的统一接口(如必须有 run() 方法),通过反射动态加载插件模块、调用接口,新增插件时直接放入指定目录即可,无需修改主程序。
场景4:处理用户动态输入(命令行工具)
开发命令行工具时,用户输入的是字符串格式的命令(如 login、register),用反射可以动态执行对应的函数,无需写大量 if/elif 判断命令,新增命令时只需添加对应的函数即可。
python
# 定义命令对应的函数
def login():
print("执行登录操作")
def register():
print("执行注册操作")
def logout():
print("执行退出操作")
# 获取用户输入(字符串)
cmd = input("请输入命令(login/register/logout):")
# 反射动态执行
if hasattr(__main__, cmd): # __main__代表当前模块
func = getattr(__main__, cmd)
func()
else:
print("命令不存在")
运行后,用户输入 login 就会执行登录操作,输入 register 就会执行注册操作,后续新增命令只需添加函数,无需修改判断逻辑,极大简化了代码维护成本。
五、反射的辅助工具:__dict__ 属性
几乎所有 Python 对象(类、实例、模块)都有 __dict__ 属性,它返回一个字典,存储对象的所有成员信息(键是成员名,值是成员值)。日常开发中,我们可以用它查看对象的所有可反射成员,方便调试和排查问题。
python
class Student:
school = "北大"
def __init__(self, name):
self.name = name
def study(self):
pass
stu = Student("张三")
# 查看实例的成员(实例属性+绑定的方法)
print(stu.__dict__) # {'name': '张三'}
# 查看类的成员(类属性+类方法+实例方法)
print(Student.__dict__) # 包含school、__init__、study等
# 查看模块的成员
import my_module
print(my_module.__dict__.keys()) # 包含add、mul、Calculator等
六、使用反射的注意事项
以下4点,能避免大部分问题:
-
反射的参数必须是字符串 :再次强调,
hasattr、getattr等函数的name参数,必须是字符串格式,否则会报错。 -
做好异常处理 :建议先用
hasattr()判断成员是否存在,再用getattr()获取;或者给getattr()设置default默认值,避免抛出AttributeError。 -
避免过度使用:反射能提升代码灵活性,但会降低代码的可读性和可调试性(动态操作无法被静态检查工具识别)。如果是非动态编程场景(如固定调用某个属性/方法),建议直接硬编码,更易维护。
-
私有成员的反射 :Python 的私有成员(双下划线开头,如
__name)会被"名称改写"(变成_类名__name),直接反射__name会失败,需用改写后的名称访问。
python
class Person:
def __init__(self):
self.__name = "李四" # 私有实例属性
p = Person()
# print(hasattr(p, "__name")) # False,直接判断失败
print(hasattr(p, "_Person__name")) # True,名称改写后
print(getattr(p, "_Person__name")) # 李四
不要用反射强行访问私有成员,这违背了 Python 的封装原则,也会增加代码的维护成本。
五、总结
-
核心是4个内置函数:
hasattr(判断)、getattr(获取)、setattr(添加/修改)、delattr(删除),依赖字符串映射成员。 -
核心价值是动态编程:运行时决定操作的成员,避免硬编码,符合开闭原则。
-
最常用组合:
hasattr()判断 +getattr()获取(加默认值),避免报错。 -
进阶用法:结合
__import__()动态导入模块,实现模块和成员的双重动态。