Corrispondenza di modelli di liste in Python
-
04-07-2019 - |
Domanda
Voglio fare alcuni pattern matching sugli elenchi in Python. Ad esempio, in Haskell, posso fare qualcosa del tipo:
fun (head : rest) = ...
Quindi quando passo in un elenco, head
sarà il primo elemento e rest
saranno gli elementi finali.
Allo stesso modo, in Python, posso decomprimere automaticamente le tuple:
(var1, var2) = func_that_returns_a_tuple()
Voglio fare qualcosa di simile con le liste in Python. In questo momento, ho una funzione che restituisce un elenco e un pezzo di codice che procede come segue:
ls = my_func()
(head, rest) = (ls[0], ls[1:])
Mi chiedevo se potevo in qualche modo farlo in una riga in Python, anziché in due.
Soluzione
Per quanto ne so, non c'è modo di renderlo un singolo elemento nell'attuale Python senza introdurre un'altra funzione, ad esempio:
split_list = lambda lst: (lst[0], lst[1:])
head, rest = split_list(my_func())
Tuttavia, in Python 3.0 la sintassi specializzata utilizzata per le firme degli argomenti variadici e la decompressione degli argomenti diventerà disponibile anche per questo tipo di decompressione della sequenza generale, quindi in 3.0 sarai in grado di scrivere:
head, *rest = my_func()
Vedi PEP 3132 per dettagli.
Altri suggerimenti
Prima di tutto, tieni presente che la corrispondenza del modello " " dei linguaggi funzionali e l'assegnazione alle tuple che menzioni non sono poi così simili. Nei linguaggi funzionali gli schemi sono usati per dare definizioni parziali di una funzione. Quindi f (x: s) = e
non significa prendere la testa e la coda dell'argomento di f
e restituire e
usandoli, ma significa che if l'argomento di f
ha la forma x: s
(per alcuni x
e s
), quindi f (x: s)
è uguale a e
.
L'assegnazione di Python è più simile a un'assegnazione multipla (sospetto che fosse la sua intenzione originale). Quindi scrivi, ad esempio, x, y = y, x
per scambiare i valori in x
e y
senza bisogno di una variabile temporanea (come lo faresti con una semplice dichiarazione di incarico). Questo ha poco a che fare con la corrispondenza dei modelli in quanto è sostanzialmente una scorciatoia per la "simultanea" esecuzione di x = y
e y = x
. Sebbene Python consenta sequenze arbitrarie anziché elenchi separati da virgole, non suggerirei di chiamare questo pattern matching. Con la corrispondenza dei motivi controlli se qualcosa corrisponde o meno a un motivo; nell'assegnazione python dovresti assicurarti che le sequenze su entrambi i lati siano uguali.
Per fare ciò che sembri desiderare di solito (anche nei linguaggi funzionali) utilizzare una funzione ausiliaria (come menzionato da altri) o qualcosa di simile a let
o dove
costrutti (che puoi considerare come usare funzioni anonime). Ad esempio:
(head, tail) = (x[0], x[1:]) where x = my_func()
Oppure, in vero pitone:
(head, tail) = (lambda x: (x[0], x[1:]))(my_func())
Nota che questo è essenzialmente lo stesso delle soluzioni fornite da altri con una funzione ausiliaria tranne per il fatto che questa è la linea che volevi. Tuttavia, non è necessariamente migliore di una funzione separata.
(Mi dispiace se la mia risposta è un po 'esagerata. Penso solo che sia importante chiarire la distinzione.)
Questo è un approccio "puro funzionale" e come tale è un linguaggio sensato in Haskell, ma probabilmente non è così appropriato per Python. Python ha solo un concetto molto limitato di pattern in questo modo - e sospetto che potresti bisogno di un sistema di tipo un po 'più rigido per implementare quel tipo di costrutto ( erlang gli appassionati invitati a non essere d'accordo qui).
Quello che hai è probabilmente il più vicino possibile a quel linguaggio, ma probabilmente stai meglio usando una comprensione dell'elenco o un approccio imperativo piuttosto che chiamare ricorsivamente una funzione con la coda dell'elenco.
Come è stato dichiarato in alcune occasioni prima , Python non è in realtà un linguaggio funzionale. Prende semplicemente in prestito idee dal mondo FP. Non è intrinsecamente Tail Recursive nel modo in cui ti aspetteresti di vedere incorporato nell'architettura di un linguaggio funzionale, quindi avresti qualche difficoltà a fare questo tipo di operazione ricorsiva su un set di dati di grandi dimensioni senza usare molto spazio nello stack.
la decompressione estesa è stata introdotta in 3.0 http://www.python.org/dev/peps/pep-3132/
A differenza di Haskell o ML, Python non ha un pattern-matching incorporato delle strutture. Il modo più Pythonic di fare il pattern matching è con un blocco try-tranne:
def recursive_sum(x):
try:
head, tail = x[0], x[1:]
return head + recursive-sum(tail)
except IndexError: # empty list: [][0] raises IndexError
return 0
Nota che funziona solo con oggetti con indicizzazione delle sezioni. Inoltre, se la funzione diventa complicata, qualcosa nel corpo dopo la riga testa, coda
potrebbe sollevare IndexError, il che porterà a bug sottili. Tuttavia, questo ti consente di fare cose come:
for frob in eggs.frob_list:
try:
frob.spam += 1
except AttributeError:
eggs.no_spam_count += 1
In Python, la ricorsione della coda è generalmente meglio implementata come un ciclo con un accumulatore, ovvero:
def iterative_sum(x):
ret_val = 0
for i in x:
ret_val += i
return ret_val
Questo è l'unico modo ovvio e giusto per farlo il 99% delle volte. Non solo è più chiaro da leggere, è più veloce e funzionerà su cose diverse dalle liste (set, per esempio). Se c'è un'eccezione in attesa che accada, la funzione fallirà felicemente e la consegnerà alla catena.
Sto lavorando su pyfpm , una libreria per la corrispondenza dei pattern in Python con una sintassi simile a Scala . Puoi usarlo per decomprimere oggetti in questo modo:
from pyfpm import Unpacker
unpacker = Unpacker()
unpacker('head :: tail') << (1, 2, 3)
unpacker.head # 1
unpacker.tail # (2, 3)
O nell'arlista di una funzione:
from pyfpm import match_args
@match_args('head :: tail')
def f(head, tail):
return (head, tail)
f(1) # (1, ())
f(1, 2, 3, 4) # (1, (2, 3, 4))
Beh, perché lo vuoi in 1 riga in primo luogo?
Se vuoi davvero, puoi sempre fare un trucco come questo:
def x(func):
y = func()
return y[0], y[1:]
# then, instead of calling my_func() call x(my_func)
(head, rest) = x(my_func) # that's one line :)
Oltre alle altre risposte, si noti che l'operazione testa / coda equivalente in Python, inclusa l'estensione della sintassi * di python3, sarà generalmente meno efficiente della corrispondenza del modello di Haskell.
Gli elenchi di Python sono implementati come vettori, quindi per ottenere la coda dovrai prendere una copia dell'elenco. Questo è O (n) wrt la dimensione della lista, mentre un implementaion che usa liste collegate come Haskell può semplicemente usare il puntatore di coda, un'operazione O (1).
L'unica eccezione potrebbe essere rappresentata dagli approcci basati sull'iteratore, in cui l'elenco non viene effettivamente restituito, ma è un iteratore. Tuttavia, ciò potrebbe non essere applicabile in tutti i luoghi in cui si desidera un elenco (ad es. Ripetendo più volte).
Ad esempio, l'approccio Cipher , se modificato per restituire il metodo iteratore anziché convertirlo in una tupla avrà questo comportamento. In alternativa, un metodo a 2 elementi più semplice che non si basa sul bytecode sarebbe:
def head_tail(lst):
it = iter(list)
yield it.next()
yield it
>>> a, tail = head_tail([1,2,3,4,5])
>>> b, tail = head_tail(tail)
>>> a,b,tail
(1, 2, <listiterator object at 0x2b1c810>)
>>> list(tail)
[3, 4]
Ovviamente devi comunque avvolgere una funzione di utilità piuttosto che esserci un buon zucchero sintattico per essa.
c'era una ricevuta nel ricettario di Python per farlo. non riesco a trovarlo ora ma ecco il codice (l'ho modificato leggermente)
def peel(iterable,result=tuple):
'''Removes the requested items from the iterable and stores the remaining in a tuple
>>> x,y,z=peel('test')
>>> print repr(x),repr(y),z
't' 'e' ('s', 't')
'''
def how_many_unpacked():
import inspect,opcode
f = inspect.currentframe().f_back.f_back
if ord(f.f_code.co_code[f.f_lasti])==opcode.opmap['UNPACK_SEQUENCE']:
return ord(f.f_code.co_code[f.f_lasti+1])
raise ValueError("Must be a generator on RHS of a multiple assignment!!")
iterator=iter(iterable)
hasItems=True
amountToUnpack=how_many_unpacked()-1
next=None
for num in xrange(amountToUnpack):
if hasItems:
try:
next = iterator.next()
except StopIteration:
next = None
hasItems = False
yield next
if hasItems:
yield result(iterator)
else:
yield None
tuttavia dovresti notare che funziona solo quando usi un compito unpack a causa del modo in cui integra il frame precedente ... è comunque abbastanza utile.
Per il tuo caso d'uso specifico: emulare il divertimento di Haskell (head: rest) = ...
, certo. Le definizioni delle funzioni supportano la decompressione dei parametri da un bel po 'di tempo:
def my_method(head, *rest):
# ...
A partire da Python 3.0, come @bpowah menzionato , Python supporta anche il disimballaggio durante l'assegnazione:
my_list = ['alpha', 'bravo', 'charlie', 'delta', 'echo']
head, *rest = my_list
assert head == 'alpha'
assert rest == ['bravo', 'charlie', 'delta', 'echo']
Nota che l'asterisco (il simbolo "splat") significa "il resto dell'iterabile", non "fino alla fine". Il seguente funziona bene:
first, *middle, last = my_list
assert first == 'alpha'
assert last == 'echo'
assert middle == ['bravo', 'charlie', 'delta']
first, *middle, last = ['alpha', 'bravo']
assert first == 'alpha'
assert last == 'bravo'
assert middle == []