掌握Python编程:下划线在代码中的十大实用技巧

发表时间: 2023-09-03 16:15

介绍

在本篇 Pydon 中,我们将了解_Python 中的所有用例。有几个地方在_语法上具有非常特殊的作用,我们将讨论这些地方。我们还将讨论它的用途,_这只是人们遵循的约定,并且允许人们编写更惯用的代码。

在这个 Pydon 中,你将:

  • 了解_Python REPL 中的实用程序;
  • 了解
  • _
  • 用作变量名的前缀和/或后缀时会发生什么:
    • 用作后缀的单个下划线;
    • 用作前缀的单个下划线;
    • 双下划线用作前缀;
    • 双下划线用作前缀和后缀;
  • _请参阅作业中“水槽”的惯用用法;
  • 并了解如何将其扩展到_新声明中的角色match
  • _请参阅本地化字符串中的惯用用法;和
  • 了解如何使用_使您的数字更具可读性。

恢复会话中的最后结果

您是否曾经在 Python 会话中调用过慢速函数,然后由于忘记将其分配给变量而丢失了返回值?我知道我已经做过无数次了!因为像(你和)我这样的人,有人做出了有史以来最好的决定,并决定_可以在 Python 会话中使用它来引用最后的返回结果:

>>> 1 + 12>>> _2>>> sum(range(100_000_000))     # Takes a couple of seconds to finish.4999999950000000>>> _4999999950000000>>> save_for_later = _>>> save_for_later4999999950000000

这可以防止您必须重新运行前一行代码,如果前一行代码需要一些时间才能完成,如果它具有您不想再次触发的副作用,或者即使它无法重新运行(例如,因为您删除了文件或因为您耗尽了可迭代对象)。

因此,下次当您在解释器会话中玩耍并且忘记分配函数调用的结果或其他代码段时,请记住使用来引用_它。

请注意,如果您显式分配给_,则您分配的值将保留在那里,直到您显式删除它为止。删除后,_会返回引用最后返回的结果:

>>> _ = "hey">>> "_ was explicitly assigned."'_ was explicitly assigned.'>>> _'hey'>>> del _>>> "_ is no longer explicitly assigned."'_ is no longer explicitly assigned.'>>> _'_ is no longer explicitly assigned.'

变量名的前缀和后缀

单下划线作为后缀

如您所知,有些单词在 Python 中具有特殊含义,因此被称为关键字。这意味着我们不能将这些名称用于我们的变量。类似地,Python 定义了一系列通常非常有用的内置函数,理想情况下我们希望避免使用与这些内置名称匹配的变量名称。

然而,有时完美的变量名要么是这些关键字之一,要么是这些内置函数之一。在这些情况下,通常使用单个_作为后缀来防止冲突。

例如,在统计学中,有一种称为“指数分布”的随机分布,它取决于数值参数,而该参数在数学文献中通常称为“lambda”。因此,当random决定在 中实现该分布时random.expovariate,他们理想地希望使用该单词 lambda作为 的参数random.expovariate,但lambda它是一个保留关键字,这会引发错误:

>>> def expovariate(lambda):  File "<stdin>", line 1    def expovariate(lambda):                    ^SyntaxError: invalid syntax

相反,他们可以将参数命名为lambda_。(然而,实施者最终选择了lambd。)

Python 标准库中有许多示例,实现者选择了尾部下划线。例如,在 IDLE(Python 默认附带的 IDE,并且完全用 Python 实现)的代码中,您可以找到以下函数:

# From Lib/idlelib/help.py in Python 3.9.2def handle_starttag(self, tag, attrs):    "Handle starttags in help.html."    class_ = ''    for a, v in attrs:        if a == 'class':            class_ = v    # Truncated for brevity...

请注意class_循环内定义和更新的变量。“class”在这里是明显的变量名称,因为我们正在处理 HTML 类,但它class是一个保留关键字,我们用来定义类……这就是我们class_在这里使用的原因!

单下划线作为前缀

虽然使用单个下划线作为后缀或多或少是一种约定,但使用单个下划线作为前缀既是一种约定,也会影响某些 Python 程序。

首先解释一下约定:当您定义一个以单下划线开头的名称时,您是在让其他程序员知道这样的名称指的是仅供内部使用的名称,外部用户不应该乱用。

例如,假设您正在实现一个在线商店框架,并且您现在正在编写用于获取商品价格的代码部分。你可以写一个像这样的小函数:

prices = {    "jeans": 20,    "tshirt": 10,    "dress": 30,}def get_price(item):    return prices.get(item, None)

现在,现在的商店如果不时不时地进行销售就无法开展业务,因此您可以在函数中添加一个参数来应用折扣:

def get_price(item, discount=0):    p = prices.get(item, None)    if p is not None:        return (1 - discount)*p    else:        return p

现在一切都很好,除了您认为验证函数尝试应用的折扣可能是个好主意,以便折扣永远不会为负数或大于100 %100%。您可以在主函数中执行此操作,或者您可以设计一个辅助函数来为您执行此操作,可能是因为您需要验证各个地方的折扣金额是否正确。

因此,您编写辅助函数:

def valid_discount(discount):    return 0 <= discount <= 1

顺便说一句,如果您想了解更多关于 Python 允许比较链的事实,就像您在上面看到的那样,您可以阅读 有关该主题的Pydon 。

现在您有一种方法来验证折扣,您可以使用它:

def get_price(item, discount=0):    if not valid_discount(discount):        raise ValueError(f"Trying to apply an illegal discount on {item}.")    p = prices.get(item, None)    if p is not None:        return (1 - discount)*p    else:        return p

完美的!您的在线商店管理框架的代码库正在顺利进行中。

现在想象一下,您是框架的用户,而不是实现者。您可能会从 PyPI 安装框架,pip或者直接从 GitHub 安装。但是,当您这样做时,当您导入代码以开始使用它时,您将导入get_pricevalid_discount函数。现在,您需要该get_price功能,但您不需要, valid_discount因为整个框架已经保护用户免受非法折扣和负价格之类的影响!换句话说,该valid_discount功能与框架内部的相关性比与框架用户的相关性更大。但用户可能不知道这一点,因为用户看到的是 valid_discount函数,并且可以公平地假设用户会认为他们必须使用该函数来为自己验证折扣...他们怎么知道他们不需要这样做?

一种解决方案是您遵循我们刚刚开始讨论的约定!如果您的函数命名稍有不同:

def _valid_discount(discount):    return 0 <= discount <= 1

框架的用户立即理解“哦,我不必担心这个函数,因为它的名称以单个下划线开头”。不仅如此,Python甚至可以帮助用户不用担心那些带有下划线前导的函数。

继续在您的onlineshop.py文件中写入以下内容:

# onlineshop.pydef _valid_discount(discount):    return 0 <= discount <= 1prices = {    "jeans": 20,    "tshirt": 10,    "dress": 30,}def get_price(item, discount=0):    if not _valid_discount(discount):        raise ValueError(f"Trying to apply an illegal discount on {item}.")    p = prices.get(item, None)    if p is not None:        return (1 - discount)*p    else:        return p

完成此操作后,打开 Python REPL,导入所有内容onlineshop并尝试获取一些价格和折扣:

>>> from onlineshop import *>>> get_price("jeans")20>>> get_price("jeans", discount=0.5)10.0>>> get_price("jeans", discount=1.3)Traceback (most recent call last):  File "<stdin>", line 1, in <module>  File "C:\Users\rodri\Documents\mathspp\onlineshop.py", line 13, in get_price    raise ValueError(f"Trying to apply an illegal discount on {item}.")ValueError: Trying to apply an illegal discount on jeans.

请注意,这两个函数似乎都工作得很好,并注意我们在最后一次调用时收到错误,因为 1.3 的折扣太大,因此该_valid_discount函数表示它无效。

让我们自己检查一下:

>>> _valid_discount(1.3)Traceback (most recent call last):  File "<stdin>", line 1, in <module>NameError: name '_valid_discount' is not defined

我们得到 aNameError因为该_valid_discount函数未定义...因为它从未导入!该函数未导入到您的代码中,尽管原始代码仍然可以在内部使用它。如果您确实需要访问_valid_discount,那么您要么显式导入它,要么只导入模块名称,然后使用其点名称访问它:

>>> from onlineshop import _valid_discount>>> _valid_discount(0.5)True>>> import onlineshop>>> onlineshop._valid_discount(1.3)False

此机制也适用于变量,只要它们的名称以下划线开头即可。继续将prices变量重命名为_prices,关闭 REPL,再次打开它,然后运行from onlineshop import *_prices不会被定义!

因此,一方面,请注意,前导下划线实际上表明了使用其他人编写的代码时您应该或不应该关心哪些事情。另一方面,前导下划线只是一个指示,它不会阻止其他人访问您用前导下划线编写的名称。

最后,当有人使用 导入模块中的所有内容时,还有另一种方法可以控制导入的内容*:您可以使用__all__变量来指定此时应导入的名称。

继续将以下行添加到文件顶部onlineshop.py

__all__ = ("get_price", "_valid_discount")

完成此操作后,关闭 REPL 并重新打开它:

>>> from onlineshop import *>>> get_price<function get_price at 0x0000029410907430>>>> _valid_discount<function _valid_discount at 0x0000029410907280>>>> pricesTraceback (most recent call last):  File "<stdin>", line 1, in <module>NameError: name 'prices' is not defined

请注意,其中的所有名称__all__都已导入,无论它们是否以下划线开头,并且列出的名称不会被包含在内。在我的示例中,我的变量已命名prices (因此它甚至没有前导下划线!)并且未导入它。

这个__all__变量是进入下一小节的完美过渡:

前导和尾随双下划线

在 Python 中,以双下划线开头和结尾的名称是与 Python 具有内部相关性的名称。例如,许多函数(如__str____repr__、 和)有时被称为“神奇”函数,因为它们以某种方式与 Python 的“内部”函数交互。 __bool__ __init__

这些神奇函数和变量的更好名称是“dunder 函数”、“dunder 变量”或“dunder 方法”,具体取决于上下文。(“dunder”这个词——Python 世界中的一个常见词——是“双下划线”的缩写!)

然而,这些 dunder 名称并不真正神奇:它们只是函数。(或者变量,就像__all__。)您可以知道的是,当您找到一个以双下划线开头和结尾的名称时,很可能它是一个以某种方式与 Python 语法交互的名称。

例如,使用某个参数调用内置函数与调用具有相同参数的函数str完全相同:__str__

>>> n = 3>>> str(n)'3'>>> n.__str__()'3'

当然,写作str(n)看起来比 好得多n.__str__(),但这只是告诉您,如果您定义自己的对象,则需要实现该__str__方法,以便您的对象可以作为str内置函数的参数给出。(我在这里str写了、__str__repr、 以及__repr__更详细的内容,所以如果您需要的话,请不要阅读 Pydon 。)

因此,总而言之,双前导和尾随下划线用于具有某些“特殊”含义的函数和变量,这些含义通常与默认的 Python 行为有关。

不要在您自己的程序中使用(创建)dunder 名称,这样您就不会遇到意外的情况,并避免与 Python 语言的未来更改/添加发生冲突!

双前导下划线

在本小节中,我们将了解在名称开头使用双下划线时会发生什么。名称开头的双下划线有一个特殊的用例:您将它用于您希望用前导下划线“保护”的变量和方法(以便用户知道不要管它),但有这样的您担心其他人可能会覆盖它们的通用名称。

这是什么意思?

首先,让我们看看它的实际效果。修改该onlineshop.py文件,使我们的代码现在属于一个名为的类OnlineShop

# onlineshop.pyclass OnlineShop:    __prices = {        "jeans": 20,        "tshirt": 10,        "dress": 30,    }    def _valid_discount(self, discount):        return 0 <= discount <= 1    def get_price(self, item, discount=0):        if not self._valid_discount(discount):            raise ValueError(f"Trying to apply an illegal discount on {item}.")        p = self.__prices.get(item, None)        if p is not None:            return (1 - discount)*p        else:            return p

请注意,prices现在的变量是__prices。让我们来体验一下这个小课程:

>>> from onlineshop import OnlineShop as OS>>> shop = OS()>>> shop.get_price("jeans")20

该代码似乎可以正常工作,所以现在让我们看一下该__prices变量:

>>> shop.__pricesTraceback (most recent call last):  File "<stdin>", line 1, in <module>AttributeError: 'OnlineShop' object has no attribute '__prices'

呃哦,又出错了!我们无法访问该__prices变量,即使该get_price方法显然(成功!)使用了它。为什么我们无法到达__prices变量?好吧,我们可以使用内置函数来列出对象的所有属性: dir() shop

>>> dir(shop)['_OnlineShop__prices', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__','__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__','__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__','__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__','__weakref__', '_valid_discount', 'get_price']

继续查找我们定义的事物的名称。你能找到_valid_discountget_price函数吗?又怎样呢__prices?您将无法__prices在该列表中找到,但列表的第一项是_OnlineShop__prices,它看起来非常相关。

还记得我说过使用双前导下划线来避免名称冲突吗?prices好吧,如果人们扩展您的在线商店框架,那么他们很有可能想要创建一个名为 name 的变量,并且您可能仍然需要原始prices变量,因此您有两个选择:

  • 给你的变量起一个巨大的、非常复杂的名字prices,这样其他人就不太可能创建同名的变量;或者
  • 你用来__prices要求 Python修改变量名,以避免将来的冲突。

使用第二个选项意味着Python采用原始变量名称,即__prices,并在其前面加上类名,再加上一个额外的前导下划线,以便用户仍然知道他们应该保留该名称。这是您可以用来从类外部访问该变量的显式名称:

>>> shop._OnlineShop__prices{'jeans': 20, 'tshirt': 10, 'dress': 30}

这个名称修饰工具适用于变量和函数,因此您可以拥有一个__valid_discount看起来像
_OnlineShop__valid_discount
从类外部看的方法。

您很可能不需要在代码中使用双前导下划线,但我不能忽略这个用例!

下划线作为水槽

我最喜欢的下划线用例之一是当我们使用下划线作为作业的目标时。_我说的是我们在赋值中用作变量名的时候。

这是一个广泛传播的约定,用作_变量名意味着“我不关心这个值”。话虽如此,您应该问自己:如果我不关心某个值,为什么我首先要分配它?很好的问题!

做类似的事情

_ = 3       # I don't care about this 3.

很傻。使用下划线作为接收器(即,作为将保存我不关心的值的变量的名称)在**其他情况下很有用。

开箱

已经详细写了有关在其他 Pydon 中解包的内容:

  • “用加星标的作业拆包”
  • “深度拆包”

解包是一项功能,可以让您一次将多个值解包为多个名称。例如,以下是将列表拆分为第一项和最后一项以及中间部分的方法:

>>> first, *mid, last = range(0, 10)>>> first0   >>> mid[1, 2, 3, 4, 5, 6, 7, 8]>>> last9

这不是很整洁吗?嗯,确实如此!但是如果您只关心第一项和最后一项怎么办?当然,有多种选择,但我认为最优雅的一种用作_中间部分的水槽:

>>> first, *_, last = range(0, 10)>>> first0>>> last9

为什么这比下面的替代方案更好?

>>> sequence = range(0, 10)>>> first, last = sequence[0], sequence[-1]

显然,sequence = range(0, 10)这只是序列的一个例子。如果我事先知道这是我要使用的序列,那么我会直接分配first = 0和。last = 9但对于通用序列,这两个用例的行为不同。

你能算出什么时候吗?我在Pydon中讨论了这一点。

sequence当只有一个元素时,行为会有所不同。因为它们的行为不同,所以在某些情况下您可能 必须使用两种替代方案之一,但是当您有选择时,解包看起来更优雅,并且传达了更好地将序列拆分为各个部分的意图。

当然_是一个有效的变量名,你可以询问它的值:

>>> first, *_, last = range(0, 10)>>> _[1, 2, 3, 4, 5, 6, 7, 8]

*_但是当我在作业中看到 the 时,我立即将该作业的语义理解为“忽略范围的中间部分”。

当您解压某些结构并且只关心结构的特定部分时,也可以使用此方法。您可以使用索引来访问您想要的特定信息:

>>> colour_info = ("lightyellow", (255, 255, 224))>>> blue_channel = colour_info[1][2]>>> blue_channel224

但如果colour_info变量格式错误,您将很难弄清楚。相反,使用解包,您可以断言结构是正确的,同时仅访问重要的值:

>>> colour_info = ("lightyellow", (255, 255, 224))>>> _, (_, _, blue_channel) = colour_info>>> blue_channel224

独立于迭代次数进行迭代

当您需要使用循环进行迭代时,会出现另一个类似的用例for,但您实际上并不关心所处的迭代次数。例如,假设您想要生成 5 个 0 到 20 之间的随机整数。您会如何编写那?我会这样写:

>>> import random>>> nums = [random.randint(0, 20) for _ in range(5)][16, 1, 17, 3, 1]

为什么我用_在前面for?因为我重复运行的表达式不依赖于迭代计数,所以它与该计数无关。因此,为了更清楚地传达该含义,我使用_作为迭代器变量的接收器。

同样,_是一个完全有效的变量名,我可以在表达式本身中使用它:

>>> [_ + 2 for _ in range(5)][2, 3, 4, 5, 6]

但重点是,使用_作为接收器是一种约定 ,可以使程序的语义更加清晰。

匹配新匹配语句中的所有内容

新的match声明将在 Python 3.10 中发布,有很多值得期待的地方。遵循在赋值中用作接收器的常见用例的精神_ ,下划线也将在新match语句中用作匹配“任何其他”的通配符:

# Needs Python 3.10 to run>>> v = 10>>> match v:...     case 0:...             print("null")...     case 1:...             print("uno")...     case 2:...             print("two")...     case _:...             print("whatever")... whatever

在语句的情况下match,它是一个真正的接收器:你不能使用 the_来引用原始值,所以在match语句中,_真正的意思是“我不在乎”!看一看:

>>> v = 10>>> match v:...     case _:...             print(_)... Traceback (most recent call last):  File "<stdin>", line 3, in <module>NameError: name '_' is not defined

如果您想匹配其他任何内容能够引用原始值,那么您需要使用有效的目标名称:

>>> v = 10>>> match v:...     case wtv:...             print(wtv)... 10

字符串本地化

下划线的另一个小众用例,但我发现它非常可爱,是当您需要本地化您的程序时。程序本地化意味着使其适合不同地区/国家。当您这样做时,您必须做的事情之一就是翻译程序中的字符串,以便可以用多种不同的语言读取它们。

您将如何实现一种机制,使您的程序能够以(任意多种)不同的语言输出?请想一想,这是一个很好的挑战!假设您不能使用专门为本地化构建的模块。

无论你做什么,例如函数调用或访问字典,都会在不同的地方发生,并且会产生太多的噪音。如果你的程序有很多字符串,从

print("Hello, world!")

print(translate("Hello, world!"))

可能看起来有害,但在具有许多字符串的程序中,所有translate调用都会增加很多视觉混乱。因此,通常的做法是为函数创建别名(例如函数)translate 并调用它_。然后,本地化字符串不会增加太多视觉混乱:

print(_("Hello, World!"))

这只是一个约定,但它非常常见,甚至在文档中提到gettext,这是专门设计用于帮助您的程序处理多种(自然)语言的模块的文档。

当我第一次发现这个用法时_我很困惑。我在查看该argparse模块的源代码时发现了它。因为argparse处理命令行界面,所以它的内部工作是本地化的,因此它的命令行消息与命令行本身的语言相匹配。我还记得第一次看到它的情景;我正在看这两行:

if prefix is None:    prefix = _('usage: ')

我对_('usage: ')作业的部分感到非常困惑,但最终我在该文件中找到了导入语句:

from gettext import gettext as _, ngettext

我意识到他们正在设置_为 的别名gettext

提高数字可读性

我们将讨论的下划线的最后一个用例与提高数字的可读性有关。

快的。

下面多少钱n

>>> n = 99999999

如果你认为/说“9900万、999千和999”,那么你就对了。

现在,现在多少钱n

>>> n = 100_000_000

我们谈论的是10000亿,这还有疑问吗?用作_千位分隔符在这里确实很重要,您不需要更多的说服力!但我只会向您展示 Python 标准库中的一个小示例。看看下面的两个条件,让我知道哪一个更容易阅读。

不带分隔符:

if not 1000 <= rounds <= 999999999:    raise ValueError('rounds out of the range 1000 to 999999999')

带分隔符:

if not 1000 <= rounds <= 999_999_999:    raise ValueError('rounds out of the range 1000 to 999_999_999')

如果你告诉我你更喜欢第一个,那就走吧。我不想再让你在这儿了!

下划线不一定是千位分隔符,您可以在任何您想要的数字之间使用它。但最重要的是,它可以与任何其他基地一起使用。

例如,用于_对二进制数字中的位进行分组:

>>> thirty_five = 0b0010_0011>>> forty_seven = 0b0010_1111

或者也许可以分离颜色的十六进制值的 R、G 和 B 通道:

>>> lightyellow = 0xff_ff_e0>>> peachpuff   = 0xff_da_b9    # I didn't invent this name!

结论

这是 Pydont 的主要内容,为您提供:

编码约定的存在是为了让我们的生活更轻松,因此值得学习它们以使我们的代码更具表现力和地道性。

这个 Pydon 没有向你展示:

  • _您可以使用;恢复 Python REPL 中表达式的最后一个值
  • _
  • 用作前缀/后缀时对名称有很大影响:
    • name_``name是保留关键字的常见选择;
    • _name
    • 是一种
    • 约定
    • ,表明这
    • name
    • 是一个内部名称,用户可能不应该弄乱它;
      • _name如果有人使用通配符导入,则不会被导入from mymodule import * ;和
      • _name如果将其添加到__all__中的列表中,则可以覆盖它mymodule
    • dunder 名称(以双下划线开头和结尾)引用 Python 的内部结构,并允许您与 Python 的语法进行交互;
    • __name当您想要使用一个名称为您担心用户可能会错误覆盖的内部变量时,在类内部使用以防止名称冲突;
  • _
  • 以惯用方式用作作业中的接收器,尤其是
    • 当解压多个值时,只有其中一些值感兴趣;
    • 当我们在for循环中迭代时,我们不关心迭代次数;
  • match语句用作_“匹配所有”情况,并使其成为真正的接收器,因为_无法用于访问原始值;
  • _由于视觉影响较小,经常用作本地化函数的别名;
  • 不同基数(十进制、二进制……)的数字可以用下划线分隔,以提高可读性。例如, 99999999999_999_999进行比较999999999

参考文献:

  • Python 3 文档,Python 标准库,gettexthttps ://docs.python.org/3/library/gettext.html
  • Bader,Dan,“Python 中下划线的含义”,https://dbader.org/blog/meaning-of-underscores-in-python

感兴趣的小伙伴可以收藏起来,再写代码的时候遇到了可以字典来检测,毕竟咱们的脑袋比不上计算机也不太可能全部记住,记住一些常用的就可能了,到了问题再搜索一下寻找答案。呵呵!!