14

We have this class:

from dataclasses import dataclass, field
from datetime import datetime
from typing import List, Dict

@dataclass
class BoardStaff:
    date: str = datetime.now()
    fullname: str
    address: str

    ## attributes to be excluded in __str__:
    degree: str
    rank: int = 10
    badges: bool = False
    cases_dict: Dict[str, str] = field(default_factory=dict)
    cases_list: List[str] = field(default_factory=list)

Emp = BoardStaff('Jack London', address='Unknown', degree='MA')

As BoardStaff is a dataclass, one can easily do print(Emp) to receive:
BoardStaff(fullname='Jack London', address='Unknown', degree='MA', rank=10, badges=False, cases={}, date=datetime.datetime(2021, 8, 10, 11, 36, 50, 693428)).

However, I want some attributes (i.e. the last 5 ones) to be excluded from the representation, so I had to define __str__ method and manually exclude some attributes like so:

    def __str__(self):
        str_info = {
            k: v
            for k, v in self.__dict__.items()
            if k not in ['degree', 'rank', 'other'] and v
        }
        return str(str_info)

But is there a better way to do the exclusion, like using some parameters when defining the attributes?

Alex Waygood
  • 6,304
  • 3
  • 24
  • 46
nino
  • 554
  • 2
  • 7
  • 20
  • 4
    Use of a leading underscore is favoured by some people to denote that variables named in that way should not be directly accessed by users of the class. What you could do is use this "convention" to indicate that variables named in that way are not included in the str dunder function. Thus, in your dictionary comprehension you could check the attribute's name to see if it starts with '_' and ignore/exclude it –  Aug 10 '21 at 07:31
  • @DarkKnight You're right, but the problem is these attributes/variables are excluded only in __str__ and I'm going to actually use them in other methods. – nino Aug 10 '21 at 07:55
  • 1
    @nino If you declare a class variable as, for example, _a then you just refer to it as self._a There's nothing special about using such names - it's just a convention. Using leading double underscore is another matter though –  Aug 10 '21 at 08:00
  • @DarkKnight I agree. Also. your way seems to be much cleaner than assigning `repr=False` for every single attribute. But I have to decide between the two, because: 1) the number of initial attributes is rather high (15), and 2) they are repeatedly used throughout the script. – nino Aug 10 '21 at 09:11

1 Answers1

24

Obvious solution

Simply define your attributes as fields with the argument repr=False:

from dataclasses import dataclass, field
from datetime import datetime
from typing import List, Dict

@dataclass
class BoardStaff:
    date: str = datetime.now()
    fullname: str
    address: str

    ## attributes to be excluded in __str__:
    degree: str = field(repr=False)
    rank: int = field(default=10, repr=False)
    badges: bool = field(default=False, repr=False)
    cases_dict: Dict[str, str] = field(default_factory=dict, repr=False)
    cases_list: List[str] = field(default_factory=list, repr=False)

Emp = BoardStaff('Jack London', address='Unknown', degree='MA')

This works nicely alongside marking attributes as "private" by giving them names starting with leading underscores, as others have suggested in the comments.

More advanced solutions

If you're looking for a more general solution that doesn't involve defining so many fields with repr=False, you could do something like this. It's pretty similar to the solution you thought up yourself, but it creates a __repr__ that's more similar to the usual dataclass __repr__:

from dataclasses import dataclass, field
from datetime import datetime
from typing import List, Dict
    
@dataclass
class BoardStaff:
    fullname: str
    address: str
    degree: str
    date: str = datetime.now()
    rank: int = 10
    badges: bool = False
    cases_dict: Dict[str, str] = field(default_factory=dict)
    cases_list: List[str] = field(default_factory=list)

    def __repr__(self):
        dict_repr = ', '.join(
            f'{k}={v!r}'
            for k, v in filter(
                lambda item: item[0] in {'fullname', 'address', 'date'},
                self.__dict__.items()
            )
        )

        return f'{self.__class__.__name__}({dict_repr})'

Emp = BoardStaff('Jack London', address='Unknown', degree='MA')
print(Emp)

(N.B. I had to reorder your fields slightly, as having default-argument parameters before parameters with no default will raise an error.)

If you don't want to hardcode your __repr__ fields into your __repr__ methods, you could mark your non-__repr__ fields as private attributes, as was suggested in the comments by @DarkKnight, and use this as a signal for your __repr__ method:

from dataclasses import dataclass, field
from datetime import datetime
from typing import List, Dict
    
@dataclass
class BoardStaff:
    fullname: str
    address: str
    _degree: str
    date: str = datetime.now()
    _rank: int = 10
    _badges: bool = False
    _cases_dict: Dict[str, str] = field(default_factory=dict)
    _cases_list: List[str] = field(default_factory=list)

    def __repr__(self):
        dict_repr = ', '.join(
            f'{k}={v!r}'
            for k, v in filter(
                lambda item: not item[0].startswith('_'),
                self.__dict__.items()
            )
        )

        return f'{self.__class__.__name__}({dict_repr})'

Emp = BoardStaff('Jack London', address='Unknown', _degree='MA')
print(Emp)

You could even potentially write your own decorator that would generate custom __repr__ methods for you on a class-by-class basis. E.g., this decorator will generate __repr__ methods that will only include the arguments you pass to the decorator:

from dataclasses import dataclass, field
from datetime import datetime
from typing import List, Dict
from functools import partial

def dataclass_with_repr_fields(
    keys, init=True, eq=True, order=False, 
    unsafe_hash=False, frozen=False, cls=None
):
    if cls is None:
        return partial(
            dataclass_with_repr_fields, keys, init=init, 
            eq=eq, order=order,  unsafe_hash=unsafe_hash,
            frozen=frozen)

    cls = dataclass(
        cls, init=init, repr=False, eq=eq, order=order, 
        unsafe_hash=unsafe_hash, frozen=frozen
    )

    def __repr__(self):
        dict_repr = ', '.join(
            f'{k}={v!r}'
            for k, v in filter(
                lambda item: item[0] in keys,
                self.__dict__.items()
            )
        )

        return f'{self.__class__.__name__}({dict_repr})'

    cls.__repr__ = __repr__
    return cls


@dataclass_with_repr_fields({'fullname', 'address', 'date'})
class BoardStaff:
    fullname: str
    address: str
    degree: str
    date: str = datetime.now()
    rank: int = 10
    badges: bool = False
    cases_dict: Dict[str, str] = field(default_factory=dict)
    cases_list: List[str] = field(default_factory=list)
    

@dataclass_with_repr_fields({'name', 'surname'})
class Manager:
    name: str
    surname: str
    salary: int
    private_medical_details: str

Emp = BoardStaff('Jack London', address='Unknown', degree='MA')
print(Emp)
manager = Manager('John', 'Smith', 600000, 'badly asthmatic')
print(manager)
Alex Waygood
  • 6,304
  • 3
  • 24
  • 46