0

In python, the dataclasses module provides a slick interface for storing data.

Suppose I have 2 classes, decorated with the @dataclass decorator, like

from dataclasses import dataclass, field @dataclass class A: data_value1 : str = field(default='foo', metadata={'help': 'info about data_value2'}) @dataclass class B: data_value2 : float = field(default=0,9, metadata={'help': 'info about data_value2'}) def __post_init__(self): self.data_value2 += 10 

where I would like to perform the operation

C = A + B 

resulting in C, a class which once instantiated, would behave like:

@cdataclass class c: data_value1 : str = field(default='foo', metadata={'help': 'info about data_value2'}) data_value2 : float = field(default=0,9, metadata={'help': 'info about data_value2'}) def __post_init__(self): self.data_value2 += 10 

I can see why the addition operation may not have been defined for the type but I thought as long as there were not overlapping attribute names it should be possible. Unless some how there are conflicting operations across different methods of the classes, like conflicts that could occur in __post_init__ functions.

Does anyone know a pythonic way to achieve this goal? I can think of ways, but they would be very convoluted.

Thank you for your help and time to read this question.

I have tried directly adding them together, but the addition operation was not defined for the type. Would the easiest thing to do be to defined the addition operation myself? But I wouldn't be too familiar with how to do so on the class decorator. Could anyone help?

2 Answers 2

2

Use multiple inheritance.

from dataclasses import dataclass, field @dataclass class A: data_value_a : str = field(default='foo', metadata={'help': 'info about data_value_a'}) @dataclass class B: data_value_b : float = field(default=0.9, metadata={'help': 'info about data_value_b'}) def __post_init__(self): self.data_value_b += 10 @dataclass class C(B, A): pass assert C() == C(data_value_a='foo', data_value_b=0.9) 
Sign up to request clarification or add additional context in comments.

Comments

2

Using multiple inheritance is not enough as the __post_init__ method of all the parents won't be called:

@dataclass class A: data_value1: str = field(default="foo", metadata={"help": "info about data_value1"}) def __post_init__(self): self.data_value1 = "bar" @dataclass class B: data_value2: float = field(default=0.9, metadata={"help": "info about data_value2"}) def __post_init__(self): self.data_value2 += 10 @dataclass class C(A, B): pass obj = C() print(f"{obj.data_value1=} {obj.data_value2=}") 
>>> obj.data_value1='bar' obj.data_value2=0.9 

Depending on what you are trying to achieve, you can opt for:

@dataclass class C(A, B): def __post_init__(self): A.__post_init__(self) B.__post_init__(self) 

If you want something more general, you need to use metaclasses. Here is an example:

class FuseDataclasses(type): def __new__(cls, future_class_name, future_class_parents, future_class_attributes): new_cls = type(future_class_name, future_class_parents, future_class_attributes) # Check if a custom `__post_init__` method is implemented old_post_init = getattr(new_cls, "__post_init__", None) custom_post_init = ( old_post_init is not None and all(getattr(parent_cls, "__post_init__", None) != old_post_init for parent_cls in future_class_parents) ) # Rewrite the `__post_init__` method to ensure all of the parents' initialization are done def new_post_init(self): for parent_cls in future_class_parents: if hasattr(parent_cls, "__post_init__"): parent_cls.__post_init__(self) if custom_post_init: old_post_init(self) new_cls.__post_init__ = new_post_init return new_cls @dataclass class A: data_value1: str = field(default="foo", metadata={"help": "info about data_value1"}) def __post_init__(self): self.data_value1 = "bar" @dataclass class B: data_value2: float = field(default=0.9, metadata={"help": "info about data_value2"}) def __post_init__(self): self.data_value2 += 10 @dataclass class C(A, B, metaclass=FuseDataclasses): pass obj = C() print(f"{obj.data_value1=} {obj.data_value2=}") 
>>> obj.data_value1='bar' obj.data_value2=10.9 

And you can use this metaclass for an arbitrary number of parents.

As @VPfB correctly pointed out in the comments, you can also do:

@dataclass class A: data_value1: str = field(default="foo", metadata={"help": "info about data_value1"}) def __post_init__(self): if hasattr(super(), "__post_init__"): super().__post_init__() self.data_value1 = "bar" @dataclass class B: data_value2: float = field(default=0.9, metadata={"help": "info about data_value2"}) def __post_init__(self): if hasattr(super(), "__post_init__"): super().__post_init__() self.data_value2 += 10 @dataclass class C(A, B): def __post_init__(self): if hasattr(super(), "__post_init__"): super().__post_init__() 

But this requires to modify the dataclasses A and B beforehand, although this is minor.

It's up to you to choose the best method based on your needs and preferences.

1 Comment

Good catch with the __post_init__. Anyway, no metaclasses are needed, just add to each __post_init__ method these two lines in order to support the inheritance: if hasattr(super(), "__post_init__"): super().__post_init__()

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.