原文地址: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,b
和alpha
不再存在。
字典的第二个问题是它们不可散列。这意味着咱们无奈将它们存储在 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)
咱们能够利用该 **
构造将包解压缩 dict
为namedtuple
。
如果我想从 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 转换为字典或惯例元组
咱们刚刚学习了如何将转换 namedtuple
为dict
。反过来呢?咱们又如何将其转换为字典实例?
试验证实,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
的几种办法,心愿您能够应用它们。