Як функціональні функції частково роблять те, що вона робить?


181

Я не в змозі переконатися в тому, як працює часткова робота у фунікулерах. У мене звідси такий код :

>>> 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?

Відповіді:


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, але має на один позиційний аргумент менше. Цей пропущений аргумент завжди замінюється на 4, так щоpartial(sum2, 4)(2) == sum2(4, 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об'єкта, щоб виконувати свою роботу:

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))

З цих трьох partial- найкоротший і найшвидший. (Для більш складного випадку, можливо, ви хочете користувальницький об’єкт).


1
звідки ви отримали extra_argsзмінну
user1865341

2
extra_argsце те, що передається частковим абонентом, у прикладі з p = partial(func, 1); f(2, 3, 4)ним є (2, 3, 4).
bereal

1
але чому ми це зробимо, будь-який випадок спеціального використання, коли щось потрібно робити лише частково, а іншого не можна зробити
user1865341

@ user1865341 Я додав приклад до відповіді.
bereal

з вашим прикладом, що співвідношення між callbackіmy_callback
user1865341

92

частки неймовірно корисні.

Наприклад, у послідовності виклику функцій (у якій викладено трубу) (у якому повернене значення з однієї функції є аргументом, переданим наступному).

Іноді функція в такому конвеєрі вимагає одного аргументу , але функція, що знаходиться негайно від неї, повертає два значення .

У цьому сценарії, 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)

але ви не можете - ключовий параметр методу сортування приймає лише функції, які беруть один аргумент.

тому перепишіть euclid_distяк функцію, що приймає один параметр:

from functools import partial

p_euclid_dist = partial(euclid_dist, target)

p_euclid_dist тепер приймає єдиний аргумент,

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

тож тепер ви можете сортувати свої дані, передавши часткову функцію для ключового аргументу методу сортування:

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

Або, наприклад, один з аргументів функції змінюється у зовнішньому циклі, але фіксується під час ітерації у внутрішньому циклі. Використовуючи частковий, вам не доведеться передавати додатковий параметр під час ітерації внутрішнього циклу, оскільки змінена (часткова) функція цього не вимагає.

>>> 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'

інший випадок використання: запис розподіленого коду за допомогою multiprocessingбібліотеки python . Пул процесів створюється за допомогою методу Pool:

>>> import multiprocessing as MP

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

Pool має метод карти, але він займає лише один ітерабельний, тому, якщо вам потрібно передати функцію з більш довгим списком параметрів, повторно визначте функцію як часткову, щоб виправити всі, крім одного:

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

1
чи практично використовувати цю функцію дещо
користувач1865341

3
@ user1865341 додав до моєї відповіді два випадкові випадки використання
doug

ІМХО, це краща відповідь, оскільки він зберігає незв’язані між собою такі поняття, як об'єкти та класи, і фокусується на функціях, для чого це все.
акхан

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і так далі
Азат Ібраков

33

Партіали можна використовувати для створення нових похідних функцій, яким попередньо призначені деякі вхідні параметри

Щоб побачити деяке використання реальних випадків у світі, перегляньте цю справді хорошу публікацію в блозі:
http://chriskiehl.com/article/Cleaner-coding-through-partial-applied-functions/

Прості , але охайні початківець приклад з блог, обкладинки , як можна було б використовувати partialна , re.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дві нові функції, отримані з re.searchцього, patternзастосовують аргумент (оскільки patternце перший аргумент у re.searchпідписі методу).

Підписом цих двох нових функцій (за викликом) є:

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

На мою думку, це спосіб реалізувати каррі в пітоні.

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) як аргумент.

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модуля. Ви можете передавати лише один аргумент функції робочого, тому мені довелося використовувати, partialщоб моя робоча функція виглядала як дзвінка лише з одним вхідним аргументом, але насправді моя функція працюючого мала кілька вхідних аргументів.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.