我试图将一个较长的中空“数据”类转换为命名元组。我的类目前看起来是这样的:
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)的出现,但这不是我喜欢的。
那么,是否存在一个好技巧,可以让我的重写成功,而不增加大量的代码复杂性(元编程),或者我应该吞下药丸,继续“搜索和替换”?:)
一个稍微扩展的例子,用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)
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使数据类不可变。
我子类化了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)
这保留了直观的类型层次结构,而创建伪装成类的工厂函数则无法做到这一点。
你也可以用这个:
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)
下面是一个简短、简单的通用答案,对于带默认参数的命名元组,它有一个很好的语法:
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