Skip to content

Commit cc061d0

Browse files
authored
bpo-38200: Add itertools.pairwise() (GH-23549)
1 parent 427613f commit cc061d0

File tree

5 files changed

+220
-26
lines changed

5 files changed

+220
-26
lines changed

Doc/library/itertools.rst

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ Iterator Arguments Results
5555
:func:`filterfalse` pred, seq elements of seq where pred(elem) is false ``filterfalse(lambda x: x%2, range(10)) --> 0 2 4 6 8``
5656
:func:`groupby` iterable[, key] sub-iterators grouped by value of key(v)
5757
:func:`islice` seq, [start,] stop [, step] elements from seq[start:stop:step] ``islice('ABCDEFG', 2, None) --> C D E F G``
58+
:func:`pairwise` iterable (p[0], p[1]), (p[1], p[2]) ``pairwise('ABCDEFG') --> AB BC CD DE EF FG``
5859
:func:`starmap` func, seq func(\*seq[0]), func(\*seq[1]), ... ``starmap(pow, [(2,5), (3,2), (10,3)]) --> 32 9 1000``
5960
:func:`takewhile` pred, seq seq[0], seq[1], until pred fails ``takewhile(lambda x: x<5, [1,4,6,4,1]) --> 1 4``
6061
:func:`tee` it, n it1, it2, ... itn splits one iterator into n
@@ -475,6 +476,22 @@ loops that truncate the stream.
475476
If *start* is ``None``, then iteration starts at zero. If *step* is ``None``,
476477
then the step defaults to one.
477478

479+
.. function:: pairwise(iterable)
480+
481+
Return successive overlapping pairs taken from the input *iterable*.
482+
483+
The number of 2-tuples in the output iterator will be one fewer than the
484+
number of inputs. It will be empty if the input iterable has fewer than
485+
two values.
486+
487+
Roughly equivalent to::
488+
489+
def pairwise(iterable):
490+
# pairwise('ABCDEFG') --> AB BC CD DE EF FG
491+
a, b = tee(iterable)
492+
next(b, None)
493+
return zip(a, b)
494+
478495

479496
.. function:: permutations(iterable, r=None)
480497

@@ -782,12 +799,6 @@ which incur interpreter overhead.
782799
return starmap(func, repeat(args))
783800
return starmap(func, repeat(args, times))
784801

785-
def pairwise(iterable):
786-
"s -> (s0,s1), (s1,s2), (s2, s3), ..."
787-
a, b = tee(iterable)
788-
next(b, None)
789-
return zip(a, b)
790-
791802
def grouper(iterable, n, fillvalue=None):
792803
"Collect data into fixed-length chunks or blocks"
793804
# grouper('ABCDEFG', 3, 'x') --> ABC DEF Gxx"

Lib/test/test_itertools.py

Lines changed: 34 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1024,6 +1024,25 @@ def run(r1, r2):
10241024
self.assertEqual(next(it), (1, 2))
10251025
self.assertRaises(RuntimeError, next, it)
10261026

1027+
def test_pairwise(self):
1028+
self.assertEqual(list(pairwise('')), [])
1029+
self.assertEqual(list(pairwise('a')), [])
1030+
self.assertEqual(list(pairwise('ab')),
1031+
[('a', 'b')]),
1032+
self.assertEqual(list(pairwise('abcde')),
1033+
[('a', 'b'), ('b', 'c'), ('c', 'd'), ('d', 'e')])
1034+
self.assertEqual(list(pairwise(range(10_000))),
1035+
list(zip(range(10_000), range(1, 10_000))))
1036+
1037+
with self.assertRaises(TypeError):
1038+
pairwise() # too few arguments
1039+
with self.assertRaises(TypeError):
1040+
pairwise('abc', 10) # too many arguments
1041+
with self.assertRaises(TypeError):
1042+
pairwise(iterable='abc') # keyword arguments
1043+
with self.assertRaises(TypeError):
1044+
pairwise(None) # non-iterable argument
1045+
10271046
def test_product(self):
10281047
for args, result in [
10291048
([], [()]), # zero iterables
@@ -1787,6 +1806,10 @@ def test_islice(self):
17871806
a = []
17881807
self.makecycle(islice([a]*2, None), a)
17891808

1809+
def test_pairwise(self):
1810+
a = []
1811+
self.makecycle(pairwise([a]*5), a)
1812+
17901813
def test_permutations(self):
17911814
a = []
17921815
self.makecycle(permutations([1,2,a,3], 3), a)
@@ -1995,6 +2018,17 @@ def test_islice(self):
19952018
self.assertRaises(TypeError, islice, N(s), 10)
19962019
self.assertRaises(ZeroDivisionError, list, islice(E(s), 10))
19972020

2021+
def test_pairwise(self):
2022+
for s in ("123", "", range(1000), ('do', 1.2), range(2000,2200,5)):
2023+
for g in (G, I, Ig, S, L, R):
2024+
seq = list(g(s))
2025+
expected = list(zip(seq, seq[1:]))
2026+
actual = list(pairwise(g(s)))
2027+
self.assertEqual(actual, expected)
2028+
self.assertRaises(TypeError, pairwise, X(s))
2029+
self.assertRaises(TypeError, pairwise, N(s))
2030+
self.assertRaises(ZeroDivisionError, list, pairwise(E(s)))
2031+
19982032
def test_starmap(self):
19992033
for s in (range(10), range(0), range(100), (7,11), range(20,50,5)):
20002034
for g in (G, I, Ig, S, L, R):
@@ -2312,15 +2346,6 @@ def test_permutations_sizeof(self):
23122346
... else:
23132347
... return starmap(func, repeat(args, times))
23142348
2315-
>>> def pairwise(iterable):
2316-
... "s -> (s0,s1), (s1,s2), (s2, s3), ..."
2317-
... a, b = tee(iterable)
2318-
... try:
2319-
... next(b)
2320-
... except StopIteration:
2321-
... pass
2322-
... return zip(a, b)
2323-
23242349
>>> def grouper(n, iterable, fillvalue=None):
23252350
... "grouper(3, 'ABCDEFG', 'x') --> ABC DEF Gxx"
23262351
... args = [iter(iterable)] * n
@@ -2451,15 +2476,6 @@ def test_permutations_sizeof(self):
24512476
>>> take(5, map(int, repeatfunc(random.random)))
24522477
[0, 0, 0, 0, 0]
24532478
2454-
>>> list(pairwise('abcd'))
2455-
[('a', 'b'), ('b', 'c'), ('c', 'd')]
2456-
2457-
>>> list(pairwise([]))
2458-
[]
2459-
2460-
>>> list(pairwise('a'))
2461-
[]
2462-
24632479
>>> list(islice(pad_none('abc'), 0, 6))
24642480
['a', 'b', 'c', None, None, None]
24652481
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added itertools.pairwise()

Modules/clinic/itertoolsmodule.c.h

Lines changed: 32 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Modules/itertoolsmodule.c

Lines changed: 136 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11

2+
23
#define PY_SSIZE_T_CLEAN
34
#include "Python.h"
45
#include "pycore_long.h" // _PyLong_GetZero()
@@ -27,8 +28,9 @@ class itertools.accumulate "accumulateobject *" "&accumulate_type"
2728
class itertools.compress "compressobject *" "&compress_type"
2829
class itertools.filterfalse "filterfalseobject *" "&filterfalse_type"
2930
class itertools.count "countobject *" "&count_type"
31+
class itertools.pairwise "pairwiseobject *" "&pairwise_type"
3032
[clinic start generated code]*/
31-
/*[clinic end generated code: output=da39a3ee5e6b4b0d input=ea05c93c6d94726a]*/
33+
/*[clinic end generated code: output=da39a3ee5e6b4b0d input=6498ed21fbe1bf94]*/
3234

3335
static PyTypeObject groupby_type;
3436
static PyTypeObject _grouper_type;
@@ -45,9 +47,140 @@ static PyTypeObject accumulate_type;
4547
static PyTypeObject compress_type;
4648
static PyTypeObject filterfalse_type;
4749
static PyTypeObject count_type;
50+
static PyTypeObject pairwise_type;
4851

4952
#include "clinic/itertoolsmodule.c.h"
5053

54+
/* pairwise object ***********************************************************/
55+
56+
typedef struct {
57+
PyObject_HEAD
58+
PyObject *it;
59+
PyObject *old;
60+
} pairwiseobject;
61+
62+
/*[clinic input]
63+
@classmethod
64+
itertools.pairwise.__new__ as pairwise_new
65+
iterable: object
66+
/
67+
Return an iterator of overlapping pairs taken from the input iterator.
68+
69+
s -> (s0,s1), (s1,s2), (s2, s3), ...
70+
71+
[clinic start generated code]*/
72+
73+
static PyObject *
74+
pairwise_new_impl(PyTypeObject *type, PyObject *iterable)
75+
/*[clinic end generated code: output=9f0267062d384456 input=6e7c3cddb431a8d6]*/
76+
{
77+
PyObject *it;
78+
pairwiseobject *po;
79+
80+
it = PyObject_GetIter(iterable);
81+
if (it == NULL) {
82+
return NULL;
83+
}
84+
po = (pairwiseobject *)type->tp_alloc(type, 0);
85+
if (po == NULL) {
86+
Py_DECREF(it);
87+
return NULL;
88+
}
89+
po->it = it;
90+
po->old = NULL;
91+
return (PyObject *)po;
92+
}
93+
94+
static void
95+
pairwise_dealloc(pairwiseobject *po)
96+
{
97+
PyObject_GC_UnTrack(po);
98+
Py_XDECREF(po->it);
99+
Py_XDECREF(po->old);
100+
Py_TYPE(po)->tp_free(po);
101+
}
102+
103+
static int
104+
pairwise_traverse(pairwiseobject *po, visitproc visit, void *arg)
105+
{
106+
Py_VISIT(po->it);
107+
Py_VISIT(po->old);
108+
return 0;
109+
}
110+
111+
static PyObject *
112+
pairwise_next(pairwiseobject *po)
113+
{
114+
PyObject *it = po->it;
115+
PyObject *old = po->old;
116+
PyObject *new, *result;
117+
118+
if (it == NULL) {
119+
return NULL;
120+
}
121+
if (old == NULL) {
122+
po->old = old = (*Py_TYPE(it)->tp_iternext)(it);
123+
if (old == NULL) {
124+
Py_CLEAR(po->it);
125+
return NULL;
126+
}
127+
}
128+
new = (*Py_TYPE(it)->tp_iternext)(it);
129+
if (new == NULL) {
130+
Py_CLEAR(po->it);
131+
Py_CLEAR(po->old);
132+
return NULL;
133+
}
134+
/* Future optimization: Reuse the result tuple as we do in enumerate() */
135+
result = PyTuple_Pack(2, old, new);
136+
Py_SETREF(po->old, new);
137+
return result;
138+
}
139+
140+
static PyTypeObject pairwise_type = {
141+
PyVarObject_HEAD_INIT(&PyType_Type, 0)
142+
"itertools.pairwise", /* tp_name */
143+
sizeof(pairwiseobject), /* tp_basicsize */
144+
0, /* tp_itemsize */
145+
/* methods */
146+
(destructor)pairwise_dealloc, /* tp_dealloc */
147+
0, /* tp_vectorcall_offset */
148+
0, /* tp_getattr */
149+
0, /* tp_setattr */
150+
0, /* tp_as_async */
151+
0, /* tp_repr */
152+
0, /* tp_as_number */
153+
0, /* tp_as_sequence */
154+
0, /* tp_as_mapping */
155+
0, /* tp_hash */
156+
0, /* tp_call */
157+
0, /* tp_str */
158+
PyObject_GenericGetAttr, /* tp_getattro */
159+
0, /* tp_setattro */
160+
0, /* tp_as_buffer */
161+
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC |
162+
Py_TPFLAGS_BASETYPE, /* tp_flags */
163+
pairwise_new__doc__, /* tp_doc */
164+
(traverseproc)pairwise_traverse, /* tp_traverse */
165+
0, /* tp_clear */
166+
0, /* tp_richcompare */
167+
0, /* tp_weaklistoffset */
168+
PyObject_SelfIter, /* tp_iter */
169+
(iternextfunc)pairwise_next, /* tp_iternext */
170+
0, /* tp_methods */
171+
0, /* tp_members */
172+
0, /* tp_getset */
173+
0, /* tp_base */
174+
0, /* tp_dict */
175+
0, /* tp_descr_get */
176+
0, /* tp_descr_set */
177+
0, /* tp_dictoffset */
178+
0, /* tp_init */
179+
PyType_GenericAlloc, /* tp_alloc */
180+
pairwise_new, /* tp_new */
181+
PyObject_GC_Del, /* tp_free */
182+
};
183+
51184

52185
/* groupby object ************************************************************/
53186

@@ -4666,6 +4799,7 @@ groupby(iterable[, keyfunc]) --> sub-iterators grouped by value of keyfunc(v)\n\
46664799
filterfalse(pred, seq) --> elements of seq where pred(elem) is False\n\
46674800
islice(seq, [start,] stop [, step]) --> elements from\n\
46684801
seq[start:stop:step]\n\
4802+
pairwise(s) --> (s[0],s[1]), (s[1],s[2]), (s[2], s[3]), ...\n\
46694803
starmap(fun, seq) --> fun(*seq[0]), fun(*seq[1]), ...\n\
46704804
tee(it, n=2) --> (it1, it2 , ... itn) splits one iterator into n\n\
46714805
takewhile(pred, seq) --> seq[0], seq[1], until pred fails\n\
@@ -4695,6 +4829,7 @@ itertoolsmodule_exec(PyObject *m)
46954829
&filterfalse_type,
46964830
&count_type,
46974831
&ziplongest_type,
4832+
&pairwise_type,
46984833
&permutations_type,
46994834
&product_type,
47004835
&repeat_type,

0 commit comments

Comments
 (0)