可変関数の引数のデフォルト値の適切な使用法は?


82

関数の引数のデフォルト値として可変オブジェクトを設定することは、Pythonでよくある間違いです。これはDavidGoodgerによるこの優れた記事から抜粋した例です。

>>> def bad_append(new_item, a_list=[]):
        a_list.append(new_item)
        return a_list
>>> print bad_append('one')
['one']
>>> print bad_append('two')
['one', 'two']

これが発生する理由の説明はここにあります

そして今私の質問のために:この構文の良いユースケースはありますか?

つまり、それに遭遇したすべての人が同じ間違いを犯し、それをデバッグし、問題を理解し、そこからそれを回避しようとすると、そのような構文にはどのような用途がありますか?


1
これについて私が知っている最も良い説明は、リンクされた質問にあります。関数は、クラスと同じように、ファーストクラスのオブジェクトです。クラスには変更可能な属性データがあります。関数には変更可能なデフォルト値があります。
カトリエル2012

10
この振る舞いは「設計上の選択」ではなく、言語の動作方法の結果であり、可能な限り少ない例外を除いて、単純な動作原理から始まります。私のためにいくつかの時点で、私はちょうど自然になったこの動作を「Pythonで考える」を始めとして-そしてそれは実現しなかった場合、私は驚かれるだろう
jsbueno

2
これも気になりました。この例はWeb全体にありますが、意味がありません。渡されたリストを変更してデフォルトを設定しても意味がないか、新しいリストを返し、すぐにコピーを作成する必要があります。関数に入ったとき。両方を行うことが有用な場合は想像できません。
マークランサム2012


2
上記で不満を言っている問題がない、より現実的な例に出くわしました。デフォルトは__init__、インスタンス変数に設定されるクラスの関数への引数です。これは完全に有効なことであり、変更可能なデフォルトではすべてがひどく間違っています。stackoverflow.com/questions/43768055/…–
Mark Ransom

回答:


61

これを使用して、関数呼び出し間で値をキャッシュできます。

def get_from_cache(name, cache={}):
    if name in cache: return cache[name]
    cache[name] = result = expensive_calculation()
    return result

しかし、通常、そのようなことは、キャッシュなどをクリアするための追加の属性を持つことができるため、クラスを使用した方がうまくいきます。


12
...またはメモ化デコレータ。
ダニエルローズマン2012

28
@functools.lru_cache(maxsize=None)
カトリエル2012

3
@katrielalexlru_cacheはPython3.2の新機能であるため、誰もが使用できるわけではありません。
ダンカン


1
lru_cacheハッシュできない値がある場合は使用できません。
Synedraacus 2018年

13

正規の答えはこのページです:http//effbot.org/zone/default-values.htm

また、変更可能なデフォルト引数の3つの「適切な」ユースケースについても説明します。

  • コールバックでローカル変数を外部変数の現在の値にバインドする
  • キャッシュ/メモ化
  • グローバル名のローカル再バインド(高度に最適化されたコードの場合)

12

たぶん、あなたは可変引数を突然変異させませんが、可変引数を期待します:

def foo(x, y, config={}):
    my_config = {'debug': True, 'verbose': False}
    my_config.update(config)
    return bar(x, my_config) + baz(y, my_config)

(はい、config=()この特定のケースで使用できることは知っていますが、それはあまり明確ではなく、一般的でもありません。)


2
また、変更せず、このデフォルト値を関数から直接返さないようにしてください。そうしないと、関数外のコードによって変更され、すべての関数呼び出しに影響します。
アンドレイセマキン

10
import random

def ten_random_numbers(rng=random):
    return [rng.random() for i in xrange(10)]

使用するrandomデフォルトの乱数発生器として、モジュール、効果的に変更可能なシングルトンを。


7
しかし、これもそれほど重要なユースケースではありません。
Evgeni Sergeev 2014年

3
Pythonの「参照を1回取得する」と、Pythonの「random関数呼び出しごとに1回ルックアップする」の動作に違いはないと思います。どちらも同じオブジェクトを使用することになります。
nyanpasu64 2018年

4

編集(明確化):変更可能なデフォルト引数の問題は、より深い設計選択の兆候です。つまり、デフォルト引数値は関数オブジェクトの属性として格納されます。なぜこの選択​​がなされたのかと疑問に思うかもしれません。いつものように、そのような質問に正しく答えることは困難です。しかし、それは確かに良い用途があります:

パフォーマンスの最適化:

def foo(sin=math.sin): ...

変数の代わりにクロージャ内のオブジェクト値を取得します。

callbacks = []
for i in range(10):
    def callback(i=i): ...
    callbacks.append(callback)

7
整数と組み込み関数は変更できません!
モニカを復活させる2012

2
@Jonathan:残りの例にはまだ変更可能なデフォルトの引数がありませんか、それとも表示されないだけですか?
モニカを復活させる2012

2
@ジョナサン:私のポイントは、これらが変更可能であるということではありません。Pythonがデフォルトの引数(コンパイル時に定義された関数オブジェクト)を格納するために使用するシステムが役立つ場合があります。これは、各関数呼び出しで引数を再評価するとトリックが役に立たなくなるため、可変のデフォルト引数の問題を意味します。
カトリエル2012

2
@katriealex:わかりました。しかし、あなたの答えの中で、議論を再評価する必要があると思い、それがなぜ悪いのかを示していると言ってください。Nit-pick:デフォルトの引数値はコンパイル時ではなく、関数定義ステートメントの実行時に保存されます。
モニカを復活

@WolframH:true:P!2つはしばしば一致しますが。
カトリエル2012

-1

可変のデフォルト引数値の適切な使用法の質問に答えて、次の例を示します。

変更可能なデフォルトは、自分で作成した使いやすくインポート可能なコマンドをプログラミングするのに役立ちます。可変のデフォルトメソッドは、最初の呼び出しで初期化できる(クラスと非常によく似ている)が、グローバルに頼る必要もなく、ラッパーを使用する必要もなく、インスタンス化する必要もなく、関数内にプライベートな静的変数を持つことになります。インポートされたクラスオブジェクト。あなたが同意することを願っていますので、それはそれ自体がエレガントです。

次の2つの例を検討してください。

def dittle(cache = []):

    from time import sleep # Not needed except as an example.

    # dittle's internal cache list has this format: cache[string, counter]
    # Any argument passed to dittle() that violates this format is invalid.
    # (The string is pure storage, but the counter is used by dittle.)

     # -- Error Trap --
    if type(cache) != list or cache !=[] and (len(cache) == 2 and type(cache[1]) != int):
        print(" User called dittle("+repr(cache)+").\n >> Warning: dittle() takes no arguments, so this call is ignored.\n")
        return

    # -- Initialize Function. (Executes on first call only.) --
    if not cache:
        print("\n cache =",cache)
        print(" Initializing private mutable static cache. Runs only on First Call!")
        cache.append("Hello World!")
        cache.append(0)
        print(" cache =",cache,end="\n\n")
    # -- Normal Operation --
    cache[1]+=1 # Static cycle count.
    outstr = " dittle() called "+str(cache[1])+" times."
    if cache[1] == 1:outstr=outstr.replace("s.",".")
    print(outstr)
    print(" Internal cache held string = '"+cache[0]+"'")
    print()
    if cache[1] == 3:
        print(" Let's rest for a moment.")
        sleep(2.0) # Since we imported it, we might as well use it.
        print(" Wheew! Ready to continue.\n")
        sleep(1.0)
    elif cache[1] == 4:
        cache[0] = "It's Good to be Alive!" # Let's change the private message.

# =================== MAIN ======================        
if __name__ == "__main__":

    for cnt in range(2):dittle() # Calls can be loop-driven, but they need not be.

    print(" Attempting to pass an list to dittle()")
    dittle([" BAD","Data"])
    
    print(" Attempting to pass a non-list to dittle()")
    dittle("hi")
    
    print(" Calling dittle() normally..")
    dittle()
    
    print(" Attempting to set the private mutable value from the outside.")
    # Even an insider's attempt to feed a valid format will be accepted
    # for the one call only, and is then is discarded when it goes out
    # of scope. It fails to interrupt normal operation.
    dittle([" I am a Grieffer!\n (Notice this change will not stick!)",-7]) 
    
    print(" Calling dittle() normally once again.")
    dittle()
    dittle()

このコードを実行すると、dittle()関数は最初の呼び出しでは内部化されますが、追加の呼び出しでは内部化されないことがわかります。呼び出し間の内部静的ストレージにプライベート静的キャッシュ(可変デフォルト)を使用し、ハイジャックの試みを拒否します静的ストレージは、悪意のある入力に対して回復力があり、動的条件(ここでは関数が呼び出された回数)に基づいて動作できます。

可変デフォルトを使用するための鍵は、メモリ内の変数を再割り当てすることを何もしないで、常に変数をその場で変更することです。

この手法の潜在的な能力と有用性を実際に確認するには、この最初のプログラムを「DITTLE.py」という名前で現在のディレクトリに保存してから、次のプログラムを実行します。新しいdittle()コマンドをインポートして使用します。覚えておく手順や、ジャンプするフープをプログラミングする必要はありません。

これが2番目の例です。これをコンパイルして、新しいプログラムとして実行します。

from DITTLE import dittle

print("\n We have emulated a new python command with 'dittle()'.\n")
# Nothing to declare, nothing to instantize, nothing to remember.

dittle()
dittle()
dittle()
dittle()
dittle()

さて、それは可能な限り滑らかできれいではありませんか?これらの変更可能なデフォルトは本当に便利です。

========================

しばらく私の答えを振り返った後、私は可変のデフォルトの方法を使用することと同じことを達成する通常の方法を使用することの違いを明確にしたかどうかわかりません。

通常の方法は、Classオブジェクトをラップする(そしてグローバルを使用する)インポート可能な関数を使用することです。したがって、比較のために、ここでは、可変のデフォルトメソッドと同じことを実行しようとするクラスベースのメソッドを示します。

from time import sleep

class dittle_class():

    def __init__(self):
        
        self.b = 0
        self.a = " Hello World!"
        
        print("\n Initializing Class Object. Executes on First Call only.")
        print(" self.a = '"+str(self.a),"', self.b =",self.b,end="\n\n")
    
    def report(self):
        self.b  = self.b + 1
        
        if self.b == 1:
            print(" Dittle() called",self.b,"time.")
        else:
            print(" Dittle() called",self.b,"times.")
        
        if self.b == 5:
            self.a = " It's Great to be alive!"
        
        print(" Internal String =",self.a,end="\n\n")
            
        if self.b ==3:
            print(" Let's rest for a moment.")
            sleep(2.0) # Since we imported it, we might as well use it.
            print(" Wheew! Ready to continue.\n")
            sleep(1.0)

cl= dittle_class()

def dittle():
    global cl
    
    if type(cl.a) != str and type(cl.b) != int:
        print(" Class exists but does not have valid format.")
        
    cl.report()

# =================== MAIN ====================== 
if __name__ == "__main__":
    print(" We have emulated a python command with our own 'dittle()' command.\n")
    for cnt in range(2):dittle() # Call can be loop-driver, but they need not be.
    
    print(" Attempting to pass arguments to dittle()")
    try: # The user must catch the fatal error. The mutable default user did not. 
        dittle(["BAD","Data"])
    except:
        print(" This caused a fatal error that can't be caught in the function.\n")
    
    print(" Calling dittle() normally..")
    dittle()
    
    print(" Attempting to set the Class variable from the outside.")
    cl.a = " I'm a griefer. My damage sticks."
    cl.b = -7
    
    dittle()
    dittle()

このクラスベースプログラムを現在のディレクトリにDITTLE.pyとして保存してから、次のコードを実行します(これは前と同じです)。

from DITTLE import dittle
# Nothing to declare, nothing to instantize, nothing to remember.

dittle()
dittle()
dittle()
dittle()
dittle()

2つの方法を比較することにより、関数で可変デフォルトを使用する利点がより明確になるはずです。可変のデフォルトメソッドはグローバルを必要とせず、その内部変数を直接設定することはできません。そして、可変メソッドは、知識のある渡された引数を1サイクル受け入れてから肩をすくめましたが、Classメソッドは、その内部変数が外部に直接公開されているため、永続的に変更されました。プログラミングが簡単な方法はどれですか?それは、方法に対するあなたの快適さのレベルとあなたの目標の複雑さに依存すると思います。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.