Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How does __setattr__ work with class attributes?

Tags:

python

I am trying to understand how exactly __setattr__ works with class attributes. This question came about when I had attempted to override __setattr__ to prevent attributes from being written to in a simple class.

My first attempt used instance level attributes as follows:

class SampleClass(object):

    def __init__(self):
        self.PublicAttribute1 = "attribute"

    def __setattr__(self, key, value):
        raise Exception("Attribute is Read-Only")

I had originally thought that only external attempts to set the attribute would throw an error, but even the statement inside the __init__ caused the exception to be thrown.

Later I ended up finding another way to do this, while attempting to solve a different problem. What I ended up with was:

class SampleClass(object):
    PublicAttribute1 = "attribute"

    def __setattr__(self, key, value):
        raise Exception("Attribute is Read-Only")

I expected to get the same result, but to my surpirse I was able to set the class attribute while preventing changes from being made after the initial declaration.

I don't understand why this works though. I know it has to do with class vs. instance variables. My theory is that using __setattr__ on a class variable will create an instance variable of the same name, since I believe I think I have seen this behavior before, but I am not sure.

Can anyone explain exactly what is happening here?

like image 277
Chris Beaulieu Avatar asked Sep 26 '16 17:09

Chris Beaulieu


4 Answers

__setattr__() applies only to instances of the class. In your second example, when you define PublicAttribute1, you are defining it on the class; there's no instance, so __setattr__() is not called.

N.B. In Python, things you access using the . notation are called attributes, not variables. (In other languages they might be called "member variables" or similar.)

You're correct that the class attribute will be shadowed if you set an attribute of the same name on an instance. For example:

class C(object):
    attr = 42

 c = C()
 print(c.attr)     # 42
 c.attr = 13
 print(c.attr)     # 13
 print(C.attr)     # 42

Python resolves attribute access by first looking on the instance, and if there's no attribute of that name on the instance, it looks on the instance's class, then that class's parent(s), and so on until it gets to object, the root object of the Python class hierarchy.

So in the example above, we define attr on the class. Thus, when we access c.attr (the instance attribute), we get 42, the value of the attribute on the class, because there's no such attribute on the instance. When we set the attribute of the instance, then print c.attr again, we get the value we just set, because there is now an attribute by that name on the instance. But the value 42 still exists as the attribute of the class, C.attr, as we see by the third print.

The statement to set the instance attribute in your __init__() method is handled by Python like any code to set an attribute on an object. Python does not care whether the code is "inside" or "outside" the class. So, you may wonder, how can you bypass the "protection" of __setattr__() when initializing the object? Simple: you call the __setattr__() method of a class that doesn't have that protection, usually your parent class's method, and pass it your instance.

So instead of writing:

self.PublicAttribute1 = "attribute"

You have to write:

 object.__setattr__(self, "PublicAttribute1", "attribute")

Since attributes are stored in the instance's attribute dictionary, named __dict__, you can also get around your __setattr__ by writing directly to that:

 self.__dict__["PublicAttribute1"] = "attribute"

Either syntax is ugly and verbose, but the relative ease with which you can subvert the protection you're trying to add (after all, if you can do that, so can anyone else) might lead you to the conclusion that Python doesn't have very good support for protected attributes. In fact it doesn't, and this is by design. "We're all consenting adults here." You should not think in terms of public or private attributes with Python. All attributes are public. There is a convention of naming "private" attributes with a single leading underscore; this warns whoever is using your object that they're messing with an implementation detail of some sort, but they can still do it if they need to and are willing to accept the risks.

like image 81
kindall Avatar answered Nov 01 '22 08:11

kindall


The __setattr__ method defined in a class is only called for attribute assignments on istances of the class. It is not called for class variables, since they're not being assigned on an instance of the class with the method.

Of course, classes are instances too. They're instances of type (or a custom metaclass, usually a subclass of type). So if you want to prevent class variable creation, you need to create a metaclass with a __setattr__ method.

But that's not really what you need to make your class do what you want. To just get a read-only attribute that can only be written once (in the __init__ method), you can probably get by with some simpler logic. One approach is to set another attribute at the end of the __init__ method, which tells __setattr__ to lock down assignments after it is set:

class Foo:
    def __init__(self, a, b):
        self.a = a
        self.b = b
        self._initialized = True

    def __setattr__(self, name, value):
        if self.__dict__.get('_initialized'):
            raise Exception("Attribute is Read-Only")
        super().__setattr__(name, value)

Another option would be to use property descriptors for the read-only attributes and store the real values in "private" variables that can be assigned to normally. You'd not use __setattr__ in this version:

class Foo():
    def __init__(a, b):
        self._a = a
        self._b = b

    @property
    def a(self):
        return self._a

    @property
    def b(self):
        return self._b
like image 30
Blckknght Avatar answered Nov 01 '22 09:11

Blckknght


__setattr__ is only invoked on instances of a class and not the actual class itself. However, the class is still an object, so when setting the attribute of the class the __setattr__ of the class of the class is invoked.

To change how attributes are set on classes you need to look into meta-classes. Metaclasses are overkill for almost any application, and can get confusing very easily. And trying to make a user-defined class/object read-only is normally a bad idea. That said, here is a simple example.

>>> class SampleMetaclass(type): # type as parent, not object
...     def __setattr__(self, name, value):
...         raise AttributeError("Class is read-only")
... 
>>> class SampleClass(metaclass=SampleMetaclass):
...     def __setattr__(self, name, value):
...         raise AttributeError("Instance is read-only")
... 
>>> SampleClass.attr = 1
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in __setattr__
AttributeError: Class is read-only
>>> s = SampleClass()
>>> s.attr = 1
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in __setattr__
AttributeError: Instance is read-only
like image 42
Dunes Avatar answered Nov 01 '22 10:11

Dunes


The below can be found in the python documentation

Attribute assignments and deletions update the instance’s dictionary, never a class’s dictionary. If the class has a __setattr__() or __delattr__() method, this is called instead of updating the instance dictionary directly.

As you can clearly see the __setattr__() changes the instances dictionary when it is called. So whenever you try to assign a self.instance it raises your exception Exception("Attribute is Read-Only"). Whereas that's not the case with setting a class's attribute so no exception is raised.

like image 44
Webman1337 Avatar answered Nov 01 '22 09:11

Webman1337