【问题标题】:Named tuple and default values for optional keyword arguments可选关键字参数的命名元组和默认值
【发布时间】:2012-07-06 06:04:31
【问题描述】:

我正在尝试将一个较长的空心“数据”类转换为一个命名元组。我的班级目前看起来像这样:

class Node(object):
    def __init__(self, val, left=None, right=None):
        self.val = val
        self.left = left
        self.right = right

转换为namedtuple 后的样子:

from collections import namedtuple
Node = namedtuple('Node', 'val left right')

但是这里有一个问题。我原来的类只允许我传入一个值,并通过使用命名/关键字参数的默认值来处理默认值。比如:

class BinaryTree(object):
    def __init__(self, val):
        self.root = Node(val)

但这不适用于我重构的命名元组,因为它希望我传递所有字段。我当然可以将出现的Node(val) 替换为Node(val, None, None),但这不是我喜欢的。

那么是否有一个好技巧可以使我的重写成功而不会增加大量代码复杂性(元编程),还是我应该吞下药丸并继续“搜索和替换”? :)

【问题讨论】:

  • 为什么要进行这种转换?我喜欢你原来的Node 课程。为什么要转换为命名元组?
  • 我想进行这种转换,因为当前的Node 和其他类是具有一堆不同字段的简单数据持有者值对象(Node 只是其中之一)。这些类声明只不过是线路噪音恕我直言,因此想要将它们修剪掉。为什么要维护不需要的东西? :)
  • 你的类中根本没有任何方法函数吗?例如,您没有一个 .debug_print() 方法来遍历树并打印它吗?
  • 当然可以,但这是针对BinaryTree 类的。 Node 和其他数据持有者不需要这样的特殊方法,尤其是考虑到命名元组具有不错的 __str____repr__ 表示。 :)
  • 好吧,看起来有道理。而且我认为 Ignacio Vazquez-Abrams 已经给了你答案:使用一个为你的节点执行默认值的函数。

标签: python default-value namedtuple optional-arguments


【解决方案1】:

我不确定仅使用内置的命名元组是否有简单的方法。有一个名为recordtype 的不错的模块具有此功能:

>>> from recordtype import recordtype
>>> Node = recordtype('Node', [('val', None), ('left', None), ('right', None)])
>>> Node(3)
Node(val=3, left=None, right=None)
>>> Node(3, 'L')
Node(val=3, left=L, right=None)

【讨论】:

  • 啊,虽然recordtype 对未来的工作看起来很有趣,但不可能使用第三方软件包。 +1
  • 模块很小,只有一个single file,所以你可以随时将它添加到你的项目中。
  • 很公平,虽然我会再等一段时间来获得一个纯命名元组解决方案,在将其标记为接受之前有一个解决方案! :)
  • 同意纯python会很好,但我认为没有:(
  • 请注意 recordtype 是可变的,而 namedtuple 不是。如果您希望对象是可散列的,这可能很重要(我猜您不这样做,因为它最初是作为一个类)。
【解决方案2】:

将其包装在一个函数中。

NodeT = namedtuple('Node', 'val left right')

def Node(val, left=None, right=None):
  return NodeT(val, left, right)

【讨论】:

  • 这很聪明,可能是一个不错的选择,但也可能通过破坏isinstance(Node('val'), Node) 引起问题:它现在会引发异常,而不是返回 True。虽然有点冗长,但@justinfay's answer (below) 正确地保留了类型层次结构信息,因此如果其他人要与 Node 实例交互,这可能是一种更好的方法。
  • 我喜欢这个答案的简洁。也许可以通过命名函数def make_node(...): 而不是假装它是一个类定义来解决上面评论中的问题。这样用户就不会试图检查函数的类型多态性,而是使用元组定义本身。
  • 查看我的答案,了解不会误导人们错误地使用isinstance
【解决方案3】:

你也可以这样用:

import inspect

def namedtuple_with_defaults(type, default_value=None, **kwargs):
    args_list = inspect.getargspec(type.__new__).args[1:]
    params = dict([(x, default_value) for x in args_list])
    params.update(kwargs)

    return type(**params)

这基本上使您可以使用默认值构造任何命名元组并仅覆盖您需要的参数,例如:

import collections

Point = collections.namedtuple("Point", ["x", "y"])
namedtuple_with_defaults(Point)
>>> Point(x=None, y=None)

namedtuple_with_defaults(Point, x=1)
>>> Point(x=1, y=None)

【讨论】:

    【解决方案4】:

    我继承了 namedtuple 并覆盖了 __new__ 方法:

    from collections import namedtuple
    
    class Node(namedtuple('Node', ['value', 'left', 'right'])):
        __slots__ = ()
        def __new__(cls, value, left=None, right=None):
            return super(Node, cls).__new__(cls, value, left, right)
    

    这保留了直观的类型层次结构,而伪装成类的工厂函数的创建却没有。

    【讨论】:

    • 这可能需要插槽和字段属性以保持命名元组的空间效率。
    • 由于某种原因,__new__ 在使用_replace 时没有被调用。
    • 请查看下面的@marc-lodato 答案,恕我直言,这是比这更好的解决方案。
    • 但是@marc-lodato 的回答并没有为子类提供具有不同默认值的能力
    • @JasonS,我怀疑子类具有不同的默认值可能违反LSP。然而,一个子类可能非常希望有 更多 个默认值。在任何情况下,子类都可以使用justinfay的method,而基类可以使用Marc的method
    【解决方案5】:

    Python 3.7

    使用 defaults 参数。

    >>> from collections import namedtuple
    >>> fields = ('val', 'left', 'right')
    >>> Node = namedtuple('Node', fields, defaults=(None,) * len(fields))
    >>> Node()
    Node(val=None, left=None, right=None)
    

    或者更好的是,使用新的dataclasses 库,它比命名元组好得多。

    >>> from dataclasses import dataclass
    >>> from typing import Any
    >>> @dataclass
    ... class Node:
    ...     val: Any = None
    ...     left: 'Node' = None
    ...     right: 'Node' = None
    >>> Node()
    Node(val=None, left=None, right=None)
    

    在 Python 3.7 之前

    Node.__new__.__defaults__ 设置为默认值。

    >>> from collections import namedtuple
    >>> Node = namedtuple('Node', 'val left right')
    >>> Node.__new__.__defaults__ = (None,) * len(Node._fields)
    >>> Node()
    Node(val=None, left=None, right=None)
    

    在 Python 2.6 之前

    Node.__new__.func_defaults 设置为默认值。

    >>> from collections import namedtuple
    >>> Node = namedtuple('Node', 'val left right')
    >>> Node.__new__.func_defaults = (None,) * len(Node._fields)
    >>> Node()
    Node(val=None, left=None, right=None)
    

    订单

    在所有版本的 Python 中,如果您设置的默认值少于命名元组中存在的默认值,则默认值将应用于最右边的参数。这允许您保留一些参数作为必需参数。

    >>> Node.__new__.__defaults__ = (1,2)
    >>> Node()
    Traceback (most recent call last):
      ...
    TypeError: __new__() missing 1 required positional argument: 'val'
    >>> Node(3)
    Node(val=3, left=1, right=2)
    

    Python 2.6 到 3.6 的包装器

    这是一个适合您的包装器,它甚至可以让您(可选地)将默认值设置为 None 以外的其他值。这不支持必需的参数。

    import collections
    def namedtuple_with_defaults(typename, field_names, default_values=()):
        T = collections.namedtuple(typename, field_names)
        T.__new__.__defaults__ = (None,) * len(T._fields)
        if isinstance(default_values, collections.Mapping):
            prototype = T(**default_values)
        else:
            prototype = T(*default_values)
        T.__new__.__defaults__ = tuple(prototype)
        return T
    

    例子:

    >>> Node = namedtuple_with_defaults('Node', 'val left right')
    >>> Node()
    Node(val=None, left=None, right=None)
    >>> Node = namedtuple_with_defaults('Node', 'val left right', [1, 2, 3])
    >>> Node()
    Node(val=1, left=2, right=3)
    >>> Node = namedtuple_with_defaults('Node', 'val left right', {'right':7})
    >>> Node()
    Node(val=None, left=None, right=7)
    >>> Node(4)
    Node(val=4, left=None, right=7)
    

    【讨论】:

    • 让我们看看...您的单行:a) 是最短/最简单的答案,b) 保持空间效率,c) 不会破坏isinstance ...所有优点,没有缺点...太糟糕了,你来晚了一点。这是最好的答案。
    • 包装器版本的一个问题:与内置的 collections.namedtuple 不同,如果 def() 包含在不同的模块中,此版本不是可腌制/多进程可序列化的。
    • 我给了这个答案一个赞成票,因为它比我自己的更可取。遗憾的是,我自己的回答不断得到支持:|
    • @ishaaq,问题是(None) 不是一个元组,它是None。如果您改用(None,),它应该可以正常工作。
    • 太棒了!您可以通过以下方式概括默认设置:Node.__new__.__defaults__= (None,) * len(Node._fields)
    【解决方案6】:

    使用None 初始化所有个缺失参数的稍微扩展的示例:

    from collections import namedtuple
    
    class Node(namedtuple('Node', ['value', 'left', 'right'])):
        __slots__ = ()
        def __new__(cls, *args, **kwargs):
            # initialize missing kwargs with None
            all_kwargs = {key: kwargs.get(key) for key in cls._fields}
            return super(Node, cls).__new__(cls, *args, **all_kwargs)
    

    【讨论】:

      【解决方案7】:

      这是一个更紧凑的版本,灵感来自 justinfay 的回答:

      from collections import namedtuple
      from functools import partial
      
      Node = namedtuple('Node', ('val left right'))
      Node.__new__ = partial(Node.__new__, left=None, right=None)
      

      【讨论】:

      • 请注意 Node(1, 2) 不适用于此配方,但适用于 @justinfay 的答案。否则,它非常漂亮(+1)。
      【解决方案8】:

      结合@Denis 和@Mark 的方法:

      from collections import namedtuple
      import inspect
      
      class Node(namedtuple('Node', 'left right val')):
          __slots__ = ()
          def __new__(cls, *args, **kwargs):
              args_list = inspect.getargspec(super(Node, cls).__new__).args[len(args)+1:]
              params = {key: kwargs.get(key) for key in args_list + kwargs.keys()}
              return super(Node, cls).__new__(cls, *args, **params) 
      

      这应该支持使用位置参数和混合大小写创建元组。 测试用例:

      >>> print Node()
      Node(left=None, right=None, val=None)
      
      >>> print Node(1,2,3)
      Node(left=1, right=2, val=3)
      
      >>> print Node(1, right=2)
      Node(left=1, right=2, val=None)
      
      >>> print Node(1, right=2, val=100)
      Node(left=1, right=2, val=100)
      
      >>> print Node(left=1, right=2, val=100)
      Node(left=1, right=2, val=100)
      
      >>> print Node(left=1, right=2)
      Node(left=1, right=2, val=None)
      

      还支持TypeError:

      >>> Node(1, left=2)
      TypeError: __new__() got multiple values for keyword argument 'left'
      

      【讨论】:

        【解决方案9】:

        我觉得这个版本更容易阅读:

        from collections import namedtuple
        
        def my_tuple(**kwargs):
            defaults = {
                'a': 2.0,
                'b': True,
                'c': "hello",
            }
            default_tuple = namedtuple('MY_TUPLE', ' '.join(defaults.keys()))(*defaults.values())
            return default_tuple._replace(**kwargs)
        

        这并不像它需要两​​次创建对象那样有效,但您可以通过在模块内定义默认副本并让函数执行替换行来更改它。

        【讨论】:

          【解决方案10】:

          这是an example straight from the docs

          默认值可以通过使用 _replace() 来自定义一个 原型实例:

          >>> Account = namedtuple('Account', 'owner balance transaction_count')
          >>> default_account = Account('<owner name>', 0.0, 0)
          >>> johns_account = default_account._replace(owner='John')
          >>> janes_account = default_account._replace(owner='Jane')
          

          所以,OP 的例子是:

          from collections import namedtuple
          Node = namedtuple('Node', 'val left right')
          default_node = Node(None, None, None)
          example = default_node._replace(val="whut")
          

          但是,我更喜欢这里给出的其他一些答案。为了完整起见,我只是想添加它。

          【讨论】:

          • +1。很奇怪,他们决定使用_ 方法(基本上意味着私有方法)来处理replace 之类的东西,这似乎非常有用..
          • @sasuke - 我也想知道。使用空格分隔的字符串而不是*args 定义元素已经有点奇怪了。可能只是在很多东西标准化之前它就被添加到语言中了。
          • _ 前缀是为了避免与用户定义的元组字段的名称冲突(相关文档引用:“任何有效的 Python 标识符都可以用于字段名,但以下划线开头的名称除外。 ”)。至于空格分隔的字符串,我认为这只是为了节省一些击键(如果你愿意,你可以传递一个字符串序列)。
          • 啊,是的,我忘了你将命名元组的元素作为属性访问,所以_ 很有意义。
          • 您的解决方案既简单又最好。其余的是恕我直言,相当丑陋。我只会做一点小小的改变。我更喜欢 node_default 而不是 default_node,因为它可以更好地使用 IntelliSense。如果你开始输入 node 你会收到你需要的一切:)
          【解决方案11】:

          这是一个不太灵活但更简洁的 Mark Lodato 包装器版本:它将字段和默认值作为字典。

          import collections
          def namedtuple_with_defaults(typename, fields_dict):
              T = collections.namedtuple(typename, ' '.join(fields_dict.keys()))
              T.__new__.__defaults__ = tuple(fields_dict.values())
              return T
          

          例子:

          In[1]: fields = {'val': 1, 'left': 2, 'right':3}
          
          In[2]: Node = namedtuple_with_defaults('Node', fields)
          
          In[3]: Node()
          Out[3]: Node(val=1, left=2, right=3)
          
          In[4]: Node(4,5,6)
          Out[4]: Node(val=4, left=5, right=6)
          
          In[5]: Node(val=10)
          Out[5]: Node(val=10, left=2, right=3)
          

          【讨论】:

          • dict 不保证订购。
          【解决方案12】:

          简短、简单且不会导致人们错误地使用isinstance

          class Node(namedtuple('Node', ('val', 'left', 'right'))):
              @classmethod
              def make(cls, val, left=None, right=None):
                  return cls(val, left, right)
          
          # Example
          x = Node.make(3)
          x._replace(right=Node.make(4))
          

          【讨论】:

            【解决方案13】:

            使用我的Advanced Enum (aenum) 库中的NamedTuple 类,并使用class 语法,这非常简单:

            from aenum import NamedTuple
            
            class Node(NamedTuple):
                val = 0
                left = 1, 'previous Node', None
                right = 2, 'next Node', None
            

            一个潜在的缺点是任何具有默认值的属性都需要__doc__ 字符串(对于简单属性,它是可选的)。在使用中它看起来像:

            >>> Node()
            Traceback (most recent call last):
              ...
            TypeError: values not provided for field(s): val
            
            >>> Node(3)
            Node(val=3, left=None, right=None)
            

            justinfay's answer相比的优势:

            from collections import namedtuple
            
            class Node(namedtuple('Node', ['value', 'left', 'right'])):
                __slots__ = ()
                def __new__(cls, value, left=None, right=None):
                    return super(Node, cls).__new__(cls, value, left, right)
            

            简单,并且基于metaclass 而不是基于exec

            【讨论】:

              【解决方案14】:

              另一种解决方案:

              import collections
              
              
              def defaultargs(func, defaults):
                  def wrapper(*args, **kwargs):
                      for key, value in (x for x in defaults[len(args):] if len(x) == 2):
                          kwargs.setdefault(key, value)
                      return func(*args, **kwargs)
                  return wrapper
              
              
              def namedtuple(name, fields):
                  NamedTuple = collections.namedtuple(name, [x[0] for x in fields])
                  NamedTuple.__new__ = defaultargs(NamedTuple.__new__, [(NamedTuple,)] + fields)
                  return NamedTuple
              

              用法:

              >>> Node = namedtuple('Node', [
              ...     ('val',),
              ...     ('left', None),
              ...     ('right', None),
              ... ])
              __main__.Node
              
              >>> Node(1)
              Node(val=1, left=None, right=None)
              
              >>> Node(1, 2, right=3)
              Node(val=1, left=2, right=3)
              

              【讨论】:

                【解决方案15】:

                在 Python 3.6.1+ 中使用typing.NamedTuple,您可以为 NamedTuple 字段提供默认值和类型注释。如果您只需要前者,请使用typing.Any

                from typing import Any, NamedTuple
                
                
                class Node(NamedTuple):
                    val: Any
                    left: 'Node' = None
                    right: 'Node' = None
                

                用法:

                >>> Node(1)
                Node(val=1, left=None, right=None)
                >>> n = Node(1)
                >>> Node(2, left=n)
                Node(val=2, left=Node(val=1, left=None, right=None), right=None)
                

                此外,如果您需要默认值和可选的可变性,Python 3.7 将拥有data classes (PEP 557),它可以在某些(很多?)情况下替换命名元组。


                旁注:Python 中annotations: 之后的参数和变量以及函数-&gt; 之后的表达式)的当前规范的一个怪癖是它们在定义时进行评估*。因此,由于“一旦执行了整个类的主体,就定义了类名”,上面类字段中'Node' 的注释必须是字符串以避免出现 NameError。

                这种类型的提示称为“前向引用”([1][2]),而PEP 563 Python 3.7+ 将有一个__future__ 导入(在 4.0 中默认启用)这将允许使用不带引号的前向引用,从而推迟它们的评估。

                * 仅 AFAICT 局部变量注释不会在运行时评估。 (来源:PEP 526

                【讨论】:

                • 这似乎是 3.6.1+ 用户最干净的解决方案。请注意,此示例(稍微)令人困惑,因为字段 leftright(即 Node)的类型提示与正在定义的类的类型相同,因此必须写为字符串。
                • @101,谢谢,我已经在答案中添加了关于此的注释。
                • 成语my_list: List[T] = Noneself.my_list = my_list if my_list is not None else []的类比是什么?我们可以不使用这样的默认参数吗?
                • @weberc2 好问题!我不确定这种解决方法是否适用于可变 def。 typing.NamedTuple 是可能的值。但是对于具有default_factory attr 的数据类you can useField 对象。为此,用my_list: List[T] = field(default_factory=list) 替换你的习语。
                【解决方案16】:

                在 python3.7+ 中有一个全新的 defaults= 关键字参数。

                defaults 可以是 None 或可迭代的默认值。由于具有默认值的字段必须在任何没有默认值的字段之后,因此 defaults 应用于最右边的参数。例如,如果字段名称为['x', 'y', 'z'],默认值为(1, 2),则x 将是必填参数,y 将默认为1z 将默认为2

                示例用法:

                $ ./python
                Python 3.7.0b1+ (heads/3.7:4d65430, Feb  1 2018, 09:28:35) 
                [GCC 5.4.0 20160609] on linux
                Type "help", "copyright", "credits" or "license" for more information.
                >>> from collections import namedtuple
                >>> nt = namedtuple('nt', ('a', 'b', 'c'), defaults=(1, 2))
                >>> nt(0)
                nt(a=0, b=1, c=2)
                >>> nt(0, 3)  
                nt(a=0, b=3, c=2)
                >>> nt(0, c=3)
                nt(a=0, b=1, c=3)
                

                【讨论】:

                  【解决方案17】:

                  this answer 对另一个问题的启发,这是我提出的基于metaclass 并使用super 的解决方案(以正确处理未来的子分类)。和justinfay's answer很像。

                  from collections import namedtuple
                  
                  NodeTuple = namedtuple("NodeTuple", ("val", "left", "right"))
                  
                  class NodeMeta(type):
                      def __call__(cls, val, left=None, right=None):
                          return super(NodeMeta, cls).__call__(val, left, right)
                  
                  class Node(NodeTuple, metaclass=NodeMeta):
                      __slots__ = ()
                  

                  然后:

                  >>> Node(1, Node(2, Node(4)),(Node(3, None, Node(5))))
                  Node(val=1, left=Node(val=2, left=Node(val=4, left=None, right=None), right=None), right=Node(val=3, left=None, right=Node(val=5, left=None, right=None)))
                  

                  【讨论】:

                    【解决方案18】:

                    由于您使用namedtuple 作为数据类,您应该知道python 3.7 将为此目的引入@dataclass 装饰器——当然它具有默认值。

                    An example from the docs:

                    @dataclass
                    class C:
                        a: int       # 'a' has no default value
                        b: int = 0   # assign a default value for 'b'
                    

                    比破解namedtuple 更干净、可读和可用。不难预测,namedtuples 的使用会随着 3.7 的采用而下降。

                    【讨论】:

                      【解决方案19】:

                      这里有一个简短的通用答案,带有一个很好的语法,用于带有默认参数的命名元组:

                      import collections
                      
                      def dnamedtuple(typename, field_names, **defaults):
                          fields = sorted(field_names.split(), key=lambda x: x in defaults)
                          T = collections.namedtuple(typename, ' '.join(fields))
                          T.__new__.__defaults__ = tuple(defaults[field] for field in fields[-len(defaults):])
                          return T
                      

                      用法:

                      Test = dnamedtuple('Test', 'one two three', two=2)
                      Test(1, 3)  # Test(one=1, three=3, two=2)
                      

                      缩小:

                      def dnamedtuple(tp, fs, **df):
                          fs = sorted(fs.split(), key=df.__contains__)
                          T = collections.namedtuple(tp, ' '.join(fs))
                          T.__new__.__defaults__ = tuple(df[i] for i in fs[-len(df):])
                          return T
                      

                      【讨论】:

                        【解决方案20】:

                        Python 3.7:在 namedtuple 定义中引入 defaults 参数。

                        文档中显示的示例:

                        >>> Account = namedtuple('Account', ['type', 'balance'], defaults=[0])
                        >>> Account._fields_defaults
                        {'balance': 0}
                        >>> Account('premium')
                        Account(type='premium', balance=0)
                        

                        阅读更多here

                        【讨论】:

                          【解决方案21】:

                          jterrace 使用 recordtype 的答案很好,但库的作者建议使用他的 namedlist 项目,该项目提供可变 (namedlist) 和不可变 (namedtuple) 实现。

                          from namedlist import namedtuple
                          >>> Node = namedtuple('Node', ['val', ('left', None), ('right', None)])
                          >>> Node(3)
                          Node(val=3, left=None, right=None)
                          >>> Node(3, 'L')
                          Node(val=3, left=L, right=None)
                          

                          【讨论】:

                            【解决方案22】:

                            如果您想保留使用类型注释的可能性,不幸的是@mark-lodato 非常好的解决方案不可用(设置__defaults__ 对我来说失败了)。 另一种方法是使用attrs

                            import attr
                            
                             
                            @attr.s
                            class Node(object):
                                val: str = attr.ib()
                                left: 'Node' = attr.ib(None)
                                right: 'Node' = attr.ib(None)
                            

                            这有:

                            • 类型注释
                            • 不错__str____repr__
                            • 可定制,因为它是一个真正的类
                            • 所有 Python 版本的实现相同

                            【讨论】:

                              猜你喜欢
                              • 1970-01-01
                              • 1970-01-01
                              • 2013-10-08
                              • 2022-01-16
                              • 1970-01-01
                              • 1970-01-01
                              • 2019-08-25
                              • 1970-01-01
                              • 1970-01-01
                              相关资源
                              最近更新 更多