Having a mix of mutable/immutable "data container" python

68 views Asked by At

I was wondering if there's a way to define within the same "data container" values of mutable/immutable/default values.

by "data container" I mean any things like tuple, dict, class, Enum, dataclass

this example makes my point clear:

if I want to have Page with first_page current_page last_page as values.

if I would go with the dataclass method the implementation would be something like this:

from dataclasses import dataclass

@dataclass(slots=True)
class Pages:
    first_page: int = 0 # need to be immutable
    current_page: int   # need to be initialized and mutable
    last_page: int      # need to be initialized and immutable

can this be done? or is there a way to do it?

3

There are 3 answers

1
Matt Pitkin On

For a class, you can use getters and setters. For example, to have first_page be immutable, you can create a quasi-private property called _first_page (it's not actually private in Python), that you access via the (getter) property first_page attribute:

class Pages:
    _first_page: int = 0
    
    @property
    def first_page(self):
        return self._first_page

With this, you can get first_page, but if you try setting first_page you will get an AttributeError:

p = Pages()
print(p.first_page)
0
p.first_page = 2
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[17], line 1
----> 1 p.first_page = 2

AttributeError: can't set attribute

You could have an equivalent setter attribute that explicitly returns a message that first_page is immuatble and does nothing, e.g.,:

class Pages:
    _first_page: int = 0
    
    @property
    def first_page(self):
        return self._first_page

    @first_page.setter
    def first_page(self, _):
        print("first_pass is immutable")
        pass

For values that you want to be mutable, you can just set the class attributes as you normally would, or (as above) use the @property and @setter decorated functions to set the equivalent quasi-private attributes.

1
Yuri R On

You can use field from dataclasses.

@dataclass(frozen=True)  # Making the dataclass frozen makes its fields immutable.
class Pages:
    _last_page: int = field(repr=False)  # Private field, and won't be shown in repr.
    _first_page: int = field(default=0, repr=False)
    current_page: int = field(default_factory=int, hash=False)  # Making it mutable by setting hash to False.

    @property
    def first_page(self) -> int:  # Property for the first_page to allow only reading.
        return self._first_page

    @property
    def last_page(self) -> int:  # Property for the last_page to allow only reading.
        return self._last_page

Testing

p = Pages(current_page=5, _last_page=10)
print(p.first_page)  # 0
print(p.current_page)  # 5
print(p.last_page)  # 10

# This will raise an exception: dataclasses.FrozenInstanceError
# p.first_page = 2

The frozen=True ensures that the attributes remain immutable unless otherwise specified.

0
link89 On

You can use the Final type annotation. Though it won't change the actual runtime behavior, at least your type checker will raise error when you try to overwrite the value.

from dataclasses import dataclass
from typing import Final

@dataclass(slots=True)
class Pages:
    current_page: int   # need to be initialized and mutable
    last_page: Final[int]      # need to be initialized and immutable
    first_page: Final[int] = 0 # need to be immutable