最全的命名元组Namedtuple使用指南!!!

共 11267字,需浏览 23分钟

 ·

2020-10-24 08:01

 △点击上方Python猫”关注 ,回复“1”领取电子书

剧照 | 《鬼灭之刃》

译者:DeanWu
来源:码农吴先生

原文地址:https://miguendes.me/everything-you-need-to-know-about-pythons-namedtuples

作者:Miguel Brito

本文将讨论 python 中namedtuple的几个重点用法。我们将由浅入深的介绍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 5020550, alpha
    elif desc == "blue":
        return 00255, alpha
    else:
        return 000, 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)
-13-8c7f9b29c633> in 
----> 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。你可以在这里[1]找到所有这些。

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

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

如何对 namedtuples 列表进行排序

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

幸运的是,Python 允许使用非常 Python 化的方式来执行此操作。我们可以使用operator.attrgetter运算符。根据文档[2]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[3]将它们称为“具有默认值的可变 namedtuple”(译者注:Data Class python 3.7 引入,参考:https://docs.python.org/zh-cn/3/library/dataclasses.html)。

现在,让我们看看如何用数据类来实现。

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的几种方法,希望你可以使用它们。

参考资料

[1]

这里: https://docs.python.org/3/library/collections.html#collections.somenamedtuple._asdict

[2]

文档: https://docs.python.org/3/library/operator.html#operator.attrgetter

[3]

PEP: https://www.python.org/dev/peps/pep-0557/#abstract

Python猫技术交流群开放啦!群里既有国内一二线大厂在职员工,也有国内外高校在读学生,既有十多年码龄的编程老鸟,也有中小学刚刚入门的新人,学习氛围良好!想入群的同学,请在公号内回复『交流群』,获取猫哥的微信(谢绝广告党,非诚勿扰!)~

近期热门文章推荐:

耗时两年,我终于出了一本电子书!

为什么说 Python 内置函数并不是万能的?

Python 函数为什么会默认返回 None?

一篇文章掌握 Python 内置 zip() 的全部内容

感谢创作者的好文
浏览 135
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报