日志

Python 为什么会有个奇怪的“...”对象?

 来源    2020-08-01    1  

本文出自“Python为什么”系列,请查看全部文章

在写上一篇《Python 为什么要有 pass 语句?》时,我想到一种特别的写法,很多人会把它当成 pass 语句的替代。在文章发布后,果然有三条留言提及了它。

所谓特别的写法就是下面这个:

# 用 ... 替代 pass
def foo():
	...

它是中文标点符号中的半个省略号,也即由英文的 3 个点组成。如果你是第一次看到,很可能会觉得奇怪:这玩意是怎么回事?(PS:如果你知道它,仔细看过本文后,你同样可能会觉得奇怪!)

1、认识一下“...”内置常量

事实上,它是 Python 3 中的一个内置对象,有个正式的名字叫作——Ellipsis,翻译成中文就是“省略号”。

更准确地说,它是一个内置常量(Built-in Constant),是 6 大内置常量之一(另外几个是 None、False、True、NotImplemented、__debug__)。

关于这个对象的基础性质,下面给出了一张截图,你们应该能明白我的意思:

“...“并不神秘,它只是一个可能不多见的符号型对象而已。用它替换 pass,在语法上并不会报错,因为 Python 允许一个对象不被赋值引用。

严格来说, 这是旁门左道,在语义上站不住脚——把“...”或其它常量或已被赋值的变量放在一个空的缩进代码块中,它们是与动作无关的,只能表达出“这有个没用的对象,不用管它”。

Python 允许这些不被实际使用的对象存在,然而聪明的 IDE 应该会有所提示(我用的是 Pycharm),比如告诉你:Statement seems to have no effect

但是“...”这个常量似乎受到了特殊对待,我的 IDE 上没有作提示。

很多人已经习惯上把它当成 pass 那样的空操作来用了(在最早引入它的邮件组讨论中,就是举了这种用法的例子)。但我本人还是倾向于使用 pass,不知道你是怎么想的呢?

2、奇怪的 Ellipsis 和 ...

... 在 PEP-3100 中被引入,最早合入在 Python 3.0 版本,而 Ellipsis 则在更早的版本中就已包含。

虽然官方说它们是同一个对象的两种写法,而且说成是单例的(singleton),但我还发现一个非常奇怪的现象,与文档的描述是冲突的:

如你所见,赋值给 ... 时会报错SyntaxError: cannot assign to Ellipsis ,然而 Ellipsis 却可以被赋值,它们的行为根本就不同嘛!被赋值之后,Ellipsis 的内存地址以及类型属性都改变了,它成了一个“变量”,不再是常量。

作为对比,给 True 或 None 之类的常量赋值时,会报错SyntaxError: cannot assign to XXX,但是给 NotImplemented 常量赋值时不会报错。

众所周知,在 Python 2 中也可以给布尔对象(True/False)赋值,然而 Python 3 已经把它们改造成不可修改的。

所以有一种可能的解释:Ellipsis 和 NotImplemented 是 Python 2 时代的遗留产物,为了兼容性或者只是因为核心开发者遗漏了,所以它们在当前版本(3.8)中还可以被赋值修改。

... 出生在 Python 3 的时代,或许在将来会完全取代 Ellipsis。目前两者共存,它们不一致的行为值得我们注意。我的建议:只使用"..."吧,就当 Ellipsis 已经被淘汰了。

3、为什么要使用“...”对象?

接下来,让我们回到标题的问题:Python 为什么要使用“...”对象?

这里就只聚焦于 Python 3 的“...”了,不去追溯 Ellipsis 的历史和现状。

之所以会问这个问题,我的意图是想知道:它有什么用处,能够解决什么问题?从而窥探到 Python 语言设计中的更多细节。

大概有如下的几种答案:

(1)扩展切片语法

官方文档中给出了这样的说明:

Special value used mostly in conjunction with extended slicing syntax for user-defined container data types.

这是个特殊的值,通常跟扩展的切片语法相结合,用在自定义的数据类型容器上。

文档中没有给出具体实现的例子,但用它结合__getitem__() 和 slice() 内置函数,可以实现类似于 [1, ..., 7] 取出 7 个数字的切片片段的效果。

由于它主要用在数据操作上,可能大部分人很少接触。听说 Numpy 把它用在了一些语法糖用法上,如果你在用 Numpy 的话,可以探索一下都有哪些玩法?

(2)表达“未完成的代码”语义

... 可以被用作占位符,也就是我在《Python 为什么要有 pass 语句?》中提到 pass 的作用。前文中对此已有部分分析。

有人觉得这样很 cute,这种想法获得了 Python 之父 Guido 的支持

(3)Type Hint 用法

Python 3.5 引入的 Type Hint 是“...”的主要使用场合。

它可以表示不定长的参数,比如Tuple[int, ...] 表示一个元组,其元素是 int 类型,但数量不限。

它还可以表示不确定的变量类型,比如文档中给出的这个例子:

from typing import TypeVar, Generic

T = TypeVar('T')

def fun_1(x: T) -> T: ...  # T here
def fun_2(x: T) -> T: ...  # and here could be different

fun_1(1)                   # This is OK, T is inferred to be int
fun_2('a')                 # This is also OK, now T is str

T 在函数定义时无法确定,当函数被调用时,T 的实际类型才被确定。

在 .pyi 格式的文件中,... 随处可见。这是一种存根文件(stub file),主要用于存放 Python 模块的类型提示信息,给 mypy、pytype 之类的类型检查工具 以及 IDE 来作静态代码检查。

(4)表示无限循环

最后,我认为有一个非常终极的原因,除了引入“...”来表示,没有更好的方法。

先看看两个例子:

两个例子的结果中都出现了“...”,它表示的是什么东西呢?

对于列表和字典这样的容器,如果其内部元素是可变对象的话,则存储的是对可变对象的引用。那么,当其内部元素又引用容器自身时,就会递归地出现无限循环引用。

无限循环是无法穷尽地表示出来的,Python 中用 ... 来表示,比较形象易懂,除了它,恐怕没有更好的选择。

最后,我们来总结一下本文的内容:

  • ... 是 Python 3 中的一个内置常量,它是一个单例对象,虽然是 Python 2 中就有的 Ellipsis 的别称,但它的性质已经跟旧对象分道扬镳
  • ... 可以替代 pass 语句作为占位符使用,但是它作为一个常量对象,在占位符语义上并不严谨。很多人已经在习惯上接受它了,不妨一用
  • ... 在 Python 中不少的使用场景,除了占位符用法,还可以支持扩展切片语法、丰富 Type Hint 类型检查,以及表示容器对象的无限循环
  • ... 对大多数人来说,可能并不多见(有人还可能因为它是一种符号特例而排斥它),但它的存在,有些时候能够带来便利。希望本文能让更多人认识它,那么文章的目的也就达成了~

如果你觉得本文分析得不错,那你应该会喜欢这些文章:

1、Python为什么使用缩进来划分代码块?

2、Python 的缩进是不是反人类的设计?

3、Python 为什么不用分号作语句终止符?

4、Python 为什么没有 main 函数?为什么我不推荐写 main 函数?

5、Python 为什么推荐蛇形命名法?

6、Python 为什么不支持 i++ 自增语法,不提供 ++ 操作符?

7、Python 为什么只需一条语句“a,b=b,a”,就能直接交换两个变量?

8、Python 为什么用 # 号作注释符?

9、Python 为什么要有 pass 语句?

本文属于“Python为什么”系列(Python猫出品),该系列主要关注 Python 的语法、设计和发展等话题,以一个个“为什么”式的问题为切入点,试着展现 Python 的迷人魅力。所有文章将会归档在 Github 上,项目地址:https://github.com/chinesehuazhou/python-whydo

相关文章
python – Django不会以一个奇怪的错误开始“AttributeError:’module’对象没有属性’getargspec’”
问答当谈到Django的内部时,我有点缺乏经验,所以我现在完全陷入困境.它昨天工作,我不记得我已经改变了任何重要的事情. 当我转向DEBUG = True时,任何模块上都有一个堆栈跟踪,恰好是列表中的第一 ...
2
python – 将特定时区中的datetime对象转换为该时区中的纪元秒
问答例如: >>>打印dt 2012-12-04 19:00:00-05:00 如您所见,我有这个日期时间对象 如何将此日期时间对象转换为格林威治标准时间-5中的纪元秒数. 我该怎么做呢 ...
2
python – 堆栈filter()调用的奇怪行为
问答所以我从for循环中堆叠的一些过滤器中获得了一些有趣的行为. 我将从演示开始: >>> x = range(100) >>> x = filter(lambda n ...
2
Python脚本用多维数据集替换对象
问答我正在尝试创建一个Python脚本来在Maya中生成多维数据集,这些多维数据集表示对象空间边界框的对象. 例如,如果我在Maya中创建随机对象(球体,立方体,金字塔,锥体等),我想用一个具有该对象边界 ...
1
python – 为什么spider.py需要一个蜘蛛对象来进行scrapy?
问答我已经看到在他们使用的类末尾的一些蜘蛛文件中 class TestSpider(BaseSpider): pass SPIDER = TestSpider() 为什么我们使用SPIDER = Test ...
python – 为什么多处理中的新对象具有相同的id?
问答我尝试在使用多处理模块时在进程中创建一个新对象.然而,有些事让我困惑. 当我使用多处理模块时,新对象的id是相同的 for i in range(4): p = multiprocessing.Pro ...
1
python – 调整扫描线填充以检测离散对象
问答我正试图在Python中检测到足够接近颜色的连续区域.我独立地偶然发现了8路递归泛光填充算法(当发现的和所需的RGB颜色之间的欧几里德距离超过阈值时终止),这在小规模下工作得很好,但是在200万像素图 ...
1
有没有办法让python pickle忽略“它不是同一个对象”的错误
问答有没有办法让python pickle忽略"它不是同一个对象"的错误? 我正在使用Mock编写一个测试,对datetime.utcnow()产生的结果进行细粒度控制.我正在使用的代 ...
python – 通过class属性调用的函数对象失败
问答我目前有一个注册回调的类系统,然后在满足某些条件时调用它们.但是我遇到了存储函数对象的一些问题. A: class Foo(object): _callback = None @classmethod ...
1
python – 将不可搜索的类文件对象流式传输到多个接收器
问答我有一个不可搜索的文件对象.特别是它是来自HTTP请求的不确定大小的文件. import requests fileobj = requests.get(url, stream=True) 我正在将此 ...
2
如何在Selenium Python绑定中等待并获取Span对象的值
问答我的网页上有以下代码. <div id="" class="user_acc_setails"> <ul id="accDtlUL& ...
1
Python – 在本地保存请求或BeautifulSoup对象
问答我有一些代码很长,所以运行需要很长时间.我想在本地保存请求对象(在本例中为"name")或BeautifulSoup对象(在本例中为"soup"),以便下次我可 ...
1
python – type()函数没有为boto3 sqs对象返回正确的结果?
问答我正在使用Python 3的类型提示语法,我正在编写一个使用SQS的小型AWS应用程序.我试图提示队列的类型.这是我获取队列类型的方式: >>> import boto3 >& ...
1
python – Django – 将URL转换为链接,图像,对象
问答我正在创建简单的注释式应用程序,需要将普通网址转换为链接,图像链接转换为图像和yt / vimeo /等.链接到flash对象.例如.: http://foo.bar to <a href=&q ...
1
python – 在Django管理员中添加许多对象的内联
问答我对Django来说是相当新鲜的,并且已经阅读了关于关系模型和内联管理表单的文档(docs on InlineModelAdmin)我正在努力找出是否可以开箱即可,或者我应该滚动我自己的表单. 假设我 ...
python – 启动芹菜:AttributeError:’module’对象没有属性’芹菜’
问答我尝试从命令行启动一个Celery工作服务器: celery -A tasks worker --loglevel=info tasks.py中的代码: import os os.environ[ ' ...
1
python – numpy数组中的奇怪赋值
问答我有一个n行数组A,其中n行为3行.每行由三个整数组成,每个整数都是一个整数,它引用numpy数组中的另一个位置.例如,如果我想要由N [4]引用的行,我使用N [N [4]].视觉: N = np. ...
2
python – sqlalchemy flask:AttributeError:’Session’对象在session.commit()上没有属性’_model_changes’
问答我已经看到了SessionMaker的很多问题,但是这一点略有不同.不知道为什么,但sqlalchemy不会让我的会话对象提交. 在我的应用程序中,我有一些代码: views.py rec = ses ...
2
python – 烧瓶中的“TypeError:’bool’对象不可调用”
问答我现在登录时,我的烧瓶应用程序中的用户模型中有is_active = db.Column(db.Boolean(),nullable = False)字段,我得到错误TypeError:'bool'对 ...
python – 使用Flask-SQLAlchemy“陈旧关联代理,父对象已超出范围”
问答我以前从未遇到过这个错误: sqlalchemy.exc.InvalidRequestError:陈旧的关联代理,父对象已超出范围 在进行一些研究之后,它看起来像是因为父对象在关联代理工作时被垃圾收集 ...
1