diff --git a/pep-0487.txt b/pep-0487.txt index a3b69c2f0..e360852cd 100644 --- a/pep-0487.txt +++ b/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