Shell单引号、双引号和反引号的区别

每次在shell用到引号的时候,都会因为用单引号还是双引号纠结不已。最近看了《Shell十三问》,终于弄清楚了它们的区别。

反引号

首先,反引号是明显与单双引号有区别的。放到这篇文章里面一起写可能是因为我觉得它的名字里面有引号二字吧。命令行是支持拼接的,而反引号的作用就是“执行反引号内的命令,将结果返回到字符串”。比如在下面的命令中,反引号内的命令就会先执行,得到结果a,然后反引号的内容会被执行结果替换,组合成最终命令cat a,执行。

这样就可以很方便地批量执行命令,例如删除所有docker的容器,可以使用:

这个作用基本等同于$(),上面的命令等价于 cat $(find . -name a) 事实上,$()更好一些,因为它支持嵌套,反引号不支持。

其实这个功能也可以使用管道配合xargs实现(例如上面的打开文件命令,可以用 find . -name a | xargs -I {} cat {} ),但是xargs要更繁琐一些。直接使用反引号这种替换更加直观。而且对于多个命令的结果组合成一条命令来说,反引号要更方便。

单引号和双引号

Shell中有些字符,是不表示字符意义的。比如说,你想 echo 一个 > ,你需要 echo \> 进行转义。

对于字符的意义和转义的意义,我经常搞混:这个字符需要转义之后是特殊意义还是字面意义呢?后来相出了一个窍门:如果一个字符是shell中的meta字符,那么它不表示字面的意义,需要转义之后表示字面的意义。这个窍门也适用于其他需要转义的地方,例如正则表达式的 . ,它因为是特殊字符所以不表示字面的意义 . ,如果你想要匹配一个 . 字符,就需要在写正则的时候加上一个 \ ,来表示它的字面意义。这段对某些朋友来说可能是废话……但是确实是让我纠结了好久的。剩下的就是需要记住环境下的特殊字符了。

So,你肯定不想在命令行写很多很多 \ 来转义,所以就出现了单引号和双引号。其实就类似于Python的字符串前面加 r ,表示引号内的字符全表示字面意义。在shell中,如果字符在引号内,就不会被shell解释成特殊意义。

例如空格表示分隔符(IFS),cat a b的意义是打开文件a和文件b。但是 cat "a b"的意义是打开文件名为 a空格b 的文件。等价于 cat a\ b 。

单引号和双引号的区别都是protect,保护字符串不被shell解释。但是区别就是,双引号不会保护三个字符:反引号字符、反斜杠字符\,以及$

这正好可以方便我们一些操作,比如将命令的结果当做字符串。或者在字符串内引用环境变量的值。下面的例子可以简单地展示它们的区别:

这个地方有个关键,就是你要明白你想要的是一个表示字面意义的字符串,还是一个命令组合。如果你想给一个命令传入参数,例如 awk 或 grep ,那么你的参数一般是字符串;如果你是要在shell上执行一连串的命令,那么可能不需要转义。其实就是区别 shell meta 和 command meta, 这个可以参考文章开头的《Shell 十三问》。

参考资料:

  1. shell metachar
 

副作用还是Feature?

我们的Python应用需要一个全局变量保存一些公用的值,但是不希望其他人随意往里面添加属性,导致这个对象很乱。于是我们是这样定义的:

很多人提到“限制类的属性”就会很自然的想到__slots__,我认为这并不合适。__slots__的初衷是节省对象占用的内存,如果我们的app中某个对象可能有上百万个,就要考虑到将该对象变成__slots__定义的了。禁止赋予对象__slots__声明之外的属性名,这只是节省内存的Feature所带来的一个副作用。它是用来优化程序的,并不是来约束程序员的。

如果这么写,可能带来的缺点有:

  1. __slots__并不会继承,也就是说,如果子类继承了有__slots__的类,子类不会有__slots__存在,你要记住在每一个子类都写上。
  2. __slots__存在之后,该类就不能成为弱引用的目标(具体原因可以看弱引用的原理),除非将__weakref__加入到__slots__中。但是这样做将会污染__slots__变量,其他看到这个东西的时候需要分辨哪些是app的变量,哪些语言需要的变量。
  3. 前面已经说到了,__slots__存在的目标是为了优化存储空间。如果有一天,Python发现可以动态地向对象添加属性而依然节省内存的方法,可能就会破坏我们的程序。换句话说,Python是不会保证未来依然保留“节省内存”所带来的这个副作用的。

我觉得这个地方争取的实现应该是用魔术方法 __setattr__每次赋值都会经过该方法:

是不是更加Pythonic?

另一个例子是Python的dict,在Python3.6中,提到内置的dict遍历的时候会保持插入的顺序,但是又强调这是一个为了节省dict内存而带来的一个副作用,并不是语言设计的标准,不应该被依赖:

The dict type has been reimplemented to use a more compact representation based on a proposal by Raymond Hettinger and similar to the PyPy dict implementation. This resulted in dictionaries using 20% to 25% less memory when compared to Python 3.5.

The order-preserving aspect of this new implementation is considered an implementation detail and should not be relied upon。

有关是否能使用Python3.6的dict保持插入顺序,这里有个很好的讨论。我也认为依赖这个“副作用”是个严重错误。

  1. 这不是Python语言的标准,不兼容其他解释器
  2. Python并不知道你是否依赖了dict的顺序,如果有错误,Python解释器层面不会报出错误
  3. 鉴于这不是一个语言标准,很可能被碰巧知道这个“实现细节”的人运用了这个“副作用”,但是别人却不知道(不知道实现细节也可以是一个合格Python程序员),未来修改代码可能不会注意其实个地方遍历是要根据插入顺序的,留下了隐患。

Python3.7中,这成为了一个语言特性,所以3.7+我们就可以放心使用啦!

Make it so. “Dict keeps insertion order” is the ruling. Thanks!

Guido van Rossum


 

今天在论坛看到一个有意思的问题:if foobar != None 和 if foobar is not None 是完全等价的吗?

挺有意思,这个问题我又想了一下:为什么大家比较 None 的时候用 is ,但是比价字符串(字符串也会有驻留)却用 == 。我自己的思考是:None是文档写明的全局变量,而字符串的驻留确是一个解释器为了优化而带来的副作用,不能依赖,解释器可能在某个时候决定不再缓存某个字符串,所以这是不可靠的。正好这个问题和本文的主题比较切合,我就贴一下自己的回答:

楼上 @gwki 说的很清楚了!

但是对于 None 来说有一点区别,你看很多 Python 代码就会发现:大部分情况下我们用 if foo is None 来做判断,因为 None 在 Python 中是一个全局唯一变量。官方文档中说:Since None is a singleton, testing for object identity (using == in C) is sufficient. 所以官方是推荐用 id 来 check 的。

即:None 只有一个,不存在值为 None 但是与 id(None) 不相等的情况。

写作 if foo != None 有点不 Pythonic (反正我是没这么见过哈哈哈)。

问题 2:

foo = 0
if foo 判断为假,
if foo is not None 判断为真。所以 is 判断的是 id 相同(对于 None 来说判断 id 相同和判断值相同没有太大区别,反正只有 1 个)。

所以二者是不一样的,除了 None 之外,文档( https://docs.python.org/3.6/library/stdtypes.html#truth-value-testing )还有下面的判断为假:

– constants defined to be false: None and False.
– zero of any numeric type: 0, 0.0, 0j, Decimal(0), Fraction(0, 1)
– empty sequences and collections: ”, (), [], {}, set(), range(0)

再啰嗦一点,对于不可变对象,为了避免重复创建,Python 做了驻留处理。比如下面代码:

>>> s1 = “ABC”
>>> s2 = “ABC”
>>> s1 is s2
True

但是我们实际比较二者的时候,应该用 s1 == s2。因为驻留操作是 CPython 的实现细节。副作用不应该被依赖。

 

无处不在的同化

电影《Detachment》里面,有一幕是Henry在黑板上写了两个词:assimilate,ubiquitousness。

Assimilate,同化,吸收周围的事物,融入。

Ubiquitousness,无处不在的,everywhere。

无处不在的同化。如果所有的事情都给你展现在眼前,你还有什么想象力呢?每天被巨大的信息冲击,告诉你应该做什么,应该怎样做。古代会为了统治宣传儒家思想,现代为了刺激经济给人们灌输结婚的潜意识,制造各种节日灌输各种观点来刺激消费。

如同《1984》描述的那样,在无处不在的同化中,就产生了“双重思想”,明知是谎言,人们还是愿意去相信。这种例子在现在的生活中比比皆是:我要变漂亮,才会快乐;我应该马上结婚,才会有正常的人生;我要变瘦;我应该健身……

这是一种Marketing Holocaust。这种会大毁灭式的营销会在我们接下来的人生中,每天24小时,不停地冲击我们,让我们努力工作,变得沉默,每天在广告的夹缝中看到一丝信息,最终在沉默中灭亡。

所以我们必须保护自己的思想,与这种无处不在的、让我们变得沉默的同化力量抗争。我们必须学会阅读,激发自己的想象力,培养自己的独立意识和信仰,保护自己的思想。

 

Python对象序列化与反序列(Pickle)

我们最近想要对爬虫拿到的下载结果进行存档,这个结果是一个Python对象(我们并不想简单的存一个HTML或json,想要让整个下载过程可以还原),于是就想到了用Python内置的pickle库(腌黄瓜库),序列化对象成bytes,需要的时候可以反序列化。

通过下面的代码可以简单了解pickle的用法和功能。

可以看到pickle的用法和json有点像,但是有几点根本区别:

json是跨语言的通用的一种数据交换格式,一般用文本表示,人类可读。pickle是用来序列化Python对象,只是针对Python的,序列化的结果是二进制数据,人类不可读。而且json默认只可以序列化一部分的内置类型,pickle可以序列化相当多的数据。

另外还有一个古老的marshal也是内置的。但这个库主要是针对.pyc文件的。不支持自定义的类型,也不完善。比如不能处理循环应用,如果有一个对象引用了自己,那么用marshal的话Python解释器就挂了。

版本兼容问题

由于pickle是针对Python的,Python有不同的版本(并且2与3之间差异非常大),所以就要考虑到序列化出来的对象能不能被更高(或低?)版本的Python反序列化出来。

目前一共有5个pickle的协议版本,版本越高对应Pyhton的版本越高,0-2针对Python2,3-4针对Python3.

  • Protocol version 0 is the original “human-readable” protocol and is backwards compatible with earlier versions of Python.
  • Protocol version 1 is an old binary format which is also compatible with earlier versions of Python.
  • Protocol version 2 was introduced in Python 2.3. It provides much more efficient pickling of new-style classes. Refer to PEP 307for information about improvements brought by protocol 2. (从这个版本往后,性能有显著提高)
  • Protocol version 3 was added in Python 3.0. It has explicit support for bytes objects and cannot be unpickled by Python 2.x. This is the default protocol, and the recommended protocol when compatibility with other Python 3 versions is required.
  • Protocol version 4 was added in Python 3.4. It adds support for very large objects, pickling more kinds of objects, and some data format optimizations. Refer to PEP 3154 for information about improvements brought by protocol 4.

pickle的大多数入口函数(例如dump()dumps()Pickler构造器)都接受一个协议版本的参数,其中内置了两个变量:

  • pickle.HIGHEST_PROTOCOL目前是4
  • pickle.DEFAULT_PROTOCOL目前是3

用法

和内置的json模块接口类似,dumps()用于返回序列化结果,dump()用于序列化然后写入文件。同理也有load()loads()。其中,序列化dump(s)的时候可以指定协议的版本,反序列化的时候就不用了,会自动识别版本。这个和zip命令很像。

内置类型的序列化

大多数内置的类型都支持序列化和反序列化。需要特殊注意的是函数。函数的序列化只是取其名字和所在的module。函数的代码和属性(Python的函数是第一等对象,可以有属性)都不会被序列化。这就要求函数所在的模块在unpickle的环境中必须是可以import的,否则会出现ImportErrorAttributeError

这里有个地方很有意思:所有的lambda函数都是不可Pickle的。因为它们的名字都叫做<lambda>

自定义类型的序列化

如同本文开头的实验代码一样,对我们自定义的对象在大部分情况下都不需要额外的操作就可以实现序列化/反序列化的操作。需要注意的是,在反序列化的过程中,并不是调用class的__init__()来初始化出一个对象,而是新建出一个未被初始化的实例,然后恢复它的属性(非常巧妙)。伪代码如下:

如果希望在序列化的过程中做一些额外操作,例如保存对象的状态,可以使用pickle协议的魔术方法,最常见的是__setstate__()__getstate__()

安全问题(!)

pickle文档的开头就说:千万不要unpickle一个未知来源的二进制。考虑下面的代码:

这段代码unpickle出来就是导入了os.system()然后调用echo。并没有啥副作用。但如果是rm -rf /·呢?

文档给出的建议是在Unpickler.find_class()里面实现检查逻辑。函数方法在要求全局变量的时候必然调用。

压缩

pickle之后并不会自动压缩的,我觉得这个设计非常好,解耦,pickle就干pickle的事情,压缩交给别的库去做。而且你自己也可以发现,pickle之后的文件尽管不可读,但内容依然是以ascii码呈现的,并不是乱码。需要调用压缩库的compress。实测压缩之后,体积是之前的1/3左右,非常可观。

我目前已知的坑:

全局变量要保持可以导入的,这个有点难。要面对的问题是:我今天pickle的东西,在将来某一天需要打开,是否还能打开呢?

这里有几个版本:项目的版本、python的版本,pickle协议版本,项目依赖的包版本。其中python的版本和pickle版本我觉得可以放心依赖他们的向后兼容,容易解决。主要是项目和版本和依赖的版本,如果要Pickle的对象非常复杂,那么很可能老版本的备份无法兼容新版本。可能的解决办法就是对所有的依赖完全锁定,例如记录他们的hash值。如果要恢复某一个二进制序列,那么就还原出当时的特定依赖、项目的特定commit。

但是目前来说,我们的需求基本上就是pickle一个requests.Response对象。我觉得是可以依赖它们的向后兼容的。如果有一天requests有了breaking change,那么就算我们的pickle能兼容,代码也不可能兼容了,那时候可以考虑别的策略。

 

一次Mongodb的admin数据库导致的事故

Mongodb的gridfs一次插入数据的时候会自动创建几个索引,我们程序里面的账号没有createIndex权限,我需要手动创建一下。结果连接到mongo服务器之后忘记执行use xxxdb来切换数据库了,于是在admin数据库里面创建了一个索引,结果导出一边的程序报出来很多验证问题。

Mongo的admin数据库太脆弱了,只是创建一个索引就挂了。长个教训,以后千万不要手动修改它,更不要用admin保存数据

反思一下,这次操作失误其实爆出我平时一些不好的习惯。

首先,连接mongo应该指定目标数据。而我之前都是连接到admin,然后用use切换到目标数据库。这样难免会忘记。

第二,错误的在admin数据库执行createIndex,返回的结果明确显示索引创建成功。

但是我忽略了,继续在正确的数据库创建索引。不然可以早一些发现问题。

最后,创建索引应该自动化,比如gridfs这种对md5, filename创建索引的。