2

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!

Pedro A
  • 3,989
  • 3
  • 32
  • 56
  • 1
    This is [documented](https://docs.python.org/3/library/dataclasses.html#mutable-default-values): **Python stores default member variable values in class attributes.** – Barmar Sep 16 '22 at 03:33
  • @Barmar then why does `Example.d` give an `AttributeError`? – Pedro A Sep 16 '22 at 04:08
  • @Barmar Also, changing the value of `Example.b` to something else did not change the default value applied to a new instance created afterwards. – Pedro A Sep 16 '22 at 04:22
  • Read the description of `dataclasses.field()`. It says that if the field doesn't have a default, the class attribute is deleted. – Barmar Sep 16 '22 at 15:05
  • The default that's used when you create new instances comes from the parameter list of the `__init__()` method. `def __init__(a, b=2, ...):`. It's not `def __init__(a, b=Example.b, ...)` – Barmar Sep 16 '22 at 15:07
  • @Barmar Thanks. I thought _"default member variable values"_ and _"default that's used when you create new instances"_ were the same, but from what you say they're different. Can you teach me the difference? Or give me a link to learn more? – Pedro A Sep 16 '22 at 15:09
  • This is a very useful presentation of the issue. I ran into this while writing a test involving SQLAlchemy where the class attribute is used to filter. Functionally, it works, but stubbing out the SQLAlchemy aspects for a unit test, it failed on the same code because the class attribute didn't exist. At least that's what it looks like to me at this moment... – iJames Nov 18 '22 at 18:45

1 Answers1

0

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.

Barmar
  • 741,623
  • 53
  • 500
  • 612
  • Thanks! But if _"reassigning D.x won't change the default that's used when constructing new instances"_ then isn't the existence of `D.x` just useless? For a moment I thought it could at least be used to inspect the default, but it can't since it can be reassigned to a value that will be irrelevant. – Pedro A Sep 17 '22 at 00:13
  • Also, thanks again for the explanations on the language specification aspects that say why this happens, but you haven't answered my questions in OP... – Pedro A Sep 17 '22 at 00:16
  • I think the answer is that you're not supposed to reassign it, it's just there for information. – Barmar Sep 17 '22 at 03:11