Adjust the redrafted PEP 422 to match reality
This commit is contained in:
parent
685514f87a
commit
cc2b99b5ad
107
pep-0422.txt
107
pep-0422.txt
|
@ -27,16 +27,6 @@ The new mechanism is also much easier to understand and use than
|
|||
implementing a custom metaclass, and thus should provide a gentler
|
||||
introduction to the full power Python's metaclass machinery.
|
||||
|
||||
.. note::
|
||||
|
||||
This PEP, in particular the use of __prepare__ to share a single
|
||||
namespace amongst multiple class objects, highlights a possible issue
|
||||
with the attribute lookup caching: when the underlying mapping is updated
|
||||
by other means, the attribute lookup cache is not invalidated correctly.
|
||||
Since the optimisation provided by that cache is highly desirable,
|
||||
some of the ideas in this PEP may need to be declared as officially
|
||||
unsupported (since the observed behaviour is rather odd when the
|
||||
caches get out of sync).
|
||||
|
||||
Background
|
||||
==========
|
||||
|
@ -153,16 +143,31 @@ the idea is worth reconsidering.
|
|||
|
||||
However, the introduction of the metaclass ``__prepare__`` method in PEP
|
||||
3115 allows a further enhancement that was not possible in Python 2: this
|
||||
PEP also proposes that ``type.__prepare__`` be updated to accept a
|
||||
``namespace`` keyword-only argument. If present, the value provided as the
|
||||
``namespace`` argument will be returned from ``type.__prepare__`` instead of
|
||||
a freshly created dictionary instance. For example, the following will use
|
||||
PEP also proposes that ``type.__prepare__`` be updated to accept a factory
|
||||
function as a ``namespace`` keyword-only argument. If present, the value
|
||||
provided as the ``namespace`` argument will be called without arguments
|
||||
to create the result of ``type.__prepare__`` instead of using a freshly
|
||||
created dictionary instance. For example, the following will use
|
||||
the ordered dictionary created in the header as the class namespace::
|
||||
|
||||
class OrderedExample(namespace=collections.OrderedDict()):
|
||||
class OrderedExample(namespace=collections.OrderedDict):
|
||||
def __init_class__(cls):
|
||||
# cls.__dict__ is still a read-only proxy to the class namespace,
|
||||
# but the underlying storage is the OrderedDict instance
|
||||
# but the underlying storage is an OrderedDict instance
|
||||
|
||||
.. note::
|
||||
|
||||
This PEP, along with the existing ability to use __prepare__ to share a
|
||||
single namespace amongst multiple class objects, highlights a possible
|
||||
issue with the attribute lookup caching: when the underlying mapping is
|
||||
updated by other means, the attribute lookup cache is not invalidated
|
||||
correctly (this is a key part of the reason class ``__dict__`` attributes
|
||||
produce a read-only view of the underlying storage).
|
||||
|
||||
Since the optimisation provided by that cache is highly desirable,
|
||||
the use of a preexisting namespace as the class namespace may need to
|
||||
be declared as officially unsupported (since the observed behaviour is
|
||||
rather strange when the caches get out of sync).
|
||||
|
||||
|
||||
Key Benefits
|
||||
|
@ -175,7 +180,8 @@ Easier use of custom namespaces for a class
|
|||
Currently, to use a different type (such as ``collections.OrderedDict``) for
|
||||
a class namespace, or to use a pre-populated namespace, it is necessary to
|
||||
write and use a custom metaclass. With this PEP, using a custom namespace
|
||||
becomes as simple as specifying it in the class header.
|
||||
becomes as simple as specifying an appropriate factory function in the
|
||||
class header.
|
||||
|
||||
|
||||
Easier inheritance of definition time behaviour
|
||||
|
@ -245,20 +251,19 @@ including some aspects of the object models of both Javascript and Ruby.
|
|||
All of the examples below are actually possible today through the use of a
|
||||
custom metaclass::
|
||||
|
||||
if 1:
|
||||
class CustomNamespace(type):
|
||||
@classmethod
|
||||
def __prepare__(meta, name, bases, *, namespace=None, **kwds):
|
||||
parent_namespace = super().__prepare__(name, bases, **kwds)
|
||||
return namespace if namespace is not None else parent_namespace
|
||||
|
||||
return namespace() if namespace is not None else parent_namespace
|
||||
def __new__(meta, name, bases, ns, *, namespace=None, **kwds):
|
||||
return super().__new__(meta, name, bases, ns, **kwds)
|
||||
|
||||
def __init__(cls, name, bases, ns, *, namespace=None, **kwds):
|
||||
return super().__init__(name, bases, ns, **kwds)
|
||||
|
||||
The advantage of implementing the new keyword directly in
|
||||
``type.__prepare__`` is that the *only* persistent effect is
|
||||
``type.__prepare__`` is that the *only* persistent effect is then
|
||||
the change in the underlying storage of the class attributes. The metaclass
|
||||
of the class remains unchanged, eliminating many of the drawbacks
|
||||
typically associated with these kinds of customisations.
|
||||
|
@ -269,7 +274,7 @@ Order preserving classes
|
|||
|
||||
::
|
||||
|
||||
class OrderedClass(namespace=collections.OrderedDict()):
|
||||
class OrderedClass(namespace=collections.OrderedDict):
|
||||
a = 1
|
||||
b = 2
|
||||
c = 3
|
||||
|
@ -281,7 +286,7 @@ Prepopulated namespaces
|
|||
::
|
||||
|
||||
seed_data = dict(a=1, b=2, c=3)
|
||||
class PrepopulatedClass(namespace=seed_data.copy()):
|
||||
class PrepopulatedClass(namespace=seed_data.copy):
|
||||
pass
|
||||
|
||||
|
||||
|
@ -290,21 +295,54 @@ Cloning a prototype class
|
|||
|
||||
::
|
||||
|
||||
class NewClass(namespace=Prototype.__dict__.copy()):
|
||||
class NewClass(namespace=Prototype.__dict__.copy):
|
||||
pass
|
||||
|
||||
|
||||
Defining an extensible class
|
||||
----------------------------
|
||||
Extending a class
|
||||
-----------------
|
||||
|
||||
.. note:: Just because the PEP makes it *possible* to do this relatively,
|
||||
cleanly doesn't mean anyone *should* do this!
|
||||
|
||||
::
|
||||
|
||||
class Extensible:
|
||||
namespace = locals()
|
||||
from collections import MutableMapping
|
||||
|
||||
class ExtendingClass(namespace=Extensible.namespace):
|
||||
# The MutableMapping + dict combination should give something that
|
||||
# generally behaves correctly as a mapping, while still being accepted
|
||||
# as a class namespace
|
||||
class ClassNamespace(MutableMapping, dict):
|
||||
def __init__(self, cls):
|
||||
self._cls = cls
|
||||
def __len__(self):
|
||||
return len(dir(self._cls))
|
||||
def __iter__(self):
|
||||
for attr in dir(self._cls):
|
||||
yield attr
|
||||
def __contains__(self, attr):
|
||||
return hasattr(self._cls, attr)
|
||||
def __getitem__(self, attr):
|
||||
return getattr(self._cls, attr)
|
||||
def __setitem__(self, attr, value):
|
||||
setattr(self._cls, attr, value)
|
||||
def __delitem__(self, attr):
|
||||
delattr(self._cls, attr)
|
||||
|
||||
def extend(cls):
|
||||
return lambda: ClassNamespace(cls)
|
||||
|
||||
class Example:
|
||||
pass
|
||||
|
||||
class ExtendedExample(namespace=extend(Example)):
|
||||
a = 1
|
||||
b = 2
|
||||
c = 3
|
||||
|
||||
>>> Example.a, Example.b, Example.c
|
||||
(1, 2, 3)
|
||||
|
||||
|
||||
Rejected Design Options
|
||||
=======================
|
||||
|
@ -335,6 +373,17 @@ documents it as an ordinary method, and the current documentation doesn't
|
|||
explicitly say anything one way or the other).
|
||||
|
||||
|
||||
Passing in the namespace directly rather than a factory function
|
||||
----------------------------------------------------------------
|
||||
|
||||
At one point, this PEP proposed that the class namespace be passed
|
||||
directly as a keyword argument, rather than passing a factory function.
|
||||
However, this encourages an unsupported behaviour (that is, passing the
|
||||
same namespace to multiple classes, or retaining direct write access
|
||||
to a mapping used as a class namespace), so the API was switched to
|
||||
the factory function version.
|
||||
|
||||
|
||||
Reference Implementation
|
||||
========================
|
||||
|
||||
|
|
Loading…
Reference in New Issue