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

3

前言

我在思考如何以一种符合 Python 风格的方式来概念化数据类。具体而言,我指的是 DTO (Data Transfer Object).

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

问题

在 Python 2.7 文档的第8.3.4节中有一个很好的示例,说明了如何将多个命名元组合并在一起。我的问题是如何实现相反的操作?

示例

考虑文档中的示例:

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

我该如何从“Pixel”实例中推导出“Color”或“Point”实例?
最好符合Pythonic精神。

你的意思是要将一个 Pixel 命名元组拆分成一个 Point 和一个 Color 吗? - PM 2Ring
不完全是split,但可以在仅拥有“Pixel”实例的情况下实例化“Color”或“Point”,就像接受的答案中所示。 - kuza
好的。您可能会对我的替代实现感兴趣,以及我旧的答案,展示如何安全地组合可能具有重复字段名称的多个命名元组。 - PM 2Ring
5个回答

5

这就是代码。顺便说一下,如果您经常需要执行此操作,可以根据 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

输出:颜色(红=128,绿=255,蓝=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)

1
你可以使用namedtuple._make()来避免创建临时列表:Color._make(getattr(pixel_ins, field) for field in Color._fields) - farsil
谢谢@NikolayProkopyev,这比我想到的要好得多(我理解super为字典,并使用“**”(双星号运算符)实例化子类。) - kuza

1

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)

1
这是 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)

这可以写成一行代码:

This could be written as a one-liner:

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

但是这种方法效率较低,因为它必须针对subtype._fields中的每个字段调用tup._asdict()

当然,对于这些特定的namedtuple,您可以直接执行以下操作:

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

但这并不太优雅,因为它硬编码了父字段的位置和长度。

FWIW,有一些代码可以将多个命名元组合并成一个命名元组,保留字段顺序并跳过重复字段


1
这非常接近我在被接受的答案评论中提到的想法。我想出了一种方法,其中我提取字典sub_dict = {k:v for k,v in super_type.iteritems() if k in sub_type._fields}并实例化子命名元组为sub_type(**sub_dict)。 但是我正在寻找一种重用namedtuple接口的方法。 - kuza

0

背景

最初我提出这个问题是因为我需要支持一些使用元组的混乱代码库,但没有给出其中值的任何解释。 经过一些重构,我注意到需要从其他元组中提取一些类型信息,并寻找一种无样板代码且类型安全的方法来实现它。

解决方案

您可以子类化命名元组定义并实现自定义__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

0

你可以通过使“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、红色、绿色和蓝色都在主对象上),您并没有真正获得任何东西,但是会失去很多可读性。如果您的命名元组参数共享字段,则压平参数也会引入错误:
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'

  


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