python inspect
### Python 的sys模块藏在标准库里的“瑞士军刀”1. 它到底是什么—— 程序与系统之间的“对话窗口”用最朴素的话说sys是 Python 解释器自带的一个工具箱专门负责处理“程序运行时”和“操作系统”之间的交互。它不是用来写业务逻辑的而是用来了解当前运行环境、控制解释器行为的。比如你的脚本跑在哪个 Python 版本上启动时传了哪些参数标准输出怎么重定向这些事儿sys全管。说白了sys就像一个透明的“窥视镜”能让你看到程序在系统层面的实时状态同时它又是个“遥控器”允许你调整一些底层的运行参数。大部分时候你可能用不上它但一旦需要其他库往往替代不了。2. 它能做什么—— 从“查户口”到“改设置”它的功能可以大致分成几类环境探测sys.version告诉你 Python 具体是 3.11 还是 3.12sys.platform告诉你是 Windows、Linux 还是 macOS。比如你在写跨平台脚本时判断不同操作系统下的路径分隔符就需要靠它。参数传递sys.argv是命令行参数列表比如执行python script.py --namekitty时它返回[script.py, --namekitty]这是绝大多数 CLI 工具的入口。输入输出控制sys.stdin、sys.stdout、sys.stderr对应标准输入、标准输出和错误输出。平时你用的print()其实默认就是把内容写到sys.stdout。如果想把输出重定向到文件直接改sys.stdout就行比如sys.stdout open(log.txt, w)。程序生命周期sys.exit()可以强制终止程序并且能带一个状态码通常 0 表示正常退出非 0 表示异常。sys.getrecursionlimit()和sys.setrecursionlimit()管理递归最大深度防止栈溢出。路径与模块sys.path是 Python 搜索模块的路径列表。如果你把第三方库装到了非标准位置可以把它的路径append到这个列表里Python 就能找到了。sys.modules存储了当前已加载的全部模块字典调试时偶尔用得到。3. 怎么使用—— 几个“接地气”的例子例子 1获取脚本运行时的参数想象你写了个图片处理工具importsysdefmain():iflen(sys.argv)2:print(用法: python process.py 文件名)sys.exit(1)filenamesys.argv[1]# 处理图片...print(f正在处理{filename})if__name____main__:main()这里sys.argv[0]永远是脚本名比如process.pysys.argv[1]才是第一个参数。很多人一开始会忘了检查长度直接拿sys.argv[1]然后程序崩了——所以先len(sys.argv)判断一下是常用的防御性写法。例子 2切换标准输出的编码解决中文乱码importsysifsys.platformwin32:sys.stdout.reconfigure(encodingutf-8)print(中文测试)# 在 Windows 终端里显示正常了sys.stdout.reconfigure()是 Python 3.7 后加入的方法可以修改已有流对象的编码。以前要搞到io.TextIOWrapper才改现在直接调就行。例子 3在调试时临时加一条搜索路径importsys sys.path.insert(0,/home/user/my_libs)# 优先搜索这个目录importcool_module# 这里的模块就能找到了注意用insert(0)而不是append()因为append会把路径放在末尾如果标准库里有同名模块会优先被找到容易出问题。4. 最佳实践——避开那些“坑”别轻易改sys.path尤其是不要在生产代码里频繁修改它。更好的做法是用虚拟环境、设置PYTHONPATH环境变量或者用importlib动态加载。如果必须改记得用sys.path.insert(0, ...)并且只用一次用完可以恢复原样。谨慎处理sys.stdin的缓冲比如你在写一个实时读取日志的脚本sys.stdin.readline()默认是行缓冲的可能不是实时的。可以用sys.stdin.buffer.raw.read(1)来读单字节相当于绕过缓冲但要注意性能。sys.exit()配合try-finallysys.exit()实际上是用SystemExit异常来终止程序所以如果有一段释放资源的代码写在try块里它仍然会被finally捕获并执行。但要注意不要在finally里又调用sys.exit()那样会导致异常链混乱。sys.getsizeof()的陷阱它返回对象占用的内存大小字节但只计算对象本身不包含引用对象。例如一个列表[1, 2, 3]sys.getsizeof()只算列表对象通常 72 字节左右里面的整数对象不算在内。要算整棵对象树得用专门的pympler库。生产环境慎用sys.stdout.reconfigure()在日志框架或多线程环境中修改标准流可能引起竞态条件。建议用logging模块配合StreamHandler来完成编码或重定向的需求。5. 和同类技术对比——它不可替代的地方功能领域sys其他替代/补充关键区别命令行参数sys.argvargparse、click、typersys.argv是原始字符串列表适合脚本极简场景argparse帮你解析--name这种命名参数、类型转换、生成帮助信息功能全面但重量级。通常先拿sys.argv快速验证再用argparse正式发布。环境变量os.environsys没有直接处理环境变量的功能二者经常混淆。os.environ是一个类似字典的对象专门管理环境变量。sys主要管解释器内部状态。路径管理sys.pathpathlib、os.path、PYTHONPATH环境变量sys.path是运行时修改模块搜索路径的最直接方式但pathlib更适合做路径字符串操作比如拼接、判断存在。生产代码建议用虚拟环境或PYTHONPATH来固化路径而不是写死在代码里。标准流重定向sys.stdin/stdout/stderrio.StringIO、contextlib.redirect_stdout直接改sys.stdout简单粗暴但容易污染全局contextlib.redirect_stdout提供了一个上下文管理器withinwith块里重定向退出时自动恢复——安全性高很多。程序退出sys.exit()os._exit()、raise SystemExitsys.exit()本质就是raise SystemExit能被try-except捕获而os._exit()直接调用系统底层exit()不会执行任何清理比如finally、__del__方法通常只# ## 聊聊Python的inspect模块一个有点被低估的工具写Python这么多年我经常会碰到一些场景比如接手一个遗留系统面对一个2000行的类想知道某个方法到底从哪里继承来的或者写框架的时候需要拿到调用方的局部变量。这时候inspect模块就像是工具箱里的瑞士军刀平时不显眼关键时刻真能解决问题。它到底是什么说起来简单inspect就是Python标准库里用来“窥探”运行时对象的工具。但不是那种粗浅的type()或者dir()之类的东西它更像是一个反射工具箱甚至可以查到对象的源码文件位置、行号、调用栈这些底层信息。举个例子你写了个装饰器里面打个日志想知道是哪个函数调用了当前函数。这时候inspect.currentframe()就能派上用场从调用栈里抓出上一层的帧对象。能做什么除了“查看对象”之外很多人以为inspect就是拿来看类或者函数的签名其实远不止这些。追踪调用来源- 调试的时候尤其是那些通过回调串联起来的逻辑inspect.stack()能帮你在运行时重建调用链路。比如在大型web框架里某个hooks触发后你想知道是谁触发的用inspect.stack()打印出调用栈一目了然。动态获取参数名- 写过API框架的应该碰到过把一个字典传递给函数但函数签名里写着**kwargs。这时候inspect.signature()能帮你拿到这个函数实际接受的参数名然后你就可以从字典里按需提取参数。检查是否是数据类- Python3.7引入数据类后有时候需要在运行时判断某个类是不是dataclass修饰过的。is_dataclass()函数就是干这个的比自己用type hints判断靠谱多了。获取源码- 偶尔需要动态显示某个函数的实现。inspect.getsource()直接返回字符串比用ast模块解析一遍轻松多了。比如写个教学工具想显示当前正在运行的代码。模拟调试器- 如果要自己写个简单的调试器或者跟踪器inspect.currentframe()加上traceback模块就够了。实际上Python自带的trace模块底层就是用到了inspect。怎么用几个常见场景场景一写一个灵活的日志装饰器importfunctoolsimportinspectimportloggingdeflog_args_and_caller(func):functools.wraps(func)defwrapper(*args,**kwargs):# 获取调用者的信息frameinspect.currentframe().f_backifframe:caller_nameframe.f_code.co_name caller_fileinspect.getfile(frame)else:caller_namecaller_fileunknownlogging.info(fFunction{func.__name__}called by{caller_name}at{caller_file})returnfunc(*args,**kwargs)returnwrapper这个例子比较实际不是简单的打印参数而是记录了是谁调用了这个函数。在调试复杂系统时这个信息通常比单纯的参数值更有用。场景二构建一个简单的依赖注入importinspectdefresolve_dependencies(func):siginspect.signature(func)params{}forname,paraminsig.parameters.items():# 如果参数有类型注解尝试从容器中获取ifparam.annotation!inspect.Parameter.empty:# 假设有个全局容器 servicesifparam.annotationinservices:params[name]services[param.annotation]else:# 没标注类型的参数用默认值param.defaultifparam.default!inspect.Parameter.emptyelseNonereturnfunc(**params)这里的关键是inspect.signature()能获取到参数的元信息包括类型注解和默认值。有了这些就能实现类似Java里Spring框架那样的依赖注入。场景三调试时查看变量defdebug_variables():frameinspect.currentframe()# 获取局部变量local_varsframe.f_locals# 获取闭包变量closure_varsframe.f_globals# 找个适合输出importpprint pprint.pprint(local_vars)写调试工具时这个很有用比手动打印变量少了很多重复劳动。最佳实践不要滥用但要用在点子上inspect能力很强但有两个常见的坑性能开销- inspect.currentframe()涉及到C扩展调用每次调用都有不小的开销。在性能敏感路径上比如每秒调用几百次的函数里尽量不用或者缓存结果。我曾经在一个日志模块里用inspect获取调用者的信息上线后发现接口响应时间从50ms飙到了200ms最后只好换成手动传参。容易造成循环依赖- 如果你写的框架用了inspect.getouterframes()或者类似的方法注意递归调用的问题。比如有个类A继承自基类B基类B的初始化方法里又调用了子类A的方法inspect扫描父类时可能会陷入死循环。所以我的建议是inspect适合用在开发环境或调试工具里生产环境慎用。真要用了最好加上条件判断比如只在DEBUG模式开启时才使用。和同类技术对比Python反射这块有几个选择type(), dir(), hasattr()这些内置函数还有ast模块和inspect模块。inspect vs 内置反射函数内置函数适合快速查询比如type(obj), isinstance(obj, ClassName)这些。它们直接性能好。但要做复杂的分析比如获取函数的完整签名、参数类型注解内置函数就不行了。举个例子dir(obj)能列出所有属性和方法但你分不清哪些是实例方法、哪些是类方法。inspect.ismethod()能帮你区分。inspect vs astast模块是在代码编译前静态分析的而inspect是在运行时动态分析的。两者定位不同ast适合做代码分析工具比如lint工具、代码生成器。你有源文件或代码字符串想分析语法结构用ast。inspect适合在运行时动态获取信息比如框架在运行时需要根据函数签名做依赖注入。想象一个场景你想知道一个函数用了哪些变量。用inspect只能看到函数的代码对象包含一些元信息但拿不到完整的语法树。这时用ast解析函数源码就对了。inspect vs sys.getframe/gettracesys模块也可以获取帧对象但比较低层功能单一。inspect是在sys的基础上封装了一层提供了更丰富的接口比如stack()直接返回调用链上的所有帧对象不用自己一个个f_back往上跳。从实用性上看inspect是平衡了易用性和功能深度的选择。它不像ast那么静态也不像sys那么底层正好适合做运行时反射这类事情。写框架或工具时如果发现需要反射的地方越来越多可以考虑用inspect作为基础但不建议让inspect成为业务代码的常用依赖。它更像是一把手术刀偶尔切一刀解决问题就好别拿着它当菜刀用。在fork()子进程里用普通脚本别碰。 |总结一句sys是 Python 标准库里的“元工具”它让你能控制解释器自身的行为。用它的地方通常比较底层、比较精细也往往伴随着一些风险。在写核心工具、框架或调试环境问题时它会发光但在普通业务代码里尽量用更高层的封装比如argparse代替sys.argvcontextlib.redirect_stdout代替直接赋值会更稳妥。