Skip to content

Commit fdcb49c

Browse files
authored
gh-104066: Improve performance of hasattr for module objects (#104063)
1 parent c9ecd3e commit fdcb49c

File tree

4 files changed

+78
-29
lines changed

4 files changed

+78
-29
lines changed

Include/internal/pycore_moduleobject.h

+3
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ static inline PyObject* _PyModule_GetDict(PyObject *mod) {
3636
return dict;
3737
}
3838

39+
PyObject* _Py_module_getattro_impl(PyModuleObject *m, PyObject *name, int suppress);
40+
PyObject* _Py_module_getattro(PyModuleObject *m, PyObject *name);
41+
3942
#ifdef __cplusplus
4043
}
4144
#endif
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Improve the performance of :func:`hasattr` for module objects with a missing
2+
attribute.

Objects/moduleobject.c

+62-29
Original file line numberDiff line numberDiff line change
@@ -702,7 +702,11 @@ int
702702
_PyModuleSpec_IsInitializing(PyObject *spec)
703703
{
704704
if (spec != NULL) {
705-
PyObject *value = PyObject_GetAttr(spec, &_Py_ID(_initializing));
705+
PyObject *value;
706+
int ok = _PyObject_LookupAttr(spec, &_Py_ID(_initializing), &value);
707+
if (ok == 0) {
708+
return 0;
709+
}
706710
if (value != NULL) {
707711
int initializing = PyObject_IsTrue(value);
708712
Py_DECREF(value);
@@ -738,19 +742,37 @@ _PyModuleSpec_IsUninitializedSubmodule(PyObject *spec, PyObject *name)
738742
return is_uninitialized;
739743
}
740744

741-
static PyObject*
742-
module_getattro(PyModuleObject *m, PyObject *name)
745+
PyObject*
746+
_Py_module_getattro_impl(PyModuleObject *m, PyObject *name, int suppress)
743747
{
748+
// When suppress=1, this function suppresses AttributeError.
744749
PyObject *attr, *mod_name, *getattr;
745-
attr = PyObject_GenericGetAttr((PyObject *)m, name);
746-
if (attr || !PyErr_ExceptionMatches(PyExc_AttributeError)) {
750+
attr = _PyObject_GenericGetAttrWithDict((PyObject *)m, name, NULL, suppress);
751+
if (attr) {
747752
return attr;
748753
}
749-
PyErr_Clear();
754+
if (suppress == 1) {
755+
if (PyErr_Occurred()) {
756+
// pass up non-AttributeError exception
757+
return NULL;
758+
}
759+
}
760+
else {
761+
if (!PyErr_ExceptionMatches(PyExc_AttributeError)) {
762+
// pass up non-AttributeError exception
763+
return NULL;
764+
}
765+
PyErr_Clear();
766+
}
750767
assert(m->md_dict != NULL);
751768
getattr = PyDict_GetItemWithError(m->md_dict, &_Py_ID(__getattr__));
752769
if (getattr) {
753-
return PyObject_CallOneArg(getattr, name);
770+
PyObject *result = PyObject_CallOneArg(getattr, name);
771+
if (result == NULL && suppress == 1 && PyErr_ExceptionMatches(PyExc_AttributeError)) {
772+
// suppress AttributeError
773+
PyErr_Clear();
774+
}
775+
return result;
754776
}
755777
if (PyErr_Occurred()) {
756778
return NULL;
@@ -763,37 +785,48 @@ module_getattro(PyModuleObject *m, PyObject *name)
763785
Py_DECREF(mod_name);
764786
return NULL;
765787
}
766-
Py_XINCREF(spec);
767-
if (_PyModuleSpec_IsInitializing(spec)) {
768-
PyErr_Format(PyExc_AttributeError,
769-
"partially initialized "
770-
"module '%U' has no attribute '%U' "
771-
"(most likely due to a circular import)",
772-
mod_name, name);
773-
}
774-
else if (_PyModuleSpec_IsUninitializedSubmodule(spec, name)) {
775-
PyErr_Format(PyExc_AttributeError,
776-
"cannot access submodule '%U' of module '%U' "
777-
"(most likely due to a circular import)",
778-
name, mod_name);
779-
}
780-
else {
781-
PyErr_Format(PyExc_AttributeError,
782-
"module '%U' has no attribute '%U'",
783-
mod_name, name);
788+
if (suppress != 1) {
789+
Py_XINCREF(spec);
790+
if (_PyModuleSpec_IsInitializing(spec)) {
791+
PyErr_Format(PyExc_AttributeError,
792+
"partially initialized "
793+
"module '%U' has no attribute '%U' "
794+
"(most likely due to a circular import)",
795+
mod_name, name);
796+
}
797+
else if (_PyModuleSpec_IsUninitializedSubmodule(spec, name)) {
798+
PyErr_Format(PyExc_AttributeError,
799+
"cannot access submodule '%U' of module '%U' "
800+
"(most likely due to a circular import)",
801+
name, mod_name);
802+
}
803+
else {
804+
PyErr_Format(PyExc_AttributeError,
805+
"module '%U' has no attribute '%U'",
806+
mod_name, name);
807+
}
808+
Py_XDECREF(spec);
784809
}
785-
Py_XDECREF(spec);
786810
Py_DECREF(mod_name);
787811
return NULL;
788812
}
789813
else if (PyErr_Occurred()) {
790814
return NULL;
791815
}
792-
PyErr_Format(PyExc_AttributeError,
793-
"module has no attribute '%U'", name);
816+
if (suppress != 1) {
817+
PyErr_Format(PyExc_AttributeError,
818+
"module has no attribute '%U'", name);
819+
}
794820
return NULL;
795821
}
796822

823+
824+
PyObject*
825+
_Py_module_getattro(PyModuleObject *m, PyObject *name)
826+
{
827+
return _Py_module_getattro_impl(m, name, 0);
828+
}
829+
797830
static int
798831
module_traverse(PyModuleObject *m, visitproc visit, void *arg)
799832
{
@@ -951,7 +984,7 @@ PyTypeObject PyModule_Type = {
951984
0, /* tp_hash */
952985
0, /* tp_call */
953986
0, /* tp_str */
954-
(getattrofunc)module_getattro, /* tp_getattro */
987+
(getattrofunc)_Py_module_getattro, /* tp_getattro */
955988
PyObject_GenericSetAttr, /* tp_setattro */
956989
0, /* tp_as_buffer */
957990
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC |

Objects/object.c

+11
Original file line numberDiff line numberDiff line change
@@ -1085,6 +1085,17 @@ _PyObject_LookupAttr(PyObject *v, PyObject *name, PyObject **result)
10851085
return 0;
10861086
}
10871087
}
1088+
else if (tp->tp_getattro == (getattrofunc)_Py_module_getattro) {
1089+
// optimization: suppress attribute error from module getattro method
1090+
*result = _Py_module_getattro_impl((PyModuleObject*)v, name, 1);
1091+
if (*result != NULL) {
1092+
return 1;
1093+
}
1094+
if (PyErr_Occurred()) {
1095+
return -1;
1096+
}
1097+
return 0;
1098+
}
10881099
else if (tp->tp_getattro != NULL) {
10891100
*result = (*tp->tp_getattro)(v, name);
10901101
}

0 commit comments

Comments
 (0)