87

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?

DhiaTN
  • 7,835
  • 9
  • 48
  • 63
astrofrog
  • 26,293
  • 28
  • 81
  • 123
  • 1
    katrielalex brings good points. There's nothing hacky about it. You could avoid using `__setattr__` but that would probably be hacky. – aaronasterling Aug 30 '10 at 19:35
  • I don't see why this is hacky? It's the best solution I could come up with and a lot more succinct than some of the others proposed. – Chris B Aug 27 '16 at 11:06

12 Answers12

88

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
Jochen Ritzel
  • 94,379
  • 28
  • 188
  • 182
  • 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...) – weronika Sep 08 '11 at 06:58
  • 5
    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](https://hynek.me/articles/hasattr/). 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. – Bas Swinckels Dec 03 '16 at 13:24
38

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'

NB:

  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).
MarianD
  • 9,720
  • 8
  • 27
  • 44
DhiaTN
  • 7,835
  • 9
  • 48
  • 63
33

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
21

Actually, you don't want __setattr__, you want __slots__. Add __slots__ = ('foo', 'bar', 'baz') to the class body, and Python will make sure that there's only foo, bar and baz on any instance. But read the caveats the documentation lists!

  • 12
    Using `__slots__` works, but it will break serialization (e.g. pickle), among other things... It's usually a bad idea to use slots to control attribute creation, rather than reduce memory overhead, in my opinion, anyway... – Joe Kington Aug 30 '10 at 19:43
  • I know, and I hestiate to use it myself - but doing extra work to disallow new attributes is usually a bad idea, too ;) –  Aug 30 '10 at 19:45
  • 2
    Using `__slots__` also breaks multiple inheritance. A class can't inherit from more than one class that either defines __slots__ or hat an instance layout defined in C code (like `list`, `tuple` or `int`). – Feuermurmel Oct 01 '12 at 11:24
  • If `__slots__` breaks your pickles, you're using an ancient pickle protocol. Pass `protocol=-1` to pickle methods for the most recent protocol available, which is 2 in Python 2 ([introduced in 2003](https://www.python.org/dev/peps/pep-0307/)). The default and latest protocols in Python 3 (3 and 4 respectively) both handle `__slots__`. – Nick Matteo Mar 12 '19 at 23:07
  • well, most of the time i wind up regretting using pickle at all: https://www.benfrederickson.com/dont-pickle-your-data/ – Erik Aronesty Aug 27 '19 at 16:39
7

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

Katriel
  • 107,638
  • 19
  • 124
  • 160
6

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
Eran Friedman
  • 198
  • 2
  • 7
4

What about this:

class A():
    __allowed_attr=('_x', '_y')

    def __init__(self,x=0,y=0):
        self._x=x
        self._y=y

    def __setattr__(self,attribute,value):
        if not attribute in self.__class__.__allowed_attr:
            raise AttributeError
        else:
            super().__setattr__(attribute,value)
Clementerf
  • 49
  • 2
2

Here is approach i came up with that doesn't need a _frozen attribute or method to freeze() in init.

During init i just add all class attributes to the instance.

I like this because there is no _frozen, freeze(), and _frozen also does not show up in the vars(instance) output.

class MetaModel(type):
    def __setattr__(self, name, value):
        raise AttributeError("Model classes do not accept arbitrary attributes")

class Model(object):
    __metaclass__ = MetaModel

    # init will take all CLASS attributes, and add them as SELF/INSTANCE attributes
    def __init__(self):
        for k, v in self.__class__.__dict__.iteritems():
            if not k.startswith("_"):
                self.__setattr__(k, v)

    # setattr, won't allow any attributes to be set on the SELF/INSTANCE that don't already exist
    def __setattr__(self, name, value):
        if not hasattr(self, name):
            raise AttributeError("Model instances do not accept arbitrary attributes")
        else:
            object.__setattr__(self, name, value)


# Example using            
class Dog(Model):
    name = ''
    kind = 'canine'

d, e = Dog(), Dog()
print vars(d)
print vars(e)
e.junk = 'stuff' # fails
gswilcox01
  • 51
  • 2
  • 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. – Reinier Torenbeek Nov 21 '17 at 03:30
2

pystrict is a pypi installable decorator inspired by this stackoverflow question that can be used with classes to freeze them. There is an example to the README that shows why a decorator like this is needed even if you have mypy and pylint running on your project:

pip install pystrict

Then just use the @strict decorator:

from pystrict import strict

@strict
class Blah
  def __init__(self):
     self.attr = 1
Erik Aronesty
  • 8,927
  • 4
  • 46
  • 29
1

I like the "Frozen" of Jochen Ritzel. The inconvenient is that the isfrozen variable then appears when printing a Class.__dict I went around this problem this way by creating a list of authorized attributes (similar to slots):

class Frozen(object):
    __List = []
    def __setattr__(self, key, value):
        setIsOK = False
        for item in self.__List:
            if key == item:
                setIsOK = True

        if setIsOK == True:
            object.__setattr__(self, key, value)
        else:
            raise TypeError( "%r has no attributes %r" % (self, key) )

class Test(Frozen):
    _Frozen__List = ["attr1","attr2"]
    def __init__(self):
        self.attr1   =  1
        self.attr2   =  1
1

The FrozenClass by Jochen Ritzel is cool, but calling _frozen() when initialing a class every time is not so cool (and you need to take the risk of forgetting it). I added a __init_slots__ function:

class FrozenClass(object):
    __isfrozen = False
    def _freeze(self):
        self.__isfrozen = True
    def __init_slots__(self, slots):
        for key in slots:
            object.__setattr__(self, key, None)
        self._freeze()
    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)
class Test(FrozenClass):
    def __init__(self):
        self.__init_slots__(["x", "y"])
        self.x = 42#
        self.y = 2**3


a,b = Test(), Test()
a.x = 10
b.z = 10 # fails
mnille
  • 1,332
  • 4
  • 13
  • 17
Endle_Zhenbo
  • 488
  • 4
  • 19
0

None of the answers mention the performance impact of overriding __setattr__, which can be an issue when creating many small objects. (And __slots__ would be the performant solution but limits pickle/inheritance).

So I came up with this variant which installs our slower settatr after init:

class FrozenClass:

    def freeze(self):
        def frozen_setattr(self, key, value):
            if not hasattr(self, key):
                raise TypeError("Cannot set {}: {} is a frozen class".format(key, self))
            object.__setattr__(self, key, value)
        self.__setattr__ = frozen_setattr

class Foo(FrozenClass): ...

If you don't want to call freeze at the end of __init__, if inheritance is an issue, or if you don't want it in vars(), it can also be adapted: for example here is a decorator version based on the pystrict answer:

import functools
def strict(cls):
    cls._x_setter = getattr(cls, "__setattr__", object.__setattr__)
    cls._x_init = cls.__init__
    @functools.wraps(cls.__init__)
    def wrapper(self, *args, **kwargs):
        cls._x_init(self, *args, **kwargs)
        def frozen_setattr(self, key, value):
            if not hasattr(self, key):
                raise TypeError("Class %s is frozen. Cannot set '%s'." % (cls.__name__, key))
            cls._x_setter(self, key, value)
        cls.__setattr__ = frozen_setattr
    cls.__init__ = wrapper
    return cls

@strict
class Foo: ...
eddygeek
  • 3,270
  • 2
  • 20
  • 28