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

本文出自“Python为何”系列,请查看 所有文章

在写上一篇《Python 为何要有 pass 语句?》时,我想到一种特别的写法,不少人会把它当成 pass 语句的替代。在文章发布后,果真有三条留言说起了它。html

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

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

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

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

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

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

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

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

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

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

可是“...”这个常量彷佛受到了特殊对待,个人 IDE 上没有做提示。工具

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

二、奇怪的 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 已经被淘汰了。

三、为何要使用“...”对象?

接下来,让咱们回到标题的问题: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 类型检查,以及表示容器对象的无限循环
  • ... 对大多数人来讲,可能并很少见(有人还可能由于它是一种符号特例而排斥它),但它的存在,有些时候可以带来便利。但愿本文能让更多人认识它,那么文章的目的也就达成了~

若是你以为本文分析得不错,那你应该会喜欢这些文章:

一、Python为何使用缩进来划分代码块?

二、Python 的缩进是否是反人类的设计?

三、Python 为何不用分号做语句终止符?

四、Python 为何没有 main 函数?为何我不推荐写 main 函数?

五、Python 为何推荐蛇形命名法?

六、Python 为何不支持 i++ 自增语法,不提供 ++ 操做符?

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

八、Python 为何用 # 号做注释符?

九、Python 为何要有 pass 语句?

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

相关文章
相关标签/搜索