Paarweise kreisförmige Python 'for'-Schleife

76

Gibt es eine gute pythonische Möglichkeit, eine Liste zu durchlaufen und zwei Elemente neu abzustimmen? Das letzte Element sollte mit dem ersten gepaart werden.

Wenn ich zum Beispiel die Liste [1, 2, 3] habe, möchte ich die folgenden Paare erhalten:

  • 1 - 2
  • 2 - 3
  • 3 - 1
Das Oddler
quelle
@ BhargavRao Sollte wahrscheinlich diesen als Betrug dieses schließen
Barry
@ Barry Die Antworten hier sind einfach toll, daher habe ich nicht gehämmert. Ich brauchte jemanden, der mir versicherte, dass der umgekehrte Betrug besser ist. Danke (und ich habe dort auch einen Kommentar hinzugefügt)
Bhargav Rao

Antworten:

112

Eine pythonische Möglichkeit, paarweise auf eine Liste zuzugreifen, ist : zip(L, L[1:]). So verbinden Sie das letzte Element mit dem ersten:

>>> L = [1, 2, 3]
>>> zip(L, L[1:] + L[:1])
[(1, 2), (2, 3), (3, 1)]
jfs
quelle
32
Wenn die Liste riesig ist, kann dies nicht ratsam sein
Aaron Hall
10
@ AaronHall richtig. Aber denken Sie daran: "Vorzeitige Optimierung ist die Wurzel allen Übels" . Wenn der Speicher ein Problem darstellt oder beliebige iterable Elemente (nicht nur Sequenzen) unterstützen soll, itertoolskann eine Lösung verwendet werden.
JFS
1
Oh, ich sehe, dass Sie auf eine andere Antwort verlinken. Leider ist die Antwort, auf die Sie verlinken, nicht auf mehr als 2 Elemente in der Breite erweiterbar. Ich habe jedoch die direkten itertools analog zu Ihrem Vorschlag unten.
Aaron Hall
1
@AaronHall Wäre es in Python 3 gegenüber 2.x immer noch problematisch? 3's zip(unter anderem) ist verdammt effizient.
Selten 'Wo ist Monica' bedürftig
5
@SeldomNeedy Python 2 zip(*iterables)ist wie 3 list(zip(*iterables))- weshalb ich izip as zipin meiner Antwort den Import für Python 2 vorschlage - stackoverflow.com/a/36918720/541136 - JF geht zipin dieser Antwort tatsächlich von Python 2 aus , was das Problem der unnötigen Erstellung langer Listen verschärft in Erinnerung. Die Antwort von JF verwendet 200% mehr Speicher als für Listen erforderlich (8 Bytes pro Element) sowie den Speicher für jedes Tupel (relativ enorm mit 64 Bytes pro Tupel, siehe stackoverflow.com/a/30316760/541136 ). 800% mehr. also 1000% mehr insgesamt (unter der Annahme einer faulen Bewertung)
Aaron Hall
49

Ich würde ein dequemit verwenden zip, um dies zu erreichen.

>>> from collections import deque
>>>
>>> l = [1,2,3]
>>> d = deque(l)
>>> d.rotate(-1)
>>> zip(l, d)
[(1, 2), (2, 3), (3, 1)]
gddc
quelle
Die Deque dreht sich sehr schnell, ist aber auch ziemlich speicherlastig.
Aaron Hall
1
Sicher. Bei kleinen Lösungen ist dies schnell und sauber. Es zeigt auch, deque.rotatewas äußerst nützlich sein kann.
GDDC
45

Ich würde eine geringfügige Änderung des pairwiseRezepts aus der itertoolsDokumentation verwenden :

def pairwise_circle(iterable):
    "s -> (s0,s1), (s1,s2), (s2, s3), ... (s<last>,s0)"
    a, b = itertools.tee(iterable)
    first_value = next(b, None)
    return itertools.zip_longest(a, b,fillvalue=first_value)

Dadurch wird einfach ein Verweis auf den ersten Wert beibehalten, und wenn der zweite Iterator erschöpft ist, zip_longestwird die letzte Stelle mit dem ersten Wert gefüllt.

(Beachten Sie auch, dass es sowohl mit Iteratoren wie Generatoren als auch mit Iterablen wie Listen / Tupeln funktioniert.)

Beachten Sie, dass die Lösung von @ Barry dieser sehr ähnlich ist, aber meiner Meinung nach etwas leichter zu verstehen und über ein Element hinaus zu erweitern ist.

Tadhg McDonald-Jensen
quelle
@GrijeshChauhan hast du @ Barrys Lösung gesehen ?
Tadhg McDonald-Jensen
Die Logik kann nicht auf mehr als ein zweites Element erweitert werden.
Aaron Hall
@ AaronHall, Wenn ich einen Grund hätte, es weiter auszudehnen, würde ich es in eine einwickeln cycle.
Tadhg McDonald-Jensen
@ TadhgMcDonald-Jensen Gerade überprüft - schön, noch einen Trick zu lernen. Aber immer noch ist für mich itertools.zip_longest(a, b, fillvalue=first_value) dann offensichtlicher izip(a, chain(b, (first,))) - keine Notwendigkeit, zusätzliches Tupel zu machen(first, )
Grijesh Chauhan
38

Ich würde mich paaren itertools.cyclemit zip:

import itertools

def circular_pairwise(l):
    second = itertools.cycle(l)
    next(second)
    return zip(l, second)

cycle Gibt eine Iterable zurück, die die Werte ihres Arguments der Reihe nach liefert und vom letzten zum ersten Wert durchläuft.

Wir überspringen den ersten Wert, also beginnt er an der Position 1(anstatt 0).

Als nächstes machen wir zipes mit der ursprünglichen, nicht mutierten Liste. zipist gut, weil es aufhört, wenn eines seiner iterierbaren Argumente erschöpft ist.

Auf diese Weise wird die Erstellung von Zwischenlisten vermieden: cycleEnthält einen Verweis auf das Original, kopiert ihn jedoch nicht. ziparbeitet auf die gleiche Weise.

Es ist wichtig zu beachten, dass dies unterbrochen wird, wenn die Eingabe eine ist iterator, z. B. a file, (oder a mapoder zipin), da das Vorrücken an einer Stelle (durch next(second)) den Iterator automatisch an allen anderen vorrücken wird. Dies lässt sich leicht lösen, indem itertools.teezwei unabhängig voneinander arbeitende Iteratoren über die ursprüngliche Iteration erzeugt werden:

def circular_pairwise(it):
    first, snd = itertools.tee(it)
    second = itertools.cycle(snd)
    next(second)
    return zip(first, second)

tee kann große Mengen zusätzlichen Speichers verwenden, wenn beispielsweise einer der zurückgegebenen Iteratoren aufgebraucht ist, bevor der andere berührt wird. Da wir jedoch immer nur einen Schritt Unterschied haben, ist der zusätzliche Speicher minimal.

RoadieRich
quelle
1
@PythonNut Danke, das ist es, was ich an der itertoolsfunktionalen Programmierung im Allgemeinen liebe. Sie ermöglicht extrem sauberen, präzisen Code und ist dennoch leicht zu lesen.
RoadieRich
Es schlägt fehl, wenn die Eingabe ein Iterator ist.
JFS
3
In den cycleDokumenten wird ausdrücklich angegeben, dass sie eine Kopie jedes Elements in der iterierbaren Eingabe speichern. Sie sparen also keinen Platz beim Erstellen einer Kopie der Liste. Einige der anderen Antworten vermeiden dieses Problem mit chainoder fillvalue.
Agf
28

Es gibt effizientere Methoden (die keine temporären Listen erstellen), aber ich denke, dies ist die prägnanteste:

> l = [1,2,3]
> zip(l, (l+l)[1:])
[(1, 2), (2, 3), (3, 1)]
chepner
quelle
7
Als Problem, wenn die Liste wirklich lang ist. Es wird eine weitere Liste erstellt, die (l+l)doppelt so groß ist.
Nigel222
Sicher; Ich habe mich bei dieser Antwort um maximale Prägnanz bemüht.
Chepner
3
"Dies ist ein Problem, wenn die Liste wirklich lang ist." Das Erstellen einer anderen Liste, ob lang oder nicht, ist an sich kein Problem. Es ist nur eine Tatsache, sich dessen bewusst zu sein.
Brian_o
22

Ich würde ein Listenverständnis verwenden und die Tatsache ausnutzen, dass dies l[-1]das letzte Element ist.

>>> l = [1,2,3]
>>> [(l[i-1],l[i]) for i in range(len(l))]
[(3, 1), (1, 2), (2, 3)]

Auf diese Weise benötigen Sie keine temporäre Liste.

Atn
quelle
2
Sie würden xrangein Python 2 benötigen , um eine temporäre Liste zu vermeiden. Das Erstellen einer Liste mit Zahlen von 1 bis n ist genauso teuer wie das Erstellen einer flachen Kopie einer Liste mit nElementen.
Chepner
5
Ich denke zu ähnlich zu deiner, um seine eigene Antwort zu sein, das könnte auch so etwas sein [(l[i-1], it) for i, it in enumerate(l)]. Sie sollten wahrscheinlich beachten, dass bei beiden Methoden das zuletzt angeforderte Element an erster Stelle steht.
Porglezomp
6
@chepner: Ich neige dazu, Python3 anzunehmen, wenn nicht angegeben;)
Atn
1
Porglezomps Recht, das erste Element der Liste sollte (1,2) sein, nicht (3,1).
Aaron Hall
22

Paarweise kreisförmige Python 'for'-Schleife

Wenn Ihnen die akzeptierte Antwort gefällt,

zip(L, L[1:] + L[:1])

Sie können mit semantisch demselben Code viel mehr Speicherlicht erzeugen, indem Sie Folgendes verwenden itertools:

from itertools import islice, chain #, izip as zip # uncomment if Python 2

Und dies materialisiert kaum etwas im Speicher, das über die ursprüngliche Liste hinausgeht (vorausgesetzt, die Liste ist relativ groß):

zip(l, chain(islice(l, 1, None), islice(l, None, 1)))

Zum Verwenden verbrauchen Sie einfach (zum Beispiel mit einer Liste):

>>> list(zip(l, chain(islice(l, 1, None), islice(l, None, 1))))
[(1, 2), (2, 3), (3, 1)]

Dies kann auf jede Breite erweiterbar gemacht werden:

def cyclical_window(l, width=2):
    return zip(*[chain(islice(l, i, None), islice(l, None, i)) for i in range(width)])

und Verwendung:

>>> l = [1, 2, 3, 4, 5]
>>> cyclical_window(l)
<itertools.izip object at 0x112E7D28>
>>> list(cyclical_window(l))
[(1, 2), (2, 3), (3, 4), (4, 5), (5, 1)]
>>> list(cyclical_window(l, 4))
[(1, 2, 3, 4), (2, 3, 4, 5), (3, 4, 5, 1), (4, 5, 1, 2), (5, 1, 2, 3)]

Unbegrenzte Generierung mit itertools.teemitcycle

Sie können auch verwenden tee, um zu vermeiden, dass ein redundantes Zyklusobjekt erstellt wird:

from itertools import cycle, tee
ic1, ic2 = tee(cycle(l))
next(ic2)    # must still queue up the next item

und nun:

>>> [(next(ic1), next(ic2)) for _ in range(10)]
[(1, 2), (2, 3), (3, 1), (1, 2), (2, 3), (3, 1), (1, 2), (2, 3), (3, 1), (1, 2)]

Das ist unglaublich effizient, eine erwartete Nutzung itermit nextund eleganter Verwendung von cycle, teeund zip.

Gehen Sie nicht cycledirekt zu weiter, es listsei denn, Sie haben Ihre Arbeit gespeichert und haben Zeit, damit Ihr Computer zum Stillstand kommt, wenn Sie den Speicher voll ausschöpfen. Wenn Sie Glück haben, wird Ihr Betriebssystem den Prozess nach einer Weile abbrechen, bevor er Ihren Computer zum Absturz bringt .

Integrierte Python-Funktionen

Schließlich keine Standard-Lib-Importe, aber dies funktioniert nur bis zur Länge der ursprünglichen Liste (sonst IndexError.)

>>> [(l[i], l[i - len(l) + 1]) for i in range(len(l))]
[(1, 2), (2, 3), (3, 1)]

Sie können dies mit modulo fortsetzen:

>>> len_l = len(l)
>>> [(l[i % len_l], l[(i + 1) % len_l]) for i in range(10)]
[(1, 2), (2, 3), (3, 1), (1, 2), (2, 3), (3, 1), (1, 2), (2, 3), (3, 1), (1, 2)]
Aaron Hall
quelle
Vielleicht möchten Sie erwähnen, dass range(10)der Zyklus niemals enden wird , wenn Sie (in diesem Fall ) keinen Endpunkt haben.
Tadhg McDonald-Jensen
4
Sicher, sie werden enden, geben Sie sie einfach an die Liste weiter und sie enden, wenn Ihnen der Speicher ausgeht. : D
Aaron Hall
19

Erstaunlich, wie viele verschiedene Möglichkeiten es gibt, dieses Problem zu lösen.

Hier ist noch einer. Sie können das pairwiseRezept verwenden, aber anstatt es zu komprimieren b, chainmit dem ersten Element, das Sie bereits entfernt haben. Muss nicht, cyclewenn wir nur einen einzigen zusätzlichen Wert benötigen:

from itertools import chain, izip, tee

def pairwise_circle(iterable):
    a, b = tee(iterable)
    first = next(b, None)
    return izip(a, chain(b, (first,)))
Barry
quelle
11

Ich mag eine Lösung, die die ursprüngliche Liste nicht ändert und die Liste nicht in den temporären Speicher kopiert:

def circular(a_list):
    for index in range(len(a_list) - 1):
        yield a_list[index], a_list[index + 1]
    yield a_list[-1], a_list[0]

for x in circular([1, 2, 3]):
    print x

Ausgabe:

(1, 2)
(2, 3)
(3, 1)

Ich kann mir vorstellen, dass dies für einige sehr große In-Memory-Daten verwendet wird.

David Cullen
quelle
1
Dies ist die beste Lösung. Effizient, einfach zu lesen und wiederverwendbar.
Jack Aidley
8

Dieser funktioniert auch dann, wenn die Liste den lgrößten Teil des Systemspeichers belegt hat. (Wenn etwas garantiert, dass dieser Fall unmöglich ist, ist der von chepner gepostete Reißverschluss in Ordnung.)

l.append( l[0] )
for i in range( len(l)-1):
   pair = l[i],l[i+1]
   # stuff involving pair
del l[-1] 

oder allgemeiner (funktioniert für jeden Offset ndh l[ (i+n)%len(l) ])

for i in range( len(l)):
   pair = l[i], l[ (i+1)%len(l) ]
   # stuff

vorausgesetzt, Sie befinden sich auf einem System mit anständig schneller Modulo-Teilung (dh nicht mit einem eingebetteten System mit Erbsenhirn).

Es scheint eine weit verbreitete Überzeugung zu geben, dass die Indizierung einer Liste mit einem ganzzahligen Index nicht pythonisch ist und am besten vermieden wird. Warum?

nigel222
quelle
2
Ihr letzter Satz sollte eine separate Frage sein. Die Antwort ist , dass neue Python - Programmierer von C, C ++ kommen, oder Java schreiben for i in range(len(a_list)): print a_list[i]statt for x in a_list: print x.
David Cullen
Das Indizieren einer Liste mit ganzzahligen Indizes ist unpythonisch, denn wenn Sie nur das Element benötigen, for el in iterable:ist dies der richtige Weg. Wenn Sie auch den Index benötigen, enumerate()ist der richtige Weg. Eine manuelle Indizierung sollte daher vermieden werden.
Rubik
7

Dies ist meine Lösung, und sie sieht für mich pythonisch genug aus:

l = [1,2,3]

for n,v in enumerate(l):
    try:
        print(v,l[n+1])
    except IndexError:
        print(v,l[0])

Drucke:

1 2
2 3
3 1

Die Generatorfunktionsversion:

def f(iterable):
    for n,v in enumerate(iterable):
        try:
            yield(v,iterable[n+1])
        except IndexError:
            yield(v,iterable[0])

>>> list(f([1,2,3]))
[(1, 2), (2, 3), (3, 1)]
alec_djinn
quelle
6

Wie wäre es damit?

li = li+[li[0]]
pairwise = [(li[i],li[i+1]) for i in range(len(li)-1)]
Aswin PJ
quelle
5
from itertools import izip, chain, islice

itr = izip(l, chain(islice(l, 1, None), islice(l, 1)))

(Wie oben mit @ jf-sebastians "zip" -Antwort, jedoch mit itertools.)

NB: BEARBEITET mit hilfreichem Anstoß von @ 200_success . zuvor war:

itr = izip(l, chain(l[1:], l[:1]))
shaunc
quelle
1
Wenn Sie verwenden itertoolsmöchten, ist die Lösung von @ RoadieRich besser, da das Kopieren und Schneiden vermieden wird.
200_erfolg
Guter Punkt - aus irgendeinem Grund hatte es in meinem Kopf, dass Slice "Copy on Write" war. Bearbeitet, um islice zu verwenden.
Shaunc
3

Nur ein weiterer Versuch

>>> L = [1,2,3]
>>> zip(L,L[1:]) + [(L[-1],L[0])]
[(1, 2), (2, 3), (3, 1)]
Eric Tsui
quelle
3

Wenn Sie nicht zu viel Speicher verbrauchen möchten, können Sie meine Lösung ausprobieren:

[(l[i], l[(i+1) % len(l)]) for i, v in enumerate(l)]

Es ist etwas langsamer, verbraucht aber weniger Speicher.

Satoru
quelle
1

L = [1, 2, 3] a = Zip (L, L [1:] + L [: 1]) für i in a: b = Liste (i) drucke b

Samarpit Arya
quelle
Willkommen bei StackOverflow! Bitte erwägen Sie, Ihrem Code eine Erklärung hinzuzufügen. Vielen Dank!
Aurasphere
1

In der kommenden Zeit Python 3.10 release schedulebietet die neue pairwiseFunktion eine Möglichkeit, gleitende Paare aufeinanderfolgender Elemente zu erstellen:

from itertools import pairwise

# l = [1, 2, 3]
list(pairwise(l + l[:1]))
# [(1, 2), (2, 3), (3, 1)]

oder einfach, pairwise(l + l[:1])wenn Sie das Ergebnis nicht als list.


Beachten Sie, dass wir pairwisein der Liste mit dem Kopf ( l + l[:1]) angehängt sind, sodass rollende Paare kreisförmig sind (dh, dass wir auch das (3, 1)Paar einschließen ):

list(pairwise(l)) # [(1, 2), (2, 3)]
l + l[:1] # [1, 2, 3, 1]
Xavier Guihot
quelle
-1

Dies scheint, als würden Kombinationen den Job machen.

from itertools import combinations
x=combinations([1,2,3],2)

Dies würde einen Generator ergeben. Dies kann dann als solches wiederholt werden

for i in x:
  print i

Die Ergebnisse würden ungefähr so ​​aussehen

(1, 2)
(1, 3)
(2, 3)
Hevon Gordon
quelle
Ihre Ergebnisse stimmen nicht mit dem Beispiel des OP überein.
David Cullen
Ja wirklich? Sie nehmen an, dass die Reihenfolge wichtig ist ... Ich habe angenommen, dass nur die Kombinationen wichtig sind. was bedeuten würde, dass diese Antwort richtig ist.
Hevon Gordon
1
Betrachten Sie vielleicht eine längere Liste, da [1,2,3,4,5]das Ergebnis [(1,2), (2,3), (3,4), (4,5), (5,1)]nicht Kombinationen, sondern nur Paare in der Liste sind.
Tadhg McDonald-Jensen