├── .vscode └── settings.json ├── .gitattributes ├── assets ├── logo.png ├── crazycoding_qr.jpg ├── 1=3 │ ├── after_board_initialized.png │ └── after_row_initialized.png ├── string-intern │ └── string_intern.png └── name-ignore-class-scope │ └── name-ignore-class-scope.png ├── test.py ├── mixed_tabs_and_spaces.py ├── CONTRIBUTORS.md ├── individual ├── 10.`is not ...` 并不是 `is (not ...)`.md ├── 46.无限.md ├── 21.亦真还假.md ├── 3.说了要执行就一定会执行.md ├── 42.试试用大括号?.md ├── 13.纠结的not.md ├── 48.`+=`更快.md ├── 40.人生苦短,我用Python.md ├── 29.不要在迭代一个字典的时候修改这个字典!.md ├── 12.最后一个反斜杠.md ├── 43.不等号的争议.md ├── 27.移形换位.md ├── 14.只剩一半的三引号.md ├── 15.消失的午夜零点.md ├── 50.字符串到浮点数的转换.md ├── 18.None的产生.md ├── 26.无效的一行?.md ├── 38.被忽略的类变量.md ├── 39.小心处理元组.md ├── 23.子类的关系.md ├── 11.尾部的逗号.md ├── 47.被修改的类成员.md ├── 35.相同的操作,不同的结果.md ├── 22.转瞬即空.md ├── 41.为什么要用`goto`?.md ├── 36.作用域之外的变量.md ├── 2.不变的哈希值.md ├── 19.不可修改的元组.md ├── 8.三子棋之一步取胜法.md ├── 28.到底哪里出错了呢?.md ├── 45.这些语句是存在的!.md ├── 4.鸠占鹊巢.md ├── 37.小心那些链式操作符.md ├── 30.删不掉的对象.md ├── 6.时间的误会.md ├── 9.没脑子的函数.md ├── 5.神奇赋值法.md ├── 24.神秘的键值转换.md ├── 16.站错队的布尔型.md ├── 17.类属性与类实例属性.md ├── 33.小心那些有默认值的可变参数!.md ├── 31.删除正在迭代中的列表项.md ├── 25.看你能不能猜到这个的结果?.md ├── 20.消失的变量e.md ├── 34.抓住这些异常!.md ├── 32.被泄露出去的循环变量!.md ├── 44.就算Python也知道爱是个复杂的东西.md ├── 1.善变的字符串.md ├── 51.最后一些小特点集合.md ├── 7.特殊的数字们.md ├── 49.超长字符串!.md ├── toc.md └── total.md ├── LICENSE ├── CONTRIBUTING.md ├── .gitignore └── README.md /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.linting.pylintEnabled": false 3 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/true1023/Crazy-Python/HEAD/assets/logo.png -------------------------------------------------------------------------------- /assets/crazycoding_qr.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/true1023/Crazy-Python/HEAD/assets/crazycoding_qr.jpg -------------------------------------------------------------------------------- /assets/1=3/after_board_initialized.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/true1023/Crazy-Python/HEAD/assets/1=3/after_board_initialized.png -------------------------------------------------------------------------------- /assets/1=3/after_row_initialized.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/true1023/Crazy-Python/HEAD/assets/1=3/after_row_initialized.png -------------------------------------------------------------------------------- /assets/string-intern/string_intern.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/true1023/Crazy-Python/HEAD/assets/string-intern/string_intern.png -------------------------------------------------------------------------------- /assets/name-ignore-class-scope/name-ignore-class-scope.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/true1023/Crazy-Python/HEAD/assets/name-ignore-class-scope/name-ignore-class-scope.png -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | def count(s, sub): 2 | result = 0 3 | for i in range(len(s) + 1 - len(sub)): 4 | result += (s[i:i + len(sub)] == sub) 5 | return result 6 | 7 | print(count('abc','')) -------------------------------------------------------------------------------- /mixed_tabs_and_spaces.py: -------------------------------------------------------------------------------- 1 | def square(x): 2 | sum_so_far = 0 3 | for _ in range(x): 4 | sum_so_far += x 5 | return sum_so_far # Python3 的话运行会报语法错误,请在python2下运行 6 | 7 | print(square(10)) -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | 感谢下面所有的贡献者. 他们全部都为Crazy-Python贡献了自己的一份力量. 2 | 3 | | Contributor | Github | Issues | 4 | |-------------|--------|--------| 5 | | true1023 | [True1023](https://github.com/true1023) | [#1](http://www.qinluo1023.com) | 6 | 7 | 感谢你们花费宝贵的时间,让这个项目更加出色! :smile: 8 | -------------------------------------------------------------------------------- /individual/10.`is not ...` 并不是 `is (not ...)`.md: -------------------------------------------------------------------------------- 1 | ### ▶ `is not ...` 并不是 `is (not ...)` 2 | 3 | ```py 4 | >>> 'something' is not None 5 | True 6 | >>> 'something' is (not None) 7 | False 8 | ``` 9 | 10 | #### 💡 解释 11 | 12 | - `is not` 是一个单独的二元运算符, 和分开使用的`is`和`not`作用是不同的。 13 | - `is not` 只有在两边的操作数相同时(id相同)结果才为`False`,否则为`True` 14 | 15 | --- -------------------------------------------------------------------------------- /individual/46.无限.md: -------------------------------------------------------------------------------- 1 | ### ▶ 无限(Inpinity) * 2 | 3 | **Output (Python 3.x):** 4 | ```py 5 | >>> infinity = float('infinity') 6 | >>> hash(infinity) 7 | 314159 8 | >>> hash(float('-inf')) 9 | -314159 10 | ``` 11 | 12 | #### 💡 解释: 13 | - 无限(infinity)的哈希值是 10⁵ x π. 14 | - 有趣的是, 在Python3中,`float('-inf')`的哈希值是"-10⁵ x π", 但是在Python2中是"-10⁵ x e"(正无穷还是一样的哈希值)。 15 | 16 | --- -------------------------------------------------------------------------------- /individual/21.亦真还假.md: -------------------------------------------------------------------------------- 1 | ### ▶ 亦真还假 2 | 3 | ```py 4 | True = False 5 | if True == False: 6 | print("It's true of false?") 7 | ``` 8 | 9 | **Output:** 10 | ``` 11 | It's true of false? 12 | ``` 13 | 14 | #### 💡 解释: 15 | 16 | - 一开始, Python是没有`bool`布尔类型的(人们用0代表假用非0代表真)。后来增加了`True`,`False`还有`bool`类型,但是为了向前兼容,所以并没有`True`和`False`的关键字常量,只不过作为一个内部变量出现。 17 | - Python 3是不向前兼容的,所以终于修复了这个问题,同时也要注意上面的例子在Python 3.X版本下是运行不了的! 18 | 19 | --- -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 2 | Version 2, December 2004 3 | 4 | Copyright (C) 2018 Satwik Kansal 5 | 6 | Everyone is permitted to copy and distribute verbatim or modified 7 | copies of this license document, and changing it is allowed as long 8 | as the name is changed. 9 | 10 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 11 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 12 | 13 | 0. You just DO WHAT THE FUCK YOU WANT TO. 14 | -------------------------------------------------------------------------------- /individual/3.说了要执行就一定会执行.md: -------------------------------------------------------------------------------- 1 | ### ▶ 说了要执行就一定会执行! 2 | 3 | ```py 4 | def some_func(): 5 | try: 6 | return 'from_try' 7 | finally: 8 | return 'from_finally' 9 | ``` 10 | 11 | **Output:** 12 | ```py 13 | >>> some_func() 14 | 'from_finally' 15 | ``` 16 | 17 | #### 💡 解释: 18 | 19 | - 当在`try`语句块中遇到`return`,`break`或者`continue`的时候,如果是"try...finlly"语句块,那么在执行完`try`语句块里的内容后,依然会执行`finally`语句块的内容。 20 | - 当`return`语句返回一个值的时候,那么因为在`finally`语句块中的`return`语句是最后执行的,那么返回的值就永远都是`finally`语句块中`return`语句返回的值。 21 | 22 | --- -------------------------------------------------------------------------------- /individual/42.试试用大括号?.md: -------------------------------------------------------------------------------- 1 | ### ▶ 试试用大括号? * 2 | 3 | 如果你不太习惯Python中用空格缩进来表示语句段(scopes)的方法,可以通过引用`__future__`模块来使用C语言大括号({})形式的表示方法来表示语句段, 4 | 5 | ```py 6 | from __future__ import braces 7 | ``` 8 | 9 | **Output:** 10 | ```py 11 | File "some_file.py", line 1 12 | from __future__ import braces 13 | SyntaxError: not a chance 14 | ``` 15 | 16 | 想用大括号?不可能的,如果你不服气,用Java去。 17 | 18 | #### 💡 解释: 19 | + `__future__`模块一般用来提供未来Python版本才拥有的一些特性。当然,在这个例子里,"future"这个词本身也算是对大括号的嘲讽。 20 | + 这是一个Python社区针对大括号这个问题设计出来的一个彩蛋。 21 | 22 | --- -------------------------------------------------------------------------------- /individual/13.纠结的not.md: -------------------------------------------------------------------------------- 1 | ### ▶ 纠结的not 2 | 3 | ```py 4 | x = True 5 | y = False 6 | ``` 7 | 8 | **Output:** 9 | ```py 10 | >>> not x == y 11 | True 12 | >>> x == not y 13 | File "", line 1 14 | x == not y 15 | ^ 16 | SyntaxError: invalid syntax 17 | ``` 18 | 19 | #### 💡 解释: 20 | 21 | * 操作符的优先级会影响表达式的计算顺序,并且在Python里,`==`操作符的优先级要高于`not`操作符。 22 | * 所以`not x == y`等于 `not (x == y)`,又等于`not (True == False)`,最终计算结果就会是`True`。 23 | * 但是`x == not y`会报错是因为这个表达式可以等价于`(x == not) y`,而不是我们第一眼认为的`x == (not y)`。 24 | 25 | --- -------------------------------------------------------------------------------- /individual/48.`+=`更快.md: -------------------------------------------------------------------------------- 1 | ### ▶ `+=`更快 2 | 3 | ```py 4 | # 使用"+"连接三个字符串: 5 | >>> timeit.timeit("s1 = s1 + s2 + s3", setup="s1 = ' ' * 100000; s2 = ' ' * 100000; s3 = ' ' * 100000", number=100) 6 | 0.25748300552368164 7 | # 使用"+="连接三个字符串: 8 | >>> timeit.timeit("s1 += s2 + s3", setup="s1 = ' ' * 100000; s2 = ' ' * 100000; s3 = ' ' * 100000", number=100) 9 | 0.012188911437988281 10 | ``` 11 | 12 | #### 💡 解释: 13 | + 当需要连接的字符串超过两个的时候,使用`+=`会比使用`+`更快。(比如上面的例子`s1 += s2 + s3`中的`s1`),因为第一个字符串不会在做链接的时候销毁重建(因为`+=`操作符是就地操作的,上面多次已经提到过这个符号特性)。 14 | 15 | --- -------------------------------------------------------------------------------- /individual/40.人生苦短,我用Python.md: -------------------------------------------------------------------------------- 1 | ### ▶ 人生苦短,我用Python * 2 | 3 | 首先,我先引用下面这个模块,然后...... 4 | 5 | ```py 6 | import antigravity 7 | ``` 8 | 9 | **Output:** 10 | 嘘....这个秘密可没几个人知道! 11 | 12 | #### 💡 解释: 13 | + `antigravity` 模块是针对Python开发者的一个小彩蛋。 14 | + `import antigravity` 会打开一个指向 [classic XKCD comic](http://xkcd.com/353/)的网站,一个关于Python的漫画网站。 15 | + 而且,这还是一个俄罗斯套娃彩蛋,也就是说**这个彩蛋中还包含着另一个彩蛋**。如果你打开这个模块的[代码](https://github.com/python/cpython/blob/master/Lib/antigravity.py#L7-L17),你会发现代码中还定义了一个实现[geohashing算法](https://xkcd.com/426/)的函数(这个算法是一个利用股票市场数据随机生成经度纬度坐标的算法)。 16 | 17 | --- -------------------------------------------------------------------------------- /individual/29.不要在迭代一个字典的时候修改这个字典!.md: -------------------------------------------------------------------------------- 1 | ### ▶ 不要在迭代一个字典的时候修改这个字典! 2 | 3 | ```py 4 | x = {0: None} 5 | 6 | for i in x: 7 | del x[i] 8 | x[i+1] = None 9 | print(i) 10 | ``` 11 | 12 | **Output (Python 2.7- Python 3.5):** 13 | 14 | ``` 15 | 0 16 | 1 17 | 2 18 | 3 19 | 4 20 | 5 21 | 6 22 | 7 23 | ``` 24 | 25 | 这个迭代会准确运行**八**次停止。 26 | 27 | #### 💡 解释: 28 | 29 | * 首先我是不赞成在迭代一个字典的时候对这个字典做修改的。 30 | * 这个迭代正好运行了八次是因为正好到了一个点,到了这个点后,这个字典对象需要重新调整大小用来容纳足够的关键字(keys)(我们在循环中已经连续做了八次删除了,当然需要重新调整字典的大小了)。实际上这个点在哪取决于Python的具体实现细节。 31 | * 对删除键值的处理和什么时候触发调整字典大小会根据不同Python的版本而有所不同。 32 | 33 | --- -------------------------------------------------------------------------------- /individual/12.最后一个反斜杠.md: -------------------------------------------------------------------------------- 1 | ### ▶ 最后一个反斜杠 2 | 3 | **Output:** 4 | ``` 5 | >>> print("\\ C:\\") 6 | \ C:\ 7 | >>> print(r"\ C:") 8 | \ C: 9 | >>> print(r"\ C:\") 10 | 11 | File "", line 1 12 | print(r"\ C:\") 13 | ^ 14 | SyntaxError: EOL while scanning string literal 15 | ``` 16 | 17 | #### 💡 解释 18 | 19 | - 如果字符串前面声明了`r`,说明后面紧跟着的是一个原始字符串,反斜杠在这种字符串中是没有特殊意义的 20 | ```py 21 | >>> print(repr(r"craz\"y")) 22 | 'craz\\"y' 23 | ``` 24 | - 解释器实际上是怎么做的呢,虽然看起来仅仅是改变了反斜杠的转义特性,实际上,它(反斜杠)会把自己和紧跟着自己的下一个字符一起传入到解释器,用来供解释器做判断和转换。这也就是为什么当反斜杠在最后一个字符的时候会报错。 25 | 26 | --- -------------------------------------------------------------------------------- /individual/43.不等号的争议.md: -------------------------------------------------------------------------------- 1 | ### ▶ 不等号的争议 * 2 | 3 | **Output (Python 3.x)** 4 | ```py 5 | >>> from __future__ import barry_as_FLUFL 6 | >>> "Ruby" != "Python" # 这句话没有任何争议 7 | File "some_file.py", line 1 8 | "Ruby" != "Python" 9 | ^ 10 | SyntaxError: invalid syntax 11 | 12 | >>> "Ruby" <> "Python" 13 | True 14 | ``` 15 | 16 | 直接看解释吧。 17 | 18 | #### 💡 解释: 19 | - 上面的问题起源自一个已经淘汰的Python修正包[PEP-401](https://www.python.org/dev/peps/pep-0401/),发布于2009年的4月1日(明白为什么它会被淘汰了吧)。 20 | - 下面引用 PEP-401 的内容 21 | > 为了让你们认识到在Python3.0中 != 这种逻辑不等式是多么的不好用,FLUFL引用会使解释器禁止这种语法只允许 <> 这种便利的逻辑不等式。 22 | 23 | --- -------------------------------------------------------------------------------- /individual/27.移形换位.md: -------------------------------------------------------------------------------- 1 | ### ▶ 移形换位 * 2 | 3 | ```py 4 | import numpy as np 5 | 6 | def energy_send(x): 7 | # 初始化一个numpy数组 8 | np.array([float(x)]) 9 | 10 | def energy_receive(): 11 | # 返回一个空numpy数组 12 | return np.empty((), dtype=np.float).tolist() 13 | ``` 14 | 15 | **Output:** 16 | ```py 17 | >>> energy_send(123.456) 18 | >>> energy_receive() 19 | 123.456 20 | ``` 21 | 22 | 是不是很神奇? 23 | 24 | #### 💡 解释: 25 | 26 | * 因为在`energy_send`函数中创建的numpy数组并没有通过`return`返回赋给任何一个变量,所以带边这个数组的那一块内存空间是可以自由分配的。 27 | * `numpy.empty()` 会返回最近的一块还没有被使用的内存块,并且不会对这个内存块进行初始化。这块内存块恰恰就是上一个函数刚刚释放的那块内存(大部分时间是这样的,但并不是绝对的)。 28 | 29 | --- -------------------------------------------------------------------------------- /individual/14.只剩一半的三引号.md: -------------------------------------------------------------------------------- 1 | ### ▶ 只剩一半的三引号 2 | 3 | **Output:** 4 | ```py 5 | >>> print('crazypython''') 6 | wtfpython 7 | >>> print("crazypython""") 8 | wtfpython 9 | >>> # 下面的语句将会产生语法错误 10 | >>> # print('''crazypython') 11 | >>> # print("""crazypython") 12 | ``` 13 | 14 | #### 💡 解释: 15 | + Python支持隐式的[字符串连接](https://docs.python.org/2/reference/lexical_analysis.html#string-literal-concatenation),比如下面这样, 16 | ``` 17 | >>> print("crazy" "python") 18 | crazypython 19 | >>> print("crazy" "") # or "crazy""" 20 | crazy 21 | ``` 22 | + 在Python中,`'''` 和 `"""` 也是一种字符串界定符,所以如果Python解释器发现了其中一个,那么就会一直在后面找对称的另一个界定符,这也就是为什么上面例子里注释掉的语句会有语法错误,因为解释器在后面找不到和前面`'''`或`"""`配对的界定符。 23 | 24 | --- -------------------------------------------------------------------------------- /individual/15.消失的午夜零点.md: -------------------------------------------------------------------------------- 1 | ### ▶ 消失的午夜零点 2 | 3 | ```py 4 | from datetime import datetime 5 | 6 | midnight = datetime(2018, 1, 1, 0, 0) 7 | midnight_time = midnight.time() 8 | 9 | noon = datetime(2018, 1, 1, 12, 0) 10 | noon_time = noon.time() 11 | 12 | if midnight_time: 13 | print("Time at midnight is", midnight_time) 14 | 15 | if noon_time: 16 | print("Time at noon is", noon_time) 17 | ``` 18 | 19 | **Output:** 20 | ```sh 21 | ('Time at noon is', datetime.time(12, 0)) 22 | ``` 23 | 午夜时间并没有被打印出来 24 | 25 | #### 💡 解释: 26 | 27 | 在Python 3.5以前, 对于被赋值为UTC零点的`datetime.time`对象的布尔值,会被认为是`False`。这是一个在用`if obj:`这种语句的时候经常会忽略的特性,所以我们在写这种`if`语句的时候,要注意判断`obj`是否等于`null`或者空。 28 | 29 | --- -------------------------------------------------------------------------------- /individual/50.字符串到浮点数的转换.md: -------------------------------------------------------------------------------- 1 | ### ▶ 字符串到浮点数的转换 2 | 3 | ```py 4 | a = float('inf') 5 | b = float('nan') 6 | c = float('-iNf') #这些字符串是大小写不敏感的 7 | d = float('nan') 8 | ``` 9 | 10 | **Output:** 11 | ```py 12 | >>> a 13 | inf 14 | >>> b 15 | nan 16 | >>> c 17 | -inf 18 | >>> float('some_other_string') 19 | ValueError: could not convert string to float: some_other_string 20 | >>> a == -c #inf==inf 21 | True 22 | >>> None == None # None==None 23 | True 24 | >>> b == d #但是 nan!=nan 为真 25 | False 26 | >>> 50/a 27 | 0.0 28 | >>> a/a 29 | nan 30 | >>> 23 + b 31 | nan 32 | ``` 33 | 34 | #### 💡 解释: 35 | 36 | `'inf'`和`'nan'`是一种特殊的字符串(大小写不敏感),它们两个都可以强制转换成`float`浮点类型,并且分别代表了数学意义上的"无限"(infinity)和"非数字"(not a number)。 37 | 38 | --- -------------------------------------------------------------------------------- /individual/18.None的产生.md: -------------------------------------------------------------------------------- 1 | ### ▶ None的产生 2 | 3 | ```py 4 | some_iterable = ('a', 'b') 5 | 6 | def some_func(val): 7 | return "something" 8 | ``` 9 | 10 | **Output:** 11 | ```py 12 | >>> [x for x in some_iterable] 13 | ['a', 'b'] 14 | >>> [(yield x) for x in some_iterable] 15 | at 0x7f70b0a4ad58> 16 | >>> list([(yield x) for x in some_iterable]) 17 | ['a', 'b'] 18 | >>> list((yield x) for x in some_iterable) 19 | ['a', None, 'b', None] 20 | >>> list(some_func((yield x)) for x in some_iterable) 21 | ['a', 'something', 'b', 'something'] 22 | ``` 23 | 24 | #### 💡 解释: 25 | - 这是一个CPython中`yield`关键字在列表构造和生成器表达式中使用的bug。已经在Python3.8中修复了。在Python3.7中也会警告过时。具体可以看Python的这篇[bug report](https://bugs.python.org/issue10544) 26 | --- -------------------------------------------------------------------------------- /individual/26.无效的一行?.md: -------------------------------------------------------------------------------- 1 | ### ▶ 无效的一行? 2 | 3 | **Output:** 4 | ```py 5 | >>> value = 11 6 | >>> valuе = 32 7 | >>> value 8 | 11 9 | ``` 10 | 11 | 哈?一脸懵逼是不是(囧)? 12 | 13 | **提示:** 最简单的复现上面结果的办法是把上面的赋值语句原封不动的复制粘贴到你的文件或者命令行中执行。 14 | 15 | #### 💡 解释 16 | 17 | 一些非西方字符集看起来和我们平时用的英文字母的外观是一模一样的,但是在解释器看来就不一样了。 18 | 19 | ```py 20 | >>> ord('е') # 俄语的字母 'e' (Ye) 21 | 1077 22 | >>> ord('e') # 拉丁文的字母'e', 也就是我们平时在英语和用标准键盘打出来的字母 23 | 101 24 | >>> 'е' == 'e' 25 | False 26 | 27 | >>> value = 42 # 拉丁的 e 28 | >>> valuе = 23 # 俄语里的 'e', 这里如果是用的Python 2.x,解释器会产生一个`SyntaxError`的异常 29 | >>> value 30 | 42 31 | ``` 32 | 33 | 内置函数`ord()`可以返回对应字符在Unicode编码中的位置([code point](https://en.wikipedia.org/wiki/Code_point)), 因为俄语的字符'e'和拉丁语的字符'e'的位置是不一样的,所以在上面的例子里,解释器会认为两个`value`变量是不同的。 34 | 35 | --- -------------------------------------------------------------------------------- /individual/38.被忽略的类变量.md: -------------------------------------------------------------------------------- 1 | ### ▶ 被忽略的类变量 2 | 3 | 1\. 4 | ```py 5 | x = 5 6 | class SomeClass: 7 | x = 17 8 | y = (x for i in range(10)) 9 | ``` 10 | 11 | **Output:** 12 | ```py 13 | >>> list(SomeClass.y)[0] 14 | 5 15 | ``` 16 | 17 | 2\. 18 | ```py 19 | x = 5 20 | class SomeClass: 21 | x = 17 22 | y = [x for i in range(10)] 23 | ``` 24 | 25 | **Output (Python 2.x):** 26 | ```py 27 | >>> SomeClass.y[0] 28 | 17 29 | ``` 30 | 31 | **Output (Python 3.x):** 32 | ```py 33 | >>> SomeClass.y[0] 34 | 5 35 | ``` 36 | 37 | #### 💡 解释 38 | - 如果一个类定义内嵌套了一个子作用域,那么在这个作用域内部会忽略所有这个类级别的变量绑定。 39 | - 一个生成器(generator)表达式有自己的作用域 40 | - 从Python 3.X开始,列表生成式也有自己的作用域了。 41 | 42 | ![name-ignore-class-scope](./assets/name-ignore-class-scope/name-ignore-class-scope.png) 43 | 44 | --- -------------------------------------------------------------------------------- /individual/39.小心处理元组.md: -------------------------------------------------------------------------------- 1 | ### ▶ 小心处理元组 2 | 3 | 1\. 4 | ```py 5 | x, y = (0, 1) if True else None, None 6 | ``` 7 | 8 | **Output:** 9 | ``` 10 | >>> x, y # 期待的结果是 (0, 1) 11 | ((0, 1), None) 12 | ``` 13 | 14 | 基本每一个Python程序员都会遇到过下面这种情况。 15 | 16 | 2\. 17 | ```py 18 | t = ('one', 'two') 19 | for i in t: 20 | print(i) 21 | 22 | t = ('one') 23 | for i in t: 24 | print(i) 25 | 26 | t = () 27 | print(t) 28 | ``` 29 | 30 | **Output:** 31 | ```py 32 | one 33 | two 34 | o 35 | n 36 | e 37 | tuple() 38 | ``` 39 | 40 | #### 💡 解释: 41 | * 第一个例子里,想要得到期待的结果的写法应该是`x, y = (0, 1) if True else (None, None)`,要显式声明元组。 42 | * 第二个例子里,想要得到期待的结果的写法应该是 `t = ('one',)` 或者 `t = 'one',`(原来的少了逗号)否则解释器会把变量`t`当做一个`str`字符串处理挨个迭代里面的每一个字符。 43 | * `()`是一个特殊的标识符专门用来表示空元组(`tuple`)。 44 | 45 | --- -------------------------------------------------------------------------------- /individual/23.子类的关系.md: -------------------------------------------------------------------------------- 1 | ### ▶ 子类的关系 * 2 | 3 | **Output:** 4 | ```py 5 | >>> from collections import Hashable 6 | >>> issubclass(list, object) 7 | True 8 | >>> issubclass(object, Hashable) 9 | True 10 | >>> issubclass(list, Hashable) 11 | False 12 | ``` 13 | 14 | 是不是觉得类的继承关系应该是可传递的(transitive)?(比如,`A`是`B`的一个子类,`B`又是`C`的一个子类,那么`A`也"应该"是`C`的子类) 15 | 16 | #### 💡 解释: 17 | 18 | * 在Python中子类的继承关系不一定是传递的,任何人都可以自定义元类(metaclass)中的`__subclasscheck__`函数(`_subclasscheck__(subclass)`检查subclass是不是调用类的子类)。 19 | * 当调用`issubclass(cls, Hashable)`的时候,函数只是简单的检查一下`cls`和它继承的类中有没有"`__hash__`"这个方法。 20 | * 因为`object`是可以被哈希的(也就是说`object`有`__hash__`这个函数),但是`list`是不能被哈希的,所以他们之间打破了传导关系。 21 | * 如果想看更详尽的解释,[这里](https://www.naftaliharris.com/blog/python-subclass-intransitivity/)有关于Python子类关系传导的详细解释。 22 | 23 | --- -------------------------------------------------------------------------------- /individual/11.尾部的逗号.md: -------------------------------------------------------------------------------- 1 | ### ▶ 尾部的逗号 2 | 3 | **Output:** 4 | ```py 5 | >>> def f(x, y,): 6 | ... print(x, y) 7 | ... 8 | >>> def g(x=4, y=5,): 9 | ... print(x, y) 10 | ... 11 | >>> def h(x, **kwargs,): 12 | File "", line 1 13 | def h(x, **kwargs,): 14 | ^ 15 | SyntaxError: invalid syntax 16 | >>> def h(*args,): 17 | File "", line 1 18 | def h(*args,): 19 | ^ 20 | SyntaxError: invalid syntax 21 | ``` 22 | 23 | #### 💡 解释: 24 | 25 | - 末尾的逗号在函数参数列表最后并不总是合法的 26 | - 在Python中,参数列表里,有一部分使用前导逗号分隔的,有一部分是用后导逗号分隔的(比如`**kwargs`这种参数用前导逗号分隔,正常参数`x`用后导逗号分隔)。而这种情况就会导致有些参数列表里的逗号前后都没有用到,就会产生冲突导致编译失败。 27 | - **注意** 这种尾部逗号的问题已经在Python 3.6中被[修复](https://bugs.python.org/issue9232)了。然后[这里](https://bugs.python.org/issue9232#msg248399)有对各种尾部逗号用法的讨论。 28 | 29 | --- -------------------------------------------------------------------------------- /individual/47.被修改的类成员.md: -------------------------------------------------------------------------------- 1 | ### ▶ 被修改的类成员 * 2 | 3 | ```py 4 | class Yo(object): 5 | def __init__(self): 6 | self.__honey = True 7 | self.bitch = True 8 | ``` 9 | 10 | **Output:** 11 | ```py 12 | >>> Yo().bitch 13 | True 14 | >>> Yo().__honey 15 | AttributeError: 'Yo' object has no attribute '__honey' 16 | >>> Yo()._Yo__honey 17 | True 18 | ``` 19 | 20 | 为什么非要`Yo()._Yo__honey`才对? 如果有印度朋友他一定会懂这个梗的(好吧,我猜不会有印度人看一个中文的文章,Yo Yo Honey Singh是一个印度著名rapper,这就是梗,也是为什么这个类要起名Yo,哈哈). 21 | 22 | #### 💡 解释: 23 | 24 | * [名称混淆](https://en.wikipedia.org/wiki/Name_mangling)是一种用来避免命名空间下命名冲突的技术。 25 | * 在Python里,如果某个类成员的名称以`__`(两个下划线)开头,并且不以两个以上的下划线结尾(这就意味着你想定义一个私有变量,但是Python是没有私有变量的),那么编辑器就会自动在这个成员名称之前加上`_NameOfTheClass`(在这里就是`_Yo`),来防止子类不小心访问修改到这个成员属性。 26 | * 所以,当要访问`__honey`这个成员属性的时候,我们需要在前面加上`_Yo`来进行访问。 27 | 28 | --- -------------------------------------------------------------------------------- /individual/35.相同的操作,不同的结果.md: -------------------------------------------------------------------------------- 1 | ### ▶ 相同的操作,不同的结果 2 | 3 | 1\. 4 | ```py 5 | a = [1, 2, 3, 4] 6 | b = a 7 | a = a + [5, 6, 7, 8] 8 | ``` 9 | 10 | **Output:** 11 | ```py 12 | >>> a 13 | [1, 2, 3, 4, 5, 6, 7, 8] 14 | >>> b 15 | [1, 2, 3, 4] 16 | ``` 17 | 18 | 2\. 19 | ```py 20 | a = [1, 2, 3, 4] 21 | b = a 22 | a += [5, 6, 7, 8] 23 | ``` 24 | 25 | **Output:** 26 | ```py 27 | >>> a 28 | [1, 2, 3, 4, 5, 6, 7, 8] 29 | >>> b 30 | [1, 2, 3, 4, 5, 6, 7, 8] 31 | ``` 32 | 33 | #### 💡 解释: 34 | 35 | * `a += b` 和 `a = a + b`的内部处理流程有时候会根据操作数的不同有所不同。比如*`op=`*这种操作符(比如`+=`,`-=`,`*=`这样),针对列表(list)对象这两个表达式就会有不一样的内在表现。 36 | 37 | * 表达式`a = a + [5,6,7,8]`会生成一个新的对象,并且`a`会引用这个新生成的对象,`b`变量不会变(继续引用之前的列表对象`[1,2,3,4]`)。 38 | 39 | * 表达式`a += [5,6,7,8]`就不会生成一个新对象,它实际上是在原有对象的基础上又“扩展了”几个元素,也就是所谓的就地改变(in-place)。所以这个时候`a`,`b`两个变量还是同时引用的一个对象。 40 | 41 | --- -------------------------------------------------------------------------------- /individual/22.转瞬即空.md: -------------------------------------------------------------------------------- 1 | ### ▶ 转瞬即空 2 | 3 | ```py 4 | some_list = [1, 2, 3] 5 | some_dict = { 6 | "key_1": 1, 7 | "key_2": 2, 8 | "key_3": 3 9 | } 10 | 11 | some_list = some_list.append(4) 12 | some_dict = some_dict.update({"key_4": 4}) 13 | ``` 14 | 15 | **Output:** 16 | ```py 17 | >>> print(some_list) 18 | None 19 | >>> print(some_dict) 20 | None 21 | ``` 22 | 23 | #### 💡 解释 24 | 25 | 大部分修改序列结构对象的方法,比如`list.append`,`dict.update`,`list.sort`等等。都会直接对序列对象本身进行操作,也就是所谓的“就地操作”(in-plcae)并且会返回`None`。这么做是为了提升程序的性能,避免还需要把原有对象拷贝一次。(参考[这里](http://docs.python.org/2/faq/design.html#why-doesn-t-list-sort-return-the-sorted-list))。 26 | 27 | ```py 28 | some_list = [1, 2, 3] 29 | ``` 30 | 31 | **Output:** 32 | ```py 33 | >>>some_list2 = some_list.append(4) 34 | >>>some_list2 35 | None 36 | >>>some_list 37 | [1, 2, 3, 4] 38 | ``` 39 | 40 | --- -------------------------------------------------------------------------------- /individual/41.为什么要用`goto`?.md: -------------------------------------------------------------------------------- 1 | ### ▶ 为什么要用`goto`? * 2 | 3 | ```py 4 | from goto import goto, label 5 | for i in range(9): 6 | for j in range(9): 7 | for k in range(9): 8 | print("I'm trapped, please rescue!") 9 | if k == 2: 10 | goto .breakout #从一个深层循环内跳出 11 | label .breakout 12 | print("Freedom!") 13 | ``` 14 | 15 | **Output (Python 2.3):** 16 | ```py 17 | I'm trapped, please rescue! 18 | I'm trapped, please rescue! 19 | Freedom! 20 | ``` 21 | 22 | #### 💡 解释: 23 | - `goto`模块是作为2004年的愚人节(2004.4.1)玩笑[发布](https://mail.python.org/pipermail/python-announce-list/2004-April/002982.html)的,只有Python 2.3版本有`goto`模块。 24 | - 当前版本的Python是没有这个模块的(如果你用的是conda甚至不能下载到2.3版本的Python)。 25 | - 虽然上面版本可以使用`goto`,但是也请不要使用它。[这里](https://docs.python.org/3/faq/design.html#why-is-there-no-goto)是为什么当前版本没有`goto`模块的原因。 26 | 27 | --- -------------------------------------------------------------------------------- /individual/36.作用域之外的变量.md: -------------------------------------------------------------------------------- 1 | ### ▶ 作用域之外的变量 2 | 3 | ```py 4 | a = 1 5 | def some_func(): 6 | return a 7 | 8 | def another_func(): 9 | a += 1 10 | return a 11 | ``` 12 | 13 | **Output:** 14 | ```py 15 | >>> some_func() 16 | 1 17 | >>> another_func() 18 | UnboundLocalError: local variable 'a' referenced before assignment 19 | ``` 20 | 21 | #### 💡 解释: 22 | * 当你在某个作用域给一个变量赋值的时候,这个变量就会自动变成这个作用域的变量。所以`a`会变成`another_func`这个函数的局部变量,但是又没有在这个函数作用域内正确的初始化`a`变量,结果就发生了异常。 23 | * 这篇[文章](http://sebastianraschka.com/Articles/2014_python_scope_and_namespaces.html)很好的解释了命名空间和作用域是如何在Python中起作用的(是英文版,等有时间我会翻译一下,如果有热心的同学帮忙就更好啦!)。 24 | * 如果想在局部作用域内使用全局作用域的变量`a`,需要使用`global`关键字。 25 | ```py 26 | def another_func() 27 | global a 28 | a += 1 29 | return a 30 | ``` 31 | 32 | **Output:** 33 | ```py 34 | >>> another_func() 35 | 2 36 | ``` 37 | 38 | --- -------------------------------------------------------------------------------- /individual/2.不变的哈希值.md: -------------------------------------------------------------------------------- 1 | ### ▶ 不变的哈希值 2 | 3 | 1\. 4 | ```py 5 | some_dict = {} 6 | some_dict[5.5] = "Ruby" 7 | some_dict[5.0] = "JavaScript" 8 | some_dict[5] = "Python" 9 | ``` 10 | 11 | **Output:** 12 | ```py 13 | >>> some_dict[5.5] 14 | "Ruby" 15 | >>> some_dict[5.0] 16 | "Python" 17 | >>> some_dict[5] 18 | "Python" 19 | ``` 20 | "Python" 把之前的 "JavaScript" 覆盖掉了吗? 21 | 22 | #### :bulb: 解释 23 | 24 | * Python的字典结构是根据key值的哈希值判断两个key值是否相等的 25 | * 在Python中,不变对象(Immutable objects)的值如果一样,那么它们的哈希值肯定也一样 26 | ```py 27 | >>> 5 == 5.0 28 | True 29 | >>> hash(5) == hash(5.0) 30 | True 31 | ``` 32 | **注意:** 有些对象有不同的值,但是它们的哈希值也有可能是一样的(所谓的哈希冲突) 33 | * 当`some_dict[5] = "Python"`这句话执行的时候, "Python"这个字符串就会覆盖掉"JavaScript"这个值,因为在Python看来,`5`和`5.0`的哈希值是一样的,也就是说对于字典结构他们对应的是一个key值。 34 | * 在 StackOverflow 上面有一个[回答](https://stackoverflow.com/a/32211042/4354153)对Python的这个特性解释的很棒。 35 | 36 | --- -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 欢迎所有的PR(Pull request)。 如果有任何有意思或者更合适的标题可以替换当前例子的现有标题,欢迎随时联系我或者提[issues](https://github.com/true1023/Crazy-Python/issues)告诉我。 2 | 3 | 我的目标是让这个仓库里的例子尽可能的更加易读并且更加有趣。 4 | 5 | 如果你有一个跟重要的更新建议,希望你可以先提交一个[issues](https://github.com/true1023/Crazy-Python/issues), 讨论后再提交一个PR。 6 | 7 | 如果你准备为这个仓库增加一个新的例子,请先把例子提交到[issues](https://github.com/true1023/Crazy-Python/issues),等大家讨论后在提交PR。 8 | 9 | 你可以使用下面的模板来增加新的例子: 10 | 11 |
12 |  ### ▶ 这是一个标题 *
13 |  首先是例子的标题,如果某个标题后面带有星号,说明这一段是最新的一个版本加上的。
14 |  ```py
15 |  # 介绍会在这里写一些初始化代码
16 |  # 为下面的神奇时刻做准备...
17 |  ```
18 | 
19 |  **输出 (Python version):**
20 |  ```py
21 |  >>> python语句,执行某个命令
22 |  一些神奇的输出
23 |  ```
24 |  (可选): 有可能会介绍一下输出的内容
25 | 
26 | 
27 |  #### 💡 解释:
28 | 
29 |  * 简短的介绍发生了什么和为什么会产生这些输出。
30 |    ```py
31 |    写一些初始化代码
32 |    ```
33 |    **Output:**
34 |    ```py
35 |    >>> 触发相应代码 # 这些代码会揭示为何会有上方那些神奇的输出内容
36 |    ```
37 | 
38 | -------------------------------------------------------------------------------- /individual/19.不可修改的元组.md: -------------------------------------------------------------------------------- 1 | ### ▶ 不可修改的元组 2 | 3 | ```py 4 | some_tuple = ("A", "tuple", "with", "values") 5 | another_tuple = ([1, 2], [3, 4], [5, 6]) 6 | ``` 7 | 8 | **Output:** 9 | ```py 10 | >>> some_tuple[2] = "change this" 11 | TypeError: 'tuple' object does not support item assignment 12 | >>> another_tuple[2].append(1000) #这里并没有报错 13 | >>> another_tuple 14 | ([1, 2], [3, 4], [5, 6, 1000]) 15 | >>> another_tuple[2] += [99, 999] 16 | TypeError: 'tuple' object does not support item assignment 17 | >>> another_tuple 18 | ([1, 2], [3, 4], [5, 6, 1000, 99, 999]) 19 | ``` 20 | 21 | 我还以为元组是不可以修改的呢...... 22 | 23 | #### 💡 解释: 24 | 25 | * 引用自 https://docs.python.org/2/reference/datamodel.html 26 | > 不可变序列 27 | 一个不可变序列对象在第一次定义后就不再可以修改。(如果这个对象中包含有对其他对象的引用,这些其他对象有可能是可变的或者可修改的;总之,只有直接引用的对象是不可以修改的。) 28 | * `+=`操作符会对左侧操作对象本身进行修改。但是元组是不允许修改的,所以会报错,但是实际上修改的是一个可变的列表(list),元组里保存的只是这个列表的地址,所以最终还是会修改成功。 29 | 30 | --- -------------------------------------------------------------------------------- /individual/8.三子棋之一步取胜法.md: -------------------------------------------------------------------------------- 1 | ### ▶ 三子棋之一步取胜法 2 | 3 | ```py 4 | # 首先先来初始化一个1*3的一维数组 5 | row = [""]*3 #row i['', '', ''] 6 | # 然后再用二维数组模拟一个3*3的棋盘 7 | board = [row]*3 8 | ``` 9 | 10 | **Output:** 11 | ```py 12 | >>> board 13 | [['', '', ''], ['', '', ''], ['', '', '']] 14 | >>> board[0] 15 | ['', '', ''] 16 | >>> board[0][0] 17 | '' 18 | >>> board[0][0] = "X" 19 | >>> board 20 | [['X', '', ''], ['X', '', ''], ['X', '', '']] 21 | ``` 22 | 23 | 我们只赋值了一个“X”为什么会出来三个呢? 24 | 25 | #### 💡 解释: 26 | 27 | 当我们初始化`row`变量的时候,下图显示的是内存中的变化 28 | 29 | ![image](./assets/1=3/after_row_initialized.png) 30 | 31 | 接着当变量`board`通过`[row]*3`初始化后,下图显示了内存的变化(其实最终每一个变量`board[0]`,`board[1]`,`board[2]`都引用了同一个`row`对象的内存地址) 32 | 33 | ![image](./assets/1=3/after_board_initialized.png) 34 | 35 | 我们可以通过不使用`row`变量来阻止这种情况的发生 36 | 37 | ```py 38 | >>> board = [['']*3 for _ in range(3)] 39 | >>> board[0][0] = "X" 40 | >>> board 41 | [['X', '', ''], ['', '', ''], ['', '', '']] 42 | ``` 43 | 44 | --- -------------------------------------------------------------------------------- /individual/28.到底哪里出错了呢?.md: -------------------------------------------------------------------------------- 1 | ### ▶ 到底哪里出错了呢? 2 | 3 | ```py 4 | def square(x): 5 | """ 6 | 通过加法运算计算平方的函数 7 | """ 8 | sum_so_far = 0 9 | for counter in range(x): 10 | sum_so_far = sum_so_far + x 11 | return sum_so_far 12 | ``` 13 | 14 | **Output (Python 2.x):** 15 | 16 | ```py 17 | >>> square(10) 18 | 10 19 | ``` 20 | 21 | 结果不应该是100吗? 22 | 23 | **注意:** 如果你不能复现上面例子的结果, 可以试试直接运行[mixed_tabs_and_spaces.py](/mixed_tabs_and_spaces.py) 这个文件。 24 | 25 | #### 💡 解释 26 | 27 | * **不要把空格和制表符混在一起使用!** 例子中`return`语句前面的是一个用制表符"tab"来缩进的,代码的其他地方都是使用“四个空格”作为缩进的。 28 | * Python是这样处理制表符的: 29 | > 首先,制表符(tabs)会被依次替换成(从左至右)1到8个不等的空格直到替换后的空格数满足8的倍数......[出自Python文档](https://docs.python.org/2.0/ref/indentation.html) 30 | * 所以在`square`这个函数最后这个制表符"tab"就被替换成了8个空格,八个空格就是两个缩进,所以就等于把`return`语句缩进到了`for`循环内。 31 | * 这种情况在Python 3中就会直接报错。 32 | 33 | **Output (Python 3.x):** 34 | ```py 35 | TabError: inconsistent use of tabs and spaces in indentation 36 | ``` 37 | 38 | --- -------------------------------------------------------------------------------- /individual/45.这些语句是存在的!.md: -------------------------------------------------------------------------------- 1 | ### ▶ 这些语句是存在的! 2 | 3 | **循环语句对应的`else`语句.** 下面是一个标准的例子: 4 | 5 | ```py 6 | def does_exists_num(l, to_find): 7 | for num in l: 8 | if num == to_find: 9 | print("Exists!") 10 | break 11 | else: 12 | print("Does not exist") 13 | ``` 14 | 15 | **Output:** 16 | ```py 17 | >>> some_list = [1, 2, 3, 4, 5] 18 | >>> does_exists_num(some_list, 4) 19 | Exists! 20 | >>> does_exists_num(some_list, -1) 21 | Does not exist 22 | ``` 23 | 24 | **异常处理对应的`else`语句** 看例子, 25 | 26 | ```py 27 | try: 28 | pass 29 | except: 30 | print("Exception occurred!!!") 31 | else: 32 | print("Try block executed successfully...") 33 | ``` 34 | 35 | **Output:** 36 | ```py 37 | Try block executed successfully... 38 | ``` 39 | 40 | #### 💡 解释: 41 | - 当一个`else`语句紧跟在一个循环语句后面的时候,只有当循环语句块内有明确使用`break`退出,否则所有循环结束就会执行`else`语句块。 42 | - 跟在`try`语句块后面的`else`语句,又叫做“完成语句(completion clause)”。意味着如果`try`语句块的内容顺利运行完毕,那么就会进入到`else`语句块。 43 | 44 | --- -------------------------------------------------------------------------------- /individual/4.鸠占鹊巢.md: -------------------------------------------------------------------------------- 1 | ### ▶ 鸠占鹊巢 * 2 | 3 | ```py 4 | class Crazy: 5 | pass 6 | ``` 7 | 8 | **Output:** 9 | ```py 10 | >>> Crazy() == Crazy() # 两个类实例是不同的 11 | False 12 | >>> Crazy() is Crazy() # 它们的id号也是不一样的 13 | False 14 | >>> hash(Crazy()) == hash(Crazy()) # 它们的哈希值按说也应该不一样 15 | True 16 | >>> id(Crazy()) == id(Crazy()) 17 | True 18 | ``` 19 | 20 | #### 💡 解释: 21 | 22 | * 当`id`函数被调用的时候,Python创建了一个`Crazy`类实例,然后把这个实例传给了`id`函数。然后`id`函数返回这个实例的"id"号(实际上就是这个实例在内存中的地址),接着这个实例就被丢弃并且销毁了。 23 | * 当我们紧接着再做一遍上面的步骤的时候,Python会把同一块内存空间分配给第二次创建的`Crazy`实例。又因为在CPython中`id`函数使用的是内存地址作为返回值,所以就会出现两个对象实例的id号相同的情况了。 24 | * 所以,"对象的id是唯一的"这句话有一个前提条件是"在这个对象的生命周期内"。当这个对象在内存被销毁以后,其他的对象就可以占用它之前所用的内存空间产生一样的id号。 25 | * 但是为什么上面的例子里`is`操作符却产生了`False`? 我们再看一个例子。 26 | ```py 27 | class Crazy(object): 28 | def __init__(self): print("I") 29 | def __del__(self): print("D") 30 | ``` 31 | 32 | **Output:** 33 | ```py 34 | >>> Crazy() is Crazy() 35 | I 36 | I 37 | D 38 | D 39 | False 40 | >>> id(Crazy()) == id(Crazy()) 41 | I 42 | D 43 | I 44 | D 45 | True 46 | ``` 47 | 现在你可以发现, 不同的调用方法会对实例销毁的时间产生不同的影响。 48 | 49 | --- -------------------------------------------------------------------------------- /individual/37.小心那些链式操作符.md: -------------------------------------------------------------------------------- 1 | ### ▶ 小心那些链式操作符 2 | 3 | ```py 4 | >>> (False == False) in [False] # 没问题,OK? 5 | False 6 | >>> False == (False in [False]) # 这个也OK对吧? 7 | False 8 | >>> False == False in [False] # 这个呢?是不是开始不对了? 9 | True 10 | 11 | >>> True is False == False 12 | False 13 | >>> False is False is False 14 | True 15 | 16 | >>> 1 > 0 < 1 17 | True 18 | >>> (1 > 0) < 1 19 | False 20 | >>> 1 > (0 < 1) 21 | False 22 | ``` 23 | 24 | #### 💡 解释: 25 | 26 | 按照 https://docs.python.org/2/reference/expressions.html#not-in 里面说的: 27 | 28 | > 如果 a, b, c, ..., y, z 这些是表达式,op1, op2, ..., opN 这些是比较符,那么 a op1 b op2 c ... y opN z 就等于 a op1 b and b op2 c and ... y opN z, 除非其中有些表达式最多只能计算一次。 29 | 30 | 这种模式运行在像 `a == b == c` 和 `0 <=x <= 100` 这种语句里就会产生很有意思的结果了,就像上边那些例子一样。 31 | 32 | * `False is False is False` 等于 `(False is False) and (False is False)`。 33 | * `True is False == False` 等于 `True is False and False == False` 而且第一部分 (`True is False`) 等于 `False`, 所以整个表达式结果就为 `False`。 34 | * `1 > 0 < 1` 等于 `1 > 0 and 0 < 1` 最后结果等于 `True`。 35 | * 表达式 `(1 > 0) < 1` 等于 `True < 1` 而且 36 | ```py 37 | >>> int(True) 38 | 1 39 | >>> True + 1 #这个跟例子没啥关系,就是想告诉你们布尔值也可以这样玩^_^ 40 | 2 41 | ``` 42 | 所以, `1 < 1`也等于`False` 43 | 44 | --- -------------------------------------------------------------------------------- /individual/30.删不掉的对象.md: -------------------------------------------------------------------------------- 1 | ### ▶ 删不掉的对象 * 2 | 3 | ```py 4 | class SomeClass: 5 | def __del__(self): 6 | print("Deleted!") 7 | ``` 8 | 9 | **Output:** 10 | 1\. 11 | ```py 12 | >>> x = SomeClass() 13 | >>> y = x 14 | >>> del x # 这里应该输出 "Deleted!" 15 | >>> del y 16 | Deleted! 17 | ``` 18 | 19 | 啧.....最后还是输出了的。你可能已经猜到了,我们第一次尝试删除`x`的时候,`__del__`方法并没有被调用。现在我们在上面例子的基础上再加几句代码。 20 | 21 | 2\. 22 | ```py 23 | >>> x = SomeClass() 24 | >>> y = x 25 | >>> del x 26 | >>> y # 检查y是否存在 27 | <__main__.SomeClass instance at 0x7f98a1a67fc8> 28 | >>> del y # 就像上一个例子一样,这里应该输出"Deleted!" 29 | >>> globals() # 嗯......好像并没有输出。让我们检查一下全局变量看看。 30 | Deleted! 31 | {'__builtins__': , 'SomeClass': , '__package__': None, '__name__': '__main__', '__doc__': None} 32 | ``` 33 | 34 | 好吧,最后还是输出了(好奇怪的说):confused: 35 | 36 | #### 💡 解释: 37 | + `del x`并不会直接调用`x.__del__()`。 38 | + 当调用`del x`的时候,Python会把所有x指向的对象的引用数减一(在这里就是前面实例化的对象`SomeClass()`),只有当这个对象的引用数为零的时候,才会调用`__del__()`。 39 | + 在第二个例子中,`y.__del__()`没有被调用时因为前面的语句(`>>> y`)引用了同一个对象(SomeClass()),增加了这个对象的引用数,所以在`del y`以后这个对象的引用数并没有变成零。 40 | + 调用`globals`会检查并销毁没用的引用,这时候针对上面对象(`SomeClass()`)的引用数就变成零了,"Deleted!"就被打印出来了。 41 | 42 | --- -------------------------------------------------------------------------------- /individual/6.时间的误会.md: -------------------------------------------------------------------------------- 1 | ### ▶ 时间的误会 2 | 3 | 1\. 4 | ```py 5 | array = [1, 8, 15] 6 | g = (x for x in array if array.count(x) > 0) 7 | array = [2, 8, 22] 8 | ``` 9 | 10 | **Output:** 11 | ```py 12 | >>> print(list(g)) 13 | [8] 14 | ``` 15 | 16 | 2\. 17 | 18 | ```py 19 | array_1 = [1,2,3,4] 20 | g1 = (x for x in array_1) 21 | array_1 = [1,2,3,4,5] 22 | 23 | array_2 = [1,2,3,4] 24 | g2 = (x for x in array_2) 25 | array_2[:] = [1,2,3,4,5] 26 | ``` 27 | 28 | **Output:** 29 | ```py 30 | >>> print(list(g1)) 31 | [1,2,3,4] 32 | 33 | >>> print(list(g2)) 34 | [1,2,3,4,5] 35 | ``` 36 | 37 | #### 💡 解释 38 | 39 | - 在[生成器](https://wiki.python.org/moin/Generators)表达式中,`in`语句会在声明阶段求值,但是条件判断语句(在这里是`array.count(x) > 0`)会在真正的运行阶段(runtime)求值。 40 | - 在生成器运行之前,`array`已经被重新赋值为`[2, 8, 22]`了,所以这个时候再用`count`函数判断`2`,`8`,`22`在原列表中的数量,只有`8`是数量大于`0`的,所以最后这个生成器只返回了一个`8`是符合条件的。 41 | - 在第二部分中,`g1`,`g2`输出结果不同,是因为对`array_1`和`array_2`的赋值方法不同导致的。 42 | - 在第一个例子中, `array_1`绑定了一个新的列表对象`[1,2,3,4,5]`(可以理解成`array_1`的指针指向了一个新的内存地址),而且在这之前,`in`语句已经在声明时就为`g1`绑定好了旧的列表对象`[1,2,3,4]`(这个就对象也没有随着新对象的赋值而销毁)。所以这时候`g1`和`array_1`是指向不同的对象地址的。 43 | - 在第二个例子中,由于切片化的赋值,`array_2`并没有绑定(指向)新对象,而是将旧的对象`[1,2,3,4]`更新(也就是说旧对象的内存地址被新对象占用了)成了新的对象`[1,2,3,4,5]`。所以,`g2`和`array_2`依旧同时指向一个地址,所以都更新成了新对象`[1,2,3,4,5]`。 44 | 45 | --- -------------------------------------------------------------------------------- /individual/9.没脑子的函数.md: -------------------------------------------------------------------------------- 1 | ### ▶ 没脑子的函数 2 | 3 | ```py 4 | funcs = [] 5 | results = [] 6 | for x in range(7): 7 | def some_func(): 8 | return x 9 | funcs.append(some_func) 10 | results.append(some_func()) 11 | 12 | funcs_results = [func() for func in funcs] 13 | ``` 14 | 15 | **Output:** 16 | ```py 17 | >>> results 18 | [0, 1, 2, 3, 4, 5, 6] 19 | >>> funcs_results 20 | [6, 6, 6, 6, 6, 6, 6] 21 | ``` 22 | 虽然我们每次把`some_func`函数加入到`funcs`列表里的时候`x`都不一样,但是`funcs`列表里的所有函数都返回了6. 23 | 24 | //下面这段代码也是这样 25 | 26 | ```py 27 | >>> powers_of_x = [lambda x: x**i for i in range(10)] 28 | >>> [f(2) for f in powers_of_x] 29 | [512, 512, 512, 512, 512, 512, 512, 512, 512, 512] 30 | ``` 31 | 32 | #### 💡 解释 33 | 34 | - 当我们在一个循环中定义一个函数,并且在函数体中用了循环中的变量时,这个函数只会绑定这个变量本身,并不会绑定当前变量循环到的值。所以最终所有在循环中定义的函数都会使用循环变量最后的值做计算。 35 | 36 | - 如果你想实现心中想的那种效果,可以把循环变量当做一个参数传递进函数体。**为什么这样可以呢?**因为这样在函数作用域内会重新定义一个变量,不是循环里面的那个变量了。 37 | 38 | ```py 39 | funcs = [] 40 | for x in range(7): 41 | def some_func(x=x): 42 | return x 43 | funcs.append(some_func) 44 | ``` 45 | 46 | **Output:** 47 | ```py 48 | >>> funcs_results = [func() for func in funcs] 49 | >>> funcs_results 50 | [0, 1, 2, 3, 4, 5, 6] 51 | ``` 52 | 53 | --- -------------------------------------------------------------------------------- /individual/5.神奇赋值法.md: -------------------------------------------------------------------------------- 1 | ### ▶ 神奇赋值法 2 | 3 | ```py 4 | some_string = "crazy" 5 | some_dict = {} 6 | for i, some_dict[i] in enumerate(some_string): 7 | pass 8 | ``` 9 | 10 | **Output:** 11 | ```py 12 | >>> some_dict # 一个带引索的字典被创建. 13 | {0: 'c', 1: 'r', 2: 'a', 3: 'z', 4: 'y'} 14 | ``` 15 | 16 | #### 💡 解释: 17 | 18 | * 一个 `for` 语句在[Python语法](https://docs.python.org/3/reference/grammar.html)中是这么定义的: 19 | ``` 20 | for_stmt: 'for' exprlist 'in' testlist ':' suite ['else' ':' suite] 21 | ``` 22 | `exprlist` 是一组被赋值的变量. 这就等于说这组变量在**每次迭代**开始的时候都会执行一次 `{exprlist} = {next_value}` 。 23 | 下面这个例子很好的解释了上面想要表达的意思: 24 | ```py 25 | for i in range(4): 26 | print(i) 27 | i = 10 28 | ``` 29 | 30 | **Output:** 31 | ``` 32 | 0 33 | 1 34 | 2 35 | 3 36 | ``` 37 | 38 | 是不是以为上面的循环就会执行一次? 39 | 40 | **💡 解释:** 41 | - 在上面这个循环中,`i=10`这个赋值语句不会整个迭代过程产生任何影响。因为在每次迭代开始前,迭代函数(在这里是`range(4)`)都会把下一次的值赋值给目标变量(在这里是`i`)。 42 | 43 | * 再来看上面的例子,`enumerate(some_string)`这个函数会在每次迭代的时候产生两个值,分别是`i`(一个从0开始的索引值)和一个字符(来自`some_string`的值)。然后这两个值会分别赋值给`i`和`some_dict[i]`。把刚才的循环展开来看就像是下面这样: 44 | ```py 45 | >>> i, some_dict[i] = (0, 'c') 46 | >>> i, some_dict[i] = (1, 'r') 47 | >>> i, some_dict[i] = (2, 'a') 48 | >>> i, some_dict[i] = (3, 'z') 49 | >>> i, some_dict[i] = (4, 'y') 50 | >>> some_dict 51 | ``` 52 | 53 | --- -------------------------------------------------------------------------------- /individual/24.神秘的键值转换.md: -------------------------------------------------------------------------------- 1 | ### ▶ 神秘的键值转换 * 2 | 3 | ```py 4 | class SomeClass(str): 5 | pass 6 | 7 | some_dict = {'s':42} 8 | ``` 9 | 10 | **Output:** 11 | ```py 12 | >>> type(list(some_dict.keys())[0]) 13 | str 14 | >>> s = SomeClass('s') 15 | >>> some_dict[s] = 40 16 | >>> some_dict # expected: Two different keys-value pairs 17 | {'s': 40} 18 | >>> type(list(some_dict.keys())[0]) 19 | str 20 | ``` 21 | 22 | #### 💡 解释: 23 | 24 | * 类`s`和字符串`s`的哈希值是一样的,因为`SomeClass`继承了`str`类的`__hash__`方法。 25 | * `SomeClass("s") == "s"` 等于`True`,因为`SomeClass`同样继承了`str`类的`__eq__`方法。 26 | * 因为两个对象的哈希值(hash)和值(value)全都相等,所以他们在字典的关键字(key)里是被认为相同的。 27 | * 如果想要打到预期的效果,我们可以重新定义`SomeClass`的`__eq__`方法 28 | ```py 29 | class SomeClass(str): 30 | def __eq__(self, other): 31 | return ( 32 | type(self) is SomeClass 33 | and type(other) is SomeClass 34 | and super().__eq__(other) 35 | ) 36 | 37 | # 当我们自定义 __eq__ 方法后, Python会停止 38 | # 自动继承 __hash__ 方法, 所以我们同样也需要定义它 39 | __hash__ = str.__hash__ 40 | 41 | some_dict = {'s':42} 42 | ``` 43 | 44 | **Output:** 45 | ```py 46 | >>> s = SomeClass('s') 47 | >>> some_dict[s] = 40 48 | >>> some_dict 49 | {'s': 40, 's': 42} 50 | >>> keys = list(some_dict.keys()) 51 | >>> type(keys[0]), type(keys[1]) 52 | (__main__.SomeClass, str) 53 | ``` 54 | 55 | --- -------------------------------------------------------------------------------- /individual/16.站错队的布尔型.md: -------------------------------------------------------------------------------- 1 | ### ▶ 站错队的布尔型 2 | 3 | 1\. 4 | ```py 5 | # 一个计算列表里布尔型和Int型数量的例子 6 | mixed_list = [False, 1.0, "some_string", 3, True, [], False] 7 | integers_found_so_far = 0 8 | booleans_found_so_far = 0 9 | 10 | for item in mixed_list: 11 | if isinstance(item, int): 12 | integers_found_so_far += 1 13 | elif isinstance(item, bool): 14 | booleans_found_so_far += 1 15 | ``` 16 | 17 | **Output:** 18 | ```py 19 | >>> booleans_found_so_far 20 | 0 21 | >>> integers_found_so_far 22 | 4 23 | ``` 24 | 25 | 2\. 26 | ```py 27 | another_dict = {} 28 | another_dict[True] = "JavaScript" 29 | another_dict[1] = "Ruby" 30 | another_dict[1.0] = "Python" 31 | ``` 32 | 33 | **Output:** 34 | ```py 35 | >>> another_dict[True] 36 | "Python" 37 | ``` 38 | 39 | 3\. 40 | ```py 41 | >>> some_bool = True 42 | >>> "crazy"*some_bool 43 | 'crazy' 44 | >>> some_bool = False 45 | >>> "crazy"*some_bool 46 | '' 47 | ``` 48 | 49 | #### 💡 解释: 50 | 51 | * 布尔型(Booleans)是 `int`类型的一个子类型(bool is instance of int in Python) 52 | ```py 53 | >>> isinstance(True, int) 54 | True 55 | >>> isinstance(False, int) 56 | True 57 | ``` 58 | 59 | * `True`的整形值是`1`,`False`的整形值是`0` 60 | ```py 61 | >>> True == 1 == 1.0 and False == 0 == 0.0 62 | True 63 | ``` 64 | 65 | * StackOverFlow有针对这个问题背后原理的[解答](https://stackoverflow.com/a/8169049/4354153)。 66 | 67 | --- -------------------------------------------------------------------------------- /individual/17.类属性与类实例属性.md: -------------------------------------------------------------------------------- 1 | ### ▶ 类属性与类实例属性 2 | 3 | 1\. 4 | ```py 5 | class A: 6 | x = 1 7 | 8 | class B(A): 9 | pass 10 | 11 | class C(A): 12 | pass 13 | ``` 14 | 15 | **Ouptut:** 16 | ```py 17 | >>> A.x, B.x, C.x 18 | (1, 1, 1) 19 | >>> B.x = 2 20 | >>> A.x, B.x, C.x 21 | (1, 2, 1) 22 | >>> A.x = 3 23 | >>> A.x, B.x, C.x 24 | (3, 2, 3) 25 | >>> a = A() 26 | >>> a.x, A.x 27 | (3, 3) 28 | >>> a.x += 1 29 | >>> a.x, A.x 30 | (4, 3) 31 | ``` 32 | 33 | 2\. 34 | ```py 35 | class SomeClass: 36 | some_var = 15 37 | some_list = [5] 38 | another_list = [5] 39 | def __init__(self, x): 40 | self.some_var = x + 1 41 | self.some_list = self.some_list + [x] 42 | self.another_list += [x] 43 | ``` 44 | 45 | **Output:** 46 | 47 | ```py 48 | >>> some_obj = SomeClass(420) 49 | >>> some_obj.some_list 50 | [5, 420] 51 | >>> some_obj.another_list 52 | [5, 420] 53 | >>> another_obj = SomeClass(111) 54 | >>> another_obj.some_list 55 | [5, 111] 56 | >>> another_obj.another_list 57 | [5, 420, 111] 58 | >>> another_obj.another_list is SomeClass.another_list 59 | True 60 | >>> another_obj.another_list is some_obj.another_list 61 | True 62 | ``` 63 | 64 | #### 💡 解释: 65 | 66 | * 类里的属性和类实例中的属性是作为一个字典列表对象进行处理的。如果在当前类中没有找到需要调用的属性名,那么就会递归去父类中寻找。 67 | * `+=`操作符会在原有易变类型对象的基础上做改变而不会重新创建一个新的对象。所以在一个类实例中修改属性值,会影响到所有调用这个属性的相关类实例和类。 68 | 69 | --- -------------------------------------------------------------------------------- /individual/33.小心那些有默认值的可变参数!.md: -------------------------------------------------------------------------------- 1 | ### ▶ 小心那些有默认值的可变参数! 2 | 3 | ```py 4 | def some_func(default_arg=[]): 5 | default_arg.append("some_string") 6 | return default_arg 7 | ``` 8 | 9 | **Output:** 10 | ```py 11 | >>> some_func() 12 | ['some_string'] 13 | >>> some_func() 14 | ['some_string', 'some_string'] 15 | >>> some_func([]) 16 | ['some_string'] 17 | >>> some_func() 18 | ['some_string', 'some_string', 'some_string'] 19 | ``` 20 | 21 | #### 💡 解释: 22 | 23 | - 在Python中,如果参数有默认值并且是一个可变对象,那么这个默认的对象会一直存在,并不会随着每次创建实例而重新生成一个新的对象。当我们显式的把`[]`对象传递给`some_func`函数的时候,`default_arg`这个参数的默认值就不会被使用而被新对象替代了,所以这个函数会按照我们预期的那样返回值。 24 | 25 | ```py 26 | def some_func(default_arg=[]): 27 | default_arg.append("some_string") 28 | return default_arg 29 | ``` 30 | 31 | **Output:** 32 | ```py 33 | >>> some_func.__defaults__ #__defaults__会把这个函数的默认参数都打印出来 34 | ([],) 35 | >>> some_func() 36 | >>> some_func.__defaults__ 37 | (['some_string'],) 38 | >>> some_func() 39 | >>> some_func.__defaults__ 40 | (['some_string', 'some_string'],) 41 | >>> some_func([]) 42 | >>> some_func.__defaults__ 43 | (['some_string', 'some_string'],) 44 | ``` 45 | 46 | - 一个常用的用来避免上述bug的做法是这样的,把默认可变参数赋值成`None`,然后在函数体中检查这个参数是否被赋值,如果没有就赋一个默认值: 47 | 48 | ```py 49 | def some_func(default_arg=None): 50 | if not default_arg: 51 | default_arg = [] 52 | default_arg.append("some_string") 53 | return default_arg 54 | ``` 55 | 56 | --- -------------------------------------------------------------------------------- /individual/31.删除正在迭代中的列表项.md: -------------------------------------------------------------------------------- 1 | ### ▶ 删除正在迭代中的列表项 2 | 3 | ```py 4 | list_1 = [1, 2, 3, 4] 5 | list_2 = [1, 2, 3, 4] 6 | list_3 = [1, 2, 3, 4] 7 | list_4 = [1, 2, 3, 4] 8 | 9 | for idx, item in enumerate(list_1): 10 | del item 11 | 12 | for idx, item in enumerate(list_2): 13 | list_2.remove(item) 14 | 15 | for idx, item in enumerate(list_3[:]): 16 | list_3.remove(item) 17 | 18 | for idx, item in enumerate(list_4): 19 | list_4.pop(idx) 20 | ``` 21 | 22 | **Output:** 23 | ```py 24 | >>> list_1 25 | [1, 2, 3, 4] 26 | >>> list_2 27 | [2, 4] 28 | >>> list_3 29 | [] 30 | >>> list_4 31 | [2, 4] 32 | ``` 33 | 34 | 知道为什么输出是`[2, 4]`吗? 35 | 36 | #### 💡 解释: 37 | 38 | * 首先,我是不赞成在迭代的过程中修改迭代参数的(就像上面例子里那样)。正确的方法应该是先把迭代的列表拷贝一份,拿拷贝后的列表做迭代,就像上面第三个迭代做的那样(`list_3[:]`)。 39 | 40 | ```py 41 | >>> some_list = [1, 2, 3, 4] 42 | >>> id(some_list) 43 | 139798789457608 44 | >>> id(some_list[:]) # 注意如果用切片法的话,Python会新建一个list对象。 45 | 139798779601192 46 | ``` 47 | 48 | 49 | **`del`,`remove`和`pop`的区别:** 50 | * `del var_name` 只会移除本地或者全局命名空间中对`var_name`这个变量的绑定(这就是为什么`list_1`迭代后没有任何改变,因为`list_1`这个变量并没有收到影响)。 51 | * `remove` 会根据值(value)进行匹配并移除第一个匹配上的元素,若没有匹配的元素则抛出`ValueError`异常。 52 | * `pop` 会移除指定索引(index)的元素并且返回这个元素,如果指定的元素不存在则抛出`IndexError`异常。 53 | 54 | **所以为什么会输出`[2, 4]`呢?** 55 | - 首先列表迭代是根据索引值一个一个迭代的,所以当我们把`1`这个元素从`list_2`或者`list_4`中移除了以后,这两个列表就变成了`[2, 3, 4]`。剩下的这几个元素会依次向左移填补刚刚删除了的元素的位置,也就是说,`2`这个元素现在的索引值(index)变成了0,`3`的索引值变成了1。接着在下次迭代的时候,`for`循环访问的索引值已经变成了1(也就是列表里`3`这个元素),所以`2`这个元素就被跳过去(没有删除)。同样的情况每迭代一次都会发生一次。 56 | 57 | --- -------------------------------------------------------------------------------- /individual/25.看你能不能猜到这个的结果?.md: -------------------------------------------------------------------------------- 1 | ### ▶ 看你能不能猜到这个的结果? 2 | 3 | ```py 4 | a, b = a[b] = {}, 5 5 | ``` 6 | 7 | **Output:** 8 | ```py 9 | >>> a 10 | {5: ({...}, 5)} 11 | ``` 12 | 13 | #### 💡 解释: 14 | 15 | * 根据Python的赋值语句的[文档](https://docs.python.org/3.7/reference/simple_stmts.html#assignment-statements),赋值语句有如下的格式 16 | ``` 17 | (target_list "=")+ (expression_list | yield_expression) 18 | ``` 19 | 并且 20 | > 一个赋值语句会对表达式列表(expression_list)进行求值(这个可以只有一个表达式,也可以多个表达式用逗号分开组成表达式列表,后者最终会表现为元组的形式)并且会将单次的求值结果对象依次从左至右赋值给目标列表(target_list)。 21 | 22 | * 在`(target_list "=")+`这个表达式中`+`意味着 **一个或者多个** 目标列表。这上面这个例子中,目标列表是`a,b`和`a[b]`(注意表达式列表永远只有一个,在我们这个例子里是`{}, 5`)。 23 | 24 | * 当表达式计算出来后,它的值会被 **从左至右** 依次赋值给目标列表。所以,在上面的例子里,第一次赋值会把`{}, 5`这个元组赋值给`a, b`这个目标列表,这个时候`a = {}` 并且 `b = 5`。 25 | 26 | * 现在`a`被赋值成了`{}`,一个可变对象(mutable object)。 27 | 28 | * 第二个目标列表是`a[b]`(你可能会认为这种写法会报错,因为我们没有之前并没有定义`a`和`b`,但是没关系,我们刚刚已经给`a`和`b`分别赋值了`{}`和`5`)。 29 | 30 | * 现在我们会把元组`({}, 5)`赋值给字典关键为`5`的字典对象(也就是`a[5]`),同时也因此创建了一个循环引用(circular reference)(`{...}`在输出中代表同一个对象`a`已经被引用了)。下面是一个关于引用循环简单点的例子 31 | 32 | ```py 33 | >>> some_list = some_list[0] = [0] 34 | >>> some_list 35 | [[...]] 36 | >>> some_list[0] 37 | [[...]] 38 | >>> some_list is some_list[0] 39 | True 40 | >>> some_list[0][0][0][0][0][0] == some_list 41 | True 42 | ``` 43 | 在我们的例子用也是同样的情况(`a[b][0]`和`a`是指向同一个对象) 44 | 45 | * 所以总结一下,我们可以把上面的列子像如下这样拆分 46 | ```py 47 | a, b = {}, 5 48 | a[b] = a, b 49 | ``` 50 | 并且通过`a[b][0]`和`a`是同一个对象来证明确实存在引用循环 51 | ```py 52 | >>> a[b][0] is a 53 | True 54 | ``` 55 | 56 | --- -------------------------------------------------------------------------------- /individual/20.消失的变量e.md: -------------------------------------------------------------------------------- 1 | ### ▶ 消失的变量e 2 | 3 | ```py 4 | e = 7 5 | try: 6 | raise Exception() 7 | except Exception as e: 8 | pass 9 | ``` 10 | 11 | **Output (Python 2.x):** 12 | ```py 13 | >>> print(e) 14 | # 什么都没有打印 15 | ``` 16 | 17 | **Output (Python 3.x):** 18 | ```py 19 | >>> print(e) 20 | NameError: name 'e' is not defined 21 | ``` 22 | 23 | #### 💡 解释: 24 | 25 | * 参考自: https://docs.python.org/3/reference/compound_stmts.html#except 26 | 27 | 当一个异常(exception)被用`as`赋值给一个变量后,这个变量将会在`except`语句块结束后清除。看下面这段 28 | 29 | ```py 30 | except E as N: 31 | foo 32 | ``` 33 | 34 | 相当于这样 35 | 36 | ```py 37 | except E as N: 38 | try: 39 | foo 40 | finally: 41 | del N 42 | ``` 43 | 44 | 这意味着你最好不要在外面定义一个和"N"重名的变量。这些被赋值的异常变量被清除是因为,Python会将这些变量加入一个异常回溯栈中(traceback,仔细观察每次程序出错控制台输出的错误信息就是从tranceback这个堆栈里面以此取出来的),记录这些异常的具体位置信息并且一直保持这个堆栈的信息直到下一次垃圾回收开始。 45 | 46 | * 这些Python的子句并没有独立的作用域,上面所有的例子都在一个作用域里,所以当变量`e`在`except`子句里面被移除后,就相当于在整个作用域都消失了。但是在函数中情况就和上面不一样了,因为Python函数有自己的“内部”作用域。下面这个例子将说明这种情况: 47 | 48 | ```py 49 | def f(x): 50 | del(x) 51 | print(x) 52 | 53 | x = 5 54 | y = [5, 4, 3] 55 | ``` 56 | 57 | **Output:** 58 | ```py 59 | >>>f(x) 60 | UnboundLocalError: local variable 'x' referenced before assignment 61 | >>>f(y) 62 | UnboundLocalError: local variable 'x' referenced before assignment 63 | >>> x 64 | 5 65 | >>> y 66 | [5, 4, 3] 67 | ``` 68 | 69 | * 在 Python 2.x 版本里面, 变量`e`会被赋值`Exception()`实例,所以当打印的时候什么都不会打印出来。 70 | 71 | **Output (Python 2.x):** 72 | ```py 73 | >>> e 74 | Exception() 75 | >>> print e 76 | # 什么都没有打印出来 77 | ``` 78 | 79 | --- -------------------------------------------------------------------------------- /individual/34.抓住这些异常!.md: -------------------------------------------------------------------------------- 1 | ### ▶ 抓住这些异常! 2 | 3 | ```py 4 | some_list = [1, 2, 3] 5 | try: 6 | # 这里应该会抛出一个 ``IndexError`` 异常 7 | print(some_list[4]) 8 | except IndexError, ValueError: 9 | print("Caught!") 10 | 11 | try: 12 | # 这里会抛出一个 ``ValueError`` 异常 13 | some_list.remove(4) 14 | except IndexError, ValueError: 15 | print("Caught again!") 16 | ``` 17 | 18 | **Output (Python 2.x):** 19 | ```py 20 | Caught! 21 | 22 | ValueError: list.remove(x): x not in list 23 | ``` 24 | 25 | **Output (Python 3.x):** 26 | ```py 27 | File "", line 3 28 | except IndexError, ValueError: 29 | ^ 30 | SyntaxError: invalid syntax 31 | ``` 32 | 33 | #### 💡 解释 34 | 35 | * 如果想在`except`语句中同时捕获多个异常类型,需要把这些类型写成元组(tuple)的形式放在第一个参数的位置。第二个参数是一个可选参数,用来绑定具体捕获异常后的对象。比如, 36 | ```py 37 | some_list = [1, 2, 3] 38 | try: 39 | # 这里会产生一个 ``ValueError`` 异常 40 | some_list.remove(4) 41 | except (IndexError, ValueError), e: 42 | print("Caught again!") 43 | print(e) 44 | ``` 45 | **Output (Python 2.x):** 46 | ``` 47 | Caught again! 48 | list.remove(x): x not in list 49 | ``` 50 | **Output (Python 3.x):** 51 | ```py 52 | File "", line 4 53 | except (IndexError, ValueError), e: 54 | ^ 55 | IndentationError: unindent does not match any outer indentation level 56 | ``` 57 | 58 | * 在Python 3中已经弃用了使用逗号分隔异常类型和异常变量了;而是要使用`as`语法了。比如, 59 | ```py 60 | some_list = [1, 2, 3] 61 | try: 62 | some_list.remove(4) 63 | 64 | except (IndexError, ValueError) as e: 65 | print("Caught again!") 66 | print(e) 67 | ``` 68 | **Output:** 69 | ``` 70 | Caught again! 71 | list.remove(x): x not in list 72 | ``` 73 | 74 | --- -------------------------------------------------------------------------------- /individual/32.被泄露出去的循环变量!.md: -------------------------------------------------------------------------------- 1 | ### ▶ 被泄露出去的循环变量! 2 | 3 | 1\. 4 | ```py 5 | for x in range(7): 6 | if x == 6: 7 | print(x, ': for x inside loop') 8 | print(x, ': x in global') 9 | ``` 10 | 11 | **Output:** 12 | ```py 13 | 6 : for x inside loop 14 | 6 : x in global 15 | ``` 16 | 17 | 但是我从来没有在循环外定义一个变量`x`呀! 18 | 19 | 2\. 20 | ```py 21 | # 这回我先初始化一个x变量 22 | x = -1 23 | for x in range(7): 24 | if x == 6: 25 | print(x, ': for x inside loop') 26 | print(x, ': x in global') 27 | ``` 28 | 29 | **Output:** 30 | ```py 31 | 6 : for x inside loop 32 | 6 : x in global 33 | ``` 34 | 35 | 3\. 36 | ``` 37 | x = 1 38 | print([x for x in range(5)]) 39 | print(x, ': x in global') 40 | ``` 41 | 42 | **Output (Python 2.x):** 43 | ``` 44 | [0, 1, 2, 3, 4] 45 | (4, ': x in global') 46 | ``` 47 | 48 | **Output (Python 3.x):** 49 | ``` 50 | [0, 1, 2, 3, 4] 51 | 1 : x in global 52 | ``` 53 | 54 | #### 💡 解释: 55 | 56 | - 在Python里, for循环有它自己的作用域,但是在循环结束后,不会销毁那些循环变量。就算我们提前在全局作用域里定义了变量(就像上面那种情况),也会被循环变量覆盖。 57 | 58 | - 关于为什么在Python2和Python3中有不同的输出,可以在Python3的文档([What's New In Python 3.0](https://docs.python.org/3/whatsnew/3.0.html))中找到答案,下面会贴出原文: 59 | 60 | >"List comprehensions no longer support the syntactic form `[... for var in item1, item2, ...]`. Use `[... for var in (item1, item2, ...)]` instead. Also, note that list comprehensions have different semantics: they are closer to syntactic sugar for a generator expression inside a `list()` constructor, and in particular the loop control variables are no longer leaked into the surrounding scope." 61 | 62 | - 只用看最后一句话就够了**in particular the loop control variables are no longer leaked into the surrounding scope**,翻译过来是**特别是循环控制变量不会再泄露到包围着它的作用域以外了**,还有这个特性是针对构造列表(List comprehensions)说的,不包括正常的for循环。 63 | 64 | --- -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | -------------------------------------------------------------------------------- /individual/44.就算Python也知道爱是个复杂的东西.md: -------------------------------------------------------------------------------- 1 | ### ▶ 就算Python也知道爱是个复杂的东西 * 2 | 3 | ```py 4 | import this 5 | ``` 6 | 7 | Wait, what's **this**? `this` is love :heart: 8 | (译注:我认为这句话原汁原味的看才能体会其中的乐趣,就不翻译了😊) 9 | 10 | **Output:** 11 | ``` 12 | The Zen of Python, by Tim Peters 13 | 14 | Beautiful is better than ugly. 15 | Explicit is better than implicit. 16 | Simple is better than complex. 17 | Complex is better than complicated. 18 | Flat is better than nested. 19 | Sparse is better than dense. 20 | Readability counts. 21 | Special cases aren't special enough to break the rules. 22 | Although practicality beats purity. 23 | Errors should never pass silently. 24 | Unless explicitly silenced. 25 | In the face of ambiguity, refuse the temptation to guess. 26 | There should be one-- and preferably only one --obvious way to do it. 27 | Although that way may not be obvious at first unless you're Dutch. 28 | Now is better than never. 29 | Although never is often better than *right* now. 30 | If the implementation is hard to explain, it's a bad idea. 31 | If the implementation is easy to explain, it may be a good idea. 32 | Namespaces are one honking great idea -- let's do more of those! 33 | ``` 34 | 35 | 这些(指上面输出的内容)就是Python之道! 36 | 37 | ```py 38 | >>> love = this 39 | >>> this is love 40 | True 41 | >>> love is True 42 | False 43 | >>> love is False 44 | False 45 | >>> love is not True or False 46 | True 47 | >>> love is not True or False; love is love # 爱(love)是个复杂的东西,不是吗? 48 | True 49 | ``` 50 | 51 | #### 💡 解释: 52 | 53 | * `this`模块是一个针对Python之道做阐述的彩蛋([PEP 20](https://www.python.org/dev/peps/pep-0020))。 54 | * 但是这并没有完,如果你看看这个模块的实现代码[this.py](https://hg.python.org/cpython/file/c3896275c0f6/Lib/this.py)。你会发现一个更加有趣的事情,那就是这个代码本身就违反了上面输出的Python之道(当然,我觉得这也是唯一一处违反的地方)。 55 | * 关于`love is not True or False; love is love`这句话,它解释的很对,不是吗? 56 | 57 | --- -------------------------------------------------------------------------------- /individual/1.善变的字符串.md: -------------------------------------------------------------------------------- 1 | ### ▶ 善变的字符串 * 2 | 3 | 1\. 4 | ```py 5 | >>> a = "crazy_python" 6 | >>> id(a) 7 | 2387669241224 8 | >>> id("crazy" + "_" + "python") # 注意这两个字符串的id号是一样的 9 | 2387669241224 10 | ``` 11 | 12 | 2\. 13 | ```py 14 | >>> a = "crazy" 15 | >>> b = "crazy" 16 | >>> a is b 17 | True 18 | 19 | >>> a = "crazy!" 20 | >>> b = "crazy!" 21 | >>> a is b 22 | False 23 | 24 | >>> a, b = "crazy!", "crazy!" 25 | >>> a is b 26 | True 27 | ``` 28 | 29 | 3\. 30 | ```py 31 | >>> 'a' * 20 is 'aaaaaaaaaaaaaaaaaaaa' 32 | True 33 | >>> 'a' * 21 is 'aaaaaaaaaaaaaaaaaaaaa' 34 | False 35 | ``` 36 | 37 | 很不可思议,对吧? 38 | 39 | #### :bulb: 解释: 40 | + 上面这种特性是CPython的一种编译器优化技术(叫做字符串驻留技术)。就是如果将要创建的字符串在之前已经创建过并且驻留在了内存没有释放,那么CPython不会创建一个新的实例,而是会把指针指向已经存在于内存的老的字符串实例。 41 | + 如果一个字符串实例已经驻留在了内存中,那么后续所有跟它值一样的变量都可以将指针指向这个内存中的字符串实例(这样就会节省内存空间) 42 | + 在上面这几段程序代码中,有几段的字符串明显驻留在了内存中供多个变量引用。 决定一个字符串是否会驻留在内存中是由这个字符串的实现方法决定的。下面是一些判断字符串变量是否会驻留内存的方法: 43 | * 所有长度为1或者0的字符串,全部会驻留 44 | * 编译阶段(也就是把源代码编译成.pyc文件的阶段)的字符串被驻留在内存,运行阶段就不会(`'crazy'`会驻留在内存,但是`''.join(['c','r','a','z','y'])`就不会) 45 | * 如果字符串包含除了ASCII字符,数字和下划线以外的字符,那么这个字符串就不会驻留内存。这就是为什么`'crazy!'`赋值给`a`,`b`的时候得到的结果是False,因为`!`感叹号。CPython中对这个特点的具体实现请参考[这里](https://github.com/python/cpython/blob/3.6/Objects/codeobject.c#L19) 46 | 47 | + 当变量`a`和`b`在同一行赋值`"crazy!"`的时候,Python的解释器会创建一个新的字符串实例,然后把这两个变量同时指向这一个实例。但是如果你用两行实现赋值的话,Python解释器就不会知道已经有一个`'crazy!'`的字符串实例存在了(因为根据上面的规则,`'crazy!'`实例不会进行内存驻留供后面的语句引用)。这是一种专门在交互环境下编译器的一种优化方法。 48 | + 常量折叠是Python实现的一种[窥孔优化(Peephole optimization)](https://baike.baidu.com/item/%E7%AA%A5%E5%AD%94%E4%BC%98%E5%8C%96)技术。意思就是`'a'*20`这个语句在编译的时候会自动替换成`'aaaaaaaaaaaaaaaaaaaa'`这个变量,用来减少运行时的运算时钟周期(`'a'*20`需要多执行20次乘法运算)。常量折叠只在字符串长度小于等于20的时候发生(至于为什么?想想如果有一个`'a*10**10'`这样的语句,折叠后需要多大一个`.pyc`才能存下折叠后的字符串啊)。[这里](https://github.com/python/cpython/blob/3.6/Python/peephole.c#L288)是实现这种技术的实现代码。 49 | 50 | --- -------------------------------------------------------------------------------- /individual/51.最后一些小特点集合.md: -------------------------------------------------------------------------------- 1 | ### ▶ 最后一些小特点集合 2 | 3 | * `join()`是一个字符串的函数而不是一个列表附属函数(第一次用的人可能会觉得有点别扭) 4 | 5 | **💡 解释:** 6 | 如果`join()`作为一个字符串的方法,那么它就可以操作所有的可迭代类型(list, tuple, iterators)。但是如果它作为了一个列表(list)的方法,那它同样的也需要为另外那些可迭代类型构造专属的函数内容。 7 | 8 | * 下面这些句子看着奇怪,但是它们确实都是符合语法规则的 9 | + `[] = ()` 在语法上是正确的 (把一个元组赋值给一个列表) 10 | + `'a'[0][0][0][0][0]` 同样也是语法正确的,因为在Python中字符串是一个序列(可迭代的对象就可以使用索引值进行访问里面的元素)。 11 | + `3 --0-- 5 == 8`和`--5 == 5`也都是语法正确的,并且最后结果都为`True`。 12 | 13 | * 给定一个数字类型变量`a`,`++a`和`--a`在Python中都是合法的,但是它们表达的意思和在C语言(或者C++,Java)中是完全不一样的。 14 | ```py 15 | >>> a = 5 16 | >>> a 17 | 5 18 | >>> ++a 19 | 5 20 | >>> --a 21 | 5 22 | ``` 23 | 24 | **💡 解释:** 25 | + 在Python语法中,是没有`++`这个操作符的。这个仅仅代表两个单独的`+`操作符而已。 26 | + `++a`在Python中会被解析成`+(+a)`,也就等于`a`。同样的,`--a`也是这种情况。 27 | + 这篇StackOverflow的[文章](https://stackoverflow.com/questions/3654830/why-are-there-no-and-operators-in-python)讨论了Python中没有自增自减符号背后的逻辑关系。 28 | 29 | * 多个Python线程并不会并发执行你的*Python代码*(你没看错,不会!)。也许你想同时开很多个线程然后并发执行你写的Python代码,但是,由于Python中存在一个叫[GIL](https://wiki.python.org/moin/GlobalInterpreterLock)(Global Interpreter Lock)的东西,所以最终你的线程还是会一个接着一个执行。Python的线程执行IO阻塞的任务比较拿手,但是如果想在CPU阻塞的任务中获得真正的并发能力,你需要了解一下Python的[multiprocessing](https://docs.python.org/2/library/multiprocessing.html)模块。 30 | 31 | * 列表切片的时候如果索引值超出界限并不会报错 32 | ```py 33 | >>> some_list = [1, 2, 3, 4, 5] 34 | >>> some_list[111:] 35 | [] 36 | ``` 37 | 38 | * 在Python3中,`int('١٢٣٤٥٦٧٨٩')`会返回`123456789`。在Python中,十进制字符不仅包含十进制的数字,还包含了大部分可以表示为十进制数字形式的字符,比如,U+0660, ARABIC_INDIC DIGIT ZERO,一种印度阿拉伯语里代表数字0的字符。不过还没有中文的"一,二,三..."或者"壹,贰,叁..."这种,有人说是因为中文在unicode中的编码是不连续的,不过无论如何,现在确实还没有。这里有一篇[小故事](http://chris.improbable.org/2014/8/25/adventures-in-unicode-digits/)是关于这个特性的,英语版,有兴趣的同学可以看看。 39 | 40 | * `'abc'.count('') == 4`。 下面模拟实现了一个相似的`count()`方法, 41 | ```py 42 | def count(s, sub): 43 | result = 0 44 | for i in range(len(s) + 1 - len(sub)): 45 | result += (s[i:i + len(sub)] == sub) 46 | return result 47 | ``` 48 | 产生这个结果的原因是子串为空时会匹配上原始字符串长度为0时的字符(总之,看代码你会懂的)。 49 | 50 | --- -------------------------------------------------------------------------------- /individual/7.特殊的数字们.md: -------------------------------------------------------------------------------- 1 | ### ▶ 特殊的数字们 2 | 3 | 下面这个例子在网上非常的流行。 4 | 5 | ```py 6 | >>> a = 256 7 | >>> b = 256 8 | >>> a is b 9 | True 10 | 11 | >>> a = 257 12 | >>> b = 257 13 | >>> a is b 14 | False 15 | 16 | >>> a = 257; b = 257 17 | >>> a is b 18 | True 19 | ``` 20 | 21 | #### 💡 解释: 22 | 23 | **`is`和`==`的区别** 24 | 25 | * `is` 操作符会检查两边的操作数是否引用的是同一个对象(也就是说,会检查两个操作数的id号是否匹配)。 26 | * `==` 操作符会比较两个操作数的值是否一样。 27 | * 所以说`is`是比较引用地址是否相同,`==`是比较值是否相同。下面的例子解释的比较清楚, 28 | ```py 29 | >>> [] == [] 30 | True 31 | >>> [] is [] # 这里的两个空list分配了不同的内存地址 32 | False 33 | ``` 34 | 35 | **`256` 是一个已经存在于内存的对象 但是 `257` 不是** 36 | 37 | 当你启动一个Python解释器的时候,数字`-5`到`256`就会自动加载进内存。 这些数字都是一些比较常用的数字,所以Python解释器会把他们提前准备好以备以后使用。 38 | 39 | 下面这段话摘抄自 https://docs.python.org/3/c-api/long.html (已经翻译为中文) 40 | > The current implementation keeps an array of integer objects for all integers between -5 and 256, when you create an int in that range you just get back a reference to the existing object. So it should be possible to change the value of 1. I suspect the behavior of Python, in this case, is undefined.:-) 41 | > 42 | > 当前的实现方法是,维护一个从-5到256的整数数组,当你使用其中某一个数字的时候,系统会自动为你引用到已经存在的对象上去。我认为应该让它可以改变数字1的值。不过就现在来说,Python还没有这个功能。:-) 43 | 44 | ```py 45 | >>> id(256) 46 | 10922528 47 | >>> a = 256 48 | >>> b = 256 49 | >>> id(a) 50 | 10922528 51 | >>> id(b) 52 | 10922528 53 | >>> id(257) 54 | 140084850247312 55 | >>> x = 257 56 | >>> y = 257 57 | >>> id(x) 58 | 140084850247440 59 | >>> id(y) 60 | 140084850247344 61 | ``` 62 | 63 | Python解释器在执行`y = 257`的时候还不能意识到我们之前已经创建过了一个值为`257`的对象,所以它又在内存创建了一个新的对象。 64 | 65 | **当`a`和`b`变量在同一行赋值并且所赋值相等时,它们会引用到同一个对象** 66 | 67 | ```py 68 | >>> a, b = 257, 257 69 | >>> id(a) 70 | 140640774013296 71 | >>> id(b) 72 | 140640774013296 73 | >>> a = 257 74 | >>> b = 257 75 | >>> id(a) 76 | 140640774013392 77 | >>> id(b) 78 | 140640774013488 79 | ``` 80 | 81 | * 当a和b在同一行被赋值为`257`的时候, Python解释器会创建一个新的对象,然后把这两个变量同时引用到这个新的对象。如果你分开两行赋值两个变量,那么解释器不会“知道”之前自己已经创建过一个同样的`257`对象了。 82 | * 这是一种专门针对交互式解释器环境的优化机制。 当你在控制面板中敲入两行命令的时候,这两行命令是分开编译的,所以他们也会单独进行优化。如果你准备把这个例子(两行分别赋值的例子)写进`.py`文件然后进行测试,那么你会发现结果跟写在一行是一样的,因为文件里的代码是一次性编译的。 83 | 84 | --- -------------------------------------------------------------------------------- /individual/49.超长字符串!.md: -------------------------------------------------------------------------------- 1 | ### ▶ 超长字符串! 2 | 3 | ```py 4 | #使用加号连接字符串 5 | def add_string_with_plus(iters): 6 | s = "" 7 | for i in range(iters): 8 | s += "xyz" 9 | assert len(s) == 3*iters 10 | 11 | #使用比特流连接字符串 12 | def add_bytes_with_plus(iters): 13 | s = b"" 14 | for i in range(iters): 15 | s += b"xyz" 16 | assert len(s) == 3*iters 17 | 18 | #使用format函数连接字符串 19 | def add_string_with_format(iters): 20 | fs = "{}"*iters 21 | s = fs.format(*(["xyz"]*iters)) 22 | assert len(s) == 3*iters 23 | 24 | #使用数组连接字符串 25 | def add_string_with_join(iters): 26 | l = [] 27 | for i in range(iters): 28 | l.append("xyz") 29 | s = "".join(l) 30 | assert len(s) == 3*iters 31 | 32 | #将列表转换成字符串 33 | def convert_list_to_string(l, iters): 34 | s = "".join(l) 35 | assert len(s) == 3*iters 36 | ``` 37 | 38 | **Output:** 39 | ```py 40 | >>> timeit(add_string_with_plus(10000)) 41 | 1000 loops, best of 3: 972 µs per loop 42 | >>> timeit(add_bytes_with_plus(10000)) 43 | 1000 loops, best of 3: 815 µs per loop 44 | >>> timeit(add_string_with_format(10000)) 45 | 1000 loops, best of 3: 508 µs per loop 46 | >>> timeit(add_string_with_join(10000)) 47 | 1000 loops, best of 3: 878 µs per loop 48 | >>> l = ["xyz"]*10000 49 | >>> timeit(convert_list_to_string(l, 10000)) 50 | 10000 loops, best of 3: 80 µs per loop 51 | ``` 52 | 53 | 让我们把循环次数提升十倍。 54 | 55 | ```py 56 | >>> timeit(add_string_with_plus(100000)) # 执行时间线性增加 57 | 100 loops, best of 3: 9.75 ms per loop 58 | >>> timeit(add_bytes_with_plus(100000)) # 平方式增加 59 | 1000 loops, best of 3: 974 ms per loop 60 | >>> timeit(add_string_with_format(100000)) # 线性增加 61 | 100 loops, best of 3: 5.25 ms per loop 62 | >>> timeit(add_string_with_join(100000)) # 线性增加 63 | 100 loops, best of 3: 9.85 ms per loop 64 | >>> l = ["xyz"]*100000 65 | >>> timeit(convert_list_to_string(l, 100000)) # 线性增加 66 | 1000 loops, best of 3: 723 µs per loop 67 | ``` 68 | 69 | #### 💡 解释 70 | - 关于`timeit`模块的用法可以看[这里](https://docs.python.org/3/library/timeit.html)。这是一个用来测量程序块执行时间的模块。 71 | - 在Python中,不要试图用`+`来连接一个长字符串,因为`str`这个类型在Python中是一个不可变类型,这就意味着每次两个字符串对象连接程序都需要从左到右把两个字符串的每一个字符都复制一份。比如你现在要连接四个长度为10的字符串,那么程序需要拷贝 (10+10) + ((10+10)+10) + (((10+10)+10)+10) = 90 个字符,而不是40个。而且程序运行时间会随着你需要连接的字符串数量和字符数量呈平方形式的增长。(请参考`add_bytes_with_plus`函数的执行时间对比)。 72 | - 所以,建议大家使用`.format`或者`%`这种语法(但是,它们在连接短字符串的时候会比`+`号慢一点)。 73 | - 或者,如果已经提前获得了一个可以迭代的对象,则可以使用`''.join(iterable_object)`这种语法把它连接成字符串,这样的速度会比前几个都快。 74 | - 至于为什么`add_string_with_plus`不会像`add_bytes_with_plus`着这样成平方形式的提升执行时间,是因为前面例子里已经解释过的`+=`操作符的优化效果。如果把`s += "xyz"`这句话替换成了`s = s + "x" + "y" + "z"`,那么执行时间还是会呈平方形式的增加。 75 | ```py 76 | def add_string_with_plus(iters): 77 | s = "" 78 | for i in range(iters): 79 | s = s + "x" + "y" + "z" 80 | assert len(s) == 3*iters 81 | 82 | >>> timeit(add_string_with_plus(10000)) 83 | 100 loops, best of 3: 9.87 ms per loop 84 | >>> timeit(add_string_with_plus(100000)) # 平方式增加 85 | 1 loops, best of 3: 1.09 s per loop 86 | ``` 87 | 88 | --- -------------------------------------------------------------------------------- /individual/toc.md: -------------------------------------------------------------------------------- 1 | ![](../assets/logo.png) 2 | 3 | *一些有趣的鲜为人知的Python特性集合* 4 | 5 | >无论你是Python新手还是Python老手,我相信,这个系列的文章都会让你获益良多! 6 | > 7 | > 本系列为我在github上的一个项目,由于markdown语法不同所以在简书上做了一些改动并且按章节来发。如果中间有什么错误,请务必留言我会第一时间改正。 8 | 9 | --- 10 | 11 | Python作为一个设计优美的交互式脚本语言,提供了许多人性化的语法。但是也因为这个原因,有些Python的代码片段并不会按照用户想象的那样运行。 12 | 13 | 这一系列文章就让我们总结一下那些Python里反直觉的代码片段,并且深入研究一下其中的运行原理。 14 | 15 | 以后的某些例子可能并不像是标题说的那样....嗯....反直觉,但是它们依旧会带你揭示一些你从来没有意识到的Python语言特性。 16 | 17 | 而且,我发现这也是一种很好的学习编程语言的方法,不过前提是你不要认为这篇文章会告诉你一切,抛砖引玉而已。相信我,你需要的知识都隐藏在互联网的某个角落里。 18 | 19 | 如果你已经写了很久的Python代码,你可以把下面的这些例子当做一个挑战,试一试自己能不能在第一次就猜对结果。也许某些例子里的坑你已经遇到过并且解决了,那么再次看见这个坑的时候我想你会为当时自己的努力而自豪的。😄 20 | 21 | 好了,下面是整个系列的目录,每发布一篇那一篇的超链接就会变得可以点击。 22 | 23 | - [▶ 善变的字符串] 24 | - [▶ 不变的哈希值] 25 | - [▶ 说了要执行就一定会执行!] 26 | - [▶ 鸠占鹊巢] 27 | - [▶ 神奇赋值法] 28 | - [▶ 时间的误会] 29 | - [▶ 特殊的数字们] 30 | - [▶ 三子棋之一步取胜法] 31 | - [▶ 没脑子的函数] 32 | - [▶ `is not ...` 并不是 `is`] 33 | - [▶ 尾部的逗号] 34 | - [▶ 最后一个反斜杠] 35 | - [▶ 纠结的not] 36 | - [▶ 只剩一半的三引号] 37 | - [▶ 消失的午夜零点] 38 | - [▶ 站错队的布尔型] 39 | - [▶ 类属性与类实例属性] 40 | - [▶ None的产生] 41 | - [▶ 不可修改的元组] 42 | - [▶ 消失的变量e] 43 | - [▶ 亦真还假] 44 | - [▶ 转瞬即空] 45 | - [▶ 子类的关系] 46 | - [▶ 神秘的键值转换 *] 47 | - [▶ 看你能不能猜到这个的结果?] 48 | - [▶ 无效的一行?] 49 | - [▶ 移形换位] 50 | - [▶ 到底哪里出错了呢?] 51 | - [▶ 不要在迭代一个字典的时候修改这个字典!] 52 | - [▶ 删不掉的对象] 53 | - [▶ 删除正在迭代中的列表项] 54 | - [▶ 被泄露出去的循环变量!] 55 | - [▶ 小心那些有默认值的可变参数!] 56 | - [▶ 抓住这些异常!] 57 | - [▶ 相同的操作,不同的结果] 58 | - [▶ 作用域之外的变量] 59 | - [▶ 小心那些链式操作符] 60 | - [▶ 被忽略的类变量] 61 | - [▶ 小心处理元组] 62 | - [▶ 人生苦短,我用Python ] 63 | - [▶ 为什么要用`goto`?] 64 | - [▶ 试试用大括号?] 65 | - [▶ 不等号的争议 ] 66 | - [▶ 就算Python也知道爱是个复杂的东西 ] 67 | - [▶ 这些语句是存在的!] 68 | - [▶ 无限 ] 69 | - [▶ 被修改的类成员 ] 70 | - [▶ `+=`更快] 71 | - [▶ 超长字符串!] 72 | - [▶ 字符串到浮点数的转换] 73 | - [▶ 最后一些小特点集合] 74 | 75 | 76 | 77 | # 示例结构说明 78 | 79 | 下面是每个例子里通用的结构说明: 80 | 81 | > ### ▶ 这里是例子的标题 * 82 | > 首先是例子的标题。 83 | > ```py 84 | > # 第一个代码段里面会有一些初始化代码 85 | > # 为后续的输出代码段做准备... 86 | > ``` 87 | > 88 | > **Output (Python version):** 89 | > ```py 90 | > >>> python语句,执行某个命令 91 | > 一些输出(可能你想得到,也可能想不到) 92 | > ``` 93 | > (可选): 有可能会说明一下上面输出的内容 94 | > 95 | > 96 | > #### 💡 解释: 97 | > 98 | > * 简短的介绍发生了什么和为什么会产生这些输出。 99 | > ```py 100 | > 一些初始化代码 101 | > ``` 102 | > **Output:** 103 | > ```py 104 | > >>> 执行代码 # 这些代码会展示为何会有上方那些输出内容 105 | > ``` 106 | 107 | **注意:** 所有的代码都是在 Python 3.5.2 环境下测试通过,理论上如果没有特殊声明,可以在所有的Python版本下运行。 108 | 109 | # 使用方法 110 | 111 | 在我看来,为了充分的利用这个仓库里的所有例子,最好的办法就是按照顺序把每个例子挨个看一遍: 112 | - 仔细阅读每个例子的初始化代码。如果你是一个经验丰富的Python程序员,那么大部分时候你都可以知道初始化代码执行后具体会发生什么。 113 | - 阅读输出结果并且, 114 | + 检查输出结果是否和你想的一样 115 | + 确认你是否知道产生这种结果背后的原理, 116 | - 如果不知道,那么请仔细阅读解释章节(如果看完解释还是不懂的话,那就在下面评论吧) 117 | - 如果知道,那么给就自己点个赞,继续看下一个例子 118 | 119 | 120 | # 一些说明 121 | 122 | - 这个系列翻译自 [What the fu*k Python](https://github.com/satwikkansal/wtfpython) 123 | - 如果你有任何好的想法和意见,请务必要分享给我 124 | - 我会在收集并修正一些bug后生成pdf版本的Crazy Python,想要的小伙伴可以[发信](mailto:shi12li12@gmail.com)给我,或者关注公众号**编程行动派**,我也会第一时间在公众号里通知大家 125 | - 转载注明出处 126 | 127 | ![编程行动派](../assets/crazycoding_qr.jpg) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

疯狂的Python! 🐍

4 |

一些有趣的鲜为人知的Python特性集合.

5 | 6 | [![WTFPL](https://img.shields.io/badge/License-WTFPL%202.0-lightgrey.svg?style=flat-square)](http://www.wtfpl.net/) 7 | [![Join the chat at https://gitter.im/Crazy-Python/Lobby](https://badges.gitter.im/Crazy-Python/Lobby.svg)](https://gitter.im/Crazy-Python/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 8 | 9 | 10 | *本文翻译自[What the f*ck Python!](https://github.com/satwikkansal/wtfpython)* 11 | 12 | --- 13 | 14 | Python作为一个设计优美的交互式脚本语言,提供了许多人性化的语法。但是也因为这个原因,有些Python的代码片段并不会按照用户想象的那样运行。 15 | 16 | 这篇文章就让我们总结一下那些Python里反直觉的代码片段,并且深入研究一下其中的运行原理。 17 | 18 | 下面的某些例子可能并不像是标题说的那样....嗯....反直觉,但是它们依旧会带你揭示一些你从来没有意识到的Python语言特性。 19 | 20 | 而且,我发现这也是一种很好的学习编程语言的方法,不过前提是你不要认为这篇文章会告诉你一切,抛砖引玉而已。相信我,你需要的知识都隐藏在互联网的某个角落里。 21 | 22 | 如果你已经写了很久的Python代码,你可以把下面的这些例子当做一个挑战,试一试自己能不能在第一次就猜对结果。也许某些例子里的坑你已经遇到过并且解决了,那么再次看见这个坑的时候我想你会为当时自己的努力而自豪的。:sweat_smile: 23 | 24 | 好了,下面是正文! 25 | 26 |

目录

27 | 28 | 29 | - [示例结构说明](#示例结构说明) 30 | - [使用方法](#使用方法) 31 | - [:eyes: 例子](#eyes-例子) 32 | - [第一章: 撕裂大脑](#第一章-撕裂大脑) 33 | - [▶ 善变的字符串 *](#-善变的字符串-) 34 | - [▶ 不变的哈希值](#-不变的哈希值) 35 | - [▶ 说了要执行就一定会执行!](#-说了要执行就一定会执行) 36 | - [▶ 鸠占鹊巢 *](#-鸠占鹊巢-) 37 | - [▶ 神奇赋值法](#-神奇赋值法) 38 | - [▶ 时间的误会](#-时间的误会) 39 | - [▶ 特殊的数字们](#-特殊的数字们) 40 | - [▶ 三子棋之一步取胜法](#-三子棋之一步取胜法) 41 | - [▶ 没脑子的函数](#-没脑子的函数) 42 | - [▶ `is not ...` 并不是 `is (not ...)`](#-is-not--并不是--is-not-) 43 | - [▶ 尾部的逗号](#-尾部的逗号) 44 | - [▶ 最后一个反斜杠](#-最后一个反斜杠) 45 | - [▶ 纠结的not](#-纠结的not) 46 | - [▶ 只剩一半的三引号](#-只剩一半的三引号) 47 | - [▶ 消失的午夜零点](#-消失的午夜零点) 48 | - [▶ 站错队的布尔型](#-站错队的布尔型) 49 | - [▶ 类属性与类实例属性](#-类属性与类实例属性) 50 | - [▶ None的产生](#-none的产生) 51 | - [▶ 不可修改的元组](#-不可修改的元组) 52 | - [▶ 消失的变量e](#-消失的变量e) 53 | - [▶ 亦真还假](#-亦真还假) 54 | - [▶ 转瞬即空](#-转瞬即空) 55 | - [▶ 子类的关系 *](#-子类的关系-) 56 | - [▶ 神秘的键值转换 *](#-神秘的键值转换-) 57 | - [▶ 看你能不能猜到这个的结果?](#-看你能不能猜到这个的结果) 58 | - [第二章: 瞒天过海](#第二章-瞒天过海) 59 | - [▶ 无效的一行?](#-无效的一行) 60 | - [▶ 移形换位 *](#-移形换位-) 61 | - [▶ 到底哪里出错了呢?](#-到底哪里出错了呢) 62 | - [第三章: 隐藏的陷阱](#第三章-隐藏的陷阱) 63 | - [▶ 不要在迭代一个字典的时候修改这个字典!](#-不要在迭代一个字典的时候修改这个字典) 64 | - [▶ 删不掉的对象 *](#-删不掉的对象-) 65 | - [▶ 删除正在迭代中的列表项](#-删除正在迭代中的列表项) 66 | - [▶ 被泄露出去的循环变量!](#-被泄露出去的循环变量) 67 | - [▶ 小心那些有默认值的可变参数!](#-小心那些有默认值的可变参数) 68 | - [▶ 抓住这些异常!](#-抓住这些异常) 69 | - [▶ 相同的操作,不同的结果](#-相同的操作不同的结果) 70 | - [▶ 作用域之外的变量](#-作用域之外的变量) 71 | - [▶ 小心那些链式操作符](#-小心那些链式操作符) 72 | - [▶ 被忽略的类变量](#-被忽略的类变量) 73 | - [▶ 小心处理元组](#-小心处理元组) 74 | - [第四章: 一起来找些有趣的东西!](#第四章-一起来找些有趣的东西) 75 | - [▶ 人生苦短,我用Python *](#-人生苦短我用python-) 76 | - [▶ 为什么要用`goto`? *](#-为什么要用goto-) 77 | - [▶ 试试用大括号? *](#-试试用大括号-) 78 | - [▶ 不等号的争议 *](#-不等号的争议-) 79 | - [▶ 就算Python也知道爱是个复杂的东西 *](#-就算python也知道爱是个复杂的东西-) 80 | - [▶ 这些语句是存在的!](#-这些语句是存在的) 81 | - [▶ 无限(Inpinity) *](#-无限inpinity-) 82 | - [▶ 被修改的类成员 *](#-被修改的类成员-) 83 | - [第五章: 杂项](#第五章-杂项) 84 | - [▶ `+=`更快](#-更快) 85 | - [▶ 超长字符串!](#-超长字符串) 86 | - [▶ 字符串到浮点数的转换](#-字符串到浮点数的转换) 87 | - [▶ 最后一些小特点集合](#-最后一些小特点集合) 88 | - [贡献](#贡献) 89 | - [感谢](#感谢) 90 | - [:mortar_board: 版权声明](#mortar_board-版权声明) 91 | - [帮助](#帮助) 92 | - [关于pdf出版的问题](#关于pdf出版的问题) 93 | 94 | 95 | 96 | # 示例结构说明 97 | 98 | 99 | 100 | 下面是每个例子里通用的结构说明: 101 | 102 | > ### ▶ 这里是例子的标题 * 103 | > 首先是例子的标题,如果某个标题后面带有星号,说明这是一个新加入的例子。 104 | > ```py 105 | > # 第一个代码段里面会有一些初始化代码 106 | > # 为后续的输出代码段做准备... 107 | > ``` 108 | > 109 | > **Output (Python version):** 110 | > ```py 111 | > >>> python语句,执行某个命令 112 | > 一些输出(可能你想得到,也可能想不到) 113 | > ``` 114 | > (可选): 有可能会说明一下上面输出的内容 115 | > 116 | > 117 | > #### 💡 解释: 118 | > 119 | > * 简短的介绍发生了什么和为什么会产生这些输出。 120 | > ```py 121 | > 一些初始化代码 122 | > ``` 123 | > **Output:** 124 | > ```py 125 | > >>> 执行代码 # 这些代码会展示为何会有上方那些输出内容 126 | > ``` 127 | 128 | **注意:** 所有的代码都是在 Python 3.5.2 环境下测试通过,理论上如果没有特殊声明,可以在所有的Python版本下运行。 129 | 130 | # 使用方法 131 | 132 | 在我看来,为了充分的利用这个仓库里的所有例子,最好的办法就是按照顺序把每个例子挨个看一遍: 133 | - 仔细阅读每个例子的初始化代码。如果你是一个经验丰富的Python程序员,那么大部分时候你都可以知道初始化代码执行后具体会发生什么。 134 | - 阅读输出结果并且, 135 | + 检查输出结果是否和你想的一样 136 | + 确认你是否知道产生这种结果背后的原理, 137 | - 如果不知道,那么请仔细阅读解释章节(如果看完解释还是不懂的话,那就提交一个 [issue](https://github.com/true1023/Crazy-Python/issues) 吧) 138 | - 如果知道,那么给就自己点个赞,继续看下一个例子 139 | 140 | # :eyes: 例子 141 | 142 | ## 第一章: 撕裂大脑 143 | 144 | ### ▶ 善变的字符串 * 145 | 146 | 1\. 147 | ```py 148 | >>> a = "crazy_python" 149 | >>> id(a) 150 | 2387669241224 151 | >>> id("crazy" + "_" + "python") # 注意这两个字符串的id号是一样的 152 | 2387669241224 153 | ``` 154 | 155 | 2\. 156 | ```py 157 | >>> a = "crazy" 158 | >>> b = "crazy" 159 | >>> a is b 160 | True 161 | 162 | >>> a = "crazy!" 163 | >>> b = "crazy!" 164 | >>> a is b 165 | False 166 | 167 | >>> a, b = "crazy!", "crazy!" 168 | >>> a is b 169 | True 170 | ``` 171 | 172 | 3\. 173 | ```py 174 | >>> 'a' * 20 is 'aaaaaaaaaaaaaaaaaaaa' 175 | True 176 | >>> 'a' * 21 is 'aaaaaaaaaaaaaaaaaaaaa' 177 | False 178 | ``` 179 | 180 | 很不可思议,对吧? 181 | 182 | #### 💡 解释: 183 | + 上面这种特性是CPython的一种编译器优化技术(叫做字符串驻留技术)。就是如果将要创建的字符串在之前已经创建过并且驻留在了内存没有释放,那么CPython不会创建一个新的实例,而是会把指针指向已经存在于内存的老的字符串实例。 184 | + 如果一个字符串实例已经驻留在了内存中,那么后续所有跟它值一样的变量都可以将指针指向这个内存中的字符串实例(这样就会节省内存空间) 185 | + 在上面这几段程序代码中,有几段的字符串明显驻留在了内存中供多个变量引用。 决定一个字符串是否会驻留在内存中是由这个字符串的实现方法决定的。下面是一些判断字符串变量是否会驻留内存的方法: 186 | * 所有长度为1或者0的字符串,全部会驻留 187 | * 编译阶段(也就是把源代码编译成.pyc文件的阶段)的字符串被驻留在内存,运行阶段就不会(`'crazy'`会驻留在内存,但是`''.join(['c','r','a','z','y'])`就不会) 188 | * 如果字符串包含除了ASCII字母,数字和下划线以外的字符,那么这个字符串就不会驻留内存。这就是为什么`'crazy!'`赋值给`a`,`b`的时候得到的结果是False,因为`!`感叹号。CPython中对这个特点的具体实现请参考[这里](https://github.com/python/cpython/blob/3.6/Objects/codeobject.c#L19) 189 | 190 | + 当变量`a`和`b`在同一行赋值`"crazy!"`的时候,Python的解释器会创建一个新的字符串实例,然后把这两个变量同时指向这一个实例。但是如果你用两行实现赋值的话,Python解释器就不会知道已经有一个`'crazy!'`的字符串实例存在了(因为根据上面的规则,`'crazy!'`实例不会进行内存驻留供后面的语句引用)。这是一种专门在交互环境下编译器的一种优化方法。 191 | + 常量折叠是Python实现的一种[窥孔优化(Peephole optimization)](https://baike.baidu.com/item/%E7%AA%A5%E5%AD%94%E4%BC%98%E5%8C%96)技术。意思就是`'a'*20`这个语句在编译的时候会自动替换成`'aaaaaaaaaaaaaaaaaaaa'`这个变量,用来减少运行时的运算时钟周期(`'a'*20`需要多执行20次乘法运算)。常量折叠只在字符串长度小于等于20的时候发生(至于为什么?想想如果有一个`'a*10**10'`这样的语句,折叠后需要多大一个`.pyc`才能存下折叠后的字符串啊)。[这里](https://github.com/python/cpython/blob/3.6/Python/peephole.c#L288)是实现这种技术的实现代码。 192 | 193 | --- 194 | 195 | 196 | ### ▶ 不变的哈希值 197 | 198 | 1\. 199 | ```py 200 | some_dict = {} 201 | some_dict[5.5] = "Ruby" 202 | some_dict[5.0] = "JavaScript" 203 | some_dict[5] = "Python" 204 | ``` 205 | 206 | **Output:** 207 | ```py 208 | >>> some_dict[5.5] 209 | "Ruby" 210 | >>> some_dict[5.0] 211 | "Python" 212 | >>> some_dict[5] 213 | "Python" 214 | ``` 215 | "Python" 把之前的 "JavaScript" 覆盖掉了吗? 216 | 217 | #### 💡 解释 218 | 219 | * Python的字典结构是根据key值的哈希值判断两个key值是否相等的 220 | * 在Python中,不变对象(Immutable objects)的值如果一样,那么它们的哈希值肯定也一样 221 | ```py 222 | >>> 5 == 5.0 223 | True 224 | >>> hash(5) == hash(5.0) 225 | True 226 | ``` 227 | **注意:** 有些对象有不同的值,但是它们的哈希值也有可能是一样的(所谓的哈希冲突) 228 | * 当`some_dict[5] = "Python"`这句话执行的时候, "Python"这个字符串就会覆盖掉"JavaScript"这个值,因为在Python看来,`5`和`5.0`的哈希值是一样的,也就是说对于字典结构他们对应的是一个key值。 229 | * 在 StackOverflow 上面有一个[回答](https://stackoverflow.com/a/32211042/4354153)对Python的这个特性解释的很棒。 230 | 231 | --- 232 | 233 | ### ▶ 说了要执行就一定会执行! 234 | 235 | ```py 236 | def some_func(): 237 | try: 238 | return 'from_try' 239 | finally: 240 | return 'from_finally' 241 | ``` 242 | 243 | **Output:** 244 | ```py 245 | >>> some_func() 246 | 'from_finally' 247 | ``` 248 | 249 | #### 💡 解释: 250 | 251 | - 当在`try`语句块中遇到`return`,`break`或者`continue`的时候,如果是"try...finlly"语句块,那么在执行完`try`语句块里的内容后,依然会执行`finally`语句块的内容。 252 | - 当`return`语句返回一个值的时候,那么因为在`finally`语句块中的`return`语句是最后执行的,那么返回的值就永远都是`finally`语句块中`return`语句返回的值。 253 | 254 | --- 255 | 256 | ### ▶ 鸠占鹊巢 * 257 | 258 | ```py 259 | class Crazy: 260 | pass 261 | ``` 262 | 263 | **Output:** 264 | ```py 265 | >>> Crazy() == Crazy() # 两个类实例是不同的 266 | False 267 | >>> Crazy() is Crazy() # 它们的id号也是不一样的 268 | False 269 | >>> hash(Crazy()) == hash(Crazy()) # 它们的哈希值按说也应该不一样 270 | True 271 | >>> id(Crazy()) == id(Crazy()) 272 | True 273 | ``` 274 | 275 | #### 💡 解释: 276 | 277 | * 当`id`函数被调用的时候,Python创建了一个`Crazy`类实例,然后把这个实例传给了`id`函数。然后`id`函数返回这个实例的"id"号(实际上就是这个实例在内存中的地址),接着这个实例就被丢弃并且销毁了。 278 | * 当我们紧接着再做一遍上面的步骤的时候,Python会把同一块内存空间分配给第二次创建的`Crazy`实例。又因为在CPython中`id`函数使用的是内存地址作为返回值,所以就会出现两个对象实例的id号相同的情况了。 279 | * 所以,"对象的id是唯一的"这句话有一个前提条件是"在这个对象的生命周期内"。当这个对象在内存被销毁以后,其他的对象就可以占用它之前所用的内存空间产生一样的id号。 280 | * 但是为什么上面的例子里`is`操作符却产生了`False`? 我们再看一个例子。 281 | ```py 282 | class Crazy(object): 283 | def __init__(self): print("I") 284 | def __del__(self): print("D") 285 | ``` 286 | 287 | **Output:** 288 | ```py 289 | >>> Crazy() is Crazy() 290 | I 291 | I 292 | D 293 | D 294 | False 295 | >>> id(Crazy()) == id(Crazy()) 296 | I 297 | D 298 | I 299 | D 300 | True 301 | ``` 302 | 现在你可以发现, 不同的调用方法会对实例销毁的时间产生不同的影响。 303 | 304 | --- 305 | 306 | ### ▶ 神奇赋值法 307 | 308 | ```py 309 | some_string = "crazy" 310 | some_dict = {} 311 | for i, some_dict[i] in enumerate(some_string): 312 | pass 313 | ``` 314 | 315 | **Output:** 316 | ```py 317 | >>> some_dict # 一个带引索的字典被创建. 318 | {0: 'c', 1: 'r', 2: 'a', 3: 'z', 4: 'y'} 319 | ``` 320 | 321 | #### 💡 解释: 322 | 323 | * 一个 `for` 语句在[Python语法](https://docs.python.org/3/reference/grammar.html)中是这么定义的: 324 | ``` 325 | for_stmt: 'for' exprlist 'in' testlist ':' suite ['else' ':' suite] 326 | ``` 327 | `exprlist` 是一组被赋值的变量. 这就等于说这组变量在**每次迭代**开始的时候都会执行一次 `{exprlist} = {next_value}` 。 328 | 下面这个例子很好的解释了上面想要表达的意思: 329 | ```py 330 | for i in range(4): 331 | print(i) 332 | i = 10 333 | ``` 334 | 335 | **Output:** 336 | ``` 337 | 0 338 | 1 339 | 2 340 | 3 341 | ``` 342 | 343 | 是不是以为上面的循环就会执行一次? 344 | 345 | **💡 解释:** 346 | - 在上面这个循环中,`i=10`这个赋值语句不会整个迭代过程产生任何影响。因为在每次迭代开始前,迭代函数(在这里是`range(4)`)都会把下一次的值赋值给目标变量(在这里是`i`)。 347 | 348 | * 再来看上面的例子,`enumerate(some_string)`这个函数会在每次迭代的时候产生两个值,分别是`i`(一个从0开始的索引值)和一个字符(来自`some_string`的值)。然后这两个值会分别赋值给`i`和`some_dict[i]`。把刚才的循环展开来看就像是下面这样: 349 | ```py 350 | >>> i, some_dict[i] = 'c' 351 | >>> i, some_dict[i] = 'r' 352 | >>> i, some_dict[i] = 'a' 353 | >>> i, some_dict[i] = 'z' 354 | >>> i, some_dict[i] = 'y' 355 | >>> some_dict 356 | ``` 357 | 358 | --- 359 | 360 | ### ▶ 时间的误会 361 | 362 | 1\. 363 | ```py 364 | array = [1, 8, 15] 365 | g = (x for x in array if array.count(x) > 0) 366 | array = [2, 8, 22] 367 | ``` 368 | 369 | **Output:** 370 | ```py 371 | >>> print(list(g)) 372 | [8] 373 | ``` 374 | 375 | 2\. 376 | 377 | ```py 378 | array_1 = [1,2,3,4] 379 | g1 = (x for x in array_1) 380 | array_1 = [1,2,3,4,5] 381 | 382 | array_2 = [1,2,3,4] 383 | g2 = (x for x in array_2) 384 | array_2[:] = [1,2,3,4,5] 385 | ``` 386 | 387 | **Output:** 388 | ```py 389 | >>> print(list(g1)) 390 | [1,2,3,4] 391 | 392 | >>> print(list(g2)) 393 | [1,2,3,4,5] 394 | ``` 395 | 396 | #### 💡 解释 397 | 398 | - 在[生成器](https://wiki.python.org/moin/Generators)表达式中,`in`语句会在声明阶段求值,但是条件判断语句(在这里是`array.count(x) > 0`)会在真正的运行阶段(runtime)求值。 399 | - 在生成器运行之前,`array`已经被重新赋值为`[2, 8, 22]`了,所以这个时候再用`count`函数判断`2`,`8`,`22`在原列表中的数量,只有`8`是数量大于`0`的,所以最后这个生成器只返回了一个`8`是符合条件的。 400 | - 在第二部分中,`g1`,`g2`输出结果不同,是因为对`array_1`和`array_2`的赋值方法不同导致的。 401 | - 在第一个例子中, `array_1`绑定了一个新的列表对象`[1,2,3,4,5]`(可以理解成`array_1`的指针指向了一个新的内存地址),而且在这之前,`in`语句已经在声明时就为`g1`绑定好了旧的列表对象`[1,2,3,4]`(这个就对象也没有随着新对象的赋值而销毁)。所以这时候`g1`和`array_1`是指向不同的对象地址的。 402 | - 在第二个例子中,由于切片化的赋值,`array_2`并没有绑定(指向)新对象,而是将旧的对象`[1,2,3,4]`更新(也就是说旧对象的内存地址被新对象占用了)成了新的对象`[1,2,3,4,5]`。所以,`g2`和`array_2`依旧同时指向一个地址,所以都更新成了新对象`[1,2,3,4,5]`。 403 | 404 | --- 405 | 406 | ### ▶ 特殊的数字们 407 | 408 | 下面这个例子在网上非常的流行。 409 | 410 | ```py 411 | >>> a = 256 412 | >>> b = 256 413 | >>> a is b 414 | True 415 | 416 | >>> a = 257 417 | >>> b = 257 418 | >>> a is b 419 | False 420 | 421 | >>> a = 257; b = 257 422 | >>> a is b 423 | True 424 | ``` 425 | 426 | #### 💡 解释: 427 | 428 | **`is`和`==`的区别** 429 | 430 | * `is` 操作符会检查两边的操作数是否引用的是同一个对象(也就是说,会检查两个操作数的id号是否匹配)。 431 | * `==` 操作符会比较两个操作数的值是否一样。 432 | * 所以说`is`是比较引用地址是否相同,`==`是比较值是否相同。下面的例子解释的比较清楚, 433 | ```py 434 | >>> [] == [] 435 | True 436 | >>> [] is [] # 这里的两个空list分配了不同的内存地址 437 | False 438 | ``` 439 | 440 | **`256` 是一个已经存在于内存的对象 但是 `257` 不是** 441 | 442 | 当你启动一个Python解释器的时候,数字`-5`到`256`就会自动加载进内存。 这些数字都是一些比较常用的数字,所以Python解释器会把他们提前准备好以备以后使用。 443 | 444 | 下面这段话摘抄自 https://docs.python.org/3/c-api/long.html (已经翻译为中文) 445 | > The current implementation keeps an array of integer objects for all integers between -5 and 256, when you create an int in that range you just get back a reference to the existing object. So it should be possible to change the value of 1. I suspect the behavior of Python, in this case, is undefined.:-) 446 | > 447 | > 当前的实现方法是,维护一个从-5到256的整数数组,当你使用其中某一个数字的时候,系统会自动为你引用到已经存在的对象上去。我认为应该让它可以改变数字1的值。不过就现在来说,Python还没有这个功能。:-) 448 | 449 | ```py 450 | >>> id(256) 451 | 10922528 452 | >>> a = 256 453 | >>> b = 256 454 | >>> id(a) 455 | 10922528 456 | >>> id(b) 457 | 10922528 458 | >>> id(257) 459 | 140084850247312 460 | >>> x = 257 461 | >>> y = 257 462 | >>> id(x) 463 | 140084850247440 464 | >>> id(y) 465 | 140084850247344 466 | ``` 467 | 468 | Python解释器在执行`y = 257`的时候还不能意识到我们之前已经创建过了一个值为`257`的对象,所以它又在内存创建了一个新的对象。 469 | 470 | **当`a`和`b`变量在同一行赋值并且所赋值相等时,它们会引用到同一个对象** 471 | 472 | ```py 473 | >>> a, b = 257, 257 474 | >>> id(a) 475 | 140640774013296 476 | >>> id(b) 477 | 140640774013296 478 | >>> a = 257 479 | >>> b = 257 480 | >>> id(a) 481 | 140640774013392 482 | >>> id(b) 483 | 140640774013488 484 | ``` 485 | 486 | * 当a和b在同一行被赋值为`257`的时候, Python解释器会创建一个新的对象,然后把这两个变量同时引用到这个新的对象。如果你分开两行赋值两个变量,那么解释器不会“知道”之前自己已经创建过一个同样的`257`对象了。 487 | * 这是一种专门针对交互式解释器环境的优化机制。 当你在控制面板中敲入两行命令的时候,这两行命令是分开编译的,所以他们也会单独进行优化。如果你准备把这个例子(两行分别赋值的例子)写进`.py`文件然后进行测试,那么你会发现结果跟写在一行是一样的,因为文件里的代码是一次性编译的。 488 | 489 | --- 490 | 491 | ### ▶ 三子棋之一步取胜法 492 | 493 | ```py 494 | # 首先先来初始化一个1*3的一维数组 495 | row = [""]*3 #row i['', '', ''] 496 | # 然后再用二维数组模拟一个3*3的棋盘 497 | board = [row]*3 498 | ``` 499 | 500 | **Output:** 501 | ```py 502 | >>> board 503 | [['', '', ''], ['', '', ''], ['', '', '']] 504 | >>> board[0] 505 | ['', '', ''] 506 | >>> board[0][0] 507 | '' 508 | >>> board[0][0] = "X" 509 | >>> board 510 | [['X', '', ''], ['X', '', ''], ['X', '', '']] 511 | ``` 512 | 513 | 我们只赋值了一个“X”为什么会出来三个呢? 514 | 515 | #### 💡 解释: 516 | 517 | 当我们初始化`row`变量的时候,下图显示的是内存中的变化 518 | 519 | ![image](./assets/1=3/after_row_initialized.png) 520 | 521 | 接着当变量`board`通过`[row]*3`初始化后,下图显示了内存的变化(其实最终每一个变量`board[0]`,`board[1]`,`board[2]`都引用了同一个`row`对象的内存地址) 522 | 523 | ![image](./assets/1=3/after_board_initialized.png) 524 | 525 | 我们可以通过不使用`row`变量来阻止这种情况的发生 526 | 527 | ```py 528 | >>> board = [['']*3 for _ in range(3)] 529 | >>> board[0][0] = "X" 530 | >>> board 531 | [['X', '', ''], ['', '', ''], ['', '', '']] 532 | ``` 533 | 534 | --- 535 | 536 | ### ▶ 没脑子的函数 537 | 538 | ```py 539 | funcs = [] 540 | results = [] 541 | for x in range(7): 542 | def some_func(): 543 | return x 544 | funcs.append(some_func) 545 | results.append(some_func()) 546 | 547 | funcs_results = [func() for func in funcs] 548 | ``` 549 | 550 | **Output:** 551 | ```py 552 | >>> results 553 | [0, 1, 2, 3, 4, 5, 6] 554 | >>> funcs_results 555 | [6, 6, 6, 6, 6, 6, 6] 556 | ``` 557 | 虽然我们每次把`some_func`函数加入到`funcs`列表里的时候`x`都不一样,但是`funcs`列表里的所有函数都返回了6. 558 | 559 | //下面这段代码也是这样 560 | 561 | ```py 562 | >>> powers_of_x = [lambda x: x**i for i in range(10)] 563 | >>> [f(2) for f in powers_of_x] 564 | [512, 512, 512, 512, 512, 512, 512, 512, 512, 512] 565 | ``` 566 | 567 | #### 💡 解释 568 | 569 | - 当我们在一个循环中定义一个函数,并且在函数体中用了循环中的变量时,这个函数只会绑定这个变量本身,并不会绑定当前变量循环到的值。所以最终所有在循环中定义的函数都会使用循环变量最后的值做计算。 570 | 571 | - 如果你想实现心中想的那种效果,可以把循环变量当做一个参数传递进函数体。**为什么这样可以呢?**因为这样在函数作用域内会重新定义一个变量,不是循环里面的那个变量了。 572 | 573 | ```py 574 | funcs = [] 575 | for x in range(7): 576 | def some_func(x=x): 577 | return x 578 | funcs.append(some_func) 579 | ``` 580 | 581 | **Output:** 582 | ```py 583 | >>> funcs_results = [func() for func in funcs] 584 | >>> funcs_results 585 | [0, 1, 2, 3, 4, 5, 6] 586 | ``` 587 | 588 | --- 589 | 590 | ### ▶ `is not ...` 并不是 `is (not ...)` 591 | 592 | ```py 593 | >>> 'something' is not None 594 | True 595 | >>> 'something' is (not None) 596 | False 597 | ``` 598 | 599 | #### 💡 解释 600 | 601 | - `is not` 是一个单独的二元运算符, 和分开使用的`is`和`not`作用是不同的。 602 | - `is not` 只有在两边的操作数相同时(id相同)结果才为`False`,否则为`True` 603 | 604 | --- 605 | 606 | ### ▶ 尾部的逗号 607 | 608 | **Output:** 609 | ```py 610 | >>> def f(x, y,): 611 | ... print(x, y) 612 | ... 613 | >>> def g(x=4, y=5,): 614 | ... print(x, y) 615 | ... 616 | >>> def h(x, **kwargs,): 617 | File "", line 1 618 | def h(x, **kwargs,): 619 | ^ 620 | SyntaxError: invalid syntax 621 | >>> def h(*args,): 622 | File "", line 1 623 | def h(*args,): 624 | ^ 625 | SyntaxError: invalid syntax 626 | ``` 627 | 628 | #### 💡 解释: 629 | 630 | - 末尾的逗号在函数参数列表最后并不总是合法的 631 | - 在Python中,参数列表里,有一部分使用前导逗号分隔的,有一部分是用后导逗号分隔的(比如`**kwargs`这种参数用前导逗号分隔,正常参数`x`用后导逗号分隔)。而这种情况就会导致有些参数列表里的逗号前后都没有用到,就会产生冲突导致编译失败。 632 | - **注意** 这种尾部逗号的问题已经在Python 3.6中被[修复](https://bugs.python.org/issue9232)了。然后[这里](https://bugs.python.org/issue9232#msg248399)有对各种尾部逗号用法的讨论。 633 | 634 | --- 635 | 636 | ### ▶ 最后一个反斜杠 637 | 638 | **Output:** 639 | ``` 640 | >>> print("\\ C:\\") 641 | \ C:\ 642 | >>> print(r"\ C:") 643 | \ C: 644 | >>> print(r"\ C:\") 645 | 646 | File "", line 1 647 | print(r"\ C:\") 648 | ^ 649 | SyntaxError: EOL while scanning string literal 650 | ``` 651 | 652 | #### 💡 解释 653 | 654 | - 如果字符串前面声明了`r`,说明后面紧跟着的是一个原始字符串,反斜杠在这种字符串中是没有特殊意义的 655 | ```py 656 | >>> print(repr(r"craz\"y")) 657 | 'craz\\"y' 658 | ``` 659 | - 解释器实际上是怎么做的呢,虽然看起来仅仅是改变了反斜杠的转义特性,实际上,它(反斜杠)会把自己和紧跟着自己的下一个字符一起传入到解释器,用来供解释器做判断和转换。这也就是为什么当反斜杠在最后一个字符的时候会报错。 660 | 661 | --- 662 | 663 | ### ▶ 纠结的not 664 | 665 | ```py 666 | x = True 667 | y = False 668 | ``` 669 | 670 | **Output:** 671 | ```py 672 | >>> not x == y 673 | True 674 | >>> x == not y 675 | File "", line 1 676 | x == not y 677 | ^ 678 | SyntaxError: invalid syntax 679 | ``` 680 | 681 | #### 💡 解释: 682 | 683 | * 操作符的优先级会影响表达式的计算顺序,并且在Python里,`==`操作符的优先级要高于`not`操作符。 684 | * 所以`not x == y`等于 `not (x == y)`,又等于`not (True == False)`,最终计算结果就会是`True`。 685 | * 但是`x == not y`会报错是因为这个表达式可以等价于`(x == not) y`,而不是我们第一眼认为的`x == (not y)`。 686 | 687 | --- 688 | 689 | ### ▶ 只剩一半的三引号 690 | 691 | **Output:** 692 | ```py 693 | >>> print('crazypython''') 694 | wtfpython 695 | >>> print("crazypython""") 696 | wtfpython 697 | >>> # 下面的语句将会产生语法错误 698 | >>> # print('''crazypython') 699 | >>> # print("""crazypython") 700 | ``` 701 | 702 | #### 💡 解释: 703 | + Python支持隐式的[字符串连接](https://docs.python.org/2/reference/lexical_analysis.html#string-literal-concatenation),比如下面这样, 704 | ``` 705 | >>> print("crazy" "python") 706 | crazypython 707 | >>> print("crazy" "") # or "crazy""" 708 | crazy 709 | ``` 710 | + 在Python中,`'''` 和 `"""` 也是一种字符串界定符,所以如果Python解释器发现了其中一个,那么就会一直在后面找对称的另一个界定符,这也就是为什么上面例子里注释掉的语句会有语法错误,因为解释器在后面找不到和前面`'''`或`"""`配对的界定符。 711 | 712 | --- 713 | 714 | ### ▶ 消失的午夜零点 715 | 716 | ```py 717 | from datetime import datetime 718 | 719 | midnight = datetime(2018, 1, 1, 0, 0) 720 | midnight_time = midnight.time() 721 | 722 | noon = datetime(2018, 1, 1, 12, 0) 723 | noon_time = noon.time() 724 | 725 | if midnight_time: 726 | print("Time at midnight is", midnight_time) 727 | 728 | if noon_time: 729 | print("Time at noon is", noon_time) 730 | ``` 731 | 732 | **Output:** 733 | ```sh 734 | ('Time at noon is', datetime.time(12, 0)) 735 | ``` 736 | 午夜时间并没有被打印出来 737 | 738 | #### 💡 解释: 739 | 740 | 在Python 3.5以前, 对于被赋值为UTC零点的`datetime.time`对象的布尔值,会被认为是`False`。这是一个在用`if obj:`这种语句的时候经常会忽略的特性,所以我们在写这种`if`语句的时候,要注意判断`obj`是否等于`null`或者空。 741 | 742 | --- 743 | 744 | ### ▶ 站错队的布尔型 745 | 746 | 1\. 747 | ```py 748 | # 一个计算列表里布尔型和Int型数量的例子 749 | mixed_list = [False, 1.0, "some_string", 3, True, [], False] 750 | integers_found_so_far = 0 751 | booleans_found_so_far = 0 752 | 753 | for item in mixed_list: 754 | if isinstance(item, int): 755 | integers_found_so_far += 1 756 | elif isinstance(item, bool): 757 | booleans_found_so_far += 1 758 | ``` 759 | 760 | **Output:** 761 | ```py 762 | >>> booleans_found_so_far 763 | 0 764 | >>> integers_found_so_far 765 | 4 766 | ``` 767 | 768 | 2\. 769 | ```py 770 | another_dict = {} 771 | another_dict[True] = "JavaScript" 772 | another_dict[1] = "Ruby" 773 | another_dict[1.0] = "Python" 774 | ``` 775 | 776 | **Output:** 777 | ```py 778 | >>> another_dict[True] 779 | "Python" 780 | ``` 781 | 782 | 3\. 783 | ```py 784 | >>> some_bool = True 785 | >>> "crazy"*some_bool 786 | 'crazy' 787 | >>> some_bool = False 788 | >>> "crazy"*some_bool 789 | '' 790 | ``` 791 | 792 | #### 💡 解释: 793 | 794 | * 布尔型(Booleans)是 `int`类型的一个子类型(bool is instance of int in Python) 795 | ```py 796 | >>> isinstance(True, int) 797 | True 798 | >>> isinstance(False, int) 799 | True 800 | ``` 801 | 802 | * `True`的整形值是`1`,`False`的整形值是`0` 803 | ```py 804 | >>> True == 1 == 1.0 and False == 0 == 0.0 805 | True 806 | ``` 807 | 808 | * StackOverFlow有针对这个问题背后原理的[解答](https://stackoverflow.com/a/8169049/4354153)。 809 | 810 | --- 811 | 812 | ### ▶ 类属性与类实例属性 813 | 814 | 1\. 815 | ```py 816 | class A: 817 | x = 1 818 | 819 | class B(A): 820 | pass 821 | 822 | class C(A): 823 | pass 824 | ``` 825 | 826 | **Ouptut:** 827 | ```py 828 | >>> A.x, B.x, C.x 829 | (1, 1, 1) 830 | >>> B.x = 2 831 | >>> A.x, B.x, C.x 832 | (1, 2, 1) 833 | >>> A.x = 3 834 | >>> A.x, B.x, C.x 835 | (3, 2, 3) 836 | >>> a = A() 837 | >>> a.x, A.x 838 | (3, 3) 839 | >>> a.x += 1 840 | >>> a.x, A.x 841 | (4, 3) 842 | ``` 843 | 844 | 2\. 845 | ```py 846 | class SomeClass: 847 | some_var = 15 848 | some_list = [5] 849 | another_list = [5] 850 | def __init__(self, x): 851 | self.some_var = x + 1 852 | self.some_list = self.some_list + [x] 853 | self.another_list += [x] 854 | ``` 855 | 856 | **Output:** 857 | 858 | ```py 859 | >>> some_obj = SomeClass(420) 860 | >>> some_obj.some_list 861 | [5, 420] 862 | >>> some_obj.another_list 863 | [5, 420] 864 | >>> another_obj = SomeClass(111) 865 | >>> another_obj.some_list 866 | [5, 111] 867 | >>> another_obj.another_list 868 | [5, 420, 111] 869 | >>> another_obj.another_list is SomeClass.another_list 870 | True 871 | >>> another_obj.another_list is some_obj.another_list 872 | True 873 | ``` 874 | 875 | #### 💡 解释: 876 | 877 | * 类里的属性和类实例中的属性是作为一个字典列表对象进行处理的。如果在当前类中没有找到需要调用的属性名,那么就会递归去父类中寻找。 878 | * `+=`操作符会在原有易变类型对象的基础上做改变而不会重新创建一个新的对象。所以在一个类实例中修改属性值,会影响到所有调用这个属性的相关类实例和类。 879 | 880 | --- 881 | 882 | ### ▶ None的产生 883 | 884 | ```py 885 | some_iterable = ('a', 'b') 886 | 887 | def some_func(val): 888 | return "something" 889 | ``` 890 | 891 | **Output:** 892 | ```py 893 | >>> [x for x in some_iterable] 894 | ['a', 'b'] 895 | >>> [(yield x) for x in some_iterable] 896 | at 0x7f70b0a4ad58> 897 | >>> list([(yield x) for x in some_iterable]) 898 | ['a', 'b'] 899 | >>> list((yield x) for x in some_iterable) 900 | ['a', None, 'b', None] 901 | >>> list(some_func((yield x)) for x in some_iterable) 902 | ['a', 'something', 'b', 'something'] 903 | ``` 904 | 905 | #### 💡 解释: 906 | - 这是一个CPython中`yield`关键字在列表构造和生成器表达式中使用的bug。已经在Python3.8中修复了。在Python3.7中也会警告过时。具体可以看Python的这篇[bug report](https://bugs.python.org/issue10544) 907 | --- 908 | 909 | ### ▶ 不可修改的元组 910 | 911 | ```py 912 | some_tuple = ("A", "tuple", "with", "values") 913 | another_tuple = ([1, 2], [3, 4], [5, 6]) 914 | ``` 915 | 916 | **Output:** 917 | ```py 918 | >>> some_tuple[2] = "change this" 919 | TypeError: 'tuple' object does not support item assignment 920 | >>> another_tuple[2].append(1000) #这里并没有报错 921 | >>> another_tuple 922 | ([1, 2], [3, 4], [5, 6, 1000]) 923 | >>> another_tuple[2] += [99, 999] 924 | TypeError: 'tuple' object does not support item assignment 925 | >>> another_tuple 926 | ([1, 2], [3, 4], [5, 6, 1000, 99, 999]) 927 | ``` 928 | 929 | 我还以为元组是不可以修改的呢...... 930 | 931 | #### 💡 解释: 932 | 933 | * 引用自 https://docs.python.org/2/reference/datamodel.html 934 | > 不可变序列 935 | 一个不可变序列对象在第一次定义后就不再可以修改。(如果这个对象中包含有对其他对象的引用,这些其他对象有可能是可变的或者可修改的;总之,只有直接引用的对象是不可以修改的。) 936 | * `+=`操作符会对左侧操作对象本身进行修改。但是元组是不允许修改的,所以会报错,但是实际上修改的是一个可变的列表(list),元组里保存的只是这个列表的地址,所以最终还是会修改成功。 937 | 938 | --- 939 | 940 | ### ▶ 消失的变量e 941 | 942 | ```py 943 | e = 7 944 | try: 945 | raise Exception() 946 | except Exception as e: 947 | pass 948 | ``` 949 | 950 | **Output (Python 2.x):** 951 | ```py 952 | >>> print(e) 953 | # 什么都没有打印 954 | ``` 955 | 956 | **Output (Python 3.x):** 957 | ```py 958 | >>> print(e) 959 | NameError: name 'e' is not defined 960 | ``` 961 | 962 | #### 💡 解释: 963 | 964 | * 参考自: https://docs.python.org/3/reference/compound_stmts.html#except 965 | 966 | 当一个异常(exception)被用`as`赋值给一个变量后,这个变量将会在`except`语句块结束后清除。看下面这段 967 | 968 | ```py 969 | except E as N: 970 | foo 971 | ``` 972 | 973 | 相当于这样 974 | 975 | ```py 976 | except E as N: 977 | try: 978 | foo 979 | finally: 980 | del N 981 | ``` 982 | 983 | 这意味着你最好不要在外面定义一个和"N"重名的变量。这些被赋值的异常变量被清除是因为,Python会将这些变量加入一个异常回溯栈中(traceback,仔细观察每次程序出错控制台输出的错误信息就是从tranceback这个堆栈里面以此取出来的),记录这些异常的具体位置信息并且一直保持这个堆栈的信息直到下一次垃圾回收开始。 984 | 985 | * 这些Python的子句并没有独立的作用域,上面所有的例子都在一个作用域里,所以当变量`e`在`except`子句里面被移除后,就相当于在整个作用域都消失了。但是在函数中情况就和上面不一样了,因为Python函数有自己的“内部”作用域。下面这个例子将说明这种情况: 986 | 987 | ```py 988 | def f(x): 989 | del(x) 990 | print(x) 991 | 992 | x = 5 993 | y = [5, 4, 3] 994 | ``` 995 | 996 | **Output:** 997 | ```py 998 | >>>f(x) 999 | UnboundLocalError: local variable 'x' referenced before assignment 1000 | >>>f(y) 1001 | UnboundLocalError: local variable 'x' referenced before assignment 1002 | >>> x 1003 | 5 1004 | >>> y 1005 | [5, 4, 3] 1006 | ``` 1007 | 1008 | * 在 Python 2.x 版本里面, 变量`e`会被赋值`Exception()`实例,所以当打印的时候什么都不会打印出来。 1009 | 1010 | **Output (Python 2.x):** 1011 | ```py 1012 | >>> e 1013 | Exception() 1014 | >>> print e 1015 | # 什么都没有打印出来 1016 | ``` 1017 | 1018 | --- 1019 | 1020 | ### ▶ 亦真还假 1021 | 1022 | ```py 1023 | True = False 1024 | if True == False: 1025 | print("It's true of false?") 1026 | ``` 1027 | 1028 | **Output:** 1029 | ``` 1030 | It's true of false? 1031 | ``` 1032 | 1033 | #### 💡 解释: 1034 | 1035 | - 一开始, Python是没有`bool`布尔类型的(人们用0代表假用非0代表真)。后来增加了`True`,`False`还有`bool`类型,但是为了向前兼容,所以并没有`True`和`False`的关键字常量,只不过作为一个内部变量出现。 1036 | - Python 3是不向前兼容的,所以终于修复了这个问题,同时也要注意上面的例子在Python 3.X版本下是运行不了的! 1037 | 1038 | --- 1039 | 1040 | ### ▶ 转瞬即空 1041 | 1042 | ```py 1043 | some_list = [1, 2, 3] 1044 | some_dict = { 1045 | "key_1": 1, 1046 | "key_2": 2, 1047 | "key_3": 3 1048 | } 1049 | 1050 | some_list = some_list.append(4) 1051 | some_dict = some_dict.update({"key_4": 4}) 1052 | ``` 1053 | 1054 | **Output:** 1055 | ```py 1056 | >>> print(some_list) 1057 | None 1058 | >>> print(some_dict) 1059 | None 1060 | ``` 1061 | 1062 | #### 💡 解释 1063 | 1064 | 大部分修改序列结构对象的方法,比如`list.append`,`dict.update`,`list.sort`等等。都会直接对序列对象本身进行操作,也就是所谓的“就地操作”(in-plcae)并且会返回`None`。这么做是为了提升程序的性能,避免还需要把原有对象拷贝一次。(参考[这里](http://docs.python.org/2/faq/design.html#why-doesn-t-list-sort-return-the-sorted-list))。 1065 | 1066 | ```py 1067 | some_list = [1, 2, 3] 1068 | ``` 1069 | 1070 | **Output:** 1071 | ```py 1072 | >>>some_list2 = some_list.append(4) 1073 | >>>some_list2 1074 | None 1075 | >>>some_list 1076 | [1, 2, 3, 4] 1077 | ``` 1078 | 1079 | --- 1080 | 1081 | ### ▶ 子类的关系 * 1082 | 1083 | **Output:** 1084 | ```py 1085 | >>> from collections import Hashable 1086 | >>> issubclass(list, object) 1087 | True 1088 | >>> issubclass(object, Hashable) 1089 | True 1090 | >>> issubclass(list, Hashable) 1091 | False 1092 | ``` 1093 | 1094 | 是不是觉得类的继承关系应该是可传递的(transitive)?(比如,`A`是`B`的一个子类,`B`又是`C`的一个子类,那么`A`也"应该"是`C`的子类) 1095 | 1096 | #### 💡 解释: 1097 | 1098 | * 在Python中子类的继承关系不一定是传递的,任何人都可以自定义元类(metaclass)中的`__subclasscheck__`函数(`_subclasscheck__(subclass)`检查subclass是不是调用类的子类)。 1099 | * 当调用`issubclass(cls, Hashable)`的时候,函数只是简单的检查一下`cls`和它继承的类中有没有"`__hash__`"这个方法。 1100 | * 因为`object`是可以被哈希的(也就是说`object`有`__hash__`这个函数),但是`list`是不能被哈希的,所以他们之间打破了传导关系。 1101 | * 如果想看更详尽的解释,[这里](https://www.naftaliharris.com/blog/python-subclass-intransitivity/)有关于Python子类关系传导的详细解释。 1102 | 1103 | --- 1104 | 1105 | ### ▶ 神秘的键值转换 * 1106 | 1107 | ```py 1108 | class SomeClass(str): 1109 | pass 1110 | 1111 | some_dict = {'s':42} 1112 | ``` 1113 | 1114 | **Output:** 1115 | ```py 1116 | >>> type(list(some_dict.keys())[0]) 1117 | str 1118 | >>> s = SomeClass('s') 1119 | >>> some_dict[s] = 40 1120 | >>> some_dict # expected: Two different keys-value pairs 1121 | {'s': 40} 1122 | >>> type(list(some_dict.keys())[0]) 1123 | str 1124 | ``` 1125 | 1126 | #### 💡 解释: 1127 | 1128 | * 类`s`和字符串`s`的哈希值是一样的,因为`SomeClass`继承了`str`类的`__hash__`方法。 1129 | * `SomeClass("s") == "s"` 等于`True`,因为`SomeClass`同样继承了`str`类的`__eq__`方法。 1130 | * 因为两个对象的哈希值(hash)和值(value)全都相等,所以他们在字典的关键字(key)里是被认为相同的。 1131 | * 如果想要打到预期的效果,我们可以重新定义`SomeClass`的`__eq__`方法 1132 | ```py 1133 | class SomeClass(str): 1134 | def __eq__(self, other): 1135 | return ( 1136 | type(self) is SomeClass 1137 | and type(other) is SomeClass 1138 | and super().__eq__(other) 1139 | ) 1140 | 1141 | # 当我们自定义 __eq__ 方法后, Python会停止 1142 | # 自动继承 __hash__ 方法, 所以我们同样也需要定义它 1143 | __hash__ = str.__hash__ 1144 | 1145 | some_dict = {'s':42} 1146 | ``` 1147 | 1148 | **Output:** 1149 | ```py 1150 | >>> s = SomeClass('s') 1151 | >>> some_dict[s] = 40 1152 | >>> some_dict 1153 | {'s': 40, 's': 42} 1154 | >>> keys = list(some_dict.keys()) 1155 | >>> type(keys[0]), type(keys[1]) 1156 | (__main__.SomeClass, str) 1157 | ``` 1158 | 1159 | --- 1160 | 1161 | ### ▶ 看你能不能猜到这个的结果? 1162 | 1163 | ```py 1164 | a, b = a[b] = {}, 5 1165 | ``` 1166 | 1167 | **Output:** 1168 | ```py 1169 | >>> a 1170 | {5: ({...}, 5)} 1171 | ``` 1172 | 1173 | #### 💡 解释: 1174 | 1175 | * 根据Python的赋值语句的[文档](https://docs.python.org/3.7/reference/simple_stmts.html#assignment-statements),赋值语句有如下的格式 1176 | ``` 1177 | (target_list "=")+ (expression_list | yield_expression) 1178 | ``` 1179 | 并且 1180 | > 一个赋值语句会对表达式列表(expression_list)进行求值(这个可以只有一个表达式,也可以多个表达式用逗号分开组成表达式列表,后者最终会表现为元组的形式)并且会将单次的求值结果对象依次从左至右赋值给目标列表(target_list)。 1181 | 1182 | * 在`(target_list "=")+`这个表达式中`+`意味着 **一个或者多个** 目标列表。这上面这个例子中,目标列表是`a,b`和`a[b]`(注意表达式列表永远只有一个,在我们这个例子里是`{}, 5`)。 1183 | 1184 | * 当表达式计算出来后,它的值会被 **从左至右** 依次赋值给目标列表。所以,在上面的例子里,第一次赋值会把`{}, 5`这个元组赋值给`a, b`这个目标列表,这个时候`a = {}` 并且 `b = 5`。 1185 | 1186 | * 现在`a`被赋值成了`{}`,一个可变对象(mutable object)。 1187 | 1188 | * 第二个目标列表是`a[b]`(你可能会认为这种写法会报错,因为我们没有之前并没有定义`a`和`b`,但是没关系,我们刚刚已经给`a`和`b`分别赋值了`{}`和`5`)。 1189 | 1190 | * 现在我们会把元组`({}, 5)`赋值给字典关键为`5`的字典对象(也就是`a[5]`),同时也因此创建了一个循环引用(circular reference)(`{...}`在输出中代表同一个对象`a`已经被引用了)。下面是一个关于引用循环简单点的例子 1191 | 1192 | ```py 1193 | >>> some_list = some_list[0] = [0] 1194 | >>> some_list 1195 | [[...]] 1196 | >>> some_list[0] 1197 | [[...]] 1198 | >>> some_list is some_list[0] 1199 | True 1200 | >>> some_list[0][0][0][0][0][0] == some_list 1201 | True 1202 | ``` 1203 | 在我们的例子用也是同样的情况(`a[b][0]`和`a`是指向同一个对象) 1204 | 1205 | * 所以总结一下,我们可以把上面的列子像如下这样拆分 1206 | ```py 1207 | a, b = {}, 5 1208 | a[b] = a, b 1209 | ``` 1210 | 并且通过`a[b][0]`和`a`是同一个对象来证明确实存在引用循环 1211 | ```py 1212 | >>> a[b][0] is a 1213 | True 1214 | ``` 1215 | 1216 | --- 1217 | 1218 | --- 1219 | 1220 | ## 第二章: 瞒天过海 1221 | 1222 | ### ▶ 无效的一行? 1223 | 1224 | **Output:** 1225 | ```py 1226 | >>> value = 11 1227 | >>> valuе = 32 1228 | >>> value 1229 | 11 1230 | ``` 1231 | 1232 | 哈?一脸懵逼是不是(囧)? 1233 | 1234 | **提示:** 最简单的复现上面结果的办法是把上面的赋值语句原封不动的复制粘贴到你的文件或者命令行中执行。 1235 | 1236 | #### 💡 解释 1237 | 1238 | 一些非西方字符集看起来和我们平时用的英文字母的外观是一模一样的,但是在解释器看来就不一样了。 1239 | 1240 | ```py 1241 | >>> ord('е') # 俄语的字母 'e' (Ye) 1242 | 1077 1243 | >>> ord('e') # 拉丁文的字母'e', 也就是我们平时在英语和用标准键盘打出来的字母 1244 | 101 1245 | >>> 'е' == 'e' 1246 | False 1247 | 1248 | >>> value = 42 # 拉丁的 e 1249 | >>> valuе = 23 # 俄语里的 'e', 这里如果是用的Python 2.x,解释器会产生一个`SyntaxError`的异常 1250 | >>> value 1251 | 42 1252 | ``` 1253 | 1254 | 内置函数`ord()`可以返回对应字符在Unicode编码中的位置([code point](https://en.wikipedia.org/wiki/Code_point)), 因为俄语的字符'e'和拉丁语的字符'e'的位置是不一样的,所以在上面的例子里,解释器会认为两个`value`变量是不同的。 1255 | 1256 | --- 1257 | 1258 | ### ▶ 移形换位 * 1259 | 1260 | ```py 1261 | import numpy as np 1262 | 1263 | def energy_send(x): 1264 | # 初始化一个numpy数组 1265 | np.array([float(x)]) 1266 | 1267 | def energy_receive(): 1268 | # 返回一个空numpy数组 1269 | return np.empty((), dtype=np.float).tolist() 1270 | ``` 1271 | 1272 | **Output:** 1273 | ```py 1274 | >>> energy_send(123.456) 1275 | >>> energy_receive() 1276 | 123.456 1277 | ``` 1278 | 1279 | 是不是很神奇? 1280 | 1281 | #### 💡 解释: 1282 | 1283 | * 因为在`energy_send`函数中创建的numpy数组并没有通过`return`返回赋给任何一个变量,所以带边这个数组的那一块内存空间是可以自由分配的。 1284 | * `numpy.empty()` 会返回最近的一块还没有被使用的内存块,并且不会对这个内存块进行初始化。这块内存块恰恰就是上一个函数刚刚释放的那块内存(大部分时间是这样的,但并不是绝对的)。 1285 | 1286 | --- 1287 | 1288 | ### ▶ 到底哪里出错了呢? 1289 | 1290 | ```py 1291 | def square(x): 1292 | """ 1293 | 通过加法运算计算平方的函数 1294 | """ 1295 | sum_so_far = 0 1296 | for counter in range(x): 1297 | sum_so_far = sum_so_far + x 1298 | return sum_so_far 1299 | ``` 1300 | 1301 | **Output (Python 2.x):** 1302 | 1303 | ```py 1304 | >>> square(10) 1305 | 10 1306 | ``` 1307 | 1308 | 结果不应该是100吗? 1309 | 1310 | **注意:** 如果你不能复现上面例子的结果, 可以试试直接运行[mixed_tabs_and_spaces.py](/mixed_tabs_and_spaces.py) 这个文件。 1311 | 1312 | #### 💡 解释 1313 | 1314 | * **不要把空格和制表符混在一起使用!** 例子中`return`语句前面的是一个用制表符"tab"来缩进的,代码的其他地方都是使用“四个空格”作为缩进的。 1315 | * Python是这样处理制表符的: 1316 | > 首先,制表符(tabs)会被依次替换成(从左至右)1到8个不等的空格直到替换后的空格数满足8的倍数......[出自Python文档](https://docs.python.org/2.0/ref/indentation.html) 1317 | * 所以在`square`这个函数最后这个制表符"tab"就被替换成了8个空格,八个空格就是两个缩进,所以就等于把`return`语句缩进到了`for`循环内。 1318 | * 这种情况在Python 3中就会直接报错。 1319 | 1320 | **Output (Python 3.x):** 1321 | ```py 1322 | TabError: inconsistent use of tabs and spaces in indentation 1323 | ``` 1324 | 1325 | --- 1326 | 1327 | --- 1328 | 1329 | 1330 | ## 第三章: 隐藏的陷阱 1331 | 1332 | 1333 | ### ▶ 不要在迭代一个字典的时候修改这个字典! 1334 | 1335 | ```py 1336 | x = {0: None} 1337 | 1338 | for i in x: 1339 | del x[i] 1340 | x[i+1] = None 1341 | print(i) 1342 | ``` 1343 | 1344 | **Output (Python 2.7- Python 3.5):** 1345 | 1346 | ``` 1347 | 0 1348 | 1 1349 | 2 1350 | 3 1351 | 4 1352 | 5 1353 | 6 1354 | 7 1355 | ``` 1356 | 1357 | 这个迭代会准确运行**八**次停止。 1358 | 1359 | #### 💡 解释: 1360 | 1361 | * 首先我是不赞成在迭代一个字典的时候对这个字典做修改的。 1362 | * 这个迭代正好运行了八次是因为正好到了一个点,到了这个点后,这个字典对象需要重新调整大小用来容纳足够的关键字(keys)(我们在循环中已经连续做了八次删除了,当然需要重新调整字典的大小了)。实际上这个点在哪取决于Python的具体实现细节。 1363 | * 对删除键值的处理和什么时候触发调整字典大小会根据不同Python的版本而有所不同。 1364 | 1365 | --- 1366 | 1367 | ### ▶ 删不掉的对象 * 1368 | 1369 | ```py 1370 | class SomeClass: 1371 | def __del__(self): 1372 | print("Deleted!") 1373 | ``` 1374 | 1375 | **Output:** 1376 | 1\. 1377 | ```py 1378 | >>> x = SomeClass() 1379 | >>> y = x 1380 | >>> del x # 这里应该输出 "Deleted!" 1381 | >>> del y 1382 | Deleted! 1383 | ``` 1384 | 1385 | 啧.....最后还是输出了的。你可能已经猜到了,我们第一次尝试删除`x`的时候,`__del__`方法并没有被调用。现在我们在上面例子的基础上再加几句代码。 1386 | 1387 | 2\. 1388 | ```py 1389 | >>> x = SomeClass() 1390 | >>> y = x 1391 | >>> del x 1392 | >>> y # 检查y是否存在 1393 | <__main__.SomeClass instance at 0x7f98a1a67fc8> 1394 | >>> del y # 就像上一个例子一样,这里应该输出"Deleted!" 1395 | >>> globals() # 嗯......好像并没有输出。让我们检查一下全局变量看看。 1396 | Deleted! 1397 | {'__builtins__': , 'SomeClass': , '__package__': None, '__name__': '__main__', '__doc__': None} 1398 | ``` 1399 | 1400 | 好吧,最后还是输出了(好奇怪的说):confused: 1401 | 1402 | #### 💡 解释: 1403 | + `del x`并不会直接调用`x.__del__()`。 1404 | + 当调用`del x`的时候,Python会把所有x指向的对象的引用数减一(在这里就是前面实例化的对象`SomeClass()`),只有当这个对象的引用数为零的时候,才会调用`__del__()`。 1405 | + 在第二个例子中,`y.__del__()`没有被调用时因为前面的语句(`>>> y`)引用了同一个对象(SomeClass()),增加了这个对象的引用数,所以在`del y`以后这个对象的引用数并没有变成零。 1406 | + 调用`globals`会检查并销毁没用的引用,这时候针对上面对象(`SomeClass()`)的引用数就变成零了,"Deleted!"就被打印出来了。 1407 | 1408 | --- 1409 | 1410 | ### ▶ 删除正在迭代中的列表项 1411 | 1412 | ```py 1413 | list_1 = [1, 2, 3, 4] 1414 | list_2 = [1, 2, 3, 4] 1415 | list_3 = [1, 2, 3, 4] 1416 | list_4 = [1, 2, 3, 4] 1417 | 1418 | for idx, item in enumerate(list_1): 1419 | del item 1420 | 1421 | for idx, item in enumerate(list_2): 1422 | list_2.remove(item) 1423 | 1424 | for idx, item in enumerate(list_3[:]): 1425 | list_3.remove(item) 1426 | 1427 | for idx, item in enumerate(list_4): 1428 | list_4.pop(idx) 1429 | ``` 1430 | 1431 | **Output:** 1432 | ```py 1433 | >>> list_1 1434 | [1, 2, 3, 4] 1435 | >>> list_2 1436 | [2, 4] 1437 | >>> list_3 1438 | [] 1439 | >>> list_4 1440 | [2, 4] 1441 | ``` 1442 | 1443 | 知道为什么输出是`[2, 4]`吗? 1444 | 1445 | #### 💡 解释: 1446 | 1447 | * 首先,我是不赞成在迭代的过程中修改迭代参数的(就像上面例子里那样)。正确的方法应该是先把迭代的列表拷贝一份,拿拷贝后的列表做迭代,就像上面第三个迭代做的那样(`list_3[:]`)。 1448 | 1449 | ```py 1450 | >>> some_list = [1, 2, 3, 4] 1451 | >>> id(some_list) 1452 | 139798789457608 1453 | >>> id(some_list[:]) # 注意如果用切片法的话,Python会新建一个list对象。 1454 | 139798779601192 1455 | ``` 1456 | 1457 | 1458 | **`del`,`remove`和`pop`的区别:** 1459 | * `del var_name` 只会移除本地或者全局命名空间中对`var_name`这个变量的绑定(这就是为什么`list_1`迭代后没有任何改变,因为`list_1`这个变量并没有收到影响)。 1460 | * `remove` 会根据值(value)进行匹配并移除第一个匹配上的元素,若没有匹配的元素则抛出`ValueError`异常。 1461 | * `pop` 会移除指定索引(index)的元素并且返回这个元素,如果指定的元素不存在则抛出`IndexError`异常。 1462 | 1463 | **所以为什么会输出`[2, 4]`呢?** 1464 | - 首先列表迭代是根据索引值一个一个迭代的,所以当我们把`1`这个元素从`list_2`或者`list_4`中移除了以后,这两个列表就变成了`[2, 3, 4]`。剩下的这几个元素会依次向左移填补刚刚删除了的元素的位置,也就是说,`2`这个元素现在的索引值(index)变成了0,`3`的索引值变成了1。接着在下次迭代的时候,`for`循环访问的索引值已经变成了1(也就是列表里`3`这个元素),所以`2`这个元素就被跳过去(没有删除)。同样的情况每迭代一次都会发生一次。 1465 | 1466 | --- 1467 | 1468 | ### ▶ 被泄露出去的循环变量! 1469 | 1470 | 1\. 1471 | ```py 1472 | for x in range(7): 1473 | if x == 6: 1474 | print(x, ': for x inside loop') 1475 | print(x, ': x in global') 1476 | ``` 1477 | 1478 | **Output:** 1479 | ```py 1480 | 6 : for x inside loop 1481 | 6 : x in global 1482 | ``` 1483 | 1484 | 但是我从来没有在循环外定义一个变量`x`呀! 1485 | 1486 | 2\. 1487 | ```py 1488 | # 这回我先初始化一个x变量 1489 | x = -1 1490 | for x in range(7): 1491 | if x == 6: 1492 | print(x, ': for x inside loop') 1493 | print(x, ': x in global') 1494 | ``` 1495 | 1496 | **Output:** 1497 | ```py 1498 | 6 : for x inside loop 1499 | 6 : x in global 1500 | ``` 1501 | 1502 | 3\. 1503 | ``` 1504 | x = 1 1505 | print([x for x in range(5)]) 1506 | print(x, ': x in global') 1507 | ``` 1508 | 1509 | **Output (Python 2.x):** 1510 | ``` 1511 | [0, 1, 2, 3, 4] 1512 | (4, ': x in global') 1513 | ``` 1514 | 1515 | **Output (Python 3.x):** 1516 | ``` 1517 | [0, 1, 2, 3, 4] 1518 | 1 : x in global 1519 | ``` 1520 | 1521 | #### 💡 解释: 1522 | 1523 | - 在Python里, for循环有它自己的作用域,但是在循环结束后,不会销毁那些循环变量。就算我们提前在全局作用域里定义了变量(就像上面那种情况),也会被循环变量覆盖。 1524 | 1525 | - 关于为什么在Python2和Python3中有不同的输出,可以在Python3的文档([What's New In Python 3.0](https://docs.python.org/3/whatsnew/3.0.html))中找到答案,下面会贴出原文: 1526 | 1527 | >"List comprehensions no longer support the syntactic form `[... for var in item1, item2, ...]`. Use `[... for var in (item1, item2, ...)]` instead. Also, note that list comprehensions have different semantics: they are closer to syntactic sugar for a generator expression inside a `list()` constructor, and in particular the loop control variables are no longer leaked into the surrounding scope." 1528 | 1529 | - 只用看最后一句话就够了**in particular the loop control variables are no longer leaked into the surrounding scope**,翻译过来是**特别是循环控制变量不会再泄露到包围着它的作用域以外了**,还有这个特性是针对构造列表(List comprehensions)说的,不包括正常的for循环。 1530 | 1531 | --- 1532 | 1533 | ### ▶ 小心那些有默认值的可变参数! 1534 | 1535 | ```py 1536 | def some_func(default_arg=[]): 1537 | default_arg.append("some_string") 1538 | return default_arg 1539 | ``` 1540 | 1541 | **Output:** 1542 | ```py 1543 | >>> some_func() 1544 | ['some_string'] 1545 | >>> some_func() 1546 | ['some_string', 'some_string'] 1547 | >>> some_func([]) 1548 | ['some_string'] 1549 | >>> some_func() 1550 | ['some_string', 'some_string', 'some_string'] 1551 | ``` 1552 | 1553 | #### 💡 解释: 1554 | 1555 | - 在Python中,如果参数有默认值并且是一个可变对象,那么这个默认的对象会一直存在,并不会随着每次创建实例而重新生成一个新的对象。当我们显式的把`[]`对象传递给`some_func`函数的时候,`default_arg`这个参数的默认值就不会被使用而被新对象替代了,所以这个函数会按照我们预期的那样返回值。 1556 | 1557 | ```py 1558 | def some_func(default_arg=[]): 1559 | default_arg.append("some_string") 1560 | return default_arg 1561 | ``` 1562 | 1563 | **Output:** 1564 | ```py 1565 | >>> some_func.__defaults__ #__defaults__会把这个函数的默认参数都打印出来 1566 | ([],) 1567 | >>> some_func() 1568 | >>> some_func.__defaults__ 1569 | (['some_string'],) 1570 | >>> some_func() 1571 | >>> some_func.__defaults__ 1572 | (['some_string', 'some_string'],) 1573 | >>> some_func([]) 1574 | >>> some_func.__defaults__ 1575 | (['some_string', 'some_string'],) 1576 | ``` 1577 | 1578 | - 一个常用的用来避免上述bug的做法是这样的,把默认可变参数赋值成`None`,然后在函数体中检查这个参数是否被赋值,如果没有就赋一个默认值: 1579 | 1580 | ```py 1581 | def some_func(default_arg=None): 1582 | if not default_arg: 1583 | default_arg = [] 1584 | default_arg.append("some_string") 1585 | return default_arg 1586 | ``` 1587 | 1588 | --- 1589 | 1590 | ### ▶ 抓住这些异常! 1591 | 1592 | ```py 1593 | some_list = [1, 2, 3] 1594 | try: 1595 | # 这里应该会抛出一个 ``IndexError`` 异常 1596 | print(some_list[4]) 1597 | except IndexError, ValueError: 1598 | print("Caught!") 1599 | 1600 | try: 1601 | # 这里会抛出一个 ``ValueError`` 异常 1602 | some_list.remove(4) 1603 | except IndexError, ValueError: 1604 | print("Caught again!") 1605 | ``` 1606 | 1607 | **Output (Python 2.x):** 1608 | ```py 1609 | Caught! 1610 | 1611 | ValueError: list.remove(x): x not in list 1612 | ``` 1613 | 1614 | **Output (Python 3.x):** 1615 | ```py 1616 | File "", line 3 1617 | except IndexError, ValueError: 1618 | ^ 1619 | SyntaxError: invalid syntax 1620 | ``` 1621 | 1622 | #### 💡 解释 1623 | 1624 | * 如果想在`except`语句中同时捕获多个异常类型,需要把这些类型写成元组(tuple)的形式放在第一个参数的位置。第二个参数是一个可选参数,用来绑定具体捕获异常后的对象。比如, 1625 | ```py 1626 | some_list = [1, 2, 3] 1627 | try: 1628 | # 这里会产生一个 ``ValueError`` 异常 1629 | some_list.remove(4) 1630 | except (IndexError, ValueError), e: 1631 | print("Caught again!") 1632 | print(e) 1633 | ``` 1634 | **Output (Python 2.x):** 1635 | ``` 1636 | Caught again! 1637 | list.remove(x): x not in list 1638 | ``` 1639 | **Output (Python 3.x):** 1640 | ```py 1641 | File "", line 4 1642 | except (IndexError, ValueError), e: 1643 | ^ 1644 | IndentationError: unindent does not match any outer indentation level 1645 | ``` 1646 | 1647 | * 在Python 3中已经弃用了使用逗号分隔异常类型和异常变量了;而是要使用`as`语法了。比如, 1648 | ```py 1649 | some_list = [1, 2, 3] 1650 | try: 1651 | some_list.remove(4) 1652 | 1653 | except (IndexError, ValueError) as e: 1654 | print("Caught again!") 1655 | print(e) 1656 | ``` 1657 | **Output:** 1658 | ``` 1659 | Caught again! 1660 | list.remove(x): x not in list 1661 | ``` 1662 | 1663 | --- 1664 | 1665 | ### ▶ 相同的操作,不同的结果 1666 | 1667 | 1\. 1668 | ```py 1669 | a = [1, 2, 3, 4] 1670 | b = a 1671 | a = a + [5, 6, 7, 8] 1672 | ``` 1673 | 1674 | **Output:** 1675 | ```py 1676 | >>> a 1677 | [1, 2, 3, 4, 5, 6, 7, 8] 1678 | >>> b 1679 | [1, 2, 3, 4] 1680 | ``` 1681 | 1682 | 2\. 1683 | ```py 1684 | a = [1, 2, 3, 4] 1685 | b = a 1686 | a += [5, 6, 7, 8] 1687 | ``` 1688 | 1689 | **Output:** 1690 | ```py 1691 | >>> a 1692 | [1, 2, 3, 4, 5, 6, 7, 8] 1693 | >>> b 1694 | [1, 2, 3, 4, 5, 6, 7, 8] 1695 | ``` 1696 | 1697 | #### 💡 解释: 1698 | 1699 | * `a += b` 和 `a = a + b`的内部处理流程有时候会根据操作数的不同有所不同。比如*`op=`*这种操作符(比如`+=`,`-=`,`*=`这样),针对列表(list)对象这两个表达式就会有不一样的内在表现。 1700 | 1701 | * 表达式`a = a + [5,6,7,8]`会生成一个新的对象,并且`a`会引用这个新生成的对象,`b`变量不会变(继续引用之前的列表对象`[1,2,3,4]`)。 1702 | 1703 | * 表达式`a += [5,6,7,8]`就不会生成一个新对象,它实际上是在原有对象的基础上又“扩展了”几个元素,也就是所谓的就地改变(in-place)。所以这个时候`a`,`b`两个变量还是同时引用的一个对象。 1704 | 1705 | --- 1706 | 1707 | ### ▶ 作用域之外的变量 1708 | 1709 | ```py 1710 | a = 1 1711 | def some_func(): 1712 | return a 1713 | 1714 | def another_func(): 1715 | a += 1 1716 | return a 1717 | ``` 1718 | 1719 | **Output:** 1720 | ```py 1721 | >>> some_func() 1722 | 1 1723 | >>> another_func() 1724 | UnboundLocalError: local variable 'a' referenced before assignment 1725 | ``` 1726 | 1727 | #### 💡 解释: 1728 | * 当你在某个作用域给一个变量赋值的时候,这个变量就会自动变成这个作用域的变量。所以`a`会变成`another_func`这个函数的局部变量,但是又没有在这个函数作用域内正确的初始化`a`变量,结果就发生了异常。 1729 | * 这篇[文章](http://sebastianraschka.com/Articles/2014_python_scope_and_namespaces.html)很好的解释了命名空间和作用域是如何在Python中起作用的(是英文版,等有时间我会翻译一下,如果有热心的同学帮忙就更好啦!)。 1730 | * 如果想在局部作用域内使用全局作用域的变量`a`,需要使用`global`关键字。 1731 | ```py 1732 | def another_func() 1733 | global a 1734 | a += 1 1735 | return a 1736 | ``` 1737 | 1738 | **Output:** 1739 | ```py 1740 | >>> another_func() 1741 | 2 1742 | ``` 1743 | 1744 | --- 1745 | 1746 | ### ▶ 小心那些链式操作符 1747 | 1748 | ```py 1749 | >>> (False == False) in [False] # 没问题,OK? 1750 | False 1751 | >>> False == (False in [False]) # 这个也OK对吧? 1752 | False 1753 | >>> False == False in [False] # 这个呢?是不是开始不对了? 1754 | True 1755 | 1756 | >>> True is False == False 1757 | False 1758 | >>> False is False is False 1759 | True 1760 | 1761 | >>> 1 > 0 < 1 1762 | True 1763 | >>> (1 > 0) < 1 1764 | False 1765 | >>> 1 > (0 < 1) 1766 | False 1767 | ``` 1768 | 1769 | #### 💡 解释: 1770 | 1771 | 按照 https://docs.python.org/2/reference/expressions.html#not-in 里面说的: 1772 | 1773 | > 如果 a, b, c, ..., y, z 这些是表达式,op1, op2, ..., opN 这些是比较符,那么 a op1 b op2 c ... y opN z 就等于 a op1 b and b op2 c and ... y opN z, 除非其中有些表达式最多只能计算一次。 1774 | 1775 | 这种模式运行在像 `a == b == c` 和 `0 <=x <= 100` 这种语句里就会产生很有意思的结果了,就像上边那些例子一样。 1776 | 1777 | * `False is False is False` 等于 `(False is False) and (False is False)`。 1778 | * `True is False == False` 等于 `True is False and False == False` 而且第一部分 (`True is False`) 等于 `False`, 所以整个表达式结果就为 `False`。 1779 | * `1 > 0 < 1` 等于 `1 > 0 and 0 < 1` 最后结果等于 `True`。 1780 | * 表达式 `(1 > 0) < 1` 等于 `True < 1` 而且 1781 | ```py 1782 | >>> int(True) 1783 | 1 1784 | >>> True + 1 #这个跟例子没啥关系,就是想告诉你们布尔值也可以这样玩^_^ 1785 | 2 1786 | ``` 1787 | 所以, `1 < 1`也等于`False` 1788 | 1789 | --- 1790 | 1791 | ### ▶ 被忽略的类变量 1792 | 1793 | 1\. 1794 | ```py 1795 | x = 5 1796 | class SomeClass: 1797 | x = 17 1798 | y = (x for i in range(10)) 1799 | ``` 1800 | 1801 | **Output:** 1802 | ```py 1803 | >>> list(SomeClass.y)[0] 1804 | 5 1805 | ``` 1806 | 1807 | 2\. 1808 | ```py 1809 | x = 5 1810 | class SomeClass: 1811 | x = 17 1812 | y = [x for i in range(10)] 1813 | ``` 1814 | 1815 | **Output (Python 2.x):** 1816 | ```py 1817 | >>> SomeClass.y[0] 1818 | 17 1819 | ``` 1820 | 1821 | **Output (Python 3.x):** 1822 | ```py 1823 | >>> SomeClass.y[0] 1824 | 5 1825 | ``` 1826 | 1827 | #### 💡 解释 1828 | - 如果一个类定义内嵌套了一个子作用域,那么在这个作用域内部会忽略所有这个类级别的变量绑定。 1829 | - 一个生成器(generator)表达式有自己的作用域 1830 | - 从Python 3.X开始,列表生成式也有自己的作用域了。 1831 | 1832 | ![name-ignore-class-scope](./assets/name-ignore-class-scope/name-ignore-class-scope.png) 1833 | 1834 | --- 1835 | ### ▶ 小心处理元组 1836 | 1837 | 1\. 1838 | ```py 1839 | x, y = (0, 1) if True else None, None 1840 | ``` 1841 | 1842 | **Output:** 1843 | ``` 1844 | >>> x, y # 期待的结果是 (0, 1) 1845 | ((0, 1), None) 1846 | ``` 1847 | 1848 | 基本每一个Python程序员都会遇到过下面这种情况。 1849 | 1850 | 2\. 1851 | ```py 1852 | t = ('one', 'two') 1853 | for i in t: 1854 | print(i) 1855 | 1856 | t = ('one') 1857 | for i in t: 1858 | print(i) 1859 | 1860 | t = () 1861 | print(t) 1862 | ``` 1863 | 1864 | **Output:** 1865 | ```py 1866 | one 1867 | two 1868 | o 1869 | n 1870 | e 1871 | tuple() 1872 | ``` 1873 | 1874 | #### 💡 解释: 1875 | * 第一个例子里,想要得到期待的结果的写法应该是`x, y = (0, 1) if True else (None, None)`,要显式声明元组。 1876 | * 第二个例子里,想要得到期待的结果的写法应该是 `t = ('one',)` 或者 `t = 'one',`(原来的少了逗号)否则解释器会把变量`t`当做一个`str`字符串处理挨个迭代里面的每一个字符。 1877 | * `()`是一个特殊的标识符专门用来表示空元组(`tuple`)。 1878 | 1879 | --- 1880 | 1881 | --- 1882 | 1883 | 1884 | ## 第四章: 一起来找些有趣的东西! 1885 | 1886 | 这一章包含了一些有趣的但是鲜为人知的彩蛋,大部分的Python新手都很少有人知道,就比如我(当然,现在我知道了)。 1887 | 1888 | ### ▶ 人生苦短,我用Python * 1889 | 1890 | 首先,我先引用下面这个模块,然后...... 1891 | 1892 | ```py 1893 | import antigravity 1894 | ``` 1895 | 1896 | **Output:** 1897 | 嘘....这个秘密可没几个人知道! 1898 | 1899 | #### 💡 解释: 1900 | + `antigravity` 模块是针对Python开发者的一个小彩蛋。 1901 | + `import antigravity` 会打开一个指向 [classic XKCD comic](http://xkcd.com/353/)的网站,一个关于Python的漫画网站。 1902 | + 而且,这还是一个俄罗斯套娃彩蛋,也就是说**这个彩蛋中还包含着另一个彩蛋**。如果你打开这个模块的[代码](https://github.com/python/cpython/blob/master/Lib/antigravity.py#L7-L17),你会发现代码中还定义了一个实现[geohashing算法](https://xkcd.com/426/)的函数(这个算法是一个利用股票市场数据随机生成经度纬度坐标的算法)。 1903 | 1904 | --- 1905 | 1906 | ### ▶ 为什么要用`goto`? * 1907 | 1908 | ```py 1909 | from goto import goto, label 1910 | for i in range(9): 1911 | for j in range(9): 1912 | for k in range(9): 1913 | print("I'm trapped, please rescue!") 1914 | if k == 2: 1915 | goto .breakout #从一个深层循环内跳出 1916 | label .breakout 1917 | print("Freedom!") 1918 | ``` 1919 | 1920 | **Output (Python 2.3):** 1921 | ```py 1922 | I'm trapped, please rescue! 1923 | I'm trapped, please rescue! 1924 | Freedom! 1925 | ``` 1926 | 1927 | #### 💡 解释: 1928 | - `goto`模块是作为2004年的愚人节(2004.4.1)玩笑[发布](https://mail.python.org/pipermail/python-announce-list/2004-April/002982.html)的,只有Python 2.3版本有`goto`模块。 1929 | - 当前版本的Python是没有这个模块的(如果你用的是conda甚至不能下载到2.3版本的Python)。 1930 | - 虽然上面版本可以使用`goto`,但是也请不要使用它。[这里](https://docs.python.org/3/faq/design.html#why-is-there-no-goto)是为什么当前版本没有`goto`模块的原因。 1931 | 1932 | --- 1933 | 1934 | ### ▶ 试试用大括号? * 1935 | 1936 | 如果你不太习惯Python中用空格缩进来表示语句段(scopes)的方法,可以通过引用`__future__`模块来使用C语言大括号({})形式的表示方法来表示语句段, 1937 | 1938 | ```py 1939 | from __future__ import braces 1940 | ``` 1941 | 1942 | **Output:** 1943 | ```py 1944 | File "some_file.py", line 1 1945 | from __future__ import braces 1946 | SyntaxError: not a chance 1947 | ``` 1948 | 1949 | 想用大括号?不可能的,如果你不服气,用Java去。 1950 | 1951 | #### 💡 解释: 1952 | + `__future__`模块一般用来提供未来Python版本才拥有的一些特性。当然,在这个例子里,"future"这个词本身也算是对大括号的嘲讽。 1953 | + 这是一个Python社区针对大括号这个问题设计出来的一个彩蛋。 1954 | 1955 | --- 1956 | 1957 | ### ▶ 不等号的争议 * 1958 | 1959 | **Output (Python 3.x)** 1960 | ```py 1961 | >>> from __future__ import barry_as_FLUFL 1962 | >>> "Ruby" != "Python" # 这句话没有任何争议 1963 | File "some_file.py", line 1 1964 | "Ruby" != "Python" 1965 | ^ 1966 | SyntaxError: invalid syntax 1967 | 1968 | >>> "Ruby" <> "Python" 1969 | True 1970 | ``` 1971 | 1972 | 直接看解释吧。 1973 | 1974 | #### 💡 解释: 1975 | - 上面的问题起源自一个已经淘汰的Python修正包[PEP-401](https://www.python.org/dev/peps/pep-0401/),发布于2009年的4月1日(明白为什么它会被淘汰了吧)。 1976 | - 下面引用 PEP-401 的内容 1977 | > 为了让你们认识到在Python3.0中 != 这种逻辑不等式是多么的不好用,FLUFL引用会使解释器禁止这种语法只允许 <> 这种便利的逻辑不等式。 1978 | 1979 | --- 1980 | 1981 | ### ▶ 就算Python也知道爱是个复杂的东西 * 1982 | 1983 | ```py 1984 | import this 1985 | ``` 1986 | 1987 | Wait, what's **this**? `this` is love :heart: 1988 | (译注:我认为这句话原汁原味的看才能体会其中的乐趣,就不翻译了😊) 1989 | 1990 | **Output:** 1991 | ``` 1992 | The Zen of Python, by Tim Peters 1993 | 1994 | Beautiful is better than ugly. 1995 | Explicit is better than implicit. 1996 | Simple is better than complex. 1997 | Complex is better than complicated. 1998 | Flat is better than nested. 1999 | Sparse is better than dense. 2000 | Readability counts. 2001 | Special cases aren't special enough to break the rules. 2002 | Although practicality beats purity. 2003 | Errors should never pass silently. 2004 | Unless explicitly silenced. 2005 | In the face of ambiguity, refuse the temptation to guess. 2006 | There should be one-- and preferably only one --obvious way to do it. 2007 | Although that way may not be obvious at first unless you're Dutch. 2008 | Now is better than never. 2009 | Although never is often better than *right* now. 2010 | If the implementation is hard to explain, it's a bad idea. 2011 | If the implementation is easy to explain, it may be a good idea. 2012 | Namespaces are one honking great idea -- let's do more of those! 2013 | ``` 2014 | 2015 | 这些(指上面输出的内容)就是Python之道! 2016 | 2017 | ```py 2018 | >>> love = this 2019 | >>> this is love 2020 | True 2021 | >>> love is True 2022 | False 2023 | >>> love is False 2024 | False 2025 | >>> love is not True or False 2026 | True 2027 | >>> love is not True or False; love is love # 爱(love)是个复杂的东西,不是吗? 2028 | True 2029 | ``` 2030 | 2031 | #### 💡 解释: 2032 | 2033 | * `this`模块是一个针对Python之道做阐述的彩蛋([PEP 20](https://www.python.org/dev/peps/pep-0020))。 2034 | * 但是这并没有完,如果你看看这个模块的实现代码[this.py](https://hg.python.org/cpython/file/c3896275c0f6/Lib/this.py)。你会发现一个更加有趣的事情,那就是这个代码本身就违反了上面输出的Python之道(当然,我觉得这也是唯一一处违反的地方)。 2035 | * 关于`love is not True or False; love is love`这句话,它解释的很对,不是吗? 2036 | 2037 | --- 2038 | 2039 | ### ▶ 这些语句是存在的! 2040 | 2041 | **循环语句对应的`else`语句.** 下面是一个标准的例子: 2042 | 2043 | ```py 2044 | def does_exists_num(l, to_find): 2045 | for num in l: 2046 | if num == to_find: 2047 | print("Exists!") 2048 | break 2049 | else: 2050 | print("Does not exist") 2051 | ``` 2052 | 2053 | **Output:** 2054 | ```py 2055 | >>> some_list = [1, 2, 3, 4, 5] 2056 | >>> does_exists_num(some_list, 4) 2057 | Exists! 2058 | >>> does_exists_num(some_list, -1) 2059 | Does not exist 2060 | ``` 2061 | 2062 | **异常处理对应的`else`语句** 看例子, 2063 | 2064 | ```py 2065 | try: 2066 | pass 2067 | except: 2068 | print("Exception occurred!!!") 2069 | else: 2070 | print("Try block executed successfully...") 2071 | ``` 2072 | 2073 | **Output:** 2074 | ```py 2075 | Try block executed successfully... 2076 | ``` 2077 | 2078 | #### 💡 解释: 2079 | - 当一个`else`语句紧跟在一个循环语句后面的时候,只有当循环语句块内有明确使用`break`退出,否则所有循环结束就会执行`else`语句块。 2080 | - 跟在`try`语句块后面的`else`语句,又叫做“完成语句(completion clause)”。意味着如果`try`语句块的内容顺利运行完毕,那么就会进入到`else`语句块。 2081 | 2082 | --- 2083 | 2084 | ### ▶ 无限(Inpinity) * 2085 | 2086 | **Output (Python 3.x):** 2087 | ```py 2088 | >>> infinity = float('infinity') 2089 | >>> hash(infinity) 2090 | 314159 2091 | >>> hash(float('-inf')) 2092 | -314159 2093 | ``` 2094 | 2095 | #### 💡 解释: 2096 | - 无限(infinity)的哈希值是 10⁵ x π. 2097 | - 有趣的是, 在Python3中,`float('-inf')`的哈希值是"-10⁵ x π", 但是在Python2中是"-10⁵ x e"(正无穷还是一样的哈希值)。 2098 | 2099 | --- 2100 | 2101 | ### ▶ 被修改的类成员 * 2102 | 2103 | ```py 2104 | class Yo(object): 2105 | def __init__(self): 2106 | self.__honey = True 2107 | self.bitch = True 2108 | ``` 2109 | 2110 | **Output:** 2111 | ```py 2112 | >>> Yo().bitch 2113 | True 2114 | >>> Yo().__honey 2115 | AttributeError: 'Yo' object has no attribute '__honey' 2116 | >>> Yo()._Yo__honey 2117 | True 2118 | ``` 2119 | 2120 | 为什么非要`Yo()._Yo__honey`才对? 如果有印度朋友他一定会懂这个梗的(好吧,我猜不会有印度人看一个中文的文章,Yo Yo Honey Singh是一个印度著名rapper,这就是梗,也是为什么这个类要起名Yo,哈哈). 2121 | 2122 | #### 💡 解释: 2123 | 2124 | * [名称混淆](https://en.wikipedia.org/wiki/Name_mangling)是一种用来避免命名空间下命名冲突的技术。 2125 | * 在Python里,如果某个类成员的名称以`__`(两个下划线)开头,并且不以两个以上的下划线结尾(这就意味着你想定义一个私有变量,但是Python是没有私有变量的),那么编辑器就会自动在这个成员名称之前加上`_NameOfTheClass`(在这里就是`_Yo`),来防止子类不小心访问修改到这个成员属性。 2126 | * 所以,当要访问`__honey`这个成员属性的时候,我们需要在前面加上`_Yo`来进行访问。 2127 | 2128 | --- 2129 | 2130 | --- 2131 | 2132 | ## 第五章: 杂项 2133 | 2134 | 2135 | ### ▶ `+=`更快 2136 | 2137 | ```py 2138 | # 使用"+"连接三个字符串: 2139 | >>> timeit.timeit("s1 = s1 + s2 + s3", setup="s1 = ' ' * 100000; s2 = ' ' * 100000; s3 = ' ' * 100000", number=100) 2140 | 0.25748300552368164 2141 | # 使用"+="连接三个字符串: 2142 | >>> timeit.timeit("s1 += s2 + s3", setup="s1 = ' ' * 100000; s2 = ' ' * 100000; s3 = ' ' * 100000", number=100) 2143 | 0.012188911437988281 2144 | ``` 2145 | 2146 | #### 💡 解释: 2147 | + 当需要连接的字符串超过两个的时候,使用`+=`会比使用`+`更快。(比如上面的例子`s1 += s2 + s3`中的`s1`),因为第一个字符串不会在做链接的时候销毁重建(因为`+=`操作符是就地操作的,上面多次已经提到过这个符号特性)。 2148 | 2149 | --- 2150 | 2151 | ### ▶ 超长字符串! 2152 | 2153 | ```py 2154 | #使用加号连接字符串 2155 | def add_string_with_plus(iters): 2156 | s = "" 2157 | for i in range(iters): 2158 | s += "xyz" 2159 | assert len(s) == 3*iters 2160 | 2161 | #使用比特流连接字符串 2162 | def add_bytes_with_plus(iters): 2163 | s = b"" 2164 | for i in range(iters): 2165 | s += b"xyz" 2166 | assert len(s) == 3*iters 2167 | 2168 | #使用format函数连接字符串 2169 | def add_string_with_format(iters): 2170 | fs = "{}"*iters 2171 | s = fs.format(*(["xyz"]*iters)) 2172 | assert len(s) == 3*iters 2173 | 2174 | #使用数组连接字符串 2175 | def add_string_with_join(iters): 2176 | l = [] 2177 | for i in range(iters): 2178 | l.append("xyz") 2179 | s = "".join(l) 2180 | assert len(s) == 3*iters 2181 | 2182 | #将列表转换成字符串 2183 | def convert_list_to_string(l, iters): 2184 | s = "".join(l) 2185 | assert len(s) == 3*iters 2186 | ``` 2187 | 2188 | **Output:** 2189 | ```py 2190 | >>> timeit(add_string_with_plus(10000)) 2191 | 1000 loops, best of 3: 972 µs per loop 2192 | >>> timeit(add_bytes_with_plus(10000)) 2193 | 1000 loops, best of 3: 815 µs per loop 2194 | >>> timeit(add_string_with_format(10000)) 2195 | 1000 loops, best of 3: 508 µs per loop 2196 | >>> timeit(add_string_with_join(10000)) 2197 | 1000 loops, best of 3: 878 µs per loop 2198 | >>> l = ["xyz"]*10000 2199 | >>> timeit(convert_list_to_string(l, 10000)) 2200 | 10000 loops, best of 3: 80 µs per loop 2201 | ``` 2202 | 2203 | 让我们把循环次数提升十倍。 2204 | 2205 | ```py 2206 | >>> timeit(add_string_with_plus(100000)) # 执行时间线性增加 2207 | 100 loops, best of 3: 9.75 ms per loop 2208 | >>> timeit(add_bytes_with_plus(100000)) # 平方式增加 2209 | 1000 loops, best of 3: 974 ms per loop 2210 | >>> timeit(add_string_with_format(100000)) # 线性增加 2211 | 100 loops, best of 3: 5.25 ms per loop 2212 | >>> timeit(add_string_with_join(100000)) # 线性增加 2213 | 100 loops, best of 3: 9.85 ms per loop 2214 | >>> l = ["xyz"]*100000 2215 | >>> timeit(convert_list_to_string(l, 100000)) # 线性增加 2216 | 1000 loops, best of 3: 723 µs per loop 2217 | ``` 2218 | 2219 | #### 💡 解释 2220 | - 关于`timeit`模块的用法可以看[这里](https://docs.python.org/3/library/timeit.html)。这是一个用来测量程序块执行时间的模块。 2221 | - 在Python中,不要试图用`+`来连接一个长字符串,因为`str`这个类型在Python中是一个不可变类型,这就意味着每次两个字符串对象连接程序都需要从左到右把两个字符串的每一个字符都复制一份。比如你现在要连接四个长度为10的字符串,那么程序需要拷贝 (10+10) + ((10+10)+10) + (((10+10)+10)+10) = 90 个字符,而不是40个。而且程序运行时间会随着你需要连接的字符串数量和字符数量呈平方形式的增长。(请参考`add_bytes_with_plus`函数的执行时间对比)。 2222 | - 所以,建议大家使用`.format`或者`%`这种语法(但是,它们在连接短字符串的时候会比`+`号慢一点)。 2223 | - 或者,如果已经提前获得了一个可以迭代的对象,则可以使用`''.join(iterable_object)`这种语法把它连接成字符串,这样的速度会比前几个都快。 2224 | - 至于为什么`add_string_with_plus`不会像`add_bytes_with_plus`着这样成平方形式的提升执行时间,是因为前面例子里已经解释过的`+=`操作符的优化效果。如果把`s += "xyz"`这句话替换成了`s = s + "x" + "y" + "z"`,那么执行时间还是会呈平方形式的增加。 2225 | ```py 2226 | def add_string_with_plus(iters): 2227 | s = "" 2228 | for i in range(iters): 2229 | s = s + "x" + "y" + "z" 2230 | assert len(s) == 3*iters 2231 | 2232 | >>> timeit(add_string_with_plus(10000)) 2233 | 100 loops, best of 3: 9.87 ms per loop 2234 | >>> timeit(add_string_with_plus(100000)) # 平方式增加 2235 | 1 loops, best of 3: 1.09 s per loop 2236 | ``` 2237 | 2238 | --- 2239 | 2240 | ### ▶ 字符串到浮点数的转换 2241 | 2242 | ```py 2243 | a = float('inf') 2244 | b = float('nan') 2245 | c = float('-iNf') #这些字符串是大小写不敏感的 2246 | d = float('nan') 2247 | ``` 2248 | 2249 | **Output:** 2250 | ```py 2251 | >>> a 2252 | inf 2253 | >>> b 2254 | nan 2255 | >>> c 2256 | -inf 2257 | >>> float('some_other_string') 2258 | ValueError: could not convert string to float: some_other_string 2259 | >>> a == -c #inf==inf 2260 | True 2261 | >>> None == None # None==None 2262 | True 2263 | >>> b == d #但是 nan!=nan 为真 2264 | False 2265 | >>> 50/a 2266 | 0.0 2267 | >>> a/a 2268 | nan 2269 | >>> 23 + b 2270 | nan 2271 | ``` 2272 | 2273 | #### 💡 解释: 2274 | 2275 | `'inf'`和`'nan'`是一种特殊的字符串(大小写不敏感),它们两个都可以强制转换成`float`浮点类型,并且分别代表了数学意义上的"无限"(infinity)和"非数字"(not a number)。 2276 | 2277 | --- 2278 | 2279 | ### ▶ 最后一些小特点集合 2280 | 2281 | * `join()`是一个字符串的函数而不是一个列表附属函数(第一次用的人可能会觉得有点别扭) 2282 | 2283 | **💡 解释:** 2284 | 如果`join()`作为一个字符串的方法,那么它就可以操作所有的可迭代类型(list, tuple, iterators)。但是如果它作为了一个列表(list)的方法,那它同样的也需要为另外那些可迭代类型构造专属的函数内容。 2285 | 2286 | * 下面这些句子看着奇怪,但是它们确实都是符合语法规则的 2287 | + `[] = ()` 在语法上是正确的 (把一个元组赋值给一个列表) 2288 | + `'a'[0][0][0][0][0]` 同样也是语法正确的,因为在Python中字符串是一个序列(可迭代的对象就可以使用索引值进行访问里面的元素)。 2289 | + `3 --0-- 5 == 8`和`--5 == 5`也都是语法正确的,并且最后结果都为`True`。 2290 | 2291 | * 给定一个数字类型变量`a`,`++a`和`--a`在Python中都是合法的,但是它们表达的意思和在C语言(或者C++,Java)中是完全不一样的。 2292 | ```py 2293 | >>> a = 5 2294 | >>> a 2295 | 5 2296 | >>> ++a 2297 | 5 2298 | >>> --a 2299 | 5 2300 | ``` 2301 | 2302 | **💡 解释:** 2303 | + 在Python语法中,是没有`++`这个操作符的。这个仅仅代表两个单独的`+`操作符而已。 2304 | + `++a`在Python中会被解析成`+(+a)`,也就等于`a`。同样的,`--a`也是这种情况。 2305 | + 这篇StackOverflow的[文章](https://stackoverflow.com/questions/3654830/why-are-there-no-and-operators-in-python)讨论了Python中没有自增自减符号背后的逻辑关系。 2306 | 2307 | * 多个Python线程并不会并发执行你的*Python代码*(你没看错,不会!)。也许你想同时开很多个线程然后并发执行你写的Python代码,但是,由于Python中存在一个叫[GIL](https://wiki.python.org/moin/GlobalInterpreterLock)(Global Interpreter Lock)的东西,所以最终你的线程还是会一个接着一个执行。Python的线程执行IO阻塞的任务比较拿手,但是如果想在CPU阻塞的任务中获得真正的并发能力,你需要了解一下Python的[multiprocessing](https://docs.python.org/2/library/multiprocessing.html)模块。 2308 | 2309 | * 列表切片的时候如果索引值超出界限并不会报错 2310 | ```py 2311 | >>> some_list = [1, 2, 3, 4, 5] 2312 | >>> some_list[111:] 2313 | [] 2314 | ``` 2315 | 2316 | * 在Python3中,`int('١٢٣٤٥٦٧٨٩')`会返回`123456789`。在Python中,十进制字符不仅包含十进制的数字,还包含了大部分可以表示为十进制数字形式的字符,比如,U+0660, ARABIC_INDIC DIGIT ZERO,一种印度阿拉伯语里代表数字0的字符。不过还没有中文的"一,二,三..."或者"壹,贰,叁..."这种,有人说是因为中文在unicode中的编码是不连续的,不过无论如何,现在确实还没有。这里有一篇[小故事](http://chris.improbable.org/2014/8/25/adventures-in-unicode-digits/)是关于这个特性的,英语版,有兴趣的同学可以看看。 2317 | 2318 | * `'abc'.count('') == 4`。 下面模拟实现了一个相似的`count()`方法, 2319 | ```py 2320 | def count(s, sub): 2321 | result = 0 2322 | for i in range(len(s) + 1 - len(sub)): 2323 | result += (s[i:i + len(sub)] == sub) 2324 | return result 2325 | ``` 2326 | 产生这个结果的原因是子串为空时会匹配上原始字符串长度为0时的字符(总之,看代码你会懂的)。 2327 | 2328 | --- 2329 | 2330 | 2331 | # 贡献 2332 | 2333 | 欢迎任何补丁和修正!详细信息请看 [CONTRIBUTING.md](./CONTRIBUTING.md) 2334 | 2335 | 如果想要参与讨论, 可以选择创建一个新的[issue](https://github.com/true1023/Crazy-Python/issues) 或者加入我创建的gitter[房间](https://gitter.im/Crazy-Python/Lobby) 2336 | 2337 | # 感谢 2338 | 2339 | 这个仓库翻译自 [What the fu*k Python](https://github.com/satwikkansal/wtfpython),迄今为止没谁需要感谢,感谢下原作者[Satwik Kansal](https://github.com/satwikkansal)吧。 2340 | 2341 | 2342 | 2343 | # :mortar_board: 版权声明 2344 | 2345 | [![WTFPL](https://img.shields.io/badge/License-WTFPL%202.0-lightgrey.svg?style=flat-square)](http://www.wtfpl.net/) 2346 | 2347 | :copyright: [True1023](https://github.com/true1023/) 2348 | 2349 | 2350 | 2351 | ## 帮助 2352 | 2353 | 如果你有任何好的想法和意见,请务必要分享给我。 2354 | 2355 | ## 关于pdf出版的问题 2356 | 2357 | 我会在收集并修正一些bug后生成pdf版本的Crazy Python,想要的小伙伴可以[发信](mailto:shi12li12@gmail.com)给我,或者关注公众号**编程行动派**,我也会第一时间在公众号里通知大家。 2358 | 2359 |

2360 | -------------------------------------------------------------------------------- /individual/total.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

疯狂的Python! 🐍

4 |

一些有趣的鲜为人知的Python特性集合.

5 | 6 | [![WTFPL](https://img.shields.io/badge/License-WTFPL%202.0-lightgrey.svg?style=flat-square)](http://www.wtfpl.net/) 7 | [![Join the chat at https://gitter.im/Crazy-Python/Lobby](https://badges.gitter.im/Crazy-Python/Lobby.svg)](https://gitter.im/Crazy-Python/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 8 | 9 | 10 | *本文翻译自[What the f*ck Python!](https://github.com/satwikkansal/wtfpython)* 11 | 12 | --- 13 | 14 | Python作为一个设计优美的交互式脚本语言,提供了许多人性化的语法。但是也因为这个原因,有些Python的代码片段并不会按照用户想象的那样运行。 15 | 16 | 这篇文章就让我们总结一下那些Python里反直觉的代码片段,并且深入研究一下其中的运行原理。 17 | 18 | 下面的某些例子可能并不像是标题说的那样....嗯....反直觉,但是它们依旧会带你揭示一些你从来没有意识到的Python语言特性。 19 | 20 | 而且,我发现这也是一种很好的学习编程语言的方法,不过前提是你不要认为这篇文章会告诉你一切,抛砖引玉而已。相信我,你需要的知识都隐藏在互联网的某个角落里。 21 | 22 | 如果你已经写了很久的Python代码,你可以把下面的这些例子当做一个挑战,试一试自己能不能在第一次就猜对结果。也许某些例子里的坑你已经遇到过并且解决了,那么再次看见这个坑的时候我想你会为当时自己的努力而自豪的。:sweat_smile: 23 | 24 | 好了,下面是正文! 25 | 26 |

目录

27 | 28 | 29 | - [示例结构说明](#示例结构说明) 30 | - [使用方法](#使用方法) 31 | - [:eyes: 例子](#eyes-例子) 32 | - [第一章: 撕裂大脑](#第一章-撕裂大脑) 33 | - [▶ 善变的字符串 *](#▶-善变的字符串-) 34 | - [▶ 不变的哈希值](#▶-不变的哈希值) 35 | - [▶ 说了要执行就一定会执行!](#▶-说了要执行就一定会执行) 36 | - [▶ 鸠占鹊巢 *](#▶-鸠占鹊巢-) 37 | - [▶ 神奇赋值法](#▶-神奇赋值法) 38 | - [▶ 时间的误会](#▶-时间的误会) 39 | - [▶ 特殊的数字们](#▶-特殊的数字们) 40 | - [▶ 三子棋之一步取胜法](#▶-三子棋之一步取胜法) 41 | - [▶ 没脑子的函数](#▶-没脑子的函数) 42 | - [▶ `is not ...` 并不是 `is (not ...)`](#▶-is-not--并不是--is-not-) 43 | - [▶ 尾部的逗号](#▶-尾部的逗号) 44 | - [▶ 最后一个反斜杠](#▶-最后一个反斜杠) 45 | - [▶ 纠结的not](#▶-纠结的not) 46 | - [▶ 只剩一半的三引号](#▶-只剩一半的三引号) 47 | - [▶ 消失的午夜零点](#▶-消失的午夜零点) 48 | - [▶ 站错队的布尔型](#▶-站错队的布尔型) 49 | - [▶ 类属性与类实例属性](#▶-类属性与类实例属性) 50 | - [▶ None的产生](#▶-none的产生) 51 | - [▶ 不可修改的元组](#▶-不可修改的元组) 52 | - [▶ 消失的变量e](#▶-消失的变量e) 53 | - [▶ 亦真还假](#▶-亦真还假) 54 | - [▶ 转瞬即空](#▶-转瞬即空) 55 | - [▶ 子类的关系 *](#▶-子类的关系-) 56 | - [▶ 神秘的键值转换 *](#▶-神秘的键值转换-) 57 | - [▶ 看你能不能猜到这个的结果?](#▶-看你能不能猜到这个的结果) 58 | - [第二章: 瞒天过海](#第二章-瞒天过海) 59 | - [▶ 无效的一行?](#▶-无效的一行) 60 | - [▶ 移形换位 *](#▶-移形换位-) 61 | - [▶ 到底哪里出错了呢?](#▶-到底哪里出错了呢) 62 | - [第三章: 隐藏的陷阱](#第三章-隐藏的陷阱) 63 | - [▶ 不要在迭代一个字典的时候修改这个字典!](#▶-不要在迭代一个字典的时候修改这个字典) 64 | - [▶ 删不掉的对象 *](#▶-删不掉的对象-) 65 | - [▶ 删除正在迭代中的列表项](#▶-删除正在迭代中的列表项) 66 | - [▶ 被泄露出去的循环变量!](#▶-被泄露出去的循环变量) 67 | - [▶ 小心那些有默认值的可变参数!](#▶-小心那些有默认值的可变参数) 68 | - [▶ 抓住这些异常!](#▶-抓住这些异常) 69 | - [▶ 相同的操作,不同的结果](#▶-相同的操作不同的结果) 70 | - [▶ 作用域之外的变量](#▶-作用域之外的变量) 71 | - [▶ 小心那些链式操作符](#▶-小心那些链式操作符) 72 | - [▶ 被忽略的类变量](#▶-被忽略的类变量) 73 | - [▶ 小心处理元组](#▶-小心处理元组) 74 | - [第四章: 一起来找些有趣的东西!](#第四章-一起来找些有趣的东西) 75 | - [▶ 人生苦短,我用Python *](#▶-人生苦短我用python-) 76 | - [▶ 为什么要用`goto`? *](#▶-为什么要用goto-) 77 | - [▶ 试试用大括号? *](#▶-试试用大括号-) 78 | - [▶ 不等号的争议 *](#▶-不等号的争议-) 79 | - [▶ 就算Python也知道爱是个复杂的东西 *](#▶-就算python也知道爱是个复杂的东西-) 80 | - [▶ 这些语句是存在的!](#▶-这些语句是存在的) 81 | - [▶ 无限(Inpinity) *](#▶-无限inpinity-) 82 | - [▶ 被修改的类成员 *](#▶-被修改的类成员-) 83 | - [第五章: 杂项](#第五章-杂项) 84 | - [▶ `+=`更快](#▶-更快) 85 | - [▶ 超长字符串!](#▶-超长字符串) 86 | - [▶ 字符串到浮点数的转换](#▶-字符串到浮点数的转换) 87 | - [▶ 最后一些小特点集合](#▶-最后一些小特点集合) 88 | - [贡献](#贡献) 89 | - [感谢](#感谢) 90 | - [:mortar_board: 版权声明](#mortar_board-版权声明) 91 | - [帮助](#帮助) 92 | - [关于pdf出版的问题](#关于pdf出版的问题) 93 | 94 | 95 | 96 | # 示例结构说明 97 | 98 | 99 | 100 | 下面是每个例子里通用的结构说明: 101 | 102 | > ### ▶ 这里是例子的标题 * 103 | > 首先是例子的标题,如果某个标题后面带有星号,说明这是一个新加入的例子。 104 | > ```py 105 | > # 第一个代码段里面会有一些初始化代码 106 | > # 为后续的输出代码段做准备... 107 | > ``` 108 | > 109 | > **Output (Python version):** 110 | > ```py 111 | > >>> python语句,执行某个命令 112 | > 一些输出(可能你想得到,也可能想不到) 113 | > ``` 114 | > (可选): 有可能会说明一下上面输出的内容 115 | > 116 | > 117 | > #### :bulb: 解释: 118 | > 119 | > * 简短的介绍发生了什么和为什么会产生这些输出。 120 | > ```py 121 | > 一些初始化代码 122 | > ``` 123 | > **Output:** 124 | > ```py 125 | > >>> 执行代码 # 这些代码会展示为何会有上方那些输出内容 126 | > ``` 127 | 128 | **注意:** 所有的代码都是在 Python 3.5.2 环境下测试通过,理论上如果没有特殊声明,可以在所有的Python版本下运行。 129 | 130 | # 使用方法 131 | 132 | 在我看来,为了充分的利用这个仓库里的所有例子,最好的办法就是按照顺序把每个例子挨个看一遍: 133 | - 仔细阅读每个例子的初始化代码。如果你是一个经验丰富的Python程序员,那么大部分时候你都可以知道初始化代码执行后具体会发生什么。 134 | - 阅读输出结果并且, 135 | + 检查输出结果是否和你想的一样 136 | + 确认你是否知道产生这种结果背后的原理, 137 | - 如果不知道,那么请仔细阅读解释章节(如果看完解释还是不懂的话,那就提交一个 [issue](https://github.com/true1023/Crazy-Python/issues) 吧) 138 | - 如果知道,那么给就自己点个赞,继续看下一个例子 139 | 140 | # :eyes: 例子 141 | 142 | ## 第一章: 撕裂大脑 143 | 144 | ### ▶ 善变的字符串 * 145 | 146 | 1\. 147 | ```py 148 | >>> a = "crazy_python" 149 | >>> id(a) 150 | 2387669241224 151 | >>> id("crazy" + "_" + "python") # 注意这两个字符串的id号是一样的 152 | 2387669241224 153 | ``` 154 | 155 | 2\. 156 | ```py 157 | >>> a = "crazy" 158 | >>> b = "crazy" 159 | >>> a is b 160 | True 161 | 162 | >>> a = "crazy!" 163 | >>> b = "crazy!" 164 | >>> a is b 165 | False 166 | 167 | >>> a, b = "crazy!", "crazy!" 168 | >>> a is b 169 | True 170 | ``` 171 | 172 | 3\. 173 | ```py 174 | >>> 'a' * 20 is 'aaaaaaaaaaaaaaaaaaaa' 175 | True 176 | >>> 'a' * 21 is 'aaaaaaaaaaaaaaaaaaaaa' 177 | False 178 | ``` 179 | 180 | 很不可思议,对吧? 181 | 182 | #### :bulb: 解释: 183 | + 上面这种特性是CPython的一种编译器优化技术(叫做字符串驻留技术)。就是如果将要创建的字符串在之前已经创建过并且驻留在了内存没有释放,那么CPython不会创建一个新的实例,而是会把指针指向已经存在于内存的老的字符串实例。 184 | + 如果一个字符串实例已经驻留在了内存中,那么后续所有跟它值一样的变量都可以将指针指向这个内存中的字符串实例(这样就会节省内存空间) 185 | + 在上面这几段程序代码中,有几段的字符串明显驻留在了内存中供多个变量引用。 决定一个字符串是否会驻留在内存中是由这个字符串的实现方法决定的。下面是一些判断字符串变量是否会驻留内存的方法: 186 | * 所有长度为1或者0的字符串,全部会驻留 187 | * 编译阶段(也就是把源代码编译成.pyc文件的阶段)的字符串被驻留在内存,运行阶段就不会(`'crazy'`会驻留在内存,但是`''.join(['c','r','a','z','y'])`就不会) 188 | * 如果字符串包含除了ASCII字符,数字和下划线以外的字符,那么这个字符串就不会驻留内存。这就是为什么`'crazy!'`赋值给`a`,`b`的时候得到的结果是False,因为`!`感叹号。CPython中对这个特点的具体实现请参考[这里](https://github.com/python/cpython/blob/3.6/Objects/codeobject.c#L19) 189 | 190 | + 当变量`a`和`b`在同一行赋值`"crazy!"`的时候,Python的解释器会创建一个新的字符串实例,然后把这两个变量同时指向这一个实例。但是如果你用两行实现赋值的话,Python解释器就不会知道已经有一个`'crazy!'`的字符串实例存在了(因为根据上面的规则,`'crazy!'`实例不会进行内存驻留供后面的语句引用)。这是一种专门在交互环境下编译器的一种优化方法。 191 | + 常量折叠是Python实现的一种[窥孔优化(Peephole optimization)](https://baike.baidu.com/item/%E7%AA%A5%E5%AD%94%E4%BC%98%E5%8C%96)技术。意思就是`'a'*20`这个语句在编译的时候会自动替换成`'aaaaaaaaaaaaaaaaaaaa'`这个变量,用来减少运行时的运算时钟周期(`'a'*20`需要多执行20次乘法运算)。常量折叠只在字符串长度小于等于20的时候发生(至于为什么?想想如果有一个`'a*10**10'`这样的语句,折叠后需要多大一个`.pyc`才能存下折叠后的字符串啊)。[这里](https://github.com/python/cpython/blob/3.6/Python/peephole.c#L288)是实现这种技术的实现代码。 192 | 193 | --- 194 | 195 | 196 | ### ▶ 不变的哈希值 197 | 198 | 1\. 199 | ```py 200 | some_dict = {} 201 | some_dict[5.5] = "Ruby" 202 | some_dict[5.0] = "JavaScript" 203 | some_dict[5] = "Python" 204 | ``` 205 | 206 | **Output:** 207 | ```py 208 | >>> some_dict[5.5] 209 | "Ruby" 210 | >>> some_dict[5.0] 211 | "Python" 212 | >>> some_dict[5] 213 | "Python" 214 | ``` 215 | "Python" 把之前的 "JavaScript" 覆盖掉了吗? 216 | 217 | #### :bulb: 解释 218 | 219 | * Python的字典结构是根据key值的哈希值判断两个key值是否相等的 220 | * 在Python中,不变对象(Immutable objects)的值如果一样,那么它们的哈希值肯定也一样 221 | ```py 222 | >>> 5 == 5.0 223 | True 224 | >>> hash(5) == hash(5.0) 225 | True 226 | ``` 227 | **注意:** 有些对象有不同的值,但是它们的哈希值也有可能是一样的(所谓的哈希冲突) 228 | * 当`some_dict[5] = "Python"`这句话执行的时候, "Python"这个字符串就会覆盖掉"JavaScript"这个值,因为在Python看来,`5`和`5.0`的哈希值是一样的,也就是说对于字典结构他们对应的是一个key值。 229 | * 在 StackOverflow 上面有一个[回答](https://stackoverflow.com/a/32211042/4354153)对Python的这个特性解释的很棒。 230 | 231 | --- 232 | 233 | ### ▶ 说了要执行就一定会执行! 234 | 235 | ```py 236 | def some_func(): 237 | try: 238 | return 'from_try' 239 | finally: 240 | return 'from_finally' 241 | ``` 242 | 243 | **Output:** 244 | ```py 245 | >>> some_func() 246 | 'from_finally' 247 | ``` 248 | 249 | #### 💡 解释: 250 | 251 | - 当在`try`语句块中遇到`return`,`break`或者`continue`的时候,如果是"try...finlly"语句块,那么在执行完`try`语句块里的内容后,依然会执行`finally`语句块的内容。 252 | - 当`return`语句返回一个值的时候,那么因为在`finally`语句块中的`return`语句是最后执行的,那么返回的值就永远都是`finally`语句块中`return`语句返回的值。 253 | 254 | --- 255 | 256 | ### ▶ 鸠占鹊巢 * 257 | 258 | ```py 259 | class Crazy: 260 | pass 261 | ``` 262 | 263 | **Output:** 264 | ```py 265 | >>> Crazy() == Crazy() # 两个类实例是不同的 266 | False 267 | >>> Crazy() is Crazy() # 它们的id号也是不一样的 268 | False 269 | >>> hash(Crazy()) == hash(Crazy()) # 它们的哈希值按说也应该不一样 270 | True 271 | >>> id(Crazy()) == id(Crazy()) 272 | True 273 | ``` 274 | 275 | #### :bulb: 解释: 276 | 277 | * 当`id`函数被调用的时候,Python创建了一个`Crazy`类实例,然后把这个实例传给了`id`函数。然后`id`函数返回这个实例的"id"号(实际上就是这个实例在内存中的地址),接着这个实例就被丢弃并且销毁了。 278 | * 当我们紧接着再做一遍上面的步骤的时候,Python会把同一块内存空间分配给第二次创建的`Crazy`实例。又因为在CPython中`id`函数使用的是内存地址作为返回值,所以就会出现两个对象实例的id号相同的情况了。 279 | * 所以,"对象的id是唯一的"这句话有一个前提条件是"在这个对象的生命周期内"。当这个对象在内存被销毁以后,其他的对象就可以占用它之前所用的内存空间产生一样的id号。 280 | * 但是为什么上面的例子里`is`操作符却产生了`False`? 我们再看一个例子。 281 | ```py 282 | class Crazy(object): 283 | def __init__(self): print("I") 284 | def __del__(self): print("D") 285 | ``` 286 | 287 | **Output:** 288 | ```py 289 | >>> Crazy() is Crazy() 290 | I 291 | I 292 | D 293 | D 294 | False 295 | >>> id(Crazy()) == id(Crazy()) 296 | I 297 | D 298 | I 299 | D 300 | True 301 | ``` 302 | 现在你可以发现, 不同的调用方法会对实例销毁的时间产生不同的影响。 303 | 304 | --- 305 | 306 | ### ▶ 神奇赋值法 307 | 308 | ```py 309 | some_string = "crazy" 310 | some_dict = {} 311 | for i, some_dict[i] in enumerate(some_string): 312 | pass 313 | ``` 314 | 315 | **Output:** 316 | ```py 317 | >>> some_dict # 一个带引索的字典被创建. 318 | {0: 'c', 1: 'r', 2: 'a', 3: 'z', 4: 'y'} 319 | ``` 320 | 321 | #### :bulb: 解释: 322 | 323 | * 一个 `for` 语句在[Python语法](https://docs.python.org/3/reference/grammar.html)中是这么定义的: 324 | ``` 325 | for_stmt: 'for' exprlist 'in' testlist ':' suite ['else' ':' suite] 326 | ``` 327 | `exprlist` 是一组被赋值的变量. 这就等于说这组变量在**每次迭代**开始的时候都会执行一次 `{exprlist} = {next_value}` 。 328 | 下面这个例子很好的解释了上面想要表达的意思: 329 | ```py 330 | for i in range(4): 331 | print(i) 332 | i = 10 333 | ``` 334 | 335 | **Output:** 336 | ``` 337 | 0 338 | 1 339 | 2 340 | 3 341 | ``` 342 | 343 | 是不是以为上面的循环就会执行一次? 344 | 345 | **:bulb: 解释:** 346 | - 在上面这个循环中,`i=10`这个赋值语句不会整个迭代过程产生任何影响。因为在每次迭代开始前,迭代函数(在这里是`range(4)`)都会把下一次的值赋值给目标变量(在这里是`i`)。 347 | 348 | * 再来看上面的例子,`enumerate(some_string)`这个函数会在每次迭代的时候产生两个值,分别是`i`(一个从0开始的索引值)和一个字符(来自`some_string`的值)。然后这两个值会分别赋值给`i`和`some_dict[i]`。把刚才的循环展开来看就像是下面这样: 349 | ```py 350 | >>> i, some_dict[i] = (0, 'c') 351 | >>> i, some_dict[i] = (1, 'r') 352 | >>> i, some_dict[i] = (2, 'a') 353 | >>> i, some_dict[i] = (3, 'z') 354 | >>> i, some_dict[i] = (4, 'y') 355 | >>> some_dict 356 | ``` 357 | 358 | --- 359 | 360 | ### ▶ 时间的误会 361 | 362 | 1\. 363 | ```py 364 | array = [1, 8, 15] 365 | g = (x for x in array if array.count(x) > 0) 366 | array = [2, 8, 22] 367 | ``` 368 | 369 | **Output:** 370 | ```py 371 | >>> print(list(g)) 372 | [8] 373 | ``` 374 | 375 | 2\. 376 | 377 | ```py 378 | array_1 = [1,2,3,4] 379 | g1 = (x for x in array_1) 380 | array_1 = [1,2,3,4,5] 381 | 382 | array_2 = [1,2,3,4] 383 | g2 = (x for x in array_2) 384 | array_2[:] = [1,2,3,4,5] 385 | ``` 386 | 387 | **Output:** 388 | ```py 389 | >>> print(list(g1)) 390 | [1,2,3,4] 391 | 392 | >>> print(list(g2)) 393 | [1,2,3,4,5] 394 | ``` 395 | 396 | #### :bulb: 解释 397 | 398 | - 在[生成器](https://wiki.python.org/moin/Generators)表达式中,`in`语句会在声明阶段求值,但是条件判断语句(在这里是`array.count(x) > 0`)会在真正的运行阶段(runtime)求值。 399 | - 在生成器运行之前,`array`已经被重新赋值为`[2, 8, 22]`了,所以这个时候再用`count`函数判断`2`,`8`,`22`在原列表中的数量,只有`8`是数量大于`0`的,所以最后这个生成器只返回了一个`8`是符合条件的。 400 | - 在第二部分中,`g1`,`g2`输出结果不同,是因为对`array_1`和`array_2`的赋值方法不同导致的。 401 | - 在第一个例子中, `array_1`绑定了一个新的列表对象`[1,2,3,4,5]`(可以理解成`array_1`的指针指向了一个新的内存地址),而且在这之前,`in`语句已经在声明时就为`g1`绑定好了旧的列表对象`[1,2,3,4]`(这个就对象也没有随着新对象的赋值而销毁)。所以这时候`g1`和`array_1`是指向不同的对象地址的。 402 | - 在第二个例子中,由于切片化的赋值,`array_2`并没有绑定(指向)新对象,而是将旧的对象`[1,2,3,4]`更新(也就是说旧对象的内存地址被新对象占用了)成了新的对象`[1,2,3,4,5]`。所以,`g2`和`array_2`依旧同时指向一个地址,所以都更新成了新对象`[1,2,3,4,5]`。 403 | 404 | --- 405 | 406 | ### ▶ 特殊的数字们 407 | 408 | 下面这个例子在网上非常的流行。 409 | 410 | ```py 411 | >>> a = 256 412 | >>> b = 256 413 | >>> a is b 414 | True 415 | 416 | >>> a = 257 417 | >>> b = 257 418 | >>> a is b 419 | False 420 | 421 | >>> a = 257; b = 257 422 | >>> a is b 423 | True 424 | ``` 425 | 426 | #### :bulb: 解释: 427 | 428 | **`is`和`==`的区别** 429 | 430 | * `is` 操作符会检查两边的操作数是否引用的是同一个对象(也就是说,会检查两个操作数的id号是否匹配)。 431 | * `==` 操作符会比较两个操作数的值是否一样。 432 | * 所以说`is`是比较引用地址是否相同,`==`是比较值是否相同。下面的例子解释的比较清楚, 433 | ```py 434 | >>> [] == [] 435 | True 436 | >>> [] is [] # 这里的两个空list分配了不同的内存地址 437 | False 438 | ``` 439 | 440 | **`256` 是一个已经存在于内存的对象 但是 `257` 不是** 441 | 442 | 当你启动一个Python解释器的时候,数字`-5`到`256`就会自动加载进内存。 这些数字都是一些比较常用的数字,所以Python解释器会把他们提前准备好以备以后使用。 443 | 444 | 下面这段话摘抄自 https://docs.python.org/3/c-api/long.html (已经翻译为中文) 445 | > The current implementation keeps an array of integer objects for all integers between -5 and 256, when you create an int in that range you just get back a reference to the existing object. So it should be possible to change the value of 1. I suspect the behavior of Python, in this case, is undefined.:-) 446 | > 447 | > 当前的实现方法是,维护一个从-5到256的整数数组,当你使用其中某一个数字的时候,系统会自动为你引用到已经存在的对象上去。我认为应该让它可以改变数字1的值。不过就现在来说,Python还没有这个功能。:-) 448 | 449 | ```py 450 | >>> id(256) 451 | 10922528 452 | >>> a = 256 453 | >>> b = 256 454 | >>> id(a) 455 | 10922528 456 | >>> id(b) 457 | 10922528 458 | >>> id(257) 459 | 140084850247312 460 | >>> x = 257 461 | >>> y = 257 462 | >>> id(x) 463 | 140084850247440 464 | >>> id(y) 465 | 140084850247344 466 | ``` 467 | 468 | Python解释器在执行`y = 257`的时候还不能意识到我们之前已经创建过了一个值为`257`的对象,所以它又在内存创建了一个新的对象。 469 | 470 | **当`a`和`b`变量在同一行赋值并且所赋值相等时,它们会引用到同一个对象** 471 | 472 | ```py 473 | >>> a, b = 257, 257 474 | >>> id(a) 475 | 140640774013296 476 | >>> id(b) 477 | 140640774013296 478 | >>> a = 257 479 | >>> b = 257 480 | >>> id(a) 481 | 140640774013392 482 | >>> id(b) 483 | 140640774013488 484 | ``` 485 | 486 | * 当a和b在同一行被赋值为`257`的时候, Python解释器会创建一个新的对象,然后把这两个变量同时引用到这个新的对象。如果你分开两行赋值两个变量,那么解释器不会“知道”之前自己已经创建过一个同样的`257`对象了。 487 | * 这是一种专门针对交互式解释器环境的优化机制。 当你在控制面板中敲入两行命令的时候,这两行命令是分开编译的,所以他们也会单独进行优化。如果你准备把这个例子(两行分别赋值的例子)写进`.py`文件然后进行测试,那么你会发现结果跟写在一行是一样的,因为文件里的代码是一次性编译的。 488 | 489 | --- 490 | 491 | ### ▶ 三子棋之一步取胜法 492 | 493 | ```py 494 | # 首先先来初始化一个1*3的一维数组 495 | row = [""]*3 #row i['', '', ''] 496 | # 然后再用二维数组模拟一个3*3的棋盘 497 | board = [row]*3 498 | ``` 499 | 500 | **Output:** 501 | ```py 502 | >>> board 503 | [['', '', ''], ['', '', ''], ['', '', '']] 504 | >>> board[0] 505 | ['', '', ''] 506 | >>> board[0][0] 507 | '' 508 | >>> board[0][0] = "X" 509 | >>> board 510 | [['X', '', ''], ['X', '', ''], ['X', '', '']] 511 | ``` 512 | 513 | 我们只赋值了一个“X”为什么会出来三个呢? 514 | 515 | #### :bulb: 解释: 516 | 517 | 当我们初始化`row`变量的时候,下图显示的是内存中的变化 518 | 519 | ![image](./assets/1=3/after_row_initialized.png) 520 | 521 | 接着当变量`board`通过`[row]*3`初始化后,下图显示了内存的变化(其实最终每一个变量`board[0]`,`board[1]`,`board[2]`都引用了同一个`row`对象的内存地址) 522 | 523 | ![image](./assets/1=3/after_board_initialized.png) 524 | 525 | 我们可以通过不使用`row`变量来阻止这种情况的发生 526 | 527 | ```py 528 | >>> board = [['']*3 for _ in range(3)] 529 | >>> board[0][0] = "X" 530 | >>> board 531 | [['X', '', ''], ['', '', ''], ['', '', '']] 532 | ``` 533 | 534 | --- 535 | 536 | ### ▶ 没脑子的函数 537 | 538 | ```py 539 | funcs = [] 540 | results = [] 541 | for x in range(7): 542 | def some_func(): 543 | return x 544 | funcs.append(some_func) 545 | results.append(some_func()) 546 | 547 | funcs_results = [func() for func in funcs] 548 | ``` 549 | 550 | **Output:** 551 | ```py 552 | >>> results 553 | [0, 1, 2, 3, 4, 5, 6] 554 | >>> funcs_results 555 | [6, 6, 6, 6, 6, 6, 6] 556 | ``` 557 | 虽然我们每次把`some_func`函数加入到`funcs`列表里的时候`x`都不一样,但是`funcs`列表里的所有函数都返回了6. 558 | 559 | //下面这段代码也是这样 560 | 561 | ```py 562 | >>> powers_of_x = [lambda x: x**i for i in range(10)] 563 | >>> [f(2) for f in powers_of_x] 564 | [512, 512, 512, 512, 512, 512, 512, 512, 512, 512] 565 | ``` 566 | 567 | #### :bulb: 解释 568 | 569 | - 当我们在一个循环中定义一个函数,并且在函数体中用了循环中的变量时,这个函数只会绑定这个变量本身,并不会绑定当前变量循环到的值。所以最终所有在循环中定义的函数都会使用循环变量最后的值做计算。 570 | 571 | - 如果你想实现心中想的那种效果,可以把循环变量当做一个参数传递进函数体。**为什么这样可以呢?**因为这样在函数作用域内会重新定义一个变量,不是循环里面的那个变量了。 572 | 573 | ```py 574 | funcs = [] 575 | for x in range(7): 576 | def some_func(x=x): 577 | return x 578 | funcs.append(some_func) 579 | ``` 580 | 581 | **Output:** 582 | ```py 583 | >>> funcs_results = [func() for func in funcs] 584 | >>> funcs_results 585 | [0, 1, 2, 3, 4, 5, 6] 586 | ``` 587 | 588 | --- 589 | 590 | ### ▶ `is not ...` 并不是 `is (not ...)` 591 | 592 | ```py 593 | >>> 'something' is not None 594 | True 595 | >>> 'something' is (not None) 596 | False 597 | ``` 598 | 599 | #### :bulb: 解释 600 | 601 | - `is not` 是一个单独的二元运算符, 和分开使用的`is`和`not`作用是不同的。 602 | - `is not` 只有在两边的操作数相同时(id相同)结果才为`False`,否则为`True` 603 | 604 | --- 605 | 606 | ### ▶ 尾部的逗号 607 | 608 | **Output:** 609 | ```py 610 | >>> def f(x, y,): 611 | ... print(x, y) 612 | ... 613 | >>> def g(x=4, y=5,): 614 | ... print(x, y) 615 | ... 616 | >>> def h(x, **kwargs,): 617 | File "", line 1 618 | def h(x, **kwargs,): 619 | ^ 620 | SyntaxError: invalid syntax 621 | >>> def h(*args,): 622 | File "", line 1 623 | def h(*args,): 624 | ^ 625 | SyntaxError: invalid syntax 626 | ``` 627 | 628 | #### :bulb: 解释: 629 | 630 | - 末尾的逗号在函数参数列表最后并不总是合法的 631 | - 在Python中,参数列表里,有一部分使用前导逗号分隔的,有一部分是用后导逗号分隔的(比如`**kwargs`这种参数用前导逗号分隔,正常参数`x`用后导逗号分隔)。而这种情况就会导致有些参数列表里的逗号前后都没有用到,就会产生冲突导致编译失败。 632 | - **注意** 这种尾部逗号的问题已经在Python 3.6中被[修复](https://bugs.python.org/issue9232)了。然后[这里](https://bugs.python.org/issue9232#msg248399)有对各种尾部逗号用法的讨论。 633 | 634 | --- 635 | 636 | ### ▶ 最后一个反斜杠 637 | 638 | **Output:** 639 | ``` 640 | >>> print("\\ C:\\") 641 | \ C:\ 642 | >>> print(r"\ C:") 643 | \ C: 644 | >>> print(r"\ C:\") 645 | 646 | File "", line 1 647 | print(r"\ C:\") 648 | ^ 649 | SyntaxError: EOL while scanning string literal 650 | ``` 651 | 652 | #### :bulb: 解释 653 | 654 | - 如果字符串前面声明了`r`,说明后面紧跟着的是一个原始字符串,反斜杠在这种字符串中是没有特殊意义的 655 | ```py 656 | >>> print(repr(r"craz\"y")) 657 | 'craz\\"y' 658 | ``` 659 | - 解释器实际上是怎么做的呢,虽然看起来仅仅是改变了反斜杠的转义特性,实际上,它(反斜杠)会把自己和紧跟着自己的下一个字符一起传入到解释器,用来供解释器做判断和转换。这也就是为什么当反斜杠在最后一个字符的时候会报错。 660 | 661 | --- 662 | 663 | ### ▶ 纠结的not 664 | 665 | ```py 666 | x = True 667 | y = False 668 | ``` 669 | 670 | **Output:** 671 | ```py 672 | >>> not x == y 673 | True 674 | >>> x == not y 675 | File "", line 1 676 | x == not y 677 | ^ 678 | SyntaxError: invalid syntax 679 | ``` 680 | 681 | #### :bulb: 解释: 682 | 683 | * 操作符的优先级会影响表达式的计算顺序,并且在Python里,`==`操作符的优先级要高于`not`操作符。 684 | * 所以`not x == y`等于 `not (x == y)`,又等于`not (True == False)`,最终计算结果就会是`True`。 685 | * 但是`x == not y`会报错是因为这个表达式可以等价于`(x == not) y`,而不是我们第一眼认为的`x == (not y)`。 686 | 687 | --- 688 | 689 | ### ▶ 只剩一半的三引号 690 | 691 | **Output:** 692 | ```py 693 | >>> print('crazypython''') 694 | wtfpython 695 | >>> print("crazypython""") 696 | wtfpython 697 | >>> # 下面的语句将会产生语法错误 698 | >>> # print('''crazypython') 699 | >>> # print("""crazypython") 700 | ``` 701 | 702 | #### :bulb: 解释: 703 | + Python支持隐试的[字符串连接](https://docs.python.org/2/reference/lexical_analysis.html#string-literal-concatenation),比如下面这样, 704 | ``` 705 | >>> print("crazy" "python") 706 | crazypython 707 | >>> print("crazy" "") # or "crazy""" 708 | crazy 709 | ``` 710 | + 在Python中,`'''` 和 `"""` 也是一种字符串界定符,所以如果Python解释器发现了其中一个,那么就会一直在后面找对称的另一个界定符,这也就是为什么上面例子里注释掉的语句会有语法错误,因为解释器在后面找不到和前面`'''`或`"""`配对的界定符。 711 | 712 | --- 713 | 714 | ### ▶ 消失的午夜零点 715 | 716 | ```py 717 | from datetime import datetime 718 | 719 | midnight = datetime(2018, 1, 1, 0, 0) 720 | midnight_time = midnight.time() 721 | 722 | noon = datetime(2018, 1, 1, 12, 0) 723 | noon_time = noon.time() 724 | 725 | if midnight_time: 726 | print("Time at midnight is", midnight_time) 727 | 728 | if noon_time: 729 | print("Time at noon is", noon_time) 730 | ``` 731 | 732 | **Output:** 733 | ```sh 734 | ('Time at noon is', datetime.time(12, 0)) 735 | ``` 736 | 午夜时间并没有被打印出来 737 | 738 | #### :bulb: 解释: 739 | 740 | 在Python 3.5以前, 对于被赋值为UTC零点的`datetime.time`对象的布尔值,会被认为是`False`。这是一个在用`if obj:`这种语句的时候经常会忽略的特性,所以我们在写这种`if`语句的时候,要注意判断`obj`是否等于`null`或者空。 741 | 742 | --- 743 | 744 | ### ▶ 站错队的布尔型 745 | 746 | 1\. 747 | ```py 748 | # 一个计算列表里布尔型和Int型数量的例子 749 | mixed_list = [False, 1.0, "some_string", 3, True, [], False] 750 | integers_found_so_far = 0 751 | booleans_found_so_far = 0 752 | 753 | for item in mixed_list: 754 | if isinstance(item, int): 755 | integers_found_so_far += 1 756 | elif isinstance(item, bool): 757 | booleans_found_so_far += 1 758 | ``` 759 | 760 | **Output:** 761 | ```py 762 | >>> booleans_found_so_far 763 | 0 764 | >>> integers_found_so_far 765 | 4 766 | ``` 767 | 768 | 2\. 769 | ```py 770 | another_dict = {} 771 | another_dict[True] = "JavaScript" 772 | another_dict[1] = "Ruby" 773 | another_dict[1.0] = "Python" 774 | ``` 775 | 776 | **Output:** 777 | ```py 778 | >>> another_dict[True] 779 | "Python" 780 | ``` 781 | 782 | 3\. 783 | ```py 784 | >>> some_bool = True 785 | >>> "crazy"*some_bool 786 | 'crazy' 787 | >>> some_bool = False 788 | >>> "crazy"*some_bool 789 | '' 790 | ``` 791 | 792 | #### :bulb: 解释: 793 | 794 | * 布尔型(Booleans)是 `int`类型的一个子类型(bool is instance of int in Python) 795 | ```py 796 | >>> isinstance(True, int) 797 | True 798 | >>> isinstance(False, int) 799 | True 800 | ``` 801 | 802 | * `True`的整形值是`1`,`False`的整形值是`0` 803 | ```py 804 | >>> True == 1 == 1.0 and False == 0 == 0.0 805 | True 806 | ``` 807 | 808 | * StackOverFlow有针对这个问题背后原理的[解答](https://stackoverflow.com/a/8169049/4354153)。 809 | 810 | --- 811 | 812 | ### ▶ 类属性与类实例属性 813 | 814 | 1\. 815 | ```py 816 | class A: 817 | x = 1 818 | 819 | class B(A): 820 | pass 821 | 822 | class C(A): 823 | pass 824 | ``` 825 | 826 | **Ouptut:** 827 | ```py 828 | >>> A.x, B.x, C.x 829 | (1, 1, 1) 830 | >>> B.x = 2 831 | >>> A.x, B.x, C.x 832 | (1, 2, 1) 833 | >>> A.x = 3 834 | >>> A.x, B.x, C.x 835 | (3, 2, 3) 836 | >>> a = A() 837 | >>> a.x, A.x 838 | (3, 3) 839 | >>> a.x += 1 840 | >>> a.x, A.x 841 | (4, 3) 842 | ``` 843 | 844 | 2\. 845 | ```py 846 | class SomeClass: 847 | some_var = 15 848 | some_list = [5] 849 | another_list = [5] 850 | def __init__(self, x): 851 | self.some_var = x + 1 852 | self.some_list = self.some_list + [x] 853 | self.another_list += [x] 854 | ``` 855 | 856 | **Output:** 857 | 858 | ```py 859 | >>> some_obj = SomeClass(420) 860 | >>> some_obj.some_list 861 | [5, 420] 862 | >>> some_obj.another_list 863 | [5, 420] 864 | >>> another_obj = SomeClass(111) 865 | >>> another_obj.some_list 866 | [5, 111] 867 | >>> another_obj.another_list 868 | [5, 420, 111] 869 | >>> another_obj.another_list is SomeClass.another_list 870 | True 871 | >>> another_obj.another_list is some_obj.another_list 872 | True 873 | ``` 874 | 875 | #### :bulb: 解释: 876 | 877 | * 类里的属性和类实例中的属性是作为一个字典列表对象进行处理的。如果在当前类中没有找到需要调用的属性名,那么就会递归去父类中寻找。 878 | * `+=`操作符会在原有易变类型对象的基础上做改变而不会重新创建一个新的对象。所以在一个类实例中修改属性值,会影响到所有调用这个属性的相关类实例和类。 879 | 880 | --- 881 | 882 | ### ▶ None的产生 883 | 884 | ```py 885 | some_iterable = ('a', 'b') 886 | 887 | def some_func(val): 888 | return "something" 889 | ``` 890 | 891 | **Output:** 892 | ```py 893 | >>> [x for x in some_iterable] 894 | ['a', 'b'] 895 | >>> [(yield x) for x in some_iterable] 896 | at 0x7f70b0a4ad58> 897 | >>> list([(yield x) for x in some_iterable]) 898 | ['a', 'b'] 899 | >>> list((yield x) for x in some_iterable) 900 | ['a', None, 'b', None] 901 | >>> list(some_func((yield x)) for x in some_iterable) 902 | ['a', 'something', 'b', 'something'] 903 | ``` 904 | 905 | #### :bulb: 解释: 906 | - 这是一个CPython中`yield`关键字在列表构造和生成器表达式中使用的bug。已经在Python3.8中修复了。在Python3.7中也会警告过时。具体可以看Python的这篇[bug report](https://bugs.python.org/issue10544) 907 | --- 908 | 909 | ### ▶ 不可修改的元组 910 | 911 | ```py 912 | some_tuple = ("A", "tuple", "with", "values") 913 | another_tuple = ([1, 2], [3, 4], [5, 6]) 914 | ``` 915 | 916 | **Output:** 917 | ```py 918 | >>> some_tuple[2] = "change this" 919 | TypeError: 'tuple' object does not support item assignment 920 | >>> another_tuple[2].append(1000) #这里并没有报错 921 | >>> another_tuple 922 | ([1, 2], [3, 4], [5, 6, 1000]) 923 | >>> another_tuple[2] += [99, 999] 924 | TypeError: 'tuple' object does not support item assignment 925 | >>> another_tuple 926 | ([1, 2], [3, 4], [5, 6, 1000, 99, 999]) 927 | ``` 928 | 929 | 我还以为元组是不可以修改的呢...... 930 | 931 | #### :bulb: 解释: 932 | 933 | * 引用自 https://docs.python.org/2/reference/datamodel.html 934 | > 不可变序列 935 | 一个不可变序列对象在第一次定义后就不再可以修改。(如果这个对象中包含有对其他对象的引用,这些其他对象有可能是可变的或者可修改的;总之,只有直接引用的对象是不可以修改的。) 936 | * `+=`操作符会对左侧操作对象本身进行修改。但是元组是不允许修改的,所以会报错,但是实际上修改的是一个可变的列表(list),元组里保存的只是这个列表的地址,所以最终还是会修改成功。 937 | 938 | --- 939 | 940 | ### ▶ 消失的变量e 941 | 942 | ```py 943 | e = 7 944 | try: 945 | raise Exception() 946 | except Exception as e: 947 | pass 948 | ``` 949 | 950 | **Output (Python 2.x):** 951 | ```py 952 | >>> print(e) 953 | # 什么都没有打印 954 | ``` 955 | 956 | **Output (Python 3.x):** 957 | ```py 958 | >>> print(e) 959 | NameError: name 'e' is not defined 960 | ``` 961 | 962 | #### :bulb: 解释: 963 | 964 | * 参考自: https://docs.python.org/3/reference/compound_stmts.html#except 965 | 966 | 当一个异常(exception)被用`as`赋值给一个变量后,这个变量将会在`except`语句块结束后清除。看下面这段 967 | 968 | ```py 969 | except E as N: 970 | foo 971 | ``` 972 | 973 | 相当于这样 974 | 975 | ```py 976 | except E as N: 977 | try: 978 | foo 979 | finally: 980 | del N 981 | ``` 982 | 983 | 这意味着你最好不要在外面定义一个和"N"重名的变量。这些被赋值的异常变量被清除是因为,Python会将这些变量加入一个异常回溯栈中(traceback,仔细观察每次程序出错控制台输出的错误信息就是从tranceback这个堆栈里面以此取出来的),记录这些异常的具体位置信息并且一直保持这个堆栈的信息直到下一次垃圾回收开始。 984 | 985 | * 这些Python的子句并没有独立的作用域,上面所有的例子都在一个作用域里,所以当变量`e`在`except`子句里面被移除后,就相当于在整个作用域都消失了。但是在函数中情况就和上面不一样了,因为Python函数有自己的“内部”作用域。下面这个例子将说明这种情况: 986 | 987 | ```py 988 | def f(x): 989 | del(x) 990 | print(x) 991 | 992 | x = 5 993 | y = [5, 4, 3] 994 | ``` 995 | 996 | **Output:** 997 | ```py 998 | >>>f(x) 999 | UnboundLocalError: local variable 'x' referenced before assignment 1000 | >>>f(y) 1001 | UnboundLocalError: local variable 'x' referenced before assignment 1002 | >>> x 1003 | 5 1004 | >>> y 1005 | [5, 4, 3] 1006 | ``` 1007 | 1008 | * 在 Python 2.x 版本里面, 变量`e`会被赋值`Exception()`实例,所以当打印的时候什么都不会打印出来。 1009 | 1010 | **Output (Python 2.x):** 1011 | ```py 1012 | >>> e 1013 | Exception() 1014 | >>> print e 1015 | # 什么都没有打印出来 1016 | ``` 1017 | 1018 | --- 1019 | 1020 | ### ▶ 亦真还假 1021 | 1022 | ```py 1023 | True = False 1024 | if True == False: 1025 | print("It's true of false?") 1026 | ``` 1027 | 1028 | **Output:** 1029 | ``` 1030 | It's true of false? 1031 | ``` 1032 | 1033 | #### :bulb: 解释: 1034 | 1035 | - 一开始, Python是没有`bool`布尔类型的(人们用0代表假用非0代表真)。后来增加了`True`,`False`还有`bool`类型,但是为了向前兼容,所以并没有`True`和`False`的关键字常量,只不过作为一个内部变量出现。 1036 | - Python 3是不向前兼容的,所以终于修复了这个问题,同时也要注意上面的例子在Python 3.X版本下是运行不了的! 1037 | 1038 | --- 1039 | 1040 | ### ▶ 转瞬即空 1041 | 1042 | ```py 1043 | some_list = [1, 2, 3] 1044 | some_dict = { 1045 | "key_1": 1, 1046 | "key_2": 2, 1047 | "key_3": 3 1048 | } 1049 | 1050 | some_list = some_list.append(4) 1051 | some_dict = some_dict.update({"key_4": 4}) 1052 | ``` 1053 | 1054 | **Output:** 1055 | ```py 1056 | >>> print(some_list) 1057 | None 1058 | >>> print(some_dict) 1059 | None 1060 | ``` 1061 | 1062 | #### :bulb: 解释 1063 | 1064 | 大部分修改序列结构对象的方法,比如`list.append`,`dict.update`,`list.sort`等等。都会直接对序列对象本身进行操作,也就是所谓的“就地操作”(in-plcae)并且会返回`None`。这么做是为了提升程序的性能,避免还需要把原有对象拷贝一次。(参考[这里](http://docs.python.org/2/faq/design.html#why-doesn-t-list-sort-return-the-sorted-list))。 1065 | 1066 | ```py 1067 | some_list = [1, 2, 3] 1068 | ``` 1069 | 1070 | **Output:** 1071 | ```py 1072 | >>>some_list2 = some_list.append(4) 1073 | >>>some_list2 1074 | None 1075 | >>>some_list 1076 | [1, 2, 3, 4] 1077 | ``` 1078 | 1079 | --- 1080 | 1081 | ### ▶ 子类的关系 * 1082 | 1083 | **Output:** 1084 | ```py 1085 | >>> from collections import Hashable 1086 | >>> issubclass(list, object) 1087 | True 1088 | >>> issubclass(object, Hashable) 1089 | True 1090 | >>> issubclass(list, Hashable) 1091 | False 1092 | ``` 1093 | 1094 | 是不是觉得类的继承关系应该是可传递的(transitive)?(比如,`A`是`B`的一个子类,`B`又是`C`的一个子类,那么`A`也"应该"是`C`的子类) 1095 | 1096 | #### :bulb: 解释: 1097 | 1098 | * 在Python中子类的继承关系不一定是传递的,任何人都可以自定义元类(metaclass)中的`__subclasscheck__`函数(`_subclasscheck__(subclass)`检查subclass是不是调用类的子类)。 1099 | * 当调用`issubclass(cls, Hashable)`的时候,函数只是简单的检查一下`cls`和它继承的类中有没有"`__hash__`"这个方法。 1100 | * 因为`object`是可以被哈希的(也就是说`object`有`__hash__`这个函数),但是`list`是不能被哈希的,所以他们之间打破了传导关系。 1101 | * 如果想看更详尽的解释,[这里](https://www.naftaliharris.com/blog/python-subclass-intransitivity/)有关于Python子类关系传导的详细解释。 1102 | 1103 | --- 1104 | 1105 | ### ▶ 神秘的键值转换 * 1106 | 1107 | ```py 1108 | class SomeClass(str): 1109 | pass 1110 | 1111 | some_dict = {'s':42} 1112 | ``` 1113 | 1114 | **Output:** 1115 | ```py 1116 | >>> type(list(some_dict.keys())[0]) 1117 | str 1118 | >>> s = SomeClass('s') 1119 | >>> some_dict[s] = 40 1120 | >>> some_dict # expected: Two different keys-value pairs 1121 | {'s': 40} 1122 | >>> type(list(some_dict.keys())[0]) 1123 | str 1124 | ``` 1125 | 1126 | #### :bulb: 解释: 1127 | 1128 | * 类`s`和字符串`s`的哈希值是一样的,因为`SomeClass`继承了`str`类的`__hash__`方法。 1129 | * `SomeClass("s") == "s"` 等于`True`,因为`SomeClass`同样继承了`str`类的`__eq__`方法。 1130 | * 因为两个对象的哈希值(hash)和值(value)全都相等,所以他们在字典的关键字(key)里是被认为相同的。 1131 | * 如果想要打到预期的效果,我们可以重新定义`SomeClass`的`__eq__`方法 1132 | ```py 1133 | class SomeClass(str): 1134 | def __eq__(self, other): 1135 | return ( 1136 | type(self) is SomeClass 1137 | and type(other) is SomeClass 1138 | and super().__eq__(other) 1139 | ) 1140 | 1141 | # 当我们自定义 __eq__ 方法后, Python会停止 1142 | # 自动继承 __hash__ 方法, 所以我们同样也需要定义它 1143 | __hash__ = str.__hash__ 1144 | 1145 | some_dict = {'s':42} 1146 | ``` 1147 | 1148 | **Output:** 1149 | ```py 1150 | >>> s = SomeClass('s') 1151 | >>> some_dict[s] = 40 1152 | >>> some_dict 1153 | {'s': 40, 's': 42} 1154 | >>> keys = list(some_dict.keys()) 1155 | >>> type(keys[0]), type(keys[1]) 1156 | (__main__.SomeClass, str) 1157 | ``` 1158 | 1159 | --- 1160 | 1161 | ### ▶ 看你能不能猜到这个的结果? 1162 | 1163 | ```py 1164 | a, b = a[b] = {}, 5 1165 | ``` 1166 | 1167 | **Output:** 1168 | ```py 1169 | >>> a 1170 | {5: ({...}, 5)} 1171 | ``` 1172 | 1173 | #### :bulb: 解释: 1174 | 1175 | * 根据Python的赋值语句的[文档](https://docs.python.org/3.7/reference/simple_stmts.html#assignment-statements),赋值语句有如下的格式 1176 | ``` 1177 | (target_list "=")+ (expression_list | yield_expression) 1178 | ``` 1179 | 并且 1180 | > 一个赋值语句会对表达式列表(expression_list)进行求值(这个可以只有一个表达式,也可以多个表达式用逗号分开组成表达式列表,后者最终会表现为元组的形式)并且会将单次的求值结果对象依次从左至右赋值给目标列表(target_list)。 1181 | 1182 | * 在`(target_list "=")+`这个表达式中`+`意味着 **一个或者多个** 目标列表。这上面这个例子中,目标列表是`a,b`和`a[b]`(注意表达式列表永远只有一个,在我们这个例子里是`{}, 5`)。 1183 | 1184 | * 当表达式计算出来后,它的值会被 **从左至右** 依次赋值给目标列表。所以,在上面的例子里,第一次赋值会把`{}, 5`这个元组赋值给`a, b`这个目标列表,这个时候`a = {}` 并且 `b = 5`。 1185 | 1186 | * 现在`a`被赋值成了`{}`,一个可变对象(mutable object)。 1187 | 1188 | * 第二个目标列表是`a[b]`(你可能会认为这种写法会报错,因为我们没有之前并没有定义`a`和`b`,但是没关系,我们刚刚已经给`a`和`b`分别赋值了`{}`和`5`)。 1189 | 1190 | * 现在我们会把元组`({}, 5)`赋值给字典关键为`5`的字典对象(也就是`a[5]`),同时也因此创建了一个循环引用(circular reference)(`{...}`在输出中代表同一个对象`a`已经被引用了)。下面是一个关于引用循环简单点的例子 1191 | 1192 | ```py 1193 | >>> some_list = some_list[0] = [0] 1194 | >>> some_list 1195 | [[...]] 1196 | >>> some_list[0] 1197 | [[...]] 1198 | >>> some_list is some_list[0] 1199 | True 1200 | >>> some_list[0][0][0][0][0][0] == some_list 1201 | True 1202 | ``` 1203 | 在我们的例子用也是同样的情况(`a[b][0]`和`a`是指向同一个对象) 1204 | 1205 | * 所以总结一下,我们可以把上面的列子像如下这样拆分 1206 | ```py 1207 | a, b = {}, 5 1208 | a[b] = a, b 1209 | ``` 1210 | 并且通过`a[b][0]`和`a`是同一个对象来证明确实存在引用循环 1211 | ```py 1212 | >>> a[b][0] is a 1213 | True 1214 | ``` 1215 | 1216 | --- 1217 | 1218 | --- 1219 | 1220 | ## 第二章: 瞒天过海 1221 | 1222 | ### ▶ 无效的一行? 1223 | 1224 | **Output:** 1225 | ```py 1226 | >>> value = 11 1227 | >>> valuе = 32 1228 | >>> value 1229 | 11 1230 | ``` 1231 | 1232 | 哈?一脸懵逼是不是(囧)? 1233 | 1234 | **提示:** 最简单的复现上面结果的办法是把上面的赋值语句原封不动的复制粘贴到你的文件或者命令行中执行。 1235 | 1236 | #### :bulb: 解释 1237 | 1238 | 一些非西方字符集看起来和我们平时用的英文字母的外观是一模一样的,但是在解释器看来就不一样了。 1239 | 1240 | ```py 1241 | >>> ord('е') # 俄语的字母 'e' (Ye) 1242 | 1077 1243 | >>> ord('e') # 拉丁文的字母'e', 也就是我们平时在英语和用标准键盘打出来的字母 1244 | 101 1245 | >>> 'е' == 'e' 1246 | False 1247 | 1248 | >>> value = 42 # 拉丁的 e 1249 | >>> valuе = 23 # 俄语里的 'e', 这里如果是用的Python 2.x,解释器会产生一个`SyntaxError`的异常 1250 | >>> value 1251 | 42 1252 | ``` 1253 | 1254 | 内置函数`ord()`可以返回对应字符在Unicode编码中的位置([code point](https://en.wikipedia.org/wiki/Code_point)), 因为俄语的字符'e'和拉丁语的字符'e'的位置是不一样的,所以在上面的例子里,解释器会认为两个`value`变量是不同的。 1255 | 1256 | --- 1257 | 1258 | ### ▶ 移形换位 * 1259 | 1260 | ```py 1261 | import numpy as np 1262 | 1263 | def energy_send(x): 1264 | # 初始化一个numpy数组 1265 | np.array([float(x)]) 1266 | 1267 | def energy_receive(): 1268 | # 返回一个空numpy数组 1269 | return np.empty((), dtype=np.float).tolist() 1270 | ``` 1271 | 1272 | **Output:** 1273 | ```py 1274 | >>> energy_send(123.456) 1275 | >>> energy_receive() 1276 | 123.456 1277 | ``` 1278 | 1279 | 是不是很神奇? 1280 | 1281 | #### :bulb: 解释: 1282 | 1283 | * 因为在`energy_send`函数中创建的numpy数组并没有通过`return`返回赋给任何一个变量,所以带边这个数组的那一块内存空间是可以自由分配的。 1284 | * `numpy.empty()` 会返回最近的一块还没有被使用的内存块,并且不会对这个内存块进行初始化。这块内存块恰恰就是上一个函数刚刚释放的那块内存(大部分时间是这样的,但并不是绝对的)。 1285 | 1286 | --- 1287 | 1288 | ### ▶ 到底哪里出错了呢? 1289 | 1290 | ```py 1291 | def square(x): 1292 | """ 1293 | 通过加法运算计算平方的函数 1294 | """ 1295 | sum_so_far = 0 1296 | for counter in range(x): 1297 | sum_so_far = sum_so_far + x 1298 | return sum_so_far 1299 | ``` 1300 | 1301 | **Output (Python 2.x):** 1302 | 1303 | ```py 1304 | >>> square(10) 1305 | 10 1306 | ``` 1307 | 1308 | 结果不应该是100吗? 1309 | 1310 | **注意:** 如果你不能复现上面例子的结果, 可以试试直接运行[mixed_tabs_and_spaces.py](/mixed_tabs_and_spaces.py) 这个文件。 1311 | 1312 | #### :bulb: 解释 1313 | 1314 | * **不要把空格和制表符混在一起使用!** 例子中`return`语句前面的是一个用制表符"tab"来缩进的,代码的其他地方都是使用“四个空格”作为缩进的。 1315 | * Python是这样处理制表符的: 1316 | > 首先,制表符(tabs)会被依次替换成(从左至右)1到8个不等的空格直到替换后的空格数满足8的倍数......[出自Python文档](https://docs.python.org/2.0/ref/indentation.html) 1317 | * 所以在`square`这个函数最后这个制表符"tab"就被替换成了8个空格,八个空格就是两个缩进,所以就等于把`return`语句缩进到了`for`循环内。 1318 | * 这种情况在Python 3中就会直接报错。 1319 | 1320 | **Output (Python 3.x):** 1321 | ```py 1322 | TabError: inconsistent use of tabs and spaces in indentation 1323 | ``` 1324 | 1325 | --- 1326 | 1327 | --- 1328 | 1329 | 1330 | ## 第三章: 隐藏的陷阱 1331 | 1332 | 1333 | ### ▶ 不要在迭代一个字典的时候修改这个字典! 1334 | 1335 | ```py 1336 | x = {0: None} 1337 | 1338 | for i in x: 1339 | del x[i] 1340 | x[i+1] = None 1341 | print(i) 1342 | ``` 1343 | 1344 | **Output (Python 2.7- Python 3.5):** 1345 | 1346 | ``` 1347 | 0 1348 | 1 1349 | 2 1350 | 3 1351 | 4 1352 | 5 1353 | 6 1354 | 7 1355 | ``` 1356 | 1357 | 这个迭代会准确运行**八**次停止。 1358 | 1359 | #### :bulb: 解释: 1360 | 1361 | * 首先我是不赞成在迭代一个字典的时候对这个字典做修改的。 1362 | * 这个迭代正好运行了八次是因为正好到了一个点,到了这个点后,这个字典对象需要重新调整大小用来容纳足够的关键字(keys)(我们在循环中已经连续做了八次删除了,当然需要重新调整字典的大小了)。实际上这个点在哪取决于Python的具体实现细节。 1363 | * 对删除键值的处理和什么时候触发调整字典大小会根据不同Python的版本而有所不同。 1364 | 1365 | --- 1366 | 1367 | ### ▶ 删不掉的对象 * 1368 | 1369 | ```py 1370 | class SomeClass: 1371 | def __del__(self): 1372 | print("Deleted!") 1373 | ``` 1374 | 1375 | **Output:** 1376 | 1\. 1377 | ```py 1378 | >>> x = SomeClass() 1379 | >>> y = x 1380 | >>> del x # 这里应该输出 "Deleted!" 1381 | >>> del y 1382 | Deleted! 1383 | ``` 1384 | 1385 | 啧.....最后还是输出了的。你可能已经猜到了,我们第一次尝试删除`x`的时候,`__del__`方法并没有被调用。现在我们在上面例子的基础上再加几句代码。 1386 | 1387 | 2\. 1388 | ```py 1389 | >>> x = SomeClass() 1390 | >>> y = x 1391 | >>> del x 1392 | >>> y # 检查y是否存在 1393 | <__main__.SomeClass instance at 0x7f98a1a67fc8> 1394 | >>> del y # 就像上一个例子一样,这里应该输出"Deleted!" 1395 | >>> globals() # 嗯......好像并没有输出。让我们检查一下全局变量看看。 1396 | Deleted! 1397 | {'__builtins__': , 'SomeClass': , '__package__': None, '__name__': '__main__', '__doc__': None} 1398 | ``` 1399 | 1400 | 好吧,最后还是输出了(好奇怪的说):confused: 1401 | 1402 | #### :bulb: 解释: 1403 | + `del x`并不会直接调用`x.__del__()`。 1404 | + 当调用`del x`的时候,Python会把所有x指向的对象的引用数减一(在这里就是前面实例化的对象`SomeClass()`),只有当这个对象的引用数为零的时候,才会调用`__del__()`。 1405 | + 在第二个例子中,`y.__del__()`没有被调用时因为前面的语句(`>>> y`)引用了同一个对象(SomeClass()),增加了这个对象的引用数,所以在`del y`以后这个对象的引用数并没有变成零。 1406 | + 调用`globals`会检查并销毁没用的引用,这时候针对上面对象(`SomeClass()`)的引用数就变成零了,"Deleted!"就被打印出来了。 1407 | 1408 | --- 1409 | 1410 | ### ▶ 删除正在迭代中的列表项 1411 | 1412 | ```py 1413 | list_1 = [1, 2, 3, 4] 1414 | list_2 = [1, 2, 3, 4] 1415 | list_3 = [1, 2, 3, 4] 1416 | list_4 = [1, 2, 3, 4] 1417 | 1418 | for idx, item in enumerate(list_1): 1419 | del item 1420 | 1421 | for idx, item in enumerate(list_2): 1422 | list_2.remove(item) 1423 | 1424 | for idx, item in enumerate(list_3[:]): 1425 | list_3.remove(item) 1426 | 1427 | for idx, item in enumerate(list_4): 1428 | list_4.pop(idx) 1429 | ``` 1430 | 1431 | **Output:** 1432 | ```py 1433 | >>> list_1 1434 | [1, 2, 3, 4] 1435 | >>> list_2 1436 | [2, 4] 1437 | >>> list_3 1438 | [] 1439 | >>> list_4 1440 | [2, 4] 1441 | ``` 1442 | 1443 | 知道为什么输出是`[2, 4]`吗? 1444 | 1445 | #### :bulb: 解释: 1446 | 1447 | * 首先,我是不赞成在迭代的过程中修改迭代参数的(就像上面例子里那样)。正确的方法应该是先把迭代的列表拷贝一份,拿拷贝后的列表做迭代,就像上面第三个迭代做的那样(`list_3[:]`)。 1448 | 1449 | ```py 1450 | >>> some_list = [1, 2, 3, 4] 1451 | >>> id(some_list) 1452 | 139798789457608 1453 | >>> id(some_list[:]) # 注意如果用切片法的话,Python会新建一个list对象。 1454 | 139798779601192 1455 | ``` 1456 | 1457 | 1458 | **`del`,`remove`和`pop`的区别:** 1459 | * `del var_name` 只会移除本地或者全局命名空间中对`var_name`这个变量的绑定(这就是为什么`list_1`迭代后没有任何改变,因为`list_1`这个变量并没有收到影响)。 1460 | * `remove` 会根据值(value)进行匹配并移除第一个匹配上的元素,若没有匹配的元素则抛出`ValueError`异常。 1461 | * `pop` 会移除指定索引(index)的元素并且返回这个元素,如果指定的元素不存在则抛出`IndexError`异常。 1462 | 1463 | **所以为什么会输出`[2, 4]`呢?** 1464 | - 首先列表迭代是根据索引值一个一个迭代的,所以当我们把`1`这个元素从`list_2`或者`list_4`中移除了以后,这两个列表就变成了`[2, 3, 4]`。剩下的这几个元素会依次向左移填补刚刚删除了的元素的位置,也就是说,`2`这个元素现在的索引值(index)变成了0,`3`的索引值变成了1。接着在下次迭代的时候,`for`循环访问的索引值已经变成了1(也就是列表里`3`这个元素),所以`2`这个元素就被跳过去(没有删除)。同样的情况每迭代一次都会发生一次。 1465 | 1466 | --- 1467 | 1468 | ### ▶ 被泄露出去的循环变量! 1469 | 1470 | 1\. 1471 | ```py 1472 | for x in range(7): 1473 | if x == 6: 1474 | print(x, ': for x inside loop') 1475 | print(x, ': x in global') 1476 | ``` 1477 | 1478 | **Output:** 1479 | ```py 1480 | 6 : for x inside loop 1481 | 6 : x in global 1482 | ``` 1483 | 1484 | 但是我从来没有在循环外定义一个变量`x`呀! 1485 | 1486 | 2\. 1487 | ```py 1488 | # 这回我先初始化一个x变量 1489 | x = -1 1490 | for x in range(7): 1491 | if x == 6: 1492 | print(x, ': for x inside loop') 1493 | print(x, ': x in global') 1494 | ``` 1495 | 1496 | **Output:** 1497 | ```py 1498 | 6 : for x inside loop 1499 | 6 : x in global 1500 | ``` 1501 | 1502 | 3\. 1503 | ``` 1504 | x = 1 1505 | print([x for x in range(5)]) 1506 | print(x, ': x in global') 1507 | ``` 1508 | 1509 | **Output (Python 2.x):** 1510 | ``` 1511 | [0, 1, 2, 3, 4] 1512 | (4, ': x in global') 1513 | ``` 1514 | 1515 | **Output (Python 3.x):** 1516 | ``` 1517 | [0, 1, 2, 3, 4] 1518 | 1 : x in global 1519 | ``` 1520 | 1521 | #### :bulb: 解释: 1522 | 1523 | - 在Python里, for循环有它自己的作用域,但是在循环结束后,不会销毁那些循环变量。就算我们提前在全局作用域里定义了变量(就像上面那种情况),也会被循环变量覆盖。 1524 | 1525 | - 关于为什么在Python2和Python3中有不同的输出,可以在Python3的文档([What's New In Python 3.0](https://docs.python.org/3/whatsnew/3.0.html))中找到答案,下面会贴出原文: 1526 | 1527 | >"List comprehensions no longer support the syntactic form `[... for var in item1, item2, ...]`. Use `[... for var in (item1, item2, ...)]` instead. Also, note that list comprehensions have different semantics: they are closer to syntactic sugar for a generator expression inside a `list()` constructor, and in particular the loop control variables are no longer leaked into the surrounding scope." 1528 | 1529 | - 只用看最后一句话就够了**in particular the loop control variables are no longer leaked into the surrounding scope**,翻译过来是**特别是循环控制变量不会再泄露到包围着它的作用域以外了**,还有这个特性是针对构造列表(List comprehensions)说的,不包括正常的for循环。 1530 | 1531 | --- 1532 | 1533 | ### ▶ 小心那些有默认值的可变参数! 1534 | 1535 | ```py 1536 | def some_func(default_arg=[]): 1537 | default_arg.append("some_string") 1538 | return default_arg 1539 | ``` 1540 | 1541 | **Output:** 1542 | ```py 1543 | >>> some_func() 1544 | ['some_string'] 1545 | >>> some_func() 1546 | ['some_string', 'some_string'] 1547 | >>> some_func([]) 1548 | ['some_string'] 1549 | >>> some_func() 1550 | ['some_string', 'some_string', 'some_string'] 1551 | ``` 1552 | 1553 | #### :bulb: 解释: 1554 | 1555 | - 在Python中,如果参数有默认值并且是一个可变对象,那么这个默认的对象会一直存在,并不会随着每次创建实例而重新生成一个新的对象。当我们显式的把`[]`对象传递给`some_func`函数的时候,`default_arg`这个参数的默认值就不会被使用而被新对象替代了,所以这个函数会按照我们预期的那样返回值。 1556 | 1557 | ```py 1558 | def some_func(default_arg=[]): 1559 | default_arg.append("some_string") 1560 | return default_arg 1561 | ``` 1562 | 1563 | **Output:** 1564 | ```py 1565 | >>> some_func.__defaults__ #__defaults__会把这个函数的默认参数都打印出来 1566 | ([],) 1567 | >>> some_func() 1568 | >>> some_func.__defaults__ 1569 | (['some_string'],) 1570 | >>> some_func() 1571 | >>> some_func.__defaults__ 1572 | (['some_string', 'some_string'],) 1573 | >>> some_func([]) 1574 | >>> some_func.__defaults__ 1575 | (['some_string', 'some_string'],) 1576 | ``` 1577 | 1578 | - 一个常用的用来避免上述bug的做法是这样的,把默认可变参数赋值成`None`,然后在函数体中检查这个参数是否被赋值,如果没有就赋一个默认值: 1579 | 1580 | ```py 1581 | def some_func(default_arg=None): 1582 | if not default_arg: 1583 | default_arg = [] 1584 | default_arg.append("some_string") 1585 | return default_arg 1586 | ``` 1587 | 1588 | --- 1589 | 1590 | ### ▶ 抓住这些异常! 1591 | 1592 | ```py 1593 | some_list = [1, 2, 3] 1594 | try: 1595 | # 这里应该会抛出一个 ``IndexError`` 异常 1596 | print(some_list[4]) 1597 | except IndexError, ValueError: 1598 | print("Caught!") 1599 | 1600 | try: 1601 | # 这里会抛出一个 ``ValueError`` 异常 1602 | some_list.remove(4) 1603 | except IndexError, ValueError: 1604 | print("Caught again!") 1605 | ``` 1606 | 1607 | **Output (Python 2.x):** 1608 | ```py 1609 | Caught! 1610 | 1611 | ValueError: list.remove(x): x not in list 1612 | ``` 1613 | 1614 | **Output (Python 3.x):** 1615 | ```py 1616 | File "", line 3 1617 | except IndexError, ValueError: 1618 | ^ 1619 | SyntaxError: invalid syntax 1620 | ``` 1621 | 1622 | #### :bulb: 解释 1623 | 1624 | * 如果想在`except`语句中同时捕获多个异常类型,需要把这些类型写成元组(tuple)的形式放在第一个参数的位置。第二个参数是一个可选参数,用来绑定具体捕获异常后的对象。比如, 1625 | ```py 1626 | some_list = [1, 2, 3] 1627 | try: 1628 | # 这里会产生一个 ``ValueError`` 异常 1629 | some_list.remove(4) 1630 | except (IndexError, ValueError), e: 1631 | print("Caught again!") 1632 | print(e) 1633 | ``` 1634 | **Output (Python 2.x):** 1635 | ``` 1636 | Caught again! 1637 | list.remove(x): x not in list 1638 | ``` 1639 | **Output (Python 3.x):** 1640 | ```py 1641 | File "", line 4 1642 | except (IndexError, ValueError), e: 1643 | ^ 1644 | IndentationError: unindent does not match any outer indentation level 1645 | ``` 1646 | 1647 | * 在Python 3中已经弃用了使用逗号分隔异常类型和异常变量了;而是要使用`as`语法了。比如, 1648 | ```py 1649 | some_list = [1, 2, 3] 1650 | try: 1651 | some_list.remove(4) 1652 | 1653 | except (IndexError, ValueError) as e: 1654 | print("Caught again!") 1655 | print(e) 1656 | ``` 1657 | **Output:** 1658 | ``` 1659 | Caught again! 1660 | list.remove(x): x not in list 1661 | ``` 1662 | 1663 | --- 1664 | 1665 | ### ▶ 相同的操作,不同的结果 1666 | 1667 | 1\. 1668 | ```py 1669 | a = [1, 2, 3, 4] 1670 | b = a 1671 | a = a + [5, 6, 7, 8] 1672 | ``` 1673 | 1674 | **Output:** 1675 | ```py 1676 | >>> a 1677 | [1, 2, 3, 4, 5, 6, 7, 8] 1678 | >>> b 1679 | [1, 2, 3, 4] 1680 | ``` 1681 | 1682 | 2\. 1683 | ```py 1684 | a = [1, 2, 3, 4] 1685 | b = a 1686 | a += [5, 6, 7, 8] 1687 | ``` 1688 | 1689 | **Output:** 1690 | ```py 1691 | >>> a 1692 | [1, 2, 3, 4, 5, 6, 7, 8] 1693 | >>> b 1694 | [1, 2, 3, 4, 5, 6, 7, 8] 1695 | ``` 1696 | 1697 | #### :bulb: 解释: 1698 | 1699 | * `a += b` 和 `a = a + b`的内部处理流程有时候会根据操作数的不同有所不同。比如*`op=`*这种操作符(比如`+=`,`-=`,`*=`这样),针对列表(list)对象这两个表达式就会有不一样的内在表现。 1700 | 1701 | * 表达式`a = a + [5,6,7,8]`会生成一个新的对象,并且`a`会引用这个新生成的对象,`b`变量不会变(继续引用之前的列表对象`[1,2,3,4]`)。 1702 | 1703 | * 表达式`a += [5,6,7,8]`就不会生成一个新对象,它实际上是在原有对象的基础上又“扩展了”几个元素,也就是所谓的就地改变(in-place)。所以这个时候`a`,`b`两个变量还是同时引用的一个对象。 1704 | 1705 | --- 1706 | 1707 | ### ▶ 作用域之外的变量 1708 | 1709 | ```py 1710 | a = 1 1711 | def some_func(): 1712 | return a 1713 | 1714 | def another_func(): 1715 | a += 1 1716 | return a 1717 | ``` 1718 | 1719 | **Output:** 1720 | ```py 1721 | >>> some_func() 1722 | 1 1723 | >>> another_func() 1724 | UnboundLocalError: local variable 'a' referenced before assignment 1725 | ``` 1726 | 1727 | #### :bulb: 解释: 1728 | * 当你在某个作用域给一个变量赋值的时候,这个变量就会自动变成这个作用域的变量。所以`a`会变成`another_func`这个函数的局部变量,但是又没有在这个函数作用域内正确的初始化`a`变量,结果就发生了异常。 1729 | * 这篇[文章](http://sebastianraschka.com/Articles/2014_python_scope_and_namespaces.html)很好的解释了命名空间和作用域是如何在Python中起作用的(是英文版,等有时间我会翻译一下,如果有热心的同学帮忙就更好啦!)。 1730 | * 如果想在局部作用域内使用全局作用域的变量`a`,需要使用`global`关键字。 1731 | ```py 1732 | def another_func() 1733 | global a 1734 | a += 1 1735 | return a 1736 | ``` 1737 | 1738 | **Output:** 1739 | ```py 1740 | >>> another_func() 1741 | 2 1742 | ``` 1743 | 1744 | --- 1745 | 1746 | ### ▶ 小心那些链式操作符 1747 | 1748 | ```py 1749 | >>> (False == False) in [False] # 没问题,OK? 1750 | False 1751 | >>> False == (False in [False]) # 这个也OK对吧? 1752 | False 1753 | >>> False == False in [False] # 这个呢?是不是开始不对了? 1754 | True 1755 | 1756 | >>> True is False == False 1757 | False 1758 | >>> False is False is False 1759 | True 1760 | 1761 | >>> 1 > 0 < 1 1762 | True 1763 | >>> (1 > 0) < 1 1764 | False 1765 | >>> 1 > (0 < 1) 1766 | False 1767 | ``` 1768 | 1769 | #### :bulb: 解释: 1770 | 1771 | 按照 https://docs.python.org/2/reference/expressions.html#not-in 里面说的: 1772 | 1773 | > 如果 a, b, c, ..., y, z 这些是表达式,op1, op2, ..., opN 这些是比较符,那么 a op1 b op2 c ... y opN z 就等于 a op1 b and b op2 c and ... y opN z, 除非其中有些表达式最多只能计算一次。 1774 | 1775 | 这种模式运行在像 `a == b == c` 和 `0 <=x <= 100` 这种语句里就会产生很有意思的结果了,就像上边那些例子一样。 1776 | 1777 | * `False is False is False` 等于 `(False is False) and (False is False)`。 1778 | * `True is False == False` 等于 `True is False and False == False` 而且第一部分 (`True is False`) 等于 `False`, 所以整个表达式结果就为 `False`。 1779 | * `1 > 0 < 1` 等于 `1 > 0 and 0 < 1` 最后结果等于 `True`。 1780 | * 表达式 `(1 > 0) < 1` 等于 `True < 1` 而且 1781 | ```py 1782 | >>> int(True) 1783 | 1 1784 | >>> True + 1 #这个跟例子没啥关系,就是想告诉你们布尔值也可以这样玩^_^ 1785 | 2 1786 | ``` 1787 | 所以, `1 < 1`也等于`False` 1788 | 1789 | --- 1790 | 1791 | ### ▶ 被忽略的类变量 1792 | 1793 | 1\. 1794 | ```py 1795 | x = 5 1796 | class SomeClass: 1797 | x = 17 1798 | y = (x for i in range(10)) 1799 | ``` 1800 | 1801 | **Output:** 1802 | ```py 1803 | >>> list(SomeClass.y)[0] 1804 | 5 1805 | ``` 1806 | 1807 | 2\. 1808 | ```py 1809 | x = 5 1810 | class SomeClass: 1811 | x = 17 1812 | y = [x for i in range(10)] 1813 | ``` 1814 | 1815 | **Output (Python 2.x):** 1816 | ```py 1817 | >>> SomeClass.y[0] 1818 | 17 1819 | ``` 1820 | 1821 | **Output (Python 3.x):** 1822 | ```py 1823 | >>> SomeClass.y[0] 1824 | 5 1825 | ``` 1826 | 1827 | #### :bulb: 解释 1828 | - 如果一个类定义内嵌套了一个子作用域,那么在这个作用域内部会忽略所有这个类级别的变量绑定。 1829 | - 一个生成器(generator)表达式有自己的作用域 1830 | - 从Python 3.X开始,列表生成式也有自己的作用域了。 1831 | 1832 | ![name-ignore-class-scope](./assets/name-ignore-class-scope/name-ignore-class-scope.png) 1833 | 1834 | --- 1835 | ### ▶ 小心处理元组 1836 | 1837 | 1\. 1838 | ```py 1839 | x, y = (0, 1) if True else None, None 1840 | ``` 1841 | 1842 | **Output:** 1843 | ``` 1844 | >>> x, y # 期待的结果是 (0, 1) 1845 | ((0, 1), None) 1846 | ``` 1847 | 1848 | 基本每一个Python程序员都会遇到过下面这种情况。 1849 | 1850 | 2\. 1851 | ```py 1852 | t = ('one', 'two') 1853 | for i in t: 1854 | print(i) 1855 | 1856 | t = ('one') 1857 | for i in t: 1858 | print(i) 1859 | 1860 | t = () 1861 | print(t) 1862 | ``` 1863 | 1864 | **Output:** 1865 | ```py 1866 | one 1867 | two 1868 | o 1869 | n 1870 | e 1871 | tuple() 1872 | ``` 1873 | 1874 | #### :bulb: 解释: 1875 | * 第一个例子里,想要得到期待的结果的写法应该是`x, y = (0, 1) if True else (None, None)`,要显式声明元组。 1876 | * 第二个例子里,想要得到期待的结果的写法应该是 `t = ('one',)` 或者 `t = 'one',`(原来的少了逗号)否则解释器会把变量`t`当做一个`str`字符串处理挨个迭代里面的每一个字符。 1877 | * `()`是一个特殊的标识符专门用来表示空元组(`tuple`)。 1878 | 1879 | --- 1880 | 1881 | --- 1882 | 1883 | 1884 | ## 第四章: 一起来找些有趣的东西! 1885 | 1886 | 这一章包含了一些有趣的但是鲜为人知的彩蛋,大部分的Python新手都很少有人知道,就比如我(当然,现在我知道了)。 1887 | 1888 | ### ▶ 人生苦短,我用Python * 1889 | 1890 | 首先,我先引用下面这个模块,然后...... 1891 | 1892 | ```py 1893 | import antigravity 1894 | ``` 1895 | 1896 | **Output:** 1897 | 嘘....这个秘密可没几个人知道! 1898 | 1899 | #### :bulb: 解释: 1900 | + `antigravity` 模块是针对Python开发者的一个小彩蛋。 1901 | + `import antigravity` 会打开一个指向 [classic XKCD comic](http://xkcd.com/353/)的网站,一个关于Python的漫画网站。 1902 | + 而且,这还是一个俄罗斯套娃彩蛋,也就是说**这个彩蛋中还包含着另一个彩蛋**。如果你打开这个模块的[代码](https://github.com/python/cpython/blob/master/Lib/antigravity.py#L7-L17),你会发现代码中还定义了一个实现[geohashing算法](https://xkcd.com/426/)的函数(这个算法是一个利用股票市场数据随机生成经度纬度坐标的算法)。 1903 | 1904 | --- 1905 | 1906 | ### ▶ 为什么要用`goto`? * 1907 | 1908 | ```py 1909 | from goto import goto, label 1910 | for i in range(9): 1911 | for j in range(9): 1912 | for k in range(9): 1913 | print("I'm trapped, please rescue!") 1914 | if k == 2: 1915 | goto .breakout #从一个深层循环内跳出 1916 | label .breakout 1917 | print("Freedom!") 1918 | ``` 1919 | 1920 | **Output (Python 2.3):** 1921 | ```py 1922 | I'm trapped, please rescue! 1923 | I'm trapped, please rescue! 1924 | Freedom! 1925 | ``` 1926 | 1927 | #### :bulb: 解释: 1928 | - `goto`模块是作为2004年的愚人节(2004.4.1)玩笑[发布](https://mail.python.org/pipermail/python-announce-list/2004-April/002982.html)的,只有Python 2.3版本有`goto`模块。 1929 | - 当前版本的Python是没有这个模块的(如果你用的是conda甚至不能下载到2.3版本的Python)。 1930 | - 虽然上面版本可以使用`goto`,但是也请不要使用它。[这里](https://docs.python.org/3/faq/design.html#why-is-there-no-goto)是为什么当前版本没有`goto`模块的原因。 1931 | 1932 | --- 1933 | 1934 | ### ▶ 试试用大括号? * 1935 | 1936 | 如果你不太习惯Python中用空格缩进来表示语句段(scopes)的方法,可以通过引用`__future__`模块来使用C语言大括号({})形式的表示方法来表示语句段, 1937 | 1938 | ```py 1939 | from __future__ import braces 1940 | ``` 1941 | 1942 | **Output:** 1943 | ```py 1944 | File "some_file.py", line 1 1945 | from __future__ import braces 1946 | SyntaxError: not a chance 1947 | ``` 1948 | 1949 | 想用大括号?不可能的,如果你不服气,用Java去。 1950 | 1951 | #### :bulb: 解释: 1952 | + `__future__`模块一般用来提供未来Python版本才拥有的一些特性。当然,在这个例子里,"future"这个词本身也算是对大括号的嘲讽。 1953 | + 这是一个Python社区针对大括号这个问题设计出来的一个彩蛋。 1954 | 1955 | --- 1956 | 1957 | ### ▶ 不等号的争议 * 1958 | 1959 | **Output (Python 3.x)** 1960 | ```py 1961 | >>> from __future__ import barry_as_FLUFL 1962 | >>> "Ruby" != "Python" # 这句话没有任何争议 1963 | File "some_file.py", line 1 1964 | "Ruby" != "Python" 1965 | ^ 1966 | SyntaxError: invalid syntax 1967 | 1968 | >>> "Ruby" <> "Python" 1969 | True 1970 | ``` 1971 | 1972 | 直接看解释吧。 1973 | 1974 | #### :bulb: 解释: 1975 | - 上面的问题起源自一个已经淘汰的Python修正包[PEP-401](https://www.python.org/dev/peps/pep-0401/),发布于2009年的4月1日(明白为什么它会被淘汰了吧)。 1976 | - 下面引用 PEP-401 的内容 1977 | > 为了让你们认识到在Python3.0中 != 这种逻辑不等式是多么的不好用,FLUFL引用会使解释器禁止这种语法只允许 <> 这种便利的逻辑不等式。 1978 | 1979 | --- 1980 | 1981 | ### ▶ 就算Python也知道爱是个复杂的东西 * 1982 | 1983 | ```py 1984 | import this 1985 | ``` 1986 | 1987 | Wait, what's **this**? `this` is love :heart: 1988 | (译注:我认为这句话原汁原味的看才能体会其中的乐趣,就不翻译了😊) 1989 | 1990 | **Output:** 1991 | ``` 1992 | The Zen of Python, by Tim Peters 1993 | 1994 | Beautiful is better than ugly. 1995 | Explicit is better than implicit. 1996 | Simple is better than complex. 1997 | Complex is better than complicated. 1998 | Flat is better than nested. 1999 | Sparse is better than dense. 2000 | Readability counts. 2001 | Special cases aren't special enough to break the rules. 2002 | Although practicality beats purity. 2003 | Errors should never pass silently. 2004 | Unless explicitly silenced. 2005 | In the face of ambiguity, refuse the temptation to guess. 2006 | There should be one-- and preferably only one --obvious way to do it. 2007 | Although that way may not be obvious at first unless you're Dutch. 2008 | Now is better than never. 2009 | Although never is often better than *right* now. 2010 | If the implementation is hard to explain, it's a bad idea. 2011 | If the implementation is easy to explain, it may be a good idea. 2012 | Namespaces are one honking great idea -- let's do more of those! 2013 | ``` 2014 | 2015 | 这些(指上面输出的内容)就是Python之道! 2016 | 2017 | ```py 2018 | >>> love = this 2019 | >>> this is love 2020 | True 2021 | >>> love is True 2022 | False 2023 | >>> love is False 2024 | False 2025 | >>> love is not True or False 2026 | True 2027 | >>> love is not True or False; love is love # 爱(love)是个复杂的东西,不是吗? 2028 | True 2029 | ``` 2030 | 2031 | #### :bulb: 解释: 2032 | 2033 | * `this`模块是一个针对Python之道做阐述的彩蛋([PEP 20](https://www.python.org/dev/peps/pep-0020))。 2034 | * 但是这并没有完,如果你看看这个模块的实现代码[this.py](https://hg.python.org/cpython/file/c3896275c0f6/Lib/this.py)。你会发现一个更加有趣的事情,那就是这个代码本身就违反了上面输出的Python之道(当然,我觉得这也是唯一一处违反的地方)。 2035 | * 关于`love is not True or False; love is love`这句话,它解释的很对,不是吗? 2036 | 2037 | --- 2038 | 2039 | ### ▶ 这些语句是存在的! 2040 | 2041 | **循环语句对应的`else`语句.** 下面是一个标准的例子: 2042 | 2043 | ```py 2044 | def does_exists_num(l, to_find): 2045 | for num in l: 2046 | if num == to_find: 2047 | print("Exists!") 2048 | break 2049 | else: 2050 | print("Does not exist") 2051 | ``` 2052 | 2053 | **Output:** 2054 | ```py 2055 | >>> some_list = [1, 2, 3, 4, 5] 2056 | >>> does_exists_num(some_list, 4) 2057 | Exists! 2058 | >>> does_exists_num(some_list, -1) 2059 | Does not exist 2060 | ``` 2061 | 2062 | **异常处理对应的`else`语句** 看例子, 2063 | 2064 | ```py 2065 | try: 2066 | pass 2067 | except: 2068 | print("Exception occurred!!!") 2069 | else: 2070 | print("Try block executed successfully...") 2071 | ``` 2072 | 2073 | **Output:** 2074 | ```py 2075 | Try block executed successfully... 2076 | ``` 2077 | 2078 | #### :bulb: 解释: 2079 | - 当一个`else`语句紧跟在一个循环语句后面的时候,只有当循环语句块内有明确使用`break`退出,否则所有循环结束就会执行`else`语句块。 2080 | - 跟在`try`语句块后面的`else`语句,又叫做“完成语句(completion clause)”。意味着如果`try`语句块的内容顺利运行完毕,那么就会进入到`else`语句块。 2081 | 2082 | --- 2083 | 2084 | ### ▶ 无限(Inpinity) * 2085 | 2086 | **Output (Python 3.x):** 2087 | ```py 2088 | >>> infinity = float('infinity') 2089 | >>> hash(infinity) 2090 | 314159 2091 | >>> hash(float('-inf')) 2092 | -314159 2093 | ``` 2094 | 2095 | #### :bulb: 解释: 2096 | - 无限(infinity)的哈希值是 10⁵ x π. 2097 | - 有趣的是, 在Python3中,`float('-inf')`的哈希值是"-10⁵ x π", 但是在Python2中是"-10⁵ x e"(正无穷还是一样的哈希值)。 2098 | 2099 | --- 2100 | 2101 | ### ▶ 被修改的类成员 * 2102 | 2103 | ```py 2104 | class Yo(object): 2105 | def __init__(self): 2106 | self.__honey = True 2107 | self.bitch = True 2108 | ``` 2109 | 2110 | **Output:** 2111 | ```py 2112 | >>> Yo().bitch 2113 | True 2114 | >>> Yo().__honey 2115 | AttributeError: 'Yo' object has no attribute '__honey' 2116 | >>> Yo()._Yo__honey 2117 | True 2118 | ``` 2119 | 2120 | 为什么非要`Yo()._Yo__honey`才对? 如果有印度朋友他一定会懂这个梗的(好吧,我猜不会有印度人看一个中文的文章,Yo Yo Honey Singh是一个印度著名rapper,这就是梗,也是为什么这个类要起名Yo,哈哈). 2121 | 2122 | #### :bulb: 解释: 2123 | 2124 | * [名称混淆](https://en.wikipedia.org/wiki/Name_mangling)是一种用来避免命名空间下命名冲突的技术。 2125 | * 在Python里,如果某个类成员的名称以`__`(两个下划线)开头,并且不以两个以上的下划线结尾(这就意味着你想定义一个私有变量,但是Python是没有私有变量的),那么编辑器就会自动在这个成员名称之前加上`_NameOfTheClass`(在这里就是`_Yo`),来防止子类不小心访问修改到这个成员属性。 2126 | * 所以,当要访问`__honey`这个成员属性的时候,我们需要在前面加上`_Yo`来进行访问。 2127 | 2128 | --- 2129 | 2130 | --- 2131 | 2132 | ## 第五章: 杂项 2133 | 2134 | 2135 | ### ▶ `+=`更快 2136 | 2137 | ```py 2138 | # 使用"+"连接三个字符串: 2139 | >>> timeit.timeit("s1 = s1 + s2 + s3", setup="s1 = ' ' * 100000; s2 = ' ' * 100000; s3 = ' ' * 100000", number=100) 2140 | 0.25748300552368164 2141 | # 使用"+="连接三个字符串: 2142 | >>> timeit.timeit("s1 += s2 + s3", setup="s1 = ' ' * 100000; s2 = ' ' * 100000; s3 = ' ' * 100000", number=100) 2143 | 0.012188911437988281 2144 | ``` 2145 | 2146 | #### :bulb: 解释: 2147 | + 当需要连接的字符串超过两个的时候,使用`+=`会比使用`+`更快。(比如上面的例子`s1 += s2 + s3`中的`s1`),因为第一个字符串不会在做链接的时候销毁重建(因为`+=`操作符是就地操作的,上面多次已经提到过这个符号特性)。 2148 | 2149 | --- 2150 | 2151 | ### ▶ 超长字符串! 2152 | 2153 | ```py 2154 | #使用加号连接字符串 2155 | def add_string_with_plus(iters): 2156 | s = "" 2157 | for i in range(iters): 2158 | s += "xyz" 2159 | assert len(s) == 3*iters 2160 | 2161 | #使用比特流连接字符串 2162 | def add_bytes_with_plus(iters): 2163 | s = b"" 2164 | for i in range(iters): 2165 | s += b"xyz" 2166 | assert len(s) == 3*iters 2167 | 2168 | #使用format函数连接字符串 2169 | def add_string_with_format(iters): 2170 | fs = "{}"*iters 2171 | s = fs.format(*(["xyz"]*iters)) 2172 | assert len(s) == 3*iters 2173 | 2174 | #使用数组连接字符串 2175 | def add_string_with_join(iters): 2176 | l = [] 2177 | for i in range(iters): 2178 | l.append("xyz") 2179 | s = "".join(l) 2180 | assert len(s) == 3*iters 2181 | 2182 | #将列表转换成字符串 2183 | def convert_list_to_string(l, iters): 2184 | s = "".join(l) 2185 | assert len(s) == 3*iters 2186 | ``` 2187 | 2188 | **Output:** 2189 | ```py 2190 | >>> timeit(add_string_with_plus(10000)) 2191 | 1000 loops, best of 3: 972 µs per loop 2192 | >>> timeit(add_bytes_with_plus(10000)) 2193 | 1000 loops, best of 3: 815 µs per loop 2194 | >>> timeit(add_string_with_format(10000)) 2195 | 1000 loops, best of 3: 508 µs per loop 2196 | >>> timeit(add_string_with_join(10000)) 2197 | 1000 loops, best of 3: 878 µs per loop 2198 | >>> l = ["xyz"]*10000 2199 | >>> timeit(convert_list_to_string(l, 10000)) 2200 | 10000 loops, best of 3: 80 µs per loop 2201 | ``` 2202 | 2203 | 让我们把循环次数提升十倍。 2204 | 2205 | ```py 2206 | >>> timeit(add_string_with_plus(100000)) # 执行时间线性增加 2207 | 100 loops, best of 3: 9.75 ms per loop 2208 | >>> timeit(add_bytes_with_plus(100000)) # 平方式增加 2209 | 1000 loops, best of 3: 974 ms per loop 2210 | >>> timeit(add_string_with_format(100000)) # 线性增加 2211 | 100 loops, best of 3: 5.25 ms per loop 2212 | >>> timeit(add_string_with_join(100000)) # 线性增加 2213 | 100 loops, best of 3: 9.85 ms per loop 2214 | >>> l = ["xyz"]*100000 2215 | >>> timeit(convert_list_to_string(l, 100000)) # 线性增加 2216 | 1000 loops, best of 3: 723 µs per loop 2217 | ``` 2218 | 2219 | #### :bulb: 解释 2220 | - 关于`timeit`模块的用法可以看[这里](https://docs.python.org/3/library/timeit.html)。这是一个用来测量程序块执行时间的模块。 2221 | - 在Python中,不要试图用`+`来连接一个长字符串,因为`str`这个类型在Python中是一个不可变类型,这就意味着每次两个字符串对象连接程序都需要从左到右把两个字符串的每一个字符都复制一份。比如你现在要连接四个长度为10的字符串,那么程序需要拷贝 (10+10) + ((10+10)+10) + (((10+10)+10)+10) = 90 个字符,而不是40个。而且程序运行时间会随着你需要连接的字符串数量和字符数量呈平方形式的增长。(请参考`add_bytes_with_plus`函数的执行时间对比)。 2222 | - 所以,建议大家使用`.format`或者`%`这种语法(但是,它们在连接短字符串的时候会比`+`号慢一点)。 2223 | - 或者,如果已经提前获得了一个可以迭代的对象,则可以使用`''.join(iterable_object)`这种语法把它连接成字符串,这样的速度会比前几个都快。 2224 | - 至于为什么`add_string_with_plus`不会像`add_bytes_with_plus`着这样成平方形式的提升执行时间,是因为前面例子里已经解释过的`+=`操作符的优化效果。如果把`s += "xyz"`这句话替换成了`s = s + "x" + "y" + "z"`,那么执行时间还是会呈平方形式的增加。 2225 | ```py 2226 | def add_string_with_plus(iters): 2227 | s = "" 2228 | for i in range(iters): 2229 | s = s + "x" + "y" + "z" 2230 | assert len(s) == 3*iters 2231 | 2232 | >>> timeit(add_string_with_plus(10000)) 2233 | 100 loops, best of 3: 9.87 ms per loop 2234 | >>> timeit(add_string_with_plus(100000)) # 平方式增加 2235 | 1 loops, best of 3: 1.09 s per loop 2236 | ``` 2237 | 2238 | --- 2239 | 2240 | ### ▶ 字符串到浮点数的转换 2241 | 2242 | ```py 2243 | a = float('inf') 2244 | b = float('nan') 2245 | c = float('-iNf') #这些字符串是大小写不敏感的 2246 | d = float('nan') 2247 | ``` 2248 | 2249 | **Output:** 2250 | ```py 2251 | >>> a 2252 | inf 2253 | >>> b 2254 | nan 2255 | >>> c 2256 | -inf 2257 | >>> float('some_other_string') 2258 | ValueError: could not convert string to float: some_other_string 2259 | >>> a == -c #inf==inf 2260 | True 2261 | >>> None == None # None==None 2262 | True 2263 | >>> b == d #但是 nan!=nan 为真 2264 | False 2265 | >>> 50/a 2266 | 0.0 2267 | >>> a/a 2268 | nan 2269 | >>> 23 + b 2270 | nan 2271 | ``` 2272 | 2273 | #### :bulb: 解释: 2274 | 2275 | `'inf'`和`'nan'`是一种特殊的字符串(大小写不敏感),它们两个都可以强制转换成`float`浮点类型,并且分别代表了数学意义上的"无限"(infinity)和"非数字"(not a number)。 2276 | 2277 | --- 2278 | 2279 | ### ▶ 最后一些小特点集合 2280 | 2281 | * `join()`是一个字符串的函数而不是一个列表附属函数(第一次用的人可能会觉得有点别扭) 2282 | 2283 | **:bulb: 解释:** 2284 | 如果`join()`作为一个字符串的方法,那么它就可以操作所有的可迭代类型(list, tuple, iterators)。但是如果它作为了一个列表(list)的方法,那它同样的也需要为另外那些可迭代类型构造专属的函数内容。 2285 | 2286 | * 下面这些句子看着奇怪,但是它们确实都是符合语法规则的 2287 | + `[] = ()` 在语法上是正确的 (把一个元组赋值给一个列表) 2288 | + `'a'[0][0][0][0][0]` 同样也是语法正确的,因为在Python中字符串是一个序列(可迭代的对象就可以使用索引值进行访问里面的元素)。 2289 | + `3 --0-- 5 == 8`和`--5 == 5`也都是语法正确的,并且最后结果都为`True`。 2290 | 2291 | * 给定一个数字类型变量`a`,`++a`和`--a`在Python中都是合法的,但是它们表达的意思和在C语言(或者C++,Java)中是完全不一样的。 2292 | ```py 2293 | >>> a = 5 2294 | >>> a 2295 | 5 2296 | >>> ++a 2297 | 5 2298 | >>> --a 2299 | 5 2300 | ``` 2301 | 2302 | **:bulb: 解释:** 2303 | + 在Python语法中,是没有`++`这个操作符的。这个仅仅代表两个单独的`+`操作符而已。 2304 | + `++a`在Python中会被解析成`+(+a)`,也就等于`a`。同样的,`--a`也是这种情况。 2305 | + 这篇StackOverflow的[文章](https://stackoverflow.com/questions/3654830/why-are-there-no-and-operators-in-python)讨论了Python中没有自增自减符号背后的逻辑关系。 2306 | 2307 | * 多个Python线程并不会并发执行你的*Python代码*(你没看错,不会!)。也许你想同时开很多个线程然后并发执行你写的Python代码,但是,由于Python中存在一个叫[GIL](https://wiki.python.org/moin/GlobalInterpreterLock)(Global Interpreter Lock)的东西,所以最终你的线程还是会一个接着一个执行。Python的线程执行IO阻塞的任务比较拿手,但是如果想在CPU阻塞的任务中获得真正的并发能力,你需要了解一下Python的[multiprocessing](https://docs.python.org/2/library/multiprocessing.html)模块。 2308 | 2309 | * 列表切片的时候如果索引值超出界限并不会报错 2310 | ```py 2311 | >>> some_list = [1, 2, 3, 4, 5] 2312 | >>> some_list[111:] 2313 | [] 2314 | ``` 2315 | 2316 | * 在Python3中,`int('١٢٣٤٥٦٧٨٩')`会返回`123456789`。在Python中,十进制字符不仅包含十进制的数字,还包含了大部分可以表示为十进制数字形式的字符,比如,U+0660, ARABIC_INDIC DIGIT ZERO,一种印度阿拉伯语里代表数字0的字符。不过还没有中文的"一,二,三..."或者"壹,贰,叁..."这种,有人说是因为中文在unicode中的编码是不连续的,不过无论如何,现在确实还没有。这里有一篇[小故事](http://chris.improbable.org/2014/8/25/adventures-in-unicode-digits/)是关于这个特性的,英语版,有兴趣的同学可以看看。 2317 | 2318 | * `'abc'.count('') == 4`。 下面模拟实现了一个相似的`count()`方法, 2319 | ```py 2320 | def count(s, sub): 2321 | result = 0 2322 | for i in range(len(s) + 1 - len(sub)): 2323 | result += (s[i:i + len(sub)] == sub) 2324 | return result 2325 | ``` 2326 | 产生这个结果的原因是子串为空时会匹配上原始字符串长度为0时的字符(总之,看代码你会懂的)。 2327 | 2328 | --- 2329 | 2330 | 2331 | # 贡献 2332 | 2333 | 欢迎任何补丁和修正!详细信息请看 [CONTRIBUTING.md](./CONTRIBUTING.md) 2334 | 2335 | 如果想要参与讨论, 可以选择创建一个新的[issue](https://github.com/true1023/Crazy-Python/issues) 或者加入我创建的gitter[房间](https://gitter.im/Crazy-Python/Lobby) 2336 | 2337 | # 感谢 2338 | 2339 | 这个仓库翻译自 [What the fu*k Python](https://github.com/satwikkansal/wtfpython),迄今为止没谁需要感谢,感谢下原作者[Satwik Kansal](https://github.com/satwikkansal)吧。 2340 | 2341 | 2342 | 2343 | # :mortar_board: 版权声明 2344 | 2345 | [![WTFPL](https://img.shields.io/badge/License-WTFPL%202.0-lightgrey.svg?style=flat-square)](http://www.wtfpl.net/) 2346 | 2347 | :copyright: [True1023](https://github.com/true1023/) 2348 | 2349 | 2350 | 2351 | ## 帮助 2352 | 2353 | 如果你有任何好的想法和意见,请务必要分享给我。 2354 | 2355 | ## 关于pdf出版的问题 2356 | 2357 | 我会在收集并修正一些bug后生成pdf版本的Crazy Python,想要的小伙伴可以[发信](mailto:shi12li12@gmail.com)给我,或者关注公众号**编程行动派**,我也会第一时间在公众号里通知大家。 2358 | 2359 |

--------------------------------------------------------------------------------