【问题标题】:How to apply default value to Python dataclass field when None was passed?传递 None 时如何将默认值应用于 python 数据类字段?
【发布时间】:2019-11-02 01:17:08
【问题描述】:

编辑:为了清楚起见,我需要一个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')

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

编辑:我应该补充一点,传递给类的参数包含 None 并且我无法控制这方面。

【问题讨论】:

  • 使用__post_init__ 方法可能是实现这一目标的唯一方法

标签: python-3.x default-value


【解决方案1】:

使用基于键的参数。你可以做r2 = Specs1('Apple', c='Cherry')。您不必使用无。参考here

输出:

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

【讨论】:

  • 明白,但在我的情况下,我将收到一个固定的值集合,其中没有默认值
【解决方案2】:

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

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

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

printMessage("Jack")

同样的事情也适用于类。

关于“无”的类似辩论可以在这里找到:Call function without optional arguments if they are None

【讨论】:

  • 该类是使用默认字符串值定义的。如果根本没有传入参数,将使用这些值,但是,当客户端显式传入 None 值时,不会使用默认值。关于这个问题的棘手部分是它专门针对数据类而不是常规类,请参阅realpython.com/python-data-classes 它们具有更简洁的语法,无需将属性定义为属性。
【解决方案3】:

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

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

【讨论】:

  • 是的,您的第一个解决方案正是我所需要的。所以我想这在dataclass 之外完成得更好,但为了好奇,你会如何用dataclass 写这个?
  • 内联条件可以包含在您提供的第一段代码中,我错了吗?
  • 我真的不知道在dataclass 的上下文中如何(这是我的问题的前提,你忽略了:-))
  • 是的,我想你是对的,我没有想太多。您在问题中提供的解决方案似乎是正确的。
【解决方案4】:

这是另一种解决方案。

定义DefaultValNoneRefersDefault 类型:

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:请注意,如果没有类继承自 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/example

【讨论】:

  • 不错的一个!我花了一分钟的时间来理解它,但我想我现在明白了。
  • 非常有趣的解决方案,我喜欢。为什么不使用更具体的DefaultStr 类和val: str
  • 我猜,您想添加DefaultStr 以获得更准确的类型提示。但是在这种情况下,我最感兴趣的类型提示是Spec3中定义的a: strb: strc: strvalDefaultVal中的类型提示对它们没有影响。
【解决方案5】:

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

【讨论】:

  • 这似乎工作除了具有dataclass.field(...) 默认值的属性。
【解决方案6】:

简单的解决方案是只在__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'

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

【讨论】:

    【解决方案7】:

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

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

    【讨论】:

      【解决方案8】:

      也许我能想到的最有效和最方便的方法是在 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's answer 中的版本对其进行计时,这实际上是同样的事情。

      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
      

      【讨论】:

        【解决方案9】:

        声明你的python数据类并使用默认参数

        @dataclass
        class Specs2:
            a: str
            b: str
            c: str
        
            def __init__(self, a, b='Bravo', c='Charlie'):
                self.a = a
                self.b = b
                self.c = c
        
        specs = Specs2('alpha')
        

        【讨论】:

          猜你喜欢
          • 2021-06-15
          • 2020-02-21
          • 2019-03-07
          • 2017-11-08
          • 2014-08-02
          • 2022-01-02
          • 2021-03-22
          • 2019-02-03
          相关资源
          最近更新 更多