修改 `**kwargs` 字典总是安全的吗?
Posted
技术标签:
【中文标题】修改 `**kwargs` 字典总是安全的吗?【英文标题】:Is it always safe to modify the `**kwargs` dictionary? 【发布时间】:2018-02-03 15:17:28 【问题描述】:使用Python函数语法def f(**kwargs)
,在函数中创建了一个关键字参数字典kwargs
,并且字典是可变的,所以问题是,如果我修改kwargs
字典,是否有可能在我的职能范围之外有什么影响?
根据我对字典解包和关键字参数打包工作原理的理解,我认为没有任何理由相信它可能不安全,而且在我看来,在 Python 3.6 中没有这种危险:
def f(**kwargs):
kwargs['demo'] = 9
if __name__ == '__main__':
demo = 4
f(demo=demo)
print(demo) # 4
kwargs =
f(**kwargs)
print(kwargs) #
kwargs['demo'] = 4
f(**kwargs)
print(kwargs) # 'demo': 4
但是,这是特定于实现的,还是 Python 规范的一部分?我是否忽略了任何情况或实现(除非修改自身可变的参数,例如kwargs['somelist'].append(3)
)这种修改可能是一个问题?
【问题讨论】:
对我来说,你的测试足以证明你的实现是安全的。还是够了吗?我很想知道答案。 @Rightleg 这个问题是在 FOSS 库函数的上下文中提出的,该函数旨在支持许多实现和用例。我相当确信它是安全的,但我没有任何铁定的理由会说:“如果这在某些实现中不安全,那就是一个错误。” 【参考方案1】:它总是安全的。作为spec says
如果存在“**identifier”形式,则将其初始化为new 接收任何多余关键字参数的有序映射,默认为 一个新的相同类型的空映射。
已添加重点。
您总是可以保证在可调用对象中获得一个新的映射对象。看这个例子
def f(**kwargs):
print((id(kwargs), kwargs))
kwargs = 'foo': 'bar'
print(id(kwargs))
# 140185018984344
f(**kwargs)
# (140185036822856, 'foo': 'bar')
因此,虽然f
可以修改通过**
传递的对象,但它不能修改调用者的**
对象本身。
更新:既然你问到了极端情况,这里对你来说是一个特殊的地狱,它实际上修改了调用者的kwargs
:
def f(**kwargs):
kwargs['recursive!']['recursive!'] = 'Look ma, recursive!'
kwargs =
kwargs['recursive!'] = kwargs
f(**kwargs)
assert kwargs['recursive!'] == 'Look ma, recursive!'
不过,这在野外你可能不会看到。
【讨论】:
我会说极端情况是“参数本身是可变的”的一种特殊情况,但它仍然很聪明。 @Paul - 确实,不需要像自指字典那样奇特的东西。如果输入字典的元素之一是可变的(如列表)并且该元素在函数内部发生了变异(例如.append()
),则输入字典将被变异。
@JohnY 是的,我在我的问题中使用了这个例子;)
@Paul - 啊,你做到了!那你去吧。 :) 除了聪明之外,这个答案中的示例还向我展示了一些我以前没有意识到的东西:输入字典中的键可以包含参数名称中非法的字符! “看马,感叹号!(还有空格,以及其他任何东西!)”
定义“安全”。它会炸毁你的电脑吗?可能不是。任意修改会导致您的功能按照您的意图进行吗?可能不是。你需要像对函数参数的任何其他修改一样小心——也许更要小心,因为这很容易让你的 Python 中的错误溜进来。【参考方案2】:
对于 Python 级别的代码,函数内的 kwargs
dict 将始终是一个新的 dict。
不过,对于 C 扩展,请小心。 kwargs
的 C API 版本有时会直接传递一个 dict。在以前的版本中,它甚至会直接传递 dict 子类,从而导致 bug (now fixed) where
'a'.format(**collections.defaultdict(int))
将产生'0'
而不是产生KeyError
。
如果您必须编写 C 扩展,可能包括 Cython,请不要尝试修改 kwargs
等效项,并注意旧 Python 版本上的 dict 子类。
【讨论】:
注意:如果你真的想要这个,请使用string.Formatter.vformat
。
...呵呵。我想事情已经改变了,因为我的旧 Python2 代码需要这个。当然,我什至需要使用未记录的内部结构来更改递归限制......
在这种情况下“有时”是什么时候?有这个参考吗?
@Paul: "Sometimes" 原因如下: 1) 据我所知,这是一个未记录的实现细节,我不知道某些 CPython 版本的行为是否不同,或者如果未来的版本可能会改变行为。 2) dict 子类修复意味着某些在技术上是 dicts 的对象(特别是子类实例)不会直接传递。 3)如果你做c_func(a=1, **b:2)
之类的事情,就会创建一个新的字典。
我认为,在任何情况下,当您向使用等效于 kwargs
的 C API 的 C 函数提供单个非子类关键字参数字典时,这种情况都会持续发生。不过,这只是我对源代码的记忆。我没有可引用的参考资料。【参考方案3】:
以上两个答案都是正确的,从技术上讲,变异 kwargs
永远不会对父作用域产生影响。
但是...... 故事还没有结束。对kwargs
的reference 可能会在函数范围之外共享,然后您会遇到所有常见的共享突变状态问题,这是您所期望的。
def create_classes(**kwargs):
class Class1:
def __init__(self):
self.options = kwargs
class Class2:
def __init__(self):
self.options = kwargs
return (Class1, Class2)
Class1, Class2 = create_classes(a=1, b=2)
a = Class1()
b = Class2()
a.options['c'] = 3
print(b.options)
# 'a': 1, 'b': 2, 'c': 3
# other class's options are mutated because we forgot to copy kwargs
从技术上讲,这回答了您的问题,因为共享对 mutable
kwargs 的引用确实会导致函数范围之外的影响。
我在生产代码中多次被此问题困扰,现在我明确注意这一点,无论是在我自己的代码中还是在查看其他代码时。这个错误在我上面人为设计的例子中很明显,但是在创建共享一些通用选项的工厂函数时,它在实际代码中要狡猾得多。
【讨论】:
以上是关于修改 `**kwargs` 字典总是安全的吗?的主要内容,如果未能解决你的问题,请参考以下文章