How can I make descriptors work with dataclass and default_factory? In the following example:
from dataclasses import dataclass, field
import numpy as np
class A():
a = np.arange(2)
class Verbose_attribute(A):
def __get__(self, obj, type=None) -> object:
print("accessing the attribute to get the value")
return self.a[-1]
def __set__(self, obj, value) -> None:
print("accessing the attribute to set the value")
self.a[-1]=value
#a = np.arange(2)
@dataclass
class Foo():
attribute1: Verbose_attribute = Verbose_attribute()
#attribute1: Verbose_attribute = field(default_factory=lambda: Verbose_attribute())
my_foo_object = Foo()
print(my_foo_object.attribute1)
my_foo_object.attribute1 = 3
print(my_foo_object.attribute1)
the first print works properly, displaying the default value 1. If one comments the first version of attribute1 in Foo(), and uncomments the second, the first print gives object instead of the attribute value.
Using default_factory is important for me, since in the real case I am creating a descriptor for a class that inherits from MutableSequence.
You don't do that straight. default_factory
is specifically designed to provide a value once for that field.
When using descriptors, you need to have a class attribute pointing to the descriptor itself. To use descriptors along with dataclasses, the annotated type (after the :
) have to be the data type your descriptor will take/provide, not the descriptor itself.
If you want default values, since you are writing the descritor class, simply create the value at descriptors __init__
or __set_name__
methods.
One extra note: descriptors have to keep a set of values per host-class instance. If you try to use an attribute in the descriptor itself (or worse, in the descriptor class, as is in your code), that value will be shared across all instances of your dataclass.
Anyway, this should work:
class Verbose_attribute:
def __init__(self, factory=None):
if factory is None:
factory = lambda: np.arange(2)
self.factory = factory
def __set_name__(self, type, name):
self.name = name
self.attrname = f"_{name}_value"
def create_default(self, obj):
setattr(obj, self.attrname, self.factory())
# if type annotations are not helping, better not
# to include them. they are optional, remember.
def __get__(self, obj, type):
if not hasattr(obj, self.attrname):
self.create_default(obj)
print("accessing the attribute to get the value")
return getattr(obj, self.attrname)[-1]
def __set__(self, obj, value):
if not hasattr(obj, self.attrname):
self.create_default(obj)
print("accessing the attribute to set the value")
if isinstance(value, type(self)):
return
getattr(obj, self.attrname)[-1]=value
...
from dataclasses import dataclass, field
...
@dataclass
class Foo:
attribute1: int = field(Verbose_attribute())
Updated: there was an error in the previous code, where I made an attribution on the __get__
method - and there is a strange behavior in dataclass.field
: although designed to work with a descriptor passed as the default parameter
, in Python 3.11.1 it tries to set the value of the descriptor to itself, if the host dataclass is instantiated with no parameters. This is likely a bug in dataclasses
- I included an if
clause to work around that. The default value is supplied by the factory
callback in the Verbose_attribute
class itself.