Всегда ли безопасно изменять словарь `** kwargs`?
Используя синтаксис функции def f(**kwargs)
на языке Python, в функции создается словарь аргументов ключевого слова 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)
), такая модификация может быть проблемой?
Ответы
Ответ 1
Это всегда безопасно. Как говорит
Если присутствует форма "** identifier", она инициализируется новойупорядоченное сопоставление, получающее любые лишние аргументы ключевого слова, по умолчанию a новое пустое сопоставление того же типа.
Добавлен акцент.
Вы всегда можете получить новый объект-сопоставление внутри вызываемого. См. Этот пример
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!'
Это, вероятно, вы не увидите в дикой природе.
Ответ 2
Для кода уровня на языке Python kwargs
dict внутри функции всегда будет новым dict.
Однако для C-расширений следите. Версия API C kwargs
будет иногда передавать напрямую через dict. В предыдущих версиях он даже передавал подклассы dict напрямую, что приводило к ошибке (теперь исправлено), где
'{a}'.format(**collections.defaultdict(int))
создаст '0'
вместо повышения KeyError
.
Если вам когда-либо приходилось писать расширения C, возможно, включая Cython, не пытайтесь изменить эквивалент kwargs
и следить за подклассами dict в старых версиях Python.
Ответ 3
Оба вышеупомянутых ответа правильны, заявив, что технически мутация kwargs
никогда не будет влиять на родительские области.
Но... , что не конец истории. Ссылка на kwargs
может быть передана за пределы области действия, а затем вы запускаете все обычные проблемы с общим мутированием, которые вы ожидаете.
def create_objs(**kwargs):
class Class1:
def __init__(self):
self.options = kwargs
class Class2:
def __init__(self):
self.options = kwargs
return (Class1, Class2)
Class1, Class2 = create_objs(a=1, b=2)
a = Class1()
b = Class2()
a.options['c'] = 3
print(b.options)
# {'a': 1, 'b': 2, 'c': 3}
# other class options are mutated because we forgot to copy kwargs
Технически это отвечает на ваш вопрос, так как обмен ссылками на mutable
kwargs приводит к эффектам вне сферы видимости функции.
Я был несколько раз укушен этим в производственном коде, и это то, что я сейчас явно наблюдаю, как в моем собственном коде, так и при рассмотрении других. Ошибка очевидна в моем надуманном примере выше, но она сильно скрывается в реальном коде при создании factory funcs, которые используют некоторые общие параметры.