Prevent creating new attributes outside __init__

28,735

Solution 1

I wouldn't use __dict__ directly, but you can add a function to explicitly "freeze" a instance:

class FrozenClass(object):
    __isfrozen = False
    def __setattr__(self, key, value):
        if self.__isfrozen and not hasattr(self, key):
            raise TypeError( "%r is a frozen class" % self )
        object.__setattr__(self, key, value)

    def _freeze(self):
        self.__isfrozen = True

class Test(FrozenClass):
    def __init__(self):
        self.x = 42#
        self.y = 2**3

        self._freeze() # no new attributes after this point.

a,b = Test(), Test()
a.x = 10
b.z = 10 # fails

Solution 2

Slots is the way to go:

The pythonic way is to use slots instead of playing around with the __setter__. While it may solve the problem, it does not give any performance improvement. The attributes of objects are stored in a dictionary "__dict__", this is the reason, why you can dynamically add attributes to objects of classes that we have created so far. Using a dictionary for attribute storage is very convenient, but it can mean a waste of space for objects, which have only a small amount of instance variables.

Slots are a nice way to work around this space consumption problem. Instead of having a dynamic dict that allows adding attributes to objects dynamically, slots provide a static structure which prohibits additions after the creation of an instance.

When we design a class, we can use slots to prevent the dynamic creation of attributes. To define slots, you have to define a list with the name __slots__. The list has to contain all the attributes, you want to use. We demonstrate this in the following class, in which the slots list contains only the name for an attribute "val".

class S(object):

    __slots__ = ['val']

    def __init__(self, v):
        self.val = v


x = S(42)
print(x.val)

x.new = "not possible"

=> It fails to create an attribute "new":

42 
Traceback (most recent call last):
  File "slots_ex.py", line 12, in <module>
    x.new = "not possible"
AttributeError: 'S' object has no attribute 'new'

Notes:

  1. Since Python 3.3 the advantage optimizing the space consumption is not as impressive any more. With Python 3.3 Key-Sharing Dictionaries are used for the storage of objects. The attributes of the instances are capable of sharing part of their internal storage between each other, i.e. the part which stores the keys and their corresponding hashes. This helps to reduce the memory consumption of programs, which create many instances of non-builtin types. But still is the way to go to avoid dynamically created attributes.
  1. Using slots come also with it's own cost. It will break serialization (e.g. pickle). It will also break multiple inheritance. A class can't inherit from more than one class that either defines slots or has an instance layout defined in C code (like list, tuple or int).

Solution 3

If someone is interested in doing that with a decorator, here is a working solution:

from functools import wraps

def froze_it(cls):
    cls.__frozen = False

    def frozensetattr(self, key, value):
        if self.__frozen and not hasattr(self, key):
            print("Class {} is frozen. Cannot set {} = {}"
                  .format(cls.__name__, key, value))
        else:
            object.__setattr__(self, key, value)

    def init_decorator(func):
        @wraps(func)
        def wrapper(self, *args, **kwargs):
            func(self, *args, **kwargs)
            self.__frozen = True
        return wrapper

    cls.__setattr__ = frozensetattr
    cls.__init__ = init_decorator(cls.__init__)

    return cls

Pretty straightforward to use:

@froze_it 
class Foo(object):
    def __init__(self):
        self.bar = 10

foo = Foo()
foo.bar = 42
foo.foobar = "no way"

Result:

>>> Class Foo is frozen. Cannot set foobar = no way

Solution 4

The proper way is to override __setattr__. That's what it's there for.

Solution 5

I like very much the solution that uses a decorator, because it's easy to use it for many classes across a project, with minimum additions for each class. But it doesn't work well with inheritance. So here is my version: It only overrides the __setattr__ function - if the attribute doesn't exist and the caller function is not __init__, it prints an error message.

import inspect                                                                                                                             

def froze_it(cls):                                                                                                                      

    def frozensetattr(self, key, value):                                                                                                   
        if not hasattr(self, key) and inspect.stack()[1][3] != "__init__":                                                                 
            print("Class {} is frozen. Cannot set {} = {}"                                                                                 
                  .format(cls.__name__, key, value))                                                                                       
        else:                                                                                                                              
            self.__dict__[key] = value                                                                                                     

    cls.__setattr__ = frozensetattr                                                                                                        
    return cls                                                                                                                             

@froze_it                                                                                                                                  
class A:                                                                                                                                   
    def __init__(self):                                                                                                                    
        self._a = 0                                                                                                                        

a = A()                                                                                                                                    
a._a = 1                                                                                                                                   
a._b = 2 # error
Share:
28,735
astrofrog
Author by

astrofrog

Updated on June 12, 2021

Comments

  • astrofrog
    astrofrog about 3 years

    I want to be able to create a class (in Python) that once initialized with __init__, does not accept new attributes, but accepts modifications of existing attributes. There's several hack-ish ways I can see to do this, for example having a __setattr__ method such as

    def __setattr__(self, attribute, value):
        if not attribute in self.__dict__:
            print "Cannot set %s" % attribute
        else:
            self.__dict__[attribute] = value
    

    and then editing __dict__ directly inside __init__, but I was wondering if there is a 'proper' way to do this?

  • astrofrog
    astrofrog almost 14 years
    What is then the proper way to set variables in __init__ ? Is it to set them in __dict__ directly?
  • Katriel
    Katriel almost 14 years
    I would override __setattr__ in __init__, by self.__setattr__ = <new-function-that-you-just-defined>.
  • weronika
    weronika almost 13 years
    Very cool! I think I'll grab that bit of code and start using it. (Hmm, I wonder if it could be done as a decorator, or if that wouldn't be a good idea...)
  • Ethan Furman
    Ethan Furman over 12 years
    @katrielalex: that won't work for new-style classes as __xxx__ methods are only looked up on the class, not on the instance.
  • Tomasz Gandor
    Tomasz Gandor about 8 years
    +1 for the decorator version. That's what I would use for a larger project, in a larger script this is overkill (maybe if they had it in standard library...). For now there's only "IDE style warnings".
  • mrgiesel
    mrgiesel about 8 years
    How does this solution works with heritage? e.g. if I have a child class of Foo, this child is by default a frozen class?
  • Bas Swinckels
    Bas Swinckels over 7 years
    Late comment: I was using this recipe successfully for some time, until I changed an attribute to a property, where the getter was raising a NotImplementedError. It took me a long time to find out that this was due to the fact that hasattr actuall calls getattr, trows away the result and returns False in case of errors, see this blog. Found a workaround by replacing not hasattr(self, key) by key not in dir(self). This might be slower, but solved the problem for me.
  • winni2k
    winni2k almost 7 years
    Is there a pypi package for this decorator?
  • Reinier Torenbeek
    Reinier Torenbeek over 6 years
    This does not seem to work if one of the fields is a list. Let's say names=[]. Then d.names.append['Fido'] will insert 'Fido' in both d.names and e.names. I do not know enough about Python to understand why.
  • Ivan Nechipayko
    Ivan Nechipayko about 4 years
    How one can enhance the decorator so it works for inherited classes?
  • F. Eser
    F. Eser over 2 years
    Is there any other way we can apply the same technique to more sub-classes? For example __init__ method of a new sub-class `Test2(Test)? The only solution I come up with is to unfreeze the class in the beginning of init and freeze it at the end. But this is a complete mess. Do you have any other clever ideas?