Banyak tugas pemrosesan data sebenarnya merupakan pertanyaan tentang transisi: Apakah sebuah pengukuran meningkat? Berapa lama jeda antara dua event? Apakah state berubah? Apakah sebuah sequence terurut? Masalah seperti ini membutuhkan nilai yang berdekatan, bukan pasangan arbitrer.

Python 3.10 menambahkan itertools.pairwise() tepat untuk pola ini. Fungsi tersebut menghasilkan pasangan berdekatan yang saling overlap secara lazy, sehingga intent lebih jelas dibanding indexing manual dan kode yang sama dapat bekerja pada list, generator, file, serta iterable lainnya.

Fungsinya kecil, tetapi penggunaannya yang tepat memerlukan pemahaman tentang perilaku boundary dan fakta bahwa iterator dikonsumsi ketika iterasi berlangsung.

Apa yang dihasilkan pairwise

Dengan iterable berikut:

A B C D

pairwise() menghasilkan:

(A, B)
(B, C)
(C, D)

Dalam Python:

from itertools import pairwise

values = [10, 13, 12, 20]

for previous, current in pairwise(values):
    print(previous, current)

Output-nya:

10 13
13 12
12 20

Perhatikan bahwa elemen tengah berpartisipasi dua kali. 13 menjadi nilai current pada pasangan pertama dan previous pada pasangan kedua. Overlap inilah yang membedakan iterasi pasangan berdekatan dari pengelompokan biasa menjadi chunk.

Jumlah output satu lebih sedikit

Jika input memiliki n nilai, pairwise() menghasilkan n - 1 pasangan ketika n >= 2.

Untuk nol atau satu nilai input, tidak ada pasangan yang dihasilkan:

from itertools import pairwise

print(list(pairwise([])))
print(list(pairwise([42])))
print(list(pairwise([1, 2])))
[]
[]
[(1, 2)]

Perilaku boundary ini sering kali memang tepat. Tidak ada perbandingan berdekatan yang dapat dilakukan jika jumlah nilai kurang dari dua.

Namun hal itu tetap dapat penting bagi logika aplikasi. Jika sebuah file diharapkan memiliki setidaknya dua sampel, tidak adanya perbandingan secara diam-diam dapat menyembunyikan input yang tidak valid. Validasi kebutuhan tersebut secara terpisah, jangan mengharapkan pairwise() melempar error.

Hitung perubahan tanpa indexing

Salah satu penggunaan umum adalah menghitung delta antara observasi berurutan:

from itertools import pairwise

readings = [101.2, 101.8, 100.9, 102.4]

deltas = [current - previous for previous, current in pairwise(readings)]
print(deltas)
[0.5999999999999943, -0.8999999999999915, 1.5]

Representasi floating-point yang terlihat di sini merupakan perilaku normal binary floating-point; gunakan representasi numerik atau kebijakan perbandingan yang sesuai jika aritmetika desimal eksak diperlukan.

Keuntungan strukturalnya adalah kode menyatakan maksud secara langsung: bandingkan setiap nilai dengan pendahulunya. Tidak ada aritmetika index seperti values[i - 1], sehingga tidak ada boundary i = 0 yang harus ditangani secara tidak sengaja.

Periksa apakah nilai sudah terurut

Perbandingan berdekatan cukup untuk menguji urutan monotonik:

from itertools import pairwise


def is_non_decreasing(values):
    return all(previous <= current for previous, current in pairwise(values))


print(is_non_decreasing([2, 2, 5, 9]))  # True
print(is_non_decreasing([2, 7, 5, 9]))  # False

Implementasi ini menganggap iterable kosong dan iterable satu elemen sebagai non-decreasing karena tidak ada pasangan berdekatan yang melanggar kondisi. Ini mengikuti perilaku all() yang mengembalikan True untuk input kosong.

Apakah perilaku tersebut valid bagi domain Anda merupakan pertanyaan terpisah. Jika business rule membutuhkan setidaknya dua observasi, enforce persyaratan itu secara eksplisit.

Temukan transisi dalam event stream

Misalkan event memiliki timestamp dan state:

from itertools import pairwise


events = [
    (0, "starting"),
    (3, "running"),
    (18, "running"),
    (25, "stopped"),
]

for previous, current in pairwise(events):
    previous_time, previous_state = previous
    current_time, current_state = current

    if previous_state != current_state:
        print(
            f"{previous_state} -> {current_state} "
            f"after {current_time - previous_time}s"
        )

Pola ini berguna untuk log, telemetry, workflow history, dan protocol trace karena perbandingannya lokal. Anda tidak memerlukan seluruh history untuk menentukan apakah event saat ini merupakan transisi dari event sebelumnya.

pairwise bersifat lazy

pairwise() mengembalikan iterator. Fungsi ini tidak terlebih dahulu mengubah seluruh input menjadi list.

Karena itu, fungsi ini cocok untuk input yang datang secara incremental:

from itertools import pairwise


def temperatures():
    yield 20.1
    yield 20.4
    yield 21.0
    yield 20.7


for previous, current in pairwise(temperatures()):
    print(current - previous)

Hanya input secukupnya yang dikonsumsi untuk menghasilkan pasangan berikutnya. Secara konseptual, operasi hanya perlu mempertahankan item sebelumnya sambil mengambil item berikutnya, bukan menyimpan seluruh iterable.

Laziness sangat berguna ketika sumber sangat besar atau tidak memiliki akhir alami. Hal ini juga berarti side effect pada source generator terjadi selama iterasi, bukan ketika pairwise() dibuat.

Berhenti lebih awal membuat source sudah terkonsumsi sebagian

Iterasi lazy memiliki konsekuensi ownership penting. Jika Anda memberikan iterator yang sudah ada ke pairwise() lalu berhenti lebih awal, iterator tersebut sudah bergerak maju.

from itertools import pairwise

source = iter([10, 20, 30, 40, 50])

for previous, current in pairwise(source):
    print(previous, current)
    if current == 30:
        break

print(list(source))

Jangan menulis kode yang bergantung pada penggunaan ulang source dari posisi awal setelahnya. Iterator umumnya merepresentasikan stream yang dapat dikonsumsi, bukan collection yang dapat di-rewind.

Jika beberapa consumer membutuhkan akses independen, putuskan hal itu pada ownership boundary. Kadang materialisasi input yang bounded tepat dilakukan. Pada kasus lain, source perlu dibuka kembali atau dibuat ulang. Hindari menduplikasi stream tanpa batas hanya untuk mempertahankan API yang nyaman.

pairwise bukan batching

pairwise() dan itertools.batched() menyelesaikan masalah berbeda.

Untuk input A B C D, adjacent pairing menghasilkan:

(A, B) (B, C) (C, D)

Batching ke grup berisi dua menghasilkan:

(A, B) (C, D)

Gunakan pairwise() ketika setiap item harus dibandingkan dengan item tepat sebelum atau sesudahnya. Gunakan batching ketika item harus dipartisi menjadi grup non-overlap untuk diproses.

Mencampuradukkan operasi ini dapat diam-diam menghilangkan transisi. Dengan batching, hubungan antara B dan C hilang sepenuhnya.

Ini adalah window tepat dua elemen

pairwise() paling tepat dipahami sebagai sliding window khusus dengan lebar dua. Fungsi ini tidak secara langsung menghasilkan window berisi tiga elemen atau lebih.

Jika Anda perlu membandingkan triple seperti (A, B, C), (B, C, D), gunakan implementasi sliding-window yang sesuai dengan versi Python yang didukung, alih-alih menumpuk pairwise() dan menghasilkan struktur yang lebih sulit dipahami.

Demikian pula, jika Anda memerlukan setiap pasangan yang mungkin, pairwise() bukan operasi yang tepat. Misalnya, memeriksa setiap pasangan elemen berbeda merupakan masalah kombinatorial dan mungkin memerlukan itertools.combinations().

Memilih operasi berdasarkan hubungan yang dibutuhkan lebih penting daripada fakta superfisial bahwa setiap hasil kebetulan berisi dua nilai.

Pertahankan boundary domain

Nilai berdekatan hanya bermakna ketika berada dalam logical sequence yang sama.

Bayangkan record yang diurutkan berdasarkan customer dan waktu:

customer A, event 1
customer A, event 2
customer B, event 1
customer B, event 2

Menjalankan satu pairwise() pada seluruh stream menghasilkan pasangan yang melintasi customer A dan customer B. Transisi itu biasanya tidak bermakna.

Group atau partition data terlebih dahulu, lalu terapkan pairwise() di dalam setiap domain boundary:

from itertools import groupby, pairwise
from operator import itemgetter

records = [
    ("A", 1, "queued"),
    ("A", 2, "sent"),
    ("B", 1, "queued"),
    ("B", 3, "sent"),
]

for customer, customer_records in groupby(records, key=itemgetter(0)):
    for previous, current in pairwise(customer_records):
        print(customer, previous, current)

groupby() sendiri mengelompokkan nilai berurutan dengan key yang sama, sehingga input harus sudah memiliki urutan yang dibutuhkan untuk grouping tersebut. Jangan menganggap fungsi ini melakukan grouping bergaya database terhadap input arbitrer.

Ordering merupakan bagian dari correctness

pairwise() menggunakan urutan iterasi. Fungsi ini tidak melakukan sorting dan tidak mengetahui urutan kronologis, numerik, atau semantik yang dimaksud aplikasi.

Untuk record bertimestamp, buat jaminan ordering secara eksplisit. Data dari database harus memakai ORDER BY yang sesuai; data dari beberapa sumber mungkin perlu di-merge atau di-sort; enumerasi filesystem tidak boleh diasumsikan merepresentasikan urutan kronologis.

Sorting juga memiliki biaya: biasanya seluruh data harus dikumpulkan. Jika source sudah menjamin urutan yang benar, mempertahankan perilaku streaming dapat jauh lebih murah daripada melakukan sorting ulang.

Kuncinya adalah mengetahui dari mana jaminan ordering berasal.

Jangan sembunyikan validasi di dalam perbandingan

Pertimbangkan gap timestamp:

from itertools import pairwise


def gaps(events):
    for previous, current in pairwise(events):
        yield current.timestamp - previous.timestamp

Kode ini mengasumsikan setiap event memiliki timestamp yang dapat dipakai dan event datang dalam urutan yang dimaksud. Jika gap negatif tidak valid, deteksi secara sengaja:

from itertools import pairwise


def gaps(events):
    for previous, current in pairwise(events):
        gap = current.timestamp - previous.timestamp
        if gap < 0:
            raise ValueError("events are not in chronological order")
        yield gap

pairwise() menyediakan struktur; fungsi ini tidak menyediakan validasi domain. Memisahkan kedua tanggung jawab membuat kegagalan lebih mudah dipahami.

Loop manual tetap dapat sesuai

Sebelum Python 3.10, iterasi adjacent lazim ditulis secara manual. Implementasi manual juga berguna ketika Anda membutuhkan state atau error handling yang tidak biasa:

def adjacent(iterable):
    iterator = iter(iterable)
    try:
        previous = next(iterator)
    except StopIteration:
        return

    for current in iterator:
        yield previous, current
        previous = current

Untuk pasangan adjacent biasa pada Python 3.10 atau lebih baru, itertools.pairwise() mengomunikasikan pola secara langsung dan menghindari pemeliharaan helper lokal.

Namun jika package Anda mendukung Python 3.9 atau lebih lama, import pairwise akan gagal. Pertahankan helper yang kompatibel, gunakan dependency yang menyediakan operasi tersebut, atau naikkan minimum Python version secara sengaja. Jangan tanpa sengaja memperkenalkan persyaratan versi runtime melalui refactor yang tampak kecil.

Uji boundary, bukan hanya happy path

Test untuk logika adjacent-pair sebaiknya mencakup lebih dari list tipikal. Kasus yang berguna meliputi:

  • input kosong;
  • input satu elemen;
  • tepat dua elemen;
  • nilai berulang;
  • generator, bukan list;
  • penghentian lebih awal;
  • ordering tidak valid ketika ordering merupakan persyaratan domain;
  • boundary antara logical group.

Contohnya:

from itertools import pairwise


def test_pairwise_boundaries():
    assert list(pairwise([])) == []
    assert list(pairwise([1])) == []
    assert list(pairwise([1, 2])) == [(1, 2)]
    assert list(pairwise([1, 2, 3])) == [(1, 2), (2, 3)]

Test tersebut mendokumentasikan aturan cardinality terpenting: perbandingan adjacent mengurangi jumlah output sebanyak satu.

Pertahankan model mental tetap kecil

itertools.pairwise() melakukan satu hal: secara lazy mengekspos setiap nilai bersama nilai yang tepat mengikutinya. Fungsi ini tidak melakukan sorting, validasi, batching, rewind, atau membandingkan nilai untuk Anda.

Kontrak yang sempit itulah yang membuatnya berguna. Setelah ordering dan domain boundary ditetapkan, perhitungan adjacent difference, deteksi transisi, pemeriksaan monotonicity, pengukuran interval, dan banyak tugas lain menjadi sederhana.

Gunakan pairwise() ketika hubungan yang Anda pedulikan bersifat lokal dan berurutan. Lalu pertahankan validasi, ordering, grouping, dan iterator ownership secara eksplisit di sekitarnya. Hasilnya biasanya lebih sederhana daripada aritmetika index dan tetap dapat dipakai untuk stream yang sejak awal memang bukan sequence.