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
としてどのように渡されますか?私にとって、4
はsum2
を置き換える必要があります。 x
と4
の関係は何ですか?
大まかに、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
が最短で最速です。 (より複雑なケースでは、カスタムオブジェクトが必要な場合があります)。
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])
パーシャルを使用して、いくつかの入力パラメーターが事前に割り当てられた新しい派生関数を作成できます
パーシャルの実際の使用方法を確認するには、この非常に良いブログ投稿を参照してください。
http://chriskiehl.com/article/Cleaner-coding-through-partially-applied-functions/
ブログのシンプルだがきちんとした初心者の例は、コードをより読みやすくするためにre.search
でpartial
を使用する方法をカバーしています。 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
はre.search
から派生した2つの新しい関数であり、pattern
引数が適用されています(pattern
はre.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()
上記のリンクを参照して、この特定の例をカバーしているため、主題のより深い理解を得ることができます。
短い答え、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
私の意見では、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です。
また、言及する価値があるのは、一部のパラメーターを「ハードコード」したい別の関数に部分関数が渡された場合、それが右端のパラメーターであることです。
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'に複数の値を取得しました」というエラーがスローされます