Tsah Weiss
Tsah Weiss

Reputation: 643

How does python know that dataclasses.field function is not a default value in a dataclass?

Consider the following class:

from dataclasses import dataclass

@dataclass
class C:
  a: int = 1
  b: int

Trying to execute this yields TypeError: non-default argument 'b' follows default argument

Now consider this:

from dataclasses import dataclass, field
from dataclasses_json import config

@dataclass
class C:
  a: int = field(metadata=config(encoder=lambda x: x, decoder=lambda x: x))
  b: int

This executes without error.

The question is: how does the field function "cheat" the python interpreter and not considered a default value? Can I replicate this behavior in my own function?

Upvotes: 0

Views: 1241

Answers (1)

MisterMiyagi
MisterMiyagi

Reputation: 50086

The @dataclass is not "interpreted by the interpreter", in the sense that the interpreter does not "know" it has to raise errors like TypeError: non-default argument 'b' follows default argument. Instead, @dataclass is a regular Python function that inspects the class object and explicitly raises the error.

The high-level descriptions of this mechanism is that field returns a Field object containing the meta-data passed to field. The @dataclass code checks if class attribute values are Field objects, not whether they are created by field – one can write a custom function to construct a Field instance if needed.
Of course, the easiest approach is just to have a function that calls field in order to create a Field.

from dataclasses import field, MISSING


def auto_field(*, default=MISSING, default_factory=MISSING, init=True, metadata=None):
    """Field that inspects defaults to decide whether it is repr/hash'able"""
    if default is MISSING and default_factory is MISSING:
        return field(init=init, metadata=metadata)
    test_default = default if default is not MISSING else default_factory()
    return field(
        default=default, default_factory=default_factory, init=init, metadata=metadata,
        repr=type(test_default).__repr__ is not object.__repr__,
        hash=getattr(test_default, '__hash__', None) is not None,
        compare=getattr(test_default, '__hash__', None) is not None,
    )
from dataclasses import dataclass

@dataclass(frozen=True)
class Foo:
    a: int = auto_field()  # not counted as a default
    b: int
    c: list = auto_field(default_factory=list)


print(Foo(12, 42), hash(Foo(12, 42)))  # works because c is ignored for hashing

Note that conceptually, one is still restricted to the logic of dataclass and its Fields. For example, that means that one cannot create a "field which has a default but is not considered a default value" – depending on how one approaches it, dataclass would either ignore it or still raise an error when preparing the actual class.

Upvotes: 2

Related Questions