使用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)
)这种修改可能是个问题?
答案 0 :(得分:55)
总是安全的。作为spec says
如果表单“** identifier”存在,则会将其初始化为 new 有序映射接收任何多余的关键字参数,默认为 新相同类型的空映射。
强调增加。
始终保证在callable中获取新的mapping-object。见这个例子
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!'
但是你可能不会在野外看到它。
答案 1 :(得分:13)
对于Python级代码,函数内的kwargs
dict将始终是一个新的字典。
对于 C扩展,请注意。 {API}版本的kwargs
有时会直接通过dict。在以前的版本中,它甚至会直接传递dict子类,导致bug(now fixed)其中
'{a}'.format(**collections.defaultdict(int))
会生成'0'
,而不是提出KeyError
。
如果您必须编写C扩展(可能包括Cython),请不要尝试修改kwargs
等效项,并注意旧Python版本上的dict子类。
答案 2 :(得分:2)
上述两个答案都是正确的,从技术上说,变异kwargs
永远不会对父作用域产生影响。
但是...... 这不是故事的结尾。 引用到kwargs
可以在函数范围之外共享,然后您遇到所有常见的共享变异状态问题,这些问题都是您期望的。
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的引用确实会导致函数范围之外的影响。
我已经在生产代码中被多次咬过了,这是我现在明确注意到的,无论是在我自己的代码中还是在审查其他代码时。在我上面的设计示例中,这个错误显而易见,但在创建具有共同选项的工厂函数时,它在实际代码中更加狡猾。