「驚き最小の原則」と可変デフォルト引数
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の関数は、コードの一部ではなく、ファーストクラスのオブジェクトであるという事実から単純に来ています。
このように考えるとすぐに、それは完全に理にかなっています。関数は、その定義で評価されるオブジェクトです。デフォルトのパラメータは一種の「メンバーデータ」であるため、他のオブジェクトとまったく同じように、呼び出しごとに状態が変わる可能性があります。
いずれにせよ、Effbotは、Pythonのデフォルトパラメータ値でこの動作の理由を非常によく説明しています。
私はそれが非常に明確であることに気づきました。関数オブジェクトがどのように機能するかをよりよく理解するために、それを読むことを強くお勧めします。
次のコードがあるとします
fruits = ("apples", "bananas", "loganberries")
def eat(food=fruits):
...
eatの宣言を見ると、最も驚くべきことは、最初のパラメーターが指定されていない場合、それはタプルと等しくなると考えることです。 ("apples", "bananas", "loganberries")
ただし、コードの後半で想定されているように、私は次のようなことをします
def some_random_function():
global fruits
fruits = ("blueberries", "mangos")
次に、デフォルトのパラメーターが関数宣言ではなく関数実行時にバインドされている場合、フルーツが変更されたことを発見すると(非常に悪い方法で)驚かれることでしょう。これは、foo
上記の関数がリストを変更していることを発見するよりも、驚くべきIMOです。
本当の問題は可変変数にあり、すべての言語にある程度この問題があります。ここに質問があります: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回評価され、同じ「事前計算された」値が各呼び出しに使用されることを意味します。これは、デフォルトパラメータがリストやディクショナリなどの可変オブジェクトである場合を理解するために特に重要です。関数がオブジェクトを変更する場合(たとえば、リストにアイテムを追加することによって)、デフォルト値は事実上変更されます。これは一般的に意図されたものではありません。これを回避する方法は
None
、デフォルトとして使用し、関数の本体で明示的にテストすることです。例:def whats_on_the_telly(penguin=None): if penguin is None: penguin = [] penguin.append("property of the zoo") return penguin
私はPythonインタープリターの内部動作について何も知りません(そして私はコンパイラーとインタープリターの専門家でもありません)ので、私が無意味または不可能なことを提案しても私を責めないでください。
Pythonオブジェクトが可変であるという条件で、デフォルトの引数を設計するときにこれを考慮に入れる必要があると思います。リストをインスタンス化する場合:
a = []
によって参照される新しいリストを取得することを期待していますa
。
なぜべきa=[]
で
def x(a=[]):
呼び出しではなく、関数定義で新しいリストをインスタンス化しますか?「ユーザーが引数を指定しない場合は、新しいリストをインスタンス化して、呼び出し元によって作成されたかのように使用する」と尋ねているのと同じです。代わりに、これはあいまいだと思います。
def x(a=datetime.datetime.now()):
ユーザー、a
定義または実行するときに対応する日時をデフォルトにしますx
か?この場合、前の例と同様に、デフォルトの引数「割り当て」が関数の最初の命令(datetime.now()
関数の呼び出し時に呼び出される)である場合と同じ動作を維持します。一方、ユーザーが定義時のマッピングを希望する場合は、次のように記述できます。
b = datetime.datetime.now()
def x(a=b):
私は知っています、私は知っています:それは閉鎖です。あるいは、Pythonは、定義時のバインドを強制するキーワードを提供する場合があります。
def x(static a=b):
理由は、コードが実行されたときにバインディングが実行され、関数定義が実行されたからです。関数が定義されたときです。
これを比較してください:
class BananaBunch:
bananas = []
def addBanana(self, banana):
self.bananas.append(banana)
このコードは、まったく同じ予期しない出来事に悩まされています。バナナはクラス属性であるため、それに何かを追加すると、そのクラスのすべてのインスタンスに追加されます。理由はまったく同じです。
それは単なる「仕組み」であり、関数の場合に異なる動作をさせることはおそらく複雑であり、クラスの場合にはおそらく不可能であるか、クラスコードを維持する必要があるため、少なくともオブジェクトのインスタンス化を大幅に遅くしますオブジェクトが作成されたときに実行します。
はい、意外です。しかし、ペニーが下がると、Pythonの一般的な動作と完全に一致します。実際、これは優れた教材であり、なぜこれが発生するのかを理解すれば、Pythonをよりよく理解できるようになります。
それはそれがどんな良いPythonチュートリアルでも目立つように機能するべきだと言った。あなたが言うように、誰もが遅かれ早かれこの問題に遭遇するからです。
内省してみませんか?
私はよ本当にPythonが提供する洞察力のイントロスペクションを行っ(た誰も驚かない2
と3
呼び出し可能オブジェクトに適用されますが)。
func
次のように定義された単純な小さな関数が与えられます。
>>> def func(a = []):
... a.append(5)
Pythonがこれに遭遇すると、最初に行うことはcode
、この関数のオブジェクトを作成するためにコンパイルすることです。このコンパイル手順が実行されている間、Pythonは*を評価し、デフォルトの引数(ここでは空のリスト)を関数オブジェクト自体に格納します[]
。一番上の答えが述べたように、リストは関数のメンバーa
と見なすことができます。func
それでは、関数オブジェクト内でリストがどのように展開されるかを調べる前と後の内省を行ってみましょう。私はPython 3.x
これに使用しています。Python2にも同じことが当てはまります(Python 2で__defaults__
またはfunc_defaults
を使用します。はい、同じものに2つの名前を付けます)。
実行前の機能:
>>> def func(a = []):
... a.append(5)
...
Pythonがこの定義を実行した後、指定されたデフォルトのパラメーター(a = []
ここ)を受け取り、それらを__defaults__関数オブジェクトの属性に詰め込みます(関連セクション:Callables):
>>> func.__defaults__
([],)
さて、__defaults__
期待どおり、の単一エントリとして空のリストです。
実行後の機能:
この関数を実行してみましょう:
>>> 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 = []
関数本体は毎回新しく実行されるため、引数が渡されなかった場合は常に新しい空のリストを取得しa
ます。
のリストが__defaults__
関数で使用されているものと同じであることをさらに確認するには、func
関数を変更して、関数本体内で使用されてid
いるリストのを返すことができますa
。その後、内のリストにそれを比較する__defaults__
(位置[0]
で__defaults__
)、これらは実際に同じリストのインスタンスに参照のうえいるあなたはどのように表示されます:
>>> 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
お気づきのように、input()
関数をbar
作成して名前にバインドするプロセスが行われる前に呼び出されます。
以前は、実行時にオブジェクトを作成する方が良いアプローチだと思っていました。初心者の混乱を防ぐためだけでも価値があるかもしれませんが、いくつかの便利な機能が失われているので、今は確信が持てません。そうすることの欠点は次のとおりです。
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
されるため、すべてが返された関数のリストを取得します。9
それ以外の場合にこれを実装する唯一の方法は、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=[]
ただし、内省してデフォルトの引数が何であるかを確認する機能が失われました。オブジェクトは構築されていないため、実際に関数を呼び出さずにオブジェクトを取得することはできません。私たちができる最善のことは、ソースコードを保存し、それを文字列として返すことです。
Pythonを守るための5つのポイント
シンプルさ:動作は次の意味でシンプルです。ほとんどの人は、このトラップに何度かではなく、一度だけ陥ります。
一貫性:Pythonは、名前ではなく、常にオブジェクトを渡します。デフォルトのパラメーターは、明らかに、関数の見出しの一部です(関数本体ではありません)。したがって、関数呼び出し時ではなく、モジュールのロード時に(ネストされていない限り、モジュールのロード時にのみ)評価する必要があります。
有用性:Frederik Lundhが「Pythonのデフォルトパラメータ値」の説明で指摘しているように、現在の動作は高度なプログラミングに非常に役立ちます。(慎重に使用してください。)
十分なドキュメント:最も基本的なPythonドキュメント、チュートリアルでは、問題が大声として発表された「重要な警告」における最初のセクションのサブセクション「関数の定義の詳細」。警告は、見出しの外ではめったに適用されない太字を使用しています。RTFM:細かいマニュアルを読んでください。
メタ学習:トラップに陥るのは、実際には非常に役立つ瞬間です(少なくとも、熟考する学習者の場合)。これは、上記の「一貫性」のポイントを理解し、Pythonについて多くのことを学ぶことができるためです。
この動作は次のように簡単に説明できます。
- 関数(クラスなど)の宣言は1回だけ実行され、すべてのデフォルト値オブジェクトが作成されます
- すべてが参照によって渡されます
そう:
def x(a=0, b=[], c=[], d=0):
a = a + 1
b = b + [1]
c.append(1)
print a, b, c
a
変更されません-すべての代入呼び出しで新しいintオブジェクトが作成されます-新しいオブジェクトが出力されますb
変更されません-新しい配列はデフォルト値から作成され、出力されますc
変更-操作は同じオブジェクトに対して実行されます-そしてそれは印刷されます
あなたが求めているのは、なぜこれなのかということです。
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)「ミュータブルデフォルト引数」のいわゆる問題は、一般に、
「この問題のあるすべての関数は、実際のパラメータで同様の副作用の問題を抱えている」ことを示す特別な例です。
これは、関数型プログラミングの規則に反します。通常は望ましくないため、両方を一緒に修正する必要があります。
例:
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]
解決策:コピー
絶対に安全な解決策は、最初にcopy
またはdeepcopy
入力オブジェクトを実行してから、コピーを使用して何でもすることです。
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()
(後者は、可変オブジェクトが可変オブジェクトで構成されている場合に役立ちます)。一部のオブジェクトは、基本的に「ファイル」オブジェクトなどの副作用に基づいており、コピーでは意味のある再現ができません。コピー
同様の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]
この関数によって返されるインスタンスのパブリック属性にも保存しないでください。(インスタンスのプライベート属性は、慣例により、このクラスまたはサブクラスの外部から変更されるべきではないと仮定します。つまり_var1
、プライベート属性です)
結論:
入力パラメーターオブジェクトは、その場で変更(変更)したり、関数によって返されるオブジェクトにバインドしたりしないでください。(強く推奨される副作用のないプログラミングを好む場合。「副作用」についてはWikiを参照してください(最初の2つの段落はこのコンテキストに関連しています。)。)
2)
実際のパラメータへの副作用が必要であるが、デフォルトパラメータでは望ましくない場合にのみ、有用な解決策はdef ...(var1=None):
if var1 is None:
var1 = []
Moreです。
3)場合によっては、デフォルトパラメータの変更可能な動作が便利です。
これは実際にはデフォルト値とは何の関係もありませんが、可変のデフォルト値で関数を作成すると予期しない動作が発生することがよくあります。
>>> 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
明示的に渡されたか、デフォルト値を取得したかを変更するべきではありません。コンテキスト/名前/ドキュメントから引数が変更されることになっていることが明らかでない限り、コードは可変引数をそのままにしておく必要があります。Pythonを使用しているかどうか、デフォルトの引数が含まれているかどうかに関係なく、引数として渡された可変値をローカル一時として使用することは非常に悪い考えです。
何かを計算する過程でローカル一時を破壊的に操作する必要があり、引数値から操作を開始する必要がある場合は、コピーを作成する必要があります。
すでに忙しいトピックですが、ここで読んだことから、内部でどのように機能しているかを理解するのに次のことが役立ちました。
def bar(a=[]):
print id(a)
a = a + [1]
print id(a)
return a
>>> bar()
4484370232
4484524224
[1]
>>> bar()
4484370232
4484524152
[1]
>>> bar()
4484370232 # Never change, this is 'class property' of the function
4484523720 # Always a new object
[1]
>>> id(bar.func_defaults[0])
4484370232
これはパフォーマンスの最適化です。この機能の結果として、これら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では、この利点はありません。
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
最短の答えはおそらく「定義は実行である」であるため、議論全体は厳密には意味がありません。より不自然な例として、これを引用することができます:
def a(): return []
def b(x=a()):
print x
うまくいけば、def
ステートメントの実行時にデフォルトの引数式を実行しないことは簡単ではないか、意味がないか、またはその両方であることを示すだけで十分です。
ただし、デフォルトのコンストラクターを使おうとすると、それが落とし穴になることに同意します。
次のことを考慮すれば、この動作は驚くべきことではありません。
- 割り当て試行時の読み取り専用クラス属性の動作、および
- 関数はオブジェクトです(受け入れられた回答でよく説明されています)。
(2)の役割については、このスレッドで詳しく説明しています。(1)他の言語から来た場合、この動作は「直感的」ではないため、驚愕の原因となる可能性があります。
(1)は、クラスに関するPythonチュートリアルで説明されています。読み取り専用のクラス属性に値を割り当てようとすると、次のようになります。
...最も内側のスコープの外側にあるすべての変数は読み取り専用です(このような変数に書き込もうとすると、同じ名前の外側の変数は変更されずに、最も内側のスコープに新しいローカル変数が作成されます)。
元の例を振り返り、上記の点を考慮してください。
def foo(a=[]):
a.append(5)
return a
これfoo
はオブジェクトでa
あり、foo
(で利用可能foo.func_defs[0]
)の属性です。以来a
リストで、a
変更可能であり、したがっての読み書き属性ですfoo
。関数がインスタンス化されると、署名で指定された空のリストに初期化され、関数オブジェクトが存在する限り、読み取りと書き込みに使用できます。
foo
デフォルトをオーバーライドせずに呼び出すと、からのデフォルトの値が使用されますfoo.func_defs
。この場合、foo.func_defs[0]
はa
関数オブジェクトのコードスコープ内で使用されます。オブジェクトの一部であり、のコードの実行間で持続するa
changeへの変更。foo.func_defs[0]
foo
foo
ここで、これを他の言語のデフォルト引数の動作のエミュレートに関するドキュメントの例と比較して、関数が実行されるたびに関数シグネチャのデフォルトが使用されるようにします。
def foo(a, L=None):
if L is None:
L = []
L.append(a)
return L
撮影(1)及び(2)これは、目的の動作を達成する理由口座に、人は見ることができます:
- ときに
foo
関数オブジェクトをインスタンス化され、foo.func_defs[0]
に設定されているNone
不変オブジェクト、。 - 関数がデフォルトで実行される場合(
L
関数呼び出しでパラメーターが指定されていない場合)、foo.func_defs[0]
(None
)はローカルスコープでL
。として使用できます。 - の場合、その属性は読み取り専用であるため
L = []
、割り当てはで成功できませんfoo.func_defs[0]
。 - (1)に従って、名前も付けられた新しいローカル変数
L
がローカルスコープに作成され、関数呼び出しの残りの部分で使用されます。foo.func_defs[0]
したがって、の将来の呼び出しのために変更されませんfoo
。
Noneを使用した簡単な回避策
>>> 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]
デフォルトのリスト値を関数に渡すための代替構造を示します(辞書でも同様に機能します)。
他の人が広範囲にコメントしているように、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の「EffectivePython:59 Specific Ways to Write Better Python」、Item 20:Use None
and Docstrings to指定動的デフォルト引数(p。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]
プログラマーがデフォルトのリストパラメーターを共有することを意図した「間違った方法」の正当な使用例があるかもしれませんが、これはルールよりも例外である可能性が高いです。
ここでの解決策は次のとおりです。
None
デフォルト値(またはナンスobject
)として使用し、それをオンにして実行時に値を作成します。またはlambda
デフォルトパラメータとしてaを使用し、tryブロック内でそれを呼び出して、デフォルト値を取得します(これは、ラムダ抽象化の目的の一種です)。
2番目のオプションは、関数のユーザーが既に存在している可能性のある呼び出し可能オブジェクト(などtype
)を渡すことができるので便利です。
オブジェクトを置き換えることでこれを回避できます(したがって、スコープとの関係)。
def foo(a=[]):
a = list(a)
a.append(5)
return a
醜いですが、動作します。
これを行うとき:
def foo(a=[]):
...
...呼び出し元がaの値を渡さない場合は、引数a
を名前のないリストに割り当てます。
この議論を簡単にするために、名前のないリストに一時的に名前を付けましょう。どうpavlo
ですか?
def foo(a=pavlo):
...
いつでも、発信者が何であるかを教えてくれない場合、私たちa
は再利用しpavlo
ます。
pavlo
が可変(変更可能)で、最終的に変更された場合foo
、次に気付くエフェクトfoo
は、を指定せずに呼び出されa
ます。
だからこれはあなたが見るものです(覚えておいてください、pavlo
[]に初期化されます):
>>> foo()
[5]
さて、pavlo
[5]です。
foo()
再度呼び出すと、再度変更さpavlo
れます。
>>> foo()
[5, 5]
a
いつ呼び出すかfoo()
を指定すると、確実にpavlo
触れられません。
>>> ivan = [1, 2, 3, 4]
>>> foo(a=ivan)
[1, 2, 3, 4, 5]
>>> ivan
[1, 2, 3, 4, 5]
だから、pavlo
まだ[5, 5]
です。
>>> foo()
[5, 5, 5]
次のパターンの代わりに、この動作を悪用することがあります。
singleton = None
def use_singleton():
global singleton
if singleton is None:
singleton = _make_singleton()
return singleton.use_me()
singleton
でのみ使用される場合はuse_singleton
、代わりに次のパターンが好きです。
# _make_singleton() is called only once when the def is executed
def use_singleton(singleton=_make_singleton()):
return singleton.use_me()
これを使用して、外部リソースにアクセスするクライアントクラスをインスタンス化したり、メモ化用のdictやリストを作成したりしました。
このパターンはあまり知られていないと思うので、将来の誤解を防ぐために短いコメントを入れます。
それは本当かもしれません:
- 誰かがすべての言語/ライブラリ機能を使用していて、
- ここで動作を切り替えることはお勧めできませんが、
上記の両方の機能を保持し、さらに別の点を指摘することは完全に一貫しています。
- これは紛らわしい機能であり、Pythonでは残念です。
他の答え、または少なくともそれらのいくつかは、ポイント1と2を作成しますが、3は作成しません。または、ポイント3を作成し、ポイント1と2を軽視します。
ここで途中で馬を切り替えると、重大な破損が発生する可能性があり、Pythonを変更してStefanoのオープニングスニペットを直感的に処理することで、さらに多くの問題が発生する可能性があります。そして、Pythonの内部をよく知っている人が、結果の地雷原を説明できるのは事実かもしれません。しかしながら、
既存の動作はPython的ではない、と非常に少ない言語についての少なくとも驚きのどこかの原則に違反しているためPythonは成功するの近くにひどくこれを。それを根絶することが賢明であるかどうかにかかわらず、それは本当の問題です。設計上の欠陥です。動作を追跡することで言語をよりよく理解できれば、C ++はこれ以上のことをすべて実行していると言えます。たとえば、微妙なポインタエラーをナビゲートすることで多くのことを学びます。しかし、これはPythonicではありません。Pythonは他の言語よりも驚きがはるかに少ないため、この動作に直面しても耐えられるほどPythonを気にする人は、その言語に惹かれる人です。Dabblersと好奇心旺盛な人は、Pythonに惹かれるプログラマーの直感に反する、デザインのfl(つまり、隠されたロジックパズル)が原因ではなく、何かが機能するのにかかる時間がどれだけ短いかに驚いたときにPythonistasになります。それはうまくいくからです。
これは設計上の欠陥ではありません。これにつまずく人は誰でも何か間違ったことをしている。
この問題が発生する可能性がある場所は3つあります。
- 関数の副作用として引数を変更する予定です。この場合、デフォルトの引数を持つことは意味がありません。唯一の例外は、引数リストを悪用して関数属性を持つ場合です。たとえば
cache={}
、実際の引数を使用して関数を呼び出すことはまったく期待できません。 - 引数を変更しないままにするつもりですが、誤って変更しました。これはバグです。修正してください。
- 関数内で使用するために引数を変更するつもりですが、変更が関数外で表示されることを期待していませんでした。その場合、それがデフォルトであったかどうかにかかわらず、引数のコピーを作成する必要があります!Pythonは値による呼び出し言語ではないため、コピーは作成されません。明示的に説明する必要があります。
質問の例は、カテゴリ1または3に分類される可能性があります。渡されたリストを変更して返すのは奇妙なことです。どちらかを選択する必要があります。
この「バグ」は私に多くの残業時間を与えました!しかし、私はそれの潜在的な使用法を見始めています(しかし、私はそれが実行時にあることを望みました、それでも)
私が有用な例として見ているものをあなたに与えるつもりです。
def example(errors=[]):
# statements
# Something went wrong
mistake = True
if mistake:
tryToFixIt(errors)
# Didn't work.. let's try again
tryToFixItAnotherway(errors)
# This time it worked
return errors
def tryToFixIt(err):
err.append('Attempt to fix it')
def tryToFixItAnotherway(err):
err.append('Attempt to fix it by another way')
def main():
for item in range(2):
errors = example()
print '\n'.join(errors)
main()
以下を印刷します
Attempt to fix it
Attempt to fix it by another way
Attempt to fix it
Attempt to fix it by another way
関数を次のように変更するだけです。
def notastonishinganymore(a = []):
'''The name is just a joke :)'''
a = a[:]
a.append(5)
return a
この質問への答えは、Pythonがデータをパラメーターに渡す方法(値または参照による受け渡し)にあり、可変性やpythonが「def」ステートメントを処理する方法にあると思います。
簡単な紹介。まず、Pythonには2つのタイプのデータ型があります。1つは数値のような単純な基本データ型であり、もう1つはオブジェクトです。次に、データをパラメーターに渡す場合、pythonは基本データ型を値で渡します。つまり、値のローカルコピーをローカル変数に渡しますが、オブジェクトは参照で渡します。つまり、オブジェクトへのポインターです。
上記の2つのポイントを認めて、Pythonコードに何が起こったのかを説明しましょう。これは、オブジェクトの参照を渡すためだけですが、可変/不変、または「def」ステートメントが定義されたときに1回だけ実行されるという事実とは関係ありません。
[]はオブジェクトであるため、Pythonは[]の参照をに渡します。a
つまり、a
オブジェクトとしてメモリ内にある[]へのポインタのみです。[]のコピーは1つだけですが、多くの参照があります。最初のfoo()の場合、リスト[]はappendメソッドによって1に変更されます。ただし、リストオブジェクトのコピーは1つだけであり、このオブジェクトは1になることに注意してください。2番目のfoo()を実行すると、effbot Webページの内容(アイテムは評価されなくなります)が間違っています。a
はリストオブジェクトであると評価されますが、オブジェクトのコンテンツは1になります。これは参照渡しの効果です!foo(3)の結果は、同じ方法で簡単に導き出すことができます。
私の答えをさらに検証するために、2つの追加コードを見てみましょう。
====== No. 2 ========
def foo(x, items=None):
if items is None:
items = []
items.append(x)
return items
foo(1) #return [1]
foo(2) #return [2]
foo(3) #return [3]
[]
はオブジェクトであるため、そうですNone
(前者は可変で、後者は不変です。ただし、可変性は質問とは関係ありません)。スペースのどこかにNoneはありませんが、そこにあることはわかっており、Noneのコピーは1つしかありません。したがって、fooが呼び出されるたびに、アイテムは(1回だけ評価されるという回答とは対照的に)Noneであると評価されます。明確にするために、Noneの参照(またはアドレス)です。次に、fooで、itemが[]に変更されます。つまり、異なるアドレスを持つ別のオブジェクトを指します。
====== No. 3 =======
def foo(x, items=[]):
items.append(x)
return items
foo(1) # returns [1]
foo(2,[]) # returns [2]
foo(3) # returns [1,3]
foo(1)を呼び出すと、アイテムは、たとえば11111111のアドレスを持つリストオブジェクト[]を指すようになります。リストの内容は、続編のfoo関数で1に変更されますが、アドレスは変更されません。それでも11111111 。次に、foo(2、[])が来ています。foo(2、[])の[]は、foo(1)を呼び出すときのデフォルトのパラメーター[]と同じ内容ですが、アドレスが異なります。パラメータを明示的に指定するため、items
この新しいアドレス[]
、たとえば2222222を取得し、変更を加えた後に返す必要があります。これでfoo(3)が実行されます。のみx
が提供されるため、アイテムはデフォルト値を再度使用する必要があります。デフォルト値は何ですか?これは、foo関数を定義するときに設定されます。11111111にあるリストオブジェクトです。したがって、アイテムは、要素1を持つアドレス11111111であると評価されます。2222222にあるリストにも1つの要素2が含まれますが、アイテムによってポイントされることはありません。もっと。したがって、3を追加するとitems
[1,3]になります。
上記の説明から、受け入れられた回答で推奨されているeffbot Webページが、この質問に関連する回答を提供できなかったことがわかります。さらに、effbotのWebページのポイントが間違っていると思います。UI.Buttonに関するコードは正しいと思います。
for i in range(10):
def callback():
print "clicked button", i
UI.Button("button %s" % i, callback)
各ボタンは、の異なる値を表示する個別のコールバック関数を保持できますi
。これを示す例を提供できます。
x=[]
for i in range(10):
def callback():
print(i)
x.append(callback)
実行するx[7]()
と、期待どおりに7x[9]()
が得られ、9に別の値が与えられますi
。
TLDR:定義時のデフォルトは一貫性があり、厳密により表現力があります。
関数の定義は、関数を含む定義スコープと、関数に含まれる実行スコープの2つのスコープに影響します。ブロックがスコープにどのようにマップされるかはかなり明確ですが、問題はどこにdef <name>(<args=defaults>):
属するかです。
... # defining scope
def name(parameter=default): # ???
... # execution scope
def name
一部はしなければならない定義スコープで評価する-私たちが望むname
すべての後に、そこに利用できるようにします。関数をそれ自体の内部でのみ評価すると、関数にアクセスできなくなります。
parameter
は定数名なので、def name
。と同時に「評価」することができます。これname(parameter=...):
には、裸の代わりに、として知られている署名を持つ関数を生成するという利点もありname(...):
ます。
さて、いつ評価するのdefault
ですか?
一貫性はすでに「定義時」と言っています。他のすべてもdef <name>(<args=defaults>):
定義時に最もよく評価されます。その一部を遅らせることは驚くべき選択でしょう。
2つの選択肢も同等ではありません。default
定義時に評価された場合でも、実行時間に影響を与える可能性があります。default
が実行時に評価される場合、定義時間に影響を与えることはできません。「定義時」を選択すると両方のケースを表現できますが、「実行時」を選択すると1つしか表現できません。
def name(parameter=defined): # set default at definition time
...
def name(parameter=default): # delay default until execution time
parameter = default if parameter is None else parameter
...
他のすべての答えは、これが実際に素晴らしく望ましい動作である理由、またはとにかくこれを必要とすべきではない理由を説明しています。私のは、言語を自分の意志に曲げる権利を行使したい頑固な人のためのものであり、その逆ではありません。
デフォルト値のままになっている各位置引数に同じインスタンスを再利用する代わりに、デフォルト値をコピーするデコレータを使用して、この動作を「修正」します。
import inspect
from copy import copy
def sanify(function):
def wrapper(*a, **kw):
# store the default values
defaults = inspect.getargspec(function).defaults # for python2
# construct a new argument list
new_args = []
for i, arg in enumerate(defaults):
# allow passing positional arguments
if i in range(len(a)):
new_args.append(a[i])
else:
# copy the value
new_args.append(copy(arg))
return function(*new_args, **kw)
return wrapper
次に、このデコレータを使用して関数を再定義しましょう。
@sanify
def foo(a=[]):
a.append(5)
return a
foo() # '[5]'
foo() # '[5]' -- as desired
これは、複数の引数を取る関数に特に適しています。比較:
# the 'correct' approach
def bar(a=None, b=None, c=None):
if a is None:
a = []
if b is None:
b = []
if c is None:
c = []
# finally do the actual work
と
# the nasty decorator hack
@sanify
def bar(a=[], b=[], c=[]):
# wow, works right out of the box!
次のようにキーワード引数を使用しようとすると、上記の解決策が機能しないことに注意することが重要です。
foo(a=[4])
デコレータはそれを可能にするように調整できますが、これは読者の練習問題として残しておきます;)