Skip to content

Commit 33a4871

Browse files
cmaloneyclaude
andcommitted
gh-87613: Use Argument Clinic vectorcall for zip
zip had no vectorcall, so every call built an args tuple and a kwargs dict, and strict=True also went through PyArg_ParseTupleAndKeywords with a temporary empty tuple. Argument error messages are unchanged except for unknown keywords: `zip(strict=1, x=2)` now reports "got an unexpected keyword argument 'x'" instead of "takes at most 1 keyword argument (2 given)". The docstring gains a summary line; `__text_signature__` is unchanged. pyperf microbenchmark (PGO+LTO, run serially): | Benchmark | base | zip | |------------------------|:-------:|:---------------------:| | zip(a, b, strict=True) | 118 ns | 61.5 ns: 1.92x faster | | zip(a, b) | 64.9 ns | 53.1 ns: 1.22x faster | | zip(a) | 55.0 ns | 47.1 ns: 1.17x faster | | list(zip(a, b)) | 123 ns | 113 ns: 1.09x faster | | zip() | 27.4 ns | 25.7 ns: 1.07x faster | | Sub(a, b) | 71.5 ns | 70.6 ns: 1.01x faster | Benchmark hidden because not significant (1): zip.`__new__` Benchmark code: ```python import pyperf runner = pyperf.Runner() setup = "a = (1, 2, 3); b = [4, 5, 6]" for stmt in ["zip()", "zip(a)", "zip(a, b)", "zip(a, b, strict=True)", "list(zip(a, b))"]: runner.timeit(stmt, stmt, setup) runner.timeit('zip.`__new__`', 'new(zip, a, b)', setup + '; new = zip.__new__') runner.timeit('Sub(a, b)', 'S(a, b)', setup + '\nclass S(zip): pass') ``` Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent c121f81 commit 33a4871

2 files changed

Lines changed: 137 additions & 40 deletions

File tree

‎Python/bltinmodule.c‎

Lines changed: 31 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3139,41 +3139,47 @@ typedef struct {
31393139

31403140
#define _zipobject_CAST(op) ((zipobject *)(op))
31413141

3142+
/*[clinic input]
3143+
class zip "zipobject *" "&PyZip_Type"
3144+
@vectorcall
3145+
@classmethod
3146+
zip.__new__ as zip_new
3147+
3148+
*iterables: array
3149+
strict: bool = False
3150+
3151+
Iterate over several iterables in parallel, yielding tuples.
3152+
3153+
The zip object yields n-length tuples, where n is the number of
3154+
iterables passed as positional arguments to zip(). The i-th element
3155+
in every tuple comes from the i-th iterable argument to zip(). This
3156+
continues until the shortest argument is exhausted.
3157+
3158+
If strict is true and one of the arguments is exhausted before the
3159+
others, raise a ValueError.
3160+
3161+
>>> list(zip('abcdefg', range(3), range(4)))
3162+
[('a', 0, 0), ('b', 1, 1), ('c', 2, 2)]
3163+
[clinic start generated code]*/
3164+
31423165
static PyObject *
3143-
zip_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
3166+
zip_new_impl(PyTypeObject *type, PyObject * const *iterables,
3167+
Py_ssize_t iterables_length, int strict)
3168+
/*[clinic end generated code: output=5e1a30829ce1f6ab input=4bd8e6a24e4bfb2e]*/
3169+
31443170
{
31453171
zipobject *lz;
31463172
Py_ssize_t i;
31473173
PyObject *ittuple; /* tuple of iterators */
31483174
PyObject *result;
3149-
Py_ssize_t tuplesize;
3150-
int strict = 0;
3151-
3152-
if (kwds) {
3153-
PyObject *empty = PyTuple_New(0);
3154-
if (empty == NULL) {
3155-
return NULL;
3156-
}
3157-
static char *kwlist[] = {"strict", NULL};
3158-
int parsed = PyArg_ParseTupleAndKeywords(
3159-
empty, kwds, "|$p:zip", kwlist, &strict);
3160-
Py_DECREF(empty);
3161-
if (!parsed) {
3162-
return NULL;
3163-
}
3164-
}
3165-
3166-
/* args must be a tuple */
3167-
assert(PyTuple_Check(args));
3168-
tuplesize = PyTuple_GET_SIZE(args);
3175+
Py_ssize_t tuplesize = iterables_length;
31693176

31703177
/* obtain iterators */
31713178
ittuple = PyTuple_New(tuplesize);
31723179
if (ittuple == NULL)
31733180
return NULL;
31743181
for (i=0; i < tuplesize; ++i) {
3175-
PyObject *item = PyTuple_GET_ITEM(args, i);
3176-
PyObject *it = PyObject_GetIter(item);
3182+
PyObject *it = PyObject_GetIter(iterables[i]);
31773183
if (it == NULL) {
31783184
Py_DECREF(ittuple);
31793185
return NULL;
@@ -3345,21 +3351,6 @@ static PyMethodDef zip_methods[] = {
33453351
{NULL} /* sentinel */
33463352
};
33473353

3348-
PyDoc_STRVAR(zip_doc,
3349-
"zip(*iterables, strict=False)\n\
3350-
--\n\
3351-
\n\
3352-
The zip object yields n-length tuples, where n is the number of\n\
3353-
iterables passed as positional arguments to zip(). The i-th element\n\
3354-
in every tuple comes from the i-th iterable argument to zip(). This\n\
3355-
continues until the shortest argument is exhausted.\n\
3356-
\n\
3357-
If strict is true and one of the arguments is exhausted before the\n\
3358-
others, raise a ValueError.\n\
3359-
\n\
3360-
>>> list(zip('abcdefg', range(3), range(4)))\n\
3361-
[('a', 0, 0), ('b', 1, 1), ('c', 2, 2)]");
3362-
33633354
PyTypeObject PyZip_Type = {
33643355
PyVarObject_HEAD_INIT(&PyType_Type, 0)
33653356
"zip", /* tp_name */
@@ -3383,7 +3374,7 @@ PyTypeObject PyZip_Type = {
33833374
0, /* tp_as_buffer */
33843375
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC |
33853376
Py_TPFLAGS_BASETYPE, /* tp_flags */
3386-
zip_doc, /* tp_doc */
3377+
zip_new__doc__, /* tp_doc */
33873378
zip_traverse, /* tp_traverse */
33883379
0, /* tp_clear */
33893380
0, /* tp_richcompare */
@@ -3402,6 +3393,7 @@ PyTypeObject PyZip_Type = {
34023393
PyType_GenericAlloc, /* tp_alloc */
34033394
zip_new, /* tp_new */
34043395
PyObject_GC_Del, /* tp_free */
3396+
.tp_vectorcall = zip_vectorcall,
34053397
};
34063398

34073399

‎Python/clinic/bltinmodule.c.h‎

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

0 commit comments

Comments
 (0)