__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.