当 None 被传递时如何将默认值应用到 Python 数据类字段?

问题描述 投票:0回答:9

我需要一个接受多个参数的类,我知道将提供所有参数,但有些参数可能作为

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')

但是,我觉得它很丑陋,而且我可能在这里错过了一些东西。我的实际课程将有更多字段,因此只会变得更难看。

传递给类的参数包含

None
,我无法控制这方面。

python python-3.x default-value python-dataclasses
9个回答
34
投票

简单的解决方案是仅实现

__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'

(代码未经测试。如果我有一些细节错误,那不是第一次)


24
投票

我知道这有点晚了,但受到 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)

将其添加到您的数据类中应该确保强制执行默认值,而不需要新的默认类。


15
投票

这是另一种解决方案。

定义

DefaultVal
NoneRefersDefault
类型:

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)

用途:

@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
,使得以下内容也成为可能:

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

编辑 #2:请注意,如果没有类继承自

Spec
,那么数据类中最好没有默认值,而使用“构造函数”函数
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)

另请参阅dataclass-abc/示例


13
投票
@dataclass
class Specs1:
    a: str
    b: str = field(default='Bravo')
    c: str = field(default='Charlie')

8
投票

在数据类中,您可以访问类属性的默认值: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')

3
投票

使用基于密钥的参数。你可以这样做

r2 = Specs1('Apple', c='Cherry')
。您不必使用“无”。请参阅此处

输出:

Specs1(a='Apple', b='Bravo', c='Cherry')

2
投票

也许对于此任务,我能想到的最有效、最方便的方法是使用 Python 中的 metaclasses 为类自动生成

__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]

现在在我们的主模块中,我们可以导入并使用我们刚刚定义的元类:

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
对象,以便根据 @Lars 的答案 中的版本对其进行计时,这本质上是同样的事情。

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

0
投票

我知道你只是想要位置参数。这可以通过内联条件来完成(为了代码可读性)。

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')

-3
投票

不太清楚你想用你的班级做什么。这些默认值不应该是属性吗?

也许您的类需要一个具有默认参数的定义,例如:

def printMessage(name, msg = "My name is "):  
    print("Hello! ",msg + name)

printMessage("Jack")

同样的事情也适用于类。

关于“None”的类似争论可以在这里找到:如果可选参数为 None,则调用不带可选参数的函数

© www.soinside.com 2019 - 2024. All rights reserved.