Подтвердить что ты не робот

Всегда ли безопасно изменять словарь `** 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)), такая модификация может быть проблемой?

4b9b3361

Ответ 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, которые используют некоторые общие параметры.