命名元组和可选关键字参数的默认值

394

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

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),但我并不喜欢这种方式。

那么,是否存在一种好的技巧,可以让我成功地重写代码,而不会增加太多的代码复杂性(元编程),或者我只能咽下这颗苦果,继续使用“搜索和替换”方法?:)


3
你为什么想要进行这个转换?我喜欢你的原始“Node”类,就像它现在的样子。为什么要转换成命名元组? - steveha
44
我希望进行这种转换,因为当前的“Node”和其他类都是简单的数据持有者值对象,具有许多不同的字段(“Node”只是其中之一)。在我看来,这些类声明不过是无用的代码,所以我希望将它们删减掉。为什么要维护不必要的东西呢? :) - sasuke
你的类中完全没有任何方法函数吗?例如,你没有一个.debug_print()方法来遍历树并打印它吗? - steveha
4
当然可以,但是这是针对BinaryTree类而言的。而Node和其他的数据容器并不需要这样特殊的方法,尤其是因为命名元组已经有了相当不错的__str____repr__表示方式。 :) - sasuke
好的,看起来很合理。我认为Ignacio Vazquez-Abrams已经给出了答案:使用一个函数为您的节点设置默认值。 - steveha
类定义清晰,完全符合读者的预期。这里的许多答案都很复杂,并且具有令人惊讶的副作用。是的,Node是一个数据持有者类,但在幕后,namedtuple也是如此! - Lorenz Forvang
23个回答

5

一个稍微复杂的示例,用于初始化所有缺失参数为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)

4
我觉得这个版本更易于阅读:
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)

这样做并不高效,因为需要两次创建对象,但是你可以通过在模块内定义默认的对偶项,只让函数执行替换行动来改变这一点。


4

由于您正在使用namedtuple作为数据类,因此您应该知道Python 3.7将引入一个@dataclass装饰器来完成这个目的 - 当然它有默认值。

文档中的示例

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

比起对 namedtuple 进行修改,它更加清晰、易读和易用。可以预见,在 3.7 的普及下,使用 namedtuple 的情况将会减少。

4

结合@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'

4
您也可以使用以下方法:
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)

2
受到这个答案的启发,针对不同问题,这里提出了一个基于元类和使用super的解决方案(以正确处理未来的子类)。它与justinfay的答案非常相似。请注意保留HTML标签。
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)))

2

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)

1

这里是一个简短、简单的通用答案,使用了带有默认参数的命名元组的漂亮语法:

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

0

1. 使用 NamedTuple >= Python 3.6

自 Python 3.7+ 起,您可以使用 typing 模块中支持默认值的命名元组(NamedTuple)

https://docs.python.org/3/library/typing.html#typing.NamedTuple

from typing import NamedTuple

class Employee(NamedTuple):
    name: str
    id: int = 3

employee = Employee('Guido')
assert employee.id == 3

注意:尽管NamedTuple出现在类语句中作为一个超类,但实际上它并不是。typing.NamedTuple使用元类的高级功能来自定义用户类的创建。

issubclass(Employee, typing.NamedTuple)
# return False
issubclass(Employee, tuple)
# return True

2. 使用 dataclass >= Python 3.7

from dataclasses import dataclass

@dataclass(frozen=True)
class Employee:
    name: str
    id: int = 3

employee = Employee('Guido')
assert employee.id == 3

frozen=True 使数据类成为不可变的。


0

使用我高级枚举(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的答案 有以下优势:

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接