Home > Mobile >  Correct way of updating __repr__ in Python using dataclasses and inheritance
Correct way of updating __repr__ in Python using dataclasses and inheritance

Time:12-13

I have the following code:

from dataclasses import MISSING, asdict, dataclass
from typing import Any
from datetime import datetime


@dataclass()
class BookMetadata():
    '''Parent class.'''
    
    isbn: str 
    title: str 
    author: str
    publisher: str
    date_published: int
    
    
    def format_time(self, unix: int) -> str:
        '''Convert unix time to %Y-%m-%d.'''
        
        return datetime.fromtimestamp(int(str(unix)[0:10])).strftime('%Y-%m-%d')
    

    def __post_init__(self):
        '''Change attributes after assignment.'''
            
        # Change date from UNIX to YYYY-MM-DD
        self.date_published = self.format_time(self.date_published)
      

@dataclass()
class RetailPrice(BookMetadata):
    '''Child class.'''
    
    def __init__(self, 
                 isbn, title, author, publisher, date_published,
                 price_usd, price_aud, price_eur, price_gbp) -> None:

        self.price_usd: float = price_usd
        self.price_aud: float = price_aud
        self.price_eur: float = price_eur
        self.price_gbp: float = price_gbp
        
        BookMetadata.__init__(self, isbn, title, author, publisher, date_published)
        # Or: super(RetailPrice, self).__init__(isbn, title, author, publisher, date_published)
        
        
    def stringify(self, obj: Any) -> str:
        '''Turn object into string.'''
        
        return str(obj)
        
        
    def __post_init__(self):
        '''Change attribute values after assignment.'''
        self.price_usd = self.stringify(self.price_usd)
        

    def __repr__(self) -> str:
        '''Update representation including parent and child class attributes.'''
        
        return f'Retailprice(isbn={super().isbn}, title={super().title}, author={super().author}, publisher={super().publisher}, date_published={super().date_published}, price_usd={self.price_usd}, price_aud={self.price_aud}, price_eur={self.price_eur}, price_gbp={self.price_gbp})'

My __repr__ method is failing with the following message: AttributeError: 'super' object has no attribute 'isbn', so I am referencing the attributes of the parent class all wrong here.

As it's possible to call the parent dataclass under the __init__ method of the child dataclass, (BookMetadata.__init__(self, isbn, title, author, publisher, date_published)), I thought that trying with super(BookMetadata, self) would work, but it failed with the same message.

How should I reference the attributes of the parent class in __repr__ within the child dataclass?

CodePudding user response:

There's a lot wrong with that code. The field values are inconsistent with their declared types, the use of int(str(unix)[0:10]) is bizarre and likely to lead to wrong dates, RetailPrice largely abandons the use of dataclass fields in favor of writing out a bunch of __init__ arguments manually...

We'll go over this in two parts. One that just fixes the immediate issues, and one that shows what this should look like.


Part 1: Fixing the immediate issues.

For the __repr__ method, the most immediate issue is the attempt to access instance attributes through super(). Attributes don't work that way. An instance doesn't have separate "superclass attributes" and "child attributes"; it just has attributes, and they're all accessed through self, regardless of what class they were set in.

Your instance already has all the attributes you need. Contrary to your self-answer, you don't need to (and absolutely shouldn't) call super().__init__ or super().__post_init__ inside __repr__ to make the attributes available. You can just access them:

def __repr__(self) -> str:
    return f'RetailPrice(isbn={self.isbn}, title={self.title}, author={self.author}, publisher={self.publisher}, date_published={self.date_published}, price_usd={self.price_usd}, price_aud={self.price_aud}, price_eur={self.price_eur}, price_gbp={self.price_gbp})'

You'll see date_published as a Unix timestamp instead of a YYYY-MM-DD string, but that's because your subclass's __post_init__ doesn't call the superclass's __post_init__. Fix that:

def __post_init__(self):
    super().__post_init__()
    self.price_usd = self.stringify(self.price_usd)

and your __repr__ output will be okay.


Part 2: How the code should really be written.

You didn't need to write your own __repr__. That's one of the things that the @dataclass decorator is designed to handle for you. You didn't let @dataclass do its job, though. Instead of declaring fields and letting @dataclass generate code based on the fields, you wrote things manually.

If you just declared fields:

@dataclass
class BookWithRetailPrice(BookMetadata):
    price_usd: float
    price_aud: float
    price_eur: float
    price_gbp: float

@dataclass could have handled almost everything, generating a sensible __repr__, as well as __eq__ and __hash__.

I left out the part about stringifying price_usd, because that was weird, inconsistent with the other price fields, and inconsistent with the declared type of price_usd. You can do it in __post_init__ if you really want, but it's a bad idea.

Similarly, converting date_published to a YYYY-MM-DD string when the field is declared as an int is a bad idea. If you want a YYYY-MM-DD string, a property would probably make more sense:

@dataclass
class BookMetadata:
    isbn: str 
    title: str 
    author: str
    publisher: str
    publication_timestamp: int

    @property
    def publication_date_string(self):
        return datetime.fromtimestamp(self.publication_timestamp).strftime('%Y-%m-%d')

You'll note that with the @dataclass-generated __repr__, you'll see quotation marks around the values of string fields, like title. The generated __repr__ uses the __repr__ of field values, instead of calling str on fields. This is useful for reducing ambiguity, especially if any of the field values have commas in them. Unless you have a really strong reason to do otherwise, you should let @dataclass write __repr__ that way.

CodePudding user response:

This was actually simpler than I thought. All it takes is to add a __repr__ method to the child dataclass class, within which two calls to the parent dataclass are made:

  • super().__init__(...) to make the parent attributes accessible
  • super().__post_init__() to make visible the parent attributes modified after assignment

The complete working example is as follows:

from dataclasses import dataclass
from typing import Any
from datetime import datetime


@dataclass()
class BookMetadata():
    '''Parent class.'''
    
    isbn: str 
    title: str 
    author: str
    publisher: str
    date_published: int
    
    
    def format_time(self, unix: int) -> str:
        '''Convert unix time to %Y-%m-%d.'''
        
        return datetime.fromtimestamp(int(str(unix)[0:10])).strftime('%Y-%m-%d')
    

    def __post_init__(self):
        '''Change attributes after assignment.'''
            
        # Change date from UNIX to YYYY-MM-DD
        self.date_published = self.format_time(self.date_published)
      

@dataclass()
class RetailPrice(BookMetadata):
    '''Child class.'''
    
    def __init__(self, 
                 isbn, title, author, publisher, date_published,
                 price_usd, price_aud, price_eur, price_gbp) -> None:

        self.price_usd: float = price_usd
        self.price_aud: float = price_aud
        self.price_eur: float = price_eur
        self.price_gbp: float = price_gbp
        
        BookMetadata.__init__(self, isbn, title, author, publisher, date_published)
        # Or: super(RetailPrice, self).__init__(isbn, title, author, publisher, date_published)
        
        
    def stringify(self, obj: Any) -> str:
        '''Turn object into string.'''
        
        return str(obj)
        
        
    def __post_init__(self):
        '''Change attribute values after assignment.'''
        self.price_usd = self.stringify(self.price_usd)


    def __repr__(self) -> str:
        '''Update representation including parent and child class attributes.'''
        
        super().__init__(self.isbn, self.title, self.author, self.publisher, self.date_published)
        super().__post_init__()
        
        return f'RetailPrice(isbn={self.isbn}, title={self.title}, author={self.author}, publisher={self.publisher}, date_published={self.date_published}, price_usd={self.price_usd}, price_aud={self.price_aud}, price_eur={self.price_eur}, price_gbp={self.price_gbp})'

The output is now:

RetailPrice(isbn=1234-5678-9000, title=My book, author=Name Surname, publisher=My publisher, date_published=2022-12-08, price_usd=17.99, price_aud=23.99, price_eur=15.99, price_gbp=16.99)
  • Related