[Python-Dev] Submitting PEP 422 (Simple class initialization hook) for pronouncement

Nick Coghlan ncoghlan at gmail.com
Mon Feb 11 11:33:24 CET 2013


On Mon, Feb 11, 2013 at 7:41 AM, PJ Eby <pje at telecommunity.com> wrote:
> On Sun, Feb 10, 2013 at 11:48 AM, Stefan Behnel <stefan_ml at behnel.de> wrote:
>> So, the way to explain it to users would be 1) don't use it, 2) if you
>> really need to do something to a class, use a decorator, 3) if you need to
>> decide dynamically what to do, define __init_class__() and 4) don't forget
>> to call super's __init_class__() in that case, and 5) only if you need to
>> do something substantially more involved and know what you're doing, use a
>> metaclass.
>
> I'd revise that to:
>
> 1) if there's no harm in forgetting to decorate a subclass, use a
> class decorator
> 2) if you want to ensure that a modification is applied to every
> subclass of a single common base class, define __init_class__ (and
> always call its super)
> 3) If you need to make the class object *act* differently (not just
> initialize it or trigger some other side-effect at creation time), or
> if you want the class suite to return some other kind of object,
> you'll need a metaclass.

I like that. Perhaps the PEP should propose some additional guidance
in PEP 8 regarding class based metaprogramming?

> Essentially, this change fixes a hole in class decorators that doesn't
> exist with function decorators: if you need the decoration applied to
> subclasses, you can end up with silent failures right now.
> Conversely, if you try prevent such failures using a metaclass, you
> not only have a big hill to climb, but the resulting code will be
> vulnerable to metaclass conflicts.
>
> The proposed solution neatly fixes both of these problems, providing
> One Obvious Way to do subclass initialization.

I also realised last night that one significant benefit of cleanly
separating class creation from class initialisation (as __new__ and
__init__ separate instance creation and initialisation) is the ability
to create a shared metaclass that just changes the namespace type with
__prepare__, and then use __init_class__ to control what you do with
it.

Here's the more extended example I'm now considering adding to the PEP in
order to show the improved composability the PEP offers (writing the below
example with only metaclasses would be... challenging). It's still a
toy example, but I don't believe there is any non-toy use case for
metaclass composition that is going to be short enough to fit in a PEP:

    # Define a metaclass as in Python 3.3 and earlier
    import collections
    class OrderedMeta(type):
        def __prepare__(self, *args, **kwds):
            return collections.OrderedDict()
        # Won't be needed if we add a noop __init_class__ to type
        def __init_class__(cls):
            pass

    class OrderedClass(metaclass=OrderedMeta):
        pass

    # Easily supplement the metaclass behaviour in a class definition
    class SimpleRecord(OrderedClass):
        """Simple ordered record type (inheritance not supported)"""
        @classmethod
        def __init_class__(cls):
            super().__init_class__()
            cls.__fields = fields = []
            for attr, obj in cls.__dict__.items():
                if attr.startswith("_") or callable(obj):
                    continue
                fields.append(attr)

        def __init__(self, *values):
            super().__init__(*values)
            for attr, obj in zip(self.__fields, values):
                setattr(self, attr, obj)

        def to_dict(self):
            fields = ((k, getattr(self, k)) for k in self.__fields)
            return collections.OrderedDict(fields)

    # Supplement the metaclass differently in another class definition
    class InheritableRecord(OrderedClass):
        """More complex record type that supports inheritance"""
        @classmethod
        def __init_class__(cls):
            super().__init_class__()
            cls.__fields = fields = []
            for mro_cls in cls.mro():
                for attr, obj in cls.__dict__.items():
                    if attr.startswith("_") or callable(obj):
                        continue
                    fields.append(attr)

        def __init__(self, *values):
            super().__init__(*values)
            for attr, obj in zip(self.__fields, values):
                setattr(self, attr, obj)

        def to_dict(self):
            fields = ((k, getattr(self, k)) for k in self.__fields)
            return collections.OrderedDict(fields)

    # Compared to custom metaclasses, composition is much simpler
    class ConfusedRecord(InheritableRecord, SimpleRecord):
        """Odd record type, only included to demonstrate composition"""

        # to_dict is inherited from InheritableRecord
        def to_simple_dict(self):
            return SimpleRecord.to_dict(self)

Perhaps it would sweeten the deal if the PEP also provided
types.OrderedMeta and types.OrderedClass, such that inheriting from
types.OrderedClass and defining __init_class__ became the
one-obvious-way to do order dependent class bodies? (I checked, we can
make types depend on collections without a circular dependency. We
would need to fix the increasingly inaccurate docstring, though)

Cheers,
Nick.

--
Nick Coghlan   |   ncoghlan at gmail.com   |   Brisbane, Australia


More information about the Python-Dev mailing list