Since your needs are actually "It is more a policy of my co-workers not being able to meddle with the number of attributes of memory sensitive classes, where adding more attributes at run time (or even develop time) may place unneeded stress on the system (kind of an API), so I want them to have the flexibility to, at the very least, choose the names of the variables but limit them to a certain number"
You actually need to use __slots__. You can use the metaclass to inspect some parameter of the class at creation time - possibly the parameters of the __init__ method, and create the names from there.
The metaclass can also inject a __setattr__ method with whatever limitations you want, of course - but without the use of __slots__, each instance will have a full __dict__ for the attributes, which will (1) use memory, and (2) could be used with a workaround to store extra attributes.
Since knowing the attribute names to set them as slots would be an extra task, and there should be some definition (for example, we could use the first two parameter names to __init__), I think it is easier to inject generic slot names, and use __setattr__ to map arbitrarily named attributes to the slots in the order they are assigned. The mapping itself can be recorded on the class object by the setattr, and once the first instance is fully initiated, no other attributes can be added in other instances.
Still - I am providing an example for this for the fun of it - from the point of view of "real world" usage, your concerns are not useful at all: if attributes are needed for a certain business logic, they are needed - trying to artificially limit them won't help. (they could just add a dict as one of the attributes. If you bar that, then use another container. Continue to try limting the code and it could degrade to the point of using raw lists, dictionaries and stand alone functions instead of being object oriented, and so on.
def limit_setattr(self, name, value):
cls = type(self)
# Acessing the attribute through "cls.__dict__"
# prevent it from being fetched from a superclass.
attrs = cls.__dict__["attr_names"]
# Allow the use of properties, without then counting to the maximum attributes:
if hasattr(cls, name) and hasattr(getattr(cls, name), "__set__"): # nice place to use the walrus - := - op, but that is supported from Python 3.8 on only
return object.__setattr__(self, name, value)
try:
index = attrs.index(name)
except ValueError:
index = None
if index is None and len(attrs) < cls.maxattrs:
index = len(attrs)
attrs += (name,)
cls.attr_names = attrs
elif index is None:
raise AttributeError(f"Class {cls.__name__} can't hold attribute {name}: max attributes exceeded!")
mapped_name = f"attr_{index}"
object.__setattr__(self, mapped_name, value)
def limit_getattr(self, name):
cls = type(self)
attrs = cls.__dict__["attr_names"]
try:
index = attrs.index(name)
except ValueError:
raise AttributeError(f"{name} not an attribute of {cls.__name__} instances" )
mapped_name = f"attr_{index}"
return object.__getattribute__(self, mapped_name)
class LimitAttrs(type):
def __new__(mcls, name, bases, namespace, maxattrs=2):
for base in bases:
if "__dict__" in dir(base):
raise TypeError(f"The base class {base} of {name} does not have slots and won't work for a slotted child class.")
namespace["__setattr__"] = limit_setattr
namespace["__getattribute__"] = limit_getattr
namespace["maxattrs"] = maxattrs
namespace["attr_names"] = tuple()
namespace["__slots__"] = tuple(f"attr_{i}" for i in range(maxattrs))
return super().__new__(mcls, name, bases, namespace)
And here is the code above being used in an interactive session:
In [81]: class A(metaclass=LimitAttrs):
...: pass
...:
In [82]: a = A()
In [83]: a.aa = 1
In [84]: a.bb = 2
In [85]: a.cc = 3
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
[...]
AttributeError: Class A can't hold attribute cc: max attributes exceeded!
In [86]: b = A()
In [87]: b.aa
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
Input In [87], in <cell line: 1>()
----> 1 b.aa
[...]
AttributeError: ...
In [88]: b.aa = 3
In [89]: b.cc = 4
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
Input In [89], in <cell line: 1>()
[...]
AttributeError: Class A can't hold attribute cc: max attributes exceeded!
__slots__?