如何从另一个命名元组推导或子类型命名元组?

How To Deduce Or Subtype Named Tuple From Another Named Tuple?

前言

我想知道如何以 pythonic 的方式将数据 类 概念化。 具体来说,我说的是 DTO (Data Transfer Object.)

我在@jeff-oneill 问题“Using Python class as a data container”中找到了一个很好的答案,其中@joe-kington 有一个使用内置 namedtuple.

的好点

问题

在 python 2.7 文档的第 8.3.4 节中,example 很好地介绍了如何组合多个命名元组。 我的问题是如何实现反向?

例子

考虑文档中的示例:

>>> p._fields            # view the field names
('x', 'y')

>>> Color = namedtuple('Color', 'red green blue')
>>> Pixel = namedtuple('Pixel', Point._fields + Color._fields)
>>> Pixel(11, 22, 128, 255, 0)
Pixel(x=11, y=22, red=128, green=255, blue=0)

如何从“像素”实例中推断出“颜色”或“点”实例?

最好有pythonic的精神。

Point._fields + Color._fields 只是一个元组。所以鉴于此:

from collections import namedtuple
Point = namedtuple('Point', ['x', 'y'])
Color = namedtuple('Color', 'red green blue')
Pixel = namedtuple('Pixel', Point._fields + Color._fields)

f = Point._fields + Color._fields

type(f) 就是 tuple。因此,无从知道它是从哪里来的。

我建议您查看 attrs 以轻松完成 属性 对象。这将允许您进行适当的继承并避免定义所有访问字段的好方法的开销。

所以你可以做到

import attr

@attr.s
class Point:
    x, y = attr.ib(), attr.ib()

@attr.s
class Color:
    red, green, blue = attr.ib(), attr.ib(), attr.ib()

class Pixel(Point, Color):
    pass

现在,Pixel.__bases__会给你(__main__.Point, __main__.Color)

在这里。顺便说一句,如果你经常需要这个操作,你可以在pixel_ins的基础上创建一个用于color_ins创建的函数。甚至对于任何子命名元组!

from collections import namedtuple

Point = namedtuple('Point', 'x y')
Color = namedtuple('Color', 'red green blue')
Pixel = namedtuple('Pixel', Point._fields + Color._fields)

pixel_ins = Pixel(x=11, y=22, red=128, green=255, blue=0)
color_ins = Color._make(getattr(pixel_ins, field) for field in Color._fields)

print color_ins

输出:Color(red=128, green=255, blue=0)

提取任意子命名元组的函数(无错误处理):

def extract_sub_namedtuple(parent_ins, child_cls):
    return child_cls._make(getattr(parent_ins, field) for field in child_cls._fields)

color_ins = extract_sub_namedtuple(pixel_ins, Color)
point_ins = extract_sub_namedtuple(pixel_ins, Point)

这是 Nikolay Prokopyev 的 extract_sub_namedtuple 的替代实现,它使用字典而不是 getattr

from collections import namedtuple

Point = namedtuple('Point', 'x y')
Color = namedtuple('Color', 'red green blue')
Pixel = namedtuple('Pixel', Point._fields + Color._fields)

def extract_sub_namedtuple(tup, subtype):
    d = tup._asdict()
    return subtype(**{k:d[k] for k in subtype._fields})

pix = Pixel(11, 22, 128, 255, 0)

point = extract_sub_namedtuple(pix, Point)
color = extract_sub_namedtuple(pix, Color)
print(point, color)

输出

Point(x=11, y=22) Color(red=128, green=255, blue=0)

这个可以写成一行:

def extract_sub_namedtuple(tup, subtype):
    return subtype(**{k:tup._asdict()[k] for k in subtype._fields})

但效率较低,因为它必须为 subtype._fields 中的每个字段调用 tup._asdict()

当然,对于这些特定的命名元组,你可以这样做

point = Point(*pix[:2])
color = Color(*pix[2:])

但这不是很优雅,因为它对父字段的位置和长度进行了硬编码。

FWIW,有代码将多个命名元组组合成一个命名元组,保留字段顺序并跳过 this answer 中的重复字段。

您可以执行此操作的另一种方法是使“Pixel”的参数与您实际想要的一致,而不是将其组成部分的所有参数展平。

我认为您应该只使用两个参数:locationcolor,而不是组合 Point._fields + Color._fields 来获取 Pixel 的字段。这两个字段可以用你的其他元组初始化,你不需要做任何推断。

例如:

# Instead of Pixel(x=11, y=22, red=128, green=255, blue=0)
pixel_ins = Pixel(Point(x=11, y=22), Color(red=128, green=255, blue=0))

# Get the named tuples that the pixel is parameterized by
pixel_color = pixel_ins.color
pixel_point = pixel_ins.location

通过将所有参数混合在一起(例如,主要对象上的 x、y、红色、绿色和蓝色),您并没有真正获得任何东西,但您失去了很多可读性。如果您的 namedtuple 参数共享字段,则展平参数也会引入错误:

from collections import namedtuple 

Point = namedtuple('Point', ['x', 'y'])
Color = namedtuple('Color', 'red green blue')
Hue = namedtuple('Hue', 'red green blue')
Pixel = namedtuple('Pixel', Point._fields + Color._fields + Hue._fields)
# Results in:
#    Traceback (most recent call last):
#      File "<stdin>", line 1, in <module>
#      File "C:\Program Files\Python38\lib\collections\__init__.py", line 370, in namedtuple
#        raise ValueError(f'Encountered duplicate field name: {name!r}')
#    ValueError: Encountered duplicate field name: 'red'

  

背景

最初我问这个问题是因为我不得不支持一些大量使用元组但没有对其中的值给出任何解释的意大利面条代码库。 经过一些重构后,我注意到我需要从其他元组中提取一些类型信息,并且正在寻找一些免费的样板文件和 type-safe 方法。

解决方案

您可以对命名元组定义进行子类化并实现自定义 __new__ 方法来支持它,并可选择在途中执行一些数据格式化和验证。有关详细信息,请参阅此 reference

例子

from __future__ import annotations

from collections import namedtuple
from typing import Union, Tuple

Point = namedtuple('Point', 'x y')
Color = namedtuple('Color', 'red green blue')
Pixel = namedtuple('Pixel', Point._fields + Color._fields)

# Redeclare "Color" to provide custom creation method
# that can deduce values from various different types
class Color(Color):

    def __new__(cls, *subject: Union[Pixel, Color, Tuple[float, float, float]]) -> Color:
        # If got only one argument either of type "Pixel" or "Color"
        if len(subject) == 1 and isinstance((it := subject[0]), (Pixel, Color)):
            # Create from invalidated color properties
            return super().__new__(cls, *cls.invalidate(it.red, it.green, it.blue))
        else:  # Else treat it as raw values and by-pass them after invalidation
            return super().__new__(cls, *cls.invalidate(*subject))

    @classmethod
    def invalidate(cls, r, g, b) -> Tuple[float, float, float]:
        # Convert values to float
        r, g, b = (float(it) for it in (r, g, b))
        # Ensure that all values are in valid range
        assert all(0 <= it <= 1.0 for it in (r, g, b)), 'Some RGB values are invalid'
        return r, g, b

现在您可以从任何受支持的值类型(ColorPixel、三元组数字)实例化 Color,而无需样板。

color = Color(0, 0.5, 1)
from_color = Color(color)
from_pixel = Color(Pixel(3.4, 5.6, 0, 0.5, 1))

并且您可以验证所有值是否相等:

>>> (0.0, 0.5, 1.0) == color == from_color == from_pixel
True