使用 namedtuple 编写 Pythonic 和干净的代码 |【生长吧!Python!】

目录

Python 的collections模块提供了一个名为的工厂函数namedtuple(),它专门设计用于在您使用元组时使您的代码更加Pythonic。使用namedtuple(),您可以创建不可变的序列类型,允许您使用描述性字段名称和点表示法而不是不明确的整数索引来访问它们的值。

如果您有使用 Python 的经验,那么您就会知道编写 Pythonic 代码是 Python 开发人员的核心技能。在本教程中,您将使用namedtuple.

在本教程中,您将学习如何:

  • namedtuple使用创建类namedtuple()
  • 识别并利用很酷的功能namedtuple
  • 使用namedtuple实例编写Pythonic 代码
  • 决定是否使用 namedtuple类似的数据结构
  • 子类namedtuple提供新功能

为了充分利用本教程,您需要对 Python 与编写 Pythonic 和可读代码相关的哲学有一个大致的了解。您还需要了解与以下人员合作的基础知识:

如果您在开始本教程之前没有掌握所有必需的知识,那也没关系!您可以根据需要停止并查看上述资源。

使用namedtuple编写Python的代码

Pythonnamedtuple()是一个工厂函数,可在collections. 它允许您创建tuple具有命名字段的子类。您可以使用点表示法和字段名称访问给定命名元组中的值,例如 in obj.attr

Python 的namedtuple创建是为了通过提供一种使用描述性字段名称而不是整数索引来访问值的方法来提高代码可读性,大多数情况下,整数索引不提供有关值的任何上下文。此功能还使代码更简洁,更易于维护。

相比之下,对常规元组中的值使用索引可能很烦人、难以阅读且容易出错。如果元组有很多字段并且构建的位置远离您使用它的地方,则尤其如此。

注意:在本教程中,您会发现用于指代 Python 的namedtuple、其工厂函数及其实例的不同术语。

为避免混淆,这里总结了整个教程中每个术语的使用方式:

学期 意义
namedtuple() 工厂功能
namedtuple,namedtuple班级 返回的元组子类 namedtuple()
namedtuple 实例,命名元组 特定namedtuple类的实例

在整个教程中,您会发现这些术语的使用及其相应的含义。

除了命名元组的这一主要功能外,您还会发现它们:

  • 不可变的数据结构
  • 具有一致的哈希
  • 可以用作字典键
  • 可以成套存储
  • 有一个基于类型和字段名称的有用文档字符串
  • 提供有用的字符串表示形式,以某种name=value格式打印元组内容
  • 支持索引
  • 提供其他方法和属性,例如._make()_asdict()._fields
  • 向后兼容与普通的元组
  • 具有常规元组相似的内存消耗

通常,您可以namedtuple在需要类似元组的对象的任何地方使用实例。命名元组的优势在于它们提供了一种使用字段名称和点表示法访问其值的方法。这将使您的代码更加 Pythonic。

通过namedtuple对它的简要介绍及其一般特性,您可以更深入地了解如何在代码中创建和使用它们。

创建类似元组的类 namedtuple()

您可以使用 anamedtuple()创建具有字段名称的不可变且类似于元组的数据结构。您可以在教程中找到的一个流行示例namedtuple是创建一个类来表示一个数学

根据问题的不同,您可能希望使用不可变的数据结构来表示给定的点。以下是使用常规元组创建二维点的方法:

>>>
>>> # Create a 2D point as a tuple
>>> point = (2, 4)
>>> point
(2, 4)

>>> # Access coordinate x
>>> point[0]
2
>>> # Access coordinate y
>>> point[1]
4

>>> # Try to update a coordinate value
>>> point[0] = 3
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment

在这里,您point使用常规tuple. 此代码有效:您有point两个坐标,并且不能修改任何这些坐标。但是,这段代码可读吗?你能预先说出01指数的含义吗?为了防止这些歧义,你可以使用namedtuple这样的:

>>>
>>> from collections import namedtuple

>>> # Create a namedtuple type, Point
>>> Point = namedtuple("Point", "x y")
>>> issubclass(Point, tuple)
True

>>> # Instantiate the new type
>>> point = Point(2, 4)
>>> point
Point(x=2, y=4)

>>> # Dot notation to access coordinates
>>> point.x
2
>>> point.y
4

>>> # Indexing to access coordinates
>>> point[0]
2
>>> point[1]
4

>>> # Named tuples are immutable
>>> point.x = 100
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: can't set attribute

现在你有point两个适当命名的字段,xy. 默认情况下,您point提供了用户友好且描述性的字符串表示形式 ( Point(x=2, y=4))。它允许您使用点表示法访问坐标,该表示法方便、易读且明确。您还可以使用索引来访问每个坐标的值。

注意:重要的是要注意,虽然元组和命名元组是不可变的,但它们存储的值不一定是不可变的。

创建包含可变值的元组或命名元组是完全合法的:

>>>
>>> from collections import namedtuple

>>> Person = namedtuple("Person", "name children")
>>> john = Person("John Doe", ["Timmy", "Jimmy"])
>>> john
Person(name='John Doe', children=['Timmy', 'Jimmy'])
>>> id(john.children)
139695902374144

>>> john.children.append("Tina")
>>> john
Person(name='John Doe', children=['Timmy', 'Jimmy', 'Tina'])
>>> id(john.children)
139695902374144

>>> hash(john)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'

您可以创建包含可变对象的命名元组。您可以修改底层元组中的可变对象。但是,这并不意味着您正在修改元组本身。元组将继续持有相同的内存引用。

最后,具有可变值的元组或命名元组不是hashable,如您在上面的示例中看到的。

最后,由于namedtuple类是 的子类tuple,它们也是不可变的。因此,如果您尝试更改坐标的值,则会得到AttributeError.

提供所需的参数 namedtuple()

正如您之前了解到的,namedtuple()是工厂函数而不是典型的数据结构。要创建 new namedtuple,您需要为函数提供两个位置参数:

  1. typenamenamedtuple为由 返回的提供类名namedtuple()。您需要将带有有效 Python 标识符的字符串传递给该参数。
  2. field_names提供您将用于访问元组中的值的字段名称。您可以使用以下方法提供字段名称:
    • 一个可迭代的字符串,例如["field1", "field2", ..., "fieldN"]
    • 每个字段名称以空格分隔的字符串,例如 "field1 field2 ... fieldN"
    • 每个字段名称以逗号分隔的字符串,例如 "field1, field2, ..., fieldN"

为了说明如何提供field_names,以下是创建点的不同方法:

>>>
>>> from collections import namedtuple

>>> # A list of strings for the field names
>>> Point = namedtuple("Point", ["x", "y"])
>>> Point
<class '__main__.Point'>
>>> Point(2, 4)
Point(x=2, y=4)

>>> # A string with comma-separated field names
>>> Point = namedtuple("Point", "x, y")
>>> Point
<class '__main__.Point'>
>>> Point(4, 8)
Point(x=4, y=8)

>>> # A generator expression for the field names
>>> Point = namedtuple("Point", (field for field in "xy"))
>>> Point
<class '__main__.Point'>
>>> Point(8, 16)
Point(x=8, y=16)

在这些示例中,您首先Point使用list字段名称创建。然后您使用带有以逗号分隔的字段名称的字符串。最后,您使用生成器表达式。在这个例子中,最后一个选项可能看起来有点矫枉过正。但是,它旨在说明该过程的灵活性。

注意:如果您使用可迭代对象来提供字段名称,那么您应该使用类似序列的可迭代对象,因为字段的顺序对于产生可靠的结果很重要。

set例如,使用 a可以工作,但可能会产生意想不到的结果:

>>>
>>> from collections import namedtuple

>>> Point = namedtuple("Point", {"x", "y"})
>>> Point(2, 4)
Point(y=2, x=4)

当您使用无序可迭代对象向 a 提供字段时namedtuple,您可能会得到意想不到的结果。在上面的示例中,坐标名称被交换,这可能不适合您的用例。

您可以对字段名称使用任何有效的 Python 标识符,但以下情况除外:

  • 以下划线 ( _)开头的名称
  • Python keywords

如果您提供违反这些条件之一的字段名称,则您会得到ValueError

>>>
>>> from collections import namedtuple

>>> Point = namedtuple("Point", ["x", "_y"])
Traceback (most recent call last):
  ...
ValueError: Field names cannot start with an underscore: '_y'

在此示例中,第二个字段名称以和下划线开头,因此您会收到ValueError通知,字段名称不能以该字符开头。这是为了避免与namedtuple方法和属性的名称冲突。

在 的情况下typename,当您查看上面的示例时可能会出现一个问题:为什么我需要提供typename参数?答案是您需要为由 返回的类命名namedtuple()。这就像为现有类创建别名:

>>>
>>> from collections import namedtuple

>>> Point1 = namedtuple("Point", "x y")
>>> Point1
<class '__main__.Point'>

>>> class Point:
...     def __init__(self, x, y):
...         self.x = x
...         self.y = y
...

>>> Point2 = Point
>>> Point2
<class '__main__.Point'>

在第一个示例中,您Point使用namedtuple(). 然后将这个新类型分配给全局 变量 Point1。在第二个示例中,您创建一个也名为 的常规 Python 类Point,然后将该类分配给Point2。在这两种情况下,类名都是PointPoint1Point2是手头类的别名。

最后,您还可以使用关键字参数或提供现有字典来创建命名元组,如下所示:

>>>
>>> from collections import namedtuple

>>> Point = namedtuple("Point", "x y")

>>> Point(x=2, y=4)
Point(x=2, y=4)

>>> Point(**{"x": 4, "y": 8})
Point(x=4, y=8)

在第一个示例中,您使用关键字参数来创建一个Point对象。在第二个示例中,您使用的字典的键与 的字段匹配Point。在这种情况下,您需要执行字典解包

使用可选参数 namedtuple()

除了两个必需的参数外,namedtuple()工厂函数还采用以下可选参数:

  • rename
  • defaults
  • module

如果设置renameTrue,则所有无效字段名称都会自动替换为位置名称。

假设您的公司有一个用 Python 编写的旧数据库应用程序,用于管理与公司一起旅行的乘客的数据。你被要求更新系统,然后你开始创建命名元组来存储你从数据库中读取的数据。

该应用程序提供了一个名为的函数get_column_names(),该函数返回带有列名的字符串列表,您认为可以使用该函数创建一个namedtuple类。你最终得到以下代码:

# passenger.py

from collections import namedtuple

from database import get_column_names

Passenger = namedtuple("Passenger", get_column_names())

但是,当您运行代码时,您会得到如下所示的异常回溯

Traceback (most recent call last):
  ...
ValueError: Type names and field names cannot be a keyword: 'class'

这告诉您class列名称不是您的namedtuple类的有效字段名称。为了防止这种情况,您决定使用rename

# passenger.py

# ...

Passenger = namedtuple("Passenger", get_column_names(), rename=True)

这会导致namedtuple()自动用位置名称替换无效名称。现在假设您从数据库中检索一行并创建您的第一个Passenger实例,如下所示:

>>>
>>> from passenger import Passenger
>>> from database import get_passenger_by_id

>>> Passenger(get_passenger_by_id("1234"))
Passenger(_0=1234, name='john', _2='Business', _3='John Doe')

在这种情况下,get_passenger_by_id()是您假设的应用程序中可用的另一个函数。它检索元组中给定乘客的数据。最终的结果是你新创建的passenger有三个位置字段名,并且只name反映了原来的列名。当您深入数据库时​​,您会发现乘客表具有以下列:

柱子 商店 换了? 原因
_id 每个乘客的唯一标识符 是的 它以下划线开头。
name 每位乘客的简称 这是一个有效的 Python 标识符。
class 乘客乘坐的舱位 是的 这是一个 Python 关键字。
name 乘客的全名 是的 它重复了。

在基于您控制之外的值创建命名元组的情况下,该rename选项应设置为,True以便使用有效的位置名称重命名无效字段。

的第二个可选参数namedtuple()defaults。此参数默认为None,这意味着字段将没有默认值。您可以设置defaults为可迭代的值。在这种情况下,namedtuple()defaults迭代中的值分配给最右边的字段:

>>>
>>> from collections import namedtuple

>>> Developer = namedtuple(
...     "Developer",
...     "name level language",
...     defaults=["Junior", "Python"]
... )

>>> Developer("John")
Developer(name='John', level='Junior', language='Python')

在此示例中,levellanguage字段具有默认值。这使它们成为可选参数。由于您没有为 定义默认值name,因此您需要在创建namedtuple实例时提供一个值。因此,需要没有默认值的参数。请注意,默认值应用于最右侧的字段。

的最后一个参数namedtuple()module。如果为此参数提供有效的模块名称,则结果的.__module__属性将namedtuple设置为该值。此属性保存定义给定函数或可调用对象的模块的名称:

>>>
>>> from collections import namedtuple

>>> Point = namedtuple("Point", "x y", module="custom")
>>> Point
<class 'custom.Point'>
>>> Point.__module__
'custom'

在此示例中,当您访问.__module__on 时Point,您会得到'custom'结果。这表明您的Point类是在您的custom模块中定义的。

Python 3.6 中添加参数的动机是使命名元组能够通过不同的Python 实现支持酸洗modulenamedtuple()

探索namedtuple类的附加功能

除了从继承的方法tuple,如.count().index()namedtuple类还提供三种额外的方法和两个属性。为了防止与自定义字段的名称冲突,这些属性和方法的名称以下划线开头。在本节中,您将了解这些方法和属性以及它们的工作原理。

namedtuple从可迭代对象创建实例

您可以使用._make()来创建命名元组实例。该方法接受一个可迭代的值并返回一个新的命名元组:

>>>
>>> from collections import namedtuple

>>> Person = namedtuple("Person", "name age height")
>>> Person._make(["Jane", 25, 1.75])
Person(name='Jane', age=25, height=1.75)

在这里,您首先Person使用namedtuple(). 然后,您._make()使用 .csv 文件中每个字段的值列表进行调用namedtuple。请注意,这._make()是一个用作替代类构造函数并返回一个新命名元组实例的类方法

最后,._make()期望一个可迭代对象作为参数,list在上面的例子中是 a 。另一方面,namedtuple构造函数可以采用位置参数或关键字参数,正如您已经了解的那样。

namedtuple实例转换为字典

您可以使用 将现有的命名元组实例转换为字典._asdict()。此方法返回一个使用字段名称作为键的新字典。结果字典的键与原始字典中的字段顺序相同namedtuple

>>>
>>> from collections import namedtuple

>>> Person = namedtuple("Person", "name age height")
>>> jane = Person("Jane", 25, 1.75)
>>> jane._asdict()
{'name': 'Jane', 'age': 25, 'height': 1.75}

当您调用._asdict()命名元组时,您将获得一个新dict对象,该对象将字段名称映射到原始命名元组中的相应值。

Python 3.8 开始._asdict()返回了一个普通字典。在此之前,它返回了一个OrderedDict对象:

>>>
Python 3.7.9 (default, Jan 14 2021, 11:41:20)
[GCC 9.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from collections import namedtuple

>>> Person = namedtuple("Person", "name age height")
>>> jane = Person("Jane", 25, 1.75)
>>> jane._asdict()
OrderedDict([('name', 'Jane'), ('age', 25), ('height', 1.75)])

Python 3.8 更新._asdict()为返回常规字典,因为字典会记住其键在 Python 3.6 及更高版本中的插入顺序。请注意,结果字典中键的顺序等同于原始命名元组中字段的顺序。

替换现有namedtuple实例中的字段

您将学习的最后一种方法是._replace(). 此方法采用表单的关键字参数field=value并返回一个namedtuple更新所选字段值的新实例:

>>>
>>> from collections import namedtuple

>>> Person = namedtuple("Person", "name age height")
>>> jane = Person("Jane", 25, 1.75)

>>> # After Jane's birthday
>>> jane = jane._replace(age=26)
>>> jane
Person(name='Jane', age=26, height=1.75)

在此示例中,您在 Jane 生日后更新她的年龄。尽管 的名称._replace()可能暗示该方法修改了现有的命名元组,但实际情况并非如此。这是因为namedtuple实例是不可变的,所以._replace()不会jane 就地更新。

探索附加namedtuple属性

命名元组还有两个额外的属性:._fields._field_defaults. 第一个属性包含一个列出字段名称的字符串元组。第二个属性包含一个字典,该字典将字段名称映射到它们各自的默认值(如果有)。

在 的情况下._fields,您可以使用它来内省您的namedtuple类和实例。您还可以从现有类创建新类:

>>>
>>> from collections import namedtuple

>>> Person = namedtuple("Person", "name age height")

>>> ExtendedPerson = namedtuple(
...     "ExtendedPerson",
...     [*Person._fields, "weight"]
... )

>>> jane = ExtendedPerson("Jane", 26, 1.75, 67)
>>> jane
ExtendedPerson(name='Jane', age=26, height=1.75, weight=67)
>>> jane.weight
67

在此示例中,您将创建一个具有新字段的新namedtuple调用ExtendedPersonweight。这种新类型扩展了您的旧Person. 为此,您可以访问._fieldsonPerson并将其解压缩到一个新列表以及一个附加字段weight.

您还可以使用 Python._fields来迭代给定namedtuple实例中的字段和值zip()

>>>
>>> from collections import namedtuple

>>> Person = namedtuple("Person", "name age height weight")
>>> jane = Person("Jane", 26, 1.75, 67)
>>> for field, value in zip(jane._fields, jane):
...     print(field, "->", value)
...
name -> Jane
age -> 26
height -> 1.75
weight -> 67

在这个例子中,zip()产生形式为 的元组(field, value)。这样,您就可以访问底层命名元组中的字段-值对的两个元素。另一种同时迭代字段和值的方法是使用._asdict().items(). 来试试看吧!

使用._field_defaults,您可以内省namedtuple类和实例以找出哪些字段提供默认值。具有默认值使您的字段可选。例如,假设您的Person类应该包含一个额外的字段来保存此人居住的国家/地区。由于您主要与来自加拿大的人一起工作,因此您可以为该country字段设置适当的默认值,如下所示:

>>>
>>> from collections import namedtuple

>>> Person = namedtuple(
...     "Person",
...     "name age height weight country",
...     defaults=["Canada"]
... )

>>> Person._field_defaults
{'country': 'Canada'}

通过对 的快速查询._field_defaults,您可以找出给定的哪些字段namedtuple提供默认值。在此示例中,您团队中的任何其他程序员都可以看到您的Person"Canada"country.

如果您namedtuple不提供默认值,则.field_defaults保存一个空字典:

>>>
>>> from collections import namedtuple

>>> Person = namedtuple("Person", "name age height weight country")
>>> Person._field_defaults
{}

如果您没有向 提供默认值列表namedtuple(),则它依赖于 的默认值defaults,即None。在这种情况下,._field_defaults持有一个空字典。

编写 Pythonic 代码 namedtuple

可以说,命名元组的基本用例是帮助您编写更多 Pythonic 代码。该namedtuple()工厂的功能是为了让你写可读的,明确的,干净,和维护的代码。

在本节中,您将编写大量实际示例,这些示例将帮助您发现使用命名元组而不是常规元组的好机会,从而使您的代码更加 Pythonic。

使用字段名称而不是索引

假设您正在创建一个绘画应用程序,并且您需要根据用户的选择定义要使用的笔属性。您已经在元组中对笔的属性进行了编码:

>>>
>>> pen = (2, "Solid", True)

>>> if pen[0] == 2 and pen[1] == "Solid" and pen[2]:
...     print("Standard pen selected")
...
Standard pen selected

这行代码定义了一个具有三个值的元组。你能说出每个值的含义吗?也许你能猜到第二个值和线型有关,但是2and是什么意思True呢?

您可以添加一个很好的注释来为 提供一些上下文pen,在这种情况下,您最终会得到如下内容:

>>>
>>> # Tuple containing: line weight, line style, and beveled edges
>>> pen = (2, "Solid", True)

凉爽的!现在您知道元组中每个值的含义。但是,如果您或其他程序员使用的pen远离此定义怎么办?他们必须回到定义来记住每个值的含义。

这是pen使用 a的另一种实现namedtuple

>>>
>>> from collections import namedtuple

>>> Pen = namedtuple("Pen", "width style beveled")
>>> pen = Pen(2, "Solid", True)

>>> if pen.width == 2 and pen.style == "Solid" and pen.beveled:
...     print("Standard pen selected")
...
Standard pen selected

现在,您的代码清楚地表明2表示笔的宽度、"Solid"线条样式等。任何阅读您代码的人都可以看到并理解这一点。您的新实现pen有两行额外的代码。这是在可读性和可维护性方面取得巨大胜利的少量工作。

从函数返回多个命名值

可以使用命名元组的另一种情况是需要从给定函数返回多个值。在这种情况下,使用命名元组可以使您的代码更具可读性,因为返回的值还将为其内容提供一些上下文。

例如,Python 提供了一个内置函数divmod(),该函数接受两个数字作为参数,并返回一个元组,其中包含输入数字的整数除法的数:

>>>
>>> divmod(8, 4)
(2, 0)

要记住每个数字的含义,您可能需要阅读 的文档,divmod()因为数字本身并没有提供有关其各自含义的太多信息。该函数的名称也没有太大帮助。

这是一个函数,它使用 anamedtuple来阐明每个divmod()返回数字的含义:

>>>
>>> from collections import namedtuple

>>> def custom_divmod(a, b):
...     DivMod = namedtuple("DivMod", "quotient remainder")
...     return DivMod(*divmod(a, b))
...

>>> custom_divmod(8, 4)
DivMod(quotient=2, remainder=0)

在此示例中,您为每个返回值添加了上下文,因此任何阅读您代码的程序员都可以立即理解每个数字的含义。

减少函数的参数数量

减少函数可以采用的参数数量被认为是最佳编程实践。这使您的函数签名更加简洁并优化您的测试过程,因为参数数量和它们之间可能的组合减少了。

同样,您应该考虑使用命名元组来处理这个用例。假设您正在编写一个应用程序来管理客户的信息。该应用程序使用数据库来存储客户的数据。为了处理数据和更新数据库,您已经创建了几个函数。您的高级函数之一是create_user(),如下所示:

def create_user(db, username, client_name, plan):
    db.add_user(username)
    db.complete_user_profile(username, client_name, plan)

这个函数有四个参数。第一个参数 ,db代表您正在使用的数据库。其余的参数与给定的客户密切相关。这是减少create_user()使用命名元组的参数数量的好机会:

User = namedtuple("User", "username client_name plan")
user = User("john", "John Doe", "Premium")

def create_user(db, user):
    db.add_user(user.username)
    db.complete_user_profile(
        user.username,
        user.client_name,
        user.plan
    )

现在create_user()只需要两个参数:dbuser。在函数内部,您使用方便且具有描述性的字段名称来为db.add_user()和提供参数db.complete_user_profile()。您的高级函数create_user()更侧重于user. 测试也更容易,因为您只需要为每个测试提供两个参数。

从文件和数据库中读取表格数据

命名元组的一个非常常见的用例是使用它们来存储数据库记录。您可以namedtuple使用列名作为字段名来定义类,并将数据从数据库中的行检索到命名元组。您还可以对CSV 文件执行类似操作

例如,假设您有一个 CSV 文件,其中包含有关贵公司员工的数据,并且您希望将该数据读入合适的数据结构以进行进一步处理。您的 CSV 文件如下所示:

name,job,email
"Linda","Technical Lead","linda@example.com"
"Joe","Senior Web Developer","joe@example.com"
"Lara","Project Manager","lara@example.com"
"David","Data Analyst","david@example.com"
"Jane","Senior Python Developer","jane@example.com"

您正在考虑使用 Python 的csv模块及其模块DictReader来处理文件,但是您还有一个额外的要求——您需要将数据存储到一个不可变的轻量级数据结构中。在这种情况下, anamedtuple可能是一个不错的选择:

>>>
>>> import csv
>>> from collections import namedtuple

>>> with open("employees.csv", "r") as csv_file:
...     reader = csv.reader(csv_file)
...     Employee = namedtuple("Employee", next(reader), rename=True)
...     for row in reader:
...         employee = Employee(*row)
...         print(employee.name, employee.job, employee.email)
...
Linda Technical Lead linda@example.com
Joe Senior Web Developer joe@example.com
Lara Project Manager lara@example.com
David Data Analyst david@example.com
Jane Senior Python Developer jane@example.com

在本例中,您首先employees.csvwith语句中打开文件。然后使用csv.reader()CSV 文件中的行获取迭代器。使用namedtuple(),您可以创建一个新Employee类。调用从next()检索第一行数据reader,其中包含 CSV 文件标题。此标头为您的namedtuple.

注意:当您创建一个namedtuple基于您无法控制的字段名称时,您应该设置.renameTrue. 这样,您可以防止无效字段名称的问题,这可能是您处理数据库表和查询​​、CSV 文件或任何其他类型的表格数据时的常见情况。

最后,for循环EmployeerowCSV 文件中的每个创建一个实例,并将员工列表打印到屏幕上。

使用namedtuplevs 其他数据结构

到目前为止,您已经学习了如何创建命名元组以使您的代码更具可读性、明确性和 Pythonic。您还编写了一些示例,帮助您发现在代码中使用命名元组的机会。

在本节中,您将大致了解namedtuple类与其他 Python 数据结构(例如字典、数据类和类型化命名元组)之间的异同。您将就以下特征将命名元组与其他数据结构进行比较:

  • 可读性
  • 可变性
  • 内存使用情况
  • 表现

这样,您将更好地准备为您的特定用例选择正确的数据结构。

namedtuple VS 字典

字典是Python中的基本数据结构。该语言本身是围绕字典构建的,因此它们无处不在。由于它们是如此常见和有用,您可能在代码中经常使用它们。但是字典和命名元组有什么不同呢?

在可读性方面,您可能会说字典与命名元组一样可读。尽管它们没有提供通过点表示法访问属性的方法,但字典样式的键查找非常易读且简单:

>>>
>>> from collections import namedtuple

>>> jane = {"name": "Jane", "age": 25, "height": 1.75}
>>> jane["age"]
25

>>> # Equivalent named tuple
>>> Person = namedtuple("Person", "name age height")
>>> jane = Person("Jane", 25, 1.75)
>>> jane.age
25

在这两个示例中,您都对代码及其意图有了全面的了解。命名元组定义需要的代码两条额外的线条,虽然:一行导入namedtuple()工厂功能和另一个定义namedtuple类,Person

两种数据结构之间的一个很大区别是字典是可变的,而命名元组是不可变的。这意味着您可以就地修改字典,但不能修改命名元组:

>>>
>>> from collections import namedtuple

>>> jane = {"name": "Jane", "age": 25, "height": 1.75}
>>> jane["age"] = 26
>>> jane["age"]
26
>>> jane["weight"] = 67
>>> jane
{'name': 'Jane', 'age': 26, 'height': 1.75, 'weight': 67}

>>> # Equivalent named tuple
>>> Person = namedtuple("Person", "name age height")
>>> jane = Person("Jane", 25, 1.75)

>>> jane.age = 26
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: can't set attribute

>>> jane.weight = 67
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Person' object has no attribute 'weight'

您可以更新字典中现有键的值,但不能在命名元组中执行类似操作。您可以向现有字典添加新的键值对,但不能向现有命名元组添加字段值对。

注意:在命名元组中,您可以使用._replace()来更新给定字段的值,但该方法会创建并返回一个新的命名元组实例,而不是就地更新底层实例。

一般来说,如果您需要一个不可变的数据结构来正确解决给定的问题,那么可以考虑使用命名元组而不是字典来满足您的要求。

关于内存使用,命名元组是一种非常轻量级的数据结构。启动您的代码编辑器或 IDE并创建以下脚本:

# namedtuple_dict_memory.py

from collections import namedtuple
from pympler import asizeof

Point = namedtuple("Point", "x y z")
point = Point(1, 2, 3)

namedtuple_size = asizeof.asizeof(point)
dict_size = asizeof.asizeof(point._asdict())
gain = 100 - namedtuple_size / dict_size * 100

print(f"namedtuple: {namedtuple_size} bytes ({gain:.2f}% smaller)")
print(f"dict:       {dict_size} bytes")

这个小脚本使用asizeof.asizeof()来自Pympler来获取命名元组及其等效字典的内存占用。

注意: Pympler 是一个监控和分析 Python 对象内存行为的工具。

您可以从安装它的PyPI使用pip像往常一样:

$ pip install pympler

运行此命令后,Pympler 将在您的Python 环境中可用,因此您可以运行上述脚本。

如果从命令行运行脚本,则会得到以下输出:

$ python namedtuple_dict_memory.py
namedtuple: 160 bytes (67.74% smaller)
dict:       496 bytes

此输出确认命名元组比等效字典消耗更少的内存。因此,如果内存消耗对您来说是一个限制,那么您应该考虑使用命名元组而不是字典。

注意:当您比较命名元组和字典时,最终的内存消耗差异将取决于值的数量及其类型。使用不同的值,您会得到不同的结果。

最后,您需要了解命名元组和字典在操作性能方面的不同之处。为此,您将测试成员资格和属性访问操作。回到您的代码编辑器并创建以下脚本:

# namedtuple_dict_time.py

from collections import namedtuple
from time import perf_counter

def average_time(structure, test_func):
    time_measurements = []
    for _ in range(1_000_000):
        start = perf_counter()
        test_func(structure)
        end = perf_counter()
        time_measurements.append(end - start)
    return sum(time_measurements) / len(time_measurements) * int(1e9)

def time_dict(dictionary):
    "x" in dictionary
    "missing_key" in dictionary
    2 in dictionary.values()
    "missing_value" in dictionary.values()
    dictionary["y"]

def time_namedtuple(named_tuple):
    "x" in named_tuple._fields
    "missing_field" in named_tuple._fields
    2 in named_tuple
    "missing_value" in named_tuple
    named_tuple.y

Point = namedtuple("Point", "x y z")
point = Point(x=1, y=2, z=3)

namedtuple_time = average_time(point, time_namedtuple)
dict_time = average_time(point._asdict(), time_dict)
gain = dict_time / namedtuple_time

print(f"namedtuple: {namedtuple_time:.2f} ns ({gain:.2f}x faster)")
print(f"dict:       {dict_time:.2f} ns")

此脚本对字典和命名元组共有的操作进行计时,例如成员资格测试和属性访问。在当前系统上运行脚本会显示类似于以下内容的输出:

$ namedtuple_dict_time.py
namedtuple: 527.26 ns (1.36x faster)
dict:       717.71 ns

此输出显示对命名元组的操作比对字典的类似操作稍快。

namedtuple VS 数据类

Python 3.7带来了一个很酷的新特性:数据类。根据PEP 557,数据类类似于命名元组,但它们是可变的:

数据类可以被认为是“具有默认值的可变命名元组”。(来源

然而,更准确地说,数据类就像带有类型提示的可变命名元组。“默认”部分根本没有区别,因为命名元组也可以为其字段提供默认值。因此,乍一看,主要区别在于可变性和类型提示。

要创建数据类,您需要dataclass()dataclasses. 然后您可以使用常规类定义语法定义数据类:

>>>
>>> from dataclasses import dataclass

>>> @dataclass
... class Person:
...     name: str
...     age: int
...     height: float
...     weight: float
...     country: str = "Canada"
...

>>> jane = Person("Jane", 25, 1.75, 67)
>>> jane
Person(name='Jane', age=25, height=1.75, weight=67, country='Canada')
>>> jane.name
'Jane'
>>> jane.name = "Jane Doe"
>>> jane.name
'Jane Doe'

在可读性方面,数据类和命名元组之间没有显着差异。它们提供类似的字符串表示,您可以使用点表示法访问它们的属性。

在可变性方面,数据类根据定义是可变的,因此您可以在需要时更改其属性的值。然而,他们袖手旁观。您可以将dataclass()装饰器的frozen参数设置为True不可变:

>>>
>>> from dataclasses import dataclass

>>> @dataclass(frozen=True)
... class Person:
...     name: str
...     age: int
...     height: float
...     weight: float
...     country: str = "Canada"
...

>>> jane = Person("Jane", 25, 1.75, 67)
>>> jane.name = "Jane Doe"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<string>", line 4, in __setattr__
dataclasses.FrozenInstanceError: cannot assign to field 'name'

如果您在对 的调用中设置frozen为,那么您将使数据类不可变。在这种情况下,当您尝试更新 Jane 的姓名时,您会得到一个.Truedataclass()FrozenInstanceError

命名元组和数据类之间的另一个细微差别是后者默认不可迭代。坚持 Jane 示例并尝试迭代她的数据:

>>>
>>> for field in jane:
...     print(field)
...
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'Person' object is not iterable

如果您尝试迭代一个基本数据类,那么您会得到一个TypeError. 这在普通课程中很常见。幸运的是,有一些方法可以解决它。例如,您可以添加一个.__iter__()特殊的方法来Person像这样:

>>>
>>> from dataclasses import astuple, dataclass

>>> @dataclass
... class Person:
...     name: str
...     age: int
...     height: float
...     weight: float
...     country: str = "Canada"
...     def __iter__(self):
...         return iter(astuple(self))
...

>>> for field in Person("Jane", 25, 1.75, 67):
...     print(field)
...
Jane
25
1.75
67
Canada

在这里,您首先astuple()dataclasses. 此函数将数据类转换为元组。然后,你将得到的元组iter(),所以你可以建立并返回一个迭代器.__iter__()。通过此添加,您可以开始迭代 Jane 的数据。

关于内存消耗,命名元组比数据类更轻量级。您可以通过创建并运行一个类似于您在上一节中看到的小脚本来确认这一点。要查看完整的脚本,请展开下面的框。

比较内存使用情况的脚本:namedtuplevs 数据类显示隐藏

以下是运行脚本的结果:

$ python namedtuple_dataclass_memory.py
namedtuple: 160 bytes (61.54% smaller)
data class: 416 bytes

namedtuple类不同,数据类保留每个实例.__dict__来存储可写的实例属性。这会导致更大的内存占用。

接下来,您可以展开以下部分以查看代码示例,该示例namedtuple根据属性访问的性能比较类和数据类。

比较性能的脚本:namedtuplevs 数据类显示隐藏

在性能方面,以下是结果:

$ python namedtuple_dataclass_time.py
namedtuple: 274.32 ns (1.08x faster)
data class: 295.37 ns

性能差异很小,因此您可以说两种数据结构在属性访问操作方面的性能相当。

namedtuple 对比 typing.NamedTuple

Python 3.5 引入了一个临时模块,称为typing支持函数类型注释或类型提示。此模块提供NamedTuple,它是namedtuple. 使用NamedTuple,您可以创建namedtuple带有类型提示的类。按照Person示例,您可以创建一个等效的类型命名元组,如下所示:

>>>
>>> from typing import NamedTuple

>>> class Person(NamedTuple):
...     name: str
...     age: int
...     height: float
...     weight: float
...     country: str = "Canada"
...

>>> issubclass(Person, tuple)
True
>>> jane = Person("Jane", 25, 1.75, 67)
>>> jane.name
'Jane'
>>> jane.name = "Jane Doe"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: can't set attribute

使用NamedTuple,您可以通过点表示法创建支持类型提示和属性访问的元组子类。由于生成的类是元组子类,因此它也是不可变的。

在上面的例子中需要注意的一个微妙细节是NamedTuple子类看起来比命名元组更类似于数据类。

在内存消耗方面,namedtupleNamedTuple实例使用相同数量的内存。您可以展开下面的框以查看比较两者内存使用情况的脚本。

比较内存使用情况的脚本:namedtuplevstyping.NamedTuple显示隐藏

这一次,比较内存使用情况的脚本产生以下输出:

$ python typed_namedtuple_memory.py
namedtuple:        160 bytes
typing.NamedTuple: 160 bytes

在这种情况下,两个实例消耗相同数量的内存,因此这次没有赢家。

由于namedtuple类和NamedTuple子类都是 的子类tuple,因此它们有很多共同点。在这种情况下,您可以对字段和值进行时间成员资格测试。您还可以使用点表示法对属性访问进行计时。展开下面的框,查看比较两者的性能脚本namedtupleNamedTuple

比较性能的脚本:namedtuplevstyping.NamedTuple显示隐藏

结果如下:

$ python typed_namedtuple_time.py
namedtuple:        503.34 ns
typing.NamedTuple: 509.91 ns

在这种情况下,您可以说两种数据结构在性能方面的行为几乎相同。除此之外,NamedTuple用于创建命名元组可以使您的代码更加明确,因为您可以向字段添加类型信息。您还可以为键入的命名元组提供默认值、添加新功能和编写文档字符串

在本节中,您已经了解了很多namedtuple其他类似的数据结构和类。下面的表格总结了namedtuple与本节介绍的数据结构的比较:

dict 数据类 NamedTuple
可读性 相似的 平等的 平等的
不变性 默认为否,如果使用则为 @dataclass(frozen=True) 是的
内存使用情况 更高 更高 平等的
表现 慢点 相似的 相似的
可迭代性 是的 默认为否,如果提供则为 .__iter__() 是的

通过此摘要,您将能够选择最适合您当前需求的数据结构。此外,您应该考虑数据类并NamedTuple允许您添加类型提示,这是目前 Python 代码中非常理想的功能。

namedtuple类化

由于namedtuple类是常规 Python 类,如果您需要提供附加功能、文档字符串、用户友好的字符串表示等,您可以将它们子类化。

例如,在对象中存储一个人的年龄不被认为是最佳实践。因此,您可能希望存储出生日期并在需要时计算年龄:

>>>
>>> from collections import namedtuple
>>> from datetime import date

>>> BasePerson = namedtuple(
...     "BasePerson",
...     "name birthdate country",
...     defaults=["Canada"]
... )

>>> class Person(BasePerson):
...     """A namedtuple subclass to hold a person's data."""
...     __slots__ = ()
...     def __repr__(self):
...         return f"Name: {self.name}, age: {self.age} years old."
...     @property
...     def age(self):
...         return (date.today() - self.birthdate).days // 365
...

>>> Person.__doc__
"A namedtuple subclass to hold a person's data."

>>> jane = Person("Jane", date(1996, 3, 5))
>>> jane.age
25
>>> jane
Name: Jane, age: 25 years old.

Person继承自BasePerson,这是一个namedtuple类。在子类定义中,您首先添加一个文档字符串来描述该类的作用。然后您设置__slots__为一个空元组,这会阻止自动创建 per-instance .__dict__。这使您的BasePerson子类内存保持高效。

您还添加了一个自定义.__repr__()来为类提供一个很好的字符串表示。最后,您添加一个属性来使用 计算此人的年龄datetime

测量创建时间:tuplevsnamedtuple

到目前为止,您已经namedtuple根据几个特性将类与其他数据结构进行了比较。在本节中,您将大致了解常规元组和命名元组在创建时间方面的比较。

假设您有一个动态创建大量元组的应用程序。您决定使用命名元组使您的代码更加 Pythonic 和可维护。一旦您更新了所有代码库以使用命名元组,您就可以运行应用程序并注意到一些性能问题。经过一些测试,您得出结论,问题可能与动态创建命名元组有关。

这是一个测量动态创建多个元组和命名元组所需的平均时间的脚本:

# tuple_namedtuple_time.py

from collections import namedtuple
from time import perf_counter

def average_time(test_func):
    time_measurements = []
    for _ in range(1_000):
        start = perf_counter()
        test_func()
        end = perf_counter()
        time_measurements.append(end - start)
    return sum(time_measurements) / len(time_measurements) * int(1e9)

def time_tuple():
    tuple([1] * 1000)

fields = [f"a{n}" for n in range(1000)]
TestNamedTuple = namedtuple("TestNamedTuple", fields)

def time_namedtuple():
    TestNamedTuple(*([1] * 1000))

namedtuple_time = average_time(time_namedtuple)
tuple_time = average_time(time_tuple)
gain = namedtuple_time / tuple_time

print(f"tuple:      {tuple_time:.2f} ns ({gain:.2f}x faster)")
print(f"namedtuple: {namedtuple_time:.2f} ns")

在此脚本中,您计算​​创建多个元组及其等效命名元组所需的平均时间。如果您从命令行运行该脚本,您将获得类似于以下内容的输出:

$ python tuple_namedtuple_time.py
tuple:      7075.82 ns (3.36x faster)
namedtuple: 23773.67 ns

当您查看此输出时,您会发现tuple动态创建对象比创建类似命名的元组快得多。在某些情况下,例如使用大型数据库,创建命名元组所需的额外时间会严重影响应用程序的性能,因此如果您的代码动态创建大量元组,请注意这一点。

结论

编写Pythonic代码是 Python 开发领域的一项急需技能。Pythonic 代码可读、明确、干净、可维护,并利用 Python 习语和最佳实践。在本教程中,您学习了创建namedtuple类和实例以及它们如何帮助您提高 Python 代码的质量。

在本教程中,您学习了:

  • 如何创建和使用namedtuple类和实例
  • 如何利用很酷的namedtuple功能
  • 何时使用namedtuple实例编写Pythonic 代码
  • 何时使用 namedtuple而不是类似的数据结构
  • 如何子类化 namedtuple以添加新功能

有了这些知识,您可以深入提高现有和未来代码的质量。如果您经常使用元组,请考虑在有意义的时候将它们转换为命名元组。这样做将使您的代码更具可读性和 Pythonic。

【生长吧!Python】有奖征文火热进行中:https://bbs.huaweicloud.com/blogs/278897

(完)