Properties in Python is a nice syntactic sugar for accessing attributes of an object/class, actually functioning as the setters, getters, etc of typical OOP paradigm. Though, since properties are syntactic sugar, they actually are using uderlying mechanisms of Python. For example, every object in Python has the method .__setattr__
, which is called to directly set the an attribute of the object, either creating the attribute or altering its value.
Having said the above, this post is about the question:
if in Python one uses:
@property def a(self): return self._a @a.setter def a(self, new_val): self._a = a
then, can one override it using self.__setattr__
? In other words, what is the order of execution for property setter and self.__setattr__
?
To investigate the above, below is a snippet and its corresponding result
# -*- coding: utf-8 -*- """Snippet for checking the __setattr__ order of execution. """ __all__ = ["Foo"] __docformat__ = "reStructuredText" __author__ = "Konstantinos Drosos" from pathlib import Path import traceback class Foo(object): def __init__(self) -> None: self._a = -1 @property def a(self): return self._a @a.setter def a(self, n): print(f" - Called setter `Foo.a` with value: {n}", flush=True) self._show_tracebak(traceback.extract_stack()) self._a = n def __setattr__(self, name: str, value) -> None: print( f" - Called `__setattr__` with name: {name} and value: {value}", flush=True ) self._show_tracebak(traceback.extract_stack()) return super().__setattr__(name, value) @staticmethod def _show_tracebak(trace_stack: traceback.StackSummary) -> None: print(" * Traceback:") for f_summary in trace_stack[:-1]: f_name = Path(f_summary.filename).name l_no = f_summary.lineno line = f_summary.line name = f_summary.name p = len("__setattr__") - len(name) print(f" ** {f_name}, line {l_no}, {name}{' ' * p}: {line}") def main(): print("-" * 20) print("* Initializing object", flush=True) foo = Foo() print("-" * 20) print("* Will now use property setter foo.a", flush=True) foo.a = 10 print("-" * 20) print("* Now altering property with `setattr`", flush=True) setattr(foo, "a", 20) print("-" * 20) print("* Now altering the _a property directly with `setattr`", flush=True) setattr(foo, "_a", 30) if __name__ == "__main__": main()
which outputs:
$ python main.py -------------------- * Initializing object - Called `__setattr__` with name: _a and value: -1 * Traceback: ** test.py, line 69, module : main() ** test.py, line 53, main : foo = Foo() ** test.py, line 19, __init__ : self._a = -1 -------------------- * Will now use property setter foo.a - Called `__setattr__` with name: a and value: 10 * Traceback: ** test.py, line 69, module : main() ** test.py, line 57, main : foo.a = 10 - Called setter `Foo.a` with value: 10 * Traceback: ** test.py, line 69, module : main() ** test.py, line 57, main : foo.a = 10 ** test.py, line 34, __setattr__: return super().__setattr__(name, value) - Called `__setattr__` with name: _a and value: 10 * Traceback: ** test.py, line 69, module : main() ** test.py, line 57, main : foo.a = 10 ** test.py, line 34, __setattr__: return super().__setattr__(name, value) ** test.py, line 29, a : self._a = n -------------------- * Now altering property with `setattr` - Called `__setattr__` with name: a and value: 20 * Traceback: ** test.py, line 69, module : main() ** test.py, line 61, main : setattr(foo, "a", 20) - Called setter `Foo.a` with value: 20 * Traceback: ** test.py, line 69, module : main() ** test.py, line 61, main : setattr(foo, "a", 20) ** test.py, line 34, __setattr__: return super().__setattr__(name, value) - Called `__setattr__` with name: _a and value: 20 * Traceback: ** test.py, line 69, module : main() ** test.py, line 61, main : setattr(foo, "a", 20) ** test.py, line 34, __setattr__: return super().__setattr__(name, value) ** test.py, line 29, a : self._a = n -------------------- * Now altering the _a property directly with `setattr` - Called `__setattr__` with name: _a and value: 30 * Traceback: ** test.py, line 69, module : main() ** test.py, line 65, main : setattr(foo, "_a", 30)
As one can see, what happens when the foo.a=10
is called, is that:
- The
__setattr__
method is called, and the execution is passed to thesuper()
object. - The
super()
object is responsible to identify that a@property.setter
exists, and calling it - Then, the
@property.setter
is actually calling the__setattr__
method of the object, in order to finally alter the value of the attribute.
Oddly enough, the above order is the exact one when the changing of the attribute is performed with the setattr(foo, "a", 20)
. This clearly shows that the @property.setter
is indeed a syntactic sugar and ther base mechanism for alterning the properties in an object is the __setattr__
magic method. Additionally, the above show that the @property
actually creates a method, which is a shorthand-call for the __setattr__.
The above is further justified from the case setattr("foo", "_a", 20)
(note the underscore at the "_a"
). In this final case, the super() object does not call the @property.setter
, since the attribute is not bind to a method.