Python 进阶指南(编程轻松进阶):九、深奥的 Python 怪现象

简介: Python 进阶指南(编程轻松进阶):九、深奥的 Python 怪现象


定义编程语言的规则系统是复杂的,并且可能导致代码,尽管没有错,但是非常奇怪和不可预料。这一章深入探讨了更难理解的 Python 语言的奇特之处。您不太可能在现实世界的编码中遇到这些情况,但是它们是 Python 语法的有趣用法(或者是滥用,取决于您的观点)。

通过学习本章中的例子,您将对 Python 如何工作有一个更好的了解。让我们找点乐子,探索一些深奥的问题。

为什么 256 是 256 而 257 不是 257

==操作符比较两个对象是否相等,而is操作符比较它们是否相等。尽管整数值42和浮点值42.0具有相同的值,但它们是保存在计算机内存中不同位置的两个不同的对象。您可以通过使用id()函数检查他们不同的 id 来确认这一点:

>>> a = 42
>>> b = 42.0
>>> a == b
True
>>> a is b
False
>>> id(a), id(b)
(140718571382896, 2526629638888)

当 Python 创建一个新的整数对象并将其存储在内存中时,该对象的创建只需要很少的时间。作为一个微小的优化,CPython(Python 解释器可从python.org下载)在每个程序开始时为-5256创建整数对象。这些整数被称为预分配整数,CPython 自动为它们创建对象,因为它们相当常见:程序更可能使用整数02,而不是1729。当在内存中创建一个新的整数对象时,CPython 首先检查它是否在-5256之间。如果是这样,CPython 通过简单地返回现有的 integer 对象而不是创建一个新的来节省时间。这种行为也通过不存储重复的小整数来节省内存,如图 9-1 所示。

图 9-1:Python 通过对单个整数对象(左)使用多个引用来节省内存,而不是对每个引用使用单独的、重复的整数对象(右)。

由于这种优化,某些人为的情况会产生奇怪的结果。要查看示例,请在交互式 Shell 中输入以下内容:

>>> a = 256
>>> b = 256
>>> a is b # 1
True
>>> c = 257
>>> d = 257
>>> c is d # 2
False

所有 256 个对象实际上都是同一个对象,所以abis运算符返回True 1 。但是 Python 为cd分别创建了 257 个对象,这就是为什么is操作符返回False 2 。

表达式257 is 257的计算结果为True,但是 CPython 在同一个语句中重用为相同字面值创建的整数对象:

>>> 257 is 257
True

当然,现实世界的程序通常只使用一个整数的值,而不是它的单位。他们永远不会使用is操作符来比较整数、浮点数、字符串、布尔值或其他简单数据类型的值。一个例外是当你使用is None而不是== None时,正如第 96 页“使用is None而不是==进行比较”中所解释的。否则,你很少会碰到这个问题。

字符串内化

类似地,Python 重用对象在代码中表示相同的字符串字面值,而不是制作相同字符串的单独副本。要在实践中看到这一点,请在交互式 Shell 中输入以下内容:

>>> spam = 'cat'
>>> eggs = 'cat'
>>> spam is eggs
True
>>> id(spam), id(eggs)
(1285806577904, 1285806577904)

Python 注意到分配给eggs'cat'字符串和分配给spam'cat'字符串相同;因此,它没有创建第二个冗余的字符串对象,而是给eggs分配了一个引用,指向spam使用的同一个字符串对象。这解释了为什么它们的字符串的 id 是相同的。

这种优化被称为字符串预留,和预分配整数一样,它只不过是 CPython 实现的一个细节。你不应该写依赖它的代码。此外,这种优化不会捕获所有可能的相同字符串。试图识别可以使用优化的每个实例通常会花费比优化节省的时间更多的时间。例如,尝试在交互 Shell 中从'c''at'创建'cat'字符串;您会注意到 CPython 创建最终的'cat'字符串作为新的字符串对象,而不是重用为spam创建的字符串对象:

>>> bacon = 'c'
>>> bacon += 'at'
>>> spam is bacon
False
>>> id(spam), id(bacon)
(1285806577904, 1285808207384)

字符串内化是解释器和编译器用于许多不同语言的一种优化技术。你可以在en.wikipedia.org/wiki/String_interning找到更多的细节。

Python 的伪递增和递减操作符

在 Python 中,您可以使用增加的赋值操作符将变量的值增加1或减少1。代码spam += 1spam -= 1分别将spam中的数值增加和减少1

其他语言,比如 C++和 JavaScript,有用于递增和递减的++--操作符。(“C++”这个名字本身就体现了这一点;这是一个半开玩笑的玩笑,表明它是 C 语言的增强形式。)C++和 JavaScript 中的代码可以有类似于++spamspam++的操作。Python 明智地没有包括这些操作符,因为它们容易受到细微错误的影响(正如在softwareengineering.stackexchange.com/q/59880帖子上所讨论的)。

但是拥有以下 Python 代码是完全合法的:

>>> spam = --spam
>>> spam
42

您应该注意的第一个细节是,Python 中的++--“操作符”实际上并不递增或递减spam中的值。相反,主要的-是 Python 的一元否定操作符。它允许您编写这样的代码:

>>> spam = 42
>>> -spam
-42

在一个值前面有多个一元负运算符是合法的。使用它们中的两个会得到值的负值,对于整数值,它只计算原始值:

>>> spam = 42
>>> -(-spam)
42

这是一个非常愚蠢的操作,您可能永远不会看到一元求反操作符在真实世界的代码中使用两次。(但如果你这样做了,那很可能是因为程序员学会了用另一种语言编程,并且刚刚编写了错误的 Python 代码!)

还有一个+一元运算符。它将整数值计算为与原始值相同的符号,也就是说,它完全不做任何事情:

>>> spam = 42
>>> +spam
42
>>> spam = -42
>>> +spam
-42

+42(或者++42)看起来和--42一样傻,那为什么 Python 还要有这个一元运算符呢?如果您需要为自己的类重载这些操作符,它的存在只是为了补充-操作符。(这是很多你可能不熟悉的术语!你会在第 17 章的里学到更多关于操作符重载的知识。)

+-一元运算符只在 Python 值的前面有效,在它后面无效。尽管spam++spam--可能是 C++或 JavaScript 中的合法代码,但它们会在 Python 中产生语法错误:

>>> spam++
  File "<stdin>", line 1
    spam++
         ^
SyntaxError: invalid syntax

Python 没有递增和递减运算符。语言语法的一个怪癖只是让它看起来是这样。

全部或者没有

all()内置函数接受一个序列值,比如一个列表,如果该序列中的所有值都是“真”,则返回True如果一个或多个值为“假”,它将返回False你可以认为函数调用all([False, True, True])等同于表达式False and True and True

您可以将all()与列表推导、结合使用,首先基于另一个列表创建一个布尔值列表,然后求值它们的集合值。例如,在交互式 Shell 中输入以下内容:

>>> spam = [67, 39, 20, 55, 13, 45, 44]
>>> [i > 42 for i in spam]
[True, False, False, True, False, True, True]
>>> all([i > 42 for i in spam])
False
>>> eggs = [43, 44, 45, 46]
>>> all([i > 42 for i in eggs])
True

如果spameggs中的所有数字都大于 42,则all()实用工具返回True

但是如果你传递一个空序列给all(),它总是返回True。在交互式 Shell 中输入以下内容:

>>> all([])
True

最好将all([])理解为求值“列表中的所有项目都是真值”而不是“列表中的所有项目都是True”否则,您可能会得到一些奇怪的结果。例如,在交互式 Shell 中输入以下内容:

>>> spam = []
>>> all([i > 42 for i in spam])
True
>>> all([i < 42 for i in spam])
True
>>> all([i == 42 for i in spam])
True

这段代码似乎表明,不仅spam(一个空列表)中的所有值都大于42,而且它们也小于42,正好等于42!这在逻辑上似乎是不可能的。但是请记住,这三个列表推导式中的每一个都计算为空列表,这就是为什么它们中的项目都不为假,并且all()函数返回True

布尔值是整数值

就像 Python 认为浮点值42.0等于整数值42一样,它认为布尔值TrueFalse分别等价于10。在 Python 中,bool数据类型是int数据类型的子类。(我们将在第 16 章中讨论类和子类。)您可以使用int()将布尔值转换为整数:

>>> int(False) 
0
>>> int(True) 
1
>>> True == 1 
True
>>> False == 0
True

您也可以使用isinstance()来确认一个布尔值被认为是一种整数:

>>> isinstance(True, bool) 
True
>>> isinstance(True, int) 
True

True属于bool数据类型。但是因为boolint的子类,True也是int。这意味着你可以在任何可以使用整数的地方使用TrueFalse。这可能会导致一些奇怪的代码:

>>> True + False + True + True  # Same as 1 + 0 + 1 + 1
3
>>> -True            # Same as -1.
-1
>>> 42 * True        # Same as 42 * 1 mathematical multiplication.
42
>>> 'hello' * False  # Same as 'hello' * 0 string replication.
' '
>>> 'hello'[False]   # Same as 'hello'[0]
'h'
>>> 'hello'[True]    # Same as 'hello'[1]
'e'
>>> 'hello'[-True]   # Same as 'hello'[-1]
'o'

当然,你可以使用bool值作为数字并不意味着你应该这样做。前面的例子都是不可读的,不应该在现实世界的代码中使用。本来 Python 没有bool数据类型。直到 Python2.3 才添加了布尔值,此时它将bool变成了int的子类以简化实现。你可以在www.python.org/dev/peps/pep-0285读取 PEP 285 中bool数据类型的历史。

顺便说一下,TrueFalse在 Python3 中只是关键字。这意味着在 Python2 中,有可能使用TrueFalse作为变量名,导致看似矛盾的代码如下:

Python2.7.14 (v2.7.14:84471935ed, Sep 16 2017, 20:25:58) [MSC v.1500 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> True is False
False
>>> True = False
>>> True is False 
True

幸运的是,这种令人困惑的代码在 Python3 中是不可能的,如果您试图使用关键字TrueFalse作为变量名,这将引发语法错误。

链接多种运算符

在同一个表达式中链接不同种类的运算符可能会产生意想不到的错误。例如,这个例子在一个表达式中使用了==in操作符:

>>> False == False in [False]
True

这个True结果令人惊讶,因为你可能会认为它是:

  • (False == False) in [False],也就是False
  • False == (False in [False]),也就是False

但是False == False in [False]并不等同于这两个表达式。确切地说,它相当于(False == False) and (False in [False]),只是作为42 < spam < 99相当于(42 < spam) and (spam < 99)。该表达式根据下图进行计算:

False == False in [False]表达式是一个有趣的 Python 谜语,但它不太可能出现在任何真实世界的代码中。

Python 的反重力特性

要启用 Python 的反重力特性,请在交互式 Shell 中输入以下内容:

>>> import antigravity

这条线是一个有趣的复活节彩蛋,它打开了网页浏览器,进入了一个经典的 XKCD 漫画,讲述了在xkcd.com/353的 Python 故事。Python 可以打开您的 web 浏览器,这可能会让您感到惊讶,但这是webbrowser模块提供的内置特性。Python 的webbrowser模块有一个open()函数,可以找到你的操作系统的默认网络浏览器,并打开一个特定 URL 的浏览器窗口。在交互式 Shell 中输入以下内容:

>>> import webbrowser
>>> webbrowser.open('https://xkcd.com/353/')

webbrowser模块是有限的,但是它可以帮助用户在互联网上找到更多的信息。

总结

人们很容易忘记,计算机和编程语言是由人类设计的,它们有自己的局限性。如此多的软件建立在语言设计师和硬件工程师的创造之上,并依赖于他们的创造。他们非常努力地工作,以确保如果你的程序有问题,那是因为你的程序有问题,而不是运行它的解释软件或 CPU 硬件有问题。我们最终会认为这些工具是理所当然的。

但这就是为什么学习计算机和软件的奇怪角落和缝隙是有价值的。当您的代码出现错误或崩溃时(或者甚至只是行为怪异,让您觉得“这很奇怪”),您需要理解调试这些问题的常见陷阱。

你几乎肯定不会碰到本章提到的任何问题,但是意识到这些小细节会让你成为一个有经验的 Python 程序员。

相关文章
|
17天前
|
安全 Java 数据处理
Python网络编程基础(Socket编程)多线程/多进程服务器编程
【4月更文挑战第11天】在网络编程中,随着客户端数量的增加,服务器的处理能力成为了一个重要的考量因素。为了处理多个客户端的并发请求,我们通常需要采用多线程或多进程的方式。在本章中,我们将探讨多线程/多进程服务器编程的概念,并通过一个多线程服务器的示例来演示其实现。
|
17天前
|
程序员 开发者 Python
Python网络编程基础(Socket编程) 错误处理和异常处理的最佳实践
【4月更文挑战第11天】在网络编程中,错误处理和异常管理不仅是为了程序的健壮性,也是为了提供清晰的用户反馈以及优雅的故障恢复。在前面的章节中,我们讨论了如何使用`try-except`语句来处理网络错误。现在,我们将深入探讨错误处理和异常处理的最佳实践。
|
20天前
|
缓存 监控 Python
解密Python中的装饰器:优雅而强大的编程利器
Python中的装饰器是一种强大而又优雅的编程工具,它能够在不改变原有代码结构的情况下,为函数或类添加新的功能和行为。本文将深入解析Python装饰器的原理、用法和实际应用,帮助读者更好地理解和利用这一技术,提升代码的可维护性和可扩展性。
|
1天前
|
机器学习/深度学习 数据挖掘 API
pymc,一个灵活的的 Python 概率编程库!
pymc,一个灵活的的 Python 概率编程库!
4 1
|
2天前
|
人工智能 算法 调度
uvloop,一个强大的 Python 异步IO编程库!
uvloop,一个强大的 Python 异步IO编程库!
10 2
|
2天前
|
机器学习/深度学习 人工智能 数据可视化
Python:探索编程之美
Python:探索编程之美
9 0
|
2天前
|
机器学习/深度学习 人工智能 数据处理
Python编程的魅力与实践
Python编程的魅力与实践
|
3天前
|
SQL 关系型数据库 MySQL
第十三章 Python数据库编程
第十三章 Python数据库编程
|
4天前
|
存储 网络协议 关系型数据库
Python从入门到精通:2.3.2数据库操作与网络编程——学习socket编程,实现简单的TCP/UDP通信
Python从入门到精通:2.3.2数据库操作与网络编程——学习socket编程,实现简单的TCP/UDP通信
|
9天前
|
安全 数据处理 开发者
《Python 简易速速上手小册》第7章:高级 Python 编程(2024 最新版)
《Python 简易速速上手小册》第7章:高级 Python 编程(2024 最新版)
19 1