web-dev-qa-db-ja.com

functoolsは部分的にどのように機能しますか?

Functoolsでパーシャルがどのように機能するかについて頭を悩ますことはできません。 here の次のコードがあります:

>>> sum = lambda x, y : x + y
>>> sum(1, 2)
3
>>> incr = lambda y : sum(1, y)
>>> incr(2)
3
>>> def sum2(x, y):
    return x + y

>>> incr2 = functools.partial(sum2, 1)
>>> incr2(4)
5

今、行に

incr = lambda y : sum(1, y)

incrに渡す引数は、yとしてlambdaに渡され、sum(1, y)、つまり1 + yが返されます。

という事は承知しています。しかし、私はこのincr2(4)を理解していませんでした。

4は、部分関数でxとしてどのように渡されますか?私にとって、4sum2を置き換える必要があります。 x4の関係は何ですか?

140
user1865341

大まかに、partialは次のようなことを行います(キーワードargsサポートなどは別として):

def partial(func, *part_args):
    def wrapper(*extra_args):
        args = list(part_args)
        args.extend(extra_args)
        return func(*args)

    return wrapper

したがって、partial(sum2, 4)を呼び出すことで、sum2のように動作し、位置引数が1つ少ない新しい関数(正確には呼び出し可能関数)を作成します。その欠落した引数は常に4に置き換えられるため、partial(sum2, 4)(2) == sum2(4, 2)

それが必要な理由については、さまざまなケースがあります。 1つだけのために、2つの引数があると予想される場所に関数を渡す必要があるとします。

class EventNotifier(object):
    def __init__(self):
        self._listeners = []

    def add_listener(self, callback):
        ''' callback should accept two positional arguments, event and params '''
        self._listeners.append(callback)
        # ...

    def notify(self, event, *params):
        for f in self._listeners:
            f(event, params)

ただし、すでに持っている関数は、3番目のcontextオブジェクトにアクセスしてジョブを実行する必要があります。

def log_event(context, event, params):
    context.log_event("Something happened %s, %s", event, params)

そのため、いくつかの解決策があります。

カスタムオブジェクト:

class Listener(object):
   def __init__(self, context):
       self._context = context

   def __call__(self, event, params):
       self._context.log_event("Something happened %s, %s", event, params)


 notifier.add_listener(Listener(context))

ラムダ:

log_listener = lambda event, params: log_event(context, event, params)
notifier.add_listener(log_listener)

パーシャル付き:

context = get_context()  # whatever
notifier.add_listener(partial(log_event, context))

この3つのうち、partialが最短で最速です。 (より複雑なケースでは、カスタムオブジェクトが必要な場合があります)。

169
bereal

partialsは非常に便利です。

たとえば、関数呼び出しの「パイプライン」シーケンス(1つの関数からの戻り値は、次の引数に渡されます)。

そのようなパイプラインの関数には、単一の引数が必要な場合がありますが、そのすぐ上流の関数は2つの値

このシナリオでは、functools.partialを使用すると、この関数パイプラインをそのまま保持できる場合があります。

特定の孤立した例を次に示します。ターゲットからの各データポイントの距離でデータを並べ替えるとします。

# create some data
import random as RND
fnx = lambda: RND.randint(0, 10)
data = [ (fnx(), fnx()) for c in range(10) ]
target = (2, 4)

import math
def euclid_dist(v1, v2):
    x1, y1 = v1
    x2, y2 = v2
    return math.sqrt((x2 - x1)**2 + (y2 - y1)**2)

このデータをターゲットからの距離でソートするには、もちろん次のようにします:

data.sort(key=euclid_dist)

しかし、できません-sortメソッドのkeyパラメーターのみが受け入れますsingle引数を取る関数。

singleパラメーターをとる関数としてeuclid_distを書き直してください:

from functools import partial

p_euclid_dist = partial(euclid_dist, target)

p_euclid_distは単一の引数を受け入れるようになりました。

>>> p_euclid_dist((3, 3))
  1.4142135623730951

したがって、sortメソッドのキー引数に部分関数を渡すことで、データをソートできます。

data.sort(key=p_euclid_dist)

# verify that it works:
for p in data:
    print(round(p_euclid_dist(p), 3))

    1.0
    2.236
    2.236
    3.606
    4.243
    5.0
    5.831
    6.325
    7.071
    8.602

または、たとえば、関数の引数の1つが外側のループで変化しますが、内側のループでの反復中に固定されます。パーシャルを使用すると、変更された(部分的な)関数が必要としないため、内側のループの反復中に追加のパラメーターを渡す必要はありません。

>>> from functools import partial

>>> def fnx(a, b, c):
      return a + b + c

>>> fnx(3, 4, 5)
      12

部分関数を作成します(キーワードargを使用)

>>> pfnx = partial(fnx, a=12)

>>> pfnx(b=4, c=5)
     21

位置引数を使用して部分関数を作成することもできます

>>> pfnx = partial(fnx, 12)

>>> pfnx(4, 5)
      21

しかし、これはスローします(例えば、キーワード引数でパーシャルを作成してから、位置引数を使用して呼び出します)

>>> pfnx = partial(fnx, a=12)

>>> pfnx(4, 5)
      Traceback (most recent call last):
      File "<pyshell#80>", line 1, in <module>
      pfnx(4, 5)
      TypeError: fnx() got multiple values for keyword argument 'a'

別のユースケース:pythonのmultiprocessingライブラリを使用して分散コードを作成します。プロセスのプールは、Poolメソッドを使用して作成されます。

>>> import multiprocessing as MP

>>> # create a process pool:
>>> ppool = MP.Pool()

Poolにはmapメソッドがありますが、単一のイテレート可能オブジェクトのみを使用するため、より長いパラメーターリストを持つ関数を渡す必要がある場合は、1つを除くすべてを修正するために関数を部分として再定義します。

>>> ppool.map(pfnx, [4, 6, 7, 8])
66
doug

パーシャルを使用して、いくつかの入力パラメーターが事前に割り当てられた新しい派生関数を作成できます

パーシャルの実際の使用方法を確認するには、この非常に良いブログ投稿を参照してください。
http://chriskiehl.com/article/Cleaner-coding-through-partially-applied-functions/

ブログのシンプルだがきちんとした初心者の例は、コードをより読みやすくするためにre.searchpartialを使用する方法をカバーしています。 re.searchメソッドのシグネチャは次のとおりです。

search(pattern, string, flags=0) 

partialを適用することで、要件に合わせて正規表現searchの複数のバージョンを作成できます。たとえば、

is_spaced_apart = partial(re.search, '[a-zA-Z]\s\=')
is_grouped_together = partial(re.search, '[a-zA-Z]\=')

これでis_spaced_apartis_grouped_togetherre.searchから派生した2つの新しい関数であり、pattern引数が適用されています(patternre.searchメソッドの署名の最初の引数であるため)。

これら2つの新しい関数(呼び出し可能)のシグネチャは次のとおりです。

is_spaced_apart(string, flags=0)     # pattern '[a-zA-Z]\s\=' applied
is_grouped_together(string, flags=0) # pattern '[a-zA-Z]\=' applied

これは、テキストでこれらの部分関数を使用する方法です。

for text in lines:
    if is_grouped_together(text):
        some_action(text)
    Elif is_spaced_apart(text):
        some_other_action(text)
    else:
        some_default_action()

上記のリンクを参照して、この特定の例をカバーしているため、主題のより深い理解を得ることができます。

26
sisanared

短い答え、partialは、そうでなければデフォルト値を持たない関数のパラメーターにデフォルト値を与えます。

from functools import partial

def foo(a,b):
    return a+b

bar = partial(foo, a=1) # equivalent to: foo(a=1, b)
bar(b=10)
#11 = 1+10
bar(a=101, b=10)
#111=101+10
19

私の意見では、Pythonで currying を実装する方法です。

from functools import partial
def add(a,b):
    return a + b

def add2number(x,y,z):
    return x + y + z

if __== "__main__":
    add2 = partial(add,2)
    print("result of add2 ",add2(1))
    add3 = partial(partial(add2number,1),2)
    print("result of add3",add3(1))

結果は3と4です。

7
Hanzhou Tang

また、言及する価値があるのは、一部のパラメーターを「ハードコード」したい別の関数に部分関数が渡された場合、それが右端のパラメーターであることです。

def func(a,b):
    return a*b
prt = partial(func, b=7)
    print(prt(4))
#return 28

しかし、同じことをするが、代わりにパラメータを変更する場合

def func(a,b):
    return a*b
 prt = partial(func, a=7)
    print(prt(4))

「TypeError:func()が引数 'a'に複数の値を取得しました」というエラーがスローされます

1
MSK