Initialize multiple variables to the same initial value

Viewed 285

I was wondering if there is another less verbose way to initialise two or more dataclass variables with the same value, e.g. something like

@dataclass
class FUN_sig:
    name: str
    result_name: str = field(default=name)    # invalid syntax

The current working solution I know of is this

from dataclasses import dataclass, field

@dataclass
class FUN_sig:
    name: str
    result_name: str = field(default=None)

    def __post_init__(self):
        if not self.result_name:
            self.result_name = self.name
1 Answers

For automation purposes, or to simplify the creation of a __post_init__() method which, in this case, sets the default value for a dataclass field based on the value passed in to __init__() for another field, what you can do is define (and use) a metaclass such as below which will auto-generate a __post_init__() method for the class.

Note that in below example, the metaclass relies on a metadata dict object that you pass in as a dataclasses.field() argument for a field. In the below case I'm passing in 'default_from' as the key in the metadata, but really this can be any other name which conveys the intent a bit more clearly.

It's admittedly not very pretty to look at, but in any case here's how I'd define the metaclass helper function, named default_meta in this case:

from __future__ import annotations

# noinspection PyProtectedMember
from dataclasses import Field, _create_fn
from typing import Any


def default_meta(name: str, bases: tuple[type, ...], cls_dict: dict[str, Any]):
    """Metaclass which takes the same arguments as builtin `type`"""
    post_init_lines = []

    # This metaclass runs before the `@dataclass` decorator, so we don't have
    # access to `dataclasses.fields`, unfortunately. Instead, we can determine
    # the valid attributes that have a :class:`dataclasses.Field` value ourselves.
    for attr, attr_val in cls_dict.items():
        # skip the attribute if it doesn't match what we're looking for

        if not isinstance(attr_val, Field):
            continue

        default_from = attr_val.metadata.get('default_from')
        if not default_from:
            continue
        
        # set the default for this field based on the value passed in to
        # __init__() for the specified dataclass field.
        post_init_lines.append(f'if not self.{attr}:')
        post_init_lines.append(f'  self.{attr} = self.{default_from}')
    
    # Set the __post_init__() method on the class
    #
    # Bonus or rather a "TODO": consider checking if a __post_init__() method
    # is a already defined for the class.
    cls_dict['__post_init__'] = _create_fn(
        '__post_init__',
        ('self', ),
        post_init_lines
    )
    
    # create and return the new class
    return type(name, bases, cls_dict)

And you'd use (the above metaclass) like this:

from __future__ import annotations

# TODO update the module name here
from other_module import default_meta
from dataclasses import dataclass, field


@dataclass
class FunSig(metaclass=default_meta):
    name: str
    result_name: str = field(default=None, metadata={'default_from': 'name'})


# usage
print(FunSig('test', result_name='setting an explicit value!'))
print(FunSig('hello world!'))

Output:

FunSig(name='test', result_name='setting an explicit value!')
FunSig(name='hello world!', result_name='hello world!')

If you wanted to fine-tune it or abstract away the implementation a bit more, I don't see why cleaner syntax such as below shouldn't work either:

@dataclass
class FunSig(metaclass=default_meta):
    name: str
    result_name: str = DefaultFrom(field='name')

Side note: I definitely think it would be cool it dataclasses adds support for this scenario in the future.

Related