修改 `**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 永远不会对父作用域产生影响。

但是...... 故事还没有结束。对kwargsreference 可能会在函数范围之外共享,然后您会遇到所有常见的共享突变状态问题,这是您所期望的。

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` 字典总是安全的吗?的主要内容,如果未能解决你的问题,请参考以下文章

def 的来龙去脉

python--*args与**kw

C中的有符号到无符号转换 - 它总是安全的吗?

编写新变量和新值的字典。用它来定义值

如何将字典转换为关键字参数字符串?

字典的创建修改删除遍历