I would like to create a dataclass A with a lot of member elements. This dataclass should not have Optional members to ensure that the full information is available in the object.
Then I want to have a "modification option", which has the same members as A, but as optional members.
What would be the best way to do that without needing to write the members in two different classes?
This here is my approach (working example):
from copy import deepcopy
from dataclasses import dataclass
from typing import Optional
@dataclass
class A:
x: int
y: int
@dataclass
class A_ModificationOptions:
x: Optional[int] = None
y: Optional[int] = None
def modifyA(original: A, modification: A_ModificationOptions):
if modification.x is not None:
original.x = deepcopy(modification.x)
if modification.y is not None:
original.y = deepcopy(modification.y)
original_A = A(x=1, y=2)
print("A before modification: ", original_A) # A(x=1, y=2)
modification_A = A_ModificationOptions(y=7)
modifyA(original_A, modification_A)
print("A after modification: ", original_A) # A(x=1, y=7)
This code fulfills the following requirements:
- The original
Ahas no optional members, so all must have been set. - In the modification of
Ajust the members that need to be adapted need to be set.
This code does not fulfill the following requirements:
- I don't want to "copy" each member of
AintoA_ModificationOptionsagain. - If possible I don't want to have the
modifyA()function but something inbuilt. - If 2 is not possible: I don't want to add 2 lines per member of
AintomodifyA.
Is there a neat way to store sparse "Modification Options" for a potentially huge dataclass?
Usecase: A user creates once a full list and then in different scenarios he can play around with deltas to that full list and also the "delta" to the full list must be stored somehow -> So I thought about an original full list class A and a "delta" class A_ModificationOptions, but I hope that is somehow possible to do in a neater way. Maybe something like a smart deepcopy?
Update 1:
Thank you @wjandrea for your feedback! Your solution for point 3 did not consider more deeply nested dataclasses, so I used your suggestion to make it work for nested dataclasses. The code below now solves point 3:
from copy import deepcopy
from dataclasses import dataclass, is_dataclass
from typing import Optional
class Original:
pass
@dataclass
class B(Original):
a1: int
a2: int
a3: int
@dataclass
class A(Original):
x: int
y: int
b: B
class Modification:
pass
@dataclass
class B_Mod(Modification):
a1: Optional[int] = None
a2: Optional[int] = None
a3: Optional[int] = None
@dataclass
class A_Mod(Modification):
x: Optional[int] = None
y: Optional[int] = None
b: Optional[B_Mod] = None
def modifyDataclass(original: Original, modification: Modification):
assert is_dataclass(original) and is_dataclass(modification)
for k, v in vars(modification).items():
if is_dataclass(v):
assert isinstance(v, Modification)
modifyDataclass(original=getattr(original, k), modification=v)
return
if v is not None:
setattr(original, k, v)
original_A = A(x=1, y=2, b=B(a1=3, a2=4, a3=5))
print(
"A before modification: ", original_A
) # A(x=1, y=2, b=B(a1=3, a2=4, a3=5))
modification_A = A_Mod(y=7, b=B_Mod(a2=19))
modifyDataclass(original_A, modification_A)
print(
"A after modification: ", original_A
) # A(x=1, y=7, b=B(a1=3, a2=19, a3=5))
Now if there is a solution for point 1 and 2 that would be amazing!
Maybe also somehow with derivations? Like A_Mod being a child from A, but then switching all members to optional Members?
I think I understand what you want, and here's a way to dynamically generate the
A_ModificationOptionsclass. As noted in the comments, this will never pass a static type checker. If you want to run something likemypyorpyrighton this, you're going to have toAnyout the modification options. This is very dynamic reflection in Python.Now, a couple of notes.
dataclassis a decorator, and like any decorator, it's just being applied to a class after-the-fact. That is,is just
So we can call
dataclasslike an ordinary Python function on a class we make up if we so choose. And while we're on the topic, we can make classes using ordinary Python too.typehas a three-argument form which acts as a constructor for new classes.So let's see how we actually do that. We'll need
dataclassandfields. I also importOptionalto get a technically correct annotation, though it doesn't affect the semantics.Now the magic sauce, commented for your convenience.
To use it, we just pass the original class and assign the result to a name.
Your
modifyAfunction is kind of close todataclasses.replace, but the latter (a) takes a dictionary, and (b) returns a new instance rather than mutating in-place. Fortunately, it's fairly straightforward to write our own.This is basically what wjandrea suggested in the comments. I just prefer to use
dataclasses.fieldsrather thanvars, as it's guaranteed to get only dataclass fields and not anything extra from a non-dataclass superclass or from someone poking around and doing funny business.And your code works as proposed.
I renamed the function
modifyinstead ofmodifyAsince it never actually does anything specific toA. This one function will work for any@dataclassand the corresponding_ModificationOptionsclass. No need to rewrite it, even superficially.Try it online!