Search code examples
pythonmetaclass

Using Python Metaclasses to Limit the Number of Attributes


I trying to define a Python metaclass to limit the number of attributes that a class may contain, starting at creation time. I am constrained to use Python 3.7 due to system limitations

I managed to do it without a metaclass:

class MyClass:
    def __init__(self):
        self.param1 = 7
        self.param2 = 8

    def __setattr__(self, key, value):
        if key not in self.__dict__ or len(self.__dict__.keys()) < 2:
           self.__dict__[key] = value

        raise ValueError("Excess of parameters.")

test = MyClass()

test.param1 = 3
test.param2 = 5

print(test.param1)      # Prints 3
print(test.param2)      # Prints 5

test.param3 = 9         # RAISES AN ERROR, AS IT SHOULD!

The problem that I am having is that I want to do this through a metaclass. Sure, I bet that there can be better options, but I find that, in this case, the best option is to do it through a metaclass (?)

I have read several posts and this is the closest Q&A I have found to solving the problem.

Question

Is there a way to limit the number of attributes of a class to a specific number, at creation time, using metaclasses?

Maybe use the dunder method __settattr__ to use the super class constructor?


Solution

  • If you really want to do it using metaclasses, you can, but I would recommend against it, because a class decorator suffices, and you can chain class decorators in future, whereas you cannot do so with metaclasses. Using a class decorator also frees up the possibility of using a metaclass in future.

    With that in mind, here are the solutions I've cooked up. All code is tested and confirmed to function as intended on Python 3.7.3.

    # A solution using a class decorator (recommended solution)
    def limited_attributes(number_attributes):
        def __setattr__(self,key,value):
            if key not in self.__dict__ and len(self.__dict__.keys())>=number_attributes:
                raise ValueError('Too many attributes')
            self.__dict__[key]=value
        def decorator(cls):
            cls.__setattr__=__setattr__
            return cls
        return decorator
    
    @limited_attributes(2)
    class Foo:
      def __init__(self):
        self.param1=4
        self.param2=5
    
    test = Foo()
    test.param1=3 # no error
    test.param2='bar' # no error
    
    test.param3='baz' # raises ValueError
    
    
    # A solution using a function as a metaclass
    def limited_attributes(number_attributes):
        def __setattr__(self,key,value):
            if key not in self.__dict__ and len(self.__dict__.keys())>=number_attributes:
                raise ValueError('Too many attributes')
            self.__dict__[key]=value
        def metaclass(name,bases,attrs):
            cls=type(name,bases,attrs)
            cls.__setattr__=__setattr__
            return cls
        return metaclass
    
    class Foo(metaclass=limited_attributes(2)):
      def __init__(self):
        self.param1=4
        self.param2=5
    
    test = Foo()
    test.param1=3 # no error
    test.param2='bar' # no error
    
    test.param3='baz' # raises ValueError