2015-12-16 13 views
11

Quindi sono curioso di vedere le visualizzazioni di programmatori Python più esperti sulla seguente domanda di stile. Supponiamo che io stia costruendo una funzione che sta andando a scorrere una riga dopo l'altra attraverso un dataframe pandas o un altro caso simile in cui una funzione richiede l'accesso al suo stato precedente. Sembra che ci siano almeno quattro modi per implementare questa in python:Idiomi in python: chiusura vs functor vs oggetto

1) Chiusure:

def outer(): 
    previous_state = None 
    def inner(current_state) : 
     nonlocal previous_state 
     #do something 
     previous_state=current_state 
     return something 

Quindi, se si proviene da un background javascript questo sarà senza dubbio sembrare naturale per te. Sembra davvero naturale anche in python, fino a quando non avrai bisogno di accedere allo scope che racchiude, quando finirai per fare qualcosa come inner.__code__.co_freevars, che ti darà i nomi delle variabili che racchiudono come una tupla, e trovare l'indice di quello vuoi, e poi andare a inner.__closure__[index].cell_contents per ottenere il suo valore. Non proprio elegante, ma suppongo che il punto sia spesso nascondere l'ambito, quindi ha senso che sia difficile da raggiungere. D'altra parte, è anche un po 'strano che Python renda privata la funzione di inclusione quando ha eliminato quasi ogni altro modo di avere una variabile privata rispetto alle lingue OOP.

2) Functor

def outer(): 
    def inner(current_state): 
     #do something 
     inner.previous_state=current_state 
     return something 
    ret = inner 
    ret.previous_state=None 
    return ret 

Questa "apre la chiusura" a che ora lo stato racchiude è completamente visibile come attributo della funzione. Funziona perché le funzioni sono in realtà solo oggetti nascosti. Mi sto appoggiando a questo come il più pignolo. È chiaro, conciso e leggibile.

3) Oggetti Questo è probabilmente il più familiare OOP programmatori

class Calculator(Object) : 
    def __init__(self): 
     self.previous_state=None 

    def do_something(self, current_state) : 
     #do_something 
     self.previous_state = current_state 
     return something 

Il più grande con qui è che si tende a finire con un sacco di definizioni di classi. Ciò va bene in un linguaggio OOP completo come Java, in cui si hanno interfacce e simili per gestirlo, ma sembra un po 'strano in un linguaggio a forma di deambulatore avere molte semplici classi solo per portare in giro una funzione che ha bisogno di un po' di stato.

4) globali - non voglio dimostrare questo, come ho espressamente voglio evitare di inquinare lo spazio dei nomi globale

5) Decoratori - questo è un po 'una palla curva, ma è possibile utilizzare decoratori per memorizzare parziale informazioni di stato.

@outer 
def inner(previous_state, current_state): 
    #do something 
    return something 

def outer(inner) : 
    def wrapper(current_state) : 
     result = inner(wrapper.previous_state, current_state) 
     wrapper.previous_state = current_state 
     return result 
    ret = wrapper 
    ret.previous_state=None 
    return result 

Questo tipo di sintassi è la meno familiare per me, ma se ora mi chiamo

func = inner 

ho effettivamente ottenere

func = outer(inner) 

e poi ripetutamente chiamando func() atti, proprio come il funtore esempio. In realtà odio in questo modo di più. Mi sembra di avere una sintassi davvero non trasparente in quanto non è chiaro se chiamare internamente (current_state) un sacco di volte ti darà lo stesso risultato o se ti darà una funzione appena decorata ogni volta, quindi sembra una cattiva pratica per fare decoratori che aggiungono lo stato a una funzione in questo modo.

Quindi qual è il modo corretto? Che pro e contro ho perso qui?

+1

Non sto seguendo il motivo per cui pensi di aver bisogno di accedere alle variabili chiuse tramite 'inner .__ closure__'; i nomi terminano solo in quella struttura perché li stai utilizzando attivamente nella funzione interna * già *. La struttura '__closure__' è un dettaglio di implementazione interno, davvero. –

+0

Farò in modo che le persone lo sviluppino, ma per esperienza, gli sviluppatori di Python tendono a usare la classe per questo tipo di scenario. O decoratori quando non hai bisogno di piena chiarezza e preferisci la bella sintassi con '@'. Non vuoi usare la parola chiave 'nonlocal' o' global'. E il functor è spesso lasciato appart per la chiusura. – Cyrbil

+0

I nomi di chiusure non sono diversi da quelli locali a questo riguardo; di solito non entri in una funzione per leggere la gente del posto, perché stai facendo la stessa cosa con le chiusure? Se è necessario esporre tale stato per l'uso al di fuori della funzione, utilizzare l'approccio functor o class. –

risposta

3

Quindi la risposta corretta a questo è l'oggetto chiamabile, che essenzialmente sostituisce l'idioma della chiusura in python.

in modo da lavorare fuori l'opzione 3 di cui sopra cambiamento:

class Calculator(Object) : 
    def __init__(self): 
     self.previous_state=None 

    def do_something(self, current_state) : 
     #do_something 
     self.previous_state = current_state 
     return something 

a

class Calculator(Object) : 
    def __init__(self): 
     self.previous_state=None 

    def __call__(self, current_state) : 
     #do_something 
     self.previous_state = current_state 
     return something 

e ora si può chiamare come una funzione. Quindi

func = Calculator(): 
for x in list: 
    func(x) 
2

È possibile definire un generatore, che è una forma limitata di una coprocessa.

def make_gen(): 
    previous_state = None 
    for row in rows: 
     # do something 
     previous_state = current_state 
     yield something 

thing = make_gen() 
for item in thing: 
    # Each iteration, item is a different value 
    # "returned" by the yield statement in the generator 

Invece di chiamare thing (che sostituisce la funzione interna) più volte, di eseguire iterazioni su di esso (che è sostanzialmente lo stesso di chiamare next(thing) più volte).

Lo stato è interamente contenuto nel corpo del generatore.

Se non si desidera eseguirne effettivamente l'iterazione, è comunque possibile "reinserire" selettivamente la coprocessione chiamando esplicitamente next.

thing = make_gen() 
first_item = next(thing) 
# do some stuff 
second_item = next(thing) 
# do more stuff 
third_item = next(thing) 
fourth_item = next(thing) 
# etc 
+0

Non è chiaro dalla domanda esattamente come questo sarebbe usato nel codice dell'OP. Ho il sospetto che le righe dal frame dei dati sarebbero passate come argomento a 'make_gen' (al momento non ho argomenti, ma la variabile libera' rows' nel ciclo è pensata per essere dal dataframe). – chepner