Merge pull request #57 from tecki/pep487a

Clarify PEP487
This commit is contained in:
Chris Angelico 2016-07-17 21:54:37 +10:00 committed by GitHub
commit 976c962b24
1 changed files with 103 additions and 37 deletions

View File

@ -232,16 +232,56 @@ PEP::
Implementation Details
======================
For those who prefer reading Python over english, the following is a Python
equivalent of the C API changes proposed in this PEP, where the new ``object``
and ``type`` defined here inherit from the usual ones::
The hooks are called in the following order: ``type.__new__`` calls
the ``__set_name__`` hooks on the descriptor after the new class has been
initialized. Then it calls ``__init_subclass__`` on the base class, on
``super()``, to be precise. This means that subclass initializers already
see the fully initialized descriptors. This way, ``__init_subclass__`` users
can fix all descriptors again if this is needed.
import types
Another option would have been to call ``__set_name__`` in the base
implementation of ``object.__init_subclass__``. This way it would be possible
even to prevent ``__set_name__`` from being called. Most of the times,
however, such a prevention would be accidental, as it often happens that a call
to ``super()`` is forgotten.
class type(type):
As a third option, all the work could have been done in ``type.__init__``.
Most metaclasses do their work in ``__new__``, as this is recommended by
the documentation. Many metaclasses modify their arguments before they
pass them over to ``super().__new__``. For compatibility with those kind
of classes, the hooks should be called from ``__new__``.
Another small change should be done: in the current implementation of
CPython, ``type.__init__`` explicitly forbids the use of keyword arguments,
while ``type.__new__`` allows for its attributes to be shipped as keyword
arguments. This is weirdly incoherent, and thus it should be forbidden.
While it would be possible to retain the current behavior, it would be better
if this was fixed, as it is probably not used at all: the only use case would
be that at metaclass calls its ``super().__new__`` with *name*, *bases* and
*dict* (yes, *dict*, not *namespace* or *ns* as mostly used with modern
metaclasses) as keyword arguments. This should not be done. This little
change simplifies the implementation of this PEP significantly, while
improving the coherence of Python overall.
As a second change, the new ``type.__init__`` just ignores keyword
arguments. Currently, it insists that no keyword arguments are given. This
leads to a (wanted) error if one gives keyword arguments to a class declaration
if the metaclass does not process them. Metaclass authors that do want to
accept keyword arguments must filter them out by overriding ``__init___``.
In the new code, it is not ``__init__`` that complains about keyword arguments,
but ``__init_subclass__``, whose default implementation takes no arguments. In
a classical inheritance scheme using the method resolution order, each
``__init_subclass__`` may take out it's keyword arguments until none are left,
which is checked by the default implementation of ``__init_subclass__``.
For readers who prefer reading Python over English, this PEP proposes to
replace the current ``type`` and ``object`` with the following::
class NewType(type):
def __new__(cls, *args, **kwargs):
if len(args) == 1:
return super().__new__(cls, args[0])
if len(args) != 3:
return super().__new__(cls, *args)
name, bases, ns = args
init = ns.get('__init_subclass__')
if isinstance(init, types.FunctionType):
@ -257,46 +297,72 @@ and ``type`` defined here inherit from the usual ones::
def __init__(self, name, bases, ns, **kwargs):
super().__init__(name, bases, ns)
class object:
class NewObject(object):
@classmethod
def __init_subclass__(cls):
pass
class object(object, metaclass=type):
Backward compatibility issues
=============================
The exact calling sequence in ``type.__new__`` is slightly changed, raising
fears of backwards compatibility. It should be assured by tests that common use
cases behave as desirerd.
The following class definitions (except the one defining the metaclass)
continue to fail with a ``TypeError`` as superfluous class arguments are passed::
class MyMeta(type):
pass
In this code, first the ``__set_name__`` are called on the descriptors, and
then the ``__init_subclass__``. This means that subclass initializers already
see the fully initialized descriptors. This way, ``__init_subclass__`` users
can fix all descriptors again if this is needed.
class MyClass(metaclass=MyMeta, otherarg=1):
pass
Another option would have been to call ``__set_name__`` in the base
implementation of ``object.__init_subclass__``. This way it would be possible
even to prevent ``__set_name__`` from being called. Most of the times,
however, such a prevention would be accidental, as it often happens that a call
to ``super()`` is forgotten.
MyMeta("MyClass", (), otherargs=1)
Another small change should be noted here: in the current implementation of
CPython, ``type.__init__`` explicitly forbids the use of keyword arguments,
while ``type.__new__`` allows for its attributes to be shipped as keyword
arguments. This is weirdly incoherent, and thus the above code forbids that.
While it would be possible to retain the current behavior, it would be better
if this was fixed, as it is probably not used at all: the only use case would
be that at metaclass calls its ``super().__new__`` with *name*, *bases* and
*dict* (yes, *dict*, not *namespace* or *ns* as mostly used with modern
metaclasses) as keyword arguments. This should not be done.
import types
types.new_class("MyClass", (), dict(metaclass=MyMeta, otherarg=1))
types.prepare_class("MyClass", (), dict(metaclass=MyMeta, otherarg=1))
As a second change, the new ``type.__init__`` just ignores keyword
arguments. Currently, it insists that no keyword arguments are given. This
leads to a (wanted) error if one gives keyword arguments to a class declaration
if the metaclass does not process them. Metaclass authors that do want to
accept keyword arguments must filter them out by overriding ``__init___``.
A metaclass defining only a ``__new__`` method which is interested in keyword
arguments now does not need to define an ``__init__`` method anymore, as the
default ``type.__init__`` ignores keyword arguments. This is nicely in line
with the recommendation to override ``__new__`` in metaclasses instead of
``__init__``. The following code does not fail anymore::
In the new code, it is not ``__init__`` that complains about keyword arguments,
but ``__init_subclass__``, whose default implementation takes no arguments. In
a classical inheritance scheme using the method resolution order, each
``__init_subclass__`` may take out it's keyword arguments until none are left,
which is checked by the default implementation of ``__init_subclass__``.
class MyMeta(type):
def __new__(cls, name, bases, namespace, otherarg):
return super().__new__(cls, name, bases, namespace)
class MyClass(metaclass=MyMeta, otherarg=1):
pass
Only defining a ``__init__`` in a metaclass continues to fail with ``TypeError``
if keyword arguments are given::
class MyMeta(type):
def __init__(self, name, bases, namespace, otherarg):
super().__init__(name, bases, namespace)
class MyClass(metaclass=MyMeta, otherarg=1):
pass
Defining both ``__init__`` and ``__new__`` continues to work fine.
About the only thing that stops working is passing the arguments of
``type.__new__`` as keyword arguments::
class MyMeta(type):
def __new__(cls, name, bases, namespace):
return super().__new__(cls, name=name, bases=bases,
dict=namespace)
class MyClass(metaclass=MyMeta):
pass
This will now raise ``TypeError``, but this is weird code, and easy
to fix even if someone used this feature.
Rejected Design Options