Search code examples
pythonclassoopcpythonmetaclass

Is there an implementation of the PyObject class that allows you to properly override magic methods?


So I read in a book, that if you want to extend from built-in types such as list, dict or str, and want to override magic methods, you should use UserList, UserDict and UserString from the collections module respectively instead. Appearently, that is because the base classes are implemented in CPython, where these methods don't call each other, and so, overriding them has no, or unwanted effects.

I was wondering if similar to the UserList class and so on, there exists a class that can be used to 'properly' extend from the object class. I looked at the PyObject documentation and found this, as well as this post, but neither do I want to extend in C, nor do I speak it. I couldn't find anything in the collections module or anywhere else.

The reason why I ask is because of a previous question of mine 'Is it possible to transform default class dunder methods into class methods?' and I have a hunch that the reason why my approach doesn't work is the same one that I outlined earlier.


Solution

  • As stated in the comments - there is no such thing, and there is no need for it - all magic methods in object can be overriden and will work fine.

    What happens with dictionaries, lists and other collections, as MisterMiyagi explains in the comments is that, for example, dict's get won't use the __getitem__ method, so if you are customizing the behavior of this, you also have to rewrite get. The classes that properly resolve this, allowing one to create fully working Mappings, Sequences and Sets with a minimal amount of code that is reused are the ones defined in the collections.abc module.

    Now, if you want one of the magic methods to work on one object's class instead of instances of that class, you have to implement the methods in the class of the class - which is the "metaclass".

    This is far different from the "superclass" - superclasses define methods and attributes the subclasses inherit, and that will be available in the subclasses. But magic methods on a class affect only the instances, not the class itself (with the exception of __init_subclass__, and, of course __new__, which can be changed to do other things than create a new instance).

    Metaclasses control how classes are built (with the __new__, __init__ and __call__ methods) - and are allowed to have methods on how they behave through the magic methods - and I think there are no special cases for how the magic-methods in a metaclass work in classes, compared to what they work in the relation of a class to an ordinary instance. If you implement __add__, __getitem__, __len__ on the metaclass, all of these will work for the classes created with that metaclass.

    What is possible to do, if you don't want to write your magic methods for the class in the metaclass itself, is to create a metaclass that will, on being called, automatically create another, dynamic metaclass, and copy over the dunder methods to that class. But it is tough to think of that as a "healthy" design in any serious application - having magic methods that apply to classes is already a bit over the top - although there can be cases where that is convenient.

    So, for example, if you want a class to have a __geitem__ that would allow you to retrieve all instances of that class, this would work:

    class InstanceRegister(type):
        def __init__(cls, name, bases, namespace, **kw):
            super().__init__(name, bases, namespace, **kw)
    
            cls._instances = []
    
            original_new = cls.__new__
            def new_wrapper(cls, *args, **kw):
                if original_new is object.__new__:
                    instance = original_new(cls)
                else:
                    instance = original_new(cls, *args, **kw)
                cls._instances.append(instance)
                return instance
    
            cls.__new__ = new_wrapper
    
        def __len__(cls):
            return len(cls._instances)
    
        def __getitem__(cls, item):
            return cls._instances[item]
    

    And this will just work, as can be seem in this interative session:

    In [26]: class A(metaclass=InstanceRegister): pass                                                                                   
    
    In [27]: a = A()                                                                                                                     
    
    In [28]: len(A)                                                                                                                      
    Out[28]: 1
    
    In [29]: A[0] is a                                                                                                                   
    Out[29]: True