Python関数のデフォルトパラメータの設計【オリジナル】

6895 ワード


Pythonチュートリアルでは、デフォルトのパラメータについて、「重要な警告」の例を示します.
def f(a, L=[]):
    L.append(a)
    return L

print(f(1))
print(f(2))
print(f(3))

デフォルト値は一度しか実行されず、理由も言われていません.結果が印刷されます.
[1]
[1, 2]
[1, 2, 3]

最初の言語はルビーなので、ちょっと変な感じがします.しかし、方法fは変数Lを必ず格納しているに違いない.
 
準備知識:ポインタ
pは、数値などの可変オブジェクトを指します.pポインタが異なるメモリアドレスを指していることに相当します.
pはlistなどの可変オブジェクトを指します.list自体の変更は、listオブジェクト自体が存在するメモリアドレスを変更することはありません.したがって、pが指すメモリアドレスは変更されません.
>>> p = 1
>>> id(p)
4523801232
>>> p = p + 1
>>> id(p)
4523801264
>>> p = 11
>>> id(p)
4523801552

>>> p = []
>>> id(p)
4527080640
>>> p.append(11)
>>> id(p)
4527080640

 
 
根本的な原因.
Python関数のパラメータのデフォルト値は、コンパイル段階でバインドされます.(コードを書くときに定義しました.)
 
Python Common Gotchasから抜粋した理由を説明します.
Python’s default arguments are evaluated once when the function is defined, not each time the function is called (like it is in say, Ruby). This means that if you use a mutable default argument and mutate it, you will and have mutated that object for all future calls to the function as well.
これにより、
  • コードを実行すると、関数定義を実行すると、デフォルトパラメータの式が実行されます. 
  • 関数呼び出しの場合、デフォルトパラメータの式は再実行されません.⚠️ これはルビーとは全然違います.
  • では、デフォルトのパラメータがL=1などの不変のオブジェクトを指していることがわかります.では、関数呼び出し時に、関数内でLに値を再割り当てます.Lは実は新しいポインタで、新しいメモリアドレスを指しています.従来のデフォルトパラメータL自体および指向メモリアドレスは、コンパイル開始時の関数定義に格納されている.使用可能_default__を参照してください.
  • デフォルトパラメータがlistのような可変オブジェクトを指す場合、L.append(a)は可変オブジェクト自体の変更であり、Lが指すメモリアドレスは変わらない.したがって、関数を呼び出すたびに、デフォルトのパラメータはこのメモリアドレスのオブジェクトを取り出します.

  • 第三条、上記の例を修正する.
     
    def f(a, L = 1):
        L = a
        print(id(L))
        return L
    
    print("self",id(f.__defaults__[0]))
    print(f(1))
    print("self",id(f.__defaults__[0]))
    print(f(33))
    print("self",id(f.__defaults__[0]))
    
    #    :
    self 4353170064
    4353170064
    1
    self 4353170064
    4353171088
    33
    self 4353170064

    デフォルトパラメータLは、コンパイルフェーズでバインドされ、__に格納されます.default__で行ないます.関数内のL=a式は、新しい変数を生成します.返されるLは、デフォルトのパラメータに関係なく、新しい変数です.
     
    4つ目は、上記の例で、デフォルトパラメータのタイプを可変オブジェクトリストに変更します.
    def f(a, L = []):
        L.append(a)
        print(id(L))
        return L
    # L = f(1)
    print("self",id(f.__defaults__[0]))
    print(f(1))
    print("self",id(f.__defaults__[0]))
    print(f(33))
    print("self",id(f.__defaults__[0]))
    #    
    self 4337586048
    4337586048
    [1]
    self 4337586048
    4337586048
    [1, 33]
    self 4337586048

    id番号から分かるように、デフォルトパラメータ自体が返されます.
     
    この罠を避けるために面倒をかける必要はありません
    def f(a, L = None):
        if L is None:
            L = []
        L.append(a)
        return L

     
    どうしてPythonがこんなデザインしたの?
    StackOverflowでは議論が多い.
     
    Rubyがこの問題を持たないのは,Rubyのdefキーワードが閉じた役割ドメインを定義しているためであり,どのデータもパラメータを介してメソッド内に転送しなければならないためであると考えられる.
    Rubyに比べてPythonパラメータの役割は大きく弱められた.Pythonの登場はRubyより遅く、その創始者はRubyのデザインを参考にしたに違いない.この設計を捨ててjavascriptのような関数方式を選択した.def定義の関数は,実行時に外部作用ドメインの変数を受信できる.
    次のような見方があります.
    Pythonコンパイラの実装を考慮すると、関数は内部の1級オブジェクトです.パラメータのデフォルト値は、このオブジェクトのプロパティです.他の言語では、オブジェクトプロパティはオブジェクトの作成時にバインドされます.したがって、関数パラメータのデフォルト値がコンパイル時にバインドされるのも不思議ではありません.
     
    以下を参照してください.http://cenalulu.github.io/python/default-mutable-arguments/#toc1と、自分の理解を加えた.転載を歓迎します!