Abel Gutiérrez
Abel Gutiérrez

Reputation: 450

Python dataclasses: assign variable to field in a frozen instance from input

I want to create an immutable class that reads a file and do other things. I have problems with the mutability:

from dataclasses import dataclass

import io


@dataclass(frozen=True)
class Book:
    
    filename: str
    #file: io.TextIOWrapper
    
    def __new__(cls, filename):
        
        self = super().__new__(cls)
        
        self.file = open(filename, "r")
        
        return self
    
    
    def __post_init__(self):
        
        #self.file = open(self.filename, "r")
        
        pass
    
    
    def close(self):
        
        self.file.close()

book = Book("testfile.txt")
book.close()
print(book)

This is the error I get:

Traceback (most recent call last):
  File "D:\Sync1\Code\Python3\EconoPy\Version_0.2\test.py", line 32, in <module>
    book = Book("testfile.txt")
  File "D:\Sync1\Code\Python3\EconoPy\Version_0.2\test.py", line 17, in __new__
    self.file = open(filename, "r")
  File "<string>", line 4, in __setattr__
dataclasses.FrozenInstanceError: cannot assign to field 'file'

I want to set the attribute self.file from the input filename, but the 'frozening' is forbidding that. With the __post_init__ I can do that if I remove the 'frozening'.

Upvotes: 1

Views: 2017

Answers (2)

nigh_anxiety
nigh_anxiety

Reputation: 2327

One option here which would avoid calling object.__setattr__ would be to do the following:

  1. Set the file field to have a default value of None
  2. Add an open function which returns a replacement dataclass object that has the same filename but opens the file and stores it in the file field of the new object.
import dataclasses
from dataclasses import dataclass, field

import io

@dataclass(frozen=True)
class Book:

    filename: str
    file: io.TextIOWrapper = field(default=None)

    def open(self):
        return dataclasses.replace(self, file=open(self.filename))

    def close(self):
        self.file.close()


book = Book("testfile.txt")
book = book.open()
book.close()
print(book)  # Book(filename='testfile.txt', file=<_io.TextIOWrapper name='testfile.txt' mode='r' encoding='cp1252'>)

Got the idea from this article: https://python.plainenglish.io/why-and-how-to-write-frozen-dataclasses-in-python-69050ad5c9d4

Upvotes: 2

Anentropic
Anentropic

Reputation: 33833

At the moment this looks like an inappropriate use of a dataclass.

Opening the file when the class is instantiated and having a close method... this looks like a glorified file object so far - maybe it would be better as a subclass of TextIOWrapper?

Or as a context manager? https://docs.python.org/3/library/stdtypes.html#typecontextmanager

Anyway, for sake of answering your question as asked, we can find the solution in the docs here https://docs.python.org/3/library/dataclasses.html#frozen-instances ...we can bypass the enforcement of immutability by using object.__setattr__.

So a working example would look like:

import io
from dataclasses import dataclass, field


@dataclass(frozen=True)
class Book:
    
    filename: str
    file: io.TextIOWrapper = field(init=False)
    
    def __post_init__(self):
        object.__setattr__(self, "file", open(self.filename, "r"))

    def close(self):
        self.file.close()

Upvotes: 2

Related Questions