用namedtuple编写Python风格和干净代码

MicroPython相关代码、库、软件、工具
回复
头像
shaoziyang
帖子: 3917
注册时间: 2019年 10月 21日 13:48

用namedtuple编写Python风格和干净代码

#1

帖子 shaoziyang »

来自:https://realpython.com/python-namedtuple/

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

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

在本教程中,您将学习如何:
  • 使用namedtuple()创建namedtuple类
  • 识别并利用namedtuple的特性
  • 使用namedtuple实例编写Pythonic代码
  • 决定是使用namedtuple还是类似的数据结构
  • 将namedtuple子类化以提供新功能
为了充分利用本教程,您需要对Python编写Pythonic和可读代码的哲学有一个大致的了解。您还需要了解使用的基本知识:
  • 元组
  • 字典
  • 类与面向对象程序设计
  • 数据类
  • 键入提示

使用namedtuple编写Pythonic代码

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

Python的namedtuple是为了提高代码可读性而创建的,它提供了一种使用描述性字段名而不是整数索引来访问值的方法,而整数索引在大多数情况下不提供任何关于值的上下文。这个特性还使代码更干净,更易于维护。

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


注意:在本教程中,您将发现用于引用Python的namedtuple、工厂函数和实例的不同术语。

为了避免混淆,这里总结了本教程中每个术语的用法:
术语含义
namedtuple()工厂函数
namedtuple, classnamedtuplenamedtuple()返回的元组子类
namedtuple instance, named tuple特定classnamedtuple的实例


除了命名元组的这个主要特性之外,您还将发现:
  • 不变的数据结构
  • 具有一致的哈希值
  • 可以用作字典的key
  • 可成套存放
  • 根据类型和字段名生成有用的docstring
  • 提供有用的字符串表示形式,以name=value格式打印元组内容
  • 支持索引
  • 提供其他方法和属性,如_make(), _asdict(), ._fields 等
  • 与常规元组向后兼容
  • 具有与常规元组相似的内存消耗
通常,只要需要类似元组的对象,就可以使用namedtuple实例。命名元组的优点是,它们提供了一种使用字段名和点访问其值的方法。这将使你的代码更加python化。

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


使用namedtouple()创建元组式类

您可以使用namedtuple()创建一个不可变的、具有字段名的类元组的数据结构。在有关namedtouple的教程中,您会发现一个流行的例子是创建一个类来表示一个数学点。

根据问题,您可能希望使用不可变的数据结构来表示给定的点。下面演示如何创建正则二维元组point:

Code: Select all

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

在这里,使用常规方式创建一个不可变的二维元组。这段代码是有效的:你有两个坐标,但不能修改这些坐标中的任何一个。但是这段代码可读吗?你能预先知道索引0和1它代表什么意思吗?为了避免这些歧义,可以这样使用:

Code: Select all

>>> 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有了一个包含两个适当命名的字段x和y。默认情况下,提供用户友好的描述性字符串进行表示(point(x=2,y=4))。允许您使用点符号访问坐标,这是方便的、可读的和明确的。同时也可以使用索引来访问每个坐标的值。

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

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

Code: Select all

>>> 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'
可以创建包含可变对象的命名元组。您可以修改基础元组中的可变对象。但是,这并不意味着要修改元组本身。元组将继续保持相同的内存引用。

最后,元组或具有可变值的命名元组是不可散列的,如上面的示例所示。

因为namedtuple类是tuple的子类,所以它们也是不可变的。因此,如果您尝试更改坐标的值,那么您将得到AttributeError。
 
 

头像
shaoziyang
帖子: 3917
注册时间: 2019年 10月 21日 13:48

Re: 用namedtuple编写Python风格和干净代码

#2

帖子 shaoziyang »

探索namedtuple类的其他特性

除了从tuple继承的方法,例如.count() 和.index(),namedtuple类还提供了三个附加方法和两个属性。为了防止名称与自定义字段冲突,这些属性和方法的名称以下划线开头。


从易用性创建实例namedtuple

可以使用._make()创建命名 tuple 实例。该方法采用可更新的值,并返回一个新的命名的 Tuple:

Code: Select all

>>> from collections import namedtuple

>>> Person = namedtuple("Person", "name age height")
>>> Person._make(["Jane", 25, 1.75])
Person(name='Jane', age=25, height=1.75)
在这里,首先使用 namedtuple() 创建一个类。然后,用每个字段中的值调用 ._make()。请注意,这是一种类方法,可作为替代类构造器工作,并返回新的命名 Tuple 实例。

最后,._make() 需要 iterable (即上面的列表)作为参数。另一方面,构造器可以采取位置或关键字参数。


将namedtuple实例转换为字典

可以使用 ._asdict() 将现有的namedtuple实例转换为字典。此方法返回使用字段名称作为密钥的新字典。由此产生的字典的键与原文中的字段顺序相同。

Code: Select all

>>> 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}
当在namedtuple中调用 ._asdict()时,会获得一个新对象,该对象在原始namedtuple 中将字段名称映射到其相应的值。

自从Python3.8以来,._asdict()将返回字典。在此之前,它返回一个OrderedDict对象。

Code: Select all

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 update  ._asdict()  返回常规字典,因为在Python 3.6及更高版本中,字典会记住其键的插入顺序。请注意,结果字典中键的顺序与原始命名元组中字段的顺序相同。

头像
shaoziyang
帖子: 3917
注册时间: 2019年 10月 21日 13:48

Re: 用namedtuple编写Python风格和干净代码

#3

帖子 shaoziyang »

替换现有实例中的字段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)
namedtuple其他属性

还有两个额外的属性:._fields和._field_defaults。第一个属性具有一串列出字段名称的字符串。第二个属性持有一本字典,该词典将字段名称映射到各自的默认值(如果有)。

对于 ._fields,可以使用它来内省namedtouple类和实例。您还可以从现有类创建新类::

代码: 全选

>>> 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
在本例中,创建了一个名为ExtendedPerson的新namedtuple,其中包含一个新字段weight。这种新类型扩展了旧的Person。为此,您可以访问Person上的._fields字段,并将其与一个附加字段weight一起解压到一个新列表中。

还可以使用._fields字段来在给定实例中对字段和值进行重复:

代码: 全选

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

头像
shaoziyang
帖子: 3917
注册时间: 2019年 10月 21日 13:48

Re: 用namedtuple编写Python风格和干净代码

#4

帖子 shaoziyang »

使用namedtouple编写Python风格代码

可以说,命名元组的基本用法是帮助编写更多Python化代码。创建namedtoupe() 工厂函数,以便您编写可读、显式、干净和可维护的代码。

在本节中,通过编写一系列实用示例,帮助您找到使用命名元组而不是常规元组的好机会,以便您的代码变得更Python化。

使用字段名而不是索引

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

代码: 全选

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

>>> if pen[0] == 2 and pen[1] == "Solid" and pen[2]:
... print("Standard pen selected")
...
Standard pen selected
这行代码定义了一个具有三个值的元组。你能说出每个值的含义吗?也许你可以猜测第二个值与线条样式有关,但是2和True的含义是什么?

您可以添加一个很好的注释,为pen提供一些上下文,在这种情况下,您将得到如下结果:

代码: 全选

>>> # Tuple containing: line weight, line style, and beveled edges
>>> pen = (2, "Solid", True)
现在您知道了元组中每个值的含义。但是,如果您或其他程序员使用的pen与此定义相差甚远呢?他们必须回到定义上来,只是为了记住每个值的含义。

下面是一个使用namedtuple的pen的替代实现:

代码: 全选

>>> 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()的文档,因为数字本身并没有提供关于它们各自含义的太多信息。函数名也没有多大帮助。

下面是一个函数,它使用namedtuple来澄清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表示正在使用的数据库。其余的参数与给定的客户机密切相关。这是一个很好的机会,使用命名元组减少参数数量:

代码: 全选

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() 只接受两个参数:db和user。在函数中,可以使用方便的描述性字段名为db.add_user() 和 db.complete_user_profile()提供参数。高级函数create_user()更关注用户。测试也更容易,因为您只需要为每个测试提供两个参数。

头像
shaoziyang
帖子: 3917
注册时间: 2019年 10月 21日 13:48

Re: 用namedtuple编写Python风格和干净代码

#5

帖子 shaoziyang »

namedtuple 和 Dictionary 对比
字典是 Python 的基本数据结构。语言本身是围绕字典构建的,所以它们无处不在。由于它们非常常见和有用,因此您可能在代码中大量使用它们。但是字典和namedtuple有什么不同呢?
在可读性方面,您也许可以说字典与namedtuple一样可读。尽管它们不提供通过点符号访问属性的方法,但字典风格的密钥查找同样可读和简单:

代码: 全选

>>> 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 是不可改变的。这意味着您可以修改字典,但您不能修改namedtuple:

代码: 全选

>>> 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'
可以更新字典中现有密钥的价值,但您不能在 namedtuple 中执行类似的事情。您可以在现有字典中添加新的密钥值对,但无法将字段值对添加到现有namedtuple 中。

:在命名的 Tuples 中,您可以使用._replace()更新给定字段的值,但该方法创建并返回一个新的命名 Tuple 实例,而不是更新基础实例。

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

关于内存使用,namedtuple 是一个相当轻量级的数据结构。
 
 

回复

  • 随机主题
    回复总数
    阅读次数
    最新文章