https://github.com/python/cpython/commit/6d3127070296d35f292b6f856825cdbf72a18b7c
commit: 6d3127070296d35f292b6f856825cdbf72a18b7c
branch: main
author: Cody Maloney <[email protected]>
committer: cmaloney <[email protected]>
date: 2026-11-03T10:42:33-08:00
summary:
gh-87613: AC VC add support for *args and use it for zip (#160944)
`@vectorcall` accepted `*args`, but the generated code expected an
args tuple. Use fastcall form to parse the args array.
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 <[email protected]>
Co-authored-by: Stan Ulbrych <[email protected]>
files:
M Lib/test/clinic.test.c
M Python/bltinmodule.c
M Python/clinic/bltinmodule.c.h
M Tools/clinic/libclinic/parse_args.py
diff --git a/Lib/test/clinic.test.c b/Lib/test/clinic.test.c
index 86b8f81776dfc3e..3b59db65f23b6f6 100644
--- a/Lib/test/clinic.test.c
+++ b/Lib/test/clinic.test.c
@@ -5220,6 +5220,7 @@ Test___init___impl(TestObj *self, PyObject *args)
/*[clinic end generated code: output=0e5836c40dbc2397 input=a615a4485c0fc3e2]*/
/*[clinic input]
+@vectorcall
@classmethod
Test.__new__
*args: tuple
@@ -5261,9 +5262,37 @@ Test(PyTypeObject *type, PyObject *args, PyObject
*kwargs)
return return_value;
}
+static PyObject *
+Test_vectorcall(PyObject *type, PyObject *const *args,
+ size_t nargsf, PyObject *kwnames)
+{
+ PyObject *return_value = NULL;
+ Py_ssize_t nargs = PyVectorcall_NARGS(nargsf);
+ PyObject *__clinic_args = NULL;
+
+ assert(_PyType_CAST(type)->tp_new == Test);
+ /* Make sure the type object is immutable: the generated
+ * vectorcall doesn't deal e.g. with users reassigning __init__. */
+ assert(PyType_HasFeature(_PyType_CAST(type), Py_TPFLAGS_IMMUTABLETYPE));
+ if (!_PyArg_NoKwnames("Test", kwnames)) {
+ goto exit;
+ }
+ __clinic_args = PyTuple_FromArray(args, nargs);
+ if (__clinic_args == NULL) {
+ goto exit;
+ }
+ return_value = Test_impl(_PyType_CAST(type), __clinic_args);
+
+exit:
+ /* Cleanup for args */
+ Py_XDECREF(__clinic_args);
+
+ return return_value;
+}
+
static PyObject *
Test_impl(PyTypeObject *type, PyObject *args)
-/*[clinic end generated code: output=e6fba0c8951882fd input=8ce30adb836aeacb]*/
+/*[clinic end generated code: output=07f68e3971ed24cc input=1dc824fd6cec5f8e]*/
/*[clinic input]
diff --git a/Python/bltinmodule.c b/Python/bltinmodule.c
index 5413e57754667fa..b04715ef30b58cc 100644
--- a/Python/bltinmodule.c
+++ b/Python/bltinmodule.c
@@ -3139,41 +3139,45 @@ typedef struct {
#define _zipobject_CAST(op) ((zipobject *)(op))
+/*[clinic input]
+class zip "zipobject *" "&PyZip_Type"
+@vectorcall
+@classmethod
+zip.__new__ as zip_new
+
+ *iterables: array
+ strict: bool = False
+
+Iterate over several iterables in parallel, yielding tuples.
+
+The zip object yields n-length tuples, where n is the number of
+iterables passed as positional arguments to zip(). The i-th element
+in every tuple comes from the i-th iterable argument to zip(). This
+continues until the shortest argument is exhausted.
+
+If strict is true and one of the arguments is exhausted before the
+others, raise a ValueError.
+
+ >>> list(zip('abcdefg', range(3), range(4)))
+ [('a', 0, 0), ('b', 1, 1), ('c', 2, 2)]
+[clinic start generated code]*/
+
static PyObject *
-zip_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
+zip_new_impl(PyTypeObject *type, PyObject * const *iterables,
+ Py_ssize_t iterables_length, int strict)
+/*[clinic end generated code: output=5e1a30829ce1f6ab input=4bd8e6a24e4bfb2e]*/
{
zipobject *lz;
Py_ssize_t i;
PyObject *ittuple; /* tuple of iterators */
PyObject *result;
- Py_ssize_t tuplesize;
- int strict = 0;
-
- if (kwds) {
- PyObject *empty = PyTuple_New(0);
- if (empty == NULL) {
- return NULL;
- }
- static char *kwlist[] = {"strict", NULL};
- int parsed = PyArg_ParseTupleAndKeywords(
- empty, kwds, "|$p:zip", kwlist, &strict);
- Py_DECREF(empty);
- if (!parsed) {
- return NULL;
- }
- }
-
- /* args must be a tuple */
- assert(PyTuple_Check(args));
- tuplesize = PyTuple_GET_SIZE(args);
/* obtain iterators */
- ittuple = PyTuple_New(tuplesize);
+ ittuple = PyTuple_New(iterables_length);
if (ittuple == NULL)
return NULL;
- for (i=0; i < tuplesize; ++i) {
- PyObject *item = PyTuple_GET_ITEM(args, i);
- PyObject *it = PyObject_GetIter(item);
+ for (i=0; i < iterables_length; ++i) {
+ PyObject *it = PyObject_GetIter(iterables[i]);
if (it == NULL) {
Py_DECREF(ittuple);
return NULL;
@@ -3182,12 +3186,12 @@ zip_new(PyTypeObject *type, PyObject *args, PyObject
*kwds)
}
/* create a result holder */
- result = PyTuple_New(tuplesize);
+ result = PyTuple_New(iterables_length);
if (result == NULL) {
Py_DECREF(ittuple);
return NULL;
}
- for (i=0 ; i < tuplesize ; i++) {
+ for (i=0 ; i < iterables_length ; i++) {
PyTuple_SET_ITEM(result, i, Py_NewRef(Py_None));
}
@@ -3199,7 +3203,7 @@ zip_new(PyTypeObject *type, PyObject *args, PyObject
*kwds)
return NULL;
}
lz->ittuple = ittuple;
- lz->tuplesize = tuplesize;
+ lz->tuplesize = iterables_length;
lz->result = result;
lz->strict = strict;
@@ -3345,21 +3349,6 @@ static PyMethodDef zip_methods[] = {
{NULL} /* sentinel */
};
-PyDoc_STRVAR(zip_doc,
-"zip(*iterables, strict=False)\n\
---\n\
-\n\
-The zip object yields n-length tuples, where n is the number of\n\
-iterables passed as positional arguments to zip(). The i-th element\n\
-in every tuple comes from the i-th iterable argument to zip(). This\n\
-continues until the shortest argument is exhausted.\n\
-\n\
-If strict is true and one of the arguments is exhausted before the\n\
-others, raise a ValueError.\n\
-\n\
- >>> list(zip('abcdefg', range(3), range(4)))\n\
- [('a', 0, 0), ('b', 1, 1), ('c', 2, 2)]");
-
PyTypeObject PyZip_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
"zip", /* tp_name */
@@ -3383,7 +3372,7 @@ PyTypeObject PyZip_Type = {
0, /* tp_as_buffer */
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC |
Py_TPFLAGS_BASETYPE, /* tp_flags */
- zip_doc, /* tp_doc */
+ zip_new__doc__, /* tp_doc */
zip_traverse, /* tp_traverse */
0, /* tp_clear */
0, /* tp_richcompare */
@@ -3402,6 +3391,7 @@ PyTypeObject PyZip_Type = {
PyType_GenericAlloc, /* tp_alloc */
zip_new, /* tp_new */
PyObject_GC_Del, /* tp_free */
+ .tp_vectorcall = zip_vectorcall,
};
diff --git a/Python/clinic/bltinmodule.c.h b/Python/clinic/bltinmodule.c.h
index bdb3dd560b39b8b..09ae184445f7d5d 100644
--- a/Python/clinic/bltinmodule.c.h
+++ b/Python/clinic/bltinmodule.c.h
@@ -1495,4 +1495,109 @@ builtin_issubclass(PyObject *module, PyObject *const
*args, Py_ssize_t nargs)
exit:
return return_value;
}
-/*[clinic end generated code: output=1dacff294aaa7485 input=a9049054013a1b77]*/
+
+PyDoc_STRVAR(zip_new__doc__,
+"zip(*iterables, strict=False)\n"
+"--\n"
+"\n"
+"Iterate over several iterables in parallel, yielding tuples.\n"
+"\n"
+"The zip object yields n-length tuples, where n is the number of\n"
+"iterables passed as positional arguments to zip(). The i-th element\n"
+"in every tuple comes from the i-th iterable argument to zip(). This\n"
+"continues until the shortest argument is exhausted.\n"
+"\n"
+"If strict is true and one of the arguments is exhausted before the\n"
+"others, raise a ValueError.\n"
+"\n"
+" >>> list(zip(\'abcdefg\', range(3), range(4)))\n"
+" [(\'a\', 0, 0), (\'b\', 1, 1), (\'c\', 2, 2)]");
+
+static PyObject *
+zip_new_impl(PyTypeObject *type, PyObject * const *iterables,
+ Py_ssize_t iterables_length, int strict);
+
+static PyObject *
+zip_new_helper(PyTypeObject *type, PyObject *const *args,
+ Py_ssize_t nargs, Py_ssize_t nkw, PyObject *kwargs, PyObject *kwnames)
+{
+ PyObject *return_value = NULL;
+ #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE)
+
+ #define NUM_KEYWORDS 1
+ static struct {
+ PyGC_Head _this_is_not_used;
+ PyObject_VAR_HEAD
+ Py_hash_t ob_hash;
+ PyObject *ob_item[NUM_KEYWORDS];
+ } _kwtuple = {
+ .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS)
+ .ob_hash = -1,
+ .ob_item = { &_Py_ID(strict), },
+ };
+ #undef NUM_KEYWORDS
+ #define KWTUPLE (&_kwtuple.ob_base.ob_base)
+
+ #else // !Py_BUILD_CORE
+ # define KWTUPLE NULL
+ #endif // !Py_BUILD_CORE
+
+ static const char * const _keywords[] = {"strict", NULL};
+ static _PyArg_Parser _parser = {
+ .keywords = _keywords,
+ .fname = "zip",
+ .kwtuple = KWTUPLE,
+ };
+ #undef KWTUPLE
+ PyObject *argsbuf[1];
+ PyObject * const *fastargs;
+ Py_ssize_t noptargs = 0 + nkw - 0;
+ PyObject * const *iterables;
+ Py_ssize_t iterables_length;
+ int strict = 0;
+
+ fastargs = _PyArg_UnpackKeywords(args, nargs, kwargs, kwnames, &_parser,
+ /*minpos*/ 0, /*maxpos*/ 0, /*minkw*/ 0, /*varpos*/ 1, argsbuf);
+ if (!fastargs) {
+ goto exit;
+ }
+ if (!noptargs) {
+ goto skip_optional_kwonly;
+ }
+ strict = PyObject_IsTrue(fastargs[0]);
+ if (strict < 0) {
+ goto exit;
+ }
+skip_optional_kwonly:
+ iterables = args;
+ iterables_length = nargs;
+ return_value = zip_new_impl(type, iterables, iterables_length, strict);
+
+exit:
+ return return_value;
+}
+
+static PyObject *
+zip_new(PyTypeObject *type, PyObject *args, PyObject *kwargs)
+{
+ return zip_new_helper(type, _PyTuple_CAST(args)->ob_item,
+ PyTuple_GET_SIZE(args),
+ kwargs ? PyDict_GET_SIZE(kwargs) : 0,
+ kwargs, NULL);
+}
+
+static PyObject *
+zip_vectorcall(PyObject *type, PyObject *const *args,
+ size_t nargsf, PyObject *kwnames)
+{
+ Py_ssize_t nargs = PyVectorcall_NARGS(nargsf);
+
+ assert(_PyType_CAST(type)->tp_new == zip_new);
+ /* Make sure the type object is immutable: the generated
+ * vectorcall doesn't deal e.g. with users reassigning __init__. */
+ assert(PyType_HasFeature(_PyType_CAST(type), Py_TPFLAGS_IMMUTABLETYPE));
+ return zip_new_helper(_PyType_CAST(type), args, nargs,
+ kwnames ? PyTuple_GET_SIZE(kwnames) : 0,
+ NULL, kwnames);
+}
+/*[clinic end generated code: output=ef3ea633d4ed8708 input=a9049054013a1b77]*/
diff --git a/Tools/clinic/libclinic/parse_args.py
b/Tools/clinic/libclinic/parse_args.py
index 703fa4e94ad5bfa..0d62e230b4a56fe 100644
--- a/Tools/clinic/libclinic/parse_args.py
+++ b/Tools/clinic/libclinic/parse_args.py
@@ -923,14 +923,14 @@ def render_option_groups_parse_tuple(
parser_code.append(" }}")
return parser_code
- def _parse_vararg(self) -> str:
+ def _parse_vararg(self, *, fastcall: bool) -> str:
assert self.varpos is not None
c = self.varpos.converter
assert isinstance(c, libclinic.converters.VarPosCConverter)
return c.parse_vararg(pos_only=self.pos_only,
min_pos=self.min_pos,
max_pos=self.max_pos,
- fastcall=self.fastcall,
+ fastcall=fastcall,
limited_capi=self.limited_capi)
def _parse_kwarg(self) -> str:
@@ -963,6 +963,7 @@ def _parse_positional_args(
argname_fmt: str,
nargs: str,
limited_capi: bool,
+ fastcall: bool,
) -> list[str] | None:
"""Emit per-parameter positional argument parsing.
@@ -995,8 +996,8 @@ def _parse_positional_args(
if self.min_pos < len(self.parameters):
parser_code.append("skip_optional:")
if self.varpos:
-
parser_code.append(libclinic.normalize_snippet(self._parse_vararg(),
- indent=4))
+ parser_code.append(libclinic.normalize_snippet(
+ self._parse_vararg(fastcall=fastcall), indent=4))
elif self.var_keyword:
parser_code.append(libclinic.normalize_snippet(self._parse_kwarg(),
indent=4))
@@ -1090,7 +1091,7 @@ def parse_pos_only(self) -> None:
pos_code = self._parse_positional_args(
argname_fmt=argname_fmt, nargs=nargs,
- limited_capi=self.limited_capi)
+ limited_capi=self.limited_capi, fastcall=self.fastcall)
if pos_code is not None:
parser_code.extend(pos_code)
else:
@@ -1148,7 +1149,8 @@ def parse_var_keyword(self) -> None:
parser_code.append("skip_optional:")
if self.varpos:
-
parser_code.append(libclinic.normalize_snippet(self._parse_vararg(), indent=4))
+ parser_code.append(libclinic.normalize_snippet(
+ self._parse_vararg(fastcall=self.fastcall), indent=4))
if self.var_keyword:
parser_code.append(libclinic.normalize_snippet(self._parse_kwarg(), indent=4))
self.parser_body(*parser_code)
@@ -1376,7 +1378,12 @@ def parse_general(self, clang: CLanguage) -> None:
if add_label:
parser_code.append("%s:" % add_label)
if self.varpos:
-
parser_code.append(libclinic.normalize_snippet(self._parse_vararg(), indent=4))
+ # The vectorcall helper gets an array of arguments, not a
+ # tuple: use the converters' fastcall convention, which
+ # handles that.
+ fastcall = True if self.func.vectorcall else self.fastcall
+ parser_code.append(libclinic.normalize_snippet(
+ self._parse_vararg(fastcall=fastcall), indent=4))
else:
self.use_converters()
@@ -1617,8 +1624,11 @@ def _vectorcall_positional(self, *,
arity_checked: Already have a number of arguments check.
"""
+ # The vectorcall gets an array of arguments, not a tuple: use the
+ # converters' fastcall convention, which handles that.
pos_code = self._parse_positional_args(
- argname_fmt='args[%d]', nargs='nargs', limited_capi=False)
+ argname_fmt='args[%d]', nargs='nargs', limited_capi=False,
+ fastcall=True)
# Converter support was validated when @vectorcall was parsed.
assert pos_code is not None
if arity_checked:
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]