如何为Python不可变容器类实现带成员更新的近似拷贝构造函数
首先得明确为什么你尝试的Container(**data0, b=7)会报错——这是Python函数调用的语法限制:当你用**展开一个Mapping作为关键字参数后,不能再传入同名的关键字参数,哪怕你是想覆盖它。而字典字面量的{**x, 'b':7}是Python专门为字典设计的语法糖,允许后面的键覆盖前面的,函数调用并不支持这种逻辑。
下面给你几个可行的优化方案,既能实现类似字典的合并更新逻辑,又能保持写法优雅,还不会改变实例类型:
方案一:在__init__中支持接收基实例参数
直接在类的构造方法里加一个base参数,先继承基实例的属性,再用传入的关键字参数覆盖:
from collections.abc import Mapping class Container: def __init__(self, base: Mapping = None, **kwargs): # 先加载基实例的所有属性 if base is not None: for key, value in base.items(): setattr(self, key, value) # 再用传入的参数覆盖或新增属性 for key, value in kwargs.items(): setattr(self, key, value) # 实现Mapping接口的必要方法 def keys(self): # 过滤掉私有属性,只返回用户定义的属性 return [k for k in dir(self) if not k.startswith('_')] def __getitem__(self, key): return getattr(self, key) def __repr__(self): attrs = {k: self[k] for k in self.keys()} return f"Container({attrs})"
调用方式非常直观:
data0 = Container(a=0, b=1, c=2) data3 = Container(base=data0, b=7) print(data3) # Container({'a': 0, 'b': 7, 'c': 2})
这个方案的优点是逻辑简单,容易理解,兼容性覆盖所有Python 3版本,而且完全不会改变实例的类型。
方案二:通过元类修改类的实例化逻辑
如果你更偏爱类似字典字面量的简洁写法,可以用元类重写类的__call__方法,让它支持直接传入基实例(作为位置参数),自动合并参数并覆盖:
from collections.abc import Mapping class ContainerMeta(type): def __call__(cls, *args, **kwargs): # 合并所有位置参数中的Mapping和关键字参数,后面的参数覆盖前面的 merged_kwargs = {} for arg in args: if isinstance(arg, Mapping): merged_kwargs.update(arg) merged_kwargs.update(kwargs) # 调用默认的实例化逻辑创建对象 return super().__call__(**merged_kwargs) class Container(metaclass=ContainerMeta): def __init__(self, **kwargs): for key, value in kwargs.items(): setattr(self, key, value) # 实现Mapping接口的方法 def keys(self): return [k for k in dir(self) if not k.startswith('_')] def __getitem__(self, key): return getattr(self, key) def __repr__(self): attrs = {k: self[k] for k in self.keys()} return f"Container({attrs})"
调用时可以直接把基实例作为位置参数传入,后面跟要修改的参数:
data0 = Container(a=0, b=1, c=2) data3 = Container(data0, b=7) print(data3) # Container({'a': 0, 'b': 7, 'c': 2})
这个方案的写法更接近你想要的优雅风格,而且实例类型依然是Container,不会产生子类的问题,兼容Python 3.5及以上版本。
方案三:在__init__中直接接收多个Mapping参数
不需要元类,直接在__init__里处理多个位置参数的Mapping,合并后初始化:
from collections.abc import Mapping class Container: def __init__(self, *maps: Mapping, **kwargs): merged = {} # 合并所有位置参数中的Mapping for m in maps: merged.update(m) # 合并关键字参数,覆盖前面的 merged.update(kwargs) # 初始化属性 for key, value in merged.items(): setattr(self, key, value) # 实现Mapping接口的方法 def keys(self): return [k for k in dir(self) if not k.startswith('_')] def __getitem__(self, key): return getattr(self, key) def __repr__(self): attrs = {k: self[k] for k in self.keys()} return f"Container({attrs})"
调用方式和方案二一样简洁:
data0 = Container(a=0, b=1, c=2) data3 = Container(data0, b=7) print(data3) # Container({'a': 0, 'b': 7, 'c': 2})
这个方案的优点是不需要依赖元类,实现更轻量,兼容性同样很好,逻辑也清晰易懂。
为什么原来的写法不行?
再补充解释一下:Python的函数调用语法中,**data0会把data0的所有键值对展开为关键字参数,当你后面再写b=7时,就相当于给函数传递了两次b参数,这在Python中是不允许的,所以会抛出TypeError: multiple values for keyword argument。而字典字面量的{**x, 'b':7}是Python的特殊语法,专门处理这种合并覆盖的场景,函数调用没有这个语法支持。
对比你之前用的Container(**{**data0, 'b':7}),上面的方案都避免了嵌套的**写法,让代码更简洁易读。
内容的提问来源于stack exchange,提问作者hiro protagonist




