11

そこで、Pythonでカリー化関数を試してみましたが、functools.partialが実際の関数ではなく部分的なオブジェクトを返すことに気づきました。これについて私を悩ませたのは、次のようなことをした場合です。

five = partial(len, 'hello')
five('something')

その後、

TypeError: len() takes exactly 1 argument (2 given)

でも私がしたいのは

TypeError: five() takes no arguments (1 given)

このように機能させるためのクリーンな方法はありますか?私は回避策を書きましたが、それは私の好みにはあまりにもハッキーです(varargsを持つ関数ではまだ機能しません):

def mypartial(f, *args):
  argcount = f.func_code.co_argcount - len(args)
  params = ''.join('a' + str(i) + ',' for i in xrange(argcount))
  code = '''
def func(f, args):
  def %s(%s):
    return f(*(args+(%s)))
  return %s
  ''' % (f.func_name, params, params, f.func_name)

  exec code in locals()
  return func(f, args)

編集:コンテキストを追加すると役立つと思います。私は次のような関数を自動的にカレーするデコレータを書いています:

@curry
def add(a, b, c):
  return a + b + c

f = add(1, 2) # f is a function
assert f(5) == 8

fがパーシャルから作成されたという事実を隠したい(おそらく悪い考え:P)。上記のTypeErrorメッセージが表示するメッセージは、何かが部分的であるかどうかを明らかにできる一例です。それを変えたい。

これは一般化可能である必要があるため、EnricoGiampieriとmgilsonの提案はその特定の場合にのみ機能します。

4

2 に答える 2

4

あなたは間違いなくこれをでやりたくないですexec

このpartialような純粋なPythonでのレシピを見つけることができます。それらの多くはレシピとして誤ったラベルが付けられているので、それも探してください。とにかく、これらはなしでそれを行うための適切な方法をあなたに示します、そしてあなたはただ一つを選んでそれをあなたが望むことをするように修正することができます。curryexec

または、単にラップすることもできますpartial…</ p>

ただし、何をするにしても、ラッパーが「five」という名前の関数を定義していることを知る方法はありません。これは、関数を格納する変数の名前にすぎません。したがって、カスタム名が必要な場合は、それを関数に渡す必要があります。

five = my_partial('five', len, 'hello')

その時点で、なぜこれが単に新しい関数を定義するよりも優れているのか疑問に思う必要があります。

しかし、とにかくこれがあなたが実際に望んでいることではないと思います。最終的な目標は@curry、decorated関数と同じ名前(およびdocstring、arg listなど)で、decorated関数のカレーバージョンを作成するデコレーターを定義することです。中間体の名前を置き換えるという考え全体partialは、赤いニシンです。functools.wraps関数内で適切に使用curryすると、カリー化された関数をどのように定義しても、元の関数の名前が保持されます。

場合によっては、functools.wraps動作しません。実際、これはそのような場合の1つである可能性があります。たとえば、argリストを変更する必要があるため、curry(len)1つのパラメーターを必要とせずに、0または1つのパラメーターを使用できます。update_wrapper、および(非常に単純な)ソースコードを参照wrapsupdate_wrapperて、基本がどのように機能するかを確認し、そこから構築してください。

前の拡張:関数をカレーするには、argsを明示的に取得(*args)または解析するものを返す必要があり、場合によっては、その他の適切な例外を明示的に発生させる必要があります。なんで?さて、もし3つのパラメータを取るなら、0、1、2、または3つのパラメータを取り、0-2のパラメータを与えられるなら、それは0からn-1のパラメータを取る関数を返します。(*args, **kw)TypeErrorfoocurry(foo)

必要な理由**kwは、呼び出し元が名前でパラメータを指定できるようにするためです。ただし、引数の蓄積が完了したときにチェックするのははるかに複雑になります。おそらく、これはカリー化とは奇妙なことです。最初に名前付きパラメータをでバインドしpartial、次にcurry結果をバインドして、残りのすべてのパラメータをカリー化されたスタイルで渡します…</ p>

foodefault-valueまたはkeywordargsがある場合、それはさらに複雑になりますが、これらの問題がなくても、すでにこの問題に対処する必要があります。

たとえばcurry、関数と、すでに実行されているすべてのパラメーターをインスタンスメンバーとして保持するクラスとして実装するとします。次に、次のようなものになります。

def __call__(self, *args):
    if len(args) + len(self.curried_args) > self.fn.func_code.co_argcount:
        raise TypeError('%s() takes exactly %d arguments (%d given)' %
                        (self.fn.func_name, self.fn.func_code.co_argcount,
                         len(args) + len(self.curried_args)))
    self.curried_args += args
    if len(self.curried_args) == self.fn.func_code.co_argcount:
        return self.fn(*self.curried_args)
    else:
        return self

これはひどく単純化されすぎていますが、基本を処理する方法を示しています。

于 2012-11-20T23:05:57.340 に答える
0

私の推測では、部分関数は関数の実行を遅らせるだけであり、それからまったく新しい関数を作成することはありません。

私の推測では、新しい関数を直接定義する方が簡単です。

def five(): return len('hello')

これは非常に単純な行であり、コードが乱雑になることはなく、非常に明確なので、特にこの状況が多くの場合に必要ない場合は、それを置き換える関数をわざわざ作成する必要はありません。

于 2012-11-20T22:57:57.640 に答える