¿Por qué almacenar una función dentro de un diccionario de Python?

68

Soy un principiante en python, y acabo de aprender una técnica que involucra diccionarios y funciones. La sintaxis es fácil y parece algo trivial, pero mis sentidos de pitón tiemblan. Algo me dice que este es un concepto profundo y muy pitónico y no estoy comprendiendo su importancia. ¿Alguien puede ponerle un nombre a esta técnica y explicar cómo / por qué es útil?


La técnica es cuando tienes un diccionario de Python y una función que tienes la intención de usar. Inserta un elemento adicional en el dict, cuyo valor es el nombre de la función. Cuando esté listo para llamar a la función, emite la llamada indirectamente haciendo referencia al elemento dict, no a la función por su nombre.

El ejemplo con el que estoy trabajando es de Learn Python the Hard Way, 2nd Ed. (Esta es la versión disponible cuando se registra a través de Udemy.com ; lamentablemente, la versión HTML gratuita en vivo actualmente es Ed 3, y ya no incluye este ejemplo).

Parafrasear:

# make a dictionary of US states and major cities
cities = {'San Diego':'CA', 'New York':'NY', 'Detroit':'MI'}

# define a function to use on such a dictionary
def find_city (map, city):
    # does something, returns some value
    if city in map:
        return map[city]
    else:
        return "Not found"

# then add a final dict element that refers to the function
cities['_found'] = find_city

Entonces las siguientes expresiones son equivalentes. Puede llamar a la función directamente o haciendo referencia al elemento dict cuyo valor es la función.

>>> find_city (cities, 'New York')
NY

>>> cities['_found'](cities, 'New York')
NY

¿Alguien puede explicar qué característica del lenguaje es esta y tal vez dónde se juega en la programación "real"? Este ejercicio de juguete fue suficiente para enseñarme la sintaxis, pero no me llevó hasta allí.

mdeutschmtl
fuente
13
¿Por qué esta publicación estaría fuera de tema? ¡Es una gran pregunta sobre el concepto de algoritmo y estructura de datos!
Martijn Pieters
He visto (y hecho) algunas cosas como esta en otros idiomas. Podría mirarlo como una declaración de cambio, pero bien envuelto en un objeto pasable con tiempo de búsqueda O (1).
KChaloux
1
Tenía el presentimiento de que había algo importante y autorreferencial acerca de incluir la función dentro de su propio dict ... ver la respuesta de @ dietbuddha ... pero tal vez no.
mdeutschmtl

Respuestas:

83

El uso de un dict le permite traducir la clave en un invocable. Sin embargo, la clave no necesita ser codificada, como en su ejemplo.

Por lo general, esta es una forma de envío de llamadas, donde utiliza el valor de una variable para conectarse a una función. Digamos que un proceso de red le envía códigos de comando, una asignación de despacho le permite traducir los códigos de comando fácilmente en código ejecutable:

def do_ping(self, arg):
    return 'Pong, {0}!'.format(arg)

def do_ls(self, arg):
    return '\n'.join(os.listdir(arg))

dispatch = {
    'ping': do_ping,
    'ls': do_ls,
}

def process_network_command(command, arg):
    send(dispatch[command](arg))

Tenga en cuenta que la función que llamamos ahora depende completamente de cuál es el valor command. La clave tampoco tiene que coincidir; ni siquiera tiene que ser una cadena, puede usar cualquier cosa que pueda usarse como clave y que se ajuste a su aplicación específica.

El uso de un método de envío es más seguro que otras técnicas, como eval(), ya que limita los comandos permitidos a lo que definió de antemano. Ningún atacante va a pasar una ls)"; DROP TABLE Students; --inyección por una mesa de despacho, por ejemplo.

Martijn Pieters
fuente
55
@Martjin: ¿No podría llamarse a esto una implementación del 'Patrón de comando' en ese caso? ¿Parece que ese es el concepto que el OP está tratando de comprender?
Doctorado
3
@PhD: Sí, el ejemplo que construí es una implementación de Patrón de comando; los dictactos como el despachador (Gestor de mando, invocador, etc.).
Martijn Pieters
Gran explicación de nivel superior, @Martijn, gracias. Creo que tengo la idea de "despacho".
mdeutschmtl
28

@Martijn Pieters hizo un buen trabajo explicando la técnica, pero quería aclarar algo de su pregunta.

Lo importante es saber que NO está almacenando "el nombre de la función" en el diccionario. Está almacenando una referencia a la función en sí. Puede ver esto usando a printen la función.

>>> def f():
...   print 1
... 
>>> print f
<function f at 0xb721c1b4>

fes solo una variable que hace referencia a la función que definió. El uso de un diccionario le permite agrupar cosas similares, pero no es diferente de asignar una función a una variable diferente.

>>> a = f
>>> a
<function f at 0xb721c3ac>
>>> a()
1

Del mismo modo, puede pasar una función como argumento.

>>> def c(func):
...   func()
... 
>>> c(f)
1
muestreador
fuente
55
Mencionar la función de primera clase definitivamente ayudaría :-)
Florian Margaine
7

Tenga en cuenta que la clase Python es realmente un azúcar de sintaxis para el diccionario. Cuando tu lo hagas:

class Foo(object):
    def find_city(self, city):
        ...

cuando usted llama

f = Foo()
f.find_city('bar')

es realmente lo mismo que:

getattr(f, 'find_city')('bar')

que, después de la resolución del nombre, es lo mismo que:

f.__class__.__dict__['find_city'](f, 'bar')

Una técnica útil es para asignar la entrada del usuario a las devoluciones de llamada. Por ejemplo:

def cb1(...): 
    ...
funcs = {
    'cb1': cb1,
    ...
}
while True:
    input = raw_input()
    funcs[input]()

Alternativamente, esto se puede escribir en clase:

class Funcs(object):
    def cb1(self, a): 
        ...
funcs = Funcs()
while True:
    input = raw_input()
    getattr(funcs, input)()

la sintaxis de devolución de llamada que sea mejor depende de la aplicación particular y del gusto del programador. El primero es un estilo más funcional, el último está más orientado a objetos. El primero puede parecer más natural si necesita modificar las entradas en el diccionario de funciones dinámicamente (tal vez en función de la entrada del usuario); este último podría sentirse más natural si tiene un conjunto de asignaciones de preajustes diferentes que se pueden elegir dinámicamente.

Lie Ryan
fuente
Creo que esta intercambiabilidad es lo que me hizo pensar "pitón", el hecho de que lo que ves en la superficie es solo una forma convencional de presentar algo mucho más profundo. Tal vez eso no sea particular de Python, aunque los ejercicios de Python (¿y los programadores de Python?) Parecen hablar mucho sobre las características del lenguaje de esta manera.
mdeutschmtl
Otro pensamiento, ¿hay algo específico para Python en la forma en que está dispuesto a evaluar lo que parecen dos "términos" simplemente adyacentes, la referencia de dict y la lista de argumentos? ¿Otros idiomas lo permiten? Es una especie de programación equivalente al salto en álgebra de 5 * xa 5x(perdona la analogía simple).
mdeutschmtl
@mdeutschmtl: no es realmente exclusivo de Python, aunque los lenguajes que carecen de función de primera clase u objeto de función pueden nunca tener situaciones en las que un acceso al diccionario seguido inmediatamente por la llamada a la función podría ser posible.
Lie Ryan
2
@mdeutschmtl "el hecho de que lo que ves en la superficie es solo una forma convencional de presentar algo mucho más profundo". - eso se llama azúcar sintáctico y existe en todas partes
Izkata
6

Hay dos técnicas que me vienen a la mente a las que te puedes estar refiriendo, ninguna de las cuales es Pythonic, ya que son más amplias que un idioma.

1. La técnica de ocultación / encapsulación y cohesión de la información (generalmente van de la mano, así que las estoy agrupando).

Tiene un objeto que tiene datos y adjunta un método (comportamiento) que es altamente coherente con los datos. Si necesita cambiar la función, ampliar la funcionalidad o realizar cualquier otro cambio, las personas que llaman no necesitarán cambiar (suponiendo que no se necesiten datos adicionales).

2. Tablas de despacho

No es el caso clásico, porque solo hay una entrada con una función. Sin embargo, las tablas de despacho se utilizan para organizar diferentes comportamientos por una clave de modo que puedan buscar y llamar dinámicamente. No estoy seguro de si estás pensando en esto, ya que no te estás refiriendo a la función de manera dinámica, pero aún así obtienes un enlace tardío efectivo (la llamada "indirecta").

Compensaciones

Una cosa a tener en cuenta es que lo que hará funcionará bien con un espacio de nombres conocido de claves. Sin embargo, corre el riesgo de colisión entre los datos y las funciones con un espacio de nombres desconocido de claves.

dietbuddha
fuente
Encapsulación porque los datos y la función almacenados en el dict (ionario) están relacionados y, por lo tanto, tienen cohesión. Los datos y la función provienen de dos dominios principales muy diferentes, por lo que, a primera vista, el ejemplo parece agrupar entidades dispares.
ChuckCottrill
0

Publico esta solución que creo que es bastante genérica y puede ser útil, ya que se mantiene simple y fácil de adaptar a casos específicos:

def p(what):
    print 'playing', cmpsr

def l(what):
    print 'listening', cmpsr

actions = {'Play' : p, 'Listen' : l}

act = 'Listen'
cmpsr = 'Vivaldi'

actions[act].__call__(cmpsr)

También se puede definir una lista donde cada elemento es un objeto de función y utilizar el __call__método incorporado. Créditos a todos por la inspiración y la cooperación.

"El gran artista es el simplificador", Henri Frederic Amiel

Emanuele
fuente