Pourquoi stocker une fonction dans un dictionnaire python?

69

Je suis un débutant en python et je viens d’apprendre une technique impliquant des dictionnaires et des fonctions. La syntaxe est simple et cela semble être une chose triviale, mais mes sens python sont picotements. Quelque chose me dit que c'est un concept profond et très pythonique et je ne comprends pas tout à fait son importance. Quelqu'un peut-il nommer cette technique et expliquer comment / pourquoi elle est utile?


La technique consiste à utiliser un dictionnaire python et une fonction à utiliser. Vous insérez un élément supplémentaire dans le dict, dont la valeur est le nom de la fonction. Lorsque vous êtes prêt à appeler la fonction, vous émettez l'appel indirectement en vous référant à l'élément dict, pas à la fonction par son nom.

L'exemple sur lequel je travaille est tiré de Learn Python The Hard Way, 2e éd. (C'est la version disponible lorsque vous vous inscrivez via Udemy.com ; malheureusement, la version HTML gratuite en direct est actuellement Ed 3 et n'inclut plus cet exemple).

Paraphraser:

# 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

Alors les expressions suivantes sont équivalentes. Vous pouvez appeler la fonction directement ou en référençant l'élément dict dont la valeur est la fonction.

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

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

Quelqu'un peut-il expliquer de quelle langue il s'agit, et peut-être de la "vraie" programmation? Cet exercice de jouet était suffisant pour m'apprendre la syntaxe, mais ne m'a pas pris tout le chemin.

mdeutschmtl
la source
13
Pourquoi ce message serait-il hors sujet? C'est une excellente question d'algorithme et de concept de structure de données!
Martijn Pieters
J'ai vu (et fait) des trucs comme celui-ci dans d'autres langues. Vous pouvez en quelque sorte la considérer comme une instruction switch, mais bien encapsulée dans un objet passable avec un temps de recherche O (1).
KChaloux
1
J'avais l'intuition qu'il y avait quelque chose d'important et d'auto-référentiel sur l'inclusion de la fonction dans son propre dict ... voir la réponse de @ dietbuddha ... mais peut-être pas?
mdeutschmtl

Réponses:

83

Utiliser un dict vous permet de traduire la clé en appelable. La clé n'a pas besoin d'être codée en dur, comme dans votre exemple.

Généralement, il s'agit d'une forme de répartition de l'appelant, dans laquelle vous utilisez la valeur d'une variable pour vous connecter à une fonction. Supposons qu'un processus réseau vous envoie des codes de commande, un mappage de répartition vous permet de convertir facilement les codes de commande en code exécutable:

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

Notez que la fonction que nous appelons maintenant dépend entièrement de la valeur de command. La clé ne doit pas nécessairement correspondre non plus; il n'est même pas nécessaire que ce soit une chaîne, vous pouvez utiliser tout ce qui peut être utilisé comme clé et convient à votre application spécifique.

L'utilisation d'une méthode de répartition est plus sûre que d'autres techniques, telles que eval(), car elle limite les commandes autorisées à ce que vous avez défini auparavant. Par exemple, aucun attaquant ne va se faufiler ls)"; DROP TABLE Students; --devant une table de répartition.

Martijn Pieters
la source
5
@Martjin - Ne pourrait-on pas appeler cela une implémentation du 'modèle de commande' dans ce cas? On dirait que c'est le concept que le PO essaie de saisir?
PhD
3
@PhD: Ouais, l'exemple que j'ai construit est une implémentation de modèle de commande; le dictagit en tant que répartiteur (gestionnaire de commandes, invocateur, etc.).
Martijn Pieters
Excellente explication de haut niveau, @Martijn, merci. Je pense que j'ai eu l'idée du "dispatch".
mdeutschmtl
28

@ Martijn Pieters a bien expliqué la technique, mais je voulais clarifier quelque chose de votre question.

La chose importante à savoir est que vous ne stockez PAS "le nom de la fonction" dans le dictionnaire. Vous stockez une référence à la fonction elle-même. Vous pouvez voir cela en utilisant un printsur la fonction.

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

fest juste une variable qui référence la fonction que vous avez définie. L'utilisation d'un dictionnaire vous permet de regrouper des éléments similaires, mais ce n'est pas différent d'affecter une fonction à une variable différente.

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

De même, vous pouvez passer une fonction en argument.

>>> def c(func):
...   func()
... 
>>> c(f)
1
unholysampler
la source
5
Mentionner une fonction de première classe serait certainement utile :-)
Florian Margaine
7

Notez que la classe Python n'est en réalité qu'un sucre de syntaxe pour dictionnaire. Quand tu fais:

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

quand vous appelez

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

est vraiment la même chose que:

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

qui, après résolution du nom, est identique à:

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

Une technique utile consiste à mapper une entrée utilisateur sur des rappels. Par exemple:

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

Cela peut alternativement être écrit en classe:

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

la meilleure syntaxe de rappel dépend de l'application et du goût du programmeur. Le premier est un style plus fonctionnel, le dernier est plus orienté objet. Le premier peut sembler plus naturel si vous devez modifier les entrées du dictionnaire de fonctions de manière dynamique (éventuellement en fonction des entrées de l'utilisateur); ce dernier peut sembler plus naturel si vous avez un ensemble de mappages de préréglages différents qui peuvent être choisis de manière dynamique.

Lie Ryan
la source
Je pense que cette interchangeabilité est ce qui m'a fait penser "pythonique", le fait que ce que vous voyez à la surface n'est qu'un moyen conventionnel de présenter quelque chose de beaucoup plus profond. Peut-être que cela n’est pas propre à Python, bien que les exercices sur Python (et les programmeurs Python?) Semblent parler énormément des fonctionnalités du langage de cette façon.
mdeutschmtl
Une autre pensée, y a-t-il quelque chose de spécifique à python dans la façon dont il est disposé à évaluer ce qui ressemble à deux "termes" placés côte à côte, la référence dict et la liste des arguments? Est-ce que d'autres langues le permettent? C'est en quelque sorte l'équivalent en programmation du saut en algèbre de 5 * xà 5x(pardonnez l'analogie simple).
mdeutschmtl
@mdeutschmtl: ce n'est pas vraiment unique en Python, bien que les langages dépourvus de fonction de première classe ou d'objet de fonction ne présentent jamais de situation dans laquelle un accès au dictionnaire immédiatement suivi d'un appel de fonction est possible.
Lie Ryan
2
@mdeutschmtl "le fait que ce que vous voyez à la surface n'est qu'un moyen conventionnel de présenter quelque chose de beaucoup plus profond." - Cela s'appelle le sucre syntaxique et existe partout
Izkata
6

Il se peut que vous parliez de deux techniques qui me sautent à l’esprit, aucune des deux n'étant pythonique en ce sens qu’elles sont plus larges qu’un seul langage.

1. La technique de la dissimulation / encapsulation de l’information et de la cohésion (elles vont généralement de pair, alors je les associe).

Vous avez un objet qui a des données et vous attachez une méthode (comportement) qui est très cohérente avec les données. Si vous devez modifier la fonction, étendre la fonctionnalité ou apporter toute autre modification que les appelants n'auront pas à modifier (en supposant qu'aucune donnée supplémentaire ne doit être transmise).

2. Tableaux d'expédition

Pas le cas classique, car il n'y a qu'une seule entrée avec une fonction. Cependant, les tables de répartition sont utilisées pour organiser différents comportements à l'aide d'une clé, de sorte qu'elles peuvent être recherchées et appelées de manière dynamique. Je ne suis pas sûr que vous pensiez à cela, puisque vous ne faites pas référence à la fonction de manière dynamique, mais vous obtenez néanmoins une liaison tardive effective (appel "indirect").

Compromis

Une chose à noter est ce que vous ferez fonctionnera bien avec un espace de noms connu de clés. Cependant, vous courez le risque de collision entre les données et les fonctions avec un espace de noms de clés inconnu.

dietbuddha
la source
Encapsulation car les données et la fonction stockées dans le dict (ionaire) sont liées et ont donc une cohésion. Les données et la fonction proviennent de deux dommains très différents, ainsi, à première vue, cet exemple semble regrouper des entités extrêmement disparates.
ChuckCottrill
0

Je publie cette solution qui, à mon avis, est assez générique et peut être utile car elle reste simple et facile à adapter à des cas spécifiques:

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)

on peut aussi définir une liste où chaque élément est un objet fonction et utiliser la __call__méthode intégrée. Crédits à tous pour l'inspiration et la coopération.

"Le grand artiste est le simplificateur", Henri Frederic Amiel

Emanuele
la source