Reputation: 1160
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.
Upvotes: 72
Views: 164236
Reputation: 525
Here is another solution.
Define DefaultVal
and NoneRefersDefault
types:
from dataclasses import dataclass, fields
from typing import Any
@dataclass(frozen=True)
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:
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
Upvotes: 16
Reputation: 895
@dataclass
class Specs1:
a: str
b: str = field(default='Bravo')
c: str = field(default='Charlie')
Upvotes: 13
Reputation: 11612
Perhaps the most efficient and convenient approach that I can think of for this task, involves using metaclasses in Python to automatically generate a __post_init__()
method for the class, which will set the default value specified for a field if a None
value is passed in for that field to __init__()
.
Assume we have these contents in a module metaclasses.py
:
import logging
LOG = logging.getLogger(__name__)
logging.basicConfig(level='DEBUG')
def apply_default_values(name, bases, dct):
"""
Metaclass to generate a __post_init__() for the class, which sets the
default values for any fields that are passed in a `None` value in the
__init__() method.
"""
# Get class annotations, which `dataclasses` uses to determine which
# fields to add to the __init__() method.
cls_annotations = dct['__annotations__']
# This is a dict which will contain: {'b': 'Bravo', 'c': 'Charlie'}
field_to_default_val = {field: dct[field] for field in cls_annotations
if field in dct}
# Now we generate the lines of the __post_init()__ method
body_lines = []
for field, default_val in field_to_default_val.items():
body_lines.append(f'if self.{field} is None:')
body_lines.append(f' self.{field} = {default_val!r}')
# Then create the function, and add it to the class
fn = _create_fn('__post_init__',
('self', ),
body_lines)
dct['__post_init__'] = fn
# Return new class with the __post_init__() added
cls = type(name, bases, dct)
return cls
def _create_fn(name, args, body, *, globals=None):
"""
Create a new function. Adapted from `dataclasses._create_fn`, so we
can also log the function definition for debugging purposes.
"""
args = ','.join(args)
body = '\n'.join(f' {b}' for b in body)
# Compute the text of the entire function.
txt = f'def {name}({args}):\n{body}'
# Log the function declaration
LOG.debug('Creating new function:\n%s', txt)
ns = {}
exec(txt, globals, ns)
return ns[name]
Now in our main module, we can import and use the metaclass we just defined:
from dataclasses import dataclass
from metaclasses import apply_default_values
@dataclass
class Specs1(metaclass=apply_default_values):
a: str
b: str = 'Bravo'
c: str = 'Charlie'
r1 = Specs1('Apple', None, 'Cherry')
print(r1)
Output:
DEBUG:metaclasses:Creating new function:
def __post_init__(self):
if self.b is None:
self.b = 'Bravo'
if self.c is None:
self.c = 'Charlie'
Specs1(a='Apple', b='Bravo', c='Cherry')
To confirm that this approach is actually as efficient as stated, I've set up a small test case to create a lot of Spec
objects, in order to time it against the version in @Lars's answer, which essentially does the same thing.
from dataclasses import dataclass
from timeit import timeit
from metaclasses import apply_default_values
@dataclass
class Specs1(metaclass=apply_default_values):
a: str
b: str = 'Bravo'
c: str = 'Charlie'
@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'
n = 100_000
print('Manual: ', timeit("Specs2('Apple', None, 'Cherry')",
globals=globals(), number=n))
print('Metaclass: ', timeit("Specs1('Apple', None, 'Cherry')",
globals=globals(), number=n))
Timing for n=100,000
runs, the results show it's close enough to not really matter:
Manual: 0.059566365
Metaclass: 0.053688744999999996
Upvotes: 3
Reputation: 356
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.
Upvotes: 24
Reputation: 922
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)
Upvotes: 37
Reputation: 131
In data classes you can access a default value of class attribute: Specs.b You can check for None and pass default value if needed
Code for this:
dataclasses.dataclass()
class Specs1:
a: str
b: str = 'Bravo'
c: str = 'Charlie'
a = 'Apple'
b = None
c = 'Potato'
specs = Specs1(a=a, b=b or Specs1.b, c=c or Specs1.c)
>>> specs
Specs1(a='Apple', b='Bravo', c='Potato')
Upvotes: 10
Reputation: 169
Not too clear what you are trying to do with your Class. Should these defaults not rather be properties?
Maybe you need a definition used by your class that has default parameters such as:
def printMessage(name, msg = "My name is "):
print("Hello! ",msg + name)
printMessage("Jack")
Same thing applies to Classes.
Similar debate about "None" can be found here: Call function without optional arguments if they are None
Upvotes: -3
Reputation: 176
I understand that you just want positional arguments. This can be accomplished with in-line conditionals (for code readability).
class Specs():
def __init__(self, a=None,b=None,c=None):
self.a = a if a is not None else 'Apple'
sefl.b = b if b is not None else 'Bravo'
self.c = c if c is not None else 'Cherry'
example = Specs('Apple', None, 'Cherry')
This approach can be done without an init method, if you prefer it that way.
However, you may considered an __init__() method with named arguments.
class Specs():
def __init__(self, a = 'Apple', b = 'Bravo', c = 'Cherry'):
self.a = a
self.b = b
self.c = c
example = Specs('Apple', c = 'Cherry')
Upvotes: 0
Reputation: 2182
Use key based parameters. You can just do r2 = Specs1('Apple', c='Cherry')
. You don't have to use None. Refer here.
Output:
Specs1(a='Apple', b='Bravo', c='Cherry')
Upvotes: 3