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


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.