As practice, I design a database with two types of objects. Each of them has several fields, and some of them are instances of classes I've created for use. As I add objects to the database, I realize that these fields, instead of being individual for each instance, are shared.
Specifically, this field is an instance of a class Historial
that accumulates notes and some changes. But all instances of the same type ( ObjVs
) share the same notes... I'm having a hard time finding and understanding the problem.
The following is a reduced version of the program that includes the classes involved.
class ObjNota:
'''Objeto básico que contiene una nota.'''
def __init__(self, text:str, tag:str):
self.nota={"text":text, "tag":tag, "fecha":time.asctime()}
class ObjNotas:
'''Objeto de control para las notas.'''
def __init__(self):
self.notas=[]
# Omito métodos que gestionan las notas.
class ObjHistorial(ObjNotas):
'''Objeto que gestiona la antiguedad, las notas y las vacaciones.'''
def __init__(self):
super().__init__() #self.notas = []
self.antiguedad = None
@dataclass
class ObjVs:
''' objeto operarios '''
nombre:str
id:int = None # Nº identificación de empresa
telf:str = None
movilidad:str = None # Fijo en servicio, correturnos o sin servicio.
tip:int = None # Nº tarjeta interprofesional
servicio_asignado:str = None
historial:ObjHistorial = ObjHistorial()
The idea is to add the operators to the database, with most of their fields empty, and edit them later. So I want the histories to be initialized objects but no data.
Indeed, if we create two objects we can easily see what you mean:
But it's not
ObjVs.historial.notas
the problem, the problem isObjVs.historial
, instance ofObjHistorial
:This problem occurs because you have inadvertently fallen into one of the most common "anti-patterns" in Python, using mutable objects as default arguments .
Note that Python stores the default values of member variables as class attributes , which makes the above practice cause all instances of
ObjVs
use the same instance ofObjHistorial
. Note that the__init__
equivalent of your dataclass would be something like this:It is very different from doing in the
__init__
:or to a correct implementation of a mutable default parameter in a function or method:
in which case you would have one instance of
ObjHistorial
for every instance ofOBjVs
.There is a way to handle mutable objects as default arguments to dataclasses by using
field
, which allows you to customize each field of a dataclass individually. It supports the following parameters:default
: Default value of the field.default_factory
: callable without arguments (we can usefunctools.partial
if necessary) that returns the initial value of the field. It should never be used together withdefault
. Even though itinit
is defined asFalse
the field it will be passed to__init__
because it is the only way to assign an initial value.init
: enable the use of the field in the method__ init __ ()
(Default isTrue
).repr
: enable the use of the field in the generation of the string by the method__repr__
(The default value isTrue
).compare
: include the field in the methods responsible for implementing comparisons and equality tests for objects, such as__eq__
(The default value isTrue
).hash
: include the field when calculatinghash()
. (By default it uses the same value ascompare
).metadata
: mapping (oNone
) with information about the field.we are interested in the parameter
default_factory
:Now everything works as it should:
This allows you to pass an instance of
ObjHistorial
orNone
when instantiatingObjVs
(a = ObjVs("Juan", historial=ObjHistorial()
)). If you don't want it to be a parameter, you can use the method__post_init__
which is executed immediately after the__init__
:Thanks for the answer Jose, very useful as always... In the end, for the case I had asked about,
historial: ObjHistorial = field(default_factory=ObjHistorial)
I have solved it perfectly.Now... I had omitted it in the question, but
ObjVs
you also have a fielddireccion
which is a very simple object with some attributes. UnlikeObjhistorial
, sometimes I want it to initialize 'empty' when I instantiateObjVs
and other times with a string as argument... butdefault_factory
it won't let me. In the end I resorted to solving it in_post_init_
to adapt it according to the case and it works fine, although maybe there is a better way to do it.