functoolsパーシャルはどのように機能しますか?


180

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

>>> 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として渡されることylambda返されるsum(1, y)すなわち1 + y

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

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

回答:


218

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

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)

しかし、すでに持っている関数contextは、その仕事をするためにいくつかの3番目のオブジェクトにアクセスする必要があります。

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、最短かつ最速です。(ただし、より複雑なケースでは、カスタムオブジェクトが必要になる場合があります)。


1
extra_args変数をどこから取得したのか
user1865341

2
extra_argsの例p = partial(func, 1); f(2, 3, 4)では、部分的な呼び出し元によって渡されたものです(2, 3, 4)
bereal 2013年

1
しかし、なぜそれを行うのか、何かが部分的にのみ行われる必要があり、他のもので行うことができない特別なユースケース
user1865341

@ user1865341答えに例を追加しました。
bereal 2013年

あなたの例で、関係するものであるcallbackmy_callback
user1865341

92

パーシャルは信じられないほど便利です。

たとえば、関数呼び出しの「パイプライン」シーケンス(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)

しかし、あなたはcan't - ソート方法のキーパラメータのみを取る機能受け入れる単一の引数を。

したがって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])

1
この関数の実用的な使い方はありますか
user1865341

3
@ user1865341が私の回答に2つの例示的な使用例を追加しました
doug

私見、これはオブジェクトやクラスなどの無関係な概念を排除し、これがすべてである機能に焦点を当てているので、これはより良い答えです。
akhan

35

短い答え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

5
私たちは、デフォルト値を上書きすることができますので、これは真の半分で、私たちも、その後でオーバーライドされたパラメータを上書きすることができますpartialというように
Azat Ibrakov

33

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

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

ブログからのシンプルだがきちんとした初心者の例は、コードを読みやすくするためにどのように使用できるかをカバーpartialre.searchています。 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_apartで、引数が適用さis_grouped_togetherれた2つの新しい関数が派生しました(メソッドのシグネチャの最初の引数であるため)。re.searchpatternpatternre.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()

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


1
これは同等is_spaced_apart = re.compile('[a-zA-Z]\s\=').searchですか?もしそうなら、partialイディオムがより速い再利用のために正規表現をコンパイルするという保証はありますか?
アリスティド

10

私の意見では、それはPythonでカレー化を実装する方法です。

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

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

if __name__ == "__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です。


1

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

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'に複数の値を取得しました」というエラーがスローされます


え?あなたはこのような左端のパラメータを実行しますprt=partial(func, 7)
DylanYoung

0

この回答は、サンプルコードの詳細です。上記のすべての回答は、なぜパーシャルを使用する必要があるかについての適切な説明を提供します。パーシャルについての観察と使用例を示します。

from functools import partial
 def adder(a,b,c):
    print('a:{},b:{},c:{}'.format(a,b,c))
    ans = a+b+c
    print(ans)
partial_adder = partial(adder,1,2)
partial_adder(3)  ## now partial_adder is a callable that can take only one argument

上記のコードの出力は次のようになります。

a:1,b:2,c:3
6

上記の例では、引数としてパラメーター(c)を取る新しい呼び出し可能オブジェクトが返されたことに注意してください。関数の最後の引数でもあることに注意してください。

args = [1,2]
partial_adder = partial(adder,*args)
partial_adder(3)

上記のコードの出力も次のとおりです。

a:1,b:2,c:3
6

*は非キーワード引数をアンパックするために使用され、呼び出し可能な引数は上記と同じであることに注意してください。

別の観察は次のとおりです。 以下の例、宣言されていないパラメーター(a)を引数として取るcallableをpartialが返すことを示しています。

def adder(a,b=1,c=2,d=3,e=4):
    print('a:{},b:{},c:{},d:{},e:{}'.format(a,b,c,d,e))
    ans = a+b+c+d+e
    print(ans)
partial_adder = partial(adder,b=10,c=2)
partial_adder(20)

上記のコードの出力は次のようになります。

a:20,b:10,c:2,d:3,e:4
39

同様に、

kwargs = {'b':10,'c':2}
partial_adder = partial(adder,**kwargs)
partial_adder(20)

上記のコードプリント

a:20,b:10,c:2,d:3,e:4
39

モジュールのPool.map_asyncメソッドを使用しているときに使用する必要がありましたmultiprocessing。ワーカー関数に渡すことができる引数は1つだけなのでpartial、ワーカー関数を入力引数が1つだけの呼び出し可能のように見せるために使用する必要がありましたが、実際には、ワーカー関数には複数の入力引数がありました。

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