commit
976c962b24
140
pep-0487.txt
140
pep-0487.txt
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue