Perché memorizzare una funzione all'interno di un dizionario Python?


68

Sono un principiante di Python e ho appena imparato una tecnica che coinvolge dizionari e funzioni. La sintassi è semplice e sembra una cosa banale, ma i miei sensi di pitone sono formicolanti. Qualcosa mi dice che questo è un concetto profondo e molto pitonico e non ne capisco l'importanza. Qualcuno può dare un nome a questa tecnica e spiegare come / perché è utile?


La tecnica è quando hai un dizionario Python e una funzione che intendi utilizzare su di esso. Inserire un elemento aggiuntivo nel dict, il cui valore è il nome della funzione. Quando sei pronto a chiamare la funzione, invii la chiamata indirettamente facendo riferimento all'elemento dict, non alla funzione per nome.

L'esempio dal quale sto lavorando è tratto da Learn Python the Hard Way, 2nd Ed. (Questa è la versione disponibile quando ti iscrivi tramite Udemy.com ; purtroppo la versione HTML gratuita dal vivo è attualmente Ed 3, e non include più questo esempio).

Per parafrasare:

# 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

Quindi le seguenti espressioni sono equivalenti. È possibile chiamare la funzione direttamente o facendo riferimento all'elemento dict il cui valore è la funzione.

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

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

Qualcuno può spiegare che lingua è questa caratteristica e forse dove si gioca nella programmazione "reale"? Questo esercizio giocattolo è stato sufficiente per insegnarmi la sintassi, ma non mi ha portato fino in fondo.


13
Perché questo post dovrebbe essere fuori tema? È una grande domanda di algoritmo e struttura dei dati!
Martijn Pieters,

Ho visto (e fatto) alcune cose come questa in altre lingue. Potresti guardarlo come un'istruzione switch, ma avvolto bene in un oggetto passabile con tempo di ricerca O (1).
KChaloux,

1
Ho avuto la sensazione che ci fosse qualcosa di importante e autoreferenziale nell'includere la funzione all'interno del suo stesso dict ... vedi la risposta di @ dietbuddha ... ma forse no?
mdeutschmtl,

Risposte:


83

Usando un dict, traduciamo la chiave in un callable. Tuttavia, non è necessario codificare con cura la chiave, come nel tuo esempio.

Di solito, questa è una forma di invio del chiamante, in cui si utilizza il valore di una variabile per connettersi a una funzione. Supponiamo che un processo di rete ti invii codici di comando, una mappatura di invio ti consente di tradurre facilmente i codici di comando in codice eseguibile:

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

Nota che la funzione che chiamiamo ora dipende interamente da quale sia il valore command. Neanche la chiave deve corrispondere; non deve nemmeno essere una stringa, è possibile utilizzare tutto ciò che può essere utilizzato come chiave e si adatta alla propria applicazione specifica.

L'uso di un metodo di invio è più sicuro di altre tecniche, come eval(), in quanto limita i comandi consentiti a ciò che hai definito in precedenza. Nessun aggressore sta per intrufolarsi in ls)"; DROP TABLE Students; --un'iniezione oltre una tabella di invio, per esempio.


5
@Martjin - In questo caso non si potrebbe definire un'implementazione del "modello di comando"? Sembra che questo sia il concetto che l'OP sta cercando di comprendere?
Dottorato di ricerca

3
@PhD: Sì, l'esempio che ho creato è un'implementazione di Command Pattern; i dictfunge da dispatcher (Command Manager, invoker, ecc).
Martijn Pieters,

Ottima spiegazione di alto livello, @Martijn, grazie. Penso di avere l'idea di "spedizione".
mdeutschmtl,

28

@Martijn Pieters ha fatto un buon lavoro spiegando la tecnica, ma volevo chiarire qualcosa dalla tua domanda.

La cosa importante da sapere è che NON stai memorizzando "il nome della funzione" nel dizionario. Stai memorizzando un riferimento alla funzione stessa. Puoi vederlo usando a printsulla funzione.

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

fè solo una variabile che fa riferimento alla funzione che hai definito. L'uso di un dizionario consente di raggruppare cose simili, ma non è diverso dall'assegnare una funzione a una variabile diversa.

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

Allo stesso modo, puoi passare una funzione come argomento.

>>> def c(func):
...   func()
... 
>>> c(f)
1

5
Citare una funzione di prima classe sarebbe sicuramente d'aiuto :-)
Florian Margaine,

7

Nota che la classe Python è davvero solo uno zucchero sintattico per il dizionario. Quando lo fai:

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

quando chiami

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

è davvero lo stesso di:

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

che, dopo la risoluzione dei nomi, è uguale a:

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

Una tecnica utile è quella di mappare l'input dell'utente ai callback. Per esempio:

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

Questo può in alternativa essere scritto in classe:

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

Qualunque sia la sintassi di callback migliore dipende dalla particolare applicazione e dal gusto del programmatore. Il primo è uno stile più funzionale, il secondo è più orientato agli oggetti. Il primo potrebbe sembrare più naturale se è necessario modificare dinamicamente le voci nel dizionario delle funzioni (forse in base all'input dell'utente); quest'ultimo potrebbe sembrare più naturale se si dispone di una serie di mappature predefinite diverse che possono essere scelte in modo dinamico.


Penso che questa intercambiabilità sia ciò che mi ha fatto pensare "pitonico", il fatto che ciò che vedi in superficie è solo un modo convenzionale di presentare qualcosa di molto più profondo. Forse questo non è particolare di Python, anche se gli esercizi di Python (e i programmatori di Python?) Sembrano parlare molto delle caratteristiche del linguaggio in questo modo.
mdeutschmtl,

Un altro pensiero, c'è qualcosa di specifico in Python nel modo in cui è disposto a valutare ciò che assomiglia a due "termini" che si trovano uno accanto all'altro, il riferimento al dict e l'elenco degli argomenti? Altre lingue lo consentono? È una specie di equivalente di programmazione del salto in algebra da 5 * xa 5x(perdona la semplice analogia).
mdeutschmtl,

@mdeutschmtl: non è davvero univoco per Python, sebbene i linguaggi privi di funzione o oggetto di prima classe non possano mai avere situazioni in cui un accesso al dizionario immediatamente seguito da una chiamata di funzione potrebbe essere possibile.
Sdraiati Ryan l'

2
@mdeutschmtl "il fatto che ciò che vedi in superficie è solo un modo convenzionale di presentare qualcosa di molto più profondo." - Si chiama zucchero sintattico ed esiste
ovunque

6

Ci sono 2 tecniche che mi saltano in mente a cui potresti riferirti, nessuna delle quali è Pythonic in quanto sono più ampie di una lingua.

1. La tecnica di occultamento / incapsulamento e coesione delle informazioni (di solito vanno di pari passo quindi le sto raggruppando insieme).

Hai un oggetto che ha dati e ti allega un metodo (comportamento) che è altamente coerente con i dati. Se è necessario modificare la funzione, estendere la funzionalità o apportare altre modifiche, i chiamanti non dovranno cambiare (supponendo che non debbano essere trasferiti dati aggiuntivi).

2. Tabelle di invio

Non è il caso classico, perché esiste una sola voce con una funzione. Tuttavia, le tabelle di invio vengono utilizzate per organizzare comportamenti diversi mediante una chiave in modo tale che possano essere cercati e chiamati dinamicamente. Non sono sicuro che tu stia pensando a questo dato che non ti riferisci alla funzione in modo dinamico, ma comunque ottieni comunque un efficace late binding (la chiamata "indiretta").

compromessi

Una cosa da notare è che cosa stai facendo funzionerà bene con uno spazio dei nomi noto di chiavi. Tuttavia, si corre il rischio di collisione tra i dati e le funzioni con uno spazio dei nomi sconosciuto delle chiavi.


Incapsulamento perché i dati e la funzione memorizzati nel dict (ionico) sono correlati e quindi hanno coesione. I dati e la funzione provengono da due domini molto diversi, quindi a prima vista l'esempio sembra raggruppare entità selvaggiamente disparate.
ChuckCottrill,

0

Pubblico questa soluzione che ritengo piuttosto generica e che può essere utile in quanto è semplice e facile da adattare a casi specifici:

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)

si può anche definire un elenco in cui ogni elemento è un oggetto funzione e usare il __call__metodo incorporato. Ringraziamo tutti per l'ispirazione e la collaborazione.

"Il grande artista è il semplificatore", Henri Frederic Amiel

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.