web-dev-qa-db-ja.com

`** kwargs`辞書を変更するのは常に安全ですか?

Python関数構文def f(**kwargs)を使用すると、関数内にキーワード引数辞書kwargsが作成され、辞書は変更可能です。 kwargs辞書、関数のスコープ外で何らかの効果がある可能性はありますか?

辞書のアンパックとキーワード引数のパックがどのように機能するかについての私の理解から、それが安全でない可能性があると信じる理由は見当たらず、Python 3.6:

def f(**kwargs):
    kwargs['demo'] = 9

if __== '__main__':
    demo = 4
    f(demo=demo)
    print(demo)     # 4

    kwargs = {}
    f(**kwargs)
    print(kwargs)   # {}

    kwargs['demo'] = 4
    f(**kwargs)
    print(kwargs)    # {'demo': 4}

しかし、これは実装固有のものですか、それともPython仕様の一部ですか?自分自身 mutableの引数への変更がない限り、状況や実装を見落としていますか、kwargs['somelist'].append(3)など)この種の変更が問題になる可能性がありますか?

47
Paul

常に安全です。 仕様によると

フォーム「** identifier」が存在する場合、余分なキーワード引数を受け取るnew順序マッピングに初期化され、デフォルトはnew同じタイプの空のマッピング。

エンファシスが追加されました。

Callable内で新しいマッピングオブジェクトを取得することが常に保証されます。この例をご覧ください

def f(**kwargs):
    print((id(kwargs), kwargs))

kwargs = {'foo': 'bar'}
print(id(kwargs))
# 140185018984344
f(**kwargs)
# (140185036822856, {'foo': 'bar'})

そのため、f**を介して渡されるオブジェクトを変更できますが、呼び出し元の**-オブジェクト自体を変更することはできません。


Update:コーナーケースについて質問したので、実際に呼び出し元のkwargsを変更する特別な地獄があります:

def f(**kwargs):
    kwargs['recursive!']['recursive!'] = 'Look ma, recursive!'

kwargs = {}
kwargs['recursive!'] = kwargs
f(**kwargs)
assert kwargs['recursive!'] == 'Look ma, recursive!'

しかし、これはおそらく野生では見られないでしょう。

55
user2722968

Pythonレベルのコードの場合、関数内のkwargs dictは常に新しいdictになります。

ただし、C拡張機能には注意してください。 kwargsのC APIバージョンは、直接辞書を渡す場合があります。以前のバージョンでは、dictサブクラスを直接渡すこともあり、バグにつながりました( 現在修正済み

'{a}'.format(**collections.defaultdict(int))

'0'KeyErrorを上げる代わりに。

Cythonを含むC拡張を記述する必要がある場合は、同等のkwargsを変更しようとせず、古いPythonバージョンのdictサブクラスに注意してください。

13
user2357112

上記の両方の答えは、技術的には、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への参照を共有すると、関数スコープの範囲外の効果が生じるため、技術的にはこれがあなたの質問に答えます。

私はこれを本番コードで何度も噛まれてきましたが、私自身のコードと他のコードをレビューするときの両方で、今のところ明示的に気をつけていることです。上記の不自然な例では間違いは明らかですが、いくつかの共通オプションを共有するファクトリーfuncを作成する場合、実際のコードでは非常に巧妙です。

2
Nick Sweeting