Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to apply default value to python dataclass field when None was passed?

I need a class that will accept a number of parameters, I know that all parameters will be provided but some maybe passed as None in which case my class will have to provide default values.

I want to setup a simple dataclass with a some default values like so:

@dataclass
class Specs1:
    a: str
    b: str = 'Bravo'
    c: str = 'Charlie'

I would like to be able to get the default value for the second field but still set a value for the third one. I cannot do this with None because it is happily accepted as a value for my string:

r1 = Specs1('Apple', None, 'Cherry') # Specs1(a='Apple', b=None, c='Cherry')

I have come up with the following solution:

@dataclass
class Specs2:
    def_b: ClassVar = 'Bravo'
    def_c: ClassVar = 'Charlie'
    a: str
    b: str = def_b
    c: str = def_c
    
    def __post_init__(self):
        self.b = self.def_b if self.b is None else self.b
        self.c = self.def_c if self.c is None else self.c

Which seems to behave as intended:

r2 = Specs2('Apple', None, 'Cherry') # Specs2(a='Apple', b='Bravo', c='Cherry')

However, I feel it is quite ugly and that I am maybe missing something here. My actual class will have more fields so it will only get uglier.

The parameters passed to the class contain None and I do not have control over this aspect.

like image 713
YeO Avatar asked Jun 19 '19 10:06

YeO


People also ask

What is __ Post_init __?

The post-init function is an in-built function in python and helps us to initialize a variable outside the __init__ function. post-init function in python.

Can Python Dataclass have methods?

A dataclass can very well have regular instance and class methods. Dataclasses were introduced from Python version 3.7. For Python versions below 3.7, it has to be installed as a library.

What does @dataclass do in Python?

Python introduced the dataclass in version 3.7 (PEP 557). The dataclass allows you to define classes with less code and more functionality out of the box.

What is @dataclass?

A data class is a class typically containing mainly data, although there aren't really any restrictions. It is created using the new @dataclass decorator, as follows: from dataclasses import dataclass @dataclass class DataClassCard: rank: str suit: str.


3 Answers

Here is another solution.

Define DefaultVal and NoneRefersDefault types:

from dataclasses import dataclass, fields

@dataclass
class DefaultVal:
    val: Any


@dataclass
class NoneRefersDefault:
    def __post_init__(self):
        for field in fields(self):

            # if a field of this data class defines a default value of type
            # `DefaultVal`, then use its value in case the field after 
            # initialization has either not changed or is None.
            if isinstance(field.default, DefaultVal):
                field_val = getattr(self, field.name)
                if isinstance(field_val, DefaultVal) or field_val is None:
                    setattr(self, field.name, field.default.val)

Usage:

@dataclass
class Specs3(NoneRefersDefault):
    a: str
    b: str = DefaultVal('Bravo')
    c: str = DefaultVal('Charlie')

r3 = Specs3('Apple', None, 'Cherry')  # Specs3(a='Apple', b='Bravo', c='Cherry')

EDIT #1: Rewritten NoneRefersDefault such that the following is possible as well:

@dataclass
r3 = Specs3('Apple', None)  # Specs3(a='Apple', b='Bravo', c='Charlie')

EDIT #2: Note that if no class inherits from Spec, it might be better to have no default values in the dataclass and a "constructor" function create_spec instead:

@dataclass
class Specs4:
    a: str
    b: str
    c: str

def create_spec(
        a: str,
        b: str = None,
        c: str = None,
):
    if b is None:
        b = 'Bravo'
    if c is None:
        c = 'Charlie'

    return Spec4(a=a, b=b, c=c)

also see dataclass-abc/example

like image 150
MikeSchneeberger Avatar answered Oct 10 '22 18:10

MikeSchneeberger


The simple solution is to just implement the default arguments in __post_init__() only!

@dataclass
class Specs2:
    a: str
    b: str
    c: str

    def __post_init__(self):
        if self.b is None:
            self.b = 'Bravo'
        if self.c is None:
            self.c = 'Charlie'

(Code is not tested. If I got some detail wrong, it wouldn't be the first time)

like image 23
Lars P Avatar answered Oct 10 '22 18:10

Lars P


I know this is a little late, but inspired by MikeSchneeberger's answer I made a small adaptation to the __post_init__ function that allows you to keep the defaults in the standard format:

from dataclasses import dataclass, fields
def __post_init__(self):
    # Loop through the fields
    for field in fields(self):
        # If there is a default and the value of the field is none we can assign a value
        if not isinstance(field.default, dataclasses._MISSING_TYPE) and getattr(self, field.name) is None:
            setattr(self, field.name, field.default)

Adding this to your dataclass should then ensure that the default values are enforced without requiring a new default class.

like image 10
Jason Avatar answered Oct 10 '22 19:10

Jason