Skip to content

Commit 3f5fc70

Browse files
pablogsalrhettinger
authored andcommitted
bpo-32492: 1.6x speed up in namedtuple attribute access using C fast-path (python#10495)
* bpo-32492: 2.5x speed up in namedtuple attribute access using C fast path * Add News entry * fixup! bpo-32492: 2.5x speed up in namedtuple attribute access using C fast path * Check for tuple in the __get__ of the new descriptor and don't cache the descriptor itself * Don't inherit from property. Implement GC methods to handle __doc__ * Add a test for the docstring substitution in descriptors * Update NEWS entry to reflect time against 3.7 branch * Simplify implementation with argument clinic, better error messages, only __new__ * Use positional-only parameters for the __new__ * Use PyTuple_GET_SIZE and PyTuple_GET_ITEM to tighter the implementation of tuplegetterdescr_get * Implement __set__ to make tuplegetter a data descriptor * Use Py_INCREF now that we inline PyTuple_GetItem * Apply the valid_index() function, saving one test * Move Py_None test out of the critical path.
1 parent b0a6196 commit 3f5fc70

File tree

5 files changed

+211
-4
lines changed

5 files changed

+211
-4
lines changed

Lib/collections/__init__.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,11 @@ def __eq__(self, other):
311311
### namedtuple
312312
################################################################################
313313

314+
try:
315+
from _collections import _tuplegetter
316+
except ImportError:
317+
_tuplegetter = lambda index, doc: property(_itemgetter(index), doc=doc)
318+
314319
_nt_itemgetters = {}
315320

316321
def namedtuple(typename, field_names, *, rename=False, defaults=None, module=None):
@@ -454,12 +459,13 @@ def __getnewargs__(self):
454459
cache = _nt_itemgetters
455460
for index, name in enumerate(field_names):
456461
try:
457-
itemgetter_object, doc = cache[index]
462+
doc = cache[index]
458463
except KeyError:
459-
itemgetter_object = _itemgetter(index)
460464
doc = f'Alias for field number {index}'
461-
cache[index] = itemgetter_object, doc
462-
class_namespace[name] = property(itemgetter_object, doc=doc)
465+
cache[index] = doc
466+
467+
tuplegetter_object = _tuplegetter(index, doc)
468+
class_namespace[name] = tuplegetter_object
463469

464470
result = type(typename, (tuple,), class_namespace)
465471

Lib/test/test_collections.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,14 @@ class Point(namedtuple('_Point', ['x', 'y'])):
514514
a.w = 5
515515
self.assertEqual(a.__dict__, {'w': 5})
516516

517+
def test_namedtuple_can_mutate_doc_of_descriptors_independently(self):
518+
A = namedtuple('A', 'x y')
519+
B = namedtuple('B', 'x y')
520+
A.x.__doc__ = 'foo'
521+
B.x.__doc__ = 'bar'
522+
self.assertEqual(A.x.__doc__, 'foo')
523+
self.assertEqual(B.x.__doc__, 'bar')
524+
517525

518526
################################################################################
519527
### Abstract Base Classes
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Speed up :class:`namedtuple` attribute access by 1.6x using a C fast-path
2+
for the name descriptors. Patch by Pablo Galindo.

Modules/_collectionsmodule.c

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@
77
#include <sys/types.h> /* For size_t */
88
#endif
99

10+
/*[clinic input]
11+
class _tuplegetter "_tuplegetterobject *" "&tuplegetter_type"
12+
[clinic start generated code]*/
13+
/*[clinic end generated code: output=da39a3ee5e6b4b0d input=ee5ed5baabe35068]*/
14+
15+
static PyTypeObject tuplegetter_type;
16+
#include "clinic/_collectionsmodule.c.h"
17+
1018
/* collections module implementation of a deque() datatype
1119
Written and maintained by Raymond D. Hettinger <python@rcn.com>
1220
*/
@@ -2328,6 +2336,156 @@ _count_elements(PyObject *self, PyObject *args)
23282336
Py_RETURN_NONE;
23292337
}
23302338

2339+
/* Helper functions for namedtuples */
2340+
2341+
typedef struct {
2342+
PyObject_HEAD
2343+
Py_ssize_t index;
2344+
PyObject* doc;
2345+
} _tuplegetterobject;
2346+
2347+
/*[clinic input]
2348+
@classmethod
2349+
_tuplegetter.__new__ as tuplegetter_new
2350+
2351+
index: Py_ssize_t
2352+
doc: object
2353+
/
2354+
[clinic start generated code]*/
2355+
2356+
static PyObject *
2357+
tuplegetter_new_impl(PyTypeObject *type, Py_ssize_t index, PyObject *doc)
2358+
/*[clinic end generated code: output=014be444ad80263f input=87c576a5bdbc0bbb]*/
2359+
{
2360+
_tuplegetterobject* self;
2361+
self = (_tuplegetterobject *)type->tp_alloc(type, 0);
2362+
if (self == NULL) {
2363+
return NULL;
2364+
}
2365+
self->index = index;
2366+
Py_INCREF(doc);
2367+
self->doc = doc;
2368+
return (PyObject *)self;
2369+
}
2370+
2371+
static PyObject *
2372+
tuplegetterdescr_get(PyObject *self, PyObject *obj, PyObject *type)
2373+
{
2374+
PyObject *result;
2375+
if (obj == NULL) {
2376+
Py_INCREF(self);
2377+
return self;
2378+
}
2379+
if (!PyTuple_Check(obj)) {
2380+
if (obj == Py_None) {
2381+
Py_INCREF(self);
2382+
return self;
2383+
}
2384+
PyErr_Format(PyExc_TypeError,
2385+
"descriptor for index '%d' for tuple subclasses "
2386+
"doesn't apply to '%s' object",
2387+
((_tuplegetterobject*)self)->index,
2388+
obj->ob_type->tp_name);
2389+
return NULL;
2390+
}
2391+
2392+
Py_ssize_t index = ((_tuplegetterobject*)self)->index;
2393+
2394+
if (!valid_index(index, PyTuple_GET_SIZE(obj))) {
2395+
PyErr_SetString(PyExc_IndexError, "tuple index out of range");
2396+
return NULL;
2397+
}
2398+
2399+
result = PyTuple_GET_ITEM(obj, index);
2400+
Py_INCREF(result);
2401+
return result;
2402+
}
2403+
2404+
static int
2405+
tuplegetter_set(PyObject *self, PyObject *obj, PyObject *value)
2406+
{
2407+
if (value == NULL) {
2408+
PyErr_SetString(PyExc_AttributeError, "can't delete attribute");
2409+
} else {
2410+
PyErr_SetString(PyExc_AttributeError, "can't set attribute");
2411+
}
2412+
return -1;
2413+
}
2414+
2415+
static int
2416+
tuplegetter_traverse(PyObject *self, visitproc visit, void *arg)
2417+
{
2418+
_tuplegetterobject *tuplegetter = (_tuplegetterobject *)self;
2419+
Py_VISIT(tuplegetter->doc);
2420+
return 0;
2421+
}
2422+
2423+
static int
2424+
tuplegetter_clear(PyObject *self)
2425+
{
2426+
_tuplegetterobject *tuplegetter = (_tuplegetterobject *)self;
2427+
Py_CLEAR(tuplegetter->doc);
2428+
return 0;
2429+
}
2430+
2431+
static void
2432+
tuplegetter_dealloc(_tuplegetterobject *self)
2433+
{
2434+
PyObject_GC_UnTrack(self);
2435+
tuplegetter_clear((PyObject*)self);
2436+
Py_TYPE(self)->tp_free((PyObject*)self);
2437+
}
2438+
2439+
2440+
static PyMemberDef tuplegetter_members[] = {
2441+
{"__doc__", T_OBJECT, offsetof(_tuplegetterobject, doc), 0},
2442+
{0}
2443+
};
2444+
2445+
static PyTypeObject tuplegetter_type = {
2446+
PyVarObject_HEAD_INIT(NULL, 0)
2447+
"_collections._tuplegetter", /* tp_name */
2448+
sizeof(_tuplegetterobject), /* tp_basicsize */
2449+
0, /* tp_itemsize */
2450+
/* methods */
2451+
(destructor)tuplegetter_dealloc, /* tp_dealloc */
2452+
0, /* tp_print */
2453+
0, /* tp_getattr */
2454+
0, /* tp_setattr */
2455+
0, /* tp_reserved */
2456+
0, /* tp_repr */
2457+
0, /* tp_as_number */
2458+
0, /* tp_as_sequence */
2459+
0, /* tp_as_mapping */
2460+
0, /* tp_hash */
2461+
0, /* tp_call */
2462+
0, /* tp_str */
2463+
0, /* tp_getattro */
2464+
0, /* tp_setattro */
2465+
0, /* tp_as_buffer */
2466+
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC, /* tp_flags */
2467+
0, /* tp_doc */
2468+
(traverseproc)tuplegetter_traverse, /* tp_traverse */
2469+
(inquiry)tuplegetter_clear, /* tp_clear */
2470+
0, /* tp_richcompare */
2471+
0, /* tp_weaklistoffset */
2472+
0, /* tp_iter */
2473+
0, /* tp_iternext */
2474+
0, /* tp_methods */
2475+
tuplegetter_members, /* tp_members */
2476+
0, /* tp_getset */
2477+
0, /* tp_base */
2478+
0, /* tp_dict */
2479+
tuplegetterdescr_get, /* tp_descr_get */
2480+
tuplegetter_set, /* tp_descr_set */
2481+
0, /* tp_dictoffset */
2482+
0, /* tp_init */
2483+
0, /* tp_alloc */
2484+
tuplegetter_new, /* tp_new */
2485+
0,
2486+
};
2487+
2488+
23312489
/* module level code ********************************************************/
23322490

23332491
PyDoc_STRVAR(module_doc,
@@ -2386,5 +2544,10 @@ PyInit__collections(void)
23862544
Py_INCREF(&dequereviter_type);
23872545
PyModule_AddObject(m, "_deque_reverse_iterator", (PyObject *)&dequereviter_type);
23882546

2547+
if (PyType_Ready(&tuplegetter_type) < 0)
2548+
return NULL;
2549+
Py_INCREF(&tuplegetter_type);
2550+
PyModule_AddObject(m, "_tuplegetter", (PyObject *)&tuplegetter_type);
2551+
23892552
return m;
23902553
}

Modules/clinic/_collectionsmodule.c.h

Lines changed: 28 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)