如何在传递 None 时将默认值应用于 Python 数据类字段?
How to apply default value to Python dataclass field when None was passed?
我需要一个 class 来接受一些参数,我知道所有参数都会被提供,但有些参数可能作为 None
传递,在这种情况下我的 class
会有提供默认值。
我想设置一个简单的 dataclass
和一些默认值,如下所示:
@dataclass
class Specs1:
a: str
b: str = 'Bravo'
c: str = 'Charlie'
我希望能够获得第二个字段的默认值,但仍然为第三个字段设置一个值。我无法使用 None 执行此操作,因为它很高兴被接受为我的字符串的值:
r1 = Specs1('Apple', None, 'Cherry') # Specs1(a='Apple', b=None, c='Cherry')
我想出了以下解决方案:
@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
这似乎符合预期:
r2 = Specs2('Apple', None, 'Cherry') # Specs2(a='Apple', b='Bravo', c='Cherry')
但是,我觉得它很丑,我可能在这里遗漏了一些东西。我的实际 class 将有更多的字段,所以它只会变得更丑。
传递给 class 的参数包含 None
,我无法控制这方面。
使用基于键的参数。你可以做 r2 = Specs1('Apple', c='Cherry')
。您不必使用 None。参考 here.
输出:
Specs1(a='Apple', b='Bravo', c='Cherry')
不太清楚您要用 Class 做什么。这些默认值不应该是属性吗?
也许您需要 class 使用的具有默认参数的定义,例如:
def printMessage(name, msg = "My name is "):
print("Hello! ",msg + name)
printMessage("Jack")
同样适用于 Classes。
关于 "None" 的类似争论可以在这里找到:
我知道您只是想要位置参数。这可以通过 in-line 条件(为了代码可读性)来完成。
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')
这种方法可以在没有 init 方法的情况下完成,如果你喜欢那样的话。
但是,您可以考虑使用具有命名参数的 __init__() 方法。
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')
这是另一个解决方案。
定义 DefaultVal
和 NoneRefersDefault
类型:
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)
用法:
@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')
编辑 #1:重写 NoneRefersDefault
使得以下内容也是可能的:
@dataclass
r3 = Specs3('Apple', None) # Specs3(a='Apple', b='Bravo', c='Charlie')
编辑 #2:请注意,如果没有 class 继承自 Spec
,最好在数据 class 和 "constructor" 中没有默认值函数 create_spec
改为:
@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)
在数据 classes 中,您可以访问 class 属性的默认值:Specs.b
您可以检查 None 并在需要时传递默认值
代码:
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')
简单的解决方案是仅在 __post_init__()
中实现默认参数!
@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'
(代码未测试,如有细节错误,也不是第一次了)
我知道这有点晚了,但受到 MikeSchneeberger 的回答的启发,我对 __post_init__
函数做了一个小改动,使您可以保留标准格式的默认值:
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)
将此添加到您的数据class 应确保强制执行默认值而不需要新的默认值class。
也许是我能想到的最有效和方便的方法,涉及在 Python 中使用 metaclasses 为 [=37= 自动生成 __post_init__()
方法],如果为该字段传入的 None
值传递给 __init__()
.
,它将设置为该字段指定的默认值
假设我们在一个模块中有这些内容 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]
现在在我们的主模块中,我们可以导入并使用我们刚刚定义的元class:
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)
输出:
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')
为了确认这种方法实际上如所述那样有效,我设置了一个小测试用例来创建很多 Spec
对象,以便根据 [=22] 中的版本计时=],本质上做同样的事情。
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))
运行 n=100,000
的时间,结果表明它足够接近,并不重要:
Manual: 0.059566365
Metaclass: 0.053688744999999996
我需要一个 class 来接受一些参数,我知道所有参数都会被提供,但有些参数可能作为 None
传递,在这种情况下我的 class
会有提供默认值。
我想设置一个简单的 dataclass
和一些默认值,如下所示:
@dataclass
class Specs1:
a: str
b: str = 'Bravo'
c: str = 'Charlie'
我希望能够获得第二个字段的默认值,但仍然为第三个字段设置一个值。我无法使用 None 执行此操作,因为它很高兴被接受为我的字符串的值:
r1 = Specs1('Apple', None, 'Cherry') # Specs1(a='Apple', b=None, c='Cherry')
我想出了以下解决方案:
@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
这似乎符合预期:
r2 = Specs2('Apple', None, 'Cherry') # Specs2(a='Apple', b='Bravo', c='Cherry')
但是,我觉得它很丑,我可能在这里遗漏了一些东西。我的实际 class 将有更多的字段,所以它只会变得更丑。
传递给 class 的参数包含 None
,我无法控制这方面。
使用基于键的参数。你可以做 r2 = Specs1('Apple', c='Cherry')
。您不必使用 None。参考 here.
输出:
Specs1(a='Apple', b='Bravo', c='Cherry')
不太清楚您要用 Class 做什么。这些默认值不应该是属性吗?
也许您需要 class 使用的具有默认参数的定义,例如:
def printMessage(name, msg = "My name is "):
print("Hello! ",msg + name)
printMessage("Jack")
同样适用于 Classes。
关于 "None" 的类似争论可以在这里找到:
我知道您只是想要位置参数。这可以通过 in-line 条件(为了代码可读性)来完成。
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')
这种方法可以在没有 init 方法的情况下完成,如果你喜欢那样的话。
但是,您可以考虑使用具有命名参数的 __init__() 方法。
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')
这是另一个解决方案。
定义 DefaultVal
和 NoneRefersDefault
类型:
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)
用法:
@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')
编辑 #1:重写 NoneRefersDefault
使得以下内容也是可能的:
@dataclass
r3 = Specs3('Apple', None) # Specs3(a='Apple', b='Bravo', c='Charlie')
编辑 #2:请注意,如果没有 class 继承自 Spec
,最好在数据 class 和 "constructor" 中没有默认值函数 create_spec
改为:
@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)
在数据 classes 中,您可以访问 class 属性的默认值:Specs.b 您可以检查 None 并在需要时传递默认值
代码:
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')
简单的解决方案是仅在 __post_init__()
中实现默认参数!
@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'
(代码未测试,如有细节错误,也不是第一次了)
我知道这有点晚了,但受到 MikeSchneeberger 的回答的启发,我对 __post_init__
函数做了一个小改动,使您可以保留标准格式的默认值:
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)
将此添加到您的数据class 应确保强制执行默认值而不需要新的默认值class。
也许是我能想到的最有效和方便的方法,涉及在 Python 中使用 metaclasses 为 [=37= 自动生成 __post_init__()
方法],如果为该字段传入的 None
值传递给 __init__()
.
假设我们在一个模块中有这些内容 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]
现在在我们的主模块中,我们可以导入并使用我们刚刚定义的元class:
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)
输出:
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')
为了确认这种方法实际上如所述那样有效,我设置了一个小测试用例来创建很多 Spec
对象,以便根据 [=22] 中的版本计时=],本质上做同样的事情。
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))
运行 n=100,000
的时间,结果表明它足够接近,并不重要:
Manual: 0.059566365
Metaclass: 0.053688744999999996