2013-07-15 04:04:57 -04:00
|
|
|
PEP: 447
|
2014-07-25 09:57:34 -04:00
|
|
|
Title: Add __getdescriptor__ method to metaclass
|
2013-07-15 04:04:57 -04:00
|
|
|
Version: $Revision$
|
|
|
|
Last-Modified: $Date$
|
|
|
|
Author: Ronald Oussoren <ronaldoussoren@mac.com>
|
2019-03-26 16:49:29 -04:00
|
|
|
Status: Deferred
|
2013-07-15 04:04:57 -04:00
|
|
|
Type: Standards Track
|
|
|
|
Content-Type: text/x-rst
|
|
|
|
Created: 12-Jun-2013
|
2015-07-22 03:14:17 -04:00
|
|
|
Post-History: 2-Jul-2013, 15-Jul-2013, 29-Jul-2013, 22-Jul-2015
|
2013-07-15 04:04:57 -04:00
|
|
|
|
|
|
|
|
|
|
|
Abstract
|
|
|
|
========
|
|
|
|
|
2013-07-17 10:57:39 -04:00
|
|
|
Currently ``object.__getattribute__`` and ``super.__getattribute__`` peek
|
|
|
|
in the ``__dict__`` of classes on the MRO for a class when looking for
|
2014-07-25 09:57:34 -04:00
|
|
|
an attribute. This PEP adds an optional ``__getdescriptor__`` method to
|
2015-07-22 03:14:17 -04:00
|
|
|
a metaclass that replaces this behavior and gives more control over attribute
|
|
|
|
lookup, especially when using a `super`_ object.
|
2013-07-15 04:04:57 -04:00
|
|
|
|
2013-08-22 06:49:43 -04:00
|
|
|
That is, the MRO walking loop in ``_PyType_Lookup`` and
|
|
|
|
``super.__getattribute__`` gets changed from::
|
|
|
|
|
|
|
|
def lookup(mro_list, name):
|
|
|
|
for cls in mro_list:
|
|
|
|
if name in cls.__dict__:
|
|
|
|
return cls.__dict__
|
|
|
|
|
|
|
|
return NotFound
|
|
|
|
|
|
|
|
to::
|
|
|
|
|
|
|
|
def lookup(mro_list, name):
|
|
|
|
for cls in mro_list:
|
|
|
|
try:
|
2014-07-25 09:57:34 -04:00
|
|
|
return cls.__getdescriptor__(name)
|
2013-08-22 06:49:43 -04:00
|
|
|
except AttributeError:
|
|
|
|
pass
|
|
|
|
|
|
|
|
return NotFound
|
|
|
|
|
2015-07-22 03:14:17 -04:00
|
|
|
The default implemention of ``__getdescriptor__`` looks in the class
|
|
|
|
dictionary::
|
|
|
|
|
|
|
|
class type:
|
|
|
|
def __getdescriptor__(cls, name):
|
|
|
|
try:
|
2017-03-23 18:57:19 -04:00
|
|
|
return cls.__dict__[name]
|
|
|
|
except KeyError:
|
|
|
|
raise AttributeError(name) from None
|
2019-04-16 10:50:15 -04:00
|
|
|
|
|
|
|
|
2019-03-26 16:49:29 -04:00
|
|
|
PEP Status
|
|
|
|
==========
|
|
|
|
|
|
|
|
This PEP is deferred until someone has time to update this PEP and push it forward.
|
|
|
|
|
2013-08-22 06:49:43 -04:00
|
|
|
|
2013-07-15 04:04:57 -04:00
|
|
|
Rationale
|
|
|
|
=========
|
|
|
|
|
2013-07-17 10:57:39 -04:00
|
|
|
It is currently not possible to influence how the `super class`_ looks
|
|
|
|
up attributes (that is, ``super.__getattribute__`` unconditionally
|
|
|
|
peeks in the class ``__dict__``), and that can be problematic for
|
2015-07-25 07:17:34 -04:00
|
|
|
dynamic classes that can grow new methods on demand, for example dynamic
|
|
|
|
proxy classes.
|
2013-07-17 10:57:39 -04:00
|
|
|
|
2014-07-25 09:57:34 -04:00
|
|
|
The ``__getdescriptor__`` method makes it possible to dynamically add
|
2013-07-17 10:57:39 -04:00
|
|
|
attributes even when looking them up using the `super class`_.
|
|
|
|
|
|
|
|
The new method affects ``object.__getattribute__`` (and
|
2013-08-22 06:49:43 -04:00
|
|
|
`PyObject_GenericGetAttr`_) as well for consistency and to have a single
|
|
|
|
place to implement dynamic attribute resolution for classes.
|
2013-07-17 10:57:39 -04:00
|
|
|
|
|
|
|
Background
|
|
|
|
----------
|
2013-07-15 04:04:57 -04:00
|
|
|
|
2013-07-17 10:57:39 -04:00
|
|
|
The current behavior of ``super.__getattribute__`` causes problems for
|
|
|
|
classes that are dynamic proxies for other (non-Python) classes or types,
|
|
|
|
an example of which is `PyObjC`_. PyObjC creates a Python class for every
|
|
|
|
class in the Objective-C runtime, and looks up methods in the Objective-C
|
|
|
|
runtime when they are used. This works fine for normal access, but doesn't
|
2015-07-22 03:14:17 -04:00
|
|
|
work for access with `super`_ objects. Because of this PyObjC currently
|
|
|
|
includes a custom `super`_ that must be used with its classes, as well as
|
|
|
|
completely reimplementing `PyObject_GenericGetAttr`_ for normal attribute
|
|
|
|
access.
|
2013-07-17 10:57:39 -04:00
|
|
|
|
2015-07-22 03:14:17 -04:00
|
|
|
The API in this PEP makes it possible to remove the custom `super`_ and
|
2013-07-17 10:57:39 -04:00
|
|
|
simplifies the implementation because the custom lookup behavior can be
|
|
|
|
added in a central location.
|
2013-07-15 04:04:57 -04:00
|
|
|
|
2015-07-22 03:14:17 -04:00
|
|
|
.. note::
|
|
|
|
|
|
|
|
`PyObjC`_ cannot precalculate the contents of the class ``__dict__``
|
2016-07-11 11:14:08 -04:00
|
|
|
because Objective-C classes can grow new methods at runtime. Furthermore,
|
2015-07-22 03:14:17 -04:00
|
|
|
Objective-C classes tend to contain a lot of methods while most Python
|
|
|
|
code will only use a small subset of them, this makes precalculating
|
|
|
|
unnecessarily expensive.
|
|
|
|
|
2013-07-15 04:04:57 -04:00
|
|
|
|
|
|
|
The superclass attribute lookup hook
|
|
|
|
====================================
|
|
|
|
|
2013-07-15 05:23:29 -04:00
|
|
|
Both ``super.__getattribute__`` and ``object.__getattribute__`` (or
|
2013-08-22 06:49:43 -04:00
|
|
|
`PyObject_GenericGetAttr`_ and in particular ``_PyType_Lookup`` in C code)
|
|
|
|
walk an object's MRO and currently peek in the class' ``__dict__`` to look up
|
|
|
|
attributes.
|
|
|
|
|
|
|
|
With this proposal both lookup methods no longer peek in the class ``__dict__``
|
2014-07-25 09:57:34 -04:00
|
|
|
but call the special method ``__getdescriptor__``, which is a slot defined
|
2013-08-22 06:49:43 -04:00
|
|
|
on the metaclass. The default implementation of that method looks
|
|
|
|
up the name the class ``__dict__``, which means that attribute lookup is
|
|
|
|
unchanged unless a metatype actually defines the new special method.
|
2013-07-15 05:23:29 -04:00
|
|
|
|
2014-07-25 09:57:34 -04:00
|
|
|
Aside: Attribute resolution algorithm in Python
|
|
|
|
-----------------------------------------------
|
|
|
|
|
2016-07-11 11:14:08 -04:00
|
|
|
The attribute resolution process as implemented by ``object.__getattribute__``
|
2015-07-22 03:14:17 -04:00
|
|
|
(or PyObject_GenericGetAttr`` in CPython's implementation) is fairly
|
|
|
|
straightforward, but not entirely so without reading C code.
|
2014-07-25 09:57:34 -04:00
|
|
|
|
2016-07-11 11:14:08 -04:00
|
|
|
The current CPython implementation of object.__getattribute__ is basically
|
2015-07-25 07:17:34 -04:00
|
|
|
equivalent to the following (pseudo-) Python code (excluding some house
|
|
|
|
keeping and speed tricks)::
|
2014-07-25 09:57:34 -04:00
|
|
|
|
|
|
|
|
|
|
|
def _PyType_Lookup(tp, name):
|
|
|
|
mro = tp.mro()
|
2017-03-23 18:57:19 -04:00
|
|
|
assert isinstance(mro, tuple)
|
2014-07-25 09:57:34 -04:00
|
|
|
|
2017-03-23 18:57:19 -04:00
|
|
|
for base in mro:
|
|
|
|
assert isinstance(base, type)
|
2014-07-25 09:57:34 -04:00
|
|
|
|
2017-03-23 18:57:19 -04:00
|
|
|
# PEP 447 will change these lines:
|
|
|
|
try:
|
|
|
|
return base.__dict__[name]
|
|
|
|
except KeyError:
|
|
|
|
pass
|
2014-07-25 09:57:34 -04:00
|
|
|
|
2017-03-23 18:57:19 -04:00
|
|
|
return None
|
2014-07-25 09:57:34 -04:00
|
|
|
|
|
|
|
|
|
|
|
class object:
|
|
|
|
def __getattribute__(self, name):
|
2017-03-23 18:57:19 -04:00
|
|
|
assert isinstance(name, str)
|
2014-07-25 09:57:34 -04:00
|
|
|
|
2017-03-23 18:57:19 -04:00
|
|
|
tp = type(self)
|
|
|
|
descr = _PyType_Lookup(tp, name)
|
2014-07-25 09:57:34 -04:00
|
|
|
|
2017-03-23 18:57:19 -04:00
|
|
|
f = None
|
|
|
|
if descr is not None:
|
|
|
|
f = descr.__get__
|
|
|
|
if f is not None and descr.__set__ is not None:
|
|
|
|
# Data descriptor
|
|
|
|
return f(descr, self, type(self))
|
2014-07-25 09:57:34 -04:00
|
|
|
|
|
|
|
dict = self.__dict__
|
2017-03-23 18:57:19 -04:00
|
|
|
if dict is not None:
|
|
|
|
try:
|
|
|
|
return self.__dict__[name]
|
2014-07-25 09:57:34 -04:00
|
|
|
except KeyError:
|
2017-03-23 18:57:19 -04:00
|
|
|
pass
|
2014-07-25 09:57:34 -04:00
|
|
|
|
|
|
|
if f is not None:
|
2017-03-23 18:57:19 -04:00
|
|
|
# Non-data descriptor
|
|
|
|
return f(descr, self, type(self))
|
2014-07-25 09:57:34 -04:00
|
|
|
|
|
|
|
if descr is not None:
|
2017-03-23 18:57:19 -04:00
|
|
|
# Regular class attribute
|
|
|
|
return descr
|
2014-07-25 09:57:34 -04:00
|
|
|
|
|
|
|
raise AttributeError(name)
|
|
|
|
|
|
|
|
|
|
|
|
class super:
|
|
|
|
def __getattribute__(self, name):
|
2017-03-23 18:57:19 -04:00
|
|
|
assert isinstance(name, unicode)
|
2014-07-25 09:57:34 -04:00
|
|
|
|
2017-03-23 18:57:19 -04:00
|
|
|
if name != '__class__':
|
|
|
|
starttype = self.__self_type__
|
|
|
|
mro = startype.mro()
|
2014-07-25 09:57:34 -04:00
|
|
|
|
2017-03-23 18:57:19 -04:00
|
|
|
try:
|
|
|
|
idx = mro.index(self.__thisclass__)
|
2014-07-25 09:57:34 -04:00
|
|
|
|
2017-03-23 18:57:19 -04:00
|
|
|
except ValueError:
|
|
|
|
pass
|
2014-07-25 09:57:34 -04:00
|
|
|
|
2017-03-23 18:57:19 -04:00
|
|
|
else:
|
|
|
|
for base in mro[idx+1:]:
|
|
|
|
# PEP 447 will change these lines:
|
|
|
|
try:
|
|
|
|
descr = base.__dict__[name]
|
2014-07-25 09:57:34 -04:00
|
|
|
except KeyError:
|
2017-03-23 18:57:19 -04:00
|
|
|
continue
|
2014-07-25 09:57:34 -04:00
|
|
|
|
2017-03-23 18:57:19 -04:00
|
|
|
f = descr.__get__
|
|
|
|
if f is not None:
|
|
|
|
return f(descr,
|
|
|
|
None if (self.__self__ is self.__self_type__) else self.__self__,
|
|
|
|
starttype)
|
2014-07-25 09:57:34 -04:00
|
|
|
|
2017-03-23 18:57:19 -04:00
|
|
|
else:
|
|
|
|
return descr
|
2014-07-25 09:57:34 -04:00
|
|
|
|
2017-03-23 18:57:19 -04:00
|
|
|
return object.__getattribute__(self, name)
|
2014-07-25 09:57:34 -04:00
|
|
|
|
|
|
|
|
|
|
|
This PEP should change the dict lookup at the lines starting at "# PEP 447" with
|
2015-07-22 03:14:17 -04:00
|
|
|
a method call to perform the actual lookup, making is possible to affect that
|
|
|
|
lookup both for normal attribute access and access through the `super proxy`_.
|
2014-07-25 09:57:34 -04:00
|
|
|
|
|
|
|
Note that specific classes can already completely override the default
|
|
|
|
behaviour by implementing their own ``__getattribute__`` slot (with or without
|
|
|
|
calling the super class implementation).
|
|
|
|
|
|
|
|
|
2013-07-15 05:23:29 -04:00
|
|
|
In Python code
|
|
|
|
--------------
|
|
|
|
|
2014-07-25 09:57:34 -04:00
|
|
|
A meta type can define a method ``__getdescriptor__`` that is called during
|
2013-08-22 06:49:43 -04:00
|
|
|
attribute resolution by both ``super.__getattribute__``
|
|
|
|
and ``object.__getattribute``::
|
2013-07-15 05:23:29 -04:00
|
|
|
|
2013-07-15 12:37:26 -04:00
|
|
|
class MetaType(type):
|
2014-07-25 09:57:34 -04:00
|
|
|
def __getdescriptor__(cls, name):
|
2013-07-15 05:23:29 -04:00
|
|
|
try:
|
|
|
|
return cls.__dict__[name]
|
|
|
|
except KeyError:
|
|
|
|
raise AttributeError(name) from None
|
|
|
|
|
2014-07-25 09:57:34 -04:00
|
|
|
The ``__getdescriptor__`` method has as its arguments a class (which is an
|
2013-08-22 06:49:43 -04:00
|
|
|
instance of the meta type) and the name of the attribute that is looked up.
|
|
|
|
It should return the value of the attribute without invoking descriptors,
|
|
|
|
and should raise `AttributeError`_ when the name cannot be found.
|
2013-07-15 05:23:29 -04:00
|
|
|
|
2014-07-25 09:57:34 -04:00
|
|
|
The `type`_ class provides a default implementation for ``__getdescriptor__``,
|
2013-08-22 06:49:43 -04:00
|
|
|
that looks up the name in the class dictionary.
|
2013-07-17 10:57:39 -04:00
|
|
|
|
|
|
|
Example usage
|
|
|
|
.............
|
|
|
|
|
2013-08-22 06:49:43 -04:00
|
|
|
The code below implements a silly metaclass that redirects attribute lookup to
|
|
|
|
uppercase versions of names::
|
2013-07-17 10:57:39 -04:00
|
|
|
|
|
|
|
class UpperCaseAccess (type):
|
2014-07-25 09:57:34 -04:00
|
|
|
def __getdescriptor__(cls, name):
|
2017-03-23 18:57:19 -04:00
|
|
|
try:
|
2014-07-25 09:57:34 -04:00
|
|
|
return cls.__dict__[name.upper()]
|
2017-03-23 18:57:19 -04:00
|
|
|
except KeyError:
|
|
|
|
raise AttributeError(name) from None
|
2013-07-17 10:57:39 -04:00
|
|
|
|
|
|
|
class SillyObject (metaclass=UpperCaseAccess):
|
|
|
|
def m(self):
|
2013-08-22 06:49:43 -04:00
|
|
|
return 42
|
2013-07-17 10:57:39 -04:00
|
|
|
|
2013-08-22 06:49:43 -04:00
|
|
|
def M(self):
|
|
|
|
return "fourtytwo"
|
2013-07-17 10:57:39 -04:00
|
|
|
|
|
|
|
obj = SillyObject()
|
|
|
|
assert obj.m() == "fortytwo"
|
|
|
|
|
2015-07-22 03:14:17 -04:00
|
|
|
As mentioned earlier in this PEP a more realistic use case of this
|
|
|
|
functionallity is a ``__getdescriptor__`` method that dynamicly populates the
|
|
|
|
class ``__dict__`` based on attribute access, primarily when it is not
|
|
|
|
possible to reliably keep the class dict in sync with its source, for example
|
|
|
|
because the source used to populate ``__dict__`` is dynamic as well and does
|
|
|
|
not have triggers that can be used to detect changes to that source.
|
2014-07-25 09:57:34 -04:00
|
|
|
|
2015-07-22 03:14:17 -04:00
|
|
|
An example of that are the class bridges in PyObjC: the class bridge is a
|
|
|
|
Python object (class) that represents an Objective-C class and conceptually
|
|
|
|
has a Python method for every Objective-C method in the Objective-C class.
|
|
|
|
As with Python it is possible to add new methods to an Objective-C class, or
|
|
|
|
replace existing ones, and there are no callbacks that can be used to detect
|
|
|
|
this.
|
2013-07-15 05:23:29 -04:00
|
|
|
|
2013-07-15 04:04:57 -04:00
|
|
|
In C code
|
|
|
|
---------
|
|
|
|
|
2015-07-25 07:17:34 -04:00
|
|
|
A new type flag ``Py_TPFLAGS_GETDESCRIPTOR`` with value ``(1UL << 11)`` that
|
|
|
|
indicates that the new slot is present and to be used.
|
|
|
|
|
2014-07-25 09:57:34 -04:00
|
|
|
A new slot ``tp_getdescriptor`` is added to the ``PyTypeObject`` struct, this
|
|
|
|
slot corresponds to the ``__getdescriptor__`` method on `type`_.
|
2013-07-15 04:04:57 -04:00
|
|
|
|
|
|
|
The slot has the following prototype::
|
|
|
|
|
2014-07-25 09:57:34 -04:00
|
|
|
PyObject* (*getdescriptorfunc)(PyTypeObject* cls, PyObject* name);
|
2013-07-15 04:04:57 -04:00
|
|
|
|
2013-10-18 13:24:59 -04:00
|
|
|
This method should lookup *name* in the namespace of *cls*, without looking at
|
|
|
|
superclasses, and should not invoke descriptors. The method returns ``NULL``
|
|
|
|
without setting an exception when the *name* cannot be found, and returns a
|
2013-08-22 06:49:43 -04:00
|
|
|
new reference otherwise (not a borrowed reference).
|
2013-07-15 04:04:57 -04:00
|
|
|
|
2015-07-25 07:17:34 -04:00
|
|
|
Classes with a ``tp_getdescriptor`` slot must add ``Py_TPFLAGS_GETDESCRIPTOR``
|
|
|
|
to ``tp_flags`` to indicate that new slot must be used.
|
|
|
|
|
2013-07-29 08:42:49 -04:00
|
|
|
Use of this hook by the interpreter
|
|
|
|
-----------------------------------
|
2013-07-15 04:04:57 -04:00
|
|
|
|
2013-08-22 06:49:43 -04:00
|
|
|
The new method is required for metatypes and as such is defined on `type_`.
|
|
|
|
Both ``super.__getattribute__`` and
|
|
|
|
``object.__getattribute__``/`PyObject_GenericGetAttr`_
|
2014-07-25 09:57:34 -04:00
|
|
|
(through ``_PyType_Lookup``) use the this ``__getdescriptor__`` method when
|
2013-08-22 06:49:43 -04:00
|
|
|
walking the MRO.
|
2013-07-17 10:57:39 -04:00
|
|
|
|
|
|
|
Other changes to the implementation
|
2013-07-29 08:42:49 -04:00
|
|
|
-----------------------------------
|
2013-07-15 04:04:57 -04:00
|
|
|
|
2013-08-22 06:49:43 -04:00
|
|
|
The change for `PyObject_GenericGetAttr`_ will be done by changing the private
|
|
|
|
function ``_PyType_Lookup``. This currently returns a borrowed reference, but
|
2014-07-25 09:57:34 -04:00
|
|
|
must return a new reference when the ``__getdescriptor__`` method is present.
|
2013-08-22 06:49:43 -04:00
|
|
|
Because of this ``_PyType_Lookup`` will be renamed to ``_PyType_LookupName``,
|
|
|
|
this will cause compile-time errors for all out-of-tree users of this
|
|
|
|
private API.
|
2013-07-15 04:04:57 -04:00
|
|
|
|
2015-07-25 07:17:34 -04:00
|
|
|
For the same reason ``_PyType_LookupId`` is renamed to ``_PyType_LookupId2``.
|
|
|
|
A number of other functions in typeobject.c with the same issue do not get
|
|
|
|
an updated name because they are private to that file.
|
|
|
|
|
2013-08-22 06:49:43 -04:00
|
|
|
The attribute lookup cache in ``Objects/typeobject.c`` is disabled for classes
|
2014-07-25 09:57:34 -04:00
|
|
|
that have a metaclass that overrides ``__getdescriptor__``, because using the
|
2013-08-22 06:49:43 -04:00
|
|
|
cache might not be valid for such classes.
|
2013-07-15 04:04:57 -04:00
|
|
|
|
2014-07-27 05:44:17 -04:00
|
|
|
Impact of this PEP on introspection
|
2015-07-25 07:17:34 -04:00
|
|
|
===================================
|
2014-07-27 05:44:17 -04:00
|
|
|
|
|
|
|
Use of the method introduced in this PEP can affect introspection of classes
|
|
|
|
with a metaclass that uses a custom ``__getdescriptor__`` method. This section
|
|
|
|
lists those changes.
|
|
|
|
|
2015-07-22 03:14:17 -04:00
|
|
|
The items listed below are only affected by custom ``__getdescriptor__``
|
|
|
|
methods, the default implementation for ``object`` won't cause problems
|
|
|
|
because that still only uses the class ``__dict__`` and won't cause visible
|
|
|
|
changes to the visible behaviour of the ``object.__getattribute__``.
|
|
|
|
|
2014-07-27 05:44:17 -04:00
|
|
|
* ``dir`` might not show all attributes
|
|
|
|
|
2015-07-22 03:14:17 -04:00
|
|
|
As with a custom ``__getattribute__`` method `dir()`_ might not see all
|
2014-07-27 05:44:17 -04:00
|
|
|
(instance) attributes when using the ``__getdescriptor__()`` method to
|
|
|
|
dynamicly resolve attributes.
|
|
|
|
|
|
|
|
The solution for that is quite simple: classes using ``__getdescriptor__``
|
2015-07-22 03:14:17 -04:00
|
|
|
should also implement `__dir__()`_ if they want full support for the builtin
|
|
|
|
`dir()`_ function.
|
2014-07-27 05:44:17 -04:00
|
|
|
|
|
|
|
* ``inspect.getattr_static`` might not show all attributes
|
|
|
|
|
|
|
|
The function ``inspect.getattr_static`` intentionally does not invoke
|
|
|
|
``__getattribute__`` and descriptors to avoid invoking user code during
|
|
|
|
introspection with this function. The ``__getdescriptor__`` method will also
|
|
|
|
be ignored and is another way in which the result of ``inspect.getattr_static``
|
|
|
|
can be different from that of ``builtin.getattr``.
|
|
|
|
|
2015-07-25 07:17:34 -04:00
|
|
|
* ``inspect.getmembers`` and ``inspect.classify_class_attrs``
|
2014-07-27 05:44:17 -04:00
|
|
|
|
|
|
|
Both of these functions directly access the class __dict__ of classes along
|
|
|
|
the MRO, and hence can be affected by a custom ``__getdescriptor__`` method.
|
|
|
|
|
2015-07-25 07:17:34 -04:00
|
|
|
Code with a custom ``__getdescriptor__`` method that want to play nice with
|
|
|
|
these methods also needs to ensure that the ``__dict__`` is set up correctly
|
|
|
|
when that is accessed directly by Python code.
|
2015-07-22 03:14:17 -04:00
|
|
|
|
2015-07-25 07:17:34 -04:00
|
|
|
Note that ``inspect.getmembers`` is used by ``pydoc`` and hence this can
|
|
|
|
affect runtime documentation introspection.
|
2015-07-22 03:14:17 -04:00
|
|
|
|
|
|
|
* Direct introspection of the class ``__dict__``
|
|
|
|
|
|
|
|
Any code that directly access the class ``__dict__`` for introspection
|
2015-07-25 07:17:34 -04:00
|
|
|
can be affected by a custom ``__getdescriptor__`` method, see the previous
|
|
|
|
item.
|
2014-07-27 05:44:17 -04:00
|
|
|
|
|
|
|
|
2013-07-29 08:42:49 -04:00
|
|
|
Performance impact
|
2015-07-25 07:17:34 -04:00
|
|
|
==================
|
2013-07-29 08:42:49 -04:00
|
|
|
|
2015-07-22 03:14:17 -04:00
|
|
|
**WARNING**: The benchmark results in this section are old, and will be updated
|
|
|
|
when I've ported the patch to the current trunk. I don't expect significant
|
|
|
|
changes to the results in this section.
|
|
|
|
|
2015-07-25 07:17:34 -04:00
|
|
|
Micro benchmarks
|
|
|
|
----------------
|
|
|
|
|
|
|
|
`Issue 18181`_ has a micro benchmark as one of its attachments
|
|
|
|
(`pep447-micro-bench.py`_) that specifically tests the speed of attribute
|
|
|
|
lookup, both directly and through super.
|
|
|
|
|
|
|
|
Note that attribute lookup with deep class hierarchies is significantly slower
|
|
|
|
when using a custom ``__getdescriptor__`` method. This is because the
|
|
|
|
attribute lookup cache for CPython cannot be used when having this method.
|
|
|
|
|
|
|
|
Pybench
|
|
|
|
-------
|
|
|
|
|
2013-08-22 06:49:43 -04:00
|
|
|
The pybench output below compares an implementation of this PEP with the
|
|
|
|
regular source tree, both based on changeset a5681f50bae2, run on an idle
|
2016-05-03 06:52:22 -04:00
|
|
|
machine and Core i7 processor running Centos 6.4.
|
2013-07-29 08:42:49 -04:00
|
|
|
|
|
|
|
Even though the machine was idle there were clear differences between runs,
|
2013-10-18 13:24:59 -04:00
|
|
|
I've seen difference in "minimum time" vary from -0.1% to +1.5%, with similar
|
2013-07-29 08:42:49 -04:00
|
|
|
(but slightly smaller) differences in the "average time" difference.
|
|
|
|
|
2013-07-29 12:49:53 -04:00
|
|
|
::
|
2013-07-29 08:42:49 -04:00
|
|
|
|
2013-08-22 06:49:43 -04:00
|
|
|
-------------------------------------------------------------------------------
|
|
|
|
PYBENCH 2.1
|
|
|
|
-------------------------------------------------------------------------------
|
|
|
|
* using CPython 3.4.0a0 (default, Jul 29 2013, 13:01:34) [GCC 4.4.7 20120313 (Red Hat 4.4.7-3)]
|
|
|
|
* disabled garbage collection
|
|
|
|
* system check interval set to maximum: 2147483647
|
|
|
|
* using timer: time.perf_counter
|
|
|
|
* timer: resolution=1e-09, implementation=clock_gettime(CLOCK_MONOTONIC)
|
|
|
|
|
|
|
|
-------------------------------------------------------------------------------
|
|
|
|
Benchmark: pep447.pybench
|
|
|
|
-------------------------------------------------------------------------------
|
|
|
|
|
|
|
|
Rounds: 10
|
|
|
|
Warp: 10
|
|
|
|
Timer: time.perf_counter
|
|
|
|
|
|
|
|
Machine Details:
|
|
|
|
Platform ID: Linux-2.6.32-358.114.1.openstack.el6.x86_64-x86_64-with-centos-6.4-Final
|
|
|
|
Processor: x86_64
|
|
|
|
|
|
|
|
Python:
|
|
|
|
Implementation: CPython
|
|
|
|
Executable: /tmp/default-pep447/bin/python3
|
|
|
|
Version: 3.4.0a0
|
|
|
|
Compiler: GCC 4.4.7 20120313 (Red Hat 4.4.7-3)
|
|
|
|
Bits: 64bit
|
|
|
|
Build: Jul 29 2013 14:09:12 (#default)
|
|
|
|
Unicode: UCS4
|
|
|
|
|
|
|
|
|
|
|
|
-------------------------------------------------------------------------------
|
|
|
|
Comparing with: default.pybench
|
|
|
|
-------------------------------------------------------------------------------
|
|
|
|
|
|
|
|
Rounds: 10
|
|
|
|
Warp: 10
|
|
|
|
Timer: time.perf_counter
|
|
|
|
|
|
|
|
Machine Details:
|
|
|
|
Platform ID: Linux-2.6.32-358.114.1.openstack.el6.x86_64-x86_64-with-centos-6.4-Final
|
|
|
|
Processor: x86_64
|
|
|
|
|
|
|
|
Python:
|
|
|
|
Implementation: CPython
|
|
|
|
Executable: /tmp/default/bin/python3
|
|
|
|
Version: 3.4.0a0
|
|
|
|
Compiler: GCC 4.4.7 20120313 (Red Hat 4.4.7-3)
|
|
|
|
Bits: 64bit
|
|
|
|
Build: Jul 29 2013 13:01:34 (#default)
|
|
|
|
Unicode: UCS4
|
|
|
|
|
|
|
|
|
|
|
|
Test minimum run-time average run-time
|
|
|
|
this other diff this other diff
|
|
|
|
-------------------------------------------------------------------------------
|
|
|
|
BuiltinFunctionCalls: 45ms 44ms +1.3% 45ms 44ms +1.3%
|
|
|
|
BuiltinMethodLookup: 26ms 27ms -2.4% 27ms 27ms -2.2%
|
|
|
|
CompareFloats: 33ms 34ms -0.7% 33ms 34ms -1.1%
|
|
|
|
CompareFloatsIntegers: 66ms 67ms -0.9% 66ms 67ms -0.8%
|
|
|
|
CompareIntegers: 51ms 50ms +0.9% 51ms 50ms +0.8%
|
|
|
|
CompareInternedStrings: 34ms 33ms +0.4% 34ms 34ms -0.4%
|
|
|
|
CompareLongs: 29ms 29ms -0.1% 29ms 29ms -0.0%
|
|
|
|
CompareStrings: 43ms 44ms -1.8% 44ms 44ms -1.8%
|
|
|
|
ComplexPythonFunctionCalls: 44ms 42ms +3.9% 44ms 42ms +4.1%
|
|
|
|
ConcatStrings: 33ms 33ms -0.4% 33ms 33ms -1.0%
|
|
|
|
CreateInstances: 47ms 48ms -2.9% 47ms 49ms -3.4%
|
|
|
|
CreateNewInstances: 35ms 36ms -2.5% 36ms 36ms -2.5%
|
|
|
|
CreateStringsWithConcat: 69ms 70ms -0.7% 69ms 70ms -0.9%
|
|
|
|
DictCreation: 52ms 50ms +3.1% 52ms 50ms +3.0%
|
|
|
|
DictWithFloatKeys: 40ms 44ms -10.1% 43ms 45ms -5.8%
|
|
|
|
DictWithIntegerKeys: 32ms 36ms -11.2% 35ms 37ms -4.6%
|
|
|
|
DictWithStringKeys: 29ms 34ms -15.7% 35ms 40ms -11.0%
|
|
|
|
ForLoops: 30ms 29ms +2.2% 30ms 29ms +2.2%
|
|
|
|
IfThenElse: 38ms 41ms -6.7% 38ms 41ms -6.9%
|
|
|
|
ListSlicing: 36ms 36ms -0.7% 36ms 37ms -1.3%
|
|
|
|
NestedForLoops: 43ms 45ms -3.1% 43ms 45ms -3.2%
|
|
|
|
NestedListComprehensions: 39ms 40ms -1.7% 39ms 40ms -2.1%
|
|
|
|
NormalClassAttribute: 86ms 82ms +5.1% 86ms 82ms +5.0%
|
|
|
|
NormalInstanceAttribute: 42ms 42ms +0.3% 42ms 42ms +0.0%
|
|
|
|
PythonFunctionCalls: 39ms 38ms +3.5% 39ms 38ms +2.8%
|
|
|
|
PythonMethodCalls: 51ms 49ms +3.0% 51ms 50ms +2.8%
|
|
|
|
Recursion: 67ms 68ms -1.4% 67ms 68ms -1.4%
|
|
|
|
SecondImport: 41ms 36ms +12.5% 41ms 36ms +12.6%
|
|
|
|
SecondPackageImport: 45ms 40ms +13.1% 45ms 40ms +13.2%
|
|
|
|
SecondSubmoduleImport: 92ms 95ms -2.4% 95ms 98ms -3.6%
|
|
|
|
SimpleComplexArithmetic: 28ms 28ms -0.1% 28ms 28ms -0.2%
|
|
|
|
SimpleDictManipulation: 57ms 57ms -1.0% 57ms 58ms -1.0%
|
|
|
|
SimpleFloatArithmetic: 29ms 28ms +4.7% 29ms 28ms +4.9%
|
|
|
|
SimpleIntFloatArithmetic: 37ms 41ms -8.5% 37ms 41ms -8.7%
|
|
|
|
SimpleIntegerArithmetic: 37ms 41ms -9.4% 37ms 42ms -10.2%
|
|
|
|
SimpleListComprehensions: 33ms 33ms -1.9% 33ms 34ms -2.9%
|
|
|
|
SimpleListManipulation: 28ms 30ms -4.3% 29ms 30ms -4.1%
|
|
|
|
SimpleLongArithmetic: 26ms 26ms +0.5% 26ms 26ms +0.5%
|
|
|
|
SmallLists: 40ms 40ms +0.1% 40ms 40ms +0.1%
|
|
|
|
SmallTuples: 46ms 47ms -2.4% 46ms 48ms -3.0%
|
|
|
|
SpecialClassAttribute: 126ms 120ms +4.7% 126ms 121ms +4.4%
|
|
|
|
SpecialInstanceAttribute: 42ms 42ms +0.6% 42ms 42ms +0.8%
|
|
|
|
StringMappings: 94ms 91ms +3.9% 94ms 91ms +3.8%
|
|
|
|
StringPredicates: 48ms 49ms -1.7% 48ms 49ms -2.1%
|
|
|
|
StringSlicing: 45ms 45ms +1.4% 46ms 45ms +1.5%
|
|
|
|
TryExcept: 23ms 22ms +4.9% 23ms 22ms +4.8%
|
|
|
|
TryFinally: 32ms 32ms -0.1% 32ms 32ms +0.1%
|
|
|
|
TryRaiseExcept: 17ms 17ms +0.9% 17ms 17ms +0.5%
|
|
|
|
TupleSlicing: 49ms 48ms +1.1% 49ms 49ms +1.0%
|
|
|
|
WithFinally: 48ms 47ms +2.3% 48ms 47ms +2.4%
|
|
|
|
WithRaiseExcept: 45ms 44ms +0.8% 45ms 45ms +0.5%
|
|
|
|
-------------------------------------------------------------------------------
|
|
|
|
Totals: 2284ms 2287ms -0.1% 2306ms 2308ms -0.1%
|
|
|
|
|
|
|
|
(this=pep447.pybench, other=default.pybench)
|
2013-07-15 04:04:57 -04:00
|
|
|
|
|
|
|
|
2013-07-29 10:36:47 -04:00
|
|
|
A run of the benchmark suite (with option "-b 2n3") also seems to indicate that
|
|
|
|
the performance impact is minimal::
|
|
|
|
|
2013-08-22 06:49:43 -04:00
|
|
|
Report on Linux fangorn.local 2.6.32-358.114.1.openstack.el6.x86_64 #1 SMP Wed Jul 3 02:11:25 EDT 2013 x86_64 x86_64
|
|
|
|
Total CPU cores: 8
|
|
|
|
|
|
|
|
### call_method_slots ###
|
|
|
|
Min: 0.304120 -> 0.282791: 1.08x faster
|
|
|
|
Avg: 0.304394 -> 0.282906: 1.08x faster
|
|
|
|
Significant (t=2329.92)
|
|
|
|
Stddev: 0.00016 -> 0.00004: 4.1814x smaller
|
|
|
|
|
|
|
|
### call_simple ###
|
|
|
|
Min: 0.249268 -> 0.221175: 1.13x faster
|
|
|
|
Avg: 0.249789 -> 0.221387: 1.13x faster
|
|
|
|
Significant (t=2770.11)
|
|
|
|
Stddev: 0.00012 -> 0.00013: 1.1101x larger
|
|
|
|
|
|
|
|
### django_v2 ###
|
|
|
|
Min: 0.632590 -> 0.601519: 1.05x faster
|
|
|
|
Avg: 0.635085 -> 0.602653: 1.05x faster
|
|
|
|
Significant (t=321.32)
|
|
|
|
Stddev: 0.00087 -> 0.00051: 1.6933x smaller
|
|
|
|
|
|
|
|
### fannkuch ###
|
|
|
|
Min: 1.033181 -> 0.999779: 1.03x faster
|
|
|
|
Avg: 1.036457 -> 1.001840: 1.03x faster
|
|
|
|
Significant (t=260.31)
|
|
|
|
Stddev: 0.00113 -> 0.00070: 1.6112x smaller
|
|
|
|
|
|
|
|
### go ###
|
|
|
|
Min: 0.526714 -> 0.544428: 1.03x slower
|
|
|
|
Avg: 0.529649 -> 0.547626: 1.03x slower
|
|
|
|
Significant (t=-93.32)
|
|
|
|
Stddev: 0.00136 -> 0.00136: 1.0028x smaller
|
|
|
|
|
|
|
|
### iterative_count ###
|
|
|
|
Min: 0.109748 -> 0.116513: 1.06x slower
|
|
|
|
Avg: 0.109816 -> 0.117202: 1.07x slower
|
|
|
|
Significant (t=-357.08)
|
|
|
|
Stddev: 0.00008 -> 0.00019: 2.3664x larger
|
|
|
|
|
|
|
|
### json_dump_v2 ###
|
|
|
|
Min: 2.554462 -> 2.609141: 1.02x slower
|
|
|
|
Avg: 2.564472 -> 2.620013: 1.02x slower
|
|
|
|
Significant (t=-76.93)
|
|
|
|
Stddev: 0.00538 -> 0.00481: 1.1194x smaller
|
|
|
|
|
|
|
|
### meteor_contest ###
|
|
|
|
Min: 0.196336 -> 0.191925: 1.02x faster
|
|
|
|
Avg: 0.196878 -> 0.192698: 1.02x faster
|
|
|
|
Significant (t=61.86)
|
|
|
|
Stddev: 0.00053 -> 0.00041: 1.2925x smaller
|
|
|
|
|
|
|
|
### nbody ###
|
|
|
|
Min: 0.228039 -> 0.235551: 1.03x slower
|
|
|
|
Avg: 0.228857 -> 0.236052: 1.03x slower
|
|
|
|
Significant (t=-54.15)
|
|
|
|
Stddev: 0.00130 -> 0.00029: 4.4810x smaller
|
|
|
|
|
|
|
|
### pathlib ###
|
|
|
|
Min: 0.108501 -> 0.105339: 1.03x faster
|
|
|
|
Avg: 0.109084 -> 0.105619: 1.03x faster
|
|
|
|
Significant (t=311.08)
|
|
|
|
Stddev: 0.00022 -> 0.00011: 1.9314x smaller
|
|
|
|
|
|
|
|
### regex_effbot ###
|
|
|
|
Min: 0.057905 -> 0.056447: 1.03x faster
|
|
|
|
Avg: 0.058055 -> 0.056760: 1.02x faster
|
|
|
|
Significant (t=79.22)
|
|
|
|
Stddev: 0.00006 -> 0.00015: 2.7741x larger
|
|
|
|
|
|
|
|
### silent_logging ###
|
|
|
|
Min: 0.070810 -> 0.072436: 1.02x slower
|
|
|
|
Avg: 0.070899 -> 0.072609: 1.02x slower
|
|
|
|
Significant (t=-191.59)
|
|
|
|
Stddev: 0.00004 -> 0.00008: 2.2640x larger
|
|
|
|
|
|
|
|
### spectral_norm ###
|
|
|
|
Min: 0.290255 -> 0.299286: 1.03x slower
|
|
|
|
Avg: 0.290335 -> 0.299541: 1.03x slower
|
|
|
|
Significant (t=-572.10)
|
|
|
|
Stddev: 0.00005 -> 0.00015: 2.8547x larger
|
|
|
|
|
|
|
|
### threaded_count ###
|
|
|
|
Min: 0.107215 -> 0.115206: 1.07x slower
|
|
|
|
Avg: 0.107488 -> 0.115996: 1.08x slower
|
|
|
|
Significant (t=-109.39)
|
|
|
|
Stddev: 0.00016 -> 0.00076: 4.8665x larger
|
|
|
|
|
|
|
|
The following not significant results are hidden, use -v to show them:
|
|
|
|
call_method, call_method_unknown, chaos, fastpickle, fastunpickle, float, formatted_logging, hexiom2, json_load, normal_startup, nqueens, pidigits, raytrace, regex_compile, regex_v8, richards, simple_logging, startup_nosite, telco, unpack_sequence.
|
2013-07-29 10:36:47 -04:00
|
|
|
|
|
|
|
|
2013-07-15 05:23:29 -04:00
|
|
|
Alternative proposals
|
2015-07-25 07:17:34 -04:00
|
|
|
=====================
|
2013-07-15 04:04:57 -04:00
|
|
|
|
2013-07-15 05:23:29 -04:00
|
|
|
``__getattribute_super__``
|
2015-07-25 07:17:34 -04:00
|
|
|
--------------------------
|
2013-07-15 04:04:57 -04:00
|
|
|
|
2013-07-15 05:23:29 -04:00
|
|
|
An earlier version of this PEP used the following static method on classes::
|
2013-07-15 04:04:57 -04:00
|
|
|
|
2013-07-15 05:23:29 -04:00
|
|
|
def __getattribute_super__(cls, name, object, owner): pass
|
2013-07-15 04:04:57 -04:00
|
|
|
|
2013-10-18 13:24:59 -04:00
|
|
|
This method performed name lookup as well as invoking descriptors and was
|
|
|
|
necessarily limited to working only with ``super.__getattribute__``.
|
2013-07-15 04:04:57 -04:00
|
|
|
|
|
|
|
|
|
|
|
Reuse ``tp_getattro``
|
2015-07-25 07:17:34 -04:00
|
|
|
---------------------
|
2013-07-15 04:04:57 -04:00
|
|
|
|
|
|
|
It would be nice to avoid adding a new slot, thus keeping the API simpler and
|
|
|
|
easier to understand. A comment on `Issue 18181`_ asked about reusing the
|
|
|
|
``tp_getattro`` slot, that is super could call the ``tp_getattro`` slot of all
|
|
|
|
methods along the MRO.
|
|
|
|
|
2013-07-15 05:23:29 -04:00
|
|
|
That won't work because ``tp_getattro`` will look in the instance
|
2013-07-15 04:04:57 -04:00
|
|
|
``__dict__`` before it tries to resolve attributes using classes in the MRO.
|
|
|
|
This would mean that using ``tp_getattro`` instead of peeking the class
|
|
|
|
dictionaries changes the semantics of the `super class`_.
|
|
|
|
|
2015-07-25 07:17:34 -04:00
|
|
|
Alternative placement of the new method
|
|
|
|
---------------------------------------
|
2015-07-22 03:14:17 -04:00
|
|
|
|
|
|
|
This PEP proposes to add ``__getdescriptor__`` as a method on the metaclass.
|
|
|
|
An alternative would be to add it as a class method on the class itself
|
2016-07-11 11:14:08 -04:00
|
|
|
(similar to how ``__new__`` is a `staticmethod`_ of the class and not a method
|
2015-07-22 03:14:17 -04:00
|
|
|
of the metaclass).
|
|
|
|
|
2015-07-25 07:17:34 -04:00
|
|
|
The advantage of using a method on the metaclass is that will give an error
|
|
|
|
when two classes on the MRO have different metaclasses that may have different
|
|
|
|
behaviors for ``__getdescriptor__``. With a normal classmethod that problem
|
|
|
|
would pass undetected while it might cause subtle errors when running the code.
|
|
|
|
|
|
|
|
History
|
|
|
|
=======
|
|
|
|
|
|
|
|
* 23-Jul-2015: Added type flag ``Py_TPFLAGS_GETDESCRIPTOR`` after talking
|
|
|
|
with Guido.
|
|
|
|
|
|
|
|
The new flag is primarily useful to avoid crashing when loading an extension
|
|
|
|
for an older version of CPython and could have positive speed implications
|
|
|
|
as well.
|
|
|
|
|
|
|
|
* Jul-2014: renamed slot to ``__getdescriptor__``, the old name didn't
|
|
|
|
match the naming style of other slots and was less descriptive.
|
|
|
|
|
|
|
|
Discussion threads
|
|
|
|
==================
|
|
|
|
|
|
|
|
* The initial version of the PEP was send with
|
|
|
|
Message-ID `<75030FAC-6918-4E94-95DA-67A88D53E6F5@mac.com>`_
|
|
|
|
|
|
|
|
* Further discusion starting at a message with
|
|
|
|
Message-ID `<5BB87CC4-F31B-4213-AAAC-0C0CE738460C@mac.com>`_
|
|
|
|
|
|
|
|
* And more discussion starting at message with
|
|
|
|
Message-ID `<00AA7433-C853-4101-9718-060468EBAC54@mac.com>`_
|
|
|
|
|
2015-07-22 03:14:17 -04:00
|
|
|
|
2013-07-15 04:04:57 -04:00
|
|
|
|
|
|
|
References
|
|
|
|
==========
|
|
|
|
|
2015-07-22 03:14:17 -04:00
|
|
|
* `Issue 18181`_ contains an out of date prototype implementation
|
2013-07-15 04:04:57 -04:00
|
|
|
|
|
|
|
Copyright
|
|
|
|
=========
|
|
|
|
|
|
|
|
This document has been placed in the public domain.
|
|
|
|
|
2015-07-25 07:17:34 -04:00
|
|
|
.. _`<75030FAC-6918-4E94-95DA-67A88D53E6F5@mac.com>`: http://marc.info/?l=python-dev&m=137510220928964&w=2
|
|
|
|
|
|
|
|
.. _`<5BB87CC4-F31B-4213-AAAC-0C0CE738460C@mac.com>`: https://mail.python.org/pipermail/python-ideas/2014-July/028420.html
|
|
|
|
|
|
|
|
.. _`<00AA7433-C853-4101-9718-060468EBAC54@mac.com>`: https://mail.python.org/pipermail/python-dev/2013-July/127321.html
|
|
|
|
|
2013-07-15 04:04:57 -04:00
|
|
|
.. _`Issue 18181`: http://bugs.python.org/issue18181
|
|
|
|
|
2013-07-15 05:23:29 -04:00
|
|
|
.. _`super class`: http://docs.python.org/3/library/functions.html#super
|
2013-07-15 04:04:57 -04:00
|
|
|
|
2014-07-25 09:57:34 -04:00
|
|
|
.. _`super proxy`: http://docs.python.org/3/library/functions.html#super
|
|
|
|
|
2015-07-22 03:14:17 -04:00
|
|
|
.. _`super`: http://docs.python.org/3/library/functions.html#super
|
|
|
|
|
|
|
|
.. _`dir()`: http://docs.python.org/3/library/functions.html#dir
|
|
|
|
|
|
|
|
.. _`staticmethod`: http://docs.python.org/3/library/functions.html#staticmethod
|
|
|
|
|
|
|
|
.. _`__dir__()`: https://docs.python.org/3/reference/datamodel.html#object.__dir__
|
|
|
|
|
2013-07-15 05:23:29 -04:00
|
|
|
.. _`NotImplemented`: http://docs.python.org/3/library/constants.html#NotImplemented
|
2013-07-15 04:04:57 -04:00
|
|
|
|
2013-07-15 05:23:29 -04:00
|
|
|
.. _`PyObject_GenericGetAttr`: http://docs.python.org/3/c-api/object.html#PyObject_GenericGetAttr
|
2013-07-15 04:04:57 -04:00
|
|
|
|
2013-07-15 05:23:29 -04:00
|
|
|
.. _`type`: http://docs.python.org/3/library/functions.html#type
|
2013-07-15 04:04:57 -04:00
|
|
|
|
2013-07-15 05:23:29 -04:00
|
|
|
.. _`AttributeError`: http://docs.python.org/3/library/exceptions.html#AttributeError
|
2013-07-17 10:57:39 -04:00
|
|
|
|
|
|
|
.. _`PyObjC`: http://pyobjc.sourceforge.net/
|
|
|
|
|
|
|
|
.. _`classmethod`: http://docs.python.org/3/library/functions.html#classmethod
|
2015-07-25 07:17:34 -04:00
|
|
|
|
|
|
|
.. _`pep447-micro-bench.py`: http://bugs.python.org/file40013/pep447-micro-bench.py
|