# 聊聊 print 的前世今生


（一）

上周，我翻译了一篇文章，解释了[为什么 Python 3 把 print 改为函数？](https://mp.weixin.qq.com/s/i0cMwVGDnx3MJs8ybGrrMA)  概括有如下几点原因：1、print 不适宜作为应用程序级的语句。2、改为一个函数，可以实现更复杂的功能。3、改为一个函数，能方便地进行替换。

在 Python 2 中，print 是个语句（statement），它的级别就跟 for、if、def 等关键字相同，这是一个古老的设计（毕竟 Python 诞生于 1989 年），改成 print() 函数，意味着它升级了。

在查阅资料的时候，我发现 print 在历代版本中，一直发展变化，到了今天，它自身已足够完善了，可是外部的挑战一直不断。

因此，这篇文章再来聊聊它：介绍 print 的现状，追溯它的历史，说说它的挑战者，挖挖那些更加本质的东西。

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1680686999165/a5ab7db0-b3bb-4c0c-a499-5a9fd1dff934.jpeg)

（二）

在 3.0 版本中，print() 函数全新登场，开发者可以自定义打印对象的间隔（默认是空格）、终止方式（默认是换行）、以及输出位置（默认是标准输出 sys.stdout）。

而到了 3.3 版本，它还添加了一个新的参数，可以决定是否要刷新数据流。

至此，这个函数的完整格式就变成了 `print(*objects, sep=' ', end='\n', file=sys.stdout, flush=False)`  ，与升级前的 print 语句是天壤之别啦。

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1680687003957/f07acab5-f22c-418b-acd5-7d2f69ad6296.jpeg)

优点是显而易见的，可定制的参数带来了使用场景的扩充。

（三）

其实，在这次大版本的改动之前，早期的 print 语句并非是一成不变的，核心开发者们一直在完善它。

例如，在 2000 年的 PEP-214 之前，print 语句只能用于标准输出（sys.stdout），只有当提出这个提案后，print 才可以打印内容到类文件对象（file-like object）中。

（注：PEP 即 Python 改进提案，更多介绍详见旧文《[学习Python，怎能不懂点PEP呢？](https://mp.weixin.qq.com/s/oRoBxZ2-IyuPOf_MWyKZyw)》）

这次调整后，它的写法可以如下（其中，mylogfile 是用于记录打印信息的文件路径）：

```python
print >> mylogfile, 'this message goes to my log file'
```

在只接触过 Python 3 的同学眼里，这个写法可能很别扭吧，其实它等同于如今的：

```python
print('this message goes to my log file', file = mylogfile)
```

（四）

上例是一次成功的改进，但有趣的是，社区内也有一次失败的修改提案。

与 print() 函数相同，print 语句在打印完一个对象后，默认会换行，因此，当打印的内容自带了换行符的时候，最终的打印结果就会出现一个多余的换行。

2001 年的时候，有开发者在 PEP-259 中提议，根据打印的最后一个字符的类型，设置几个标志位，以此决定是否要默认换行。校验规则如下：

- -1 ——如果最后一个对象是以换行符结束的字符串
- 0  ——如果最后一个对象是以空白字符结尾的字符串，既不是空格也不是换行符
- 1  ——在所有其它情况下(包括最后一个对象是空字符串或不是字符串的情况)

根据这些规则，print 语句遇到 -1 标志位的时候，就不再做默认的换行了，似乎可以解决多余换行的问题。

然而，这个提案被否决了。反对的意见主要是：这样可能会破坏掉无数个 CGI 脚本，而且 Python 中已经有太多的“魔法”了。

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1680687007916/151c884b-ac82-4d7a-a8f6-2eae31e7d72a.jpeg)

这一套规则确实太神奇了，幸好没有实施。在当前的版本中，只需调整 end 参数，就可以避免多余换行的问题。

（五）

阅读过往的 PEP 文档，就是在阅读 Python 的历史，从中你可以看到设计者们对功能细节的打磨过程，最终你就明白了，Python 是如何一步一步地发展成今天的样子。

不过，历史中除了能看到精华，也可以看到一些包袱。print() 函数的升级就是在甩掉包袱，前不久我写了《[聊聊 Python 的内置电池](https://mp.weixin.qq.com/s/XzCqoCvcpFJt4A-E4WMqaA)》，聊到了 Python 中废弃部分标准库的话题，也是一个很好的观察例子。

除此之外，“print”的命名本身也算是一种包袱。

早期的计算机使用纸带作为信息载体，程序的运算结果需要 print 在纸带上，所以顺理成章地，有些编程语言就使用了“print”来表示程序的输出操作。尽管后来不再使用纸带了，一些语言仍然延用这个词，例如 C 语言以及借鉴了 C 语言的 Python。

Python 的另一个借鉴对象是 Shell，这是一种古老的脚本语言，可它没有“print”的包袱，它用的是 echo。这个词的本意是回声，后来也指雷达的回波，被用于计算机编程中，则又被赋予了“应答、回显”之义，更直白的表述应该是“输出、打印”。

Python 从 C 中借用了“print”命名，又从 Shell 中借用语句式的表达，形成了自己 print 语句，如今到了新的版本，它去除了语句式的表达，却仍保留着原始命名，可以说这个包袱是永远脱不掉了。

但是，话说回来，词语在演化过程中会获得新的生命，它的意义全在于如何使用。所以，虽然没有了纸带这个物理载体，print 这个词却“改头换面”地活了下来。

它还拥有很多的表兄弟姐妹呢，非常热闹（试试你能认出几个？）：

```
print("点个赞吧!")
printf("点个赞吧!");
print_r('点个赞吧！');
var_dump('点个赞吧！');
NSLog(@"点个赞吧！");
System.out.println("点个赞吧!");
console.log("点个赞吧!");
cout << "点个赞吧!" << endl;
Console.WriteLine("点个赞吧!");
writeln('点个赞吧!')
fmt.Println("点个赞吧!");
Response.Write("点个赞吧！");
alert("点个赞吧！")
echo "点个赞吧！"
puts "点个赞吧！"
say "点个赞吧!";  
```

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1680687012053/e1a375d5-bb8c-40b2-aaa6-a97f86b14376.gif)

（六）

语言内部的发展历史，以及不同语言的相似表述，都表明着一件事，那就是打印操作很重要，而且我们对它的要求还很复杂多样。

Python 中的 print 语句能发展成今天的 print() 函数，已经非常完善了。

不过，需求是无止境的，作为最常用的调试手段，print() 还达不到十全十美。它的好处是简单直白、容易上手，但缺点则是功能单一、效率较低，在需要定制格式的频繁使用场景下，不堪大用。

这在不同编程语言中是通病，因此大家都默契地提供了用于调试的日志模块，例如 Java 的 log4j，C++ 的 log4cxx，当然还有 Python 的 logging。

日志模块 logging 可以说是对 print() 函数的替代式升级，主要优点是更加灵活高效，例如可以设置不同的日志等级、配置多样的格式化信息、甚至可以输出日志到远程服务器上。

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1680687017722/041e227d-eee8-4205-9adc-2a43e9f9cb49.gif)

当然，日志模块只是一种解决方案，也并不是最完美的。

在 Python 中还有一些模块可以用于调试，例如最主流的 pdb，它可以设置断点、分步调试、查看栈片段、动态调值等，用得好，有奇效。主流的 IDE 工具也都提供了一些调试手段，相比于简单的 print()，它们具有降维打击的优势。

今年 4 月，Github 上开源了一个专用于调试程序的库，名叫 **PySnooper** ，短短两个月，它就收获了近 12K 个关注。这个三方库的口号是“Never use print for debugging again”，其目标就是在调试代码时完全替代 print。

这个库的用法非常简单，只需一行代码，就可以实现对整个函数的监听，做到记录每一行的执行时间、记录每个变量的赋值等等，而且还可以使用“with”语句，监听部分的代码块，或者使用“watch”命令，专门监听特定的变量值。

这个库强大而惊艳，除了上述作用，它还能监听指定格式开头的代码，能在多线程中监听线程，甚至支持用户自定义的监听规则。难怪它一经面世，就好评如潮，人人奔走相告。

**snoop** 这个单词很有意思，它指的是嗅探、窥探和监听。首字母大写的 **Snoop** ，译作史努比，则是一只被很多人喜爱的漫画小狗。所以这个 **PySnooper** 库就令我不由地产生了一种联想：它是一只嗅觉异常敏锐的小狗，明白无误地为你执行各种监听任务。

![史努比小狗（图片来源于网络）](https://cdn.hashnode.com/res/hashnode/image/upload/v1680687020878/e2aa52a4-c08d-4bf6-accc-acfc3dc381d3.jpeg)

（七）

最后，我们可以来回顾一下 print 的发展历史了，有两条线索，一条是它自身发展的明线，另一条是它的挑战者们的暗线。

先看明线吧，早期版本的 print 语句带有 C 和 Shell 的影子，它是个应用程序级的 statement，使用十几年间，有过一些改进的尝试，例如 PEP-214 和 PEP-259；到了 2009 年的大版本 3.0，Python 把 print 语句改成了 print() 函数，使它成为了众多内置函数的一员，随后在 3.3 版本，又对它做了一次功能增强，至此，它完成了自己的华丽蜕变，占据了稳固的一席之地。

至于暗线，print 的竞争对手们可谓众多，像传统的日志模块 logging、调试模块 pdb、以及主流 IDE 的调试功能，等等，如今还有一位后起之秀 PySnooper，无不瞄准了 print 的位置，摩拳擦掌，虎视眈眈。

print 一词最早应该跟纸带相关，用途和需求场景都很少，如今的计算机世界已经不可同日而语，所以才促进了 print 自身的发展，也刺激了众多对手们的崛起。

**print 代表了一种诉求/思想：输出计算结果、记录程序过程、监察对象变化，然后用于查看、分析、调试、展示等等。** 

明线上的发展，就是继承了它的名字，壮大 print；暗线上的发展，则是继承了它的思想，为了实现目的，各施手段，百花齐放。

print 当然不是 Python 所特有的，这明暗两线的发展也同理，如果你把视野放到任何一个经得起时间考验的语言上，必然也会看到相似的发展历程与竞争故事。
