尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

避开 Dataclass 的“经典大坑”:深入理解 field 中的 default 与 default_factory

避开 Dataclass 的“经典大坑”:深入理解 field 中的 default 与 default_factory 避开 Dataclass 的“经典大坑”深入理解 field 中的 default 与 default_factory摘要本文深入解析 Python dataclass 中field()函数的default与default_factory参数的核心区别与使用陷阱。通过对比分析揭示default在类定义时一次性求值“现成的”而default_factory在每次实例化时动态计算“现做的”的本质差异。重点探讨两个经典坑1可变对象list/dict/set共享问题2环境变量读取时机导致的配置加载陷阱。最后提供清晰的使用场景指南帮助开发者避免在实际项目中踩坑。max_concurrent_files:intfield(defaultget_env_value(MAX_CONCURRENT_FILES,1,int))Maximum number of files to process concurrently.# 同时最多处理多少文件supported_file_extensions:List[str]field(default_factorylambda:[x.strip()forxinget_env_value(SUPPORTED_FILE_EXTENSIONS,.pdf,.jpg,.jpeg,.png,.bmp,.tiff,.tif,.gif,.webp,.doc,.docx,.ppt,.pptx,.xls,.xlsx,.txt,.md,str,).split(,)# 环境变量里只能存字符串所以列表型配置就用逗号分隔的字符串存读出来再切分])List of supported file extensions for batch processing.# 可处理的文件类型上面是raganything的一段源码中文注释仅供参考这里涉及到了python装饰器dataclass中的一个“经典的坑”:field中的default与default_factory我们今天就来看看这两个参数的具体区别核心认知在dataclass的field()中这两个参数有着本质的区别default接收一个具体的值如1、“abc”。在类定义阶段直接求值把求值结果作为字段默认值可以理解为现成的值一次性算好。default_factory接收一个可调用对象Callable如函数、lambda、类名。不立刻执行只有每次实例化对象的时候才调用这个函数将返回结果作为默认值可以理解为现做的值每次新建对象重新计算。简单来说两者的核心区别在于“这个东西是现成的还是现做的”如果不太明白这句话不用着急看完后面的讲解你会理解的坑一可变对象的共享Python 普通类中如果默认参数是可变对象list/dict/set所有实例会共享同一个对象修改一个实例会污染其他实例。在我之前关于python参数传递机制那篇文章中有详细的讲解错误示范classWrong:def__init__(self,items[]):self.itemsitems aWrong()bWrong()a.items.append(1)print(a.items)# [1]print(b.items)# [1]被污染print(id(a.items),id(b.items))# 同一个内存地址而在dataclass中直接写可变默认值会直接抛异常提前阻止这个 bugfromdataclassesimportdataclass,fielddataclassclassWrong:items:list[]# 错误示范默认参数是可变对象aWrong()bWrong()a.items.append(1)print(a.items)print(f实例a的items地址:{id(a.items)})print(f实例b的items地址:{id(b.items)}) 在 dataclass 里可变默认值会在类定义时被直接拦截并报错ValueError: mutable default class list for field items is not allowed: use default_factory 补充该检测仅在默认 initTrue 生效如果设置 initFalsedataclass 不再做校验依旧会出现对象共享 bug。 items: list []这行代码的后果所有的实例共享同一个list对象。a.items.append(1)会影响到b.items。这就是“现成的”正确写法使用default_factorydataclassclassRight:items:listfield(default_factorylist)aRight()bRight()a.items.append(1)print(a.items)# [1]print(b.items)# []互相独立保证每次创建一个Right实例时都会生成一个全新的、独立的列表对象。这就是“现做的”坑二容易忽略执行时机带来的环境变量读取陷阱回到开篇的配置代码同样调用get_env_value()两者执行时机完全不同。字段max_concurrent_files****default类定义时执行模块导入瞬间defaultget_env_value(MAX_CONCURRENT_FILES,1,int)执行时机模块导入、类加载的时候结果get_env_value在程序启动时只执行一次返回值比如5被硬编码进类的默认值里。隐患如果你的程序运行后动态修改了系统环境变量或者这个配置依赖于运行时才生成的.env文件那么这个字段永远不会更新它死死记住了启动时的值。实战坑dotenv 加载时序问题# main.pyfromconfigimportProcessorConfig# 导入瞬间default里面get_env_value已经执行完毕fromdotenvimportload_dotenv load_dotenv()# 后加载.env已经晚了拿不到.env中的配置cfgProcessorConfig()此时max_concurrent_files永远只能拿到代码写死的回退值读不到.env里配置。两种解决方案先 load_dotenv再 import 配置类改用default_factory实例化阶段再读取环境变量。字段 supported_file_extensionsdefault_factory每次实例化才执行default_factorylambda:[x.strip()forxinget_env_value(SUPPORTED_FILE_EXTENSIONS,...,str).split(,)]执行时机每一次实例化对象时执行一次结果get_env_value在每次创建对象时都会被重新调用。优势假如你在程序运行中间修改了环境变量或者这个变量依赖于当前进程的上下文那么每次新实例都能拿到最新的值。代价频繁实例化对象时会重复读取环境变量、重复字符串处理带来额外开销。如果每秒创建大量配置对象会有性能损耗。小结参数接收类型执行时机使用场景default字面量、不可变对象int/str/bool/tuple类导入定义时仅一次静态不变配置简单数字字符串启动后不会变化的值default_factory可调用对象函数 /lambda每次实例化对象list/dict/set 可变容器需要运行时动态计算需要感知运行时环境变化
返回列表