简体   繁体   中英

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'.

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()

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM