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.setterexists, and calling it - Then, the
@property.setteris 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.
