Skip to content

Freezing an attribute in __attrs_post_init__ #1412

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
jacopoabramo opened this issue Feb 26, 2025 · 2 comments
Closed

Freezing an attribute in __attrs_post_init__ #1412

jacopoabramo opened this issue Feb 26, 2025 · 2 comments

Comments

@jacopoabramo
Copy link

jacopoabramo commented Feb 26, 2025

I have a class like this:

from attrs import define, field, validators

@define
class LightModelInfo:

    wavelength: int = field(
        validator=validators.instance_of(int),
        metadata={"description": "Wavelength in nm."},
    )
    wavecolor: str = field(
        init=False,
        metadata={"description": "Hexadecimal representation of the light color."},
    )

    def __attrs_post_init__(self) -> None:
        # this should be frozen after being computed
        self.wavecolor = wavelength_to_hex(self.wavelength)

I would like to freeze the wavecolor attribute during post initialization, or an alternative approach which allows me to compute the value of wavecolor based on wavelength and then freeze it. Is it possible?

@redruin1
Copy link

redruin1 commented Feb 26, 2025

It sounds like you want your wavecolor attribute to be a read-only attribute computed off of wavelength. If this is the case, the simplest solution is to make it a property with only a getter:

@define
class LightModelInfo:
    wavelength: int = field(
        validator=validators.instance_of(int),
        metadata={"description": "Wavelength in nm."},
    )

    @property
    def wavecolor(self) -> str:
        return wavelength_to_hex(self.wavelength)

Of course, this calculates the value every time you access it, which might not be ideal. You can combat this with functools.lru_cache as long as your model is hashable:

from functools import lru_cache

@define(hash=True) # add this here or implement your own __hash__
class LightModelInfo:
    wavelength: int = field(
        validator=validators.instance_of(int),
        metadata={"description": "Wavelength in nm."},
    )

    @property
    @lru_cache
    def wavecolor(self) -> str:
        return wavelength_to_hex(self.wavelength)

This will cache frequently accessed instances automatically, essentially bringing it in line with primitive attribute getting. The downside of these methods is that it prevents the wavelength property from inheriting the other goodies from attrs (such as a meaningful repr for example), but this is by far the most conventional way to do this, so I would go with this if you can.

If you do need all the associated helpers with wavecolor as a proper attribute, then you can use a custom default function to populate it along side a custom on_setattr to essentially mimic a frozen attribute:

def read_only(self, attr, value):
    raise AttributeError(f"{attr.name} is read-only")

@define
class LightModelInfo:
    wavelength: int = field(
        validator=validators.instance_of(int),
        metadata={"description": "Wavelength in nm."},
    )
    wavecolor: str = field(
        init=False,
        on_setattr=read_only, # make sure attempting to set this *specific* attribute results in an error
        metadata={"description": "Hexadecimal representation of the light color."},
    )
    @wavecolor.default
    def calculate_wavecolor(self):
        return wavelength_to_hex(self.wavelength) # wavelength is populated by this point

a = LightModelInfo(wavelength=750)
print(a) # LightModelInfo(wavelength=750, wavecolor='red')
a.wavecolor = "blue" # AttributeError: wavecolor is read-only

Note that with this method modifying wavelength after instantiation does not modify wavecolor:

a.wavelength = 450
print(a)  # LightModelInfo(wavelength=450, wavecolor='red')

Which follows the axiom that frozen attributes cannot change after initialization. The property methods above don't have this limitation, and will properly work with any dynamic wavelength value.

@jacopoabramo
Copy link
Author

@redruin1 wonderful, thank you so much for the in-depth explanation. This is exactly what I needed. Closing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants