2

I often do interactive work in Python that involves some expensive operations that I don't want to repeat often. I'm generally running whatever Python file I'm working on frequently.

If I write:

import functools32

@functools32.lru_cache()
def square(x):
    print "Squaring", x
    return x*x

I get this behavior:

>>> square(10)
Squaring 10
100
>>> square(10)
100
>>> runfile(...)
>>> square(10)
Squaring 10
100

That is, rerunning the file clears the cache. This works:

try:
    safe_square
except NameError:
    @functools32.lru_cache()
    def safe_square(x):
        print "Squaring", x
        return x*x

but when the function is long it feels strange to have its definition inside a try block. I can do this instead:

def _square(x):
    print "Squaring", x
    return x*x

try:
    safe_square_2
except NameError:
    safe_square_2 = functools32.lru_cache()(_square)

but it feels pretty contrived (for example, in calling the decorator without an '@' sign)

Is there a simple way to handle this, something like:

@non_resetting_lru_cache()
def square(x):
    print "Squaring", x
    return x*x

?

4

4 に答える 4

7

同じセッションで繰り返し実行されるスクリプトを作成するのは、奇妙なことです。

あなたがそれをしたい理由はわかりますが、それでも奇妙です。コードが少し奇妙に見え、それを説明するコメントを付けて、その奇妙さを明らかにすることは不合理ではないと思います。

ただし、必要以上に醜いものを作成しました。

まず、これを行うことができます:

@functools32.lru_cache()
def _square(x):
    print "Squaring", x
    return x*x

try:
    safe_square_2
except NameError:
    safe_square_2 = _square

_square新しい定義にキャッシュをアタッチしても害はありません。時間や数バイト以上のストレージを無駄にすることはありません。また、最も重要なことは、以前 _squareの定義のキャッシュに影響を与えないことです。それが閉鎖の要点です。


ここに、再帰関数に関する潜在的な問題があります。それはあなたの作業方法にすでに内在しており、キャッシュがそれに追加されることはありませんが、キャッシュがあるためにのみ気付くかもしれないので、それを説明し、修正する方法を示します. この関数を考えてみましょう:

@lru_cache()
def _fact(n):
    if n < 2:
        return 1
    return _fact(n-1) * n

スクリプトを再実行すると、 old への参照があっても、グローバル名としてアクセスしているため_fact、 new を呼び出すことになります。;とは何の関係もありません。それを削除すると、古い関数は引き続きnew を呼び出すことになります。_fact_fact@lru_cache_fact

ただし、上記の名前変更のトリックを使用している場合は、名前を変更したバージョンを呼び出すことができます。

@lru_cache()
def _fact(n):
    if n < 2:
        return 1
    return fact(n-1) * n

古い_factは を呼び出しますがfact、これはまだ古い_factです。繰り返しますが、これはキャッシュ デコレータの有無にかかわらず同じように機能します。


その最初のトリックを超えて、そのパターン全体を単純なデコレーターに分解することができます。以下で順を追って説明するか、このブログ投稿を参照してください。


とにかく、見栄えの悪いバージョンでも、まだ少し見苦しく冗長です。そして、これを何十回も行っていると、私の「まあ、少し醜く見えるはずだ」という正当化はすぐに薄れてしまいます。したがって、これは、常に醜さを取り除くのと同じ方法で処理する必要があります。つまり、関数でラップします。

Python では、実際に名前をオブジェクトとして渡すことはできません。そして、これに対処するためだけに恐ろしいフレームハックを使用したくありません. そのため、名前を文字列として渡す必要があります。これを好き:

globals().setdefault('fact', _fact)

このglobals関数は、現在のスコープのグローバル ディクショナリを返すだけです。dictつまり、メソッドがあることを意味します。これは、値がまだない場合setdefaultはグローバル名factを値に設定することを意味し_factますが、値がある場合は何もしません。これはまさにあなたが望んでいたものです。(現在のモジュールで使用することもできますがsetattr、この方法は、スクリプトがモジュールとして使用されるのではなく、他の誰かのスコープで(繰り返し)実行されることを意図していることを強調していると思います。)

したがって、ここでは関数にまとめられています。

def new_bind(name, value):
    globals().setdefault(name, value)

…これをほとんど簡単にデコレータに変えることができます:

def new_bind(name):
    def wrap(func):
        globals().setdefault(name, func)
        return func
    return wrap

次のように使用できます。

@new_bind('foo')
def _foo():
    print(1)

しかし、待ってください。取得する には がありますfuncよね?「プライベート」名は接頭辞付きの「パブリック」名でなければならないなど、命名規則に固執する場合、これを行うことができます。new_bind__name___

def new_bind(func):
    assert func.__name__[0] == '_'
    globals().setdefault(func.__name__[1:], func)
    return func

そして、これがどこに向かっているのかを見ることができます:

@new_bind
@lru_cache()
def _square(x):
    print "Squaring", x
    return x*x

小さな問題が 1 つあります。関数を適切にラップしない他のデコレータを使用すると、命名規則が破られます。だから… それをしないでください。:)


そして、これはすべてのエッジケースであなたが望むように正確に機能すると思います. 特に、ソースを編集して、新しいキャッシュで新しい定義を強制したい場合はdel square、ファイルを再実行する直前に、それが機能します。


もちろん、これら 2 つのデコレーターを 1 つにマージしたい場合は、それを .xml と呼ぶのは簡単non_resetting_lru_cacheです。

しかし、私はそれらを別々に保ちます。彼らが何をしているのかは、より明白だと思います。また、別のデコレータを にラップしたい場合@lru_cacheでも、おそらく@new_bind最も外側のデコレータになりたいと思うでしょう。


new_bindインポートできるモジュールに入れたい場合はどうしますか? 現在書いているモジュールではなく、そのモジュールのグローバルを参照するため、機能しません。

globalsdict、モジュールオブジェクト、またはモジュール名を引数として明示的に渡すことで修正でき@new_bind(__name__)ます。しかし、それは醜く、繰り返します。

醜いフレームハックで修正することもできます. 少なくとも CPython ではsys._getframe()、呼び出し元のフレームを取得するために使用できframe objects、グローバル名前空間への参照を持つことができます。

def new_bind(func):
    assert func.__name__[0] == '_'
    g = sys._getframe(1).f_globals
    g.setdefault(func.__name__[1:], func)
    return func

これが CPython にのみ適用される可能性がある「実装の詳細」であり、「内部および特殊な目的のみ」であることを示すドキュメントの大きなボックスに注意してください。これを真剣に考えてください。誰かが純粋な Python で実装できる標準ライブラリまたはビルトインのクールなアイデアを持っているときはいつでも_getframe、それは通常、純粋な Python ではまったく実装できないアイデアとほぼ同じように扱われます。しかし、あなたが何をしているのかを知っていて、これを使いたいと思っていて、現在のバージョンの CPython だけを気にしているなら、それはうまくいくでしょう。

于 2013-11-20T03:11:44.220 に答える