How to get only instance attributes with dataclasses, not class attributes?

Viewed 33

Consider:

from dataclasses import dataclass, field

@dataclass
class Example:
    a: int
    b: int = 2
    c: int = field(default=3)
    d: int = field(default_factory=lambda: 4)

To my surprise, Example.b and Example.c exist, while Example.a and Example.d don't (yes, I am talking about the class attributes here, not instance attributes):

try:
    print(Example.a)
except AttributeError as e:
    print(e)
    # AttributeError: type object 'Example' has no attribute 'a'

print(Example.b)
# 2

print(Example.c)
# 3

try:
    print(Example.d)
except AttributeError as e:
    print(e)
    # AttributeError: type object 'Example' has no attribute 'd'

I expected all of them to give an error. What I want is instance attributes, not class attributes. The fact that sometimes a class attribute also exists seems like a door for bugs.

Obviously, from what I learned above, I can just do the following:

from dataclasses import dataclass, field

@dataclass
class Example:
    a: int
    b: int = field(default_factory=lambda: 2)
    c: int = field(default_factory=lambda: 3)
    d: int = field(default_factory=lambda: 4)

Questions:

1. Is there a cleaner way of achieving this? Is this usage of default_factory considered unreadable?

2. Why would anyone want a field that is both a class attribute and an instance attribute?

Thanks!

1 Answers

This is all explained in the documentation, although it's somewhat scattered throughout it.

The section describing Mutable default values says

Python stores default member variable values in class attributes.

Since a doesn't have a default value, there's no corresponding class attribute.

It then shows an example:

@dataclass
class D:
    x: List = []
    def add(self, element):
        self.x += element

generates code similar to

class D:
    x = []
    def __init__(self, x=x):
        self.x = x
    def add(self, element):
        self.x += element

Remember that argument default values are evaluated when the function is defined, not when it's called. So reassigning D.x won't change the default that's used when constructing new instances. (The example is in this section to illustrate a different issue: all instances created using the default value will share a reference to the same list; see "Least Astonishment" and the Mutable Default Argument).

The description of dataclasses.field says:

If the default value of a field is specified by a call to field(), then the class attribute for this field will be replaced by the specified default value. If no default is provided, then the class attribute will be deleted. The intent is that after the dataclass() decorator runs, the class attributes will all contain the default values for the fields, just as if the default value itself were specified.

This explains why there's a class attribute for c, but not d. There's no class attribute when default_factory is used, because this default is generated dynamically by calling the function whenever a default is needed.

Related