5
\$\begingroup\$

I have implemented a metaclass in Python 3 that, apart from the usual instance constructor (i.e. __init__), enables you to define a class constructor (as a class method that I called __init_class__). The metaclass extends abc.ABCMeta so it supports abstract classes, i.e. the class constructor is not called if a class is abstract.

I've written up some tests for it, they pass, and the metaclass also works in practice for me, but I'd like to know:

  • Is this even a good approach for adding a class constructor functionality?
  • Are there any problematic corner-cases that I might run into with my code?
  • Am I testing it correctly?
  • Any comments about my code being (or not) pythonic?

Implementation:

import abc
import inspect

class MyType(abc.ABCMeta):
    """
        Metaclass.

        It adds extra functionality of an optional class constructor, which might by added
        to the class by adding a class method __init_class__(cls) to the class definition.

        Class constructor takes no arguments.
    """

    def __init__(cls, name, bases, nmspc):
        super().__init__(name, bases, nmspc)
        cls.__has_init_class__ = hasattr(cls, '__init_class__')
        if cls.__has_init_class__ and not inspect.isabstract(cls):
            cls.__init_class__()

Tests:

import unittest

class MyTypeTests(unittest.TestCase):
    """ Unit tests for MyType metaclass. """

    def test_class_constructor(self):
        """ Is the class constructor invoked? """

        class WithoutInit(metaclass=MyType): # pylint: disable=R0903
            """ Dummy class. """
            initialized = False

        class AbstractWithoutInit(metaclass=MyType): # pylint: disable=R0903
            """ Dummy abstract class. """
            initialized = False
            @abc.abstractmethod
            def spam(self):
                """ Dummy abstract method. """
                pass

        class WithInit(metaclass=MyType): # pylint: disable=R0903
            """ Dummy class with class constructor. """
            initialized = False
            @classmethod
            def __init_class__(cls):
                cls.initialized = True

        class AbstractWithInit(metaclass=MyType): # pylint: disable=R0903
            """ Dummy abstract class with class constructor. """
            initialized = False
            @abc.abstractmethod
            def spam(self):
                """ Dummy abstract method. """
                pass
            @classmethod
            def __init_class__(cls):
                cls.initialized = True

        self.assertFalse(WithoutInit.initialized)
        self.assertFalse(AbstractWithoutInit.initialized)
        self.assertTrue(WithInit.initialized)
        self.assertFalse(AbstractWithInit.initialized)
\$\endgroup\$
  • 2
    \$\begingroup\$ I don't understand the purpose of this code. Can you give us a motivating example? \$\endgroup\$ – Gareth Rees May 23 '16 at 12:21
3
\$\begingroup\$
  • Any comments about my code being (or not) pythonic?

It's 100% not pythonic code. If we go to PEP8 -- Descriptive: Naming Styles we'll see:

__double_leading_and_trailing_underscore__: "magic" objects or attributes that live in user-controlled namespaces.
E.g. __init__, __import__ or __file__. Never invent such names; only use them as documented.

Yes never create these. I can give you one example for why this is in place.

Say a novice to Python comes across this magic attribute and method that you have created. They don't know what it does, they've not come across a magic method that has not been documented in the Python docs. But they have yours. They laboriously search the Python docs, but just can't find it. They then don't use your code, as they don't know what it does.

Now, I maybe being a foolish hobgoblin, and so sure your new 'init' could possible be a magic method. But I'd still argue that __has_init_class__ should defiantly not be. Instead that could be _has_init_class.

I'd argue that _has_init_class is somewhat pointless and you should just use hasattr(cls, '__init_class__'). You also don't test this, and so it seems to me that you don't use it. In short it may be some nice sugar, but you probably don't need or use it.

I don't think __init_class__ is a good name. It makes me think you'll be running it near __init__, which from your tests in definitely not the case. You may say, you'll use __new_class__ instead. No, __new__ works together with __init__ to construct objects, and so would shift confusion.
I would name it more closely to how you are using it. From what I know I'd call it _nonabstract_meta_constructor. That's a bit long for my liking so I'd just use _meta_const.

And so I'd use:

class MyType(abc.ABCMeta):
    """
        Metaclass.

        It adds extra functionality of an optional class constructor, which might by added
        to the class by adding a class method __init_class__(cls) to the class definition.

        Class constructor takes no arguments.
    """

    def __init__(cls, name, bases, nmspc):
        super().__init__(name, bases, nmspc)
        if hasattr(cls, '_meta_const') and not inspect.isabstract(cls):
            cls._meta_const()

Ok it's not "100% not pythonic code". If you remove the magic methods, your code has no glairing PEP8 errors. You probably don't need all the docstrings on the tests, and your MyType docstring should be changed to follow PEP257. You should have a summery line, and then your multi-line docstring as you have it.

But it's good. Apart from the magic I'd have no, non-petty, reason to complain.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.