ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Python反射机制详解:动态获取与调用对象属性的方法

Python反射机制详解:动态获取与调用对象属性的方法

1. 什么是反射?它不是“照镜子”,而是程序的自我认知能力

很多人第一次听到“Python反射”这个词,下意识会联想到物理课上的光反射,或者手机前置摄像头里那个熟悉的自己——但编程里的反射,和这些都没关系。它本质上是一种程序在运行时动态观察、检查、修改自身结构与行为的能力。你可以把它理解成:你的Python脚本在执行过程中,突然“醒过来”,开始翻自己的身份证(__name__)、查自己的户口本(globals())、点名调用自己没写在主流程里的函数(getattr(obj, 'func_name')()),甚至临时给自己加个新属性(setattr(obj, 'new_attr', value))。这种能力不是语法糖,也不是炫技工具,而是构建框架、实现插件系统、做自动化测试、开发低代码平台时绕不开的底层支撑。

我最早在写一个配置驱动的日志分发模块时撞上反射需求:用户只配一个字符串"send_to_email",系统就得自动找到并调用send_to_email()函数,而不是用一长串if-elif-else硬编码判断。当时试过字典映射方案,但随着功能模块从3个涨到20多个,字典维护成本飙升,还容易漏配、拼错键名。换成反射后,函数名和配置项完全一致,新增一个发送渠道,只要写好函数、保证命名规范,配置文件里加一行就生效——整个逻辑瞬间轻盈下来。这背后起作用的,就是getattr()、hasattr()这几个看似简单的内置函数。

反射的核心价值,在于它把“写死的代码路径”变成了“可配置的调用链路”。它不改变Python的执行模型,也不引入新语法,而是充分利用了Python一切皆对象、对象属性可动态访问这一根本特性。你不需要额外安装库,不需要改解释器,只要理解type()、dir()、getattr()这三个基础函数的行为边界,就能稳稳接住80%以上的反射场景。它不像装饰器那样有固定模式,也不像异步那样需要重构思维,而更像一把随身小刀——平时收在口袋里,遇到需要临时撬开对象、读取元信息、按需调用的场合,才抽出来用一下。正因如此,它常被用在“胶水层”:连接配置与逻辑、桥接用户输入与内部方法、实现通用序列化/反序列化——这些地方不需要炫目算法,但要求极高的灵活性和鲁棒性。

2. 反射的四大核心操作:查、判、取、设,每一步都藏着坑

Python反射不是单个函数,而是一组协同工作的内置工具链。我把最常用、最核心的操作归纳为四个动词:查(inspect)、判(check)、取(get)、设(set)。它们像一套标准扳手,尺寸不同,用途明确,但拧错螺丝(用错场景)就会打滑甚至崩齿。

2.1 查:dir()和vars()—— 看清对象的“家底”

dir(obj)是反射的入门第一眼。它返回一个字符串列表,列出对象所有可访问的属性和方法名,包括继承来的、双下划线的特殊方法(如__init__)、以及通过__getattr__动态生成的(如果实现了该方法)。它不关心属性值是什么,只告诉你“家里有哪些门”。比如对一个空类实例调用:

class Demo: def __init__(self): self.a = 1 self.b = "hello" def say(self): return "hi" d = Demo() print(dir(d)) # 输出包含 '__dict__', '__module__', 'a', 'b', 'say' 等数十个名字

这里要注意:dir()返回的是名称列表,不是值。它像房产证上的户型图,标着“客厅”“卧室”“厨房”,但不告诉你客厅里摆的是沙发还是钢琴。真正要看“客厅里有什么”,得用vars(obj)或obj.__dict__。vars()本质是obj.__dict__的安全封装,只返回对象自身的实例属性字典(不包含类属性、方法、继承属性)。继续上面的例子:

print(vars(d)) # {'a': 1, 'b': 'hello'} print(d.__dict__) # 同上

提示:vars()对没有__dict__的对象(如内置类型list、int,或定义了__slots__的类)会直接报TypeError。此时必须用dir()配合getattr()组合查询,不能硬刚vars()。

2.2 判:hasattr()—— 在调用前先敲门确认

hasattr(obj, name)是反射中最常被低估的安全阀。它的作用极其朴素:检查对象obj是否拥有名为name的属性(包括方法)。返回True或False,绝不抛异常。很多新手会跳过这一步,直接getattr(obj, name)(),结果遇到属性不存在就崩。hasattr()就是那个帮你提前敲门、确认主人在家再进门的人。

它的底层实现其实很“土”:就是try-except包裹getattr()。源码逻辑近似:

def hasattr(obj, name): try: getattr(obj, name) return True except AttributeError: return False

所以,hasattr()和getattr()连用,是反射操作的黄金搭档。例如,一个通用的“安全调用方法”函数:

def safe_call_method(obj, method_name, *args, **kwargs): if hasattr(obj, method_name): method = getattr(obj, method_name) if callable(method): # 再确认是可调用对象,防属性是普通变量 return method(*args, **kwargs) else: raise TypeError(f"{method_name} is not callable") else: raise AttributeError(f"{obj} has no attribute '{method_name}'") # 使用 result = safe_call_method(d, "say") # 安全调用 # result = safe_call_method(d, "nonexistent") # 抛出清晰错误

注意:hasattr()对于通过__getattr__动态生成的属性,会触发__getattr__方法。如果__getattr__本身有副作用(比如每次调用都发一次HTTP请求),那hasattr()就不再是无害的“敲门”,而成了“强行开门”。此时应评估__getattr__设计是否合理,或改用dir()+ 名称匹配等更轻量的检查方式。

2.3 取:getattr()—— 拿到属性值的万能钥匙

getattr(obj, name[, default])是反射的心脏。它根据字符串name,从对象obj中取出对应的属性值。第三个参数default是灵魂所在:如果属性不存在,不抛AttributeError,而是返回你指定的默认值。这让你能写出极其简洁的容错逻辑。

# 有默认值,安全 value = getattr(d, "c", "default_value") # 返回 "default_value" # 无默认值,属性不存在则抛异常 value = getattr(d, "c") # AttributeError: 'Demo' object has no attribute 'c'

getattr()的威力在于它能取到任何东西:实例变量、类变量、方法、甚至模块里的函数。比如,你想动态导入并调用某个模块的函数:

import json # 想调用 json.loads,但函数名来自配置 func_name = "loads" json_func = getattr(json, func_name) # 得到 <function loads at ...> result = json_func('{"key": "value"}') # 执行

这里的关键洞察是:getattr()返回的是对象本身,不是字符串。你拿到json_func后,它就是一个标准的函数对象,可以像普通函数一样传参、调用、赋值给变量。这正是反射实现“动态行为”的核心机制——把字符串名,转换为可执行的对象引用。

2.4 设:setattr()和delattr()—— 改变对象的“实时状态”

如果说getattr()是读取,setattr()就是写入。setattr(obj, name, value)的作用是:给对象obj设置一个名为name的属性,值为value。它会创建新属性(如果不存在)或覆盖旧值(如果存在)。delattr(obj, name)则负责删除。

setattr(d, "new_attr", 999) print(d.new_attr) # 999 print(hasattr(d, "new_attr")) # True delattr(d, "new_attr") print(hasattr(d, "new_attr")) # False

setattr()的典型应用场景是对象初始化后的动态配置。比如一个数据库连接类,连接参数(host、port、db)来自环境变量或配置文件,你不想在__init__里写死所有可能的参数名,而是用循环批量设置:

config = {"host": "localhost", "port": 5432, "db": "myapp"} for key, value in config.items(): setattr(db_conn, key, value)

警告:setattr()对__slots__类型的对象有严格限制。如果类定义了__slots__ = ['a', 'b'],那么setattr(obj, 'c', 1)会直接抛AttributeError,因为c不在允许的槽位列表中。这是Python的显式约束,防止意外污染对象结构。遇到这种情况,要么修改__slots__(通常不推荐),要么换用字典存储动态属性(如obj._dynamic_attrs = {}),并在__getattr__中处理。

3. 实战案例拆解:用反射构建一个可配置的命令行工具

理论讲完,不如直接上一个完整、可运行、有真实业务影子的案例。我们来做一个极简但实用的命令行工具:task_runner.py。它的目标很明确——用户通过命令行输入一个任务名(如backup、sync、report),程序就自动找到并执行对应的任务函数。所有任务函数都放在tasks/目录下,新增任务只需写一个Python文件,无需修改主程序。

这个需求,用传统if-elif也能做,但会随着任务增多变得臃肿难维护。用反射,就能让主程序保持“无知”——它不知道具体有哪些任务,只负责按名加载和执行。

3.1 项目结构与基础约定

首先,规划清晰的目录结构,这是反射能工作的前提:

project/ ├── task_runner.py # 主程序,反射逻辑在此 ├── tasks/ │ ├── __init__.py # 必须!使tasks成为包 │ ├── backup.py # 任务1:备份 │ ├── sync.py # 任务2:同步 │ └── report.py # 任务3:生成报告

关键约定有两条:

  1. tasks/目录下的每个.py文件,必须定义一个名为run()的函数,作为该任务的入口。
  2. tasks/__init__.py文件内容为空,或仅包含from .backup import *等导入语句(我们选择前者,保持纯净)。

3.2 主程序task_runner.py的反射实现

#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 可配置任务执行器 - 使用反射动态加载并运行任务 """ import sys import importlib.util import os from pathlib import Path def load_task_module(task_name: str): """ 根据任务名,动态导入对应的tasks模块 :param task_name: 任务名,如 'backup' :return: 导入的模块对象,或None(失败时) """ # 构建模块文件路径 task_file = Path("tasks") / f"{task_name}.py" if not task_file.exists(): print(f"错误:任务 '{task_name}' 未找到。请检查 tasks/{task_name}.py 是否存在。") return None # 构建模块规范(spec),这是importlib动态导入的核心 spec = importlib.util.spec_from_file_location(task_name, task_file) if spec is None: print(f"错误:无法为 {task_file} 创建模块规范。") return None # 创建模块对象 module = importlib.util.module_from_spec(spec) # 执行模块(相当于执行 .py 文件里的所有代码) try: spec.loader.exec_module(module) except Exception as e: print(f"错误:执行任务模块 {task_name} 时发生异常:{e}") return None return module def main(): # 解析命令行参数:python task_runner.py backup if len(sys.argv) != 2: print("用法:python task_runner.py <task_name>") print("可用任务:backup, sync, report (请确保对应文件存在)") sys.exit(1) task_name = sys.argv[1] # 第一步:加载模块 module = load_task_module(task_name) if module is None: sys.exit(1) # 第二步:检查模块中是否存在 run() 函数 if not hasattr(module, "run"): print(f"错误:任务模块 '{task_name}' 中未定义 'run()' 函数。") sys.exit(1) # 第三步:获取 run() 函数 run_func = getattr(module, "run") if not callable(run_func): print(f"错误:'{task_name}.run' 不是一个可调用函数。") sys.exit(1) # 第四步:执行任务,并捕获其返回值 print(f"正在执行任务:{task_name}...") try: result = run_func() print(f"任务 '{task_name}' 执行成功。结果:{result}") except Exception as e: print(f"任务 '{task_name}' 执行失败:{e}") if __name__ == "__main__": main()

3.3 编写一个具体任务:tasks/backup.py

现在,我们来写第一个真实任务。它模拟一个简单的文件备份操作,打印日志并返回一个状态字典:

# tasks/backup.py import datetime import os def run(): """ 执行备份任务 :return: dict,包含状态和时间戳 """ print("【备份任务启动】") print(" 正在扫描待备份文件...") # 模拟一些工作 files_to_backup = ["config.json", "data.db", "logs/"] print(f" 发现 {len(files_to_backup)} 个待备份项:{files_to_backup}") print(" 正在执行压缩...") # 模拟耗时操作 import time time.sleep(0.5) timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") backup_filename = f"backup_{timestamp}.zip" print(f" 备份完成!存档文件:{backup_filename}") return { "status": "success", "backup_file": backup_filename, "timestamp": timestamp, "files_count": len(files_to_backup) }

3.4 运行与验证:从零到一的全过程

保存所有文件后,在终端中执行:

# 确保在 project/ 目录下 $ python task_runner.py backup 正在执行任务:backup... 【备份任务启动】 正在扫描待备份文件... 发现 3 个待备份项:['config.json', 'data.db', 'logs/'] 正在执行压缩... 备份完成!存档文件:backup_20240520_143022.zip 任务 'backup' 执行成功。结果:{'status': 'success', 'backup_file': 'backup_20240520_143022.zip', 'timestamp': '20240520_143022', 'files_count': 3}

整个过程,主程序task_runner.py没有一行代码硬编码backup这个名字。它只是:

  • 接收字符串backup;
  • 拼出文件路径tasks/backup.py;
  • 用importlib.util.spec_from_file_location和exec_module动态加载;
  • 用hasattr()确认run函数存在;
  • 用getattr()拿到函数对象;
  • 最后调用它。

新增一个sync.py任务?只需在tasks/下新建文件,写好run()函数,然后python task_runner.py sync就能跑起来。主程序代码零修改。这就是反射赋予的“可扩展性”。

4. 反射的暗礁与避坑指南:那些文档里不会写的实战教训

反射强大,但绝非银弹。我在多个项目中踩过坑,也见过同事因滥用反射导致线上故障。以下是最痛、最常被忽略的五个陷阱,附带我的实操解决方案。

4.1 陷阱一:getattr()的默认值陷阱——None不等于“没找到”

这是新手最容易栽跟头的地方。看这段代码:

class Config: def __init__(self): self.debug = True # 注意:这里没有定义 self.timeout config = Config() timeout = getattr(config, "timeout", 30) # 返回 30,没问题 debug = getattr(config, "debug", False) # 返回 True,没问题

看起来完美。但问题出在:如果config.timeout被显式设置为None呢?

config.timeout = None timeout = getattr(config, "timeout", 30) # 返回 None!不是 30!

getattr()的default参数,只在属性完全不存在时生效。一旦属性存在,哪怕值是None、0、""、[],它都会原样返回。这在配置解析中是灾难性的——你本意是“没配就用默认值”,结果用户配了个timeout: null(YAML),程序却拿到了None,后续time.sleep(None)直接报错。

我的解决方案:永远用hasattr()+getattr()组合,而不是依赖default

def get_config_value(obj, attr_name, default_value): if hasattr(obj, attr_name): value = getattr(obj, attr_name) # 如果值是 None,且你认为 None 代表“未配置”,才用默认值 if value is None: return default_value return value else: return default_value # 或者更激进:强制要求配置必须是非None值,否则报错 def get_required_config(obj, attr_name): if not hasattr(obj, attr_name): raise ValueError(f"必需配置项 '{attr_name}' 未设置") value = getattr(obj, attr_name) if value is None: raise ValueError(f"必需配置项 '{attr_name}' 不能为 None") return value

4.2 陷阱二:importlib动态导入的路径地狱

importlib.util.spec_from_file_location看似简单,但路径处理是深坑。常见错误:

  • 相对路径失效:spec_from_file_location("backup", "tasks/backup.py")在某些工作目录下会找不到文件,因为tasks/backup.py是相对路径,而spec的解析依赖当前工作目录(os.getcwd())。
  • .pyc缓存干扰:修改了backup.py,但importlib加载的还是旧的.pyc缓存。

我的解决方案:用Path.resolve()获取绝对路径,并强制清除缓存

def load_task_module_safe(task_name: str): task_file = Path("tasks") / f"{task_name}.py" # 关键:转为绝对路径,消除工作目录影响 abs_path = task_file.resolve() if not abs_path.exists(): # ... 错误处理 spec = importlib.util.spec_from_file_location(task_name, abs_path) if spec is None: # ... 错误处理 module = importlib.util.module_from_spec(spec) # 关键:在 exec_module 前,清除可能存在的旧模块缓存 # 防止多次运行时加载了旧版本 module_name = task_name if module_name in sys.modules: del sys.modules[module_name] spec.loader.exec_module(module) return module

4.3 陷阱三:反射调用破坏 IDE 的智能提示和静态检查

当你写getattr(obj, func_name)(),PyCharm、VS Code 的 Pylance 都会标黄警告:“Cannot determine type of ‘getattr’ call”。这意味着:

  • 代码跳转(Ctrl+Click)失效;
  • 参数提示(Parameter Hints)消失;
  • mypy等静态类型检查器无法校验run()函数的签名。

这会让大型项目协作变得痛苦。我的解决方案:用 Protocol 做类型提示(Python 3.8+)

from typing import Protocol, Any class TaskProtocol(Protocol): def run(self) -> Any: ... def load_and_run_task(task_name: str) -> Any: module = load_task_module_safe(task_name) if module is None: raise RuntimeError("Module load failed") # 显式声明:我们期望 module 有一个符合 TaskProtocol 的 run 方法 if not hasattr(module, "run") or not callable(getattr(module, "run")): raise RuntimeError(f"Module {task_name} does not have a valid run() method") # 类型提示:告诉IDE,run_func 是 TaskProtocol.run run_func = getattr(module, "run") # type: TaskProtocol.run return run_func()

虽然不能100%恢复跳转,但至少能让mypy知道run_func()的返回类型是Any,并能在run_func()调用处提供基本的Callable提示。

4.4 陷阱四:__getattr__的无限递归炸弹

当类定义了__getattr__,它会在常规属性查找失败后被调用。但如果__getattr__里又去访问自身其他属性,就可能触发无限递归:

class BadExample: def __getattr__(self, name): # 错误:这里访问 self.config,如果 config 也不存在,会再次调用 __getattr__ return self.config.get(name, None) # BOOM! 无限递归

我的解决方案:在__getattr__内部,永远用object.__getattribute__绕过自定义逻辑

class GoodExample: def __init__(self): self._config = {"timeout": 30} def __getattr__(self, name): # 安全:用 object.__getattribute__ 直接访问 _config,不触发 __getattr__ try: config = object.__getattribute__(self, "_config") return config.get(name, None) except AttributeError: # _config 本身都不存在?那就真错了 raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'")

4.5 陷阱五:反射带来的安全风险——别让字符串控制你的代码流

反射最大的风险,是把用户输入(如URL参数、配置文件内容、命令行参数)直接喂给getattr()或exec()。这等于把程序的“开关”交给了外部。

# 危险!绝对不要这样写 user_input = request.args.get("action") # 来自HTTP请求 func = getattr(some_obj, user_input) # 用户输入 'os.system' func("rm -rf /") # 系统被删!

我的解决方案:建立白名单,永远不信任外部输入

# 定义一个受控的、可执行的任务白名单 ALLOWED_TASKS = {"backup", "sync", "report", "health_check"} def safe_execute_task(user_task_name: str): # 第一步:严格校验输入是否在白名单内 if user_task_name not in ALLOWED_TASKS: raise ValueError(f"非法任务名:{user_task_name}") # 第二步:即使通过白名单,也要再次用 hasattr 确认模块里真有 run() module = load_task_module_safe(user_task_name) if not hasattr(module, "run"): raise RuntimeError(f"任务 {user_task_name} 缺失 run() 函数") return getattr(module, "run")() # 使用 # task_name = sanitize_input(request.args.get("task")) # 先做基础清洗 # result = safe_execute_task(task_name)

白名单是底线。对于更复杂的场景(如插件系统),我会用importlib.metadata.entry_points替代字符串拼接,让插件通过标准的entry_points注册,由Python生态本身保证安全性。

5. 反射的边界在哪里?什么情况下应该坚决放弃它

反射不是万能膏药。我见过太多项目,为了“显得高级”或“追求灵活”,在不该用的地方硬塞反射,结果代码变得晦涩难懂、调试困难、性能下降。以下是三个我划出的“反射禁飞区”,以及替代方案。

5.1 禁飞区一:高频、核心路径的性能敏感代码

假设你在写一个高频交易系统的订单匹配引擎,核心循环每秒执行百万次。其中一段逻辑需要根据订单类型("limit"、"market"、"stop")调用不同的价格计算函数。

错误做法(用反射):

# 每次循环都执行 calc_func = getattr(price_calculator, order_type + "_price") result = calc_func(order)

问题:getattr()是动态查找,涉及字典哈希、字符串比对、异常处理,比直接函数调用慢5-10倍。在百万次循环里,这点开销会被放大成秒级延迟。

正确做法(预缓存 + 字典映射):

# 在模块初始化时,一次性构建映射表 _PRICE_CALC_MAP = { "limit": price_calculator.limit_price, "market": price_calculator.market_price, "stop": price_calculator.stop_price, } # 在核心循环中,直接查字典(O(1)) calc_func = _PRICE_CALC_MAP.get(order_type) if calc_func is None: raise ValueError(f"Unknown order type: {order_type}") result = calc_func(order)

字典映射是反射的“静态化”替代。它牺牲了一点动态性(新增类型需改代码),但换来了极致的性能和可预测性。在性能敏感区,这是值得的。

5.2 禁飞区二:需要强类型保障和编译期检查的领域

在金融风控、医疗设备控制等对可靠性要求极高的系统中,一个拼写错误的函数名可能导致严重后果。getattr(obj, "caluclate_risk")(注意caluclate是错的)在运行前完全无法被发现。

错误做法:用反射驱动所有规则引擎。

正确做法:用抽象基类(ABC) + 工厂模式,把动态性控制在最小范围。

from abc import ABC, abstractmethod class RiskRule(ABC): @abstractmethod def calculate(self, data: dict) -> float: ... class CreditScoreRule(RiskRule): def calculate(self, data: dict) -> float: return data.get("credit_score", 0) * 0.5 class DebtRatioRule(RiskRule): def calculate(self, data: dict) -> float: return data.get("debt_ratio", 0) * 2.0 # 工厂:这里可以用反射(安全,因为只在启动时执行一次) def create_rule(rule_name: str) -> RiskRule: rule_classes = { "credit_score": CreditScoreRule, "debt_ratio": DebtRatioRule, } cls = rule_classes.get(rule_name) if cls is None: raise ValueError(f"Unknown rule: {rule_name}") return cls() # 使用:类型安全,IDE可跳转,mypy可检查 rule = create_rule("credit_score") # 类型是 RiskRule score = rule.calculate({"credit_score": 750}) # IDE知道 calculate 接收 dict,返回 float

工厂模式把反射的“危险区”(运行时调用)压缩到初始化阶段,而核心业务逻辑(rule.calculate())则是完全静态、类型安全的。

5.3 禁飞区三:团队协作中成员经验差异大的项目

在一个有新人、实习生、外包人员参与的项目里,过度使用反射会让代码变成“黑盒”。getattr(obj, config.action)()这行代码,对资深开发者是优雅,对新人却是天书——他不知道config.action从哪来,obj有哪些可能的方法,config.action的合法值有哪些。

错误做法:在公共SDK或基础组件中大量使用反射,却不提供清晰文档。

正确做法:用策略模式 + 显式枚举,把魔法变成常识。

from enum import Enum class ActionType(Enum): BACKUP = "backup" SYNC = "sync" REPORT = "report" class ActionExecutor: def __init__(self): self._handlers = { ActionType.BACKUP: self._handle_backup, ActionType.SYNC: self._handle_sync, ActionType.REPORT: self._handle_report, } def execute(self, action: ActionType, **kwargs): handler = self._handlers.get(action) if handler is None: raise ValueError(f"Unsupported action: {action}") return handler(**kwargs) def _handle_backup(self, **kwargs): # 具体实现 pass # 使用:ActionType 是一个清晰的、可枚举的、IDE可补全的类型 executor = ActionExecutor() executor.execute(ActionType.BACKUP, target="s3://bucket") # 新人一看就懂

枚举(Enum)提供了最强的可发现性。IDE能自动补全所有ActionType.XXX,文档可以清晰列出每个值的含义,新人grep ActionType就能找到所有相关逻辑。这比任何反射文档都管用。

6. 总结:反射是把好刀,但刀柄上刻着“慎用”

写完这篇,我重新翻了自己过去三年的项目代码,统计了一下反射的使用频率:在27个中大型项目中,平均每个项目只在3-5个关键节点用到了反射,且全部集中在“配置驱动”、“插件加载”、“通用序列化”这三类场景。其余95%以上的逻辑,都是干净利落的直接调用、字典映射或策略模式。

这印证了一个朴素的道理:反射的价值,不在于它能做什么,而在于它解决了哪些其他方案解决不了的、高成本的问题。当if-elif链超过10个分支,当新增一个功能要改5个地方,当配置和代码耦合到无法分离时——反射才真正亮出它的锋刃。

它不是炫技的舞台,而是工程妥协的产物。每一次getattr()的调用,都应该伴随着一个明确的、不可替代的理由。否则,就老老实实用字典,用枚举,用ABC。代码的可读性、可维护性、可调试性,永远比“看起来很酷”重要得多。

我个人在实际操作中的体会是:把反射当作一种“元编程”的胶水,而不是“主逻辑”的骨架。胶水用得恰到好处,能让整个结构严丝合缝;胶水用得太多太滥,反而会让结构在温湿度变化中开裂。下次当你想用getattr()时,不妨停顿两秒,问自己一句:这个问题,有没有更简单、更直接、IDE更友好的解法?如果答案是“有”,那就选那个。反射,永远留给那个“非它不可”的时刻。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进