乐趣区

关于python:一个被低估的Python数据结构Namedtuple

原文地址:https://miguendes.me/everythi…

作者:Miguel Brito

译者:DeanWu

本文将探讨 python 中 namedtuple 的重点用法。咱们将由浅入深的介绍 namedtuple 的各概念。您将理解为什么要应用它们,以及如何应用它们,从而是代码更简洁。在学习本指南之后,你肯定会喜爱上应用它。

学习指标

在本教程完结时,您应该可能:

  • 理解为什么以及何时应用它
  • 将惯例元组和字典转换为Namedtuple
  • Namedtuple 转化为字典或惯例元组
  • Namedtuple 列表进行排序
  • 理解 Namedtuple 和数据类 (DataClass) 之间的区别
  • 应用可选字段创立Namedtuple
  • Namedtuple 序列化为 JSON
  • 增加文档字符串(docstring)

为什么要应用namedtuple

namedtuple是一个十分乏味(也被低估了)的数据结构。咱们能够轻松找到重大依赖惯例元组和字典来存储数据的 Python 代码。我并不是说,这样不好,只是有时候他们经常被滥用,且听我缓缓道来。

假如你有一个将字符串转换为色彩的函数。色彩必须在 4 维空间 RGBA 中示意。

def convert_string_to_color(desc: str, alpha: float = 0.0):
    if desc == "green":
        return 50, 205, 50, alpha
    elif desc == "blue":
        return 0, 0, 255, alpha
    else:
        return 0, 0, 0, alpha

而后,咱们能够像这样应用它:

r, g, b, a = convert_string_to_color(desc="blue", alpha=1.0)

好的,能够。然而咱们这里有几个问题。第一个是,无奈确保返回值的程序。也就是说,没有什么能够阻止其余开发者这样调用

convert_string_to_color:g, b, r, a = convert_string_to_color(desc="blue", alpha=1.0)

另外,咱们可能不晓得该函数返回 4 个值,可能会这样调用该函数:

r, g, b = convert_string_to_color(desc="blue", alpha=1.0)

于是,因为返回值不够,抛出 ValueError 谬误,调用失败。

的确如此。然而,你可能会问,为什么不应用字典呢?

Python 的字典是一种十分通用的数据结构。它们是一种存储多个值的简便办法。然而,字典并非没有毛病。因为其灵活性,字典很容易被滥用。让
咱们看看应用字典之后的例子。

def convert_string_to_color(desc: str, alpha: float = 0.0):
    if desc == "green":
        return {"r": 50, "g": 205, "b": 50, "alpha": alpha}
    elif desc == "blue":
        return {"r": 0, "g": 0, "b": 255, "alpha": alpha}
    else:
        return {"r": 0, "g": 0, "b": 0, "alpha": alpha}

好的,咱们当初能够像这样应用它,冀望只返回一个值:

color = convert_string_to_color(desc="blue", alpha=1.0)

无需记住程序,但它至多有两个毛病。第一个是咱们必须跟踪密钥的名称。如果咱们将其更改 {"r": 0,“g”: 0,“b”: 0,“alpha”: alpha}{”red": 0,“green”: 0,“blue”: 0,“a”: alpha},则在拜访字段时会失去 KeyError 返回,因为键 r,g,balpha不再存在。

字典的第二个问题是它们不可散列。这意味着咱们无奈将它们存储在 set 或其余字典中。假如咱们要跟踪特定图像有多少种颜色。如果咱们应用 collections.Counter 计数,咱们将失去TypeError: unhashable type:‘dict’

而且,字典是可变的,因而咱们能够依据须要增加任意数量的新键。置信我,这是一些很难发现的令人讨厌的谬误点。

好的,很好。那么当初怎么办?我能够用什么代替呢?

namedtuple!对,就是它!

将咱们的函数转换为应用namedtuple

from collections import namedtuple
...
Color = namedtuple("Color", "r g b alpha")
...
def convert_string_to_color(desc: str, alpha: float = 0.0):
    if desc == "green":
        return Color(r=50, g=205, b=50, alpha=alpha)
    elif desc == "blue":
        return Color(r=50, g=0, b=255, alpha=alpha)
    else:
        return Color(r=50, g=0, b=0, alpha=alpha)

与 dict 的状况一样,咱们能够将值调配给单个变量并依据须要应用。无需记住程序。而且,如果你应用的是诸如 PyCharm 和 VSCode 之类的 IDE,还能够主动提醒补全。

color = convert_string_to_color(desc="blue", alpha=1.0)
...
has_alpha = color.alpha > 0.0
...
is_black = color.r == 0 and color.g == 0 and color.b == 0

最重要的是 namedtuple 是不可变的。如果团队中的另一位开发人员认为在运行时增加新字段是个好主见,则该程序将报错。

>>> blue = Color(r=0, g=0, b=255, alpha=1.0)

>>> blue.e = 0
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-13-8c7f9b29c633> in <module>
----> 1 blue.e = 0

AttributeError: 'Color' object has no attribute 'e'

不仅如此,当初咱们能够应用它 Counter 来跟踪一个汇合有多少种颜色。

>>> Counter([blue, blue])
>>> Counter({Color(r=0, g=0, b=255, alpha=1.0): 2})

如何将惯例元组或字典转换为 namedtuple

当初咱们理解了为什么应用 namedtuple,当初该学习如何将惯例元组和字典转换为 namedtuple 了。假如因为某种原因,你有蕴含黑白 RGBA 值的字典实例。如果要将其转换为Color namedtuple,则能够按以下步骤进行:

>>> c = {"r": 50, "g": 205, "b": 50, "alpha": alpha}
>>> Color(**c)
>>> Color(r=50, g=205, b=50, alpha=0)

咱们能够利用该 ** 构造将包解压缩 dictnamedtuple

如果我想从 dict 创立一个 namedtupe,如何做?

没问题,上面这样做就能够了:

>>> c = {"r": 50, "g": 205, "b": 50, "alpha": alpha}
>>> Color = namedtuple("Color", c)
>>> Color(**c)
Color(r=50, g=205, b=50, alpha=0)

通过将 dict 实例传递给 namedtuple 工厂函数,它将为你创立字段。而后,Color 像上边的例子一样解压字典 c,创立新实例。

如何将 namedtuple 转换为字典或惯例元组

咱们刚刚学习了如何将转换 namedtupledict。反过来呢?咱们又如何将其转换为字典实例?

试验证实,namedtuple 它带有一种称为的办法._asdict()。因而,转换它就像调用办法一样简略。

>>> blue = Color(r=0, g=0, b=255, alpha=1.0)
>>> blue._asdict()
{'r': 0, 'g': 0, 'b': 255, 'alpha': 1.0}

您可能想晓得为什么该办法以 _ 结尾。这是与 Python 的惯例标准不统一的一个中央。通常,_代表公有办法或属性。然而,namedtuple为了防止 命名抵触 将它们增加到了公共办法中。除了 _asdict,还有_replace_fields_field_defaults。您能够在这里找到所有这些。

要将 namedtupe 转换为惯例元组,只需将其传递给 tuple 构造函数即可。

>>> tuple(Color(r=50, g=205, b=50, alpha=0.1))
(50, 205, 50, 0.1)

如何对 namedtuples 列表进行排序

另一个常见的用例是将多个 namedtuple 实例存储在列表中,并依据某些条件对它们进行排序。例如,假如咱们有一个色彩列表,咱们须要按 alpha 强度对其进行排序。

侥幸的是,Python 容许应用十分 Python 化的形式来执行此操作。咱们能够应用 operator.attrgetter 运算符。依据文档,attrgetter“返回从其操作数获取 attr 的可调用对象”。简略来说就是,咱们能够通过该运算符,来获取传递给 sorted 函数排序的字段。例:

from operator import attrgetter
...
colors = [Color(r=50, g=205, b=50, alpha=0.1),
    Color(r=50, g=205, b=50, alpha=0.5),
    Color(r=50, g=0, b=0, alpha=0.3)
]
...
>>> sorted(colors, key=attrgetter("alpha"))
[Color(r=50, g=205, b=50, alpha=0.1),
 Color(r=50, g=0, b=0, alpha=0.3),
 Color(r=50, g=205, b=50, alpha=0.5)]

当初,色彩列表按 alpha 强度升序排列!

如何将 namedtuples 序列化为 JSON

有时你可能须要将贮存 namedtuple 转为 JSON。Python 的字典能够通过 json 模块转换为 JSON。那么咱们能够应用_asdict 办法将元组转换为字典,而后接下来就和字典一样了。例如:

>>> blue = Color(r=0, g=0, b=255, alpha=1.0)
>>> import json
>>> json.dumps(blue._asdict())
'{"r": 0,"g": 0,"b": 255,"alpha": 1.0}'

如何给 namedtuple 增加 docstring

在 Python 中,咱们能够应用纯字符串来记录办法,类和模块。而后,此字符串可作为名为的非凡属性应用 __doc__。话虽这么说,咱们如何向咱们的Color namedtuple 增加 docstring 的?

咱们能够通过两种形式做到这一点。第一个(比拟麻烦)是应用包装器扩大元组。这样,咱们便能够 docstring 在此包装器中定义。例如,请思考以下代码片段:

_Color = namedtuple("Color", "r g b alpha")

class Color(_Color):
    """A namedtuple that represents a color.
    It has 4 fields:
    r - red
    g - green
    b - blue
    alpha - the alpha channel
    """

>>> print(Color.__doc__)
A namedtuple that represents a color.
    It has 4 fields:
    r - red
    g - green
    b - blue
    alpha - the alpha channel
>>> help(Color)
Help on class Color in module __main__:

class Color(Color)
 |  Color(r, g, b, alpha)
 |  
 |  A namedtuple that represents a color.
 |  It has 4 fields:
 |  r - red
 |  g - green
 |  b - blue
 |  alpha - the alpha channel
 |  
 |  Method resolution order:
 |      Color
 |      Color
 |      builtins.tuple
 |      builtins.object
 |  
 |  Data descriptors defined here:
 |  
 |  __dict__
 |      dictionary for instance variables (if defined)

如上,通过继承 _Color 元组,咱们为 namedtupe 增加了一个 __doc__ 属性。

增加的第二种办法,间接设置 __doc__ 属性。这种办法不须要扩大元组。

>>> Color.__doc__ = """A namedtuple that represents a color.
    It has 4 fields:
    r - red
    g - green
    b - blue
    alpha - the alpha channel
    """

留神,这些办法仅实用于Python 3+

namedtuples 和数据类 (Data Class) 之间有什么区别?

性能

在 Python 3.7 之前,可应用以下任一办法创立一个简略的数据容器:

  • namedtuple
  • 惯例类
  • 第三方库,attrs

如果您想应用惯例类,那意味着您将必须实现几个办法。例如,惯例类将须要一种 __init__ 办法来在类实例化期间设置属性。如果您心愿该类是可哈希的,则意味着本人实现一个 __hash__ 办法。为了比拟不同的对象,还须要 __eq__ 实现一个办法。最初,为了简化调试,您须要一种 __repr__ 办法。

让咱们应用惯例类来实现下咱们的色彩用例。

class Color:
    """A regular class that represents a color."""

    def __init__(self, r, g, b, alpha=0.0):
        self.r = r
        self.g = g
        self.b = b
        self.alpha = alpha

    def __hash__(self):
        return hash((self.r, self.g, self.b, self.alpha))

    def __repr__(self):
        return "{0}({1}, {2}, {3}, {4})".format(self.__class__.__name__, self.r, self.g, self.b, self.alpha)

    def __eq__(self, other):
        if not isinstance(other, Color):
            return False
        return (
            self.r == other.r
            and self.g == other.g
            and self.b == other.b
            and self.alpha == other.alpha
        )

如上,你须要实现好多办法。您只须要一个容器来为您保留数据,而不用放心扩散注意力的细节。同样,人们偏爱实现类的一个要害区别是惯例类是可变的。

实际上,引入 数据类(Data Class)的 PEP 将它们称为“具备默认值的可变 namedtuple”(译者注:Data Class python 3.7 引入,参考:https://docs.python.org/zh-cn…。

当初,让咱们看看如何用 数据类 来实现。

from dataclasses import dataclass
...
@dataclass
class Color:
    """A regular class that represents a color."""
    r: float
    g: float
    b: float
    alpha: float

哇!就是这么简略。因为没有__init__,您只需在 docstring 前面定义属性即可。此外,必须应用类型提醒对其进行正文。

除了可变之外,数据类还能够开箱即用提供可选字段。假如咱们的 Color 类不须要 alpha 字段。而后咱们能够设置为可选。

from dataclasses import dataclass
from typing import Optional
...
@dataclass
class Color:
    """A regular class that represents a color."""
    r: float
    g: float
    b: float
    alpha: Optional[float]

咱们能够像这样实例化它:

>>> blue = Color(r=0, g=0, b=255)

因为它们是可变的,因而咱们能够更改所需的任何字段。咱们能够像这样实例化它:

>>> blue = Color(r=0, g=0, b=255)
>>> blue.r = 1
>>> # 能够设置更多的属性字段
>>> blue.e = 10

相较之下,namedtuple默认状况下没有可选字段。要增加它们,咱们须要一点技巧和一些元编程。

提醒:要增加 __hash__ 办法,您须要通过将设置 unsafe_hash 为使其不可变True

@dataclass(unsafe_hash=True)
class Color:
    ...

另一个区别是,拆箱 (unpacking) 是 namedtuples 的自带的性能 (first-class citizen)。如果心愿 数据类 具备雷同的行为,则必须实现本人。

from dataclasses import dataclass, astuple
...
@dataclass
class Color:
    """A regular class that represents a color."""
    r: float
    g: float
    b: float
    alpha: float

    def __iter__(self):
        yield from dataclasses.astuple(self)

性能比拟

仅比拟性能是不够的,namedtuple 和数据类在性能上也有所不同。数据类基于纯 Python 实现 dict。这使得它们在拜访字段时更快。另一方面,namedtuples 只是惯例的扩大 tuple。这意味着它们的实现基于更快的 C 代码并具备较小的内存占用量。

为了证实这一点,请思考在 Python 3.8.5 上进行此试验。

In [6]: import sys

In [7]: ColorTuple = namedtuple("Color", "r g b alpha")

In [8]: @dataclass
   ...: class ColorClass:
   ...:     """A regular class that represents a color."""
   ...:     r: float
   ...:     g: float
   ...:     b: float
   ...:     alpha: float
   ...: 

In [9]: color_tup = ColorTuple(r=50, g=205, b=50, alpha=1.0)

In [10]: color_cls = ColorClass(r=50, g=205, b=50, alpha=1.0)

In [11]: %timeit color_tup.r
36.8 ns ± 0.109 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)

In [12]: %timeit color_cls.r
38.4 ns ± 0.112 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)

In [15]: sys.getsizeof(color_tup)
Out[15]: 72

In [16]: sys.getsizeof(color_cls) + sys.getsizeof(vars(color_cls))
Out[16]: 152

如上,数据类在中拜访字段的速度稍快一些,然而它们比 nametuple 占用更多的内存空间。

如何将类型提醒增加到 namedtuple

数据类默认应用类型提醒。咱们也能够将它们放在 namedtuples 上。通过导入 Namedtuple 正文类型并从中继承,咱们能够对 Color 元组进行正文。

from typing import NamedTuple
...
class Color(NamedTuple):
    """A namedtuple that represents a color."""
    r: float
    g: float
    b: float
    alpha: float

另一个可能未引起留神的细节是,这种形式还容许咱们应用 docstring。如果输出,help(Color)咱们将可能看到它们。

Help on class Color in module __main__:

class Color(builtins.tuple)
 |  Color(r: float, g: float, b: float, alpha: Union[float, NoneType])
 |  
 |  A namedtuple that represents a color.
 |  
 |  Method resolution order:
 |      Color
 |      builtins.tuple
 |      builtins.object
 |  
 |  Methods defined here:
 |  
 |  __getnewargs__(self)
 |      Return self as a plain tuple.  Used by copy and pickle.
 |  
 |  __repr__(self)
 |      Return a nicely formatted representation string
 |  
 |  _asdict(self)
 |      Return a new dict which maps field names to their values.

如何将可选的默认值增加到 namedtuple

在上一节中,咱们理解了数据类能够具备可选值。另外,我提到要模拟上的雷同行为,namedtuple须要进行一些技巧批改操作。事实证明,咱们能够应用继承,如下例所示。

from collections import namedtuple

class Color(namedtuple("Color", "r g b alpha")):
    __slots__ = ()
    def __new__(cls, r, g, b, alpha=None):
        return super().__new__(cls, r, g, b, alpha)
>>> c = Color(r=0, g=0, b=0)
>>> c
Color(r=0, g=0, b=0, alpha=None)

论断

元组是一个十分弱小的数据结构。它们使咱们的代码更清洁,更牢靠。只管与新的 数据类 竞争强烈,但他们仍有大量的场景可用。在本教程中,咱们学习了应用 namedtuples 的几种办法,心愿您能够应用它们。

退出移动版