1

I am trying to implement a metaclass that initializes class variables when a first its instance is being created. I want to keep a new magic method __load__ that should be called as a classmethod (like __new__). So I implemented it like this:

class StaticLoad(type):
    __loaded_classes = set()

    def __call__(cls, *args, **kwargs):
        if cls not in cls.__loaded_classes:
            if hasattr(cls, '__load__'):
                cls.__load__()
            cls.__loaded_classes.add(cls)
        return super().__call__(*args, **kwargs)


class BaseClass(metaclass=StaticLoad):
    s = 0


class MyClass(BaseClass):
    @classmethod
    def __load__(cls):
        print("Loading", cls.__name__, "...")
        cls.s += 1


obj1 = MyClass()
obj2 = MyClass()
print(MyClass.s)

It works fine and gives the correct result:

Loading MyClass ...
1

Now I want to implement the method __load__ as a classmethod by default like __new__ (without the need to type @classmethod above each time). I tried this:

class StaticLoad(type):
    __loaded_classes = set()

    def __call__(cls, *args, **kwargs):
        if cls not in cls.__loaded_classes:
            if hasattr(cls, '__load__'):
                # I try to apply classmethod routine to make
                # cls.__load__ a classmethod
                classmethod(cls.__load__)()
            cls.__loaded_classes.add(cls)
        return super().__call__(*args, **kwargs)


class BaseClass(metaclass=StaticLoad):
    s = 0


class MyClass(BaseClass):
    # @classmethod line was deleted
    def __load__(cls):
        print("Loading", cls.__name__, "...")
        cls.s += 1


obj1 = MyClass()
obj2 = MyClass()
print(MyClass.s)

I got the error:

Traceback (most recent call last):
  File "example.py", line 22, in <module>
    obj1 = MyClass()
  File "example.py", line 7, in __call__
    classmethod(cls.__load__)()
TypeError: 'classmethod' object is not callable

It looks like classmethod routine is correctly available only inside a class definition.

How should I improve my metaclass to make it work fine? I would like to keep the content of classes BaseClass and MyClass as I wrote above, placing all magic into StaticLoad.

3
  • 1
    cls.__load__() or cls.__load__(cls) doesn't work?! Surprisingly, a classmethod object is not callable. Its __get__ will return the callable object when it is looked up on a class or an instance. Commented Dec 4, 2017 at 9:35
  • I haven't tried to call cls.__load__(cls). Commented Dec 4, 2017 at 9:36
  • @AnttiHaapala Replacing that with cls.__load__(cls) helped. Thanks you. Commented Dec 4, 2017 at 9:39

2 Answers 2

2

With the help of @AnttiHaapala the solution is simple. Instead of calling

classmethod(cls.__load__)()

I had to call

cls.__load__(cls)
Sign up to request clarification or add additional context in comments.

Comments

2

If you want to perform transforms on the certain methods and attributes of a class creation, you do that on the metaclass' __new__ function.

Since yu already have a metaclass, all you have to do is to implement its __new__ method to convert any __load__ methods in a classmethod:

class StaticLoad(type):
    __loaded_classes = set()

    def __new__(metacls, name, bases, namespace):
        if "__load__" in namespace and not isinstance(namespace["__load__"], classmethod):
            namespace["__load__"] = classmethod(namespace["load"])
        return super().__new__(metacls, name, bases, namespace)

    def __call__(cls, *args, **kwargs):
        if cls not in cls.__class__.__loaded_classes:
            if hasattr(cls, '__load__'):
                cls.__load__()
            type(cls).__loaded_classes.add(cls)
        return super().__call__(*args, **kwargs)

(the other change I made was to make explict that "__loaded_classes" should be accessed on the metaclass, not on the class itself).

4 Comments

Your "other change" is incomplete. You missed the use of cls.__loaded_classes in the if statement. I don't think that change is needed, since the double leading underscores will invoke name mangling on the attribute name (using the metaclass name as a prefix). That means the class is unlikely to overwrite it.
yes. I changed it there too. I kept the double underscore, but I d rather not use name mangling at all - another option would be to use the bare __class__ instead of cls.__class__.
This is exactly the kind of situation name mangling is designed for. The metaclass can't know in advance what names might be useful for attributes in its instances (the normal classes). When it wants to use an attribute for its own implementation, it can use a double leading underscored name to make it much less likely that the name will accidentally collide with a name the instance is using for some other purpose. As for cls.__class__ vesus type(cls) or __class__, I'd actually suggest the better alternative would be to explicitly name the metaclass: StaticLoad.__loaded_classes.
yes, it will work well here. But not the "precise situation it was designed for" - that would be for simple inheritance. Semantically, it makes more sense to explicitly address the attribute on the metaclass. But yes, th emangling would work just fine without the .__class__ component anyway.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.