0

I just realised that __setattr__ doesn't work on the class itself. So this implementation,

class Integer:
    me_is_int = 0

    def __setattr__(self, name, value):
        if not isinstance(value, int):
            raise TypeError

doesn't raise on this:

Integer.me_is_int = "lol"

So, switching to a metaclass:

class IntegerMeta:
    def __setattr__(cls, name, value):
        if not isinstance(value, int):
            raise TypeError

class Integer(metaclass=IntegerMeta):
    me_is_int = 0

this works, but this:

Integer().me_is_int = "lol"

doesn't work yet again. So do I need to copy the __setattr__ method in Integer again to make it work on instances? Is it not possible for Integer to use IntegerMeta's __setattr__ for instances?

demberto
  • 489
  • 5
  • 15
  • The metaclass can influence the entire class creating, including adding a default `__setattr__` (say, the one from the metaclass itself). But you have to actually tell it to do so. – MisterMiyagi Apr 01 '22 at 17:20
  • Maybe [this](https://stackoverflow.com/a/10763270/7867968) could answer your question? – C.Nivs Apr 01 '22 at 17:25
  • @MisterMiyagi how can I achieve that, I cannot even find the meaning of the arguments passed to `type.__new__` (i.e. metaclass' `__new__` function signature) – demberto Apr 01 '22 at 18:02

1 Answers1

1

You are right in your reasoning: having a custom __setattr__ special method in the metaclass will affect any value setting on the class, and having the it on the class will affect all instances of the class.

With that in mind, if you don't want to duplicate code, is to arrange the metaclass itself to inject the logic in a class, whenever it is created.

The way you've written it, even thinking as an example, is dangerous, as it will affect any attribute set on the class or instances - but if you have a list of the attributes you want to guard in that way, it would also work.



attributes_to_guard = {"me_is_int",}

class Meta:
    def __init__(cls, name, bases, ns, **kw):
        # This line itself would not work if the setattr would not check
        # for a restricted set of attributes to guard:
        cls.__setattr__ = cls.__class__.__setattr__
        # Also, note that this overrides any manually customized
        # __setattr__ on the classes. The mechanism to call those, 
        # and still add the guarding logic in the metaclass would be
        # more complicated, but it can be done
        super().__init__(name, bases, ns, **kw)

    def __setattr__(self, name, value):
        if name in attributes_to_guard not isinstance(value, int):
            raise TypeError()

class Integer(metaclass=Meta):
    me_is_int = 0

jsbueno
  • 99,910
  • 10
  • 151
  • 209
  • I think a better way to achieve this (as opposed to having a list of attributes to guard) would be using a descriptor on the metaclass. No need for custom `__setattr__`. – wim Apr 01 '22 at 18:44
  • So TLDR is, I shouldn't override `__setattr__` for any `Integer` subclasses? Also why didn't you use `__new__` method instead? – demberto Apr 01 '22 at 18:48
  • yes - in the example code above, as it is, any `__setattr__` in the classes would be suppressed by the one in the metaclass. You maybe could check "descriptors" on how to control writting to a single attribute without having to mess with `__setattr__`: they might be better for "everyday use". As for `__init__` instead of `__new__`: it is a bit less intrusive: both are called at class creation, but `__init__` in a later stage, when the class actually already exists. As we do with normal classes, we can use `__init__` instead of `__new__` on metaclasses. – jsbueno Apr 01 '22 at 19:22