我试图将一个较长的中空“数据”类转换为命名元组。我的类目前看起来是这样的:

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

但这里有一个问题。我最初的类允许我只传入一个值,并通过为named/keyword参数使用默认值来处理默认值。喜欢的东西:

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

但这在重构的命名tuple中不起作用,因为它期望我传递所有字段。我当然可以替换Node(val)到Node(val, None, None)的出现,但这不是我喜欢的。

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


当前回答

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在类语句中作为超类出现,但它实际上不是。打字。NamedTuple使用元类的高级功能来自定义用户类的创建。

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

2. 使用数据类>= 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使数据类不可变。

其他回答

你也可以用这个:

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)

将其包装在函数中。

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

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

受到这个对另一个问题的回答的启发,下面是我提出的基于元类并使用super(正确处理未来的子类)的解决方案。这和justinfay的答案很相似。

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

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

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)

这并不高效,因为它需要创建两次对象,但你可以通过在模块内定义默认的duple并让函数执行replace行来改变这一点。

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在类语句中作为超类出现,但它实际上不是。打字。NamedTuple使用元类的高级功能来自定义用户类的创建。

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

2. 使用数据类>= 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使数据类不可变。