Search code examples
pythonpropertiespython-dataclassessetattr

Python: trying to access instance attributes in self.__setattr__


I was trying to create a dataclass with shorter aliases for longer fields. Since I wanted to be able to add new fields (with long and short names) without having to write a @property decorated "getter" and a @short_name.setter decorated "setter", I decided to use a dictionary mapping long names to short names, and implement custom __getattr__ and __setattr__ methods. However, my kernel crashed (when running the code in JupyterLab) / I got a Recursion error (when running the script). After some trial and error, it seems I cannot access any instance or class attribute via self inside the __setattr__ method. My current (failing) implementation is

@dataclass
class Foo:
    very_long_name_of_a_number: int
    very_long_name_of_a_string: str

    short_name_map: dict = field(
        default_factory=lambda: {
            "number": "very_long_name_of_a_number",
            "string": "very_long_name_of_a_string",
        },
        init=False,
        repr=False,
    )

    def __getattr__(self, name):
        if name in self._short_name_map:
            return getattr(self, self._short_name_map[name])
        raise AttributeError(
            f"'{self.__class__.__name__}' object has no attribute '{name}'"
        )

    def __setattr__(self, name, value):
        short_name = self.short_name_map[name]
        self.__dict__[short_name ] = value

Two questions arise:

  1. Where does the problem come from? I see no evil in accessing attributes of self inside .__setattr__, but I am fairly sure that is the problem (renaming __setattr__ solves the problem, as does deleting the reference to self.short_name_map).
  2. How could I work around this problem? Or is there some underlying paradigm I did not consider?

For the record: this is the behaviour I wanted to have, but without having to write the two decorated functions for every field:

@dataclass
class Foo:
    very_long_name_of_a_number: int

    @property
    def number(self) -> int:
        return self.very_long_name_of_a_number

    @number.setter
    def short_name(self, value: int):
        self.very_long_name_of_a_number= value

Solution

  • What you’re trying to do is extremely bad practice. Anyone who inspect your class and finds neither a property, nor a getter/setter will be truly confused.

    Explicit is better than implicit.

    The only good approach is the one you mentioned in the question:

    @dataclass
    class Foo:
        very_long_name_of_a_number: int
    
        @property
        def number(self) -> int:
            return self.very_long_name_of_a_number
    
        @number.setter
        def number(self, value: int):
            self.very_long_name_of_a_number = value
    

    I highly recommend using it for your purposes, no matter how many attributes you have and how long their names are.

    You may also want to consider using Pydantic instead of dataclass, as it provides an alias attribute that may be useful to you:

    from pydantic import BaseModel, Field
    
    
    class Foo(BaseModel):
        very_long_name_of_a_number: int = Field(alias="number")
    
    
    foo = Foo(number=2)
    

    It doesn't quite cover cases you described but can be useful for initialization, serializing / deserializing by alias.