Home > Back-end >  Python: use special methods on a class (not necessarily its instances)
Python: use special methods on a class (not necessarily its instances)

Time:04-14

(Edited to frame it as a question as requested by the guidelines)

I am implementing this class to manage a library of prime numbers, the instances are supposed to work as a range object, but on primes only. I also want an easy way to iterate over all primes, check if a number was prime and call the nth prime as if it was a list, something like:

class primes: ...

for p in primes: ...
if p in primes: ...
p = primes[2] # = 5

Is there a general way to apply magic methods to classes themselves instead of their instances?

CodePudding user response:

What do you think about the whole thing?

Not bad. It coukld have taken other approaches, but this is as nice as many others.

Just as a remark: you don't need the __metaclass__ attribute to be present on the created classes - that was an artifact on Python 2. One can use type(cls) to get to the metaclass (as you already do).

I tried to think of a way of staying with a single metaclass, but that, as you found out, is not possible: the correctly named "dunder" methods have to be present in the metaclass itself, just as you did there.

If you'd wish, this could be made a metaclass itself, instead of a decorator - the same logic would apply in the __new__ method before actually instantiating the class: that could avoid having a duplicate class without the "lifted" capabilities (the one you point to as __wrapped__), and could be made in a way as to be inherited by the subclasses - so no need to apply a decorator to subclasses. But then, that would require some care: when creating a new class you have to persist the "lift dunder methods" behavior and not just the "serve the dunder methods" of the dynamic metaclass.

And, instead of having an ordinary metaclass and instrument its __new__ method to create a dynamic metaclass and return its instance, you could instrument the __call__ method of a metaclass, and have effectively a "meta-meta-class". It would be even harder to understand, and with no gains, though - the decorator approach might be better in all cases.

CodePudding user response:

Idea.

I thought I could use a metaclass to give the class its dunder methods as an instance of the metaclass. Since those methods were class-specific I thought of something like the following:

class Meta(type):
    def __iter__(cls):
        return cls.__cls_iter__()

class Klass(metaclass=Meta):
    @classmethod
    def __cls_iter__(cls):
        ...

It worked fine and making it into a decorator was easy, but it was still too specific and implementing twice every method would have been repetitive.

Last Iteration.

I decided for a decorator that creates a different metaclass for every class it is used on, it creates only the __*__ methods that are required and it also takes care of making __cls_*__ methods as classmethod:

def dundered(my_cls):
    def lift_method(cls_method_name):
        def lifted_method(cls, *args, **kwargs):
            return getattr(cls, cls_method_name)(*args, **kwargs)

        lifted_method.__name__ = f"__{cls_method_name[6:]}"
        return lifted_method

    meta_dict = dict()
    for key, val in dict(my_cls.__dict__).items():
        if key == f"__cls_{key[6:-2]}__":
            if not isinstance(val, classmethod):
                setattr(my_cls, key, classmethod(val))
            meta_dict[f"__{key[6:]}"] = lift_method(key)
    return type(f"{type(my_cls).__name__}_dunderable", (type(my_cls),), meta_dict)(
        my_cls.__name__   "__", (my_cls,), {"__wrapped__": my_cls}
    )

This is very easy to use and it seems to be working fine with any dunder method:

@dundered
class Klass:
    def __iter__(self):
        # iters on instances
        ...

    def __cls_iter__(cls):
        # iters on the class
        ...

for _ in Klass(...): ... # calls __iter__
for _ in Klass: ...      # calls __cls_iter__
  • Related