¿Cómo hacen los functools parciales lo que hacen?

180

No puedo entender cómo funciona el parcial en functools. Tengo el siguiente código desde aquí :

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

Ahora en la linea

incr = lambda y : sum(1, y)

Entiendo que cualquier argumento que le pase incrserá pasado en cuanto ya lambdacuál devolverá, sum(1, y)es decir 1 + y.

Entiendo que. Pero no entendí esto incr2(4).

¿Cómo 4se pasa como xen función parcial? Para mí, 4debería reemplazar el sum2. ¿Cuál es la relación entre xy 4?

usuario1865341
fuente

Respuestas:

218

Aproximadamente, partialhace algo como esto (aparte de la compatibilidad con argumentos de palabras clave, etc.):

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

    return wrapper

Entonces, al llamar partial(sum2, 4), crea una nueva función (una llamada, para ser precisos) que se comporta como sum2, pero tiene un argumento posicional menos. Ese argumento perdido siempre se sustituye por 4, de modo quepartial(sum2, 4)(2) == sum2(4, 2)

En cuanto a por qué es necesario, hay una variedad de casos. Solo por uno, supongamos que tiene que pasar una función en algún lugar donde se espera que tenga 2 argumentos:

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)

Pero una función que ya tiene necesita acceso a algún tercer contextobjeto para hacer su trabajo:

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

Entonces, hay varias soluciones:

Un objeto personalizado:

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

Lambda

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

Con parciales:

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

De esos tres, partiales el más corto y el más rápido. (Para un caso más complejo, es posible que desee un objeto personalizado).

se real
fuente
1
de donde extra_args
obtuviste
2
extra_argses algo que pasó por el llamador parcial, en el ejemplo con p = partial(func, 1); f(2, 3, 4)él es (2, 3, 4).
Bereal
1
pero por qué haríamos eso, cualquier caso de uso especial en el que algo debe hacerse solo de manera parcial y no puede hacerse con otra cosa
usuario1865341
@ user1865341 Agregué un ejemplo a la respuesta.
Bereal
con su ejemplo, cuál es la relación entre callbackymy_callback
user1865341
92

los parciales son increíblemente útiles.

Por ejemplo, en una secuencia de llamadas de función 'en línea' (en la que el valor devuelto de una función es el argumento pasado a la siguiente).

A veces, una función en una tubería de este tipo requiere un único argumento , pero la función inmediatamente aguas arriba devuelve dos valores .

En este escenario, functools.partialpodría permitirle mantener intacta esta canalización de funciones.

Aquí hay un ejemplo específico y aislado: suponga que desea ordenar algunos datos por la distancia de cada punto de datos desde algún objetivo:

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

Para ordenar estos datos por distancia del objetivo, lo que le gustaría hacer, por supuesto, es esto:

data.sort(key=euclid_dist)

pero no puede: el parámetro clave del método de clasificación solo acepta funciones que toman un solo argumento.

reescriba euclid_distcomo una función tomando un solo parámetro:

from functools import partial

p_euclid_dist = partial(euclid_dist, target)

p_euclid_dist ahora acepta un solo argumento,

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

así que ahora puede ordenar sus datos pasando la función parcial para el argumento clave del método de clasificación:

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

O, por ejemplo, uno de los argumentos de la función cambia en un bucle externo, pero se repara durante la iteración en el bucle interno. Al usar un parcial, no tiene que pasar el parámetro adicional durante la iteración del bucle interno, porque la función modificada (parcial) no lo requiere.

>>> from functools import partial

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

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

crear una función parcial (usando la palabra clave arg)

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

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

También puede crear una función parcial con un argumento posicional

>>> pfnx = partial(fnx, 12)

>>> pfnx(4, 5)
      21

pero esto arrojará (por ejemplo, crear parciales con argumentos de palabras clave y luego llamar utilizando argumentos posicionales)

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

otro caso de uso: escribir código distribuido usando la multiprocessingbiblioteca de Python . Se crea un grupo de procesos utilizando el método Pool:

>>> import multiprocessing as MP

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

Pool tiene un método de mapa, pero solo toma un único iterable, por lo que si necesita pasar una función con una lista de parámetros más larga, redefina la función como parcial, para arreglar todos menos uno:

>>> ppool.map(pfnx, [4, 6, 7, 8])
Doug
fuente
1
¿hay algún uso práctico de esta función en algún momento
user1865341
3
@ user1865341 agregó dos casos de uso ejemplar a mi respuesta
doug
En mi humilde opinión, esta es una mejor respuesta, ya que evita conceptos no relacionados como objetos y clases y se centra en las funciones, de eso se trata.
akhan
35

respuesta corta, partialda valores predeterminados a los parámetros de una función que de otro modo no tendría valores predeterminados.

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
Alex Fortin
fuente
55
esto es cierto a medias porque podemos anular los valores predeterminados, incluso podemos anular los parámetros anulados por posteriores partialy así sucesivamente
Azat Ibrakov
33

Los parciales se pueden usar para crear nuevas funciones derivadas que tengan algunos parámetros de entrada preasignados

Para ver el uso de parciales en el mundo real, consulte esta muy buena publicación de blog:
http://chriskiehl.com/article/Cleaner-coding-through-partially-applied-functions/

Un simple pero claro ejemplo de principiante del blog, cubiertas cómo se podrían utilizar partialen re.searchhacer el código más legible. re.searchLa firma del método es:

search(pattern, string, flags=0) 

Al aplicar partialpodemos crear múltiples versiones de la expresión regular searchpara satisfacer nuestros requisitos, por ejemplo:

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

Ahora is_spaced_aparty is_grouped_togetherson dos nuevas funciones derivadas de re.searchque tienen el patternargumento aplicado (ya que patternes el primer argumento en la re.searchfirma del método).

La firma de estas dos nuevas funciones (invocables) es:

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

Así es como podría usar estas funciones parciales en algún texto:

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

Puede consultar el enlace de arriba para obtener una comprensión más profunda del tema, ya que cubre este ejemplo específico y mucho más.

sisanared
fuente
1
¿No es esto equivalente a is_spaced_apart = re.compile('[a-zA-Z]\s\=').search? Si es así, ¿hay una garantía de que el partialidioma compila la expresión regular para una reutilización más rápida?
Aristide
10

En mi opinión, es una forma de implementar curry en 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))

El resultado es 3 y 4.

Hanzhou Tang
fuente
1

También vale la pena mencionar que cuando una función parcial pasa a otra función en la que queremos "codificar" algunos parámetros, ese debería ser el parámetro más adecuado

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

pero si hacemos lo mismo, pero cambiando un parámetro en su lugar

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

arrojará el error, "TypeError: func () obtuvo múltiples valores para el argumento 'a'"

MSK
fuente
¿Eh? Haces el parámetro más a la izquierda de esta manera:prt=partial(func, 7)
DylanYoung
0

Esta respuesta es más un código de ejemplo. Todas las respuestas anteriores dan buenas explicaciones sobre por qué uno debe usar parcial. Daré mis observaciones y usaré casos sobre parcial.

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

La salida del código anterior debe ser:

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

Observe que en el ejemplo anterior se devolvió un nuevo invocable que tomará el parámetro (c) como argumento. Tenga en cuenta que también es el último argumento de la función.

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

La salida del código anterior también es:

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

Observe que * se usó para desempaquetar los argumentos que no son palabras clave y que el invocable devuelto en términos de qué argumento puede tomar es el mismo que el anterior.

Otra observación es: el siguiente ejemplo demuestra que parcial devuelve un invocable que tomará el parámetro no declarado (a) como argumento.

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)

La salida del código anterior debe ser:

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

Similar,

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

Imprime el código anterior

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

Tuve que usarlo cuando estaba usando el Pool.map_asyncmétodo del multiprocessingmódulo. Puede pasar solo un argumento a la función de trabajador, por lo que tuve que usar partialpara hacer que mi función de trabajo parezca invocable con solo un argumento de entrada, pero en realidad mi función de trabajador tenía múltiples argumentos de entrada.

Ruthvik Vaila
fuente