python - principle of least astonishment




「最悪の驚き」と変更可能なデフォルト引数 (20)

Pythonを使いこなす人は誰でも、次のような問題によって噛まれてしまった(または断片化している)

def foo(a=[]):
    a.append(5)
    return a

Python初心者は、この関数が常に1つの要素、すなわち[5]だけを持つリストを返すと期待します。 結果は非常に異なっており、非常に驚​​くべきことです(初心者のために):

>>> foo()
[5]
>>> foo()
[5, 5]
>>> foo()
[5, 5, 5]
>>> foo()
[5, 5, 5, 5]
>>> foo()

私のマネージャーは、かつてこの機能との最初の出会いがあり、それを "劇的なデザイン上の欠陥"と呼んでいました。 私はその行動に根本的な説明があったと答えました。そして、あなたが内部を理解していなければ、それは本当に困惑していました。 しかし、私は以下の質問に答えることができませんでした:関数の実行時ではなく、関数定義でデフォルト引数をバインドする理由は何ですか? 私は経験豊富な振る舞いが実際に使用されているのか疑問に思っています(バグを育てることなくCで静的変数を本当に使っていましたか?

編集

Baczekは興味深い例を作りました。 あなたの意見の大部分とUtaalのとりわけとともに、私はさらに詳しく述べました:

>>> def a():
...     print("a executed")
...     return []
... 
>>>            
>>> def b(x=a()):
...     x.append(5)
...     print(x)
... 
a executed
>>> b()
[5]
>>> b()
[5, 5]

私にとっては、設計の決定は、パラメータの範囲をどこに置くかに比例していると思われます。

関数内でバインディングを行うと、定義されていない関数が呼び出されたときに、 xが実際に指定されたデフォルトに効果的にバインドされることになります。深刻な欠陥を示すもの: def行はバインディングの一部(関数オブジェクトの)定義が発生し、部分(デフォルトパラメータの代入)が関数の呼び出し時に発生します。

実際の動作はより一貫しています。その行が実行されると、その行のすべてが評価されます。つまり、関数定義で意味されます。


Pythonの防御に5ポイント

  1. シンプルさ :この行動は次のような意味で単純です。ほとんどの人はこのトラップに数回ではなく1回しか入りません。

  2. 一貫性 :Pythonは常に名前ではなくオブジェクトを渡します。 デフォルトのパラメータは、明らかに、関数見出しの一部です(関数本体ではありません)。 したがって、関数呼び出し時ではなく、モジュールのロード時(ネストされていない限り、モジュールのロード時のみ)に評価する必要があります。

  3. 有用性 :Frederik Lundhが"Pythonでのデフォルトパラメータ値"についての彼の説明で指摘しているように、現在の動作は高度なプログラミングにとって非常に役立ちます。 (控えめに使用してください)

  4. 十分な文書化 :最も基本的なPythonのドキュメント、チュートリアルでは、 関数の定義の詳細」の第1項のサブセクションで"重要な警告"として大きな問題が発表されています。 警告では太字が使用されていますが、見出しの外側に適用されることはほとんどありません。 RTFM:詳細なマニュアルを読んでください。

  5. メタラーニング :トラップへの落とし込みは、実際には(少なくともあなたが反射的な学習者であれば)非常に有用な瞬間です。上記の「一貫性」のポイントをよりよく理解することができ、Pythonについて多くのことを教えてくれるからです。


Python:変更可能なデフォルト引数

デフォルトの引数は、関数が関数オブジェクトにコンパイルされた時点で評価されます。関数によって複数回使用されると、それらは同じオブジェクトであり、同じオブジェクトのままです。

それらが変更可能であるとき、突然変異したとき(例えばそれに要素を加えることによって)、それらは連続した呼び出しで突然変異したままである。

それらは毎回同じオブジェクトであるため、突然変異したままです。

同等のコード:

関数オブジェクトがコンパイルされてインスタンス化されるとき、リストは関数に束縛されるので、これは:

def foo(mutable_default_argument=[]): # make a list the default argument
    """function that uses a list"""

これとほぼ同じです:

_a_list = [] # create a list in the globals

def foo(mutable_default_argument=_a_list): # make it the default argument
    """function that uses a list"""

del _a_list # remove globals name binding

デモンストレーション

デモンストレーションは次のとおりです。これらのデモは、参照されるたびに同じオブジェクトであることを確認できます。

  • 関数が関数オブジェクトへのコンパイルを完了する前にリストが作成されているのを見て、
  • リストが参照されるたびにidが同じであることを確認し、
  • リストを使用する関数が2回目に呼び出されたときにリストが変更されたままであることを観察すると、
  • 出力がソースから印刷される順序を観察します(これは便利な番号です)。

example.py

print('1. Global scope being evaluated')

def create_list():
    '''noisily create a list for usage as a kwarg'''
    l = []
    print('3. list being created and returned, id: ' + str(id(l)))
    return l

print('2. example_function about to be compiled to an object')

def example_function(default_kwarg1=create_list()):
    print('appending "a" in default default_kwarg1')
    default_kwarg1.append("a")
    print('list with id: ' + str(id(default_kwarg1)) + 
          ' - is now: ' + repr(default_kwarg1))

print('4. example_function compiled: ' + repr(example_function))


if __name__ == '__main__':
    print('5. calling example_function twice!:')
    example_function()
    example_function()

それを実行するpython example.py

1. Global scope being evaluated
2. example_function about to be compiled to an object
3. list being created and returned, id: 140502758808032
4. example_function compiled: <function example_function at 0x7fc9590905f0>
5. calling example_function twice!:
appending "a" in default default_kwarg1
list with id: 140502758808032 - is now: ['a']
appending "a" in default default_kwarg1
list with id: 140502758808032 - is now: ['a', 'a']

これは "最小驚異"の原則に違反していますか?

この実行順序は、Pythonの新しいユーザーにとってはしばしば混乱します。Pythonの実行モデルを理解すれば、それはかなり期待されます。

新しいPythonユーザーへの通常の指示:

しかし、これは新しいユーザーへの通常の指示は、代わりにこのようなデフォルトの引数を作成することです:

def example_function_2(default_kwarg=None):
    if default_kwarg is None:
        default_kwarg = []

これはNoneをシニネルオブジェクトとして使用して、デフォルト以外の引数を取得したかどうかを関数に伝えます。引数がなければ、実際には新しい空のリストを[]デフォルトとして使用します。

以下のような制御フローのチュートリアルセクション言います:

後続の呼び出し間でデフォルトを共有したくない場合は、代わりに次のような関数を書くことができます:

def f(a, L=None):
    if L is None:
        L = []
    L.append(a)
    return L

なぜあなたはイントロスペクトしないのですか?

私は本当に誰もCallableでPython( 23適用される)が提供する洞察的なイントロスペクションを実行していないことに驚いています。

与えられた単純な関数func次のように定義されます。

>>> def func(a = []):
...    a.append(5)

Pythonがそれに遭遇すると、まずこの関数のcodeオブジェクトを作成するためにPythonがコンパイルされます。 このコンパイルのステップが行われている間、 Python *を評価して 、デフォルトの引数(ここでは空のリスト[] )を関数オブジェクト自体に格納します 。 上の答えが述べられているように、リストaは関数func メンバーと見なすことができます。

関数オブジェクト内でリストがどのように展開されるかを調べるために、前と後のいくつかのイントロスペクションをしましょう。 私はこのためにPython 3.xを使用しています__defaults__ 2では同じことが適用されます(Python 2では__defaults__またはfunc_defaultsを使用します;同じ場合は2つの名前)。

実行前の機能:

>>> def func(a = []):
...     a.append(5)
...     

Pythonがこの定義を実行すると、指定されたデフォルトのパラメータ(ここではa = []__defaults__され、関数オブジェクト (関連セクション:Callables) __defaults__属性で__defaults__ます

>>> func.__defaults__
([],)

__defaults__ 、期待どおり、 __defaults__の1つのエントリとして空のリストが表示されます。

実行後の機能:

この関数を実行しましょう:

>>> func()

さて、これらの__defaults__もう一度見てみましょう:

>>> func.__defaults__
([5],)

驚いた? オブジェクト内の値が変わります! 関数への連続呼び出しは、単にその埋め込みlistオブジェクトに追加されます:

>>> func(); func(); func()
>>> func.__defaults__
([5, 5, 5, 5],)

だから、あなたが持っているのは、この「欠陥」が起こる理由は、デフォルトの引数が関数オブジェクトの一部であるからです。 ここでは何も変わっていません。ちょっと驚くべきことです。

この問題を解決する一般的な解決方法は、 Noneをデフォルトとして使用し、関数本体で初期化することです。

def func(a = None):
    # or: a = [] if a is None else a
    if a is None:
        a = []

関数本体は毎回新たに実行されるので、引数が渡されなければ、常に新しい空のリストが新しくなります。

__defaults__のリストが関数funcで使用されているものと同じであることをさらに検証するには、関数本体内で使用されているリストのidを返すように関数を変更するだけです。 次に、それを__defaults____defaults__位置[0] )のリストと比較すると、これらが実際に同じリストインスタンスをどのように参照しているかがわかります。

>>> def func(a = []): 
...     a.append(5)
...     return id(a)
>>>
>>> id(func.__defaults__[0]) == func()
True

すべての内省の力で!

* Pythonが関数のコンパイル時にデフォルト引数を評価することを確認するには、以下を実行してみてください:

def bar(a=input('Did you just see me without calling the function?')): 
    pass  # use raw_input in Py2

関数をビルドして名前barバインドする前に、 input()が呼び出されます。


1)「Mutable Default Argument」といういわゆる問題は、一般的に次のことを示す特別な例です:
"この問題のすべての関数は、実際のパラメータと同様の副作用の問題からも苦しんでいます。"
これは関数型プログラミングのルールに反し、通常は望ましくないので、両方とも一緒に修正する必要があります。

例:

def foo(a=[]):                 # the same problematic function
    a.append(5)
    return a

>>> somevar = [1, 2]           # an example without a default parameter
>>> foo(somevar)
[1, 2, 5]
>>> somevar
[1, 2, 5]                      # usually expected [1, 2]

解決策コピー
絶対的に安全な解決策は、まず入力オブジェクトcopydeepcopy、次にコピーと何かを行うことです。

def foo(a=[]):
    a = a[:]     # a copy
    a.append(5)
    return a     # or everything safe by one line: "return a + [5]"

多くの組み込み変更可能なタイプは次のようにコピーする方法を持っているsome_dict.copy()some_set.copy()などを簡単にコピーすることができますsomelist[:]list(some_list)。各オブジェクトはまたによってコピーすることができcopy.copy(any_object)、またはによってより徹底的なcopy.deepcopy()(可変オブジェクトが変更可能なオブジェクトから構成されている場合、後者の有用)。オブジェクトの中には、基本的に「ファイル」オブジェクトのような副作用に基づいているものもあり、コピーによって意味のある複製ができないものもあります。 copying

同様のSOの質問の問題例

class Test(object):            # the original problematic class
  def __init__(self, var1=[]):
    self._var1 = var1

somevar = [1, 2]               # an example without a default parameter
t1 = Test(somevar)
t2 = Test(somevar)
t1._var1.append([1])
print somevar                  # [1, 2, [1]] but usually expected [1, 2]
print t2._var1                 # [1, 2, [1]] but usually expected [1, 2]

この関数によって返されたインスタンスのpublic属性には保存しないでください。(インスタンスのプライベート属性を、このクラスの外から、または慣例によってサブクラスから変更してはならない、つまりプライベート属性であると仮定して_var1

結論:
入力パラメータオブジェクトは、変更されてはいけません(変更されている)か、関数によって返されたオブジェクトにバインドされるべきではありません。(副作用のないプログラミングを優先すると強く推奨されますが、副作用についてはWikiを参照してください(最初の2つの段落は関連しています)。

2)
実際のパラメータに対する副作用が必要であるが、デフォルトのパラメータで望ましくない場合にのみ、有用な解決策はdef ...(var1=None): if var1 is None: var1 = [] More..

3)場合によっては、デフォルトパラメータの変更可能な動作が有用である


これは設計上の欠陥ではありません。この上を旅する人は、何か間違っている。

この問題が発生する可能性のある箇所が3つあります。

  1. 関数の副作用として引数を変更するつもりです。この場合、デフォルトの引数を持つことは決して意味がありません。唯一の例外は、あなたが関数の属性を持つために引数リストを乱用してcache={}いる場合で、実際の引数で関数を呼び出すことはまったく期待されません。
  2. あなたは引数を変更しないままにするつもりですが、あなたは誤っそれを変更しました。それはバグです、修正してください。
  3. 関数内で使用するために引数を変更しようとしていますが、関数の外部で変更を表示できるとは考えていませんでした。その場合は、引数のコピーを作成する必要があります。デフォルトであるかどうかは関係ありません。Pythonは値渡しの言語ではないため、コピーを作成しません。明示的に説明する必要があります。

質問の例は、カテゴリ1またはカテゴリ3に分類される可能性があります。それは、渡されたリストを変更して返します。あなたはどちらかを選ぶべきです。


あなたが求めているのは、なぜこれです:

def func(a=[], b = 2):
    pass

これと内部的には同等ではありません。

def func(a=None, b = None):
    a_default = lambda: []
    b_default = lambda: 2
    def actual_func(a=None, b=None):
        if a is None: a = a_default()
        if b is None: b = b_default()
    return actual_func
func = func()

func(None、None)を明示的に呼び出す場合を除き、無視します。

言い換えると、デフォルトのパラメータを評価する代わりに、それぞれのパラメータを保存せずに、関数が呼び出されたときに評価するのはなぜですか?

1つの答えはおそらくそこにあります。それは、デフォルトのパラメータを持つすべての関数を効果的にクロージャに変換します。 たとえ完全に閉鎖されていなくても、インタプリタに隠されていても、データはどこかに保存されなければなりません。 それは遅くなり、より多くのメモリを使用します。


この動作は次のように簡単に説明できます。

  1. 関数(クラス等)の宣言は一度だけ実行され、すべてのデフォルト値オブジェクト
  2. すべてが参照渡しです

そう:

def x(a=0, b=[], c=[], d=0):
    a = a + 1
    b = b + [1]
    c.append(1)
    print a, b, c
  1. aは変更されません - すべての代入呼び出しは新しいintオブジェクトを作成します - 新しいオブジェクトが出力されます
  2. bは変更されません - 新しい配列はデフォルト値からビルドされ、印刷されます
  3. c変更 - 同じオブジェクトに対して操作が実行され、印刷されます

これは実際にはデフォルト値とは無関係ですが、変更可能なデフォルト値を持つ関数を書くときには予期しない動作となることがよくあります。

>>> def foo(a):
    a.append(5)
    print a

>>> a  = [5]
>>> foo(a)
[5, 5]
>>> foo(a)
[5, 5, 5]
>>> foo(a)
[5, 5, 5, 5]
>>> foo(a)
[5, 5, 5, 5, 5]

このコードでは既定値は見当たりませんが、まったく同じ問題が発生します。

問題は、それがされfooている修正呼び出し側はこれを期待していない場合、呼び出し側から渡された変更可能な変数を。このようなコードは、関数が次のように呼び出された場合には問題ありませんappend_5。呼び出し側は、渡した値を変更するために関数を呼び出すことになり、その動作は期待されます。しかし、このような関数はデフォルトの引数を取ることは非常に困難で、おそらくリストを返すことはありません(呼び出し元はすでにそのリストを参照しています。

fooデフォルトの引数を持つオリジナルは、a明示的に渡されたのか、デフォルト値を受けたのかを変更するべきではありません。context / name / documentationから引数が変更されるはずであることが明らかでない限り、コードは変更可能な引数だけを残すべきです。ローカルの一時変数として引数として渡された変更可能な値を使用することは、Pythonであろうとなかろうと、デフォルトの引数が関係しているかどうかにかかわらず、非常に悪い考えです。

あなたが何かを計算する過程で局所的な一時的なものを破壊的に操作する必要があり、引数の値から操作を開始する必要がある場合は、コピーを作成する必要があります。


それは事実かもしれません:

  1. 誰かがすべての言語/ライブラリ機能を使用しています。
  2. ここでの行動を変えることはあまりお勧めできませんが

上記の両方の機能を保持することは完全に一貫しており、それでもなお別の点を立てています。

  1. それは混乱する機能であり、Pythonでは残念です。

他の答え、または少なくともそれらのうちのいくつかはポイント1と2を作るが、ポイント3とダウンポイント1と2を作る。しかし、3つはすべて正しい。

ここで途中の馬を切り替えると大きな破損が起きる可能性があります。また、Pythonを変更してStefanoのオープニングスニペットを直感的に扱うことで問題が発生する可能性もあります。そして、Pythonの内部構造をよく知っている人が、地雷の影響を説明できるのは事実かもしれません。 しかしながら、

既存の振る舞いはPythonではありません。Pythonは成功しています。なぜなら、その言語が近くで最も驚くべきことの原則に違反しているからですこれはひどい。それを根絶することが賢明であろうと、それは本当の問題です。それは設計上の欠陥です。あなたがその振る舞いを追跡することによって言語をはるかに良く理解すれば、私はC ++がこれ以上のことをしていると言うことができます。微妙なポインタエラーなどをナビゲートして多くのことを学ぶことができます。しかし、これはPythonではありません。Pythonがこの振る舞いに耐えるのに十分な世話をしているのは、Pythonが他の言語よりもはるかに驚くべきものだからです。ダブラーと好奇心に満ちた人たちは、Pythonに慣れ親しんだプログラマーの直感に反して、デザイン・フレームではなく、何かがうまくいくまでの時間を驚かせると、それはちょうど動作するので。


なしを使用した簡単な回避策

>>> def bar(b, data=None):
...     data = data or []
...     data.append(b)
...     return data
... 
>>> bar(3)
[3]
>>> bar(3)
[3]
>>> bar(3)
[3]
>>> bar(3, [34])
[34, 3]
>>> bar(3, [34])
[34, 3]

オブジェクトを置き換えることで、これを丸めることができます(したがって、ネクタイをスコープに置き換えます)。

def foo(a=[]):
    a = list(a)
    a.append(5)
    return a

醜いですが、それは動作します。


実際には、これは設計上の欠陥ではなく、内部や性能のためではありません。
Pythonの関数は、コードだけでなくファーストクラスのオブジェクトであるという事実からくるものです。

このように考えるとすぐに、それは完全に意味があります:関数は、その定義上で評価されるオブジェクトです。 デフォルトのパラメータは一種の「メンバーデータ」なので、それらの状態はあるコールから他のコールに変わる可能性があります。

いずれにしても、EffbotはPythonのデフォルトパラメータ値でこの動作の理由を非常にうまく説明しています。
私はそれが非常に明確であることを発見しました。そして、関数オブジェクトがどのように機能するかをよりよく知るために読むことを提案します。


次のことを考慮すると、この動作は驚くことではありません。

  1. 割り当て時の読み取り専用クラス属性の動作、および
  2. 関数はオブジェクトです(受け入れられた回答でうまく説明されています)。

(2)の役割は、このスレッドで広範囲にわたってカバーされています。(1)は、他の言語から来たときにこの動作が「直感的」ではないため、驚異の原因となる可能性があります。

(1)については、クラスに関する Python チュートリアルで説明しています。読み込み専用のクラス属性に値を代入しようとすると、次のようになります。

...最も内側のスコープ外にある変数はすべて読み取り専用です(このような変数に書き込むと、最も内側のスコープに新しいローカル変数が作成され、同じ名前の外部変数は変更されません)。

元の例に戻って、上記の点を考慮してください:

def foo(a=[]):
    a.append(5)
    return a

ここにfooはオブジェクトがありafoo(で利用できるfoo.func_defs[0])の属性です。以来a、リストは、a変更可能であり、従って、の読み書き属性ですfoo。関数がインスタンス化されたときに署名によって指定された空のリストに初期化され、関数オブジェクトが存在する限り、読み取りと書き込みが可能です。

fooデフォルトを上書きせずに呼び出すと、そのデフォルト値が使用されますfoo.func_defs。この場合、関数オブジェクトのコードスコープ内foo.func_defs[0]で使用されaます。変更a変更foo.func_defs[0]、の一部であるfooオブジェクトとのコードの実行の間持続しますfoo

これを、他の言語のデフォルトの引数動作をエミュレートするドキュメントの例と比較してください。関数の実行時に関数シグニチャのデフォルトが使用されます。

def foo(a, L=None):
    if L is None:
        L = []
    L.append(a)
    return L

撮影(1)及び(2)これは必要な動作を実現する理由を考慮に入れ、1を見ることができます:

  • ときにfoo関数オブジェクトをインスタンス化され、foo.func_defs[0]に設定されているNone不変オブジェクト、。
  • 関数がデフォルトで実行されるとき(L関数呼び出しでパラメータが指定されていない)、foo.func_defs[0]None)はローカルスコープで利用可能ですL
  • この属性は読み込み専用なのでL = []、代入はで成功することはできませんfoo.func_defs[0]
  • Per (1)では、ローカルスコープ内に新しいローカル変数もL作成され、残りの関数呼び出しに使用されます。foo.func_defs[0]したがって、将来の呼び出しのために変更されませんfoo

次のコードがあるとします

fruits = ("apples", "bananas", "loganberries")

def eat(food=fruits):
    ...

私が食べると宣言したとき、少なくとも驚くべきことは、最初のパラメータが与えられていなければ、タプル("apples", "bananas", "loganberries")と等しいと考えることです

しかし、後でコードで想定されるように、私は何かのように

def some_random_function():
    global fruits
    fruits = ("blueberries", "mangos")

デフォルトのパラメータが関数宣言ではなく関数の実行時にバインドされていた場合、果物が変更されたことを発見するために(非常に悪い方法で)驚くでしょう。 これは上記のあなたのfoo関数がリストを変更していたことを発見するよりも驚くべきことです。

実際の問題は可変変数にあり、すべての言語でこの問題がある程度起こります。 ここに質問があります:Javaで私は次のコードを持っていると仮定します:

StringBuffer s = new StringBuffer("Hello World!");
Map<StringBuffer,Integer> counts = new HashMap<StringBuffer,Integer>();
counts.put(s, 5);
s.append("!!!!");
System.out.println( counts.get(s) );  // does this work?

今、私のマップは、マップに配置されたときにStringBufferキーの値を使用するか、またはキーを参照として格納しますか? いずれにせよ、誰かが驚いている。 オブジェクトをMapオブジェクトと同じ値で取得しようとした人か、オブジェクトのキーを使用していてもオブジェクトを取得できない人は、文字通り同じですこれは実際にPythonがその可変の組み込みデータ型を辞書のキーとして使用できない理由です。

あなたの例は、Pythonの新規参入者が驚いて噛まれるケースの良い例です。 しかし私は、これを「固定」すれば、それは代わりに噛んだ別の状況を作り出し、それは直感的ではないと主張します。 さらに、これは可変変数を扱う場合に常に当てはまります。 誰かが書いているコードに応じて、直感的にどちらか一方または反対の行動を期待できるケースが常に発生します。

私は個人的にPythonの現在のアプローチが好きです。デフォルトの関数引数は、関数が定義されているときに評価され、そのオブジェクトは常にデフォルトです。 私は彼らが空リストを使用して特別なケースができると思いますが、そのような特別なケースは後方互換性がないことは言うまでもなく、さらに驚きを引き起こします。


私は、実行時にオブジェクトを作成する方が良いアプローチだと思っていました。 私はあなたがいくつかの便利な機能を失うので、今はそれほど確かではありませんが、単に初心者の混乱を防ぐためにも関係なく価値があるかもしれません。 そうすることの欠点は次のとおりです。

1.パフォーマンス

def foo(arg=something_expensive_to_compute())):
    ...

呼び出し時間の評価が使用されている場合、関数が引数なしで使用されるたびに高価な関数が呼び出されます。 それぞれの呼び出しで高価な価格を支払うか、値を外部に手動でキャッシュし、名前空間を汚染し、冗長を追加する必要があります。

2.結合されたパラメータを強制する

便利なトリックは、ラムダのパラメータを、ラムダが作成されたときに変数の現在のバインディングにバインドすることです。 例えば:

funcs = [ lambda i=i: i for i in range(10)]

これは、それぞれ0,1,2,3 ...を返す関数のリストを返します。 振る舞いが変更された場合、代わりにi呼び出し時の値にバインドされるので、すべてが返された関数のリストが得られます。

これ以外の方法を実装する唯一の方法は、iバウンドのクロージャをさらに作成することです。

def make_func(i): return lambda: i
funcs = [make_func(i) for i in range(10)]

3.イントロスペクション

コードを考えてみましょう:

def foo(a='test', b=100, c=[]):
   print a,b,c

inspectモジュールを使用して、引数とデフォルトについての情報を得ることができます。

>>> inspect.getargspec(foo)
(['a', 'b', 'c'], None, None, ('test', 100, []))

この情報は、ドキュメントの生成、メタプログラミング、デコレータなどに非常に便利です。

さて、デフォルトの振る舞いを以下のように変更できるとしましょう:

_undefined = object()  # sentinel value

def foo(a=_undefined, b=_undefined, c=_undefined)
    if a is _undefined: a='test'
    if b is _undefined: b=100
    if c is _undefined: c=[]

しかし、私たちはイントロスペクション能力を失い、デフォルトの引数何であるかを見ています。 オブジェクトは構築されていないので、関数を実際に呼び出すことなく、オブジェクトを保持することはできません。 私たちができることは、ソースコードを保存してそれを文字列として返すことです。


私は、関数にデフォルトのリスト値を渡すための代替構造を実証しようとしています(辞書と同じように機能します)。

他の人が広くコメントしているので、listパラメータは、関数が実行されたときとは対照的に定義されているときに関数にバインドされます。リストと辞書は変更可能なので、このパラメータを変更すると、この関数への他の呼び出しに影響します。その結果、関数への後続の呼び出しは、関数への他の呼び出しによって変更された可能性があるこの共有リストを受け取ります。さらに悪いことに、2つのパラメータは、同時にこの関数の共有パラメータを使用して、他のパラメータによって変更が行われたことを知らない。

間違った方法(たぶん...)

def foo(list_arg=[5]):
    return list_arg

a = foo()
a.append(6)
>>> a
[5, 6]

b = foo()
b.append(7)
# The value of 6 appended to variable 'a' is now part of the list held by 'b'.
>>> b
[5, 6, 7]  

# Although 'a' is expecting to receive 6 (the last element it appended to the list),
# it actually receives the last element appended to the shared list.
# It thus receives the value 7 previously appended by 'b'.
>>> a.pop()             
7

以下を使用して、それらが1つの同じオブジェクトであることを確認できますid

>>> id(a)
5347866528

>>> id(b)
5347866528

Brett Slatkinの「効果的なPython:より良いPythonを書くための59の特定の方法」、項目20:None動的なデフォルト引数を指定するためのUse とDocStrings(48ページ)

Pythonで望ましい結果を達成するための規約は、とのデフォルト値を提供Noneしてdocstringの実際の動作を文書化することです。

この実装は、関数への各呼び出しが既定のリストを受け取るか、関数に渡されるリストを受け取るようにします。

好ましい方法

def foo(list_arg=None):
   """
   :param list_arg:  A list of input values. 
                     If none provided, used a list with a default value of 5.
   """
   if not list_arg:
       list_arg = [5]
   return list_arg

a = foo()
a.append(6)
>>> a
[5, 6]

b = foo()
b.append(7)
>>> b
[5, 7]

c = foo([10])
c.append(11)
>>> c
[10, 11]

プログラマがデフォルトのリストパラメータを共有することを意図した「間違ったメソッド」の正当なユースケースがあるかもしれませんが、これはルールよりも例外です。


私は時々、次のパターンの代わりにこの振る舞いを悪用します:

singleton = None

def use_singleton():
    global singleton

    if singleton is None:
        singleton = _make_singleton()

    return singleton.use_me()

singletonbyがのみ使用されている場合use_singleton、私は次のパターンを置き換えることが好きです:

# _make_singleton() is called only once when the def is executed
def use_singleton(singleton=_make_singleton()):
    return singleton.use_me()

私は外部リソースにアクセスするクライアントクラスをインスタンス化するために、またmemoization用のdictsまたはリストを作成するためにこれを使用しました。

このパターンはよく知られているとは思わないので、私は将来の誤解を防ぐために短いコメントをします。


関数を次のように変更してください:

def notastonishinganymore(a = []): 
    '''The name is just a joke :)'''
    a = a[:]
    a.append(5)
    return a

これはパフォーマンスの最適化です。この機能の結果、これら2つの関数呼び出しのどちらが速いと思いますか?

def print_tuple(some_tuple=(1,2,3)):
    print some_tuple

print_tuple()        #1
print_tuple((1,2,3)) #2

私はあなたにヒントを与えます。分解は次のとおりです(http://docs.python.org/library/dis.html参照)。

# 1

0 LOAD_GLOBAL              0 (print_tuple)
3 CALL_FUNCTION            0
6 POP_TOP
7 LOAD_CONST               0 (None)
10 RETURN_VALUE

# 2

 0 LOAD_GLOBAL              0 (print_tuple)
 3 LOAD_CONST               4 ((1, 2, 3))
 6 CALL_FUNCTION            1
 9 POP_TOP
10 LOAD_CONST               0 (None)
13 RETURN_VALUE

私は経験豊富な振る舞いが実際に使用されているのか疑問に思っています(バグを育てることなくCで静的変数を本当に使っていましたか?

ご覧のとおり、不変のデフォルト引数を使用する、パフォーマンス上の利点があります。これは頻繁に呼び出される関数またはデフォルトの引数が構築に長い時間を要する場合には、これを変更することができます。また、PythonはCではないことに注意してください。Cでは、かなり自由な定数があります。Pythonではこの利点はありません。


最短の答えはおそらく "定義が実行"であるため、引数全体が厳密な意味を持たない。より人為的な例として、あなたはこれを引用するかもしれません:

def a(): return []

def b(x=a()):
    print x

うまくいけば、defステートメントの実行時にデフォルトの引数式を実行するのが簡単ではない、あるいは意味がない、あるいはその両方でないことを示すだけで十分です。

私はあなたがデフォルトのコンストラクタを使用しようとすると、それがうんざりだと思います。







least-astonishment