Паровий круговий цикл Python 'for'


76

Чи є приємний пітонічний спосіб перебирати список, перенастроюючи пару елементів? Останній елемент повинен бути в парі з першим.

Так, наприклад, якщо у мене є список [1, 2, 3], я хотів би отримати наступні пари:

  • 1 - 2
  • 2 - 3
  • 3 - 1


@BhargavRao повинен, мабуть, закрити цей, як обман цього
Баррі,

@Barry Відповіді тут просто чудові, отже, я не забивав. Мені потрібен був хтось, щоб запевнити, що зворотний обман краще. Дякую (і я теж там додав коментар)
Бхаргав Рао

Відповіді:


112

Pythonic спосіб отримати доступ до списку парно є: zip(L, L[1:]). Щоб підключити останній елемент до першого:

>>> L = [1, 2, 3]
>>> zip(L, L[1:] + L[:1])
[(1, 2), (2, 3), (3, 1)]

32
Якщо список величезний, це може бути недоцільним
Аарон Холл

10
@AaronHall правильно. Але пам’ятайте: «Передчасна оптимізація - корінь усього зла» . Якщо пам'ять є проблемою або для підтримки довільних ітерацій (а не лише послідовностей), itertoolsрішення може бути використано.
jfs

1
О, я бачу, що ви посилаєтесь на іншу відповідь. На жаль, відповідь, на яку ви посилаєтесь, не можна розширити до ширини більше 2 елементів. Однак у мене є прямий аналог itertools вашої пропозиції, наведений нижче.
Аарон Холл

1
@AaronHall Чи все одно це буде проблематично в Python 3 проти 2.x? Коефіцієнти 3 zip(крім усього іншого) досить дивно ефективні.
Рідко "Де Моніка" Потребність

5
@SeldomNeedy Python 2 zip(*iterables)схожий на 3 list(zip(*iterables))- саме тому я пропоную імпортувати izip as zipдля Python 2 у своїй відповіді - stackoverflow.com/a/36918720/541136 - JF насправді передбачає Python 2 zipу цій відповіді, що поглиблює проблему непотрібного створення довгих списків в пам'яті. Відповідь JF використовує на 200% більше пам'яті, ніж потрібно для списків (8 байт на елемент), а також пам'ять для кожного кортежу (відносно величезна, 64 байти на кортеж, див. Stackoverflow.com/a/30316760/541136 ) на 800% більше, тож на 1000% більше загальної кількості (припускаючи ледачу оцінку.)
Аарон Холл

49

Я б використовував dequeсzip , щоб досягти цього.

>>> from collections import deque
>>>
>>> l = [1,2,3]
>>> d = deque(l)
>>> d.rotate(-1)
>>> zip(l, d)
[(1, 2), (2, 3), (3, 1)]

Деке обертається дуже швидко, але це також досить велика пам'ять.
Аарон Холл

1
Звичайно. Для малих рішень це швидко і чисто. Це також демонструє, deque.rotateщо може бути надзвичайно корисним.
gddc

45

Я б використав невелику модифікацію pairwiseрецепта з itertoolsдокументації :

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)

Це просто збереже посилання на перше значення, а коли другий ітератор буде вичерпаний, zip_longest останнє місце заповниться першим значенням.

(Також зверніть увагу, що він працює з ітераторами, такими як генератори, а також з ітераторами, такими як списки / кортежі.)

Зауважте, що рішення @ Barry дуже схоже на це, але дещо легше зрозуміти, на мій погляд, і легше вийти за рамки одного елемента.


@GrijeshChauhan ви бачили рішення @ Barry ?
Tadhg McDonald-Jensen

Логіка не розширюється більш ніж на другий елемент.
Зал Аарона

@AaronHall, якби я мав будь-яку причину продовжувати його подальше, я б обернув його в cycle.
Tadhg McDonald-Jensen

@ TadhgMcDonald-Jensen Щойно перевірив - приємно навчитися ще одному фокусу. Але все-таки для мене itertools.zip_longest(a, b, fillvalue=first_value) це очевидніше izip(a, chain(b, (first,))) - не потрібно робити зайвий кортеж(first, )
Грієш Чаухан

38

Я б поєднався itertools.cycleз zip:

import itertools

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

cycle повертає ітерабель, який видає значення аргументу в порядку, переходячи від останнього значення до першого.

Ми пропускаємо перше значення, тому воно починається з позиції 1(а не0 ).

Далі ми zipвикористовуємо оригінальний, немутований список. zipце добре, оскільки він зупиняється, коли будь-який його ітерабель аргументів вичерпується.

Це робить таким чином, щоб уникнути створення проміжних списків: cycleзберігає посилання на оригінал, але не копіює його. zipдіє так само.

Важливо зауважити, що це призведе до поломки, якщо вхідне значення є iteratorтаким, як a file, (або amap або zipin), оскільки просування в одному місці (наскрізь next(second)) автоматично просуне ітератор у всіх інших. Це легко вирішити за допомогою itertools.tee, яка створює два ітератори, що працюють незалежно від оригінального ітеративного:

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

tee може використовувати великий обсяг додаткового сховища, наприклад, якщо один із повернутих ітераторів використовується до того, як торкнуться іншого, але, оскільки у нас коли-небудь є лише один крок, додатковий обсяг є мінімальним.


1
@PythonNut Дякую, саме це мені подобається itertools, а функціональне програмування в цілому дозволяє надзвичайно чистий, стислий код, і при цьому його легко читати.
RoadieRich

Це не вдається, якщо введенням є ітератор.
jfs

3
У cycleдокументах конкретно заявляють , що вони зберегти копію кожного елемента на вході Iterable - так що ви не економити будь-який простір над зробити копію списку. Деякі інші відповіді дозволяють уникнути цієї проблеми, використовуючи chainабо fillvalue.
agf

28

Є більш ефективні способи (які не створюють тимчасові списки), але я думаю, що це найбільш стисло:

> l = [1,2,3]
> zip(l, (l+l)[1:])
[(1, 2), (2, 3), (3, 1)]

7
Це проблема, якщо список справді довгий. Він створює ще один список (l+l)удвічі більший.
nigel222

Звичайно; Я збирався досягти максимальної лаконічності у цій відповіді.
Чепнер

3
"Це проблема, якщо список справді довгий." Створення іншого списку, довгого чи ні, само по собі не є проблемою; це просто факт, про який слід пам’ятати.
brian_o

22

Я б використав розуміння списку та скористався тим, що l[-1]є останнім елементом.

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

Вам не потрібен тимчасовий список таким чином.


2
Вам знадобиться xrangePython 2, щоб уникнути тимчасового списку. Створення списку з числа від 1 до n так само дорого, як створення дрібної копії списку з nелементами.
Чепнер

5
Я думаю, що занадто схожий на ваш, щоб бути власною відповіддю, це також може бути чимось на зразок [(l[i-1], it) for i, it in enumerate(l)]. Напевно, слід зауважити, що обидва ці методи ставлять останній запитуваний елемент першим.
porglezomp

6
@chepner: Я схильний вважати python3, коли не вказано;)
Atn

1
porglezomp праворуч, першим елементом списку має бути (1,2), а не (3,1).
Аарон Холл

22

Паровий круговий цикл Python 'for'

Якщо вам подобається прийнята відповідь,

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

Ви можете використовувати набагато більше світла пам’яті за допомогою семантично того самого коду, використовуючи itertools:

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

І це ледве матеріалізує щось у пам’яті, окрім вихідного списку (якщо припустити, що список відносно великий):

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

Для використання просто споживайте (наприклад, зі списком):

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

Це можна зробити розширюваним до будь-якої ширини:

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

та використання:

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

Необмежена генерація за itertools.teeдопомогоюcycle

Ви також можете використовувати, teeщоб уникнути створення надлишкового об’єкта циклу:

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

і зараз:

>>> [(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)]

Це неймовірно ефективний, очікуване використання iterз next, і елегантним використанням cycle, teeі zip.

Не переходьте cycleбезпосередньо до, listякщо ви не зберегли свою роботу і не встигнете, щоб ваш комп’ютер зупинився, коли ви максимізуєте його пам’ять - якщо вам пощастить, через деякий час ваша ОС вб’є процес, перш ніж він вийде з ладу комп’ютера .

Функції чистого Python Builtin

Нарешті, немає стандартного імпорту lib, але це працює лише до довжини оригінального списку (в іншому випадку IndexError).

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

Ви можете продовжити це за модулем:

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

Ви можете згадати, що якщо у вас немає якоїсь кінцевої точки (у цьому випадку range(10)), цикл ніколи не закінчиться.
Tadhg McDonald-Jensen

4
Звичайно, вони закінчаться, просто передайте їх до списку, і вони закінчаться, коли у вас закінчиться пам’ять. : D
Аарон Холл

19

Вражаюче, скільки різних способів вирішити цю проблему.

Ось ще один. Ви можете використовувати pairwiseрецепт , але замість того , щоб стиснути з b, chainйого з першим елементом , який ви вже відскочили. Не потрібно, cycleколи нам потрібна лише одна додаткова цінність:

from itertools import chain, izip, tee

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

11

Мені подобається рішення, яке не змінює оригінальний список і не копіює список на тимчасове сховище:

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

Вихід:

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

Я можу уявити, що це використовується для деяких дуже великих даних у пам'яті.


1
Це найкраще рішення. Ефективний, зручний для читання та багаторазового використання.
Джек Едлі,

8

Це буде працювати, навіть якщо список lзайняв більшу частину пам'яті системи. (Якщо щось гарантує, що цей випадок стане неможливим, тоді zip, розміщений chepner, чудово)

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

або більш загально (працює для будь-якого зсуву, nтобто l[ (i+n)%len(l) ])

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

за умови, що ви працюєте в системі з пристойно швидким модульним розподілом (тобто не в якійсь вбудованій системі з мозком).

Здається, існує поширена думка, що індексація списку цілочисельним індексом є непітонічною і найкраще уникати. Чому?


2
Ваше останнє речення має бути окремим питанням. Відповідь полягає в тому, що нові програмісти Python, що походять з C, C ++ або Java, пишуть for i in range(len(a_list)): print a_list[i]замість for x in a_list: print x.
Девід Каллен,

Індексація списку за допомогою цілочисельних індексів є непітонічною, оскільки якщо вам просто потрібен елемент, for el in iterable:це шлях. Якщо вам також потрібен індекс, enumerate()це шлях. Тому слід уникати ручного індексування.
rubik

7

Це моє рішення, і воно мені здається досить пітонічним:

l = [1,2,3]

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

відбитки:

1 2
2 3
3 1

Версія функції генератора:

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


5
from itertools import izip, chain, islice

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

(Як і вище у відповіді @ zf-sebastian "zip" , але за допомогою itertools.)

Примітка: ВИДАЛЕНО, надано корисний поштовх від @ 200_success . раніше було:

itr = izip(l, chain(l[1:], l[:1]))

1
Якщо ви збираєтеся використовувати itertools, тоді рішення @ RoadieRich краще, оскільки воно дозволяє уникнути копіювання та нарізки.
200_success

Хороший момент - чомусь у мене в голові було те, що фрагмент був "копіювати на запис". Відредаговано для використання islice.
shaunc


3

Якщо ви не хочете споживати занадто багато пам'яті, спробуйте моє рішення:

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

Це трохи повільніше, але споживає менше пам'яті.



1

У майбутньому Python 3.10 release scheduleнова pairwiseфункція забезпечує спосіб створення розсувних пар послідовних елементів:

from itertools import pairwise

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

або просто pairwise(l + l[:1])якщо вам не потрібен результат як list.


Зверніть увагу, що ми pairwiseв списку додані з його head ( l + l[:1]) так, щоб прокатні пари були круговими (тобто, щоб ми також включили (3, 1)пару):

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

-1

здається, комбінації зробили б цю роботу.

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

це дало б генератор. це потім можна повторити як таке

for i in x:
  print i

результати виглядали б приблизно так

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

Ваші результати не збігаються з прикладом OP.
Девід Каллен,

справді? ви припускаєте, що порядок має значення ... Я припускав, що мають значення лише комбінації. що означало б, що ця відповідь правильна.
Гевон Гордон,

1
можливо, розглянемо довший список, враховуючи [1,2,3,4,5]результат, [(1,2), (2,3), (3,4), (4,5), (5,1)]який буде не комбінаціями, а лише парами у списку.
Tadhg McDonald-Jensen
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.