You are on page 1of 267

Python 黑魔法指南 v3.

作者:王炳明
版本:v3.0
v1.0发布时间:2020年05月12日
v2.0发布时间:2020年08月01日
v3.0发布时间:2021年05月23日
微信公众号:Python编程时光
联系邮箱:wongbingming@163.com
Github:https://github.com/iswbm/magic-python

版权归个人所有,欢迎交流分享,不允许用作商业及为个人谋利等用途,违者必究。

本电子书半年左右更新一次,想最新版的电子书,可以扫描下方的二维码,回复 "黑魔法" 即可获


取。
第一章:魔法冷知识

1.1 默默无闻的省略号很好用

在Python中,一切皆对象,省略号也不例外。

在 Python 3 中你可以直接写 ... 来得到它

>>> ...
Ellipsis
>>> type(...)
<class 'ellipsis'>

而在 Python 2 中没有 ... 这个语法,只能直接写Ellipsis来获取。

>>> Ellipsis
Ellipsis
>>> type(Ellipsis)
<type 'ellipsis'>
>>>

它转为布尔值时为真

>>> bool(...)
True
最后,这东西是一个单例。

>>> id(...)
4362672336
>>> id(...)
4362672336

那这东西有啥用呢?

1. 它是 Numpy 的一个语法糖
2. 在 Python 3 中可以使用 ... 代替 pass

$ cat demo.py
def func01():
...

def func02():
pass

func01()
func02()

print("ok")

$ python3 demo.py
ok

1.2 使用 end 来结束代码块

有不少编程语言,循环、判断代码块需要用 end 标明结束,这样一定程度上会使代码逻辑更加清


晰一点。

但是其实在 Python 这种严格缩进的语言里并没有必要这样做。

如果你真的想用,也不是没有办法,具体你看下面这个例子。

__builtins__.end = None

def my_abs(x):
if x > 0:
return x
else:
return -x
end
end
print(my_abs(10))
print(my_abs(-10))

执行后,输出如下

[root@localhost ~]$ python demo.py


10
10

1.3 可直接运行的 zip 包

我们可以经常看到有 Python 包,居然可以以 zip 包进行发布,并且可以不用解压直接使用。

这与大多数人的认识的 Python 包格式不一样,正常人认为 Python 包的格式要嘛 是 egg,要嘛是


whl 格式。

那么这个zip 是如何制作的呢,请看下面的示例。

[root@localhost ~]# ls -l demo


total 8
-rw-r--r-- 1 root root 30 May 8 19:27 calc.py
-rw-r--r-- 1 root root 35 May 8 19:33 __main__.py
[root@localhost ~]#
[root@localhost ~]# cat demo/__main__.py
import calc

print(calc.add(2, 3))
[root@localhost ~]#
[root@localhost ~]# cat demo/calc.py
def add(x, y):
return x+y
[root@localhost ~]#
[root@localhost ~]# python -m zipfile -c demo.zip demo/*
[root@localhost ~]#

制作完成后,我们可以执行用 python 去执行它

[root@localhost ~]# python demo.zip


5
[root@localhost ~]#

1.4 反斜杠的倔强: 不写最后


\ 在 Python 中的用法主要有两种

1、在行尾时,用做续行符

[root@localhost ~]$ cat demo.py


print("hello "\
"world")
[root@localhost ~]$
[root@localhost ~]$ python demo.py
hello world

2、在字符串中,用做转义字符,可以将普通字符转化为有特殊含义的字符。

>>> str1='\nhello'
>>> print(str1)

hello
>>> str2='\thello' tab
>>> print(str2)
hello

但是如果你用单 \ 结尾是会报语法错误的

>>> str3="\"
File "<stdin>", line 1
str3="\"
^
SyntaxError: EOL while scanning string literal

就算你指定它是个 raw 字符串,也不行。

>>> str3=r"\"
File "<stdin>", line 1
str3=r"\"
^
SyntaxError: EOL while scanning string literal
1.5 如何修改解释器提示符

这个当做今天的一个小彩蛋吧。应该算是比较冷门的,估计知道的人很少了吧。

正常情况下,我们在 终端下 执行Python 命令是这样的。

>>> for i in range(2):


... print (i)
...
0
1

你是否想过 >>> 和 ... 这两个提示符也是可以修改的呢?

>>> import sys


>>> sys.ps1
'>>> '
>>> sys.ps2
'... '
>>>
>>> sys.ps2 = '---------------- '
>>> sys.ps1 = 'Python >>>'
Python >>>for i in range(2):
---------------- print (i)
----------------
0
1

1.6 简洁而优雅的链式比较
先给你看一个示例:

>>> False == False == True


False

你知道这个表达式为什么会会返回 False 吗?

它的运行原理与下面这个类似,是不是有点头绪了:

if 80 < score <= 90:


print(" ")

如果你还是不明白,那我再给你整个第一个例子的等价写法。

>>> False == False and False == True


False

这个用法叫做链式比较。

1.7 and 和 or 的短路效应

and 和 or 是我们再熟悉不过的两个逻辑运算符,在 Python 也有它有妙用。

当一个 or 表达式中所有值都为真,Python会选择第一个值

当一个 and 表达式 所有值都为真,Python 会选择最后一个值。

示例如下:

>>>(2 or 3) * (5 and 6 and 7)


14 # 2*7

1.8 连接多个列表最极客的方式

>>> a = [1,2]
>>> b = [3,4]
>>> c = [5,6]
>>>
>>> sum((a,b,c), [])
[1, 2, 3, 4, 5, 6]
1.9 字典居然是可以排序的?

在 Python 3.6 之前字典不可排序的思想,似乎已经根深蒂固。

## Python2.7.10
>>> mydict = {str(i):i for i in range(5)}
>>> mydict
{'1': 1, '0': 0, '3': 3, '2': 2, '4': 4}

假如哪一天,有人跟你说字典也可以是有序的,不要惊讶,那确实是真的

在 Python3.6 + 中字典已经是有序的,并且效率相较之前的还有所提升,具体信息你可以去查询相
关资料。

## Python3.6.7
>>> mydict = {str(i):i for i in range(5)}
>>> mydict
{'0': 0, '1': 1, '2': 2, '3': 3, '4': 4}

1.10 哪些情况下不需要续行符?

在写代码时,为了代码的可读性,代码的排版是尤为重要的。

为了实现高可读性的代码,我们常常使用到的就是续行符 \ 。

>>> a = 'talk is cheap,'\


... 'show me the code.'
>>>
>>> print(a)
talk is cheap,show me the code.

那有哪些情况下,是不需要写续行符的呢?

经过总结,在这些符号中间的代码换行可以省略掉续行符: [] , () , {}

>>> my_list=[1,2,3,
... 4,5,6]

>>> my_tuple=(1,2,3,
... 4,5,6)

>>> my_dict={"name": "MING",


... "gender": "male"}

另外还有,在多行文本注释中 ''' ,续行符也是可以不写的。

>>> text = '''talk is cheap,


... show me code.'''
>>>

但是这种写法回车会自动转化为 \n

>>> text = '''talk is cheap,


... show me code.'''
>>> text
'talk is cheap,\nshow me code.'

1.11 用户无感知的小整数池

为避免整数频繁申请和销毁内存空间,Python 定义了一个小整数池 [-5, 256] 这些整数对象是提前


建立好的,不会被垃圾回收。

以上代码请在 终端Python环境下测试,如果你是在IDE中测试,由于 IDE 的影响,效果会有所不


同。

>>> a = -6
>>> b = -6
>>> a is b
False

>>> a = 256
>>> b = 256
>>> a is b
True

>>> a = 257
>>> b = 257
>>> a is b
False

>>> a = 257; b = 257


>>> a is b
True

问题又来了:最后一个示例,为啥是True?

因为当你在同一行里,同时给两个变量赋同一值时,解释器知道这个对象已经生成,那么它就会引
用到同一个对象。如果分成两行的话,解释器并不知道这个对象已经存在了,就会重新申请内存存
放这个对象。

1.12 神奇的 intern 机制

字符串类型作为Python中最常用的数据类型之一,Python解释器为了提高字符串使用的效率和使用
性能,做了很多优化.

例如:Python解释器中使用了 intern(字符串驻留)的技术来提高字符串效率,什么是intern机
制?就是同样的字符串对象仅仅会保存一份,放在一个字符串储蓄池中,是共用的,当然,肯定不
能改变,这也决定了字符串必须是不可变对象。

>>> s1="hello"
>>> s2="hello"
>>> s1 is s2
True

## intern
>>> s1="hell o"
>>> s2="hell o"
>>> s1 is s2
False

## 20 intern
>>> s1 = "a" * 20
>>> s2 = "a" * 20
>>> s1 is s2
True

>>> s1 = "a" * 21
>>> s2 = "a" * 21
>>> s1 is s2
False
>>> s1 = "ab" * 10
>>> s2 = "ab" * 10
>>> s1 is s2
True

>>> s1 = "ab" * 11
>>> s2 = "ab" * 11
>>> s1 is s2
False

1.13 site-packages和 dist-packages

如果你足够细心,你会在你的机器上,有些包是安装在 site-packages 下,而有些包安装在 dist-


packages 下。

它们有什么区别呢?

一般情况下,你只见过 site-packages 这个目录,而你所安装的包也将安装在 这个目录下。

而 dist-packages 其实是 debian 系的 Linux 系统(如 Ubuntu)才特有的目录,当你使用 apt 去安装


的 Python 包会使用 dist-packages,而你使用 pip 或者 easy_install 安装的包还是照常安装在 site-
packages 下。

Debian 这么设计的原因,是为了减少不同来源的 Python 之间产生的冲突。

如何查找 Python 安装目录

>>> from distutils.sysconfig import get_python_lib


>>> print(get_python_lib())
/usr/lib/python2.7/site-packages

1.14 argument 和 parameter 的区别?

arguments 和 parameter 的翻译都是参数,在中文场景下,二者混用基本没有问题,毕竟都叫参数


嘛。

但若要严格再进行区分,它们实际上还有各自的叫法

parameter:形参(formal parameter),体现在函数内部,作用域是这个函数体。
argument :实参(actual parameter),调用函数实际传递的参数。

举个例子,如下这段代码, "error" 为 argument,而 msg 为 parameter 。

def output_msg(msg):
print(msg)

output_msg("error")

1.15 /usr/bin/env python 有什么用?

我们经常会在别人的脚本或者项目的入口文件里看到第一行是下面这样

#!/usr/bin/python

或者这样

#!/usr/bin/env python

这两者有什么区别呢?

稍微接触过 linux 的人都知道 /usr/bin/python 就是我们执行 python 进入console 模式里的


python
而当你在可执行文件头里使用 #! + /usr/bin/python ,意思就是说你得用哪个软件 (python)
来执行这个文件。

那么加和不加有什么区别呢?

不加的话,你每次执行这个脚本时,都得这样: python xx.py ,

有没有一种方式?可以省去每次都加 python 呢?

当然有,你可以文件头里加上 #!/usr/bin/python ,那么当这个文件有可执行权限 时,只直接写


这个脚本文件,就像下面这样。

明白了这个后,再来看看 !/usr/bin/env python 这个 又是什么意思 ?

当我执行 env python 时,自动进入了 python console 的模式。


这是为什么?和 直接执行 python 好像没什么区别呀

当你执行 env python 时,它其实会去 env | grep PATH 里(也就是


/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin )这几个路径里去依次查找名为python的
可执行文件。

找到一个就直接执行,上面我们的 python 路径是在 /usr/bin/python 里,在 PATH 列表里倒数第


二个目录下,所以当我在 /usr/local/sbin 下创建一个名字也为 python 的可执行文件时,就会执
行 /usr/local/sbin/python 了。

具体演示过程,你可以看下面。

那么对于这两者,我们应该使用哪个呢?

个人感觉应该优先使用 #!/usr/bin/env python ,因为不是所有的机器的 python 解释器都是


/usr/bin/python 。

1.16 dict() 与 {} 生成空字典有什么区别?


在初始化一个空字典时,有的人会写 dict(),而有的人会写成 {}

很多人会想当然的认为二者是等同的,但实际情况却不是这样的。

在运行效率上,{} 会比 dict() 快三倍左右。

使用 timeit 模块,可以轻松测出这个结果

$ python -m timeit -n 1000000 -r 5 -v "dict()"


raw times: 0.0996 0.0975 0.0969 0.0969 0.0994
1000000 loops, best of 5: 0.0969 usec per loop
$
$ python -m timeit -n 1000000 -r 5 -v "{}"
raw times: 0.0305 0.0283 0.0272 0.03 0.0317
1000000 loops, best of 5: 0.0272 usec per loop

那为什么会这样呢?

探究这个过程,可以使用 dis 模块

当使用 {} 时

$ cat demo.py
{}
$
$ python -m dis demo.py
1 0 BUILD_MAP 0
2 POP_TOP
4 LOAD_CONST 0 (None)
6 RETURN_VALUE

当使用 dict() 时:

$ cat demo.py
dict()
$
$ python -m dis demo.py
1 0 LOAD_NAME 0 (dict)
2 CALL_FUNCTION 0
4 POP_TOP
6 LOAD_CONST 0 (None)
8 RETURN_VALUE

可以发现使用 dict(),会多了个调用函数的过程,而这个过程会有进出栈的操作,相对更加耗时。

1.17 有趣但没啥用的 import 用法


import 是 Python 导包的方式。

你知道 Python 中内置了一些很有(wu)趣(liao)的包吗?

Hello World

>>> import __hello__


Hello World!

Python之禅

>>> import this

The Zen of Python, by Tim Peters

Beautiful is better than ugly.


Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!

反地心引力漫画

在 cmd 窗口中导入 antigravity

>>> import antigravity

就会自动打开一个网页。
1.18 正负得负,负负得正

从初中开始,我们就开始接触了 ,并且都知道了 的思想。

Python 作为一门高级语言,它的编写符合人类的思维逻辑,包括 。

>>> 5-3
2
>>> 5--3
8
>>> 5+-3
2
>>> 5++3
8
>>> 5---3
2

1.19 return不一定都是函数的终点

众所周知,try…finally… 的用法是:不管try里面是正常执行还是有报异常,最终都能保证finally能够
执行。

同时我们又知道,一个函数里只要遇到 return 函数就会立马结束。

那问题就来了,以上这两种规则,如果同时存在,Python 解释器会如何选择?哪个优先级更高?

写个示例验证一下,就明白啦

>>> def func():


... try:
... return 'try'
... finally:
... return 'finally'
...
>>> func()
'finally'

从输出中,我们可以发现:在try…finally…语句中,try中的 return 会被直接忽视(这里的 return 不


是函数的终点),因为要保证 finally 能够执行。

如果 try 里的 return 真的是直接被忽视吗?

我们都知道如果一个函数没有 return,会隐式的返回 None,假设 try 里的 return 真的是直接被忽


视,那当finally 下没有显式的 return 的时候,是不是会返回None呢?

还是写个 示例来验证一下:

>>> def func():


... try:
... return 'try'
... finally:
... print('finally')
...
>>>
>>> func()
finally
'try'
>>>
从结果来看,当 finally 下没有 reutrn ,其实 try 里的 return 仍然还是有效的。

那结论就出来了,如果 finally 里有显式的 return,那么这个 return 会直接覆盖 try 里的 return,而


如果 finally 里没有 显式的 return,那么 try 里的 return 仍然有效。

1.20 字符串里的缝隙是什么?

在Python中求一个字符串里,某子字符(串)出现的次数。

大家都懂得使用 count() 函数,比如下面几个常规例子:

>>> "aabb".count("a")
2
>>> "aabb".count("b")
2
>>> "aabb".count("ab")
1

但是如果我想计算空字符串的个数呢?

>>> "aabb".count("")
5

奇怪了吧?

不是应该返回 0 吗?怎么会返回 5?

实际上,在 Python 看来,两个字符之间都是一个空字符,通俗的说就是缝隙。


因此 对于 aabb 这个字符串在 Python 来看应该是这样的

理解了这个“缝隙” 的概念后,以下这些就好理解了。

>>> (" " * 10).count("")


11
>>>
>>> "" in ""
True
>>>
>>> "" in "M"
True

1.21 Python2下 也能使用 print(“”)

可能会有不少人,觉得只有 Python 3 才可以使用 print(),而 Python 2 只能使用 print "" 。

但是其实并不是这样的。

在Python 2.6之前,只支持

print "hello"

在Python 2.6和2.7中,可以支持如下三种

print "hello"
print("hello")
print ("hello")

在Python3.x中,可以支持如下两种

print("hello")
print ("hello")
虽然 在 Python 2.6+ 可以和 Python3.x+ 一样,像函数一样去调用 print ,但是这仅用于两个 python
版本之间的代码兼容,并不是说在 python2.6+下使用 print() 后,就成了函数。

1.22 字母也玩起了障眼法

以下我分别在 Python2.7 和 Python 3.7 的 console 模式下,运行了如下代码。

在Python 2.x 中

>>> valuе = 32
File "<stdin>", line 1
valuе = 32
^
SyntaxError: invalid syntax

在Python 3.x 中

>>> valuе = 32
>>> value
11

什么?没有截图你不信?

如果你在自己的电脑上尝试一下,结果可能是这样的
怎么又好了呢?

如果你想复现的话,请复制我这边给出的代码: valuе = 32

这是为什么呢?

原因在于,我上面使用的 value 变量名里的 е 又不是我们熟悉的 e ,它是 Cyrillic(西里尔)字


母。

>>> ord('е') # cyrillic 'e' (Ye)


1077
>>> ord('e') # latin 'e', as used in English and typed using standard keyboard
101
>>> 'е' == 'e'
False

细思恐极,在这里可千万不要得罪同事们,万一离职的时候,对方把你项目里的 e 全局替换成 e
,到时候你就哭去吧,肉眼根本看不出来嘛。

1.23 数值与字符串的比较

在 Python2 中,数字可以与字符串直接比较。结果是数值永远比字符串小。

>>> 100000000 < ""


True
>>> 100000000 < "hello"
True

但在 Python3 中,却不行。
>>> 100000000 < ""
TypeError: '<' not supported between instances of 'int' and 'str'

1.24 时有时无的切片异常

这是个简单例子,alist 只有5 个元素,当你取第 6 个元素时,会抛出索引异常。这与我们的认知一


致。

>>> alist = [0, 1, 2, 3, 4]


>>> alist[5]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IndexError: list index out of range

但是当你使用 alist[5:] 取一个区间时,即使 alist 并没有 第 6个元素,也不抛出异常,而是会返回


一个新的列表。

>>> alist = [0, 1, 2, 3, 4]


>>> alist[5:]
[]
>>> alist[100:]
[]

1.25 迷一样的字符串

示例一
## Python2.7
>>> a = "Hello_Python"
>>> id(a)
32045616
>>> id("Hello" + "_" + "Python")
32045616

## Python3.7
>>> a = "Hello_Python"
>>> id(a)
38764272
>>> id("Hello" + "_" + "Python")
32045616

示例二

>>> a = "MING"
>>> b = "MING"
>>> a is b
True

## Python2.7
>>> a, b = "MING!", "MING!"
>>> a is b
True

## Python3.7
>>> a, b = "MING!", "MING!"
>>> a is b
False

示例三

## Python2.7
>>> 'a' * 20 is 'aaaaaaaaaaaaaaaaaaaa'
True
>>> 'a' * 21 is 'aaaaaaaaaaaaaaaaaaaaa'
False

## Python3.7
>>> 'a' * 20 is 'aaaaaaaaaaaaaaaaaaaa'
True
>>> 'a' * 21 is 'aaaaaaaaaaaaaaaaaaaaa'
True

1.26 x 与 +x 等价吗?
在大多数情况下,这个等式是成立的。

>>> n1 = 10086
>>> n2 = +n1
>>>
>>> n1 == n2
True

什么情况下,这个等式会不成立呢?

由于Counter的机制, + 用于两个 Counter 实例相加,而相加的结果如果元素的个数 <= 0,就会


被丢弃。

>>> from collections import Counter


>>> ct = Counter('abcdbcaa')
>>> ct
Counter({'a': 3, 'b': 2, 'c': 2, 'd': 1})
>>> ct['c'] = 0
>>> ct['d'] = -2
>>>
>>> ct
Counter({'a': 3, 'b': 2, 'c': 0, 'd': -2})
>>>
>>> +ct
Counter({'a': 3, 'b': 2})

1.27 += 不等同于=+

对列表 进行 += 操作相当于 extend,而使用 =+ 操作是新增了一个列表。

因此会有如下两者的差异。

## =+
>>> a = [1, 2, 3, 4]
>>> b = a
>>> a = a + [5, 6, 7, 8]
>>> a
[1, 2, 3, 4, 5, 6, 7, 8]
>>> b
[1, 2, 3, 4]

## +=
>>> a = [1, 2, 3, 4]
>>> b = a
>>> a += [5, 6, 7, 8]
>>> a
[1, 2, 3, 4, 5, 6, 7, 8]
>>> b
[1, 2, 3, 4, 5, 6, 7, 8]

1.28 循环中的局部变量泄露

在Python 2中 x 的值在一个循环执行之后被改变了。

## Python2
>>> x = 1
>>> [x for x in range(5)]
[0, 1, 2, 3, 4]
>>> x
4

不过在Python3 中这个问题已经得到解决了。

## Python3
>>> x = 1
>>> [x for x in range(5)]
[0, 1, 2, 3, 4]
>>> x
1

1.29 局部/全局变量傻傻分不清

在开始讲之前,你可以试着运行一下下面这小段代码。

## demo.py
a = 1

def add():
a += 1

add()

看似没有毛病,但实则已经犯了一个很基础的问题,运行结果如下:

$ python demo.py
Traceback (most recent call last):
File "demo.py", line 6, in <module>
add()
File "demo.py", line 4, in add
a += 1
UnboundLocalError: local variable 'a' referenced before assignment

回顾一下,什么是局部变量?在非全局下定义声明的变量都是局部变量。

当程序运行到 a += 1 时,Python 解释器就认为在函数内部要给 a 这个变量赋值,当然就把 a


当做局部变量了,但是做为局部变量的 a 还没有被还没被定义。

因此报错是正常的。

理解了上面的例子,给你留个思考题。为什么下面的代码不会报错呢?

$ cat demo.py
a = 1

def output():
print(a)

output()

$ python demo.py
1

1.30 break /continue 和 上下文管理器哪个优先级高?

众所周知,在循环体中(无论是 for 还是 while),continue 会用来跳入下一个循环,而 break 则


用来跳出某个循环体。

同时我们又知道:在上下文管理器中,被包裹的程序主体代码结束会运行上下文管理器中的一段代
码(通常是资源的释放)。
但如果把上下文管理器放在一个循环体中,而在这个上下文管理器中执行了 break ,是否会直接跳
出循环呢?

换句话说,上下文管理器与 break/continue 这两个规则哪一个优先级会更高一些?

这个问题其实不难,只要做一下试验都能轻易地得出答案,难就难在很多对这个答案都是半猜半
疑,无法肯定的回答。

试验代码如下:

import time
import contextlib

@contextlib.contextmanager
def runtime(value):
time.sleep(1)
print("start: a = " + str(value))
yield
print("end: a = " + str(value))

a = 0
while True:
a+=1
with runtime(a):
if a % 2 == 0:
break

从输出的结果来看,当 a = 2 时执行了 break ,此时的并不会直接跳出循环,依然要运行上下文管


理器里清理释放资源的代码(示例中,我使用 print 来替代)。

start: a = 1
end: a = 1
start: a = 2
end: a = 2

另外还有几个与此类似的问题,我这里也直接给出答案,不再细说了

1. continue 与 break 一样,如果先遇到上下文管理器会先进行资源的释放


2. 上面只举例了 while 循环体,而 for 循环也是同样的。

1.31 如何像 awk一样分割字符串?

若你使用过 Shell 中的 awk 工具,会发现用它来分割字符串是非常方便的。特别是多个连续空格会


被当做一个处理。
[root@localhost ~]# cat demo.txt
hello world
[root@localhost ~]#
[root@localhost ~]# awk '{print$1,$2}' demo.txt
hello world

可是转换到 Python 上面来呢?结果可能是这样的。

>>> msg='hello world'


>>> msg.split(' ')
['hello', '', '', '', 'world']

与我预想的结果不符,多个空格会被分割多次。

那有什么办法可以达到 awk 一样的效果呢?

有两种方法。

第一种方法

不加参数,这种只适用于将多个空格当成一个空格处理,如果不是以空格为分隔符的场景,这种就
不适用了。

>>> msg='hello world'


>>> msg.split()
['hello', 'world']

第二种方法

使用 filter 来辅助,这种适用于所有的分隔符,下面以 - 为分隔符来举例。

>>> msg='hello----world'
>>> msg.split('-')
['hello', '', '', '', 'world']
>>>
>>> filter(None, msg.split('-'))
['hello', 'world']

是不是很神奇,filter 印象中第一个参数接收的是 函数,这里直接传 None 居然有奇效。

查看了注释,原来是这个函数会适配 None 的情况,当第一个参数是None的时候,返回第二个参数


(可迭代对象)中非空的值,非常方便。
换用函数的写法,可以这样

>>> msg='hello----world'
>>> msg.split('-')
['hello', '', '', '', 'world']
>>>
>>> filter(lambda item: True if item else False, msg.split('-'))
['hello', 'world']

1.32 如何让大数变得更易于阅读?

当一个数非常大时,可能过百万,也可能上亿,太多位的数字 ,会给我们阅读带来很大的障碍。

比如下面这个数,你能一下子说出它是多少万呢,还是多少亿呢?

281028344

是不是没法很快的辩识出来?

这时候,你可以使用 _ 来辅助标识,写成这样子就清晰多了

281_028_344

关键这种写法,在代码中并不会报错噢(Python2 不支持)

>>> number=281_028_344
>>> number
281028344

第二章:魔法命令行
2.1 懒人必备技能:使用 “_”

对于 _ ,大家对于他的印象都是用于 占位符,省得为一个不需要用到的变量,绞尽脑汁的想变量
名。

今天要介绍的是他的第二种用法,就是在交互式模式下的应用。

示例如下:

>>> 3 + 4
7
>>> _
7
>>> name=' : Python '
>>> name
' : Python '
>>> _
' : Python '

它可以返回上一次的运行结果。

但是,如果是print函数打印出来的就不行了。

>>> 3 + 4
7
>>> _
7
>>> print(" : Python ")
ming
>>> _
7

我自己写了个例子,验证了下,用 __repr__ 输出的内容可以被获取到的。


首先,在我们的目录下,写一个文件 demo.py。内容如下

## demo.py
class mytest():
def __str__(self):
return "hello"

def __repr__(self):
return "world"

然后在这个目录下进入交互式环境。
>>> import demo
>>> mt=demo.mytest()
>>> mt
world
>>> print(mt)
hello
>>> _
world

知道这两个魔法方法的人,一看就明白了,这里不再解释啦。

2.2 最快查看包搜索路径的方式

当你使用 import 导入一个包或模块时,Python 会去一些目录下查找,而这些目录是有优先级顺序


的,正常人会使用 sys.path 查看。

>>> import sys


>>> from pprint import pprint
>>> pprint(sys.path)
['',
'/usr/local/Python3.7/lib/python37.zip',
'/usr/local/Python3.7/lib/python3.7',
'/usr/local/Python3.7/lib/python3.7/lib-dynload',
'/home/wangbm/.local/lib/python3.7/site-packages',
'/usr/local/Python3.7/lib/python3.7/site-packages']
>>>

那有没有更快的方式呢?

我这有一种连 console 模式都不用进入的方法呢?

你可能会想到这种,但这本质上与上面并无区别

[wangbm@localhost ~]$ python -c "print('\n'.join(__import__('sys').path))"

/usr/lib/python2.7/site-packages/pip-18.1-py2.7.egg
/usr/lib/python2.7/site-packages/redis-3.0.1-py2.7.egg
/usr/lib64/python27.zip
/usr/lib64/python2.7
/usr/lib64/python2.7/plat-linux2
/usr/lib64/python2.7/lib-tk
/usr/lib64/python2.7/lib-old
/usr/lib64/python2.7/lib-dynload
/home/wangbm/.local/lib/python2.7/site-packages
/usr/lib64/python2.7/site-packages
/usr/lib64/python2.7/site-packages/gtk-2.0
/usr/lib/python2.7/site-packages

这里我要介绍的是比上面两种都方便的多的方法,一行命令即可解决

[wangbm@localhost ~]$ python3 -m site


sys.path = [
'/home/wangbm',
'/usr/local/Python3.7/lib/python37.zip',
'/usr/local/Python3.7/lib/python3.7',
'/usr/local/Python3.7/lib/python3.7/lib-dynload',
'/home/wangbm/.local/lib/python3.7/site-packages',
'/usr/local/Python3.7/lib/python3.7/site-packages',
]
USER_BASE: '/home/wangbm/.local' (exists)
USER_SITE: '/home/wangbm/.local/lib/python3.7/site-packages' (exists)
ENABLE_USER_SITE: True

从输出你可以发现,这个列的路径会比 sys.path 更全,它包含了用户环境的目录。

2.3 使用 json.tool 来格式化 JSON

假设现在你需要查看你机器上的json文件,而这个文件没有经过任何的美化,阅读起来是非常困难
的。

$ cat demo.json
{"_id":"5f12d319624e57e27d1291fe","index":0,"guid":"4e482708-c6aa-4ef9-a45e-d5ce2c72
c68d","isActive":false,"balance":"$2,954.93","picture":"http://placehold.it/32x32","
age":36,"eyeColor":"green","name":"MasseySaunders","gender":"male","company":"TALAE"
,"email":"masseysaunders@talae.com","phone":"+1(853)508-3237","address":"246IndianaP
lace,Glenbrook,Iowa,3896","about":"Velitmagnanostrudexcepteurduisextemporirurefugiat
aliquasunt.Excepteurvelitquiseuinexinoccaecatoccaecatveliteuet.Commodonisialiquipiru
reminimconsequatminimconsecteturipsumsitex.\r\n","registered":"2017-02-06T06:42:20-0
8:00","latitude":-10.269827,"longitude":-103.12419,"tags":["laborum","excepteur","ve
niam","reprehenderit","voluptate","laborum","in"],"friends":[{"id":0,"name":"Dorothe
aShields"},{"id":1,"name":"AnnaRosales"},{"id":2,"name":"GravesBryant"}],"greeting":
"Hello,MasseySaunders!Youhave8unreadmessages.","favoriteFruit":"apple"}

这时候你就可以使用 python 的命令行来直接美化。

$ python -m json.tool demo.json


{
"_id": "5f12d319624e57e27d1291fe",
"about": "Velitmagnanostrudexcepteurduisextemporirurefugiataliquasunt.Excepteurv
elitquiseuinexinoccaecatoccaecatveliteuet.Commodonisialiquipirureminimconsequatminim
consecteturipsumsitex.\r\n",
"address": "246IndianaPlace,Glenbrook,Iowa,3896",
"age": 36,
"balance": "$2,954.93",
"company": "TALAE",
"email": "masseysaunders@talae.com",
"eyeColor": "green",
"favoriteFruit": "apple",
"friends": [
{
"id": 0,
"name": "DorotheaShields"
},
{
"id": 1,
"name": "AnnaRosales"
},
{
"id": 2,
"name": "GravesBryant"
}
],
"gender": "male",
"greeting": "Hello,MasseySaunders!Youhave8unreadmessages.",
"guid": "4e482708-c6aa-4ef9-a45e-d5ce2c72c68d",
"index": 0,
"isActive": false,
"latitude": -10.269827,
"longitude": -103.12419,
"name": "MasseySaunders",
"phone": "+1(853)508-3237",
"picture": "http://placehold.it/32x32",
"registered": "2017-02-06T06:42:20-08:00",
"tags": [
"laborum",
"excepteur",
"veniam",
"reprehenderit",
"voluptate",
"laborum",
"in"
]
}

2.4 命令行式执行 Python 代码

有时候你只是想验证一小段 Python 代码是否可用时,通常有两种方法

1. 输入 python 回车,进入 console 模式,然后敲入代码进行验证


2. 将你的代码写入 demo.py 脚本中,然后使用 python demo.py 验证

其实还有一种更简单的方法,比如我要计算一个字符串的md5

$ python -c "import hashlib;print(hashlib.md5('hello').hexdigest())"


5d41402abc4b2a76b9719d911017c592

只要加 -c 参数,就可以输入你的 Python 代码了。

2.5 用调试模式执行脚本

当你使用 pdb 进行脚本的调试时,你可能会先在目标代码处输入 import pdb;pdb.set_trace() 来


设置断点。

除此之外,还有一种方法,就是使用 -m pdb

$ python -m pdb demo.py


> /Users/MING/demo.py(1)<module>()
-> import sys
(Pdb)

2.6 如何快速搭建 HTTP 服务器

搭建FTP,或者是搭建网络文件系统,这些方法都能够实现Linux的目录共享。但是FTP和网络文件
系统的功能都过于强大,因此它们都有一些不够方便的地方。比如你想快速共享Linux系统的某个
目录给整个项目团队,还想在一分钟内做到,怎么办?很简单,使用Python中的
SimpleHTTPServer。

SimpleHTTPServer是Python 2自带的一个模块,是Python的Web服务器。它在Python 3已经合并到


http.server模块中。具体例子如下,如不指定端口,则默认是8000端口。

## python2
python -m SimpleHTTPServer 8888

## python3
python3 -m http.server 8888

SimpleHTTPServer有一个特性,如果待共享的目录下有index.html,那么index.html文件会被视为默
认主页;如果不存在index.html文件,那么就会显示整个目录列表。

2.7 快速构建 HTML 帮助文档

当你不知道一个内置模块如何使用时,会怎么做呢?

百度?Google?

其实完全没必要,这里教你一个离线学习 Python 模块的方法。

是的,你没有听错。
就算没有外网网络也能学习 Python 模块.

你只要在命令行下输入 python -m pydoc -p xxx 命令即可开启一个 HTTP 服务,xxx 为端口,你


可以自己指定。

$ python -m pydoc -p 5200


pydoc server ready at http://localhost:5200/

帮助文档的效果如下
2.8 最正确且优雅的装包方法

当你使用 pip 来安装第三方的模块时,通常会使用这样的命令

$ pip install requests

此时如果你的环境中有 Python2 也有 Python 3,那你使用这条命令安装的包是安装 Python2 呢?


还是安装到 Python 3 呢?

就算你的环境上没有安装 Python2,那也有可能存在着多个版本的 Python 吧?比如安装了


Python3.8,也安装了 Python3.9,那你安装包时就会很困惑,我到底把包安装在了哪里?

但若你使用这样的命令去安装,就没有了这样的烦恼了

## python2
$ python -m pip install requests

## python3
$ python3 -m pip install requests

## python3.8
$ python3.8 -m pip install requests

## python3.9
$ python3.9 -m pip install requests

2.9 往 Python Shell 中传入参数


往一个 Python 脚本传入参数,是一件非常简单的事情。

比如这样:

$ python demo.py arg1 arg2

我在验证一些简单的 Python 代码时,喜欢使用 Python Shell 。

那有没有办法在使用 Python Shell 时,向上面传递参数一样,传入参数呢?

经过我的摸索,终于找到了方法,具体方法如下:

2.10 让脚本报错后立即进入调试模式

当你在使用 python xxx.py 这样的方法,执行 Python 脚本时,若因为代码 bug 导致异常未捕


获,那整个程序便会终止退出。

这个时候,我们通常会去排查是什么原因导致的程序崩溃。

大家都知道,排查问题的思路,第一步肯定是去查看日志,若这个 bug 隐藏的比较深,只有在特


定场景下才会现身,那么还需要开发者,复现这个 bug,方能优化代码。

复现有时候很难,有时候虽然简单,但是要伪造各种数据,相当麻烦。

如果有一种方法能在程序崩溃后,立马进入调试模式该有多好啊?

明哥都这么问了,那肯定是带着解决方案来的。

只要你在执行脚本行,加上 -i 参数,即可在脚本执行完毕后进入 Python Shell 模式,方便你进行


调试。

具体演示如下:
需要注意的是:脚本执行完毕,有两种情况:

1. 正常退出
2. 异常退出

这两种都会进入 Python Shell,如果脚本并无异常,最终也会进入 Python Shell 模式,需要你手动


退出

如果希望脚本正确完成时自动推出,可以在脚本最后加上一行 __import__("os")._exit(0)

2.11 极简模式执行 Python Shell

在终端输入 Python 就会进入 Python Shell 。


方便是挺方便,就是有点说不出的难受,谁能告诉我,为什么要多出这么大一段无关的内容。

这有点像,你上爱某艺看视频吧,都要先看个 90 秒的广告。

如果你和我一样不喜欢这种 『牛皮癣』,那么可以加个 -q 参数,静默进入 Python Shell,就像下


面这样子,开启了极简模式,舒服多了。

2.12 在执行任意代码前自动念一段平安经

最近的"平安经"可谓是引起了不小的风波啊。

作为一个正儿八经的程序员,最害怕的就是自己的代码上线出现各种各样的 BUG。

为此明哥就研究了一下,如何在你执行任意 Python 代码前,让 Python 解释器自动念上一段平安


经,保佑代码不出 BUG 。
没想到还真被我研究出来了

做好心理准备了嘛?

我要开始作妖了,噢不,是开始念经了。

感谢佛祖保佑,Everything is ok,No bugs in the code.

你一定很想知道这是如何的吧?

如果你对 Linux 比较熟悉,就会知道,当你在使用 SSH 远程登陆 Linux 服务器的时候?会读取


.bash_profile 文件加载一些环境变量。

.bash_profile 你可以视其为一个 shell 脚本,可以在这里写一些 shell 代码达到你的定制化需


求。

而在 Python 中,也有类似 .bash_profile 的文件,这个文件一般情况下是不存在的。

我们需要新建一个用户环境目录,这个目录比较长,不需要你死记硬背,使用 site 模块的方法就可


以获取,然后使用 mkdir -p 命令创建它。
在这个目录下,新建一个 usercustomize.py 文件,注意名字必须是这个,换成其他的可就识别不
到啦。

这个 usercustomize.py 的内容如下(明哥注:佛祖只保佑几个 Python 的主要应用方向,毕竟咱


是 Python 攻城狮嘛...)

这个文件我放在了我的 github 上,你可以点此前往获取。


一切都完成后,无论你是使用 python xxx.py 执行脚本

还是使用 python 进入 Python Shell ,都会先念一下平安经保平安。


2.13 启动 Python Shell 前自动执行某脚本

前一节我们介绍了一种,只要运行解释器就会自动触发执行 Python 脚本的方法。


除此之外,可还有其他方法呢?

当然是有,只不过相对来说,会麻烦一点了。

先来看一下效果,在 ~/Library/Python/3.9/lib/python/site-packages 目录下并没有


usercustomize.py 文件,但是在执行 python 进入 Python Shell 模式后,还是会打印平安经。

这是如何做到的呢?

很简单,只要做两件事

第一件事,在任意你喜欢的目录下,新建 一个Python 脚本,名字也随意,比如我叫 startup.py


,内容还是和上面一样
第二件事,设置一个环境变量 PYTHONSTARTUP,指向你的脚本路径

$ export PYTHONSTARTUP=/Users/MING/startup.py

这样就可以了。

但是这种方法只适用于 Python Shell ,只不适合 Python 执行脚本的方法。

如果要在脚本中实现这种效果,我目前想到最粗糙我笨拙的方法了 --
2.14 把模块当做脚本来执行 7 种方法及原理

1. 用法举例

前面的文章里其实分享过不少类似的用法。比如:

1、 快速搭建一个 HTTP 服务

## python2
$ python -m SimpleHTTPServer 8888

## python3
$ python3 -m http.server 8888

2、快速构建 HTML 帮助文档


$ python -m pydoc -p 5200

3、快速进入 pdb 调试模式

$ python -m pdb demo.py

4、最优雅且正确的包安装方法

$ python3 -m pip install requests

5、快速美化 JSON 字符串

$ echo '{"name": "MING"}' | python -m json.tool

6、快速打印包的搜索路径

$ python -m site

7、用于快速计算程序执行时长

$ python3 -m timeit '"-".join(map(str, range(100)))'

2. 原理剖析

上面的诸多命令,都有一个特点,在命令中都含有 -m 参数选项,而参数的值,
SimpleHTTPServer, http.server, pydoc,pdb,pip, json.tool,site ,timeit这些都是模块或者
包。

通常来说模块或者包,都是用做工具包由其他模块导入使用,而很少直接使用命令来执行(脚本除
外)。

Python 给我们提供了一种方法,可以让我们将模块里的部分功能抽取出来,直接用于命令行式的
调用。效果就是前面你所看到的。

那这是如何实现的呢?

最好的学习方式,莫过于模仿,直接以 pip 和 json 模块为学习对象,看看目录结构和代码都有什


么特点。

先看一下 pip 的源码目录,发现在其下有一个 __main__.py 的文件,难道这是 -m 的入口?


再看一下 json.tool 的源码文件,json 库下面却没有 __main__.py 的文件。

这就很奇怪了。

不对,再回过头看,我们调用的不是 json 库,而是 json 库下的 tool 模块。

查看 tool 模块的源代码,有一个名为 main 的函数


但它这不是关键,main 函数是在模块中直接被调用的。

只有当 __name__ 为 __main___ 时,main 函数才会被调用

if __name__ == '__main__':
main()

当模块被导入时, __name__ 的值为模块名,

而当模块被直接执行, __name__ 的值就变成了 __main__ 。

这下思路清晰了。

想要使用 -m 的方式执行模块,有两种方式:

第一种:以 -m <package> 的方式执行,只要在 package 下写一个 __main__.py 的文件即可。


第二种:以 -m <package.module> 的方式执行,只要在 module 的代码中,定义一个 main 函
数,然后在最外层写入下面这段固定的代码

if __name__ == '__main__':
main()

上面我将 -m 的使用情况分为两种,但是实际上,只有一种,对于第一种,你完全可以将
-m <package> 理解为 -m <package.__main__> 的简写形式。

3. 实践一下

先把当前路径设置追加到 PATH 的环境变量中

$ export PATH=${PATH}:`pwd`

先来验证一下第一种方法。

然后在当前目录下新建一个 demo 文件夹,并且在 demo 目录下新建一个 __main__.py 的文件,


随便打印点东西

## __main__.py
print("hello, world")

然后直接执行如下命令,立马就能看到效果。

$ python3 -m demo
hello,world
执行过程如下:

再来验证一下使用第二种方法。

在 demo 目录下再新建一个 foobar.py 文件

## foobar.py
def main():
print("hello, world")

if __name__ == "__main__":
main()

最后执行一下如下命令,输出与预期相符

$ python3 -m demo.foobar
hello, foobar

4. -m 存在的意义

-m 实现的效果,无异于直接执行一个 Python 模块/脚本。

那么问题就来了,那我直接执行不就行啦,何必多此一举再加个 -m 呢?

这个问题很有意思,值得一提。
当我们使用一个模块的时候,往往只需要记住模块名,然后使用 import 去导入它就行了。

之所以能这么便利,这得益于 Python 完善的导入机制,你完全不需要知道这个模块文件存在哪个


目录下,它的绝对路径是什么?因为 Python 的包导入机制会帮你做这些事情。

换句话说,如果你不使用 -m 的方式,当你要使用 python -m json.tool ,你就得这样子写

$ echo '{"name": "MING"}' | python /usr/lib64/python2.7/json/tool.py


{
"name": "MING"
}

如此一对比,哪个更方便?你心里应该有数了。

2.15 命令行式打开 idle 编辑脚本

在你安装 Python 解释器的时候,会有一个选项,让你选择是否安装 idle,这是一个极简的 Python


编辑器,对于有点 python 编码的经验的同学,一般都已经安装了更加专业的代码编辑器,比如
pycharm,vscode 等,所以一般是不会去勾选它的。

但是对于第一次接触 Python 编程的初学者来说,在自己的电脑上,大概率是没有安装代码编辑器


的,这时候有一个现成的编辑器,可以尽快的将 hello world 跑起来,是一件非常重要的事情,因
此初学者一般会被建议安装 idle,这也是为什么 idle 是大多数人的第一个 Python 代码编辑器。

在你安装了 idle 后,如果你使用 Windows 电脑,点击 py 文件会有使用 idle 打开的选项,非常方


便你直接编辑 py 文件。

但如若你在 mac 电脑上,你的右键,是没有这个选项的,那如何使用 idle 打开编辑呢?

可以在终端上使用如下这条命令即可调用 idle 打开指定文件

python3 -m idlelib unshelve.py

使用的效果如下
如果你不加文件的路径,默认会打开 idle 的 shell 模式

2.16 快速计算字符串 base64编码

对字符串编码和解码
对一个字符串进行 base64 编码 和 解码(加上 -d 参数即可)

$ echo "hello, world" | python3 -m base64


aGVsbG8sIHdvcmxkCg==

$ echo "aGVsbG8sIHdvcmxkCg==" | python3 -m base64 -d


hello, world

效果如下

对文件进行编码和解码

在命令后面直接加文件的路径

##
$ python3 -m base64 demo.py
ZGVmIG1haW4oKToKICAgcHJpbnQoJ0hlbGxvIFdvcmxk8J+RjCcpCiAgIAppZiBfX25hbWVfXz09
J19fbWFpbl9fJzoKICAgbWFpbigpCg==

##
$ echo "ZGVmIG1haW4oKToKICAgcHJpbnQoJ0hlbGxvIFdvcmxk8J+RjCcpCiAgIAppZiBfX25hbWVfXz09
J19fbWFpbl9fJzoKICAgbWFpbigpCg==" | python3 -m base64 -d
def main():
print('Hello World ')

if __name__=='__main__':
main()

效果如下
如果你的文件是 py 脚本的话,可以直接执行它

2.17 快速找到指定文件的mime类型

识别 html 文件

##
$ python -m mimetypes https://docs.python.org/3/library/mimetypes.html
type: text/html encoding: None

##
$ python -m mimetypes index.html
type: text/html encoding: None

识别图片格式

$ python -m mimetypes https://www.google.com/images/branding/googlelogo/2x/googlelog


o_color_272x92dp.png
type: image/png encoding: None

识别 Python 脚本

$ python -m mimetypes sample.py


type: text/x-python encoding: None # python

识别压缩文件

$ python -m mimetypes sample.py.gz


type: text/x-python encoding: gzip # python gzip

2.18 快速查看 Python 的环境信息

所有与 Python 相关的信息与配置,你都可以使用下面这条命令将其全部打印出来

$ python -m sysconfig

信息包括:

你当前的操作系统平台
Python 的具体版本
包的搜索路径
以及各种环境变量
2.19 快速解压和压缩文件

tar 格式压缩包

创建一个 tar 压缩包

## demo demo.tar
$ python3 -m tarfile -c demo.tar demo

解压 tar 压缩包

## demo.tar demo_new
$ python3 -m tarfile -e demo.tar demo_new

gzip 格式压缩包

创建一个 gzip 格式的压缩包(gzip 的输入,只能是一个文件,而不能是一个目录)

$ ls -l | grep message
-rw-r--r--@ 1 MING staff 97985 4 22 08:30 message
## message.html message.gz
$ python3 -m gzip message

$ ls -l | grep message
-rw-r--r--@ 1 MING staff 97985 4 22 08:30 message
-rw-r--r-- 1 MING staff 24908 5 4 12:49 message.gz

解压一个 gzip 格式的压缩包

$ rm -rf message

$ ls -l | grep message
-rw-r--r-- 1 MING staff 87 5 4 12:51 message.gz

## message.gz
$ python3 -m gzip -d message.gz

$ ls -l | grep message
-rw-r--r-- 1 MING staff 62 5 4 12:52 message
-rw-r--r-- 1 MING staff 87 5 4 12:51 message.gz

zip 格式压缩包

创建一个 zip 格式的压缩包

$ ls -l | grep demo
drwxr-xr-x 3 MING staff 96 5 4 12:44 demo

## demo demo.zip
$ python3 -m zipfile -c demo.zip demo

$ ls -l | grep demo
drwxr-xr-x 3 MING staff 96 5 4 12:44 demo
-rw-r--r-- 1 MING staff 74890 5 4 12:55 demo.zip

解压一个 zip 格式的压缩包

$ rm -rf demo

$ ls -l | grep demo
-rw-r--r-- 1 MING staff 74890 5 4 12:55 demo.zip

$ python3 -m zipfile -e demo.zip demo

$ ls -l | grep demo
drwxr-xr-x 3 MING staff 96 5 4 12:57 demo
-rw-r--r-- 1 MING staff 74890 5 4 12:55 demo.zip

2.20 快速编辑 Python 脚本

pyc是一种二进制文件,是由py文件经过编译后,生成的文件,是一种byte code,py文件变成pyc文
件后,加载的速度会有所提高。因此在一些场景下,可以预先编译成 pyc 文件,来提高加载速度。

编译的命令非常的简单,示例如下

$ tree demo
demo
"## main.py

$ python3 -O -m compileall demo


Listing 'demo'...
Compiling 'demo/main.py'...

$ tree demo
demo
$## __pycache__
% "## main.cpython-39.opt-1.pyc
"## main.py

2.21 使用自带的 telnet 端口检测工具

若你想检测指定的机器上有没有开放某端口,但本机并没有安装 telnet 工具,不如尝试一下


python 自带的 telnetlib 库,亦可实现你的需求。

检查 192.168.56.200 上的 22 端口有没有开放。

$ python3 -m telnetlib -d 192.168.56.200 22


Telnet(192.168.56.200,22): recv b'SSH-2.0-OpenSSH_7.4\r\n'
SSH-2.0-OpenSSH_7.4

Telnet(192.168.56.200,22): send b'\n'


Telnet(192.168.56.200,22): recv b'Protocol mismatch.\n'
Protocol mismatch.
Telnet(192.168.56.200,22): recv b''
*** Connection closed by remote host ***

2.22 快速将项目打包成应用程序

假设我当前有一个 demo 项目,目录结构树及相关文件的的代码如下


现在我使用如下命令,将该项目进行打包,其中 demo 是项目的文件夹名, main:main 中的第一
个 main 指的 main.py ,而第二个 main 指的是 main 函数

$ python3 -m zipapp demo -m "main:main"

执行完成后,会生成一个 demo.pyz 文件,可直接执行它。

具体演示过程如下
2.23 快速打印函数的调用栈

在使用pdb时,手动打印调用栈

import traceback
traceback.print_stack(file=sys.stdout)

或者直接使用 where (更简单的直接一个 w ):https://www.codenong.com/1156023/

(Pdb) where
/usr/lib/python2.7/site-packages/eventlet/greenpool.py(82)_spawn_n_impl()
-> func(*args, **kwargs)
/usr/lib/python2.7/site-packages/eventlet/wsgi.py(719)process_request()
-> proto.__init__(sock, address, self)
/usr/lib64/python2.7/SocketServer.py(649)__init__()
-> self.handle()
/usr/lib64/python2.7/BaseHTTPServer.py(340)handle()
-> self.handle_one_request()
/usr/lib/python2.7/site-packages/eventlet/wsgi.py(384)handle_one_request()
-> self.handle_one_response()
/usr/lib/python2.7/site-packages/eventlet/wsgi.py(481)handle_one_response()

第三章:炫技魔法操作

3.1 八种连接列表的方式
1、最直观的相加

使用 + 对多个列表进行相加,你应该懂,不多说了。

>>> list01 = [1,2,3]


>>> list02 = [4,5,6]
>>> list03 = [7,8,9]
>>>
>>> list01 + list02 + list03
[1, 2, 3, 4, 5, 6, 7, 8, 9]
>>>

2、借助 itertools

itertools 在 Python 里有一个非常强大的内置模块,它专门用于操作可迭代对象。

在前面的文章中也介绍过,使用 itertools.chain() 函数先可迭代对象(在这里指的是列表)串


联起来,组成一个更大的可迭代对象。

最后你再利用 list 将其转化为 列表。

>>> from itertools import chain


>>> list01 = [1,2,3]
>>> list02 = [4,5,6]
>>> list03 = [7,8,9]
>>>
>>> list(chain(list01, list02, list03))
[1, 2, 3, 4, 5, 6, 7, 8, 9]
>>>

3、使用 * 解包

使用 * 可以解包列表,解包后再合并。

示例如下:

>>> list01 = [1,2,3]


>>> list02 = [4,5,6]
>>>
>>> [*list01, *list02]
[1, 2, 3, 4, 5, 6]
>>>
4、使用 extend

在字典中,使用 update 可实现原地更新,而在列表中,使用 extend 可实现列表的自我扩展。

>>> list01 = [1,2,3]


>>> list02 = [4,5,6]
>>>
>>> list01.extend(list02)
>>> list01
[1, 2, 3, 4, 5, 6]

5、使用列表推导式

Python 里对于生成列表、集合、字典,有一套非常 Pythonnic 的写法。

那就是列表解析式,集合解析式和字典解析式,通常是 Python 发烧友的最爱,那么今天的主题:


列表合并,列表推导式还能否胜任呢?

当然可以,具体示例代码如下:

>>> list01 = [1,2,3]


>>> list02 = [4,5,6]
>>> list03 = [7,8,9]
>>>
>>> [x for l in (list01, list02, list03) for x in l]
[1, 2, 3, 4, 5, 6, 7, 8, 9]
>>>

6、使用 heapq

heapq 是 Python 的一个标准模块,它提供了堆排序算法的实现。

该模块里有一个 merge 方法,可以用于合并多个列表,如下所示

>>> list01 = [1,2,3]


>>> list02 = [4,5,6]
>>> list03 = [7,8,9]
>>>
>>> from heapq import merge
>>>
>>> list(merge(list01, list02, list03))
[1, 2, 3, 4, 5, 6, 7, 8, 9]
>>>
要注意的是,heapq.merge 除了合并多个列表外,它还会将合并后的最终的列表进行排序。

>>> list01 = [2,5,3]


>>> list02 = [1,4,6]
>>> list03 = [7,9,8]
>>>
>>> from heapq import merge
>>>
>>> list(merge(list01, list02, list03))
[1, 2, 4, 5, 3, 6, 7, 9, 8]
>>>

它的效果等价于下面这行代码:

sorted(itertools.chain(*iterables))

如果你希望得到一个始终有序的列表,那请第一时间想到 heapq.merge,因为它采用堆排序,效率
非常高。但若你不希望得到一个排过序的列表,就不要使用它了。

7、借助魔法方法

有一个魔法方法叫 __add__ ,当我们使用第一种方法 list01 + list02 的时候,内部实际上是作用在


__add__ 这个魔法方法上的。

所以以下两种方法其实是等价的

>>> list01 = [1,2,3]


>>> list02 = [4,5,6]
>>>
>>> list01 + list02
[1, 2, 3, 4, 5, 6]
>>>
>>>
>>> list01.__add__(list02)
[1, 2, 3, 4, 5, 6]
>>>

借用这个魔法特性,我们可以 reduce 这个方法来对多个列表进行合并,示例代码如下

>>> list01 = [1,2,3]


>>> list02 = [4,5,6]
>>> list03 = [7,8,9]
>>>
>>> from functools import reduce
>>> reduce(list.__add__, (list01, list02, list03))
[1, 2, 3, 4, 5, 6, 7, 8, 9]
>>>

8. 使用 yield from

在 yield from 后可接一个可迭代对象,用于迭代并返回其中的每一个元素。

因此,我们可以像下面这样自定义一个合并列表的工具函数。

>>> list01 = [1,2,3]


>>> list02 = [4,5,6]
>>> list03 = [7,8,9]
>>>
>>> def merge(*lists):
... for l in lists:
... yield from l
...
>>> list(merge(list01, list02, list03))
[1, 2, 3, 4, 5, 6, 7, 8, 9]
>>

3.2 合并字典的 8 种方法

1、最简单的原地更新

字典对象内置了一个 update 方法,用于把另一个字典更新到自己身上。

>>> profile = {"name": "xiaoming", "age": 27}


>>> ext_info = {"gender": "male"}
>>>
>>> profile.update(ext_info)
>>> print(profile)
{'name': 'xiaoming', 'age': 27, 'gender': 'male'}

如果想使用 update 这种最简单、最地道原生的方法,但又不想更新到自己身上,而是生成一个新


的对象,那请使用深拷贝。

>>> profile = {"name": "xiaoming", "age": 27}


>>> ext_info = {"gender": "male"}
>>>
>>> from copy import deepcopy
>>>
>>> full_profile = deepcopy(profile)
>>> full_profile.update(ext_info)
>>>
>>> print(full_profile)
{'name': 'xiaoming', 'age': 27, 'gender': 'male'}
>>> print(profile)
{"name": "xiaoming", "age": 27}

2、先解包再合并字典

使用 ** 可以解包字典,解包完后再使用 dict 或者 {} 就可以合并。

>>> profile = {"name": "xiaoming", "age": 27}


>>> ext_info = {"gender": "male"}
>>>
>>> full_profile01 = {**profile, **ext_info}
>>> print(full_profile01)
{'name': 'xiaoming', 'age': 27, 'gender': 'male'}
>>>
>>> full_profile02 = dict(**profile, **ext_info)
>>> print(full_profile02)
{'name': 'xiaoming', 'age': 27, 'gender': 'male'}

若你不知道 dict(**profile, **ext_info) 做了啥,你可以将它等价于

>>> dict((("name", "xiaoming"), ("age", 27), ("gender", "male")))


{'name': 'xiaoming', 'age': 27, 'gender': 'male'}

3、借助 itertools

在 Python 里有一个非常强大的内置模块,它专门用于操作可迭代对象。

正好我们字典也是可迭代对象,自然就可以想到,可以使用 itertools.chain() 函数先将多个字


典(可迭代对象)串联起来,组成一个更大的可迭代对象,然后再使用 dict 转成字典。

>>> import itertools


>>>
>>> profile = {"name": "xiaoming", "age": 27}
>>> ext_info = {"gender": "male"}
>>>
>>>
>>> dict(itertools.chain(profile.items(), ext_info.items()))
{'name': 'xiaoming', 'age': 27, 'gender': 'male'}

4、借助 ChainMap
如果可以引入一个辅助包,那我就再提一个, ChainMap 也可以达到和 itertools 同样的效果。

>>> from collections import ChainMap


>>>
>>> profile = {"name": "xiaoming", "age": 27}
>>> ext_info = {"gender": "male"}
>>>
>>> dict(ChainMap(profile, ext_info))
{'name': 'xiaoming', 'age': 27, 'gender': 'male'}

使用 ChainMap 有一点需要注意,当字典间有重复的键时,只会取第一个值,排在后面的键值并不
会更新掉前面的(使用 itertools 就不会有这个问题)。

>>> from collections import ChainMap


>>>
>>> profile = {"name": "xiaoming", "age": 27}
>>> ext_info={"age": 30}
>>> dict(ChainMap(profile, ext_info))
{'name': 'xiaoming', 'age': 27}

5、使用dict.items() 合并

在 Python 3.9 之前,其实就已经有 | 操作符了,只不过它通常用于对集合(set)取并集。

利用这一点,也可以将它用于字典的合并,只不过得绕个弯子,有点不好理解。

你得先利用 items 方法将 dict 转成 dict_items,再对这两个 dict_items 取并集,最后利用 dict 函


数,转成字典。

>>> profile = {"name": "xiaoming", "age": 27}


>>> ext_info = {"gender": "male"}
>>>
>>> full_profile = dict(profile.items() | ext_info.items())
>>> full_profile
{'gender': 'male', 'age': 27, 'name': 'xiaoming'}

当然了,你如果嫌这样太麻烦,也可以简单点,直接使用 list 函数再合并(示例为 Python 3.x )

>>> profile = {"name": "xiaoming", "age": 27}


>>> ext_info = {"gender": "male"}
>>>
>>> dict(list(profile.items()) + list(ext_info.items()))
{'name': 'xiaoming', 'age': 27, 'gender': 'male'}

若你在 Python 2.x 下,可以直接省去 list 函数。


>>> profile = {"name": "xiaoming", "age": 27}
>>> ext_info = {"gender": "male"}
>>>
>>> dict(profile.items() + ext_info.items())
{'name': 'xiaoming', 'age': 27, 'gender': 'male'}

6、最酷炫的字典解析式

Python 里对于生成列表、集合、字典,有一套非常 Pythonnic 的写法。

那就是列表解析式,集合解析式和字典解析式,通常是 Python 发烧友的最爱,那么今天的主题:


字典合并,字典解析式还能否胜任呢?

当然可以,具体示例代码如下:

>>> profile = {"name": "xiaoming", "age": 27}


>>> ext_info = {"gender": "male"}
>>>
>>> {k:v for d in [profile, ext_info] for k,v in d.items()}
{'name': 'xiaoming', 'age': 27, 'gender': 'male'}

7、Python 3.9 新特性

在 2 月份发布的 Python 3.9.04a 版本中,新增了一个抓眼球的新操作符操作符: | , PEP584 将


它称之为合并操作符(Union Operator),用它可以很直观地合并多个字典。

>>> profile = {"name": "xiaoming", "age": 27}


>>> ext_info = {"gender": "male"}
>>>
>>> profile | ext_info
{'name': 'xiaoming', 'age': 27, 'gender': 'male'}
>>>
>>> ext_info | profile
{'gender': 'male', 'name': 'xiaoming', 'age': 27}
>>>
>>>

除了 | 操作符之外,还有另外一个操作符 |= ,类似于原地更新。

>>> ext_info |= profile


>>> ext_info
{'gender': 'male', 'name': 'xiaoming', 'age': 27}
>>>
>>>
>>> profile |= ext_info
>>> profile
{'name': 'xiaoming', 'age': 27, 'gender': 'male'}

看到这里,有没有涨姿势了,学了这么久的 Python ,没想到合并字典还有这么多的方法。本篇文


章的主旨,并不在于让你全部掌握这 7 种合并字典的方法,实际在工作中,你只要选用一种最顺手
的方式即可,但是在协同工作中,或者在阅读他人代码时,你不可避免地会碰到各式各样的写法,
这时候你能下意识的知道这是在做合并字典的操作,那这篇文章就是有意义的。

3.3 花式导包的八种方法

1. 直接 import

人尽皆知的方法,直接导入即可

>>> import os
>>> os.getcwd()
'/home/wangbm'

与此类似的还有,不再细讲

import ...
import ... as ...
from ... import ...
from ... import ... as ...

一般情况下,使用 import 语句导入模块已经够用的。

但是在一些特殊场景中,可能还需要其他的导入方式。

下面我会一一地给你介绍。

2. 使用 __import__

__import__ 函数可用于导入模块,import 语句也会调用函数。其定义为:

__import__(name[, globals[, locals[, fromlist[, level]]]])

参数介绍:

name (required): 被加载 module 的名称


globals (optional): 包含全局变量的字典,该选项很少使用,采用默认值 global()
locals (optional): 包含局部变量的字典,内部标准实现未用到该变量,采用默认值 - local()
fromlist (Optional): 被导入的 submodule 名称
level (Optional): 导入路径选项,Python 2 中默认为 -1,表示同时支持 absolute import 和
relative import。Python 3 中默认为 0,表示仅支持 absolute import。如果大于 0,则表示相对
导入的父目录的级数,即 1 类似于 '.',2 类似于 '. .'。

使用示例如下:

>>> os = __import__('os')
>>> os.getcwd()
'/home/wangbm'

如果要实现 import xx as yy 的效果,只要修改左值即可

如下示例,等价于 import os as myos :

>>> myos = __import__('os')


>>> myos.getcwd()
'/home/wangbm'

上面说过的 __import__ 是一个内建函数,既然是内建函数的话,那么这个内建函数必将存在于


__buildins__ 中,因此我们还可以这样导入 os 的模块:

>>> __builtins__.__dict__['__import__']('os').getcwd()
'/home/wangbm'

3. 使用 importlib 模块

importlib 是 Python 中的一个标准库,importlib 能提供的功能非常全面。

它的简单示例:

>>> import importlib


>>> os=importlib.import_module("os")
>>> os.getcwd()
'/home/wangbm'

如果要实现 import xx as yy 效果,可以这样

>>> import importlib


>>>
>>> myos = importlib.import_module("os")
>>> myos.getcwd()
'/home/wangbm'
4. 使用 imp 模块

imp 模块提供了一些 import 语句内部实现的接口。例如模块查找(find_module)、模块加载


(load_module)等等(模块的导入过程会包含模块查找、加载、缓存等步骤)。可以用该模块来
简单实现内建的 __import__ 函数功能:

>>> import imp


>>> file, pathname, desc = imp.find_module('os')
>>> myos = imp.load_module('sep', file, pathname, desc)
>>> myos
<module 'sep' from '/usr/lib64/python2.7/os.pyc'>
>>> myos.getcwd()
'/home/wangbm'

从 python 3 开始,内建的 reload 函数被移到了 imp 模块中。而从 Python 3.4 开始,imp 模块被否
决,不再建议使用,其包含的功能被移到了 importlib 模块下。即从 Python 3.4 开始,importlib 模
块是之前 imp 模块和 importlib 模块的合集。

5. 使用 execfile

在 Python 2 中有一个 execfile 函数,利用它可以用来执行一个文件。

语法如下:

execfile(filename[, globals[, locals]])

参数有这么几个:

filename:文件名。
globals:变量作用域,全局命名空间,如果被提供,则必须是一个字典对象。
locals:变量作用域,局部命名空间,如果被提供,可以是任何映射对象。

>>> execfile("/usr/lib64/python2.7/os.py")
>>>
>>> getcwd()
'/home/wangbm'

6. 使用 exec 执行

execfile 只能在 Python2 中使用,Python 3.x 里已经删除了这个函数。


但是原理值得借鉴,你可以使用 open ... read 读取文件内容,然后再用 exec 去执行模块。

示例如下:

>>> with open("/usr/lib64/python2.7/os.py", "r") as f:


... exec(f.read())
...
>>> getcwd()
'/home/wangbm'

7. import_from_github_com

有一个包叫做 import_from_github_com,从名字上很容易得知,它是一个可以从 github 下载安装


并导入的包。为了使用它,你需要做的就是按照如下命令使用pip 先安装它。

$ python3 -m pip install import_from_github_com

这个包使用了PEP 302中新的引入钩子,允许你可以从github上引入包。这个包实际做的就是安装
这个包并将它添加到本地。你需要 Python 3.2 或者更高的版本,并且 git 和 pip 都已经安装才能使
用这个包。

pip 要保证是较新版本,如果不是请执行如下命令进行升级。

$ python3 -m pip install --upgrade pip

确保环境 ok 后,你就可以在 Python shell 中使用 import_from_github_com

示例如下

>>> from github_com.zzzeek import sqlalchemy


Collecting git+https://github.com/zzzeek/sqlalchemy
Cloning https://github.com/zzzeek/sqlalchemy to /tmp/pip-acfv7t06-build
Installing collected packages: SQLAlchemy
Running setup.py install for SQLAlchemy ... done
Successfully installed SQLAlchemy-1.1.0b1.dev0
>>> locals()
{'__builtins__': <module 'builtins' (built-in)>, '__spec__': None,
'__package__': None, '__doc__': None, '__name__': '__main__',
'sqlalchemy': <module 'sqlalchemy' from '/usr/local/lib/python3.5/site-packages/\
sqlalchemy/__init__.py'>,
'__loader__': <class '_frozen_importlib.BuiltinImporter'>}
>>>

看了 import_from_github_com的源码后,你会注意到它并没有使用importlib。实际上,它的原理就
是使用 pip 来安装那些没有安装的包,然后使用Python的 __import__() 函数来引入新安装的模
块。

8、远程导入模块

我在这篇文章里(深入探讨 Python 的 import 机制:实现远程导入模块),深入剖析了导入模块的


内部原理,并在最后手动实现了从远程服务器上读取模块内容,并在本地成功将模块导入的导入
器。

具体内容非常的多,你可以点击这个链接进行深入学习。

示例代码如下:

## py my_importer.py
import sys
import importlib
import urllib.request as urllib2

class UrlMetaFinder(importlib.abc.MetaPathFinder):
def __init__(self, baseurl):
self._baseurl = baseurl

def find_module(self, fullname, path=None):


if path is None:
baseurl = self._baseurl
else:
# url
if not path.startswith(self._baseurl):
return None
baseurl = path

try:
loader = UrlMetaLoader(baseurl)
return loader
except Exception:
return None

class UrlMetaLoader(importlib.abc.SourceLoader):
def __init__(self, baseurl):
self.baseurl = baseurl

def get_code(self, fullname):


f = urllib2.urlopen(self.get_filename(fullname))
return f.read()

def get_data(self):
pass
def get_filename(self, fullname):
return self.baseurl + fullname + '.py'

def install_meta(address):
finder = UrlMetaFinder(address)
sys.meta_path.append(finder)

并且在远程服务器上开启 http 服务(为了方便,我仅在本地进行演示),并且手动编辑一个名为


my_info 的 python 文件,如果后面导入成功会打印 ok 。

$ mkdir httpserver && cd httpserver


$ cat>my_info.py<EOF
name='wangbm'
print('ok')
EOF
$ cat my_info.py
name='wangbm'
print('ok')
$
$ python3 -m http.server 12800
Serving HTTP on 0.0.0.0 port 12800 (http://0.0.0.0:12800/) ...
...

一切准备好,验证开始。

>>> from my_importer import install_meta


>>> install_meta('http://localhost:12800/') # sys.meta_path finder
>>> import my_info # ok
ok
>>> my_info.name #
'wangbm'

好了,8 种方法都给大家介绍完毕,对于普通开发者来说,其实只要掌握 import 这种方法足够


了,而对于那些想要自己开发框架的人来说,深入学习 __import__ 以及 importlib 是非常有必要
的。

3.4 条件语句的七种写法

第一种:原代码

这是一段非常简单的通过年龄判断一个人是否成年的代码,由于代码行数过多,有些人就不太愿意
这样写,因为这体现不出自己多年的 Python 功力。
if age > 18:
return " "
else:
return " "

下面我列举了六种这段代码的变异写法,一个比一个还 6 ,单独拿出来比较好理解,放在工程代码
里,没用过这些学法的人,一定会看得一脸懵逼,理解了之后,又不经意大呼:卧槽,还可以这样
写?,而后就要开始骂街了:这是给人看的代码? (除了第一种之外)

第二种

语法:

<on_true> if <condition> else <on_false>

例子

>>> age1 = 20
>>> age2 = 17
>>>
>>>
>>> msg1 = " " if age1 > 18 else " "
>>> print msg1

>>>
>>> msg2 = " " if age2 > 18 else " "
>>> print msg2

>>>

第三种

语法

<condition> and <on_true> or <on_false>

例子

>>> msg1 = age1 > 18 and " " or " "


>>> msg2 = " " if age2 > 18 else " "
>>>
>>> print(msg1)

>>>
>>> print(msg2)

第四种

语法

(<on_false>, <on_true>)[condition]

例子

>>> msg1 = (" ", " ")[age1 > 18]


>>> print(msg1)

>>>
>>>
>>> msg2 = (" ", " ")[age2 > 18]
>>> print(msg2)

第五种

语法

(lambda: <on_false>, lambda:<on_true>)[<condition>]()

例子

>>> msg1 = (lambda:" ", lambda:" ")[age1 > 18]()


>>> print(msg1)

>>>
>>> msg2 = (lambda:" ", lambda:" ")[age2 > 18]()
>>> print(msg2)

第六种

语法:

{True: <on_true>, False: <on_false>}[<condition>]


例子:

>>> msg1 = {True: " ", False: " "}[age1 > 18]
>>> print(msg1)

>>>
>>> msg2 = {True: " ", False: " "}[age2 > 18]
>>> print(msg2)

第七种

语法

((<condition>) and (<on_true>,) or (<on_false>,))[0]

例子

>>> msg1 = ((age1 > 18) and (" ",) or (" ",))[0]
>>> print(msg1)

>>>
>>> msg2 = ((age2 > 18) and (" ",) or (" ",))[0]
>>> print(msg2)

以上代码,都比较简单,仔细看都能看懂,我就不做解释了。

看到这里,有没有涨姿势了,学了这么久的 Python ,这么多骚操作,还真是活久见。。这六种写


法里,我最推荐使用的是第一种,自己也经常在用,简洁直白,代码行还少。而其他的写法虽然能
写,但是不会用,也不希望在我余生里碰到会在公共代码里用这些写法的同事。
3.5 判断是否包含子串的七种方法

1、使用 in 和 not in

in 和 not in 在 Python 中是很常用的关键字,我们将它们归类为 。

使用这两个成员运算符,可以很让我们很直观清晰的判断一个对象是否在另一个对象中,示例如
下:

>>> "llo" in "hello, python"


True
>>>
>>> "lol" in "hello, python"
False

2、使用 find 方法

使用 字符串 对象的 find 方法,如果有找到子串,就可以返回指定子串在字符串中的出现位置,如


果没有找到,就返回 -1

>>> "hello, python".find("llo") != -1


True
>>> "hello, python".find("lol") != -1
False
>>
3、使用 index 方法

字符串对象有一个 index 方法,可以返回指定子串在该字符串中第一次出现的​索引,如果没有找到


会抛出异常,因此使用时需要注意捕获。

def is_in(full_str, sub_str):


try:
full_str.index(sub_str)
return True
except ValueError:
return False

print(is_in("hello, python", "llo")) # True


print(is_in("hello, python", "lol")) # False

4、使用 count 方法

利用和 index 这种曲线救国的思路,同样我们可以使用 count 的方法来判断。

只要判断结果大于 0 就说明子串存在于字符串中。

def is_in(full_str, sub_str):


return full_str.count(sub_str) > 0

print(is_in("hello, python", "llo")) # True


print(is_in("hello, python", "lol")) # False

5、通过魔法方法

在第一种方法中,我们使用 in 和 not in 判断一个子串是否存在于另一个字符中,实际上当你使用


in 和 not in 时,Python 解释器会先去检查该对象是否有 __contains__ 魔法方法。

若有就执行它,若没有,Python 就自动会迭代整个序列,只要找到了需要的一项就返回 True 。

示例如下;

>>> "hello, python".__contains__("llo")


True
>>>
>>> "hello, python".__contains__("lol")
False
>>>

这个用法与使用 in 和 not in 没有区别,但不排除有人会特意写成这样来增加代码的理解难度。


6、借助 operator

operator模块是python中内置的操作符函数接口,它定义了一些算术和比较内置操作的函数。
operator模块是用c实现的,所以执行速度比 python 代码快。

在 operator 中有一个方法 contains 可以很方便地判断子串是否在字符串中。

>>> import operator


>>>
>>> operator.contains("hello, python", "llo")
True
>>> operator.contains("hello, python", "lol")
False
>>>

7、使用正则匹配

说到查找功能,那正则绝对可以说是专业的工具,多复杂的查找规则,都能满足你。

对于判断字符串是否存在于另一个字符串中的这个需求,使用正则简直就是大材小用。

import re

def is_in(full_str, sub_str):


if re.findall(sub_str, full_str):
return True
else:
return False

print(is_in("hello, python", "llo")) # True


print(is_in("hello, python", "lol")) # False

3.6 海象运算符的三种用法

Python 版本发展非常快,如今最新的版本已经是 Pyhton 3.9,即便如此,有很多人甚至还停留在


3.6 或者 3.7,连 3.8 还没用上。

很多 Python 3.8 的特性还没来得及了解,就已经成为旧知识了,比如今天要说的海象运算符。

海象运算符是在 PEP 572 被提出的,直到 3.8 版本合入发布。

它的英文原名叫 Assignment Expressions ,翻译过来也就是 ,不过现在大家更普遍地


称之为海象运算符,就是因为它长得真的太像海象了。
第一个用法:if/else

可能有朋友是第一次接触这个新特性,所以还是简单的介绍一下这个海象运算符有什么用?

在 Golang 中的条件语句可以直接在 if 中运算变量的获取后直接对这个变量进行判断,可以让你少


写一行代码

import "fmt"

func main() {
if age := 20;age > 18 {
fmt.Println(" ")
}
}

若在 Python 3.8 之前,Python 必须得这样子写

age = 20
if age > 18:
print(" ")

但有了海象运算符之后,你可以和 Golang 一样(如果你没学过 Golang,那这里要注意,Golang


中的 := 叫短变量声明,意思是声明并初始化,它和 Python 中的 := 不是一个概念)
if (age:= 20) > 18:
print(" ")

第二个用法:while

在不使用 海象运算符之前,使用 while 循环来读取文件的时候,你也许会这么写

file = open("demo.txt", "r")


while True:
line = file.readline()
if not line:
break
print(line.strip())

但有了海象运算符之后,你可以这样

file = open("demo.txt", "r")


while (line := file.readline()):
print(line.strip())

使用它替换以往的无限 while 循环写法更为惊艳

比如,实现一个需要命令行交互输入密码并检验的代码,你也许会这样子写

while True:
p = input("Enter the password: ")
if p == "youpassword":
break

有了海象运算符之后,这样子写更为舒服

while (p := input("Enter the password: ")) != "youpassword":


continue

第三个用法:推导式

这个系列的文章,几乎每篇都能看到推导式的身影,这一篇依旧如此。

在编码过程中,我很喜欢使用推导式,在简单的应用场景下,它简洁且不失高效。

如下这段代码中,我会使用列表推导式得出所有会员中过于肥胖的人的 bmi 指数

members = [
{"name": " ", "age": 23, "height": 1.75, "weight": 72},
{"name": " ", "age": 17, "height": 1.72, "weight": 63},
{"name": " ", "age": 20, "height": 1.78, "weight": 82},
]

count = 0

def get_bmi(info):
global count
count += 1

print(f" {count} ")

height = info["height"]
weight = info["weight"]

return weight / (height**2)

## bmi
fat_bmis = [get_bmi(m) for m in members if get_bmi(m) > 24]

print(fat_bmis)

输出如下

1
2
3
4
[25.88057063502083]

可以看到,会员数只有 3 个,但是 get_bmi 函数却执行了 4 次,原因是在判断时执行了 3 次,而


在构造新的列表时又重复执行了一遍。

如果所有会员都是过于肥胖的,那最终将执行 6 次,这种在大量的数据下是比较浪费性能的,因此
对于这种结构,我通常会使用传统的for 循环 + if 判断。

fat_bmis = []

## bmi
for m in members:
bmi = get_bmi(m)
if bmi > 24:
fat_bmis.append(bmi)

在有了海象运算符之后,你就可以不用在这种场景下做出妥协。
## bmi
fat_bmis = [bmi for m in members if (bmi := get_bmi(m)) > 24]

最终从输出结果可以看出,只执行了 3 次

1
2
3
[25.88057063502083]

这里仅介绍了列表推导式,但在字典推导式和集合推导式中同样适用。不再演示。

海象运算符,是一个新奇的特性,有不少人觉得这样这种特性会破坏代码的可读性。确实在一个新
鲜事物刚出来时是会这样,但我相信经过时间的沉淀后,越来越多的人使用它并享受它带来的便利
时,这种争议也会慢慢消失在历史的长河中。

3.7 模块重载的五种方法

环境准备

新建一个 foo 文件夹,其下包含一个 bar.py 文件

$ tree foo
foo
"## bar.py

0 directories, 1 file

bar.py 的内容非常简单,只写了个 print 语句

print("successful to be imported")

只要 bar.py 被导入一次,就被执行一次 print

禁止重复导入

由于有 sys.modules 的存在,当你导入一个已导入的模块时,实际上是没有效果的。

>>> from foo import bar


successful to be imported
>>> from foo import bar
>>>

重复导入方法一

如果你使用的 python2(记得前面在 foo 文件夹下加一个 __init__.py ),有一个 reload 的方法


可以直接使用

>>> from foo import bar


successful to be imported
>>> from foo import bar
>>>
>>> reload(bar)
successful to be imported
<module 'foo.bar' from 'foo/bar.pyc'>

如果你使用的 python3 那方法就多了,详细请看下面

重复导入方法二

如果你使用 Python3.0 -> 3.3,那么可以使用 imp.reload 方法

>>> from foo import bar


successful to be imported
>>> from foo import bar
>>>
>>> import imp
>>> imp.reload(bar)
successful to be imported
<module 'foo.bar' from '/Users/MING/Code/Python/foo/bar.py'>

但是这个方法在 Python 3.4+,就不推荐使用了

<stdin>:1: DeprecationWarning: the imp module is deprecated in favour of importlib;


see the module's documentation for alternative uses

重复导入方法三

如果你使用的 Python 3.4+,请使用 importlib.reload 方法

>>> from foo import bar


successful to be imported
>>> from foo import bar
>>>
>>> import importlib
>>> importlib.reload(bar)
successful to be imported
<module 'foo.bar' from '/Users/MING/Code/Python/foo/bar.py'>

重复导入方法四

如果你对包的加载器有所了解(详细可以翻阅我以前写的文章:https://iswbm.com/84.html)

还可以使用下面的方法

>>> from foo import bar


successful to be imported
>>> from foo import bar
>>>
>>> bar.__spec__.loader.load_module()
successful to be imported
<module 'foo.bar' from '/Users/MING/Code/Python/foo/bar.py'>

重复导入方法五

既然影响我们重复导入的是 sys.modules,那我们只要将已导入的包从其中移除是不是就好了呢?

>>> import foo.bar


successful to be imported
>>>
>>> import foo.bar
>>>
>>> import sys
>>> sys.modules['foo.bar']
<module 'foo.bar' from '/Users/MING/Code/Python/foo/bar.py'>
>>> del sys.modules['foo.bar']
>>>
>>> import foo.bar
successful to be imported

有没有发现在前面的例子里我使用的都是 from foo import bar ,在这个例子里,却使用


import foo.bar ,这是为什么呢?

这是因为如果你使用 from foo import bar 这种方式,想使用移除 sys.modules 来重载模块这种


方法是失效的。

这应该算是一个小坑,不知道的人,会掉入坑中爬不出来。

>>> import foo.bar


successful to be imported
>>>
>>> import foo.bar
>>>
>>> import sys
>>> del sys.modules['foo.bar']
>>> from foo import bar
>>>

3.8 Python 转义的五种表示法

1. 为什么要有转义?

ASCII 表中一共有 128 个字符。这里面有我们非常熟悉的字母、数字、标点符号,这些都可以从我


们的键盘中输出。除此之外,还有一些非常特殊的字符,这些字符,我通常很难用键盘上的找到,
比如制表符、响铃这种。

为了能将那些特殊字符都能写入到字符串变量中,就规定了一个用于转义的字符 \ ,有了这个字
符,你在字符串中看的字符,print 出来后就不一定你原来看到的了。

举个例子

>>> msg = "hello\013world\013hello\013python"


>>> print(msg)
hello
world
hello
python
>>>

是不是有点神奇?变成阶梯状的输出了。

那个 \013 又是什么意思呢?

\ 是转义符号,上面已经说过

013 是 ASCII 编码的八进制表示,注意前面是 0 且不可省略,而不是字母 o

把八进制的 13 转成 10 进制后是 11
对照查看 ASCII 码表,11 对应的是一个垂直定位符号,这就能解释,为什么是阶梯状的输出字符
串。
2. 转义的 5 种表示法

ASCII 有 128 个字符,如果用 八进制表示,至少得有三位数,才能将其全部表示。这就是为什么说


上面的首位 0 不能省略的原因,即使现在用不上,我也得把它空出来。

而如果使用十六进制,只要两位数就其 ASCII 的字符全部表示出来。同时为了避免和八进制的混淆


起来,所以在 \ 后面要加上英文字母 x 表示十六进制,后面再接两位十六进制的数值。

\ 开头并接三位 0-7 的数值,表示 8 进制


\x 开头并接两位 0-f 的数值,表示 16进制

因此,当我定义一个字符串的值为 hello + 回车 + world 时,就有了多种方法:

## 8
>>> msg = "hello\012world"
>>> print(msg)
hello
world
>>>

## 16
>>> msg = "hello\x0aworld"
>>> print(msg)
hello
world
>>>

通常我们很难记得住一个字符的 ASCII 编号,即使真记住了,也要去转换成八进制或者16进制,实


在是太难了。

因此对于一些常用并且比较特殊字符,我们习惯用另一种类似别名的方式,比如使用 \n 表示换
行,它与 \012 、 \x0a 是等价的。

与此类似的表示法,还有如下这些
于是,要实现 hello + 回车 + world ,就有了第三种方法

##
>>> msg = "hello\nworld"
>>> print(msg)
hello
world
>>>

到目前为止,我们掌握了 三种转义的表示法。

已经非常难得了,让我们的脑洞再大一点吧,接下来再介绍两种。

ASCII 码表所能表示字符实在太有限了,想打印一个中文汉字,抱歉,你得借助 Unicode 码。

Unicode 编码由 4 个16进制数值组合而成

>>> print("\u4E2D")

什么?我为什么知道 的 unicode 是 \u4E2D ?像下面这样打印就知道啦


## Python 2.7
>>> a = u" "
>>> a
u'\u4e2d'

由此,要实现 hello + 回车 + world ,就有了第四种方法。

## unicode \u000a
>>> print('hello\u000aworld')
hello
world

看到这里,你是不是以为要结束啦?

不,还没有。下面还有一种。

Unicode 编码其实还可以由 8 个32进制数值组合而成,为了以前面的区分开来,这里用 \U 开头。

## unicode \U0000000A
>>> print('hello\U0000000Aworld')
hello
world

好啦,目前我们掌握了五种转义的表示法。

总结一下:

1. \ 开头并接三位 0-7 的数值(八进制) --- 可以表示所有ASCII 字符


2. \x 开头并接两位 0-f 的数值(十六进制) --- 可以表示所有ASCII 字符
3. \u 开头并接四位 0-f 的数值(十六进制) --- 可以表示所有 Unicode 字符
4. \U 开头并接八位 0-f 的数值(三十二进制)) --- 可以表示所有 Unicode 字符
5. \ 开头后接除 x、u、U 之外的特定字符 --- 仅可表示部分字符

为什么标题说,转义也可以炫技呢?

试想一下,假如你的同事,在打印日志时,使用这种 unicode 编码,然后你在定位问题的时候使用


这个关键词去搜,却发现什么都搜不到?这就扑街了。
虽然这种行为真的很 sb,但在某些人看来也许是非常牛逼的操作呢?

五种转义的表示法到这里就介绍完成,接下来是更多转义相关的内容,也是非常有意思的内容,有
兴趣的可以继续往下看。

3. raw 字符串

当一个字符串中具有转义的字符时,我们使用 print 打印后,正常情况下,输出的不是我们原来在


字符串中看到的那样子。

那如果我们需要输出 hello\nworld ,不希望 Python 将 \n 转义成 换行符呢?

这种情况下,你可以在定义时将字符串定义成 raw 字符串,只要在字符串前面加个 r 或者 R 即


可。

>>> print(r"hello\nworld")
hello\nworld
>>>
>>> print(R"hello\nworld")
hello\nworld

然而,不是所有时候都可以加 r 的,比如当你的字符串是由某个程序/函数返回给你的,而不是你
自己生成的

## "hello\nworld"
>>> body = spider()
>>> print(body)
hello
world

这个时候打印它, \n 就是换行打印。

4. 使用 repr

对于上面那种无法使用 r 的情况,可以试一下 repr 来解决这个需求:

>>> body = repr(spider())


>>> print(body)
'hello\nworld'

经过 repr 函数的处理后,为让 print 后的结果,接近字符串本身的样子,它实际上做了两件事

1. 将 \ 变为了 \\

2. 在字符串的首尾添加 ' 或者 "

你可以在 Python Shell 下敲入 变量 回车,就可以能看出端倪。

首尾是添加 ' 还是 " ,取决于你原字符串。

>>> body="hello\nworld"
>>> repr(body)
"'hello\\nworld'"
>>>
>>>
>>> body='hello\nworld'
>>> repr(body)
"'hello\\nworld'"

5. 使用 string_escape
如果你还在使用 Python 2 ,其实还可以使用另一种方法。

那就是使用 string.encode('string_escape') 的方法,它同样可以达到 repr 的效果

>>> "hello\nworld".encode('string_escape')
'hello\\nworld'
>>>

6. 查看原生字符串

综上,想查看原生字符串有两种方法:

1. 如果你在 Python Shell 交互模式下,那么敲击变量回车


2. 如果不在 Python Shell 交互模式下,可先使用 repr 处理一下,再使用 print 打印

>>> body="hello\nworld"
>>>
>>> body
'hello\nworld'
>>>
>>> print(repr(body))
'hello\nworld'
>>>

7. 恢复转义:转成原字符串

经过 repr 处理过或者 \\ 取消转义过的字符串,有没有办法再回退出去,变成原先的有转义的


字符串呢?

答案是:有。

如果你使用 Python 2,可以这样:

>>> body="hello\\nworld"
>>>
>>> body
'hello\\nworld'
>>>
>>> body.decode('string_escape')
'hello\nworld'
>>>

如果你使用 Python 3 ,可以这样:

>>> body="hello\\nworld"
>>>
>>> body
'hello\\nworld'
>>>
>>> bytes(body, "utf-8").decode("unicode_escape")
'hello\nworld'
>>>

什么?还要区分 Python 2 和 Python 3?太麻烦了吧。

明哥教你用一种可以兼容 Python 2 和 Python 3 的写法。

首先是在 Python 2 中的输出

>>> import codecs


>>> body="hello\\nworld"
>>>
>>> codecs.decode(body, 'unicode_escape')
u'hello\nworld'
>>>

然后再看看 Python 3 中的输出

>>> import codecs


>>> body="hello\\nworld"
>>>
>>> codecs.decode(body, 'unicode_escape')
'hello\nworld'
>>>

可以看到 Pyhton 2 中的输出 有一个 u ,而 Python 3 的输出没有了 u ,但无论如何 ,他们都取


消了转义。

以上,就是我为大家整理的关于 Python 中转义的全部内容了,整理的过程,不断的发现新知识,


帮助到大家的同时,自己也对转义的一些内容有了更深的理解。

如果本文对你有些许帮助,不如给明哥 来个四连 ~ 比心

3.9 Python 装包的八种方法

1. 使用 easy_install

这应该是最古老的包安装方式了,目前基本没有人使用了。下面是 easy_install
easy_install
的一些安装示例
## PyPI
$ easy_install pkg_name

##
$ easy_install -f http://pythonpaste.org/package_index.html

##
$ easy_install http://example.com/path/to/MyPackage-1.2.3.tgz

## .egg
$ easy_install xxx.egg

2. 使用 pip install

pip 是最主流的包管理方案,使用 pip install xxx 就可以从 PYPI 上搜索并安装 xxx (如果该
包存在的话)。

下面仅列出一些常用的 pip install 的安装示例

$ pip install requests

## pkg /local/wheels
$ pip install --no-index --find-links=/local/wheels pkg

## 2.1.2
$ pip install pkg==2.1.2

## 2.1.2
$ pip install pkg>=2.1.2

## 2.1.2
$ pip install pkg<=2.1.2

更多 pip 的使用方法,可参考我之前写过的文章,介绍得非常清楚:8.8 pip 的详细使用指南

3. 使用 pipx

pipx 是一个专门用于安装和管理 cli 应用程序的工具,使用它安装的 Python 包会单独安装到一个


全新的独有虚拟环境。

由于它是一个第三方工具,因此在使用它之前,需要先安装

$ python3 -m pip install --user pipx


$ python3 -m userpath append ~/.local/bin
Success!
安装就可以使用 pipx 安装cli 工具了。

##
$ pipx install pkg

更多 pipx 的使用方法,可参考我之前写过的文章,介绍得非常清楚:12.4 pipx 安装程序的使用指


4. 使用 setup.py

如果你有编写 setup.py 文件,可以使用如下命令直接安装

##
$ python setup.py install

5. 使用 yum

Python 包在使用 setup.py 构建的时候,对于包的发布格式有多种选项,其中有一个选项是


bdist_rpm ,以这个选项发布出来的包是 rpm 的包格式。

## rpm
$ python setup.py bdist_rpm

对于 rpm 这种格式,你需要使用 yum install xxx 或者 rpm install xxx 来安装。

## yum
$ yum install pkg

## rpm
$ rpm -ivh pkg

6. 使用 pipenv

如果你在使用 pipenv 创建的虚拟环境中,可以使用下面这条命令把包安装到虚拟环境中

$ pipenv install pkg

7. 使用 poetry

如果你有使用 poetry 管理项目依赖,那么可以使用下面这条命令安装包


##
$ poetry add pkg

##
$ poetry add pytest --dev

8. 使用 curl + 管道

有一些第三方工具包提供的安装方法,是直接使用 curl 配置管道来安装,比如上面提到的 poetry


就可以用这种方法安装。

$ curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry


.py | python

3.10 Python装饰器的六种写法

装饰器本质上是一个Python函数,它可以让其他函数在不需要做任何代码变动的前提下增加额外功
能,装饰器的返回值也是一个函数对象。

它经常用于有切面需求的场景,比如:插入日志、性能测试、事务处理、缓存、权限校验等场景。

装饰器是解决这类问题的绝佳设计,有了装饰器,我们就可以抽离出大量与函数功能本身无关的雷
同代码并继续重用。

装饰器的使用方法很固定

先定义一个装饰器(帽子)
再定义你的业务函数或者类(人)
最后把这装饰器(帽子)扣在这个函数(人)头上

就像下面这样子

##
def decorator(func):
def wrapper(*args, **kw):
return func()
return wrapper

##
@decorator
def function():
print("hello, decorator")

实际上,装饰器并不是编码必须性,意思就是说,你不使用装饰器完全可以,它的出现,应该是使
我们的代码

更加优雅,代码结构更加清晰
将实现特定的功能代码封装成装饰器,提高代码复用率,增强代码可读性

接下来,我将以实例讲解,如何编写出各种简单及复杂的装饰器。

1. 第一种:普通装饰器

首先咱来写一个最普通的装饰器,它实现的功能是:

在函数执行前,先记录一行日志
在函数执行完,再记录一行日志

## func
def logger(func):
def wrapper(*args, **kw):
print(' {} :'.format(func.__name__))

#
func(*args, **kw)

print(' ')
return wrapper

假如,我的业务函数是,计算两个数之和。写好后,直接给它带上帽子。

@logger
def add(x, y):
print('{} + {} = {}'.format(x, y, x+y))
然后执行一下 add 函数。

add(200, 50)

来看看输出了什么?

add :
200 + 50 = 250

2. 第二种:带参数的函数装饰器

通过上面两个简单的入门示例,你应该能体会到装饰器的工作原理了。

不过,装饰器的用法还远不止如此,深究下去,还大有文章。今天就一起来把这个知识点学透。

回过头去看看上面的例子,装饰器是不能接收参数的。其用法,只能适用于一些简单的场景。不传
参的装饰器,只能对被装饰函数,执行固定逻辑。

装饰器本身是一个函数,做为一个函数,如果不能传参,那这个函数的功能就会很受限,只能执行
固定的逻辑。这意味着,如果装饰器的逻辑代码的执行需要根据不同场景进行调整,若不能传参的
话,我们就要写两个装饰器,这显然是不合理的。

比如我们要实现一个可以定时发送邮件的任务(一分钟发送一封),定时进行时间同步的任务(一
天同步一次),就可以自己实现一个 periodic_task (定时任务)的装饰器,这个装饰器可以接收一
个时间间隔的参数,间隔多长时间执行一次任务。

可以这样像下面这样写,由于这个功能代码比较复杂,不利于学习,这里就不贴了。

@periodic_task(spacing=60)
def send_mail():
pass

@periodic_task(spacing=86400)
def ntp()
pass

那我们来自己创造一个伪场景,可以在装饰器里传入一个参数,指明国籍,并在函数执行前,用自
己国家的母语打一个招呼。

##
@say_hello("china")
def xiaoming():
pass
## jack
@say_hello("america")
def jack():
pass

那我们如果实现这个装饰器,让其可以实现 呢?

会比较复杂,需要两层嵌套。

def say_hello(contry):
def wrapper(func):
def deco(*args, **kwargs):
if contry == "china":
print(" !")
elif contry == "america":
print('hello.')
else:
return

#
func(*args, **kwargs)
return deco
return wrapper

来执行一下

xiaoming()
print("------------")
jack()

看看输出结果。

!
------------
hello.

3. 第三种:不带参数的类装饰器

以上都是基于函数实现的装饰器,在阅读别人代码时,还可以时常发现还有基于类实现的装饰器。

基于类装饰器的实现,必须实现 __call__ 和 __init__ 两个内置函数。


__init__ :接收被装饰函数
__call__ :实现装饰逻辑。

还是以日志打印这个简单的例子为例
class logger(object):
def __init__(self, func):
self.func = func

def __call__(self, *args, **kwargs):


print("[INFO]: the function {func}() is running..."\
.format(func=self.func.__name__))
return self.func(*args, **kwargs)

@logger
def say(something):
print("say {}!".format(something))

say("hello")

执行一下,看看输出

[INFO]: the function say() is running...


say hello!

4. 第四种:带参数的类装饰器

上面不带参数的例子,你发现没有,只能打印 INFO 级别的日志,正常情况下,我们还需要打印


DEBUG WARNING 等级别的日志。这就需要给类装饰器传入参数,给这个函数指定级别了。

带参数和不带参数的类装饰器有很大的不同。

__init__ :不再接收被装饰函数,而是接收传入参数。
__call__ :接收被装饰函数,实现装饰逻辑。

class logger(object):
def __init__(self, level='INFO'):
self.level = level

def __call__(self, func): #


def wrapper(*args, **kwargs):
print("[{level}]: the function {func}() is running..."\
.format(level=self.level, func=func.__name__))
func(*args, **kwargs)
return wrapper #

@logger(level='WARNING')
def say(something):
print("say {}!".format(something))

say("hello")
我们指定 WARNING 级别,运行一下,来看看输出。

[WARNING]: the function say() is running...


say hello!

5. 第五种:使用偏函数与类实现装饰器

绝大多数装饰器都是基于函数和闭包实现的,但这并非制造装饰器的唯一方式。

事实上,Python 对某个对象是否能通过装饰器( @decorator )形式使用只有一个要求:decorator


必须是一个“可被调用(callable)的对象。

对于这个 callable 对象,我们最熟悉的就是函数了。

除函数之外,类也可以是 callable 对象,只要实现了 __call__ 函数(上面几个例子已经接触过


了)。

还有容易被人忽略的偏函数其实也是 callable 对象。

接下来就来说说,如何使用 类和偏函数结合实现一个与众不同的装饰器。

如下所示,DelayFunc 是一个实现了 __call__ 的类,delay 返回一个偏函数,在这里 delay 就可以


做为一个装饰器。(以下代码摘自 Python工匠:使用装饰器的小技巧)

import time
import functools

class DelayFunc:
def __init__(self, duration, func):
self.duration = duration
self.func = func

def __call__(self, *args, **kwargs):


print(f'Wait for {self.duration} seconds...')
time.sleep(self.duration)
return self.func(*args, **kwargs)

def eager_call(self, *args, **kwargs):


print('Call without delay')
return self.func(*args, **kwargs)

def delay(duration):
"""

.eager_call
"""
#
# functools.partial DelayFunc
return functools.partial(DelayFunc, duration)

我们的业务函数很简单,就是相加

@delay(duration=2)
def add(a, b):
return a+b

来看一下执行过程

>>> add # add Delay


<__main__.DelayFunc object at 0x107bd0be0>
>>>
>>> add(3,5) # __call__
Wait for 2 seconds...
8
>>>
>>> add.func #
<function add at 0x107bef1e0>

6. 第六种:能装饰类的装饰器

用 Python 写单例模式的时候,常用的有三种写法。其中一种,是用装饰器来实现的。

以下便是我自己写的装饰器版的单例写法。

instances = {}

def singleton(cls):
def get_instance(*args, **kw):
cls_name = cls.__name__
print('===== 1 ====')
if not cls_name in instances:
print('===== 2 ====')
instance = cls(*args, **kw)
instances[cls_name] = instance
return instances[cls_name]
return get_instance

@singleton
class User:
_instance = None

def __init__(self, name):


print('===== 3 ====')
self.name = name
可以看到我们用singleton 这个装饰函数来装饰 User 这个类。装饰器用在类上,并不是很常见,但
只要熟悉装饰器的实现过程,就不难以实现对类的装饰。在上面这个例子中,装饰器就只是实现对
类实例的生成的控制而已。

其实例化的过程,你可以参考我这里的调试过程,加以理解。

3.11 Python 读取文件的六种方式

第一种:使用 open

常规操作
with open('data.txt') as fp:
content = fp.readlines()

第二种:使用 fileinput

使用内置库 fileinput

import fileinput

with fileinput.input(files=('data.txt',)) as file:


content = [line for line in file]

第三种:使用 filecache

使用内置库 filecache,你可以用它来指定读取具体某一行,或者某几行,不指定就读取全部行。

import linecache

content = linecache.getlines('werobot.toml')

第四种:使用 codecs

使用 codecs.open 来读取

import codecs
file=codecs.open("README.md", 'r')
file.read()

如果你还在使用 Python2,那么它可以帮你处理掉 Python 2 下写文件时一些编码错误,一般的建


议是:

在 Python 3 下写文件,直接使用 open


在 Python 2 下写文件,推荐使用 codecs.open,特别是有中文的情况下
如果希望代码同时兼容Python2和Python3,那么也推荐用codecs.open

第五种:使用 io 模块

使用 io 模块的 open 函数

import io
file=io.open("README.md")
file.read()

经朋友提醒,我才发现 io.open 和 open 是同一个函数

Python 3.9.2 (default, Feb 28 2021, 17:03:44)


[GCC 10.2.1 20210110] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import os
>>> (open1:=open) is (open2:=os.open)
False
>>> import io
>>> (open3:=open) is (open3:=io.open)
True

第六种:使用 os 模块

os 模块也自带了 open 函数,直接操作的是底层的 I/O 流,操作的时候是最麻烦的

>>> import os
>>> fp = os.open("hello.txt", os.O_RDONLY)
>>> os.read(fp, 12)
b'hello, world'
>>> os.close(fp)

3.12 调用函数的九种方法

方法一:直接调用函数运行

这种是最简单且直观的方法

def task():
print("running task")

task()

如果是在类中,也是如此

class Task:
def task(self):
print("running task")

Task().task()
方法二:使用偏函数来执行

在 functools 这个内置库中,有一个 partial 方法专门用来生成偏函数。

def power(x, n):


s = 1
while n > 0:
n = n - 1
s = s * x
return s

from functools import partial

power_2=partial(power, n=2)
power_2(2) # output: 4
power_2(3) # output: 9

方法三:使用 eval 动态执行

如果你有需要动态执行函数的需要,可以使用 eval + 字符串 来执行函数。

import sys

def pre_task():
print("running pre_task")

def task():
print("running task")

def post_task():
print("running post_task")

argvs = sys.argv[1:]

for action in argvs:


eval(action)()

运行效果如下

$ python demo.py pre_task task post_task


running pre_task
running task
running post_task
方法四:使用 getattr 动态获取执行

若把所有的函数是放在类中,并定义成静态方法,那就不需要用 eval 了,接着使用 getattr 去获取


并调用。

import sys

class Task:
@staticmethod
def pre_task():
print("running pre_task")

@staticmethod
def task():
print("running task")

@staticmethod
def post_task():
print("running post_task")

argvs = sys.argv[1:]

task = Task()

for action in argvs:


func = getattr(task, action)
func()

方法五:使用类本身的字典

我们都知道对象都有一个 __dict__() 的魔法方法,存放所有对象的属性及方法。

到这里,大家可以思考一下, 如果还是上面的代码,我直接取实例的 __dict__() 能不能取到函


数呢?

我相信很多人都会答错。

上面我们定义的是静态方法,静态方法并没有与实例进行绑定,因此静态方法是属于类的,但是不
是属于实例的,实例虽然有使用权(可以调用),但是并没有拥有权。

因此要想通过 __dict__ 获取函数,得通过类本身 Task ,取出来的函数,调用方法和平时的也不


一样,必须先用 __func__ 获取才能调用。

import sys

class Task:
@staticmethod
def pre_task():
print("running pre_task")

func = Task.__dict__.get("pre_task")
func.__func__()

方法六:使用 global() 获取执行

上面放入类中,只是为了方便使用 getattr 的方法,其实不放入类中,也是可以的。此时你需要


借助 globals() 或者 locals() ,它们本质上就是一个字典,你可以直接 get 来获得函数。

import sys

def pre_task():
print("running pre_task")

def task():
print("running task")

def post_task():
print("running post_task")

argvs = sys.argv[1:]

for action in argvs:


globals().get(action)()

方法七:从文本中编译运行

先定义一个字符串,内容是你函数的内容,比如上面的 pre_task ,再通过 compile 函数编进 编


译,转化为字节代码,最后再使用 exec 去执行它。

pre_task = """
print("running pre_task")
"""
exec(compile(pre_task, '<string>', 'exec'))

若你的代码是放在一个 txt 文本中,虽然无法直接导入运行,但仍然可以通过 open 来读取,最后


使用 compile 函数编译运行。

with open('source.txt') as f:
source = f.read()
exec(compile(source, 'source.txt', 'exec'))
方法八:使用 attrgetter 获取执行

在 operator 这个内置库中,有一个获取属性的方法,叫 attrgetter ,获取到函数后再执行。

from operator import attrgetter

class People:
def speak(self, dest):
print("Hello, %s" %dest)

p = People()
caller = attrgetter("speak")
caller(p)(" ")

方法九:使用 methodcaller 执行

同样还是 operator 这个内置库,有一个 methodcaller 方法,使用它,也可以做到动态调用实例方


法的效果。

from operator import methodcaller

class People:
def speak(self, dest):
print("Hello, %s" %dest)

caller = methodcaller("speak", " ")


p = People()
caller(p)

第四章:魔法进阶扫盲

4.1 精通上下文管理器

with 这个关键字,对于每一学习Python的人,都不会陌生。

操作文本对象的时候,几乎所有的人都会让我们要用 with open ,这就是一个上下文管理的例


子。你一定已经相当熟悉了,我就不再废话了。

with open('test.txt') as f:
print(f.readlines())
what context manager?

基本语法

with EXPR as VAR:


BLOCK

先理清几个概念

1. with open('test.txt') as f:
2. open('test.txt')
3. f

how context manager?

要自己实现这样一个上下文管理,要先知道上下文管理协议。

简单点说,就是在一个类里,实现了 __enter__ 和 __exit__ 的方法,这个类的实例就是一个上下


文管理器。

例如这个示例:

class Resource():
def __enter__(self):
print('===connect to resource===')
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print('===close resource connection===')

def operate(self):
print('===in operation===')

with Resource() as res:


res.operate()

我们执行一下,通过日志的打印顺序。可以知道其执行过程。

===connect to resource===
===in operation===
===close resource connection===

从这个示例可以很明显的看出,在编写代码时,可以将资源的连接或者获取放在 __enter__ 中,而


将资源的关闭写在 __exit__ 中。

why context manager?

学习时多问自己几个为什么,养成对一些细节的思考,有助于加深对知识点的理解。

为什么要使用上下文管理器?

在我看来,这和 Python 崇尚的优雅风格有关。

1. 可以以一种更加优雅的方式,操作(创建/获取/释放)资源,如文件操作、数据库连接;
2. 可以以一种更加优雅的方式,处理异常;

第一种,我们上面已经以资源的连接为例讲过了。

而第二种,会被大多数人所忽略。这里会重点讲一下。

大家都知道,处理异常,通常都是使用 try...execept.. 来捕获处理的。这样做一个不好的地方


是,在代码的主逻辑里,会有大量的异常处理代理,这会很大的影响我们的可读性。

好一点的做法呢,可以使用 with 将异常的处理隐藏起来。

仍然是以上面的代码为例,我们将 1/0 这个 写在 operate 里

class Resource():
def __enter__(self):
print('===connect to resource===')
return self

def __exit__(self, exc_type, exc_val, exc_tb):


print('===close resource connection===')
return True

def operate(self):
1/0

with Resource() as res:


res.operate()

运行一下,惊奇地发现,居然不会报错。

这就是上下文管理协议的一个强大之处,异常可以在 __exit__ 进行捕获并由你自己决定如何处


理,是抛出呢还是在这里就解决了。在 __exit__ 里返回 True (没有return 就默认为 return
False),就相当于告诉 Python解释器,这个异常我们已经捕获了,不需要再往外抛了。

在 写 __exit__ 函数时,需要注意的事,它必须要有这三个参数:

exc_type:异常类型
exc_val:异常值
exc_tb:异常的错误栈信息

当主逻辑代码没有报异常时,这三个参数将都为None。

how contextlib?

在上面的例子中,我们只是为了构建一个上下文管理器,却写了一个类。如果只是要实现一个简单
的功能,写一个类未免有点过于繁杂。这时候,我们就想,如果只写一个函数就可以实现上下文管
理器就好了。

这个点Python早就想到了。它给我们提供了一个装饰器,你只要按照它的代码协议来实现函数内
容,就可以将这个函数对象变成一个上下文管理器。

我们按照 contextlib 的协议来自己实现一个打开文件(with open)的上下文管理器。

import contextlib

@contextlib.contextmanager
def open_func(file_name):
# __enter__
print('open file:', file_name, 'in __enter__')
file_handler = open(file_name, 'r')

# yield
yield file_handler

# __exit__
print('close file:', file_name, 'in __exit__')
file_handler.close()
return

with open_func('/Users/MING/mytest.txt') as file_in:


for line in file_in:
print(line)

在被装饰函数里,必须是一个生成器(带有yield),而yield之前的代码,就相当于 __enter__ 里
的内容。yield 之后的代码,就相当于 __exit__ 里的内容。

上面这段代码只能实现上下文管理器的第一个目的(管理资源),并不能实现第二个目的(处理异
常)。

如果要处理异常,可以改成下面这个样子。

import contextlib

@contextlib.contextmanager
def open_func(file_name):
# __enter__
print('open file:', file_name, 'in __enter__')
file_handler = open(file_name, 'r')

try:
yield file_handler
except Exception as exc:
# deal with exception
print('the exception was thrown')
finally:
print('close file:', file_name, 'in __exit__')
file_handler.close()

return

with open_func('/Users/MING/mytest.txt') as file_in:


for line in file_in:
1/0
print(line)

好像只要讲到上下文管理器,大多数人都会谈到打开文件这个经典的例子。

但是在实际开发中,可以使用到上下文管理器的例子也不少。我这边举个我自己的例子。

在OpenStack中,给一个虚拟机创建快照时,需要先创建一个临时文件夹,来存放这个本地快照镜
像,等到本地快照镜像创建完成后,再将这个镜像上传到Glance。然后删除这个临时目录。

这段代码的主逻辑是 ,而 ,属于前置条件, ,是收尾工作。

虽然代码量很少,逻辑也不复杂,但是“ ”这个功能,在一个项目
中很多地方都需要用到,如果可以将这段逻辑处理写成一个工具函数作为一个上下文管理器,那代
码的复用率也大大提高。

代码是这样的
总结起来,使用上下文管理器有三个好处:

1. 提高代码的复用率;
2. 提高代码的优雅度;
3. 提高代码的可读性;

4.2 深入理解描述符

学习 Python 这么久了,说起 Python 的优雅之处,能让我脱口而出的, Descriptor(描述符)特性


可以排得上号。

描述符 是Python 语言独有的特性,它不仅在应用层使用,在语言语法糖的实现上也有使用到(在


下面的文章会一一介绍)。

当你点进这篇文章时

你也许没学过描述符,甚至没听过描述符。
或者你对描述符只是一知半解

无论你是哪种,本篇都将带你全面的学习描述符,一起来感受 Python 语言的优雅。

1. 为什么要使用描述符?
假想你正在给学校写一个成绩管理系统,并没有太多编码经验的你,可能会这样子写。

class Student:
def __init__(self, name, math, chinese, english):
self.name = name
self.math = math
self.chinese = chinese
self.english = english

def __repr__(self):
return "<Student: {}, math:{}, chinese: {}, english:{}>".format(
self.name, self.math, self.chinese, self.english
)

看起来一切都很合理

>>> std1 = Student(' ', 76, 87, 68)


>>> std1
<Student: , math:76, chinese: 87, english:68>

但是程序并不像人那么智能,不会自动根据使用场景判断数据的合法性,如果老师在录入成绩的时
候,不小心录入了将成绩录成了负数,或者超过100,程序是无法感知的。

聪明的你,马上在代码中加入了判断逻辑。

class Student:
def __init__(self, name, math, chinese, english):
self.name = name
if 0 <= math <= 100:
self.math = math
else:
raise ValueError("Valid value must be in [0, 100]")

if 0 <= chinese <= 100:


self.chinese = chinese
else:
raise ValueError("Valid value must be in [0, 100]")

if 0 <= chinese <= 100:


self.english = english
else:
raise ValueError("Valid value must be in [0, 100]")

def __repr__(self):
return "<Student: {}, math:{}, chinese: {}, english:{}>".format(
self.name, self.math, self.chinese, self.english
)
这下程序稍微有点人工智能了,能够自己明辨是非了。

程序是智能了,但在 __init__ 里有太多的判断逻辑,很影响代码的可读性。巧的是,你刚好学过


Property 特性,可以很好的应用在这里。于是你将代码修改成如下,代码的可读性瞬间提升了不少

class Student:
def __init__(self, name, math, chinese, english):
self.name = name
self.math = math
self.chinese = chinese
self.english = english

@property
def math(self):
return self._math

@math.setter
def math(self, value):
if 0 <= value <= 100:
self._math = value
else:
raise ValueError("Valid value must be in [0, 100]")

@property
def chinese(self):
return self._chinese

@chinese.setter
def chinese(self, value):
if 0 <= value <= 100:
self._chinese = value
else:
raise ValueError("Valid value must be in [0, 100]")

@property
def english(self):
return self._english

@english.setter
def english(self, value):
if 0 <= value <= 100:
self._english = value
else:
raise ValueError("Valid value must be in [0, 100]")

def __repr__(self):
return "<Student: {}, math:{}, chinese: {}, english:{}>".format(
self.name, self.math, self.chinese, self.english
)

程序还是一样的人工智能,非常好。

你以为你写的代码,已经非常优秀,无懈可击了。

没想到,人外有天,你的主管看了你的代码后,深深地叹了口气:类里的三个属性,math、
chinese、english,都使用了 Property 对属性的合法性进行了有效控制。功能上,没有问题,但就
是太啰嗦了,三个变量的合法性逻辑都是一样的,只要大于0,小于100 就可以,代码重复率太高
了,这里三个成绩还好,但假设还有地理、生物、历史、化学等十几门的成绩呢,这代码简直没法
忍。去了解一下 Python 的描述符吧。

经过主管的指点,你知道了「描述符」这个东西。怀着一颗敬畏之心,你去搜索了下关于 描述符
的用法。

其实也很简单,一个实现了 的类就是一个描述符。

什么描述符协议:在类里实现了 __get__() 、 __set__() 、 __delete__() 其中至少一个方法。

__get__ : 用于访问属性。它返回属性的值,若属性不存在、不合法等都可以抛出对应的异
常。
__set__ :将在属性分配操作中调用。不会返回任何内容。
__delete__ :控制删除操作。不会返回内容。

对描述符有了大概的了解后,你开始重写上面的方法。

如前所述,Score 类是一个描述符,当从 Student 的实例访问 math、chinese、english这三个属性


的时候,都会经过 Score 类里的三个特殊的方法。这里的 Score 避免了 使用Property 出现大量的代
码无法复用的尴尬。

class Score:
def __init__(self, default=0):
self._score = default

def __set__(self, instance, value):


if not isinstance(value, int):
raise TypeError('Score must be integer')
if not 0 <= value <= 100:
raise ValueError('Valid value must be in [0, 100]')

self._score = value

def __get__(self, instance, owner):


return self._score

def __delete__(self):
del self._score

class Student:
math = Score(0)
chinese = Score(0)
english = Score(0)

def __init__(self, name, math, chinese, english):


self.name = name
self.math = math
self.chinese = chinese
self.english = english
def __repr__(self):
return "<Student: {}, math:{}, chinese: {}, english:{}>".format(
self.name, self.math, self.chinese, self.english
)

实现的效果和前面的一样,可以对数据的合法性进行有效控制(字段类型、数值区间等)

以上,我举了下具体的实例,从最原始的编码风格到 Property ,最后引出描述符。由浅入深,一步


一步带你感受到描述符的优雅之处。

到这里,你需要记住的只有一点,就是描述符给我们带来的编码上的便利,它在实现
、 的基本功能,同时有大大提高代码的复用率。

2. 描述符的访问规则

描述符分两种:

数据描述符:实现了 __get__ 和 __set__ 两种方法的描述符


非数据描述符:只实现了 __get__ 一种方法的描述符

你一定会问,他们有什么区别呢?网上的讲解,我看过几个,很多都把一个简单的东西讲得复杂
了。

其实就一句话,数据描述器和非数据描述器的区别在于:它们相对于实例的字典的优先级不同。

如果实例字典中有与描述符同名的属性,那么:
描述符是数据描述符的话,优先使用数据描述符
描述符是非数据描述符的话,优先使用字典中的属性。

这边还是以上节的成绩管理的例子来说明,方便你理解。

##
class DataDes:
def __init__(self, default=0):
self._score = default

def __set__(self, instance, value):


self._score = value

def __get__(self, instance, owner):


print(" __get__")
return self._score

##
class NoDataDes:
def __init__(self, default=0):
self._score = default

def __get__(self, instance, owner):


print(" __get__")
return self._score

class Student:
math = DataDes(0)
chinese = NoDataDes(0)

def __init__(self, name, math, chinese):


self.name = name
self.math = math
self.chinese = chinese

def __getattribute__(self, item):


print(" __getattribute__")
return super(Student, self).__getattribute__(item)

def __repr__(self):
return "<Student: {}, math:{}, chinese: {},>".format(
self.name, self.math, self.chinese)

需要注意的是,math 是数据描述符,而 chinese 是非数据描述符。从下面的验证中,可以看出,


当实例属性和数据描述符同名时,会优先访问数据描述符(如下面的math),而当实例属性和非
数据描述符同名时,会优先访问实例属性( __getattribute__ )
>>> std = Student('xm', 88, 99)
>>>
>>> std.math
__getattribute__
__get__
88
>>> std.chinese
__getattribute__
99

讲完了数据描述符和非数据描述符,我们还需要了解的对象属性的查找规律。

当我们对一个实例属性进行访问时,Python 会按 obj.__dict__ → type(obj).__dict__ →


type(obj) .__dict__ 顺序进行查找,如果查找到目标属性并发现是一个描述符,Python 会
调用描述符协议来改变默认的控制行为。

3. 基于描述符如何实现property

经过上面的讲解,我们已经知道如何定义描述符,且明白了描述符是如何工作的。

正常人所见过的描述符的用法就是上面提到的那些,我想说的是那只是描述符协议最常见的应用之
一,或许你还不知道,其实有很多 Python 的特性的底层实现机制都是基于 的,比如我
们熟悉的 @property 、 @classmethod 、 @staticmethod 和 super 等。

先来说说 property 吧。

有了前面的基础,我们知道了 property 的基本用法。这里我直接切入主题,从第一篇的例子里精


简了一下。

class Student:
def __init__(self, name):
self.name = name

@property
def math(self):
return self._math

@math.setter
def math(self, value):
if 0 <= value <= 100:
self._math = value
else:
raise ValueError("Valid value must be in [0, 100]")

不防再简单回顾一下它的用法,通过property装饰的函数,如例子中的 math 会变成 Student 实例


的属性。而对 math 属性赋值会进入 使用 math.setter 装饰函数的逻辑代码块。
为什么说 property 底层是基于描述符协议的呢?通过 PyCharm 点击进入 property 的源码,很可
惜,只是一份类似文档一样的伪源码,并没有其具体的实现逻辑。

不过,从这份伪源码的魔法函数结构组成,可以大体知道其实现逻辑。

这里我自己通过模仿其函数结构,结合「描述符协议」来自己实现类 property 特性。

代码如下:

class TestProperty(object):

def __init__(self, fget=None, fset=None, fdel=None, doc=None):


self.fget = fget
self.fset = fset
self.fdel = fdel
self.__doc__ = doc

def __get__(self, obj, objtype=None):


print("in __get__")
if obj is None:
return self
if self.fget is None:
raise AttributeError
return self.fget(obj)

def __set__(self, obj, value):


print("in __set__")
if self.fset is None:
raise AttributeError
self.fset(obj, value)

def __delete__(self, obj):


print("in __delete__")
if self.fdel is None:
raise AttributeError
self.fdel(obj)

def getter(self, fget):


print("in getter")
return type(self)(fget, self.fset, self.fdel, self.__doc__)

def setter(self, fset):


print("in setter")
return type(self)(self.fget, fset, self.fdel, self.__doc__)

def deleter(self, fdel):


print("in deleter")
return type(self)(self.fget, self.fset, fdel, self.__doc__)
然后 Student 类,我们也相应改成如下

class Student:
def __init__(self, name):
self.name = name

#
@TestProperty
def math(self):
return self._math

@math.setter
def math(self, value):
if 0 <= value <= 100:
self._math = value
else:
raise ValueError("Valid value must be in [0, 100]")

为了尽量让你少产生一点疑惑,我这里做两点说明:

1. 使用 TestProperty 装饰后, math 不再是一个函数,而是 TestProperty 类的一个实例。所以


第二个math函数可以使用 math.setter 来装饰,本质是调用 TestProperty.setter 来产生一
个新的 TestProperty 实例赋值给第二个 math 。

2. 第一个 math 和第二个 math 是两个不同 TestProperty 实例。但他们都属于同一个描述符类


(TestProperty),当对 math 对于赋值时,就会进入 TestProperty.__set__ ,当对math 进行
取值里,就会进入 TestProperty.__get__ 。仔细一看,其实最终访问的还是Student实例的
_math 属性。

说了这么多,还是运行一下,更加直观一点。

## TestProperty math
in setter
>>>
>>> s1.math = 90
in __set__
>>> s1.math
in __get__
90

对于以上理解 property 的运行原理有困难的同学,请务必参照我上面写的两点说明。如有其他疑


问,可以加微信与我进行探讨。

4. 基于描述符如何实现staticmethod

说完了 property ,这里再来讲讲 @classmethod 和 @staticmethod 的实现原理。


我这里定义了一个类,用了两种方式来实现静态方法。

class Test:
@staticmethod
def myfunc():
print("hello")

##

class Test:
def myfunc():
print("hello")
#
myfunc = staticmethod(myfunc)

这两种写法是等价的,就好像在 property 一样,其实以下两种写法也是等价的。

@TestProperty
def math(self):
return self._math

math = TestProperty(fget=math)

话题还是转回到 staticmethod 这边来吧。

由上面的注释,可以看出 staticmethod 其实就相当于一个描述符类,而 myfunc 在此刻变成了一


个描述符。关于 staticmethod 的实现,你可以参照下面这段我自己写的代码,加以理解。

调用这个方法可以知道,每调用一次,它都会经过描述符类的 __get__ 。

>>> Test.myfunc()
in staticmethod __get__
hello
>>> Test().myfunc()
in staticmethod __get__
hello

5. 基于描述符如何实现classmethod

同样的 classmethod 也是一样。

class classmethod(object):
def __init__(self, f):
self.f = f

def __get__(self, instance, owner=None):


print("in classmethod __get__")

def newfunc(*args):
return self.f(owner, *args)
return newfunc

class Test:
def myfunc(cls):
print("hello")

#
myfunc = classmethod(myfunc)

验证结果如下

>>> Test.myfunc()
in classmethod __get__
hello
>>> Test().myfunc()
in classmethod __get__
hello

讲完了 property 、 staticmethod 和 classmethod 与 描述符的关系。我想你应该对描述符在


Python 中的应用有了更深的理解。对于 super 的实现原理,就交由你来自己完成。

6. 所有实例共享描述符

通过以上内容的学习,你是不是觉得自己已经对描述符足够了解了呢?

可在这里,我想说以上的描述符代码都有问题。

问题在哪里呢?请看下面这个例子。
class Score:
def __init__(self, default=0):
self._value = default

def __get__(self, instance, owner):


return self._value

def __set__(self, instance, value):


if 0 <= value <= 100:
self._value = value
else:
raise ValueError

class Student:
math = Score(0)
chinese = Score(0)
english = Score(0)

def __repr__(self):
return "<Student math:{}, chinese:{}, english:{}>".format(self.math, self.ch
inese, self.english)

Student 里没有像前面那样写了构造函数,但是关键不在这儿,没写只是因为没必要写。

然后来看一下会出现什么样的问题呢

>>> std1 = Student()


>>> std1
<Student math:0, chinese:0, english:0>
>>> std1.math = 85
>>> std1
<Student math:85, chinese:0, english:0>
>>> std2 = Student()
>>> std2 # std2 std1
<Student math:85, chinese:0, english:0>
>>> std2.math = 100
>>> std1 # std2 std1
<Student math:100, chinese:0, english:0>

从结果上来看,std2 居然共享了 std1 的属性值,只要其中一个实例的变量发生改变,另一个实例


的变量也会跟着改变。

探其根因,是由于此时 math,chinese,english 三个全部是类变量,导致 std2 和 std1 在访问


math,chinese,english 这三个变量时,其实都是访问类变量。

问题是不是来了?小明和小强的分数怎么可能是绑定的呢?这很明显与实际业务不符。
使用描述符给我们制造了便利,却无形中给我们带来了麻烦,难道这也是描述符的特性吗?

描述符是个很好用的特性,会出现这个问题,是由于我们之前写的描述符代码都是错误的。

描述符的机制,在我看来,只是抢占了访问顺序,而具体的逻辑却要因地制宜,视情况而定。

如果要把 math,chinese,english 这三个变量变成实例之间相互隔离的属性,应该这么写。

class Score:
def __init__(self, subject):
self.name = subject

def __get__(self, instance, owner):


return instance.__dict__[self.name]

def __set__(self, instance, value):


if 0 <= value <= 100:
instance.__dict__[self.name] = value
else:
raise ValueError

class Student:
math = Score("math")
chinese = Score("chinese")
english = Score("english")

def __init__(self, math, chinese, english):


self.math = math
self.chinese = chinese
self.english = english

def __repr__(self):
return "<Student math:{}, chinese:{}, english:{}>".format(self.math, self.ch
inese, self.english)

引导程序逻辑进入描述符之后,不管你是获取属性,还是设置属性,都是直接作用于 instance 的。
这段代码,你可以仔细和前面的对比一下。

不难看出:

之前的错误代码,更像是把描述符当做了存储节点。
之后的正确代码,则是把描述符直接当做代理,本身不存储值。

以上便是我对描述符的全部分享,希望能对你有所帮助。

4.3 神奇的元类编程

1. 类是如何产生的

类是如何产生?这个问题也许你会觉得很傻。

实则不然,很多初学者只知道使用继承的表面形式来创建一个类,却不知道其内部真正的创建是由
type 来创建的。

type?这不是判断对象类型的函数吗?

是的,type通常用法就是用来判断对象的类型。但除此之外,他最大的用途是用来动态创建类。当
Python扫描到class的语法的时候,就会调用type函数进行类的创建。

2. 如何使用type创建类

首先, type() 需要接收三个参数

1. 类的名称,若不指定,也要传入空字符串: ""
2. 父类,注意以tuple的形式传入,若没有父类也要传入空tuple: () ,默认继承object
3. 绑定的方法或属性,注意以dict的形式传入

来看个例子

##
class BaseClass:
def talk(self):
print("i am people")

##
def say(self):
print("hello")

## type User
User = type("User", (BaseClass, ), {"name":"user", "say":say})

3. 理解什么是元类

什么是类?可能谁都知道,类就是用来创建对象的「模板」。

那什么是元类呢?一句话通俗来说,元类就是创建类的「模板」。

为什么type能用来创建类?因为它本身是一个元类。使用元类创建类,那就合理了。

type是Python在背后用来创建所有类的元类,我们熟知的类的始祖 object 也是由type创建的。更


有甚者,连type自己也是由type自己创建的,这就过份了。

>>> type(type)
<class 'type'>

>>> type(object)
<class 'type'>

>>> type(int)
<class 'type'>

>>> type(str)
<class 'type'>

如果要形象的来理解的话,就看下面这三行话。

str:用来创建字符串对象的类。
int:是用来创建整数对象的类。
type:是用来创建类对象的类。

反过来看
一个实例的类型,是类
一个类的类型,是元类
一个元类的类型,是type

写个简单的小示例来验证下

>>> class MetaPerson(type):


... pass
...
>>> class Person(metaclass=MetaPerson):
... pass
...
>>> Tom = Person()
>>> print(type(Tom))
<class '__main__.Person'>
>>> print(type(Tom.__class__))
<class '__main__.MetaPerson'>
>>> print(type(Tom.__class__.__class__))
<class 'type'>

下面再来看一个稍微完整的

## type
class BaseClass(type):
def __new__(cls, *args, **kwargs):
print("in BaseClass")
return super().__new__(cls, *args, **kwargs)

class User(metaclass=BaseClass):
def __init__(self, name):
print("in User")
self.name = name

## in BaseClass

user = User("wangbm")
## in User

综上,我们知道了类是元类的实例,所以在创建一个普通类时,其实会走元类的 __new__ 。

同时,我们又知道在类里实现了 __call__ 就可以让这个类的实例变成可调用。

所以在我们对普通类进行实例化时,实际是对一个元类的实例(也就是普通类)进行直接调用,所
以会走进元类的 __call__

在这里可以借助 「单例的实现」举一个例子,你就清楚了

class MetaSingleton(type):
def __call__(cls, *args, **kwargs):
print("cls:{}".format(cls.__name__))
print("====1====")
if not hasattr(cls, "_instance"):
print("====2====")
cls._instance = type.__call__(cls, *args, **kwargs)
return cls._instance

class User(metaclass=MetaSingleton):
def __init__(self, *args, **kw):
print("====3====")
for k,v in kw:
setattr(self, k, v)

验证结果

>>> u1 = User('wangbm1')
cls:User
====1====
====2====
====3====
>>> u1.age = 20
>>> u2 = User('wangbm2')
cls:User
====1====
>>> u2.age
20
>>> u1 is u2
True

4. 使用元类的意义

正常情况下,我们都不会使用到元类。但是这并不意味着,它不重要。假如某一天,我们需要写一
个框架,很有可能就需要你对元类要有进一步的研究。

元类有啥用,用我通俗的理解,元类的作用过程:

1. 拦截类的创建
2. 拦截下后,进行修改
3. 修改完后,返回修改后的类

所以,很明显,为什么要用它呢?不要它会怎样?

使用元类,是要对类进行定制修改。使用元类来动态生成元类的实例,而99%的开发人员是不需要
动态修改类的,因为这应该是框架才需要考虑的事。

但是,这样说,你一定不会服气,到底元类用来干什么?其实元类的作用就是 API ,一个最典


型的应用是 Django ORM 。

5. 元类实战:ORM

使用过Django ORM的人都知道,有了ORM,使得我们操作数据库,变得异常简单。

ORM的一个类(User),就对应数据库中的一张表。id,name,email,password 就是字段。

class User(BaseModel):
id = IntField('id')
name = StrField('username')
email = StrField('email')
password = StrField('password')

class Meta:
db_table = "user"

如果我们要插入一条数据,我们只需这样做

##
u = User(id=20180424, name="xiaoming",
email="xiaoming@163.com", password="abc123")

##
u.save()

通常用户层面,只需要懂应用,就像上面这样操作就可以了。

但是今天我并不是来教大家如何使用ORM,我们是用来探究ORM内部究竟是如何实现的。我们也
可以自己写一个简易的ORM。

从上面的 User 类中,我们看到 StrField 和 IntField ,从字段意思上看,我们很容易看出这代表


两个字段类型。字段名分别是 id , username , email , password 。

和 IntField 在这里的用法,叫做
StrField 。
简单来说呢, 可以实现对属性值的类型,范围等一切做约束,意思就是说变量id只能是
int类型,变量name只能是str类型,否则将会抛出异常。

那如何实现这两个 呢?请看代码。

import numbers

class Field:
pass

class IntField(Field):
def __init__(self, name):
self.name = name
self._value = None

def __get__(self, instance, owner):


return self._value

def __set__(self, instance, value):


if not isinstance(value, numbers.Integral):
raise ValueError("int value need")
self._value = value

class StrField(Field):
def __init__(self, name):
self.name = name
self._value = None

def __get__(self, instance, owner):


return self._value

def __set__(self, instance, value):


if not isinstance(value, str):
raise ValueError("string value need")
self._value = value

我们看到 User 类继承自 BaseModel ,这个 BaseModel 里,定义了数据库操作的各种方法,譬如我


们使用的 save 函数,也可以放在这里面的。所以我们就可以来写一下这个 BaseModel 类

class BaseModel(metaclass=ModelMetaClass):
def __init__(self, *args, **kw):
for k,v in kw.items():
# __set__
setattr(self, k, v)
return super().__init__()

def save(self):
db_columns=[]
db_values=[]
for column, value in self.fields.items():
db_columns.append(str(column))
db_values.append(str(getattr(self, column)))
sql = "insert into {table} ({columns}) values({values})".format(
table=self.db_table, columns=','.join(db_columns),
values=','.join(db_values))
pass

从 BaseModel 类中,save函数里面有几个新变量。

1. fields: 存放所有的字段属性
2. db_table:表名

我们思考一下这个 u 实例的创建过程:

type -> ModelMetaClass -> BaseModel -> User -> u

这里会有几个问题。

init的参数是User实例时传入的,所以传入的id是int类型,name是str类型。看起来没啥问题,
若是这样,我上面的数据描述符就失效了,不能起约束作用。所以我们希望init接收到的id是
IntField类型,name是StrField类型。
同时,我们希望这些字段属性,能够自动归类到fields变量中。因为,做为BaseModel,它可不
是专门为User类服务的,它还要兼容各种各样的表。不同的表,表里有不同数量,不同属性的
字段,这些都要能自动类别并归类整理到一起。这是一个ORM框架最基本的。
我们希望对表名有两种选择,一个是User中若指定Meta信息,比如表名,就以此为表名,若未
指定就以类名的小写 做为表名。虽然BaseModel可以直接取到User的db_table属性,但是如果在
数据库业务逻辑中,加入这段复杂的逻辑,显然是很不优雅的。

上面这几个问题,其实都可以通过元类的 __new__ 函数来完成。

下面就来看看,如何用元类来解决这些问题呢?请看代码。

class ModelMetaClass(type):
def __new__(cls, name, bases, attrs):
if name == "BaseModel":
# __new__ BaseModel name="BaseModel"
# __new__ User name="User"
return super().__new__(cls, name, bases, attrs)

#
fields = {k:v for k,v in attrs.items() if isinstance(v, Field)}

# User Meta
# User user
_meta = attrs.get("Meta", None)
db_table = name.lower()
if _meta is not None:
table = getattr(_meta, "db_table", None)
if table is not None:
db_table = table

# User attrs
#
#
attrs["db_table"] = db_table
attrs["fields"] = fields
return super().__new__(cls, name, bases, attrs)
6. __new__ 有什么用?

在没有元类的情况下,每次创建实例,在先进入 __init__ 之前都会先进入 __new__ 。

class User:
def __new__(cls, *args, **kwargs):
print("in BaseClass")
return super().__new__(cls)

def __init__(self, name):


print("in User")
self.name = name

使用如下

>>> u = User('wangbm')
in BaseClass
in User
>>> u.name
'wangbm'

在有元类的情况下,每次创建类时,会都先进入 元类的 __new__ 方法,如果你要对类进行定制,


可以在这时做一些手脚。

综上,元类的 __new__ 和普通类的不一样:

元类的 __new__ 在创建类时就会进入,它可以获取到上层类的一切属性和方法,包括类名,魔


法方法。
而普通类的 __new__ 在实例化时就会进入,它仅能获取到实例化时外界传入的属性。

附录:参考文章

Python Cookbook - 元编程


深刻理解Python中的元类

第五章:魔法开发技巧

5.1 嵌套上下文管理的另类写法

当我们要写一个嵌套的上下文管理器时,可能会这样写
import contextlib

@contextlib.contextmanager
def test_context(name):
print('enter, my name is {}'.format(name))

yield

print('exit, my name is {}'.format(name))

with test_context('aaa'):
with test_context('bbb'):
print('========== in main ============')

输出结果如下

enter, my name is aaa


enter, my name is bbb
========== in main ============
exit, my name is bbb
exit, my name is aaa

除此之外,你可知道,还有另一种嵌套写法

with test_context('aaa'), test_context('bbb'):


print('========== in main ============')

5.2 将嵌套 for 循环写成单行

我们经常会如下这种嵌套的 for 循环代码

list1 = range(1,3)
list2 = range(4,6)
list3 = range(7,9)
for item1 in list1:
for item2 in list2:
for item3 in list3:
print(item1+item2+item3)

这里仅仅是三个 for 循环,在实际编码中,有可能会有更层。

这样的代码,可读性非常的差,很多人不想这么写,可又没有更好的写法。

这里介绍一种我常用的写法,使用 itertools 这个库来实现更优雅易读的代码。


from itertools import product
list1 = range(1,3)
list2 = range(4,6)
list3 = range(7,9)
for item1,item2,item3 in product(list1, list2, list3):
print(item1+item2+item3)

输出如下

$ python demo.py
12
13
13
14
13
14
14
15

5.3 单行实现 for 死循环如何写?

如果让你在不借助 while ,只使用 for 来写一个死循环?

你会写吗?

如果你还说简单,你可以自己试一下。

...

如果你尝试后,仍然写不出来,那我给出自己的做法。

for i in iter(int, 1):pass

是不是傻了?iter 还有这种用法?这为啥是个死循环?

关于这个问题,你如果看中文网站,可能找不到相关资料。

还好你可以通过 IDE 看py源码里的注释内容,介绍了很详细的使用方法。

原来iter有两种使用方法。

通常我们的认知是第一种,将一个列表转化为一个迭代器。

而第二种方法,他接收一个 callable对象,和一个sentinel 参数。第一个对象会一直运行,直到


它返回 sentinel 值才结束。
那 int 呢?

这又是一个知识点,int 是一个内建方法。通过看注释,可以看出它是有默认值0的。你可以在
console 模式下输入 int() 看看是不是返回0。

由于int() 永远返回0,永远返回不了1,所以这个 for 循环会没有终点。一直运行下去。

5.4 如何关闭异常自动关联上下文?

当你在处理异常时,由于处理不当或者其他问题,再次抛出另一个异常时,往外抛出的异常也会携
带原始的异常信息。

就像这样子。

try:
print(1 / 0)
except Exception as exc:
raise RuntimeError("Something bad happened")

从输出可以看到两个异常信息

Traceback (most recent call last):


File "demo.py", line 2, in <module>
print(1 / 0)
ZeroDivisionError: division by zero

During handling of the above exception, another exception occurred:

Traceback (most recent call last):


File "demo.py", line 4, in <module>
raise RuntimeError("Something bad happened")
RuntimeError: Something bad happened

如果在异常处理程序或 finally 块中引发异常,默认情况下,异常机制会隐式工作会将先前的异常


附加为新异常的 __context__ 属性。这就是 Python 默认开启的自动关联异常上下文。

如果你想自己控制这个上下文,可以加个 from 关键字( from 语法会有个限制,就是第二个表达


式必须是另一个异常类或实例。),来表明你的新异常是直接由哪个异常引起的。

try:
print(1 / 0)
except Exception as exc:
raise RuntimeError("Something bad happened") from exc

输出如下
Traceback (most recent call last):
File "demo.py", line 2, in <module>
print(1 / 0)
ZeroDivisionError: division by zero

The above exception was the direct cause of the following exception:

Traceback (most recent call last):


File "demo.py", line 4, in <module>
raise RuntimeError("Something bad happened") from exc
RuntimeError: Something bad happened

当然,你也可以通过 with_traceback() 方法为异常设置上下文 __context__ 属性,这也能在


traceback 更好的显示异常信息。

try:
print(1 / 0)
except Exception as exc:
raise RuntimeError("bad thing").with_traceback(exc)

最后,如果我想彻底关闭这个自动关联异常上下文的机制?有什么办法呢?

可以使用 raise...from None ,从下面的例子上看,已经没有了原始异常

$ cat demo.py
try:
print(1 / 0)
except Exception as exc:
raise RuntimeError("Something bad happened") from None
$
$ python demo.py
Traceback (most recent call last):
File "demo.py", line 4, in <module>
raise RuntimeError("Something bad happened") from None
RuntimeError: Something bad happened
(PythonCodingTime)
5.5 自带的缓存机制不用白不用

缓存是一种将定量数据加以保存,以备迎合后续获取需求的处理方式,旨在加快数据获取的速度。

数据的生成过程可能需要经过计算,规整,远程获取等操作,如果是同一份数据需要多次使用,每
次都重新生成会大大浪费时间。所以,如果将计算或者远程请求等操作获得的数据缓存下来,会加
快后续的数据获取需求。

为了实现这个需求,Python 3.2 + 中给我们提供了一个机制,可以很方便的实现,而不需要你去写


这样的逻辑代码。

这个机制实现于 functool 模块中的 lru_cache 装饰器。

@functools.lru_cache(maxsize=None, typed=False)

参数解读:

maxsize:最多可以缓存多少个此函数的调用结果,如果为None,则无限制,设置为 2 的幂
时,性能最佳
typed:若为 True,则不同参数类型的调用将分别缓存。

举个例子

from functools import lru_cache

@lru_cache(None)
def add(x, y):
print("calculating: %s + %s" % (x, y))
return x + y
print(add(1, 2))
print(add(1, 2))
print(add(2, 3))

输出如下,可以看到第二次调用并没有真正的执行函数体,而是直接返回缓存里的结果

calculating: 1 + 2
3
3
calculating: 2 + 3
5

5.6 如何流式读取数G超大文件

使用 with...open... 可以从一个文件中读取数据,这是所有 Python 开发者都非常熟悉的操作。

但是如果你使用不当,也会带来很大的麻烦。

比如当你使用了 read 函数,其实 Python 会将文件的内容一次性的全部载入内存中,如果文件有


10 个G甚至更多,那么你的电脑就要消耗的内存非常巨大。

##
with open("big_file.txt", "r") as fp:
content = fp.read()

对于这个问题,你也许会想到使用 readline 去做一个生成器来逐行返回。

def read_from_file(filename):
with open(filename, "r") as fp:
yield fp.readline()

可如果这个文件内容就一行呢,一行就 10个G,其实你还是会一次性读取全部内容。

最优雅的解决方法是,在使用 read 方法时,指定每次只读取固定大小的内容,比如下面的代码


中,每次只读取 8kb 返回。

def read_from_file(filename, block_size = 1024 * 8):


with open(filename, "r") as fp:
while True:
chunk = fp.read(block_size)
if not chunk:
break

yield chunk
上面的代码,功能上已经没有问题了,但是代码看起来代码还是有些臃肿。

借助偏函数 和 iter 函数可以优化一下代码

from functools import partial

def read_from_file(filename, block_size = 1024 * 8):


with open(filename, "r") as fp:
for chunk in iter(partial(fp.read, block_size), ""):
yield chunk

如果你使用的是 Python 3.8 +,还有一种更直观、易于理解的写法,既不用使用偏函数,也不用掌


握 iter 这种另类的用法。而只要用利用 海象运算符就可以,具体代码如下

def read_from_file(filename, block_size = 1024 * 8):


with open(filename, "r") as fp:
while chunk := fp.read(block_size):
yield chunk

5.7 实现类似 defer 的延迟调用

在 Golang 中有一种延迟调用的机制,关键字是 defer,例如下面的示例

import "fmt"

func myfunc() {
fmt.Println("B")
}

func main() {
defer myfunc()
fmt.Println("A")
}

输出如下,myfunc 的调用会在函数返回前一步完成,即使你将 myfunc 的调用写在函数的第一行,


这就是延迟调用。

A
B

那么在 Python 中否有这种机制呢?

当然也有,只不过并没有 Golang 这种简便。

在 Python 可以使用 上下文管理器 达到这种效果


import contextlib

def callback():
print('B')

with contextlib.ExitStack() as stack:


stack.callback(callback)
print('A')

输出如下

A
B

5.8 如何快速计算函数运行时间

计算一个函数的运行时间,你可能会这样子做

import time

start = time.time()

## run the function

end = time.time()
print(end-start)

你看看你为了计算函数运行时间,写了几行代码了。

有没有一种方法可以更方便的计算这个运行时间呢?

有。

有一个内置模块叫 timeit

使用它,只用一行代码即可

import time
import timeit

def run_sleep(second):
print(second)
time.sleep(second)
##
print(timeit.timeit(lambda :run_sleep(2), number=5))

运行结果如下

2
2
2
2
2
10.020059824

5.9 重定向标准输出到日志

假设你有一个脚本,会执行一些任务,比如说集群健康情况的检查。

检查完成后,会把各服务的的健康状况以 JSON 字符串的形式打印到标准输出。

如果代码有问题,导致异常处理不足,最终检查失败,是很有可能将一些错误异常栈输出到标准错
误或标准输出上。

由于最初约定的脚本返回方式是以 JSON 的格式输出,此时你的脚本却输出各种错误异常,异常调


用方也无法解析。

如何避免这种情况的发生呢?

我们可以这样做,把你的标准错误输出到日志文件中。

import contextlib

log_file="/var/log/you.log"

def you_task():
pass

@contextlib.contextmanager
def close_stdout():
raw_stdout = sys.stdout
file = open(log_file, 'a+')
sys.stdout = file

yield

sys.stdout = raw_stdout
file.close()
with close_stdout():
you_task()

5.10 快速定位错误进入调试模式

当你在写一个程序时,最初的程序一定遇到不少零零散散的错误,这时候就免不了调试一波。

如果你和我一样,习惯使用 pdb 进行调试的话,一定有所体会,通常我们都要先把


pdb.set_trace() 去掉,让程序畅通无阻,直到它把异常抛出来。

出现异常后,再使用 vim 跳转到抛出异常的位置,敲入 import pdb;pdb.set_trace() ,然后再到


运行,进入调试模式,找到问题并修改代码后再去掉我们加上的那行 pdb 的代码。

如此反复这样一个过程,直到最后程序没有异常。

你应该能够感受到这个过程有多繁锁,令人崩溃。

接下来介绍一种,可以让你不需要修改源代码,就可以在异常抛出时,快速切换到调试模式,进入
『案发现场』排查问题。

方法很简单,只需要你在执行脚本时,加入 -i 参考
如果你的程序没有任何问题,加上 -i 后又会有什么不一样呢?

从下图可以看出,程序执行完成后会自动进入 console 交互模式。

5.11 在程序退出前执行代码的技巧

使用 atexit 这个内置模块,可以很方便的注册退出函数。

不管你在哪个地方导致程序崩溃,都会执行那些你注册过的函数。
示例如下

如果 clean() 函数有参数,那么你可以不用装饰器,而是直接调用
atexit.register(clean_1, 1, 2, 3='xxx') 。

可能你有其他方法可以处理这种需求,但肯定比上不使用 atexit 来得优雅,来得方便,并且它很容


易扩展。

但是使用 atexit 仍然有一些局限性,比如:

如果程序是被你没有处理过的系统信号杀死的,那么注册的函数无法正常执行。
如果发生了严重的 Python 内部错误,你注册的函数无法正常执行。
如果你手动调用了 os._exit() ,你注册的函数无法正常执行。

5.12 逗号也有它的独特用法

逗号,虽然是个很不起眼的符号,但在 Python 中也有他的用武之地。

第一个用法

元组的转化

[root@localhost ~]# cat demo.py


def func():
return "ok",

print(func())
[root@localhost ~]# python3 demo.py
('ok',)

第二个用法r
print 的取消换行

[root@localhost ~]# cat demo.py


for i in range(3):
print i
[root@localhost ~]#
[root@localhost ~]# python demo.py
0
1
2
[root@localhost ~]#
[root@localhost ~]# vim demo.py
[root@localhost ~]#
[root@localhost ~]# cat demo.py
for i in range(3):
print i,
[root@localhost ~]#
[root@localhost ~]# python demo.py
0 1 2
[root@localhost ~]#

5.13 如何在运行状态查看源代码?

查看函数的源代码,我们通常会使用 IDE 来完成。

比如在 PyCharm 中,你可以 Ctrl + 鼠标点击 进入函数的源代码。

那如果没有 IDE 呢?

当我们想使用一个函数时,如何知道这个函数需要接收哪些参数呢?

当我们在使用函数时出现问题的时候,如何通过阅读源代码来排查问题所在呢?

这时候,我们可以使用 inspect 来代替 IDE 帮助你完成这些事

## demo.py
import inspect

def add(x, y):


return x + y

print("===================")
print(inspect.getsource(add))

运行结果如下
$ python demo.py
===================
def add(x, y):
return x + y

5.14 单分派泛函数如何写?

泛型,如果你尝过java,应该对他不陌生吧。但你可能不知道在 Python 中(3.4+ ),也可以实现


简单的泛型函数。

在Python中只能实现基于单个(第一个)参数的数据类型来选择具体的实现方式,官方名称 是
single-dispatch 。你或许听不懂,说人话,就是可以实现第一个参数的数据类型不同,其调用的
函数也就不同。

singledispatch 是 PEP443 中引入的,如果你对此有兴趣,PEP443 应该是最好的学习文档:http


s://www.python.org/dev/peps/pep-0443/

它使用方法极其简单,只要被 singledispatch 装饰的函数,就是一个 single-dispatch 的泛函数


( generic functions )。

单分派:根据一个参数的类型,以不同方式执行相同的操作的行为。
多分派:可根据多个参数的类型选择专门的函数的行为。
泛函数:多个函数绑在一起组合成一个泛函数。

这边举个简单的例子。

from functools import singledispatch

@singledispatch
def age(obj):
print(' ')

@age.register(int)
def _(age):
print(' {} '.format(age))

@age.register(str)
def _(age):
print('I am {} years old.'.format(age))

age(23) # int
age('twenty three') # str
age(['23']) # list
执行结果

23
I am twenty three years old.

说起泛型,其实在 Python 本身的一些内建函数中并不少见,比如 len() , iter() ,


copy.copy() , pprint() 等

你可能会问,它有什么用呢?实际上真没什么用,你不用它或者不认识它也完全不影响你编码。

我这里举个例子,你可以感受一下。

大家都知道,Python 中有许许多的数据类型,比如 str,list, dict, tuple 等,不同数据类型的拼


接方式各不相同,所以我这里我写了一个通用的函数,可以根据对应的数据类型对选择对应的拼接
方式拼接,而且不同数据类型我还应该提示无法拼接。以下是简单的实现。

def check_type(func):
def wrapper(*args):
arg1, arg2 = args[:2]
if type(arg1) != type(arg2):
return ' !!'
return func(*args)
return wrapper

@singledispatch
def add(obj, new_obj):
raise TypeError

@add.register(str)
@check_type
def _(obj, new_obj):
obj += new_obj
return obj

@add.register(list)
@check_type
def _(obj, new_obj):
obj.extend(new_obj)
return obj

@add.register(dict)
@check_type
def _(obj, new_obj):
obj.update(new_obj)
return obj
@add.register(tuple)
@check_type
def _(obj, new_obj):
return (*obj, *new_obj)

print(add('hello',', world'))
print(add([1,2,3], [4,5,6]))
print(add({'name': 'wangbm'}, {'age':25}))
print(add(('apple', 'huawei'), ('vivo', 'oppo')))

## list
print(add([1,2,3], '4,5,6'))

输出结果如下

hello, world
[1, 2, 3, 4, 5, 6]
{'name': 'wangbm', 'age': 25}
('apple', 'huawei', 'vivo', 'oppo')
!!

如果不使用singledispatch 的话,你可能会写出这样的代码。

def check_type(func):
def wrapper(*args):
arg1, arg2 = args[:2]
if type(arg1) != type(arg2):
return ' !!'
return func(*args)
return wrapper

@check_type
def add(obj, new_obj):
if isinstance(obj, str) :
obj += new_obj
return obj

if isinstance(obj, list) :
obj.extend(new_obj)
return obj

if isinstance(obj, dict) :
obj.update(new_obj)
return obj

if isinstance(obj, tuple) :
return (*obj, *new_obj)

print(add('hello',', world'))
print(add([1,2,3], [4,5,6]))
print(add({'name': 'wangbm'}, {'age':25}))
print(add(('apple', 'huawei'), ('vivo', 'oppo')))

## list
print(add([1,2,3], '4,5,6'))

输出如下

hello, world
[1, 2, 3, 4, 5, 6]
{'name': 'wangbm', 'age': 25}
('apple', 'huawei', 'vivo', 'oppo')
!!

5.15 让我爱不释手的用户环境

当你在机器上并没有 root 权限时,如何安装 Python 的第三方包呢?

可以使用 pip install --user pkg 将你的包安装在你的用户环境中,该用户环境与全局环境并不


冲突,并且多用户之间相互隔离,互不影响。

## requests
[root@localhost ~]$ pip list | grep requests
[root@localhost ~]$ su - wangbm

##
[wangbm@localhost ~]$ pip list | grep requests
[wangbm@localhost ~]$ pip install --user requests
[wangbm@localhost ~]$ pip list | grep requests
requests (2.22.0)
[wangbm@localhost ~]$

## Location requests
[wangbm@localhost ~]$ pip show requests
---
Metadata-Version: 2.1
Name: requests
Version: 2.22.0
Summary: Python HTTP for Humans.
Home-page: http://python-requests.org
Author: Kenneth Reitz
Author-email: me@kennethreitz.org
Installer: pip
License: Apache 2.0
Location: /home/wangbm/.local/lib/python2.7/site-packages
[wangbm@localhost ~]$ exit
logout

## wangbm root requests


[root@localhost ~]$ pip list | grep requests
[root@localhost ~]$

5.16 字符串的分割技巧

当我们对字符串进行分割时,且分割符是 \n ,有可能会出现这样一个窘境:

>>> str = "a\nb\n"


>>> print(str)
a
b

>>> str.split('\n')
['a', 'b', '']
>>>

会在最后一行多出一个元素,为了应对这种情况,你可以会多加一步处理。

但我想说的是,完成没有必要,对于这个场景,你可以使用 splitlines

>>> str.splitlines()
['a', 'b']

5.17 反转字符串/列表最优雅的方式
反转序列并不难,但是如何做到最优雅呢?

先来看看,正常是如何反转的。

最简单的方法是使用列表自带的reverse()方法。

>>> ml = [1,2,3,4,5]
>>> ml.reverse()
>>> ml
[5, 4, 3, 2, 1]

但如果你要处理的是字符串,reverse就无能为力了。你可以尝试将其转化成list,再reverse,然后
再转化成str。转来转去,也太麻烦了吧?需要这么多行代码(后面三行是不能合并成一行的),一
点都不Pythonic。

mstr1 = 'abc'
ml1 = list(mstr1)
ml1.reverse()
mstr2 = str(ml1)

对于字符串还有一种稍微复杂一点的,是自定义递归函数来实现。

def my_reverse(str):
if str == "":
return str
else:
return my_reverse(str[1:]) + str[0]

在这里,介绍一种最优雅的反转方式,使用切片,不管你是字符串,还是列表,简直通杀。

>>> mstr = 'abc'


>>> ml = [1,2,3]
>>> mstr[::-1]
'cba'
>>> ml[::-1]
[3, 2, 1]

5.18 如何将 print 内容输出到文件

Python 3 中的 print 作为一个函数,由于可以接收更多的参数,所以功能变为更加强大。

比如今天要说的使用 print 将你要打印的内容,输出到日志文件中(但是我并不推荐使用它)。

>>> with open('test.log', mode='w') as f:


... print('hello, python', file=f, flush=True)
>>> exit()

$ cat test.log
hello, python

5.19 改变默认递归次数限制

上面才提到递归,大家都知道使用递归是有风险的,递归深度过深容易导致堆栈的溢出。如果你这
字符串太长啦,使用递归方式反转,就会出现问题。

那到底,默认递归次数限制是多少呢?

>>> import sys


>>> sys.getrecursionlimit()
1000

可以查,当然也可以自定义修改次数,退出即失效。

>>> sys.setrecursionlimit(2000)
>>> sys.getrecursionlimit()
2000

5.20 让你晕头转向的 else 用法

if else 用法可以说最基础的语法表达式之一,但是今天不是讲这个的。

if else 早已烂大街,但我相信仍然有很多人都不曾见过 for else 和 try else 的用法。为什么说它曾


让我晕头转向,因为它不像 if else 那么直白,非黑即白,脑子经常要想一下才能才反应过来代码怎
么走。

先来说说,for ... else ...

def check_item(source_list, target):


for item in source_list:
if item == target:
print("Exists!")
break

else:
print("Does not exist")

在往下看之前,你可以思考一下,什么情况下才会走 else。是循环被 break,还是没有break?

给几个例子,你体会一下。

check_item(["apple", "huawei", "oppo"], "oppo")


## Exists!

check_item(["apple", "huawei", "oppo"], "vivo")


## Does not exist

可以看出,没有被 break 的程序才会正常走else流程。

再来看看,try else 用法。

def test_try_else(attr1 = None):


try:
if attr1:
pass
else:
raise
except:
print("Exception occurred...")
else:
print("No Exception occurred...")

同样来几个例子。当不传参数时,就抛出异常。

test_try_else()
## Exception occurred...

test_try_else("ming")
## No Exception occurred...
可以看出,没有 try 里面的代码块没有抛出异常的,会正常走else。

总结一下,for else 和 try else 相同,只要代码正常走下去不被 break,不抛出异常,就可以走


else。

5.21 字典访问不存在的key时不再报错

当一个字典里没有某个 key 时,此时你访问他是会报 KeyError 的。

>>> profile={}
>>> profile["age"]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 'age'

这里有一个小技巧,使用 collections 的 defaultdict 方法,可以帮你处理这个小问题,当你访问一


个不存在的 key 时,会返回默认值。

defaultdict 接收一个工厂方法,工厂方法返回的对象就是字典的默认值。

常用的工厂方法有,我们常见的 int,str,bool 等

>>> a=int()
>>> a
0
>>>
>>> b=str()
>>> b
''
>>>
>>> c=bool()
>>> c
False

因为 defaultdict 可以这样子用。

>>> import collections


>>> profile=collections.defaultdict(int)
>>> profile
defaultdict(<class 'int'>, {})
>>> profile["age"]
0
>>> profile=collections.defaultdict(str)
>>> profile
defaultdict(<class 'str'>, {})
>>> profile["name"]
''

当然既然是工厂方法,你也可以使用 lambda 匿名函数来实现自定义的效果,比如我们使用 str 就


会设置一个空字符串,但这并不是我想要的,我想要的是设置一个其他字符串,你就可以像下面这
样子。

>>> info=collections.defaultdict(lambda: "default value")


>>> info
defaultdict(<function <lambda> at 0x10ff10488>, {})
>>>
>>> info["msg"]
'default value'

5.22 如何实现函数的连续调用?

现在我想写一个函数可以实现把所有的数进行求和,并且可以达到反复调用的目的。

比如这样子。

>>> add(2)(3)(4)(5)(6)(7)
27

当只调用一次时,也必须适用。

>>> add(2)
2

每次调用的返回结果都是一个 int 类型的实例,要实现将一个实例看做一个函数一样调用,那就不


得不使用到 __call__ 这个魔法方法。

>>> class AddInt(int):


... def __call__(self, x):
... print("calling __call__ function")
... return AddInt(self.numerator + x)
...
>>>
>>> age = AddInt(18)
>>> age
18
>>> age(1)
calling __call__ function
19
有了上面的铺垫,可以再 AddInt 外层再加一层封装即可。

>>> def add(x):


... class AddInt(int):
... def __call__(self, x):
... return AddInt(self.numerator + x)
... return AddInt(x)
...
>>> add(2)
2
>>> add(2)(3)(4)(5)(6)(7)
27
>>>

5.23 如何实现字典的多级排序

在一个列表中,每个元素都是一个字典,里面的每个字典结构都是一样的。

里面的每个字典会有多个键值对,根据某个 key 或 value 的值大小,对该列表进行排序,使用 sort


函数就可以轻松实现。

>>> students = [{'name': 'Jack', 'age': 17, 'score': 89}, {'name': 'Julia', 'age': 1
7, 'score': 80}, {'name': 'Tom', 'age': 16, 'score': 80}]
>>> students.sort(key=lambda student: student['score'])
>>> students
[{'age': 17, 'score': 80, 'name': 'Julia'}, {'age': 16, 'score': 80, 'name': 'Tom'},
{'age': 17, 'score': 89, 'name': 'Jack'}]

如果两名同学的成绩一样,那谁排在前面呢?

那就再额外再定个第二指标呗,成绩一样,就再看年龄,年龄小的,成绩还能一样,那不是更历害
嘛。

规则定下了:先按成绩降序,如果成绩一致,再按年龄降序。

问题来了,这样的规则,代码该如何实现呢?

用字典本身的 sort 函数也能实现,方法如下:

>>> students = [{'name': 'Jack', 'age': 17, 'score': 89}, {'name': 'Julia', 'age': 1
7, 'score': 80}, {'name': 'Tom', 'age': 16, 'score': 80}]
>>> students.sort(key=lambda student: (student['score'], student['age']))
>>> students
[{'age': 16, 'score': 80, 'name': 'Tom'}, {'age': 17, 'score': 80, 'name': 'Julia'},
{'age': 17, 'score': 89, 'name': 'Jack'}]
那如果一个降序,而另一个是升序,那又该怎么写呢?

很简单,只要在对应的 key 上,前面加一个负号,就会把顺序给颠倒过来。

还是以上面为例,我现在要实现先按成绩降序,如果成绩一致,再按年龄升序。可以这样写

>>> students = [{'name': 'Jack', 'age': 17, 'score': 89}, {'name': 'Julia', 'age': 1
7, 'score': 80}, {'name': 'Tom', 'age': 16, 'score': 80}]
>>> students.sort(key=lambda student: (student['score'], -student['age']))
>>> students
[{'age': 17, 'score': 80, 'name': 'Julia'}, {'age': 16, 'score': 80, 'name': 'Tom'},
{'age': 17, 'score': 89, 'name': 'Jack'}]

5.24 对齐字符串的两种方法

第一种:使用 format

左对齐

>>> "{:<10}".format("a")
'a '
>>>

右对齐

>>> "{:>10}".format("a")
' a'
>>>

居中

>>> "{:^10}".format("a")
' a '
>>>

当你不指定 < 、 > 、 ^ 时,默认就是左对齐

>>> "{:10}".format("a")
'a '
>>>

有了上面的铺垫,写一个整齐的 1-10 的平方、立方表就很容易了。


>>> for x in range(1, 11):
... print('{:2d} {:3d} {:4d}'.format(x, x*x, x*x*x))
...
1 1 1
2 4 8
3 9 27
4 16 64
5 25 125
6 36 216
7 49 343
8 64 512
9 81 729
10 100 1000

对齐的思想其实就是在不足的位自动给你补上空格。

如果不想使用空格,可以指定你想要的字符进行填充,比如下面我用 0 来补全。

>>> for x in range(1, 11):


... print('{:02d} {:03d} {:04d}'.format(x, x*x, x*x*x))
...
01 001 0001
02 004 0008
03 009 0027
04 016 0064
05 025 0125
06 036 0216
07 049 0343
08 064 0512
09 081 0729
10 100 1000

第二种:使用 ljust, rjust

左对齐

>>> "a".ljust(10)
'a '
>>>

右对齐

>>> "a".rjust(10)
' a'
>>>
居中

>>> "a".center(10)
' a '
>>>

同样写一个整齐的 1-10 的平方、立方表

>>> for x in range(1, 11):


... print(' '.join([str(x).ljust(2), str(x * x).ljust(3), str(x * x * x).ljust(4
)]))
...
1 1 1
2 4 8
3 9 27
4 16 64
5 25 125
6 36 216
7 49 343
8 64 512
9 81 729
10 100 1000

如果不想使用空格,而改用 0 来补齐呢?可以这样

>>> for x in range(1, 11):


... print(' '.join([str(x).rjust(2, "0"), str(x*x).rjust(3, "0"), str(x*x*x).rju
st(4, "0")]))
...
01 001 0001
02 004 0008
03 009 0027
04 016 0064
05 025 0125
06 036 0216
07 049 0343
08 064 0512
09 081 0729
10 100 1000
5.25 将位置参数变成关键字参数

在 Python 中,参数的种类,大概可以分为四种:

1. ,也叫 ,调用函数时一定指定的参数,并且在传参的时候必须按函数定义时的
顺序来
2. ,也叫 ,调用函数时,可以指定也可以不指定,不指定就默认的参数值来。
3. ,就是参数个数可变,可以是 0 个或者任意个,但是传参时不能指定参数名,通常使
用 *args 来表示。
4. ,就是参数个数可变,可以是 0 个或者任意个,但是传参时必须指定参数名,通常
使用 **kw 来表示

使用单独的 * ,可以后面的位置参数变成关键字参数,关键字参数在你传参时,必须要写参数
名,不然会报错。

>>> def demo_func(a, b, *, c):


... print(a)
... print(b)
... print(c)
...
>>>
>>> demo_func(1, 2, 3)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: demo_func() takes 2 positional arguments but 3 were given
>>>
>>> demo_func(1, 2, c=3)
1
2
3
5.26 如何获取一个函数设定的参数

在 Python 中有一个叫 inspect 的库,非常的好用,利用它可以获取一些数据,这在写一些框架时


非常有用。

比如有下面这样一个函数

def demo(name, age, gender="male", *args, **kw):


pass

使用 inspect 可以直接获取

>>> from inspect import signature


>>>
>>> sig = signature(demo) # #
>>> sig
<Signature (name, age, gender='male', *args, **kw)>

利用 inspect 还可以检查传参是否匹配签名

>>> sig.bind(" ", 27)


<BoundArguments (name=' ', age=27)>
>>>
>>> sig.bind(" ")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/usr/lib64/python3.6/inspect.py", line 2997, in bind
return args[0]._bind(args[1:], kwargs)
File "/usr/lib64/python3.6/inspect.py", line 2912, in _bind
raise TypeError(msg) from None
TypeError: missing a required argument: 'age'

5.27 如何进行版本的比较

使用 distutils

是 Python 的内置模块,它做为最古老的 python 分发工具,本身也实现了版本的比较


distutils
与检查的功能。

>>> from distutils.version import LooseVersion, StrictVersion


>>> LooseVersion("2.3.1") < LooseVersion("10.1.2")
True
>>> StrictVersion("2.3.1") < StrictVersion("10.1.2")
True

使用 packaging

如果你的环境中安装过 setuptools ,那么一定会附带安装了 packaging 这个包,而如果你的环境


中并没有 setuptools ,也可以通过 pip 来快速安装

$ python3 -m pip install packaging

在 packaging 中有一个 version 模块,专门用来为 setuptools 提供包版本的版本解析。

>>> from packaging import version


>>> version.parse("2.3.1") < version.parse("10.1.2")
True
>>> version.parse("1.3.a4") < version.parse("10.1.2")
True

5.28 如何捕获警告?(注意不是捕获异常)

1. 警告不是异常

你是不是经常在使用一些系统库或者第三方模块的时候,会出现一些既不是异常也不是错误的警告
信息?

这些警告信息,有时候非常多,对于新手容易造成一些误判,以为是程序出错了。

实则不然,异常和错误,都是程序出现了一些问题,但是警告不同,他的紧急程度非常之低,以致
于大多数的警告都是可以直接忽略的。

如果不想显示这些告警信息,可以直接加上参数 -W ignore 参数,就不会再显示了。

2. 警告能捕获吗

能捕获的只有错误异常,但是通过一系列的操作后,你可以将这些警告转化为异常。

这样一来,你就可以像异常一样去捕获他们了。

在不进行任何设置的情况下,警告会直接打印在终端上。
3. 捕获警告方法一

在 warnings 中有一系列的过滤器。

值 处置

"default" 为发出警告的每个位置(模块+行号)打印第一个匹配警告

"error" 将匹配警告转换为异常

"ignore" 从不打印匹配的警告

"always" 总是打印匹配的警告

"module" 为发出警告的每个模块打印第一次匹配警告(无论行号如何)

"once" 无论位置如何,仅打印第一次出现的匹配警告

当你指定为 error 的时候,就会将匹配警告转换为异常。

之后你就可以通过异常的方式去捕获警告了。

import warnings
warnings.filterwarnings('error')

try:
warnings.warn("deprecated", DeprecationWarning)
except Warning as e:
print(e)

运行后,效果如下
4. 捕获警告方法二

如果你不想对在代码中去配置将警告转成异常。

import warnings

try:
warnings.warn("deprecated", DeprecationWarning)
except Warning as e:
print(e)

可以在执行的时候,只要加上一个参数 -W error ,就可以实现一样的效果

$ python3 -W error demo.py


deprecated

5. 捕获警告方法三

除了上面的方法之外 ,warnings 还自带了个捕获警告的上下文管理器。

当你加上 record=True 它会返回一个列表,列表里存放的是所有捕获到的警告,我将它赋值为 w


,然后就可以将它打印出来了。

import warnings

def do_warning():
warnings.warn("deprecated", DeprecationWarning)

with warnings.catch_warnings(record=True) as w:
do_warning()
if len(w) >0:
print(w[0].message)

运行后,效果如下

## 5.29 如何禁止对象深拷贝?

当你使用 copy 模块的 deepcopy 拷贝一个对象后,会创建出来一个全新的的对象。

>>> from copy import deepcopy


>>>
>>> profile = {"name": "wangbm"}
>>> id(profile)
21203408
>>>
>>> new_profile = deepcopy(profile)
>>> id(new_profile)
21236144

但是有的时候,我们希望基于我们的类实例化后对象,禁止被深拷贝,这时候就要用到 Python 的
魔法方法了。

在如下代码中,我们重写了 Sentinel 类的 __deepcopy__ 和 __copy__ 方法

class Sentinel(object):
def __deepcopy__(self, memo):
# Always return the same object because this is essentially a constant.
return self
def __copy__(self):
# called via copy.copy(x)
return self

此时你如果对它进行深度拷贝的话,会发现返回的永远都是原来的对象

>>> obj = Sentinel()


>>> id(obj)
140151569169808
>>>
>>> new_obj = deepcopy(obj)
>>> id(new_obj)
140151569169808

5.30 如何将变量名和变量值转为字典?

千言万语,不如上示例演示下效果

>>> name="wangbm"
>>> age=28
>>> gender="male"
>>>
>>> convert_vars_to_dict(name, age, gender)
{'name': 'wangbm', 'age': 28, 'gender': 'male'}

convert_vars_to_dict 是我要自己定义的这么一个函数,功能如上,代码如下。

import re
import inspect
def varname(*args):
current_frame = inspect.currentframe()
back_frame = current_frame.f_back
back_frame_info = inspect.getframeinfo(back_frame)

current_func_name = current_frame.f_code.co_name

caller_file_path = back_frame_info[0]
caller_line_no = back_frame_info[1]
caller_type = back_frame_info[2]
caller_expression = back_frame_info[3]

keys = []

for line in caller_expression:


re_match = re.search(r'\b{}\((.*?)\)'.format(current_func_name), line)
match_string = re_match.groups(1)[0]
keys = [match.strip() for match in match_string.split(',') if match]

return dict(zip(keys, args))

附上 :inspect 学习文档

5.31 替换实例方法的最佳实践

思路一:简单替换

当你想对类实例的方法进行替换时,你可能想到的是直接对他进行粗暴地替换

class People:
def speak(self):
print("hello, world")

def speak(self):
print("hello, python")

p = People()
p.speak = speak
p.speak()

但当你试着执行这段代码的时候,就会发现行不通,它提示我们要传入 self 参数

Traceback (most recent call last):


File "/Users/MING/Code/Python/demo.py", line 12, in <module>
p.speak()
TypeError: speak() missing 1 required positional argument: 'self'

不对啊~ self 不是实例本身吗?函数不是一直就这么写的?

实际上你这么替换,speak 就变成了一个 function,而不是一个和实例绑定的 method ,你可以把


替换前后的 speak 打印出来

p = People()
print(p.speak)
p.speak = speak
print(p.speak)

输出结果如下,区别非常明显

<bound method People.speak of <__main__.People object at 0x10cfa7fd0>>


<function speak at 0x10ca10040>

这种方法,只能用在替换不与实例绑定的静态方法上,不然你每次调用的时候,就得手动传入实例
本身,但这样调用就会变得非常怪异。

思路二:利用 im_func

有 Python 2 使用经验的朋友,可以会知道类实例的方法,都有 im_func 和 im_class 属性,分别


指向了该方法的函数和类。

很抱歉的是,这些在 Python3 中全都取消了,意味你无法再使用 im_func 和 im_class 。

但即使你身处 Python 2 的环境下,你想通过 im_func 去直接替换函数,也仍然是有问题的。


因为在 Python2 中不推荐普通用户对类实例的方法进行替换,所以 Python 给类实例的方法赋予了
只读属性

思路三:非常危险的字节码替换

表层不行,但这个方法在字节码层面却是可行的

这种方法,非常的粗暴且危险,他会直接影响到使用 People 的所有实例的 speak 方法,因此这种


方法千万不要使用。

思路四:利用 types 绑定方法

在 types 中有一个 MethodType,可以将普通方法与实例进行绑定。


绑定后,就可以直接替换掉原实例的 speak 方法了,完整代码如下:

import types

class People:
def speak(self):
print("hello, world")

def speak(self):
print("hello, python")

p = People()
p.speak = types.MethodType(speak, p)
p.speak()

这种方法,最为安全,不会影响其他实例。并且 Python 2 和 Python 3 都适用,是官方推荐的一种


做法。

5.32 如何动态创建函数?

在下面的代码中,每一次 for 循环都会创建一个返回特定字符串的函数。

from types import FunctionType

for name in ("world", "python"):


func = FunctionType(compile(
("def hello():\n"
" return '{}'".format(name)),
"<string>",
"exec").co_consts[0], globals())

print(func())

输出如下

world
python

第六章:良好编码习惯
6.1 不要直接调用类的私有方法

大家都知道,类中可供直接调用的方法,只有公有方法(protected类型的方法也可以,但是不建
议)。也就是说,类的私有方法是无法直接调用的。

这里先看一下例子

class Kls():
def public(self):
print('Hello public world!')

def __private(self):
print('Hello private world!')

def call_private(self):
self.__private()

ins = Kls()

##
ins.public()

##
ins.__private()

##
ins.call_private()

既然都是方法,那我们真的没有方法可以直接调用吗?

当然有啦,只是建议你千万不要这样弄,这里只是普及,让你了解一下。

##
ins._Kls__private()
ins.call_private()

6.2 默认参数最好不为可变对象

函数的参数分三种

可变参数
默认参数
关键字参数

当你在传递默认参数时,有新手很容易踩雷的一个坑。
先来看一个示例

def func(item, item_list=[]):


item_list.append(item)
print(item_list)

func('iphone')
func('xiaomi', item_list=['oppo','vivo'])
func('huawei')

在这里,你可以暂停一下,思考一下会输出什么?

思考过后,你的答案是否和下面的一致呢

['iphone']
['oppo', 'vivo', 'xiaomi']
['iphone', 'huawei']

如果是,那你可以跳过这部分内容,如果不是,请接着往下看,这里来分析一下。

Python 中的 def 语句在每次执行的时候都初始化一个函数对象,这个函数对象就是我们要调用的


函数,可以把它当成一个一般的对象,只不过这个对象拥有一个可执行的方法和部分属性。

对于参数中提供了初始值的参数,由于 Python 中的函数参数传递的是对象,也可以认为是传地


址,在第一次初始化 def 的时候,会先生成这个可变对象的内存地址,然后将这个默认参数
item_list 会与这个内存地址绑定。在后面的函数调用中,如果调用方指定了新的默认值,就会将原
来的默认值覆盖。如果调用方没有指定新的默认值,那就会使用原来的默认值。
6.3 增量赋值的性能更好

诸如 += 和 *= 这些运算符,叫做 增量赋值运算符。

这里使用用 += 举例,以下两种写法,在效果上是等价的。

##
a = 1 ; a += 1

##
a = 1; a = a + 1

+= 其背后使用的魔法方法是 __iadd__ ,如果没有实现这个方法则会退而求其次,使用 __add__


这两种写法有什么区别呢?

用列表举例 a += b,使用 __add__ 的话就像是使用了a.extend(b),如果使用 __add__ 的话,则是 a


= a+b,前者是直接在原列表上进行扩展,而后者是先从原列表中取出值,在一个新的列表中进行扩
展,然后再将新的列表对象返回给变量,显然后者的消耗要大些。

所以在能使用增量赋值的时候尽量使用它。

6.4 别再使用 pprint 打印了

1. 吐槽问题

pprint 你应该很熟悉了吧?
随便在搜索引擎上搜索如何打印漂亮的字典或者格式化字符串时,大部分人都会推荐你使用这货

比如这下面这个 json 字符串或者说字典(我随便在网上找的),如果不格式化美化一下,根本无


法阅读。

[{"id":1580615,"name":" ","packageName":"com.renren.mobile.android","iconUrl":"a
pp/com.renren.mobile.android/icon.jpg","stars":2,"size":21803987,"downloadUrl":"app/
com.renren.mobile.android/com.renren.mobile.android.apk","des":"2011-2017
SNS "},{"id":1540629,"name":" ","packageName"
:"com.ct.client","iconUrl":"app/com.ct.client/icon.jpg","stars":2,"size":4794202,"do
wnloadUrl":"app/com.ct.client/com.ct.client.apk","des":" 271934
"}]

如果你不想看到一堆密密麻麻的字,那就使用大伙都极力推荐的 pprint 看下什么效果(以下在


Python 2 中演示,Python 3 中是不一样的效果)。

>>> info=[{"id":1580615,"name":" ","packageName":"com.renren.mobile.android","ic


onUrl":"app/com.renren.mobile.android/icon.jpg","stars":2,"size":21803987,"downloadU
rl":"app/com.renren.mobile.android/com.renren.mobile.android.apk","des":"2011-2017
SNS "},{"id":1540629,"name":" ","pac
kageName":"com.ct.client","iconUrl":"app/com.ct.client/icon.jpg","stars":2,"size":47
94202,"downloadUrl":"app/com.ct.client/com.ct.client.apk","des":" 271934
"}]
>>>
>>> from pprint import pprint
>>> pprint(info)
[{'des': '2011-2017 \xe4\xbd\xa0\xe7\x9a\x84\xe9\x93\x81\xe5\xa4\xb4\xe5\xa8\x83\xe4
\xb8\x80\xe7\x9b\xb4\xe5\x9c\xa8\xe8\xbf\x99\xe5\x84\xbf\xe3\x80\x82\xe4\xb8\xad\xe5
\x9b\xbd\xe6\x9c\x80\xe5\xa4\xa7\xe7\x9a\x84\xe5\xae\x9e\xe5\x90\x8d\xe5\x88\xb6SNS\
xe7\xbd\x91\xe7\xbb\x9c\xe5\xb9\xb3\xe5\x8f\xb0\xef\xbc\x8c\xe5\xab\xa9\xe5\xa4\xb4\
xe9\x9d\x92',
'downloadUrl': 'app/com.renren.mobile.android/com.renren.mobile.android.apk',
'iconUrl': 'app/com.renren.mobile.android/icon.jpg',
'id': 1580615,
'name': '\xe7\x9a\xae\xe7\x9a\x84\xe5\x98\x9b',
'packageName': 'com.renren.mobile.android',
'size': 21803987,
'stars': 2},
{'des': '\xe6\x96\x97\xe9\xb1\xbc271934 \xe8\xb5\xb0\xe8\xbf\x87\xe8\xb7\xaf\xe8\xb
f\x87\xe4\xb8\x8d\xe8\xa6\x81\xe9\x94\x99\xe8\xbf\x87\xef\xbc\x8c\xe8\xbf\x99\xe9\x8
7\x8c\xe6\x9c\x89\xe6\x9c\x80\xe5\xa5\xbd\xe7\x9a\x84\xe9\xb8\xa1\xe5\x84\xbf',
'downloadUrl': 'app/com.ct.client/com.ct.client.apk',
'iconUrl': 'app/com.ct.client/icon.jpg',
'id': 1540629,
'name': '\xe4\xb8\x8d\xe5\xad\x98\xe5\x9c\xa8\xe7\x9a\x84',
'packageName': 'com.ct.client',
'size': 4794202,
'stars': 2}]

好像有点效果,真的是 “神器”呀。

但是你告诉我, \xe4\xbd\xa0\xe7\x9a 这些是什么玩意?本来想提高可读性的,现在变成完全不


可读了。

好在我懂点 Python 2 的编码,知道 Python 2 中默认(不带u)的字符串格式都是 str 类型,也是


bytes 类型,它是以 byte 存储的。

行吧,好像是我错了,我改了下,使用 unicode 类型来定义中文字符串吧。

>>> info = [{"id":1580615,"name":u" ","packageName":"com.renren.mobile.android",


"iconUrl":"app/com.renren.mobile.android/icon.jpg","stars":2,"size":21803987,"downlo
adUrl":"app/com.renren.mobile.android/com.renren.mobile.android.apk","des":u"2011-20
17 SNS "},{"id":1540629,"name":u" ",
"packageName":"com.ct.client","iconUrl":"app/com.ct.client/icon.jpg","stars":2,"size
":4794202,"downloadUrl":"app/com.ct.client/com.ct.client.apk","des":u" 271934
"}]
>>>
>>> from pprint import pprint
>>> pprint(info)
[{'des': u'2011-2017\u4f60\u7684\u94c1\u5934\u5a03\u4e00\u76f4\u5728\u8fd9\u513f\u30
02\u4e2d\u56fd\u6700\u5927\u7684\u5b9e\u540d\u5236SNS\u7f51\u7edc\u5e73\u53f0\uff0c\
u5ae9\u5934\u9752',
'downloadUrl': 'app/com.renren.mobile.android/com.renren.mobile.android.apk',
'iconUrl': 'app/com.renren.mobile.android/icon.jpg',
'id': 1580615,
'name': u'\u76ae\u7684\u561b',
'packageName': 'com.renren.mobile.android',
'size': 21803987,
'stars': 2},
{'des': u'\u6597\u9c7c271934\u8d70\u8fc7\u8def\u8fc7\u4e0d\u8981\u9519\u8fc7\uff0c\
u8fd9\u91cc\u6709\u6700\u597d\u7684\u9e21\u513f',
'downloadUrl': 'app/com.ct.client/com.ct.client.apk',
'iconUrl': 'app/com.ct.client/icon.jpg',
'id': 1540629,
'name': u'\u4e0d\u5b58\u5728\u7684',
'packageName': 'com.ct.client',
'size': 4794202,
'stars': 2}]

确实是有好点了,但是看到下面这些,我崩溃了,我哪里知道这是什么鬼,难道是我太菜了吗?当
我是计算机呀?

u'\u6597\u9c7c271934\u8d70\u8fc7\u8def\u8fc7\u4e0d\u8981\u9519\u8fc7\uff0c\u8fd9\u91
cc\u6709\u6700\u597d\u7684\u9e21\u513f'
除此之外,我们知道 json 的严格要求必须使用 双引号,而我定义字典时,也使用了双引号了,为
什么打印出来的为什么是 单引号?我也太难了吧,我连自己的代码都无法控制了吗?

到这里,我们知道了 pprint 带来的两个问题:

1. 没法在 Python 2 下正常打印中文


2. 没法输出 JSON 标准格式的格式化内容(双引号)

2. 解决问题

打印中文

如果你是在 Python 3 下使用,你会发现中文是可以正常显示的。

## Python3.7
>>> info = [{"id":1580615,"name":u" ","packageName":"com.renren.mobile.android",
"iconUrl":"app/com.renren.mobile.android/icon.jpg","stars":2,"size":21803987,"downlo
adUrl":"app/com.renren.mobile.android/com.renren.mobile.android.apk","des":u"2011-20
17 SNS "},{"id":1540629,"name":u" ",
"packageName":"com.ct.client","iconUrl":"app/com.ct.client/icon.jpg","stars":2,"size
":4794202,"downloadUrl":"app/com.ct.client/com.ct.client.apk","des":u" 271934
"}]
>>>
>>> from pprint import pprint
>>> pprint(info)
[{'des': '2011-2017 SNS ',
'downloadUrl': 'app/com.renren.mobile.android/com.renren.mobile.android.apk',
'iconUrl': 'app/com.renren.mobile.android/icon.jpg',
'id': 1580615,
'name': ' ',
'packageName': 'com.renren.mobile.android',
'size': 21803987,
'stars': 2},
{'des': ' 271934 ',
'downloadUrl': 'app/com.ct.client/com.ct.client.apk',
'iconUrl': 'app/com.ct.client/icon.jpg',
'id': 1540629,
'name': ' ',
'packageName': 'com.ct.client',
'size': 4794202,
'stars': 2}]
>>>

但是很多时候(在公司的一些服务器)你无法选择自己使用哪个版本的 Python,本来我可以选择
不用的,因为有更好的替代方案(这个后面会讲)。
但是我出于猎奇,正好前两天不是写过一篇关于 编码 的文章吗,我自认为自己对于 编码还是掌握
比较熟练的,就想着来解决一下这个问题。

索性就来看下 pprint 的源代码,还真被我找到了解决方法,如果你也想挑战一下,不防在这里停


住,自己研究一下如何实现,我相信对你阅读源码会有帮助。

以下是我的解决方案,供你参考:

写一个自己的 printer 对象,继承自 PrettyPrinter (pprint 使用的printer)

并且复写 format 方法,判断传进来的字符串对象是否 str 类型,如果不是 str 类型,而是 unicode


类型,就用 uft8 编码成 str 类型。

## coding: utf-8
from pprint import PrettyPrinter

## PrettyPrinter format
class MyPrettyPrinter(PrettyPrinter):
def format(self, object, context, maxlevels, level):
if isinstance(object, unicode):
return (object.encode('utf8'), True, False)
return PrettyPrinter.format(self, object, context, maxlevels, level)

info = [{"id":1580615,"name":u" ","packageName":"com.renren.mobile.android","ico


nUrl":"app/com.renren.mobile.android/icon.jpg","stars":2,"size":21803987,"downloadUr
l":"app/com.renren.mobile.android/com.renren.mobile.android.apk","des":u"2011-2017
SNS "},{"id":1540629,"name":u" ","pa
ckageName":"com.ct.client","iconUrl":"app/com.ct.client/icon.jpg","stars":2,"size":4
794202,"downloadUrl":"app/com.ct.client/com.ct.client.apk","des":u" 271934
"}]

MyPrettyPrinter().pprint(info)

输出如下,已经解决了中文的显示问题:
打印双引号

解决了中文问题后,再来看看如何让 pprint 打印双引号。

在实例化 PrettyPrinter 对象的时候,可以接收一个 stream 对象,它表示你要将内容输出到哪里,


默认是使用 sys.stdout 这个 stream,也就是标准输出。

现在我们要修改输出的内容,也就是将输出的单引号替换成双引号。

那我们完全可以自己定义一个 stream 类型的对象,该对象不需要继承任何父类,只要你实现 write


方法就可以。

有了思路,就可以开始写代码了,如下:

## coding: utf-8
from pprint import PrettyPrinter

class MyPrettyPrinter(PrettyPrinter):
def format(self, object, context, maxlevels, level):
if isinstance(object, unicode):
return (object.encode('utf8'), True, False)
return PrettyPrinter.format(self, object, context, maxlevels, level)

class MyStream():
def write(self, text):
print text.replace('\'', '"')

info = [{"id":1580615,"name":u" ","packageName":"com.renren.mobile.android","ico


nUrl":"app/com.renren.mobile.android/icon.jpg","stars":2,"size":21803987,"downloadUr
l":"app/com.renren.mobile.android/com.renren.mobile.android.apk","des":u"2011-2017
SNS "},{"id":1540629,"name":u" ","pa
ckageName":"com.ct.client","iconUrl":"app/com.ct.client/icon.jpg","stars":2,"size":4
794202,"downloadUrl":"app/com.ct.client/com.ct.client.apk","des":u" 271934
"}]
MyPrettyPrinter(stream=MyStream()).pprint(info)

尝试执行了下,我的天,怎么是这样子的。

[
{
"des"
:
2011-2017 SNS
,
"downloadUrl":
"app/com.renren.mobile.android/com.renren.mobile.android.apk"
,
"iconUrl":
"app/com.renren.mobile.android/icon.jpg"
,
"id":
1580615
,
"name":

,
"packageName":
"com.renren.mobile.android"
,
"size":
21803987
,
"stars":
2
}
,

{
"des"
:
271934
,
"downloadUrl":
"app/com.ct.client/com.ct.client.apk"
,
"iconUrl":
"app/com.ct.client/icon.jpg"
,
"id":
1540629
,
"name":

,
"packageName":
"com.ct.client"
,
"size":
4794202
,
"stars":
2
}
]

经过一番研究,才知道是因为 print 函数默认会将打印的内容后面加个 换行符。

那如何将使 print 函数打印的内容,不进行换行呢?

方法很简单,但是我相信很多人都不知道,只要在 print 的内容后加一个 逗号 就行。

就像下面这样。

知道了问题所在,再修改下代码

## coding: utf-8
from pprint import PrettyPrinter

class MyPrettyPrinter(PrettyPrinter):
def format(self, object, context, maxlevels, level):
if isinstance(object, unicode):
return (object.encode('utf8'), True, False)
return PrettyPrinter.format(self, object, context, maxlevels, level)

class MyStream():
def write(self, text):
print text.replace('\'', '"'),

info = [{"id":1580615,"name":u" ","packageName":"com.renren.mobile.android","ico


nUrl":"app/com.renren.mobile.android/icon.jpg","stars":2,"size":21803987,"downloadUr
l":"app/com.renren.mobile.android/com.renren.mobile.android.apk","des":u"2011-2017
SNS "},{"id":1540629,"name":u" ","pa
ckageName":"com.ct.client","iconUrl":"app/com.ct.client/icon.jpg","stars":2,"size":4
794202,"downloadUrl":"app/com.ct.client/com.ct.client.apk","des":u" 271934
"}]

MyPrettyPrinter(stream=MyStream()).pprint(info)

终于成功了,太不容易了吧。

3. 何必折腾

通过上面的一番折腾,我终于实现了我 梦寐以求 的需求。

代价就是我整整花费了两个小时,才得以实现,而对于小白来说,可能没有信心,也没有耐心去做
这样的事情。

所以我想说的是,Python 2 下的 pprint ,真的不要再用了。

为什么我要用这么 说,因为明明有更好的替代品,人生苦短,既然用了 Python ,当然是怎么简单


怎么来咯,何必为难自己呢,一行代码可以解决的事情,偏偏要去写两个类,那不是自讨苦吃吗?
(我这是在骂自己吗?

如果你愿意抛弃 pprint ,那我推荐你用 json.dumps ,我保证你再也不想用 pprint 了。

打印中文
其实无法打印中文,是 Python 2 引来的大坑,并不能全怪 pprint 。

但是同样的问题,在 json.dumps 这里,却只要加个参数就好了,可比 pprint 简单得不要太多。

具体的代码示例如下:

>>> info = [{"id":1580615,"name":" ","packageName":"com.renren.mobile.android","


iconUrl":"app/com.renren.mobile.android/icon.jpg","stars":2,"size":21803987,"downloa
dUrl":"app/com.renren.mobile.android/com.renren.mobile.android.apk","des":"2011-2017
SNS "},{"id":1540629,"name":" ","pa
ckageName":"com.ct.client","iconUrl":"app/com.ct.client/icon.jpg","stars":2,"size":4
794202,"downloadUrl":"app/com.ct.client/com.ct.client.apk","des":" 271934
"}]
>>>
>>> import json
>>>
>>>
>>> print json.dumps(info, indent=4, ensure_ascii=False)
[
{
"downloadUrl": "app/com.renren.mobile.android/com.renren.mobile.android.apk"
,
"iconUrl": "app/com.renren.mobile.android/icon.jpg",
"name": " ",
"stars": 2,
"packageName": "com.renren.mobile.android",
"des": "2011-2017 SNS ",
"id": 1580615,
"size": 21803987
},
{
"downloadUrl": "app/com.ct.client/com.ct.client.apk",
"iconUrl": "app/com.ct.client/icon.jpg",
"name": " ",
"stars": 2,
"packageName": "com.ct.client",
"des": " 271934 ",
"id": 1540629,
"size": 4794202
}
]
>>>

json.dumps 的关键参数有两个:

indent=4:以 4 个空格缩进单位
ensure_ascii=False:接收非 ASCII 编码的字符,这样才能使用中文

与 pprint 相比 json.dumps 可以说完胜:


1. 两个参数就能实现所有我的需求(打印中文与双引号)
2. 就算在 Python 2 下,使用中文也不需要用 u' ' 这种写法
3. Python2 和 Python3 的写法完全一致,对于这一点不需要考虑兼容问题

4. 总结一下

本来很简单的一个观点,我为了证明 pprint 实现那两个需求有多么困难,花了很多的时间去研究了


pprint 的源码(各种处理其实还是挺复杂的),不过好在最后也能有所收获。

本文的分享就到这里,阅读本文,我认为你可以获取到三个知识点

1. 核心观点:Python2 下不要再使用 pprint


2. 若真要使用,且有和一样的改造需求,可以参考我的实现
3. Python 2 中的 print 语句后居然可以加 逗号

6.5 变量名与保留关键冲突怎么办?

所有的编程语言都有一些保留关键字,这是代码得以编译/解释的基础。

有了这些关键字就组成了语法,当你的变量名和这些保留关键字冲突时,该怎么办呢?

在回答这个问题前,先要看看 Python 中的保留关键字有哪些?

Python 的关键字,可以通过 keyword 这个模块列出来,一共有 33 个。

>>> import keyword;


>>> print('\n'.join(keyword.kwlist))
False
None
True
and
as
assert
break
class
continue
def
del
elif
else
except
finally
for
from
global
if
import
in
is
lambda
nonlocal
not
or
pass
raise
return
try
while
with
yield
>>> len(keyword.kwlist)
33

使用这些关键字来做为变量名,是会报语法错误的。

>>> try = True


File "<stdin>", line 1
try = True
^
SyntaxError: invalid syntax

关于这个问题,PEP8 建议当你想使用的变量名被关键字所占用时,可以使用 _ 这样在变量后


面加一个单下划线的形式来命名,这种后缀一下划线的方式优先于缩写或拼写错误。

有了 PEP8 做为指导,我们可以这样子写了

>>> try_ = True

6.6 不想让子类继承的变量名该怎么写?

先来看下面这段代码

class Parent:
def __init__(self):
self.name = "MING"
class Son(Parent):
def __init__(self):
self.name = "Xiao MING"

bar = Son()
print(bar.name)
## Xiao MING

Bar 作为 Foo 的子类,会继承父类的 name 属性。

如果有一些属性,是父类自己独有的,不想被子类继承,该怎么写呢?

可以在属性前面加两个下划线,两个类的定义如下

class Parent:
def __init__(self):
self.name = "MING"
self.__wife = "Julia"

class Son(Parent):
def __init__(self):
self.name = "Xiao MING"
super().__init__()

从本章节的第一篇文章(6.1 不要直接调用类的私有方法)我们知道了私有的变量或函数,是不能
直接调用的,需要用这样的形式才能访问 _ __ 。

Son 类的实例的并没有初始化 __wife 属性,但是 Parent 类的实例有,但是由于这个属性是父类


私有的,当子类想要访问时,是会提示该变量不存在。

验证过程如下:
6.7 利用 any 代替 for 循环

在某些场景下,我们需要判断是否满足某一组集合中任意一个条件

这时候,很多同学自然会想到使用 for 循环。

found = False
for thing in things:
if thing == other_thing:
found = True
break

但其实更好的写法,是使用 any() 函数,能够使这段代码变得更加清晰、简洁

found = any(thing == other_thing for thing in things)

使用 any 并不会减少 for 循环的次数,只要有一个条件为 True,any 就能得到结果。

同理,当你需要判断是否满足某一组集合中所有条件,也可以使用 all() 函数。


found = all(thing == other_thing for thing in things)

只要有一个不满足条件,all 函数的结果就会立刻返回 False

6.8 不同条件分支里应减少重合度

如下是一个简单的条件语句模型

if A:
<code_block_1>
elif B:
<code_block_2>
else:
<code_block_3>

如果 code_block_2 和 code_block_1 的代码完全一致,那么应该想办法减少代码冗余。

这边举个例子,下面这段代码中

def process_payment(payment):
if payment.currency == 'USD':
process_standard_payment(payment)
elif payment.currency == 'EUR':
process_standard_payment(payment)
else:
process_international_payment(payment)

其实更好的做法是用 in 来合并条件一和条件二
def process_payment(payment):
if payment.currency in ('USD', 'EUR'):
process_standard_payment(payment)
else:
process_international_payment(payment)

6.9 如无必要,勿增实体噢

删除没必要的调用 keys()

字典是由一个个的键值对组成的,如果你遍历字典时只需要访问键,用不到值,有很多同学会用下
面这种方式:

for currency in currencies.keys():


process(currency)

在这种情况下,不需要调用keys(),因为遍历字典时的默认行为是遍历键。

for currency in currencies:


process(currency)

现在,该代码更加简洁,易于阅读,并且避免调用函数会带来性能改进。

简化序列比较

我们经常要做的是在尝试对列表或序列进行操作之前检查列表或序列是否包含元素。

if len(list_of_hats) > 0:
hat_to_wear = choose_hat(list_of_hats)

使用Python的方法则更加简单:如果Python列表和序列具有元素,则返回为True,否则为False:

if list_of_hats:
hat_to_wear = choose_hat(list_of_hats)

仅使用一次的内联变量

我们在很多代码中经常看到,有些同学分配结果给变量,然后马上返回它,例如,
def state_attributes(self):
"""Return the state attributes."""
state_attr = {
ATTR_CODE_FORMAT: self.code_format,
ATTR_CHANGED_BY: self.changed_by,
}
return state_attr

如果直接返回,则更加直观、简洁,

def state_attributes(self):
"""Return the state attributes."""
return {
ATTR_CODE_FORMAT: self.code_format,
ATTR_CHANGED_BY: self.changed_by,
}

这样可以缩短代码并删除不必要的变量,从而减轻了读取函数的负担。

6.10 保持代码的简洁与可诗性

将条件简化为return语句

如果,我们实现的函数要返回一个布尔型的结果,通常会这样去做,

def function():
if isinstance(a, b) or issubclass(b, a):
returnTrue
returnFalse

但是,对比这样,直接返回结果会更加明智:

def function():
return isinstance(a, b) or issubclass(b, a)

6.11 给模块的私有属性上保险

保护对象

有的朋友,喜欢简单粗暴的使用 from x import * 来导入 x 模块中的所有对象,实际上有一些对


象或者变量,是实现细节,不需要暴露给导入方的,因为导入了也用不上。

对于这些变量或者对象,就可以在前面其名字前加上下划线,只要在变量名前加上下划线,就属于
"保护对象"。

使用 from x import * 后,这些 "保护对象" 是会直接跳过导入。

比如下面这些代码中,只有 drive 函数才会被 from x import * 所导入

_moto_type = 'L15b2'
_wheel_type = 'michelin'

def drive():
_start_engine()
_drive_wheel()

def _start_engine():
print('start engine %s'%_moto_type)

def _drive_wheel():
print('drive wheel %s'%_wheel_type)

突破保护

前面之所以说是“保护”并不是“私有”,是因为Python没有提供解释器机制来控制访问权限。我们依
然可以访问这些属性:

import tools
tools._moto_type = 'EA211'
tools.drive()

以上代码,以越过“保护属性”。此外,还有两种方法能突破这个限制,一种是将“私有属性”添加到
tool.py文件的 __all__ 列表里,使 from tools import * 也导入这些本该隐藏的属性。

__all__ = ['drive','_moto_type','_wheel_type']

另一种是导入时指定“受保护属性”名。

from tools import drive,_start_engine


_start_engine()

甚至是,使用 import tools 也可以轻易突破保护限制。所以可见,“保护属性”是一种简单的隐藏


机制,只有在 from tools import * 时,由解释器提供简单的保护,但是可以轻易突破。这种保护
更多地依赖程序员的共识:不访问、修改“保护属性”。除此之外,有没有更安全的保护机制呢?
有,就是下一部分讨论的私有变量。

6.12 变量不能与保留关键字重名

在 Python 中有很多的保留关键字,这些关键字的使用,不需要我们定义,也不需要我们导入,只
要你进入到了 Python 的环境中,就可以立即使用。

使用如下方法,可以查看 Python 中的保留关键字

>>> import keyword


>>> keyword.kwlist
['and', 'as', 'assert', 'break', 'class', 'continue', 'def', 'del', 'elif', 'else',
'except', 'exec', 'finally', 'for', 'from', 'global', 'if', 'import', 'in', 'is', 'l
ambda', 'not', 'or', 'pass', 'print', 'raise', 'return', 'try', 'while', 'with', 'yi
eld']

而很尴尬的是,如果你在日常编码中,不经意地用到其中的一些关键字,就会产生冲突。

比如说 class,这个有类别的意思,可能你也想使用它来作为变量名,如果直接使用,会发生冲突

更好的做法是,使用下划线来避免冲突

def type_obj_class(name,class_):
pass

def tag(name,*content,class_):
pass

第七章:神奇魔法模块
7.1 远程登陆服务器的最佳利器

在使用 Python 写一些脚本的时候,在某些情况下,我们需要频繁登陆远程服务去执行一次命令,


并返回一些结果。

在 shell 环境中,我们是这样子做的。

$ sshpass -p ${passwd} ssh -p ${port} -l ${user} -o StrictHostKeyChecking=no xx.xx.x


x.xx "ls -l"

然后你会发现,你的输出有很多你并不需要,但是又不去不掉的一些信息(也许有方法,请留言交
流),类似这样

host: xx.xx.xx.xx, port: xx


Warning: Permanently added '[xx.xx.xx.xx]:xx' (RSA) to the list of known hosts.
Login failure: [Errno 1] This server is not registered to rmp platform, please confi
rm whether cdn server.
total 4
-rw-r--r-- 1 root root 239 Mar 30 2018 admin-openrc

对于直接使用 shell 命令,来执行命令的,可以直接使用管道,或者将标准输出重定向到文件的方


法取得执行命令返回的结果

1. 使用 subprocess

若是使用 Python 来做这件事,通常我们会第一时间,想到使用 os.popen,os.system,


commands,subprocess 等一些命令执行库来间接获取 。
但是据我所知,这些库获取的 output 不仅只有标准输出,还包含标准错误(也就是上面那些多余
的信息)

所以每次都要对 output 进行的数据清洗,然后整理格式化,才能得到我们想要的数据。

用 subprocess 举个例子,就像这样子

import subprocess
ssh_cmd = "sshpass -p ${passwd} ssh -p 22 -l root -o StrictHostKeyChecking=no xx.xx.
xx.xx 'ls -l'"
status, output = subprocess.getstatusoutput(ssh_cmd)

##
<code...>

通过以上的文字 + 代码的展示 ,可以感觉到 ssh 登陆的几大痛点

痛点一:需要额外安装 sshpass(如果不免密的话)
痛点二:干扰信息太多,数据清理、格式化相当麻烦
痛点三:代码实现不够优雅(有点土),可读性太差
痛点四:ssh 连接不能复用,一次连接仅能执行一次
痛点五:代码无法全平台,仅能在 Linux 和 OSX 上使用

为了解决这几个问题,我搜索了全网关于 Python ssh 的文章,没有看到有完整介绍这方面的技巧


的。

为此,我就翻阅了一个很火的 Github 项目: awesome-python-cn (https://github.com/BingmingW


ong/awesome-python-cn)。

期望在这里,找到有一些关于 远程连接 的一些好用的库。

还真的被我找到了两个

sh.ssh
Paramiko

2. 使用 sh.ssh

首先来介绍第一个, sh.ssh
sh是一个可以让你通过函数的调用来完成 Linxu/OSX 系统命令的一个库,非常好用,关于它有机
会也写篇介绍。

$ python3 -m pip install sh

今天只介绍它其中的一个函数: ssh

通常两台机器互访,为了方便,可设置免密登陆,这样就不需要输入密码。

这段代码可以实现免密登陆,并执行我们的命令 ls -l

from sh import ssh


output=ssh("root@xx.xx.xx.xx", "-p 22", "ls -l")
print(output)

但有可能 ,我们并不想设置互信免密,为了使这段代码更通用,我假定我们没有设置免密,只能
使用密码进行登陆。

问题就来了,要输入密码,必须得使用交互式的方法来输入呀,在 Python 中要如何实现呢?

原来 ssh 方法接收一个 _out 参数,这个参数可以为一个字符串,表示文件路径,也可以是一个文


件对象(或者类文件对象),还可以是一个回调函数,意思是当有标准输出时,就会调用将输出内
容传给这个函数。

这就好办了呀。

我只要识别到有 password: 字样,就往标准输入写入我的密码就好了呀。

完整代码如下:

import sys
from sh import ssh

aggregated = ""
def ssh_interact(char, stdin):
global aggregated
sys.stdout.write(char.encode())
sys.stdout.flush()
aggregated += char
if aggregated.endswith("password: "):
stdin.put("you_password\n")

output=ssh("root@xx.xx.xx.xx", "-p 22", "ls -l",_tty_in=True, _out_bufsize=0, _out=s


sh_interact)
print(output)

这是官方文档(http://amoffat.github.io/sh/tutorials/interacting_with_processes.html?highlight=ssh
)给的一些信息,写的一个demo。

尝试运行后,发现程序会一直在运行中,永远不会返回,不会退出,回调函数也永远不会进入。

通过调试查看源代码,仍然查不到问题所在,于是去 Github 上搜了下,原来在 2017 年就已经存在


这个问题了,到现在 2020 年了还没有修复,看来使用 sh.ssh 的人并不多,于是我又“追问”了
下,期望能得到回复。

以上这个问题,只有在需要输入密码才会出现,如果设置了机器互信是没有问题的。

为了感受 sh.ssh 的使用效果,我设置了机器互信免密,然后使用如下这段代码。


from sh import ssh

my_server=ssh.bake("root@xx.xx.xx.xx", "-p 22")

##
print(my_server.ls())

## sleep top
time.sleep(5)

## +1 -1
print(my_server.ifconfig())

惊奇地发现使用 bake 这种方式, my_server.ls() 和 my_server.ifconfig() 这种看似是通过同


一个ssh连接,执行两次命令,可实际上,你可以在远程机器上,执行 top 命令看到已连接的终端
的变化,会先 +1 再 -1 ,说明两次命令的执行是通过两次连接实现的。

如此看来,使用 sh.ssh 可以解决痛点一(如果上述问题能得到解决)、痛点二、痛点三。

但是它仍然无法复用 ssh 连接,还是不太方便,不是我理想中的最佳方案。

最重要的一点是, sh 这个模块,仅支持 Linxu/OSX ,在 Windows 你得使用它的兄弟库 - pbs ,


然后我又去 pypi 看了一眼 pbs,已经 “年久失修”,没人维护了。

至此,我离 “卒”,就差最后一根稻草了。

3. 使用 paramiko

带着最后一丝希望,我尝试使用了 paramiko 这个库,终于在 paramiko 这里,找回了本应属于


Python 的那种优雅。

你可以通过如下命令去安装它
$ python3 -m pip install paramiko

然后接下来,就介绍几种常用的 ssh 登陆的方法

方法1:基于用户名和密码的 sshclient 方式登录

然后你可以参考如下这段代码,在 Linux/OSX 系统下进行远程连接

import paramiko

ssh = paramiko.SSHClient()
## know_hosts
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())

##
ssh.connect("xx.xx.xx.xx", username="root", port=22, password="you_password")

##
ssh_stdin, ssh_stdout, ssh_stderr = ssh.exec_command("ls -l")

##
print(ssh_stdout.read())

##
ssh.close()

方法2:基于用户名和密码的 transport 方式登录

方法1 是传统的连接服务器、执行命令、关闭的一个操作,多个操作需要连接多次,无法复用连接
[痛点四]。

有时候需要登录上服务器执行多个操作,比如执行命令、上传/下载文件,方法1 则无法实现,那
就可以使用 transport 的方法。

import paramiko

##
trans = paramiko.Transport(("xx.xx.xx.xx", 22))
trans.connect(username="root", password="you_passwd")

## sshclient transport trans


ssh = paramiko.SSHClient()
ssh._transport = trans

##
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh_stdin, ssh_stdout, ssh_stderr = ssh.exec_command("ls -l")
print(ssh_stdout.read())

##
trans.close()

方法3:基于公钥密钥的 SSHClient 方式登录

import paramiko

## RSA
## password password
pkey = paramiko.RSAKey.from_private_key_file('/home/you_username/.ssh/id_rsa', passw
ord='12345')

##
ssh = paramiko.SSHClient()
ssh.connect(hostname='xx.xx.xx.xx',
port=22,
username='you_username',
pkey=pkey)

##
stdin, stdout, stderr = ssh.exec_command('ls -l')

## stdout stderr
print(stdout.read())

##
ssh.close()

方法4:基于密钥的 Transport 方式登录

import paramiko

## RSA
## password password
pkey = paramiko.RSAKey.from_private_key_file('/home/you_username/.ssh/id_rsa', passw
ord='12345')

##
trans = paramiko.Transport(('xx.xx.xx.xx', 22))
trans.connect(username='you_username', pkey=pkey)

## sshclient transport trans


ssh = paramiko.SSHClient()
ssh._transport = trans
##
stdin, stdout, stderr = ssh.exec_command('df -hl')
print(stdout.read().decode())

##
trans.close()

以上四种方法,可以帮助你实现远程登陆服务器执行命令,如果需要复用连接:一次连接执行多次
命令,可以使用 方法二 和 方法四

用完后,记得关闭连接。

实现 sftp 文件传输

同时,paramiko 做为 ssh 的完美解决方案,它非常专业,利用它还可以实现 sftp 文件传输。

import paramiko

## trans ## transport
trans = paramiko.Transport(('xx.xx.xx.xx', 22))

##
trans.connect(username='you_username', password='you_passwd')

## sftp ,
sftp = paramiko.SFTPClient.from_transport(trans)

##
sftp.put(localpath='/tmp/11.txt', remotepath='/tmp/22.txt')

##
sftp.get(remotepath='/tmp/22.txt', localpath='/tmp/33.txt')
trans.close()

到这里,Paramiko 已经完胜了,但是仍然有一个痛点我们没有提及,就是多平台,说的就是
Windows,这里就有一件好事,一件坏事了,。

好事就是:paramiko 支持 windows

坏事就是:你需要做很多复杂的准备,你可 google 解决,但是我建议你直接放弃,坑太深了。


注意事项

使用 paramiko 的时候,有一点需要注意一下,这个也是我自己 "踩坑" 后才发现的,其实我觉得这


个设计挺好的,如果你不需要等待它返回数据,可以直接实现异步效果,只不过对于不知道这个设
计的人,确实是个容易掉坑的点

就是在执行 ssh.exec_command(cmd) 时,这个命令并不是同步阻塞的。

比如下面这段代码,执行时,你会发现 脚本立马就结束退出了,并不会等待 5 s 后,再 执行


ssh.close()

import paramiko

trans = paramiko.Transport(("172.20.42.1", 57891))


trans.connect(username="root", password="youpassword")
ssh = paramiko.SSHClient()
ssh._transport = trans
stdin, stdout, stderr = ssh.exec_command("sleep 5;echo ok")
ssh.close()

但是如果改成这样,加上一行 stdout.read(), paramiko 就知道,你需要这个执行的结果,就会在


read() 进行阻塞。

import paramiko

trans = paramiko.Transport(("172.20.42.1", 57891))


trans.connect(username="root", password="youpassword")
ssh = paramiko.SSHClient()
ssh._transport = trans
stdin, stdout, stderr = ssh.exec_command("sleep 5;echo ok")

## read()
print(stdout.read())
ssh.close()

4. 写在最后

经过了一番对比,和一些实例的展示,可以看出 Paramiko 是一个专业、让人省心的 ssh 利器,个


人认为 Paramiko 模块是运维人员必学模块之一,如果你恰好需要在 Python 代码中实现 ssh 到远程
服务器去获取一些信息,那么我把 Paramiko 推荐给你。

7.2 代码 BUG 变得酷炫的利器


当我们写的一个脚本或程序发生各种不可预知的异常时,如果我们没有进行捕获处理的时候,通常
都会致使程序崩溃退出,并且会在终端打印出一堆 密密麻麻 的 traceback 堆栈信息来告诉我们,是
哪个地方出了问题。

就像这样子,天呐,密集恐惧症要犯了都

上面这段 traceback

只有黑白两个颜色,无法像代码高亮那样,对肉眼实现太不友好了
无法直接显示报错的代码,排查问题慢人一步,效率太低

那有没有一种办法,可以解决这些问题呢?

当然有了,在 Python 中,没有什么问题是一个库解决不了的,如果有,那就等你去开发这个库。

今天要介绍的这个库呢,叫做 pretty-errors ,从名字上就可以知道它的用途,是用来美化错误


信息的。

通过这条命令你可以安装它

$ python3 -m pip install pretty-errors

1. 环境要求

由于使用了 pretty-errors 后,你的 traceback 信息输出,会有代码高亮那样的效果,因此当你在


使用测试使用 pretty-error 时,请确保你使用的终端可以输出带有颜色的字体。
在 windows 上你可以使用 Powershell,cmder 等

在 Mac 上你可以使用自带的终端,或者安装一个更好用的 iTerm2

2. 效果对比

随便写一个没有使用 pretty-errors ,并且报错了的程序,是这样子的。

而使用了 pretty_errors 后,报错信息被美化成这样了。


是不是感觉清楚了不少,那种密密麻麻带来的焦虑感是不是都消失了呢?

当然这段代码少,你可能还没感受到,那就来看下 该项目在 Github上的一张效果对比图吧

3. 配置全局可用

可以看到使用了 pretty_errors 后,无非就是把过滤掉了一些干扰我们视线的无用信息,然后把有用


的关键信息给我们高亮显示。
既然既然这样,那 pretty_errors 应该也能支持我们如何自定义我们选用什么样的颜色,怎么排版
吧?

答案是显而易见的。

pretty_errors 和其他库不太一样,在一定程度上(如果你使用全局配置的话),它并不是开箱即用
的,你在使用它之前可能需要做一下配置。

使用这一条命令,会让你进行配置,可以让你在该环境中运行其他脚本时的 traceback 输出都自动


美化。

$ python3 -m pretty_errors

配置完成后,你再运行任何脚本,traceback 都会自动美化了。

不仅是在我的 iTerm 终端下

在 PyCharm 中也会
唯一的缺点就是,原先在 PyCharm 中的 traceback 可以直接点击 直接跳转到对应错误文
件代码行,而你如果是在 VSCode 可以使用 下面自定义配置的方案解决这个问题(下面会讲到,参
数是: display_link )。

因此,有些情况下,你并不想设置 pretty_errors 全局可用。

那怎么取消之前的配置呢?

只需要再次输出 python -m pretty_errors ,输出入 C 即可清除。

4. 单文件中使用
取消全局可用后,你可以根据自己需要,在你需要使用 pretty-errors 的脚本文件中导入
pretty_errors ,即可使用

import pretty_errors

就像这样

import pretty_errors

def foo():
1/0

if __name__ == "__main__":
foo()

值得一提的是,使用这种方式,若是你的脚本中,出现语法错误,则输出的异常信息还是按照之前
的方式展示,并不会被美化。

因此,为了让美化更彻底,官方推荐你使用 python -m pretty_errors

5. 自定义设置

上面的例子里,我们使用的都是 pretty_errors 的默认美化格式,展示的信息并没有那么全。

比如

它并没有展示报错文件的绝对路径,这将使我们很难定位到是哪个文件里的代码出现错误。
如果能把具体报错的代码,给我们展示在终端屏幕上,就不需要我们再到源码文件中排查原因
了。

如果使用了 pretty_errors 导致异常信息有丢失,那还不如不使用 pretty_errors 呢。

不过,可以告诉你的是, pretty_errors 并没有你想象的那么简单。

它足够开放,支持自定义配置,可以由你选择你需要展示哪些信息,怎么展示?

这里举一个例子

import pretty_errors

##
pretty_errors.configure(
separator_character = '*',
filename_display = pretty_errors.FILENAME_EXTENDED,
line_number_first = True,
display_link = True,
lines_before = 5,
lines_after = 2,
line_color = pretty_errors.RED + '> ' + pretty_errors.default_config.li
ne_color,
code_color = ' ' + pretty_errors.default_config.line_color,
)

##
def foo():
1/0

if __name__ == "__main__":
foo()

在你像上面这样使用 pretty_errrs.configure 进行配置时,抛出的的异常信息就变成这样了。

当然了, pretty_errors.configure() 还可以接收很多的参数,你可以根据你自己的需要进行配


置。

5.1 设置颜色

header_color :设置标题行的颜色。
timestamp_color :设置时间戳颜色
default_color :设置默认的颜色
filename_color :设置文件名颜色
line_number_color :设置行号颜色。
function_color :设置函数颜色。
link_color :设置链接的颜色。

在设置颜色的时候, pretty_errors 提供了一些常用的 颜色常量供你直接调取。

BLACK :黑色
GREY :灰色
RED :红色
GREEN :绿色
YELLOW :黄色
BLUE :蓝色
MAGENTA :品红色
CYAN :蓝绿色
WHITE :白色

而每一种颜色,都相应的匹配的 BRIGHT_ 变体 和 _BACKGROUND 变体,

其中, _BACKGROUND 用于设置背景色,举个例子如下。


5.2 设置显示内容

line_number_first启用后,将首先显示行号,而不是文件名。
lines_before : 显示发生异常处的前几行代码
lines_after : 显示发生异常处的后几行代码
display_link :启用后,将在错误位置下方写入链接,VScode将允许您单击该链接。
separator_character :用于创建标题行的字符。默认情况下使用连字符。如果设置为 '' 或
者 None ,标题将被禁用。
display_timestamp :启用时,时间戳将写入回溯头中。

display_locals
启用后,将显示在顶部堆栈框架代码中的局部变量及其值。

display_trace_locals
启用后,其他堆栈框架代码中出现的局部变量将与它们的值一起显示。

5.3 设置怎么显示

:设置每行的长度,默认为0,表示每行的输出将与控制台尺寸相匹配,如果你设
line_length
置的长度将好与控制台宽度匹配,则可能需要禁用 full_line_newline ,以防止出现明显的双
换行符。

full_line_newline :当输出的字符满行时,是否要插入换行符。

timestamp_function
调用该函数以生成时间戳。默认值为 time.perf_counter 。

top_first
启用后,堆栈跟踪将反转,首先显示堆栈顶部。

display_arrow
启用后,将针对语法错误显示一个箭头,指向有问题的令牌。

truncate_code
启用后,每行代码将被截断以适合行长。

stack_depth
要显示的堆栈跟踪的最大条目数。什么时候 0 将显示整个堆栈,这是默认值。

exception_above
启用后,异常将显示在堆栈跟踪上方。


exception_below
启用后,异常显示在堆栈跟踪下方。
reset_stdout
启用后,重置转义序列将写入stdout和stderr;如果您的控制台留下错误的颜色,请启用此选
项。

filename_display

设置文件名的展示方式,有三个选项: pretty_errors.FILENAME_COMPACT 、
pretty_errors.FILENAME_EXTENDED ,或者 pretty_errors.FILENAME_FULL

以上,就是我对 pretty_errors 的使用体验,总的来说,这个库功能非常强大,使用效果也特别


酷炫,它就跟 PEP8 规范一样,没有它是可以,但是有了它会更好一样。对于某些想自定义错误输
出场景的人, pretty_errors 会是一个不错的解决方案,明哥把它推荐给你。

7.3 少有人知的 Python "重试机制"

为了避免由于一些网络或等其他不可控因素,而引起的功能性问题。比如在发送请求时,会因为网
络不稳定,往往会有请求超时的问题。

这种情况下,我们通常会在代码中加入重试的代码。重试的代码本身不难实现,但如何写得优雅、
易用,是我们要考虑的问题。

这里要给大家介绍的是一个第三方库 - Tenacity ,它实现了几乎我们可以使用到的所有重试场


景,比如:

1. 在什么情况下才进行重试?
2. 重试几次呢?
3. 重试多久后结束?
4. 每次重试的间隔多长呢?
5. 重试失败后的回调?

在使用它之前 ,先要安装它

$ pip install tenacity

最基本的重试

无条件重试,重试之间无间隔

from tenacity import retry

@retry
def test_retry():
print(" ...")
raise Exception
test_retry()

无条件重试,但是在重试之前要等待 2 秒

from tenacity import retry, wait_fixed

@retry(wait=wait_fixed(2))
def test_retry():
print(" ...")
raise Exception

test_retry()

设置停止基本条件

只重试7 次

from tenacity import retry, stop_after_attempt

@retry(stop=stop_after_attempt(7))
def test_retry():
print(" ...")
raise Exception

test_retry()

重试 10 秒后不再重试

from tenacity import retry, stop_after_delay

@retry(stop=stop_after_delay(10))
def test_retry():
print(" ...")
raise Exception

test_retry()

或者上面两个条件满足一个就结束重试

from tenacity import retry, stop_after_delay, stop_after_attempt

@retry(stop=(stop_after_delay(10) | stop_after_attempt(7)))
def test_retry():
print(" ...")
raise Exception
test_retry()

设置何时进行重试

在出现特定错误/异常(比如请求超时)的情况下,再进行重试

from requests import exceptions


from tenacity import retry, retry_if_exception_type

@retry(retry=retry_if_exception_type(exceptions.Timeout))
def test_retry():
print(" ...")
raise exceptions.Timeout

test_retry()

在满足自定义条件时,再进行重试。

如下示例,当 test_retry 函数返回值为 False 时,再进行重试

from tenacity import retry, stop_after_attempt, retry_if_result

def is_false(value):
return value is False

@retry(stop=stop_after_attempt(3),
retry=retry_if_result(is_false))
def test_retry():
return False

test_retry()

多个条件注意顺序

如果想对一个异常进行重试,但是最多重试3次。

下面这个代码是无效的,因为它会一直重试,重试三次的限制不会生效,因为它的条件是有顺序
的,在前面的条件会先被走到,就永远走不到后面的条件。

import time
from requests import exceptions
from tenacity import retry, retry_if_exception_type, stop_after_attempt

@retry(retry=retry_if_exception_type(exceptions.Timeout), stop=stop_after_attempt(3)
)
def test_retry():
time.sleep(1)
print("retry")
raise exceptions.Timeout

test_retry()

如果你把 stop_after_attempt 写到前边,就没有问题了。

import time
from requests import exceptions
from tenacity import retry, retry_if_exception_type, stop_after_attempt

@retry(stop=stop_after_attempt(5), retry=retry_if_exception_type(exceptions.Timeout)
)
def test_retry():
time.sleep(1)
print("retry")
raise exceptions.Timeout

test_retry()

重试后错误重新抛出

当出现异常后,tenacity 会进行重试,若重试后还是失败,默认情况下,往上抛出的异常会变成
RetryError,而不是最根本的原因。

因此可以加一个参数( reraise=True ),使得当重试失败后,往外抛出的异常还是原来的那个。

from tenacity import retry, stop_after_attempt

@retry(stop=stop_after_attempt(7), reraise=True)
def test_retry():
print(" ...")
raise Exception

test_retry()

设置回调函数

当最后一次重试失败后,可以执行一个回调函数

from tenacity import *


def return_last_value(retry_state):
print(" ")
return retry_state.outcome.result() #

def is_false(value):
return value is False

@retry(stop=stop_after_attempt(3),
retry_error_callback=return_last_value,
retry=retry_if_result(is_false))
def test_retry():
print(" ...")
return False

print(test_retry())

输出如下

...
...
...

False

7.4 规整字符串提取数据的神器

从一段指定的字符串中,取得期望的数据,正常人都会想到正则表达式吧?

写过正则表达式的人都知道,正则表达式入门不难,写起来也容易。

但是正则表达式几乎没有可读性可言,维护起来,真的会让人抓狂,别以为这段正则是你写的就可
以驾驭它,过个一个月你可能就不认识它了。

今天给你介绍一个好东西,可以让你在某些场景下摆脱正则的噩梦,那就是 Python 中一个非常冷


门的库 -- parse 。

1. 真实案例

拿一个最近使用 parse 的真实案例来举例说明。

下面是 ovs 一个条流表,现在我需要收集提取一个虚拟机(网口)里有多少流量、多少包流经了这


条流表。也就是每个 in_port 对应的 n_bytes、n_packets 的值 。

cookie=0x9816da8e872d717d, duration=298506.364s, table=0, n_packets=480, n_bytes=201


60, priority=10,ip,in_port="tapbbdf080b-c2" actions=NORMAL
如果是你,你会怎么做呢?

先以逗号分隔开来,再以等号分隔取出值来?

你不防可以尝试一下,写出来的代码应该和我想象的一样,没有一丝美感而言。

我来给你展示一下,我是怎么做的?

可以看到,我使用了一个叫做 parse 的第三方包,是需要自行安装的

$ python -m pip install parse

从上面这个案例中,你应该能感受到 parse 对于解析规范的字符串,是非常强大的。

2. parse 的结果

parse 的结果只有两种结果:

1. 没有匹配上,parse 的值为None

>>> parse("halo", "hello") is None


True
>>>

1. 如果匹配上,parse 的值则 为 Result 实例


>>> parse("hello", "hello world")
>>> parse("hello", "hello")
<Result () {}>
>>>

如果你编写的解析规则,没有为字段定义字段名,也就是匿名字段, Result 将是一个 类似 list 的


实例,演示如下:

>>> profile = parse("I am {}, {} years old, {}", "I am Jack, 27 years old, male")
>>> profile
<Result ('Jack', '27', 'male') {}>
>>> profile[0]
'Jack'
>>> profile[1]
'27'
>>> profile[2]
'male'

而如果你编写的解析规则,为字段定义了字段名, Result 将是一个 类似 字典 的实例,演示如下:

>>> profile = parse("I am {name}, {age} years old, {gender}", "I am Jack, 27 years o
ld, male")
>>> profile
<Result () {'gender': 'male', 'age': '27', 'name': 'Jack'}>
>>> profile['name']
'Jack'
>>> profile['age']
'27'
>>> profile['gender']
'male'

3. 重复利用 pattern

和使用 re 一样,parse 同样支持 pattern 复用。

>>> from parse import compile


>>>
>>> pattern = compile("I am {}, {} years old, {}")
>>> pattern.parse("I am Jack, 27 years old, male")
<Result ('Jack', '27', 'male') {}>
>>>
>>> pattern.parse("I am Tom, 26 years old, male")
<Result ('Tom', '26', 'male') {}>
4. 类型转化

从上面的例子中,你应该能注意到,parse 在获取年龄的时候,变成了一个 "27" ,这是一个字符


串,有没有一种办法,可以在提取的时候就按照我们的类型进行转换呢?

你可以这样写。

>>> from parse import parse


>>> profile = parse("I am {name}, {age:d} years old, {gender}", "I am Jack, 27 years
old, male")
>>> profile
<Result () {'gender': 'male', 'age': 27, 'name': 'Jack'}>
>>> type(profile["age"])
<type 'int'>

除了将其转为 整型,还有其他格式吗?

内置的格式还有很多,比如

匹配时间

>>> parse('Meet at {:tg}', 'Meet at 1/2/2011 11:00 PM')


<Result (datetime.datetime(2011, 2, 1, 23, 0),) {}>

更多类型请参考官方文档:

Type Characters Matched Output

l Letters (ASCII) str

w Letters, numbers and underscore str

W Not letters, numbers and underscore str

s Whitespace str

S Non-whitespace str

d Digits (effectively integer numbers) int

D Non-digit str

n Numbers with thousands separators (, or .) int

% Percentage (converted to value/100.0) float

f Fixed-point numbers float


F Decimal numbers Decimal

Floating-point numbers with exponent e.g. 1.1e-10, NAN (all case


e float
insensitive)

g General number format (either d, f or e) float

b Binary numbers int

o Octal numbers int

x Hexadecimal numbers (lower and upper case) int

ISO 8601 format date/time e.g. 1972-01-20T10:21:36Z (“T” and “Z”


ti datetime
optional)

te RFC2822 e-mail format date/time e.g. Mon, 20 Jan 1972 10:21:36 +1000 datetime

tg Global (day/month) format date/time e.g. 20/1/1972 10:21:36 AM +1:00 datetime

ta US (month/day) format date/time e.g. 1/20/1972 10:21:36 PM +10:30 datetime

tc ctime() format date/time e.g. Sun Sep 16 01:03:52 1973 datetime

th HTTP log format date/time e.g. 21/Nov/2011:00:07:11 +0000 datetime

ts Linux system log format date/time e.g. Nov 9 03:37:44 datetime

tt Time e.g. 10:21:36 PM -5:30 time

5. 提取时去除空格

去除两边空格

>>> parse('hello {} , hello python', 'hello world , hello python')


<Result (' world ',) {}>
>>>
>>>
>>> parse('hello {:^} , hello python', 'hello world , hello python')
<Result ('world',) {}>

去除左边空格

>>> parse('hello {:>} , hello python', 'hello world , hello python')


<Result ('world ',) {}>

去除右边空格
>>> parse('hello {:<} , hello python', 'hello world , hello python')
<Result (' world',) {}>

6. 大小写敏感开关

Parse 默认是大小写不敏感的,你写 hello 和 HELLO 是一样的。

如果你需要区分大小写,那可以加个参数,演示如下:

>>> parse('SPAM', 'spam')


<Result () {}>
>>> parse('SPAM', 'spam') is None
False
>>> parse('SPAM', 'spam', case_sensitive=True) is None
True

7. 匹配字符数

精确匹配:指定最大字符数

>>> parse('{:.2}{:.2}', 'hello') #


>>>
>>> parse('{:.2}{:.2}', 'hell') #
<Result ('he', 'll') {}>

模糊匹配:指定最小字符数

>>> parse('{:.2}{:2}', 'hello')


<Result ('h', 'ello') {}>
>>>
>>> parse('{:2}{:2}', 'hello')
<Result ('he', 'llo') {}>

若要在精准/模糊匹配的模式下,再进行格式转换,可以这样写

>>> parse('{:2}{:2}', '1024')


<Result ('10', '24') {}>
>>>
>>>
>>> parse('{:2d}{:2d}', '1024')
<Result (10, 24) {}>
8. 三个重要属性

Parse 里有三个非常重要的属性

fixed:利用位置提取的匿名字段的元组
named:存放有命名的字段的字典
spans:存放匹配到字段的位置

下面这段代码,带你了解他们之间有什么不同

>>> profile = parse("I am {name}, {age:d} years old, {}", "I am Jack, 27 years old,
male")
>>> profile.fixed
('male',)
>>> profile.named
{'age': 27, 'name': 'Jack'}
>>> profile.spans
{0: (25, 29), 'age': (11, 13), 'name': (5, 9)}
>>>

9. 自定义类型的转换

匹配到的字符串,会做为参数传入对应的函数

比如我们之前讲过的,将字符串转整型

>>> parse("I am {:d}", "I am 27")


<Result (27,) {}>
>>> type(_[0])
<type 'int'>
>>>

其等价于

>>> def myint(string):


... return int(string)
...
>>>
>>>
>>> parse("I am {:myint}", "I am 27", dict(myint=myint))
<Result (27,) {}>
>>> type(_[0])
<type 'int'>
>>>
利用它,我们可以定制很多的功能,比如我想把匹配的字符串弄成全大写

>>> def shouty(string):


... return string.upper()
...
>>> parse('{:shouty} world', 'hello world', dict(shouty=shouty))
<Result ('HELLO',) {}>
>>>

10 总结一下

parse 库在字符串解析处理场景中提供的便利,肉眼可见,上手简单。

在一些简单的场景中,使用 parse 可比使用 re 去写正则开发效率不知道高几个 level,用它写出来


的代码富有美感,可读性高,后期维护起代码来一点压力也没有,推荐你使用。

7.5 一行代码让代码运行速度提高100倍

python一直被病垢运行速度太慢,但是实际上python的执行效率并不慢,慢的是python用的解释器
Cpython运行效率太差。

“一行代码让python的运行速度提高100倍”这绝不是哗众取宠的论调。

我们来看一下这个最简单的例子,从1一直累加到1亿。

最原始的代码:

import time
def foo(x,y):
tt = time.time()
s = 0
for i in range(x,y):
s += i
print('Time used: {} sec'.format(time.time()-tt))
return s

print(foo(1,100000000))

结果:

Time used: 6.779874801635742 sec


4999999950000000

我们来加一行代码,再看看结果:
from numba import jit
import time
@jit
def foo(x,y):
tt = time.time()
s = 0
for i in range(x,y):
s += i
print('Time used: {} sec'.format(time.time()-tt))
return s
print(foo(1,100000000))

结果:

Time used: 0.04680037498474121 sec


4999999950000000

是不是快了100多倍呢?

那么下面就分享一下“为啥numba库的jit模块那么牛掰?”

NumPy的创始人Travis Oliphant在离开Enthought之后,创建了CONTINUUM,致力于将Python大数
据处理方面的应用。最近推出的Numba项目能够将处理NumPy数组的Python函数JIT编译为机器码
执行,从而上百倍的提高程序的运算速度。

Numba项目的主页上有Linux下的详细安装步骤。编译LLVM需要花一些时间。Windows用户可以从
Unofficial Windows Binaries for Python Extension Packages下载安装LLVMPy、meta和numba等几个
扩展库。

下面我们看一个例子:

import numba as nb
from numba import jit

@jit('f8(f8[:])')
def sum1d(array):
s = 0.0
n = array.shape[0]
for i in range(n):
s += array[i]
return s

import numpy as np
array = np.random.random(10000)
%timeit sum1d(array)
%timeit np.sum(array)
%timeit sum(array)
10000 loops, best of 3: 38.9 us per loop
10000 loops, best of 3: 32.3 us per loop
100 loops, best of 3: 12.4 ms per loop

numba中提供了一些修饰器,它们可以将其修饰的函数JIT编译成机器码函数,并返回一个可在
Python中调用机器码的包装对象。为了能将Python函数编译成能高速执行的机器码,我们需要告诉
JIT编译器函数的各个参数和返回值的类型。我们可以通过多种方式指定类型信息,在上面的例子
中,类型信息由一个字符串’f8(f8[:])’指定。其中’f8’表示8个字节双精度浮点数,括号前面的’f8’表示
返回值类型,括号里的表示参数类型,’[:]’表示一维数组。因此整个类型字符串表示sum1d()是一个
参数为双精度浮点数的一维数组,返回值是一个双精度浮点数。需要注意的是,JIT所产生的函数
只能对指定的类型的参数进行运算:

print sum1d(np.ones(10, dtype=np.int32))


print sum1d(np.ones(10, dtype=np.float32))
print sum1d(np.ones(10, dtype=np.float64))
1.2095376009e-312
1.46201599944e+185
10.0

如果希望JIT能针对所有类型的参数进行运算,可以使用autojit:

from numba import autojit


@autojit
def sum1d2(array):
s = 0.0
n = array.shape[0]
for i in range(n):
s += array[i]
return s

%timeit sum1d2(array)
print sum1d2(np.ones(10, dtype=np.int32))
print sum1d2(np.ones(10, dtype=np.float32))
print sum1d2(np.ones(10, dtype=np.float64))
10000 loops, best of 3: 143 us per loop
10.0
10.0
10.0

autoit虽然可以根据参数类型动态地产生机器码函数,但是由于它需要每次检查参数类型,因此计
算速度也有所降低。numba的用法很简单,基本上就是用jit和autojit这两个修饰器,和一些类型对
象。下面的程序列出numba所支持的所有类型:

print [obj for obj in nb.__dict__.values() if isinstance(obj, nb.minivect.minitypes.


Type)]
[size_t, Py_uintptr_t, uint16, complex128, float, complex256, void, int , long doubl
e,
unsigned PY_LONG_LONG, uint32, complex256, complex64, object_, npy_intp, const char
*,
double, unsigned short, float, object_, float, uint64, uint32, uint8, complex128, ui
nt16,
int, int , uint8, complex64, int8, uint64, double, long double, int32, double, long
double,
char, long, unsigned char, PY_LONG_LONG, int64, int16, unsigned long, int8, int16, i
nt32,
unsigned int, short, int64, Py_ssize_t]

工作原理
numba的通过meta模块解析Python函数的ast语法树,对各个变量添加相应的类型信息。然后调用
llvmpy生成机器码,最后再生成机器码的Python调用接口。

meta模块

通过研究numba的工作原理,我们可以找到许多有用的工具。例如meta模块可在程序源码、ast语
法树以及Python二进制码之间进行相互转换。下面看一个例子:

def add2(a, b):


return a + b

decompile_func能将函数的代码对象反编译成ast语法树,而str_ast能直观地显示ast语法树,使用这
两个工具学习Python的ast语法树是很有帮助的。

from meta.decompiler import decompile_func


from meta.asttools import str_ast
print str_ast(decompile_func(add2))
FunctionDef(args=arguments(args=[Name(ctx=Param(),
id='a'),
Name(ctx=Param(),
id='b')],
defaults=[],
kwarg=None,
vararg=None),
body=[Return(value=BinOp(left=Name(ctx=Load(),
id='a'),
op=Add(),
right=Name(ctx=Load(),
id='b')))],
decorator_list=[],
name='add2')

而python_source可以将ast语法树转换为Python源代码:
from meta.asttools import python_source
python_source(decompile_func(add2))
def add2(a, b):
return (a + b)

decompile_pyc将上述二者结合起来,它能将Python编译之后的pyc或者pyo文件反编译成源代码。
下面我们先写一个tmp.py文件,然后通过py_compile将其编译成tmp.pyc。

with open("tmp.py", "w") as f:


f.write("""
def square_sum(n):
s = 0
for i in range(n):
s += i**2
return s
""")
import py_compile
py_compile.compile("tmp.py")

下面调用decompile_pyc将tmp.pyc显示为源代码:

with open("tmp.pyc", "rb") as f:


decompile_pyc(f)
def square_sum(n):
s = 0
for i in range(n):
s += (i ** 2)
return s

llvmpy模块

LLVM是一个动态编译器,llvmpy则可以通过Python调用LLVM动态地创建机器码。直接通过llvmpy
创建机器码是比较繁琐的,例如下面的程序创建一个计算两个整数之和的函数,并调用它计算结
果。

from llvm.core import Module, Type, Builder


from llvm.ee import ExecutionEngine, GenericValue

## Create a new module with a function implementing this:


#
## int add(int a, int b) {
## return a + b;
## }
#
my_module = Module.new('my_module')
ty_int = Type.int()
ty_func = Type.function(ty_int, [ty_int, ty_int])
f_add = my_module.add_function(ty_func, "add")
f_add.args[0].name = "a"
f_add.args[1].name = "b"
bb = f_add.append_basic_block("entry")

## IRBuilder for our basic block


builder = Builder.new(bb)
tmp = builder.add(f_add.args[0], f_add.args[1], "tmp")
builder.ret(tmp)

## Create an execution engine object. This will create a JIT compiler


## on platforms that support it, or an interpreter otherwise
ee = ExecutionEngine.new(my_module)

## Each argument needs to be passed as a GenericValue object, which is a kind


## of variant
arg1 = GenericValue.int(ty_int, 100)
arg2 = GenericValue.int(ty_int, 42)

## Now let's compile and run!


retval = ee.run_function(f_add, [arg1, arg2])

## The return value is also GenericValue. Let's print it.


print "returned", retval.as_int()
returned 142

f_add就是一个动态生成的机器码函数,我们可以把它想象成C语言编译之后的函数。在上面的程序
中,我们通过ee.run_function调用此函数,而实际上我们还可以获得它的地址,然后通过Python的
ctypes模块调用它。

首先通过ee.get_pointer_to_function获得f_add函数的地址:

addr = ee.get_pointer_to_function(f_add)
addr
2975997968L

然后通过ctypes.PYFUNCTYPE创建一个函数类型:

import ctypes
f_type = ctypes.PYFUNCTYPE(ctypes.c_int, ctypes.c_int, ctypes.c_int)

最后通过f_type将函数的地址转换为可调用的Python函数,并调用它:

f = f_type(addr)
f(100, 42)
142
numba所完成的工作就是:解析Python函数的ast语法树并加以改造,添加类型信息;将带类型信
息的ast语法树通过llvmpy动态地转换为机器码函数,然后再通过和ctypes类似的技术为机器码函数
创建包装函数供Python调用。

7.6 新一代的调试神器:PySnooper

对于每个程序开发者来说,调试几乎是必备技能。

代码写到一半卡住了,不知道这个函数执行完的返回结果是怎样的?调试一下看看

代码运行到一半报错了,什么情况?怎么跟预期的不一样?调试一下看看

调试的方法多种多样,不同的调试方法适合不同的场景和人群。

如果你是刚接触编程的小萌新,对很多工具的使用还不是很熟练,那么 print 和 log 大法好


如果你在本地(Win或者Mac)电脑上开发,那么 IDE 的图形化界面调试无疑是最适合的;
如果你在服务器上排查BUG,那么使用 PDB 进行无图形界面的调试应该是首选;
如果你要在本地进行开发,但是项目的进行需要依赖复杂的服务器环境,那么可以了解下
PyCharm 的远程调试

除了以上,今天明哥再给你介绍一款非常好用的调试工具,它能在一些场景下,大幅度提高调试的
效率, 那就是 PySnooper ,它在 Github 上已经收到了 13k 的 star,获得大家的一致好评。

有了这个工具后,就算是小萌新也可以直接无门槛上手,从此与 print 说再见~

1. 快速安装

执行下面这些命令进行安装 PySnooper
$ python3 -m pip install pysnooper

##
$ conda install -c conda-forge pysnooper

##
$ yay -S python-pysnooper

2. 简单案例

下面这段代码,定义了一个 demo_func 的函数,在里面生成一个 profile 的字典变量,然后去更新


它,最后返回。

代码本身没有什么实际意义,但是用来演示 PySnooper 已经足够。

import pysnooper

@pysnooper.snoop()
def demo_func():
profile = {}
profile["name"] = " "
profile["age"] = 27
profile["gender"] = "male"

return profile

def main():
profile = demo_func()

main()

现在我使用终端命令行的方式来运行它

[root@iswbm ~]# python3 demo.py


Source path:... demo.py
17:52:49.624943 call 4 def demo_func():
17:52:49.625124 line 5 profile = {}
New var:....... profile = {}
17:52:49.625156 line 6 profile["name"] = " "
Modified var:.. profile = {'name': ' '}
17:52:49.625207 line 7 profile["age"] = 27
Modified var:.. profile = {'name': ' ', 'age': 27}
17:52:49.625254 line 8 profile["gender"] = "male"
Modified var:.. profile = {'name': ' ', 'age': 27, 'gender': 'male'}
17:52:49.625306 line 10 return profile
17:52:49.625344 return 10 return profile
Return value:.. {'name': ' ', 'age': 27, 'gender': 'male'}
Elapsed time: 00:00:00.000486

可以看到 PySnooper 把函数运行的过程全部记录了下来,包括:

代码的片段、行号等信息,以及每一行代码是何时调用的?
函数内局部变量的值如何变化的?何时新增了变量,何时修改了变量。
函数的返回值是什么?
运行函数消耗了多少时间?

而作为开发者,要得到这些如此详细的调试信息,你需要做的非常简单,只要给你想要调试的函数
上带上一顶帽子(装饰器) -- @pysnooper.snoop() 即可。

3. 详细使用

2.1 重定向到日志文件

@pysnooper.snoop() 不加任何参数时,会默认将调试的信息输出到标准输出。

对于单次调试就能解决的 BUG ,这样没有什么问题,但是有一些 BUG 只有在特定的场景下才会出


现,需要你把程序放在后面跑个一段时间才能复现。

这种情况下,你可以将调试信息重定向输出到某一日志文件中,方便追溯排查。

@pysnooper.snoop(output='/var/log/debug.log')
def demo_func():
...

2.2 跟踪非局部变量值

PySnooper 是以函数为单位进行调试的,它默认只会跟踪函数体内的局部变量,若想跟踪全局变
量,可以给 @pysnooper.snoop() 加上 watch 参数

out = {"foo": "bar"}

@pysnooper.snoop(watch=('out["foo"]'))
def demo_func():
...

如此一来,PySnooper 会在 out["foo"] 值有变化时,也将其打印出来


watch 参数,接收一个可迭代对象(可以是list 或者 tuple),里面的元素为字符串表达式,什么意
思呢?看下面例子就知道了

@pysnooper.snoop(watch=('out["foo"]', 'foo.bar', 'self.foo["bar"]'))


def demo_func():
...

和 watch 相对的, pysnooper.snoop() 还可以接收一个函数 watch_explode ,表示除了这几个参


数外的其他所有全局变量都监控。

@pysnooper.snoop(watch_explode=('foo', 'bar'))
def demo_func():
...

2.3 设置跟踪函数的深度

当你使用 PySnooper 调试某个函数时,若该函数中还调用了其他函数,PySnooper 是不会傻傻的跟


踪进去的。

如果你想继续跟踪该函数中调用的其他函数,可以通过指定 depth 参数来设置跟踪深度(不指定


的话默认为 1)。

@pysnooper.snoop(depth=2)
def demo_func():
...

2.4 设置调试日志的前缀
当你在使用 PySnooper 跟踪多个函数时,调试的日志会显得杂乱无章,不方便查看。

在这种情况下,PySnooper 提供了一个参数,方便你为不同的函数设置不同的标志,方便你在查看
日志时进行区分。

@pysnooper.snoop(output="/var/log/debug.log", prefix="demo_func: ")


def demo_func():
...

效果如下

2.5 设置最大的输出长度

默认情况下,PySnooper 输出的变量和异常信息,如果超过 100 个字符,被会截断为 100 个字


符。

当然你也可以通过指定参数 进行修改

@pysnooper.snoop(max_variable_length=200
def demo_func():
...

您也可以使用max_variable_length=None它从不截断它们。

@pysnooper.snoop(max_variable_length=None
def demo_func():
...
2.6 支持多线程调试模式

PySnooper 同样支持多线程的调试,通过设置参数 thread_info=True ,它就会在日志中打印出是


在哪个线程对变量进行的修改。

@pysnooper.snoop(thread_info=True)
def demo_func():
...

效果如下

2.7 自定义对象的格式输出

pysnooper.snoop() 函数有一个参数是 custom_repr ,它接收一个元组对象。

在这个元组里,你可以指定特定类型的对象以特定格式进行输出。

这边我举个例子。

假如我要跟踪 person 这个 Person 类型的对象,由于它不是常规的 Python 基础类型,PySnooper


是无法正常输出它的信息的。

因此我在 pysnooper.snoop() 函数中设置了 custom_repr 参数,该参数的第一个元素为 Person,


第二个元素为 print_persion_obj 函数。

PySnooper 在打印对象的调试信息时,会逐个判断它是否是 Person 类型的对象,若是,就将该对


象传入 print_persion_obj 函数中,由该函数来决定如何显示这个对象的信息。
class Person:pass

def print_person_obj(obj):
return f"<Person {obj.name} {obj.age} {obj.gender}>"

@pysnooper.snoop(custom_repr=(Person, print_person_obj))
def demo_func():
...

完整的代码如下

import pysnooper

class Person:pass

def print_person_obj(obj):
return f"<Person {obj.name} {obj.age} {obj.gender}>"

@pysnooper.snoop(custom_repr=(Person, print_person_obj))
def demo_func():
person = Person()
person.name = " "
person.age = 27
person.gender = "male"

return person

def main():
profile = demo_func()

main()

运行一下,观察一下效果。
如果你要自定义格式输出的有很多个类型,那么 custom_repr 参数的值可以这么写

@pysnooper.snoop(custom_repr=((Person, print_person_obj), (numpy.ndarray, print_ndar


ray)))
def demo_func():
...

还有一点我提醒一下,元组的第一个元素可以是类型(如类名Person 或者其他基础类型 list等),


也可以是一个判断对象类型的函数。

也就是说,下面三种写法是等价的。

##
@pysnooper.snoop(custom_repr=(Person, print_persion_obj))
def demo_func():
...

##
def is_persion_obj(obj):
return isinstance(obj, Person)

@pysnooper.snoop(custom_repr=(is_persion_obj, print_persion_obj))
def demo_func():
...

##
@pysnooper.snoop(custom_repr=(lambda obj: isinstance(obj, Person), print_persion_obj
))
def demo_func():
...

以上就是明哥今天给大家介绍的一款调试神器( PySnooper ) 的详细使用手册,是不是觉得还不


错?

7.7 比open更好用、更优雅的读取文件

使用 open 函数去读取文件,似乎是所有 Python 工程师的共识。

今天明哥要给大家推荐一个比 open 更好用、更优雅的读取文件方法 -- 使用 fileinput

fileinput 是 Python 的内置模块,但我相信,不少人对它都是陌生的。今天我把 fileinput 的所有的


用法、功能进行详细的讲解,并列举了一些非常实用的案例,对于理解和使用它可以说完全没有问
题。

1. 从标准输入中读取

当你的 Python 脚本没有传入任何参数时,fileinput 默认会以 stdin 作为输入源

## demo.py
import fileinput

for line in fileinput.input():


print(line)

效果如下,不管你输入什么,程序会自动读取并再打印一次,像个复读机似的。

$ python demo.py
hello
hello

python
python

2. 单独打开一个文件

单独打开一个文件,只需要在 files 中输入一个文件名即可

import fileinput

with fileinput.input(files=('a.txt',)) as file:


for line in file:
print(f'{fileinput.filename()} {fileinput.lineno()} : {line}', end='')
其中 a.txt 的内容如下

hello
world

执行后就会输出如下

$ python demo.py
a.txt 1 : hello
a.txt 2 : world

需要说明的一点是, fileinput.input() 默认使用 mode='r' 的模式读取文件,如果你的文件是


二进制的,可以使用 mode='rb' 模式。fileinput 有且仅有这两种读取模式。

3. 批量打开多个文件

从上面的例子也可以看到,我在 fileinput.input 函数中传入了 files 参数,它接收一个包含多


个文件名的列表或元组,传入一个就是读取一个文件,传入多件就是读取多个文件。

import fileinput

with fileinput.input(files=('a.txt', 'b.txt')) as file:


for line in file:
print(f'{fileinput.filename()} {fileinput.lineno()} : {line}', end='')

a.txt 和 b.txt 的内容分别是

$ cat a.txt
hello
world
$ cat b.txt
hello
python

运行后输出结果如下,由于 a.txt 和 b.txt 的内容被整合成一个文件对象 file ,因此


fileinput.lineno() 只有在读取一个文件时,才是原文件中真实的行号。

$ python demo.py
a.txt 1 : hello
a.txt 2 : world
b.txt 3 : hello
b.txt 4 : python
如果想要在读取多个文件的时候,也能读取原文件的真实行号,可以使用
fileinput.filelineno() 方法

import fileinput

with fileinput.input(files=('a.txt', 'b.txt')) as file:


for line in file:
print(f'{fileinput.filename()} {fileinput.filelineno()} : {line}', end='')

运行后,输出如下

$ python demo.py
a.txt 1 : hello
a.txt 2 : world
b.txt 1 : hello
b.txt 2 : python

这个用法和 glob 模块简直是绝配

import fileinput
import glob

for line in fileinput.input(glob.glob("*.txt")):


if fileinput.isfirstline():
print('-'*20, f'Reading {fileinput.filename()}...', '-'*20)
print(str(fileinput.lineno()) + ': ' + line.upper(), end="")

运行效果如下

$ python demo.py
-------------------- Reading b.txt... --------------------
1: HELLO
2: PYTHON
-------------------- Reading a.txt... --------------------
3: HELLO
4: WORLD

4. 读取的同时备份文件

fileinput.input 有一个 backup 参数,你可以指定备份的后缀名,比如 .bak

import fileinput
with fileinput.input(files=("a.txt",), backup=".bak") as file:
for line in file:
print(f'{fileinput.filename()} {fileinput.lineno()} : {line}', end='')

运行的结果如下,会多出一个 a.txt.bak 文件

$ ls -l a.txt*
-rw-r--r-- 1 MING staff 12 2 27 10:43 a.txt

$ python demo.py
a.txt 1 : hello
a.txt 2 : world

$ ls -l a.txt*
-rw-r--r-- 1 MING staff 12 2 27 10:43 a.txt
-rw-r--r-- 1 MING staff 42 2 27 10:39 a.txt.bak

5. 标准输出重定向替换

fileinput.input 有一个 inplace 参数,表示是否将标准输出的结果写回文件,默认不取代

请看如下一段测试代码

import fileinput

with fileinput.input(files=("a.txt",), inplace=True) as file:


print("[INFO] task is started...")
for line in file:
print(f'{fileinput.filename()} {fileinput.lineno()} : {line}', end='')
print("[INFO] task is closed...")

运行后,会发现在 for 循环体内的 print 内容会写回到原文件中了。而在 for 循环体外的 print 则没


有变化。

$ cat a.txt
hello
world

$ python demo.py
[INFO] task is started...
[INFO] task is closed...

$ cat a.txt
a.txt 1 : hello
a.txt 2 : world
利用这个机制,可以很容易的实现文本替换。

import sys
import fileinput

for line in fileinput.input(files=('a.txt', ), inplace=True):


# Windows/DOS Linux
if line[-2:] == "\r\n":
line = line + "\n"
sys.stdout.write(line)

附:如何实现 DOS 和 UNIX 格式互换以供程序测试,使用 vim 输入如下指令即可

DOS UNIX :setfileformat=unix


UNIX DOS :setfileformat=dos

6. 不得不介绍的方法

如果只是想要 fileinput 当做是替代 open 读取文件的工具,那么以上的内容足以满足你的要


求。

fileinput.filenam()
返回当前被读取的文件名。 在第一行被读取之前,返回 None 。

fileinput.fileno()
返回以整数表示的当前文件“文件描述符”。 当未打开文件时(处在第一行和文件之间),返回
-1 。

fileinput.lineno()
返回已被读取的累计行号。 在第一行被读取之前,返回 0 。 在最后一个文件的最后一行被读
取之后,返回该行的行号。

fileinput.filelineno()
返回当前文件中的行号。 在第一行被读取之前,返回 0 。 在最后一个文件的最后一行被读取
之后,返回此文件中该行的行号。

但若要想基于 fileinput 来做一些更加复杂的逻辑,也许你会需要用到如下这几个方法

fileinput.isfirstline() 如果刚读取的行是其所在文件的第一行则返回 True ,否则返回


False 。
fileinput.isstdin() 如果最后读取的行来自 sys.stdin 则返回 True ,否则返回 False 。
fileinput.nextfile() 关闭当前文件以使下次迭代将从下一个文件(如果存在)读取第一行;
不是从该文件读取的行将不会被计入累计行数。 直到下一个文件的第一行被读取之后文件名才
会改变。 在第一行被读取之前,此函数将不会生效;它不能被用来跳过第一个文件。 在最后一
个文件的最后一行被读取之后,此函数将不再生效。
fileinput.close() 关闭序列。

7. 进阶一点的玩法

在 fileinput.input() 中有一个 openhook 的参数,它支持用户传入自定义的对象读取方法。

若你没有传入任何的勾子,fileinput 默认使用的是 open 函数。

fileinput 为我们内置了两种勾子供你使用

1. fileinput.hook_compressed(*filename*, *mode*)

使用 gzip 和 bz2 模块透明地打开 gzip 和 bzip2 压缩的文件(通过扩展名 '.gz' 和 '.bz2'


来识别)。 如果文件扩展名不是 '.gz' 或 '.bz2' ,文件会以正常方式打开(即使用 open()
并且不带任何解压操作)。使用示例:
fi = fileinput.FileInput(openhook=fileinput.hook_compressed)

2. fileinput.hook_encoded(*encoding*, *errors=None*)

返回一个通过 open() 打开每个文件的钩子,使用给定的 encoding 和 errors 来读取文件。使用


示例:
fi = fileinput.FileInput(openhook=fileinput.hook_encoded("utf-8",
"surrogateescape"))

如果你自己的场景比较特殊,以上的三种勾子都不能满足你的要求,你也可以自定义。

这边我举个例子来抛砖引玉下

假如我想要使用 fileinput 来读取网络上的文件,可以这样定义勾子。

1. 先使用 requests 下载文件到本地


2. 再使用 open 去读取它

def online_open(url, mode):


import requests
r = requests.get(url)
filename = url.split("/")[-1]
with open(filename,'w') as f1:
f1.write(r.content.decode("utf-8"))
f2 = open(filename,'r')
return f2

直接将这个函数传给 openhook 即可

import fileinput

file_url = 'https://www.csdn.net/robots.txt'
with fileinput.input(files=(file_url,), openhook=online_open) as file:
for line in file:
print(line, end="")

运行后按预期一样将 CSDN 的 robots 的文件打印了出来

User-agent: *
Disallow: /scripts
Disallow: /public
Disallow: /css/
Disallow: /images/
Disallow: /content/
Disallow: /ui/
Disallow: /js/
Disallow: /scripts/
Disallow: /article_preview.html*
Disallow: /tag/
Disallow: /*?*
Disallow: /link/

Sitemap: https://www.csdn.net/sitemap-aggpage-index.xml
Sitemap: https://www.csdn.net/article/sitemap.txt

8. 列举一些实用案例

案例一:读取一个文件所有行

import fileinput
for line in fileinput.input('data.txt'):
print(line, end="")

案例二:读取多个文件所有行

import fileinput
import glob

for line in fileinput.input(glob.glob("*.txt")):


if fileinput.isfirstline():
print('-'*20, f'Reading {fileinput.filename()}...', '-'*20)
print(str(fileinput.lineno()) + ': ' + line.upper(), end="")

案例三:利用fileinput将CRLF文件转为LF

import sys
import fileinput

for line in fileinput.input(files=('a.txt', ), inplace=True):


# Windows/DOS Linux
if line[-2:] == "\r\n":
line = line + "\n"
sys.stdout.write(line)

案例四:配合 re 做日志分析:取所有含日期的行

#-- -- error.log
aaa
1970-01-01 13:45:30 Error: **** Due to System Disk spacke not enough...
bbb
1970-01-02 10:20:30 Error: **** Due to System Out of Memory...
ccc

#--- ---
import re
import fileinput
import sys

pattern = '\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}'

for line in fileinput.input('error.log',backup='.bak',inplace=1):


if re.search(pattern,line):
sys.stdout.write("=> ")
sys.stdout.write(line)

#--- ---
=> 1970-01-01 13:45:30 Error: **** Due to System Disk spacke not enough...
=> 1970-01-02 10:20:30 Error: **** Due to System Out of Memory...

案例五:利用fileinput实现类似于grep的功能

import sys
import re
import fileinput

pattern= re.compile(sys.argv[1])
for line in fileinput.input(sys.argv[2]):
if pattern.match(line):
print(fileinput.filename(), fileinput.filelineno(), line)

$ ./demo.py import.*re *.py


# py import re
addressBook.py 2 import re
addressBook1.py 10 import re
addressBook2.py 18 import re
test.py 238 import re

9. 写在最后

fileinput 是 Python 的内置模块,但我相信,不少人对它都是陌生的。今天我把 fileinput 的所有的


用法、功能进行详细的讲解,并列举了一些非常实用的案例,对于理解和使用它可以说完全没有问
题。

fileinput 是对 open 函数的再次封装,在仅需读取数据的场景中, fileinput 显然比 open 做得更专


业、更人性,当然在其他有写操作的复杂场景中,fileinput 就无能为力啦,本身从 fileinput 的命名
上就知道这个模块只专注于输入(读)而不是输出(写)。

7.8 像操作路径一样,操作嵌套字典

在使用前先安装它,要注意的是该模块只能在 Python 3.8+ 中使用

$ python3 -m pip install dpath

下边是一个简单的使用案例

import dpath.util

data = {
"foo": {
"bar": {
"a": 10,
"b": 20,
"c": [],
"d": ['red', 'buggy', 'bumpers'],
}
}
}
print(dpath.util.get(data, "/foo/bar/d"))

使用 [ab] 会把 键为 a 和 b 的都筛选出来

print(dpath.util.search(data, "/foo/bar/[ab]"))
## output: {'foo': {'bar': {'a': 10, 'b': 20}}}

获取所有匹配的键值对的 value 值列表

print(dpath.util.values(data, "/foo/bar/*"))
## output: [10, 20, [], ['red', 'buggy', 'bumpers']]

更多案例,请前往 官方文档 查阅。

7.9 读取文件中任意行的数据

linecache 是 Python 中的一个内置模块。

它允许从任何文件中获取任意行,同时尝试使用缓存进行内部优化,这是一种常见的情况,即从单
个文件读取多行。它被 traceback 模块用来检索包含在格式化回溯中的源代码行。

这是一个简单的例子。

>>> import linecache


>>> linecache.getline('/etc/passwd', 4)
'sys:x:3:3:sys:/dev:/bin/sh\n'

如果你指定的行数超过了文件原有的行数,该函数也不会抛出错误,而是返回空字符串。

>>> import linecache


>>> linecache.getline('/etc/passwd', 10000)

>>>

7.10 让你的装饰器写得更轻松的神库

本篇文章会为你介绍的是一个已经存在十三年,但是依旧不红的库 decorator,好像很少有人知道
他的存在一样。

这个库可以帮你做什么呢 ?

其实很简单,就是可以帮你更方便地写python装饰器代码,更重要的是,它让 Python 中被装饰器


装饰后的方法长得更像装饰前的方法。

本篇文章不会过多的向你介绍装饰器的基本知识,我会默认你知道什么是装饰器,并且懂得如何写
一个简单的装饰器。

不了解装饰器的可以先去阅读我之前写的文章,非常全且详细的介绍了装饰器的各种实现方法。

1. 常规的装饰器

下面这是一个最简单的装饰器示例,在运行 myfunc 函数的前后都会打印一条日志。

def deco(func):
def wrapper(*args, **kw):
print("Ready to run task")
func(*args, **kw)
print("Successful to run task")
return wrapper

@deco
def myfunc():
print("Running the task")

myfunc()

装饰器使用起来,似乎有些高端和魔幻,对于一些重复性的功能,往往我们会封装成一个装饰器函
数。

在定义一个装饰器的时候,我们都需要像上面一样机械性的写一个嵌套的函数,对装饰器原理理解
不深的初学者,往往过段时间就会忘记如何定义装饰器。

有一些比较聪明的同学,会利用 PyCharm 来自动生成装饰器模板


然后要使用的时候,直接敲入 deco 就会生成一个简单的生成器代码,提高编码的准备效率
该图为GIF,请前往 magic.iswbm.com 浏览

2. 使用神库

使用 PyCharm 的 Live Template ,虽然能降低编写装饰器的难度,但却要依赖 PyCharm 这一专业


的代码编辑器。

这里,明哥要教你一个更加简单的方法,使用这个方法呢,你需要先安装一个库 : decorator ,
使用 pip 可以很轻易地去安装它

$ python3 -m pip install decorator

从库的名称不难看出,这是一个专门用来解决装饰器问题的第三方库。

有了它之后,你会惊奇的发现,以后自己定义的装饰器,就再也不需要写嵌套的函数了

from decorator import decorator

@decorator
def deco(func, *args, **kw):
print("Ready to run task")
func(*args, **kw)
print("Successful to run task")

@deco
def myfunc():
print("Running the task")

myfunc()

deco 作为装饰函数,第一个参数是固定的,都是指被装饰函数,而后面的参数都固定使用 可变参


数 *args 和 **kw 的写法,代码被装饰函数的原参数。

这种写法,不得不说,更加符合直觉,代码的逻辑也更容易理解。

3. 带参数的装饰器可用?

装饰器根据有没有携带参数,可以分为两种

第一种:不带参数,最简单的示例,上面已经举例

def decorator(func):
def wrapper(*args, **kw):
func(*args, **kw)
return wrapper

第二种:带参数,这就相对复杂了,理解起来了也不是那么容易。

def decorator(arg1, arg2):


def wrapper(func):
def deco(*args, **kwargs)
func(*args, **kwargs)
return deco
return wrapper

那么对于需要带参数的装饰器, decorator 是否也一样能很好的支持呢?

下面是一个官方的示例

from decorator import decorator

@decorator
def warn_slow(func, timelimit=60, *args, **kw):
t0 = time.time()
result = func(*args, **kw)
dt = time.time() - t0
if dt > timelimit:
logging.warn('%s took %d seconds', func.__name__, dt)
else:
logging.info('%s took %d seconds', func.__name__, dt)
return result
@warn_slow(timelimit=600) # warn if it takes more than 10 minutes
def run_calculation(tempdir, outdir):
pass

可以看到

装饰函数的第一个参数,还是被装饰器 func ,这个跟之前一样


而第二个参数 timelimit 写成了位置参数的写法,并且有默认值
再往后,就还是跟原来一样使用了可变参数的写法

不难推断,只要你在装饰函数中第二个参数开始,使用了非可变参数的写法,这些参数就可以做为
装饰器调用时的参数。

4. 签名问题有解决?

我们在自己写装饰器的时候,通常都会顺手加上一个叫 functools.wraps 的装饰器,我想你应该


也经常见过,那他有啥用呢?

先来看一个例子

def wrapper(func):
def inner_function():
pass
return inner_function

@wrapper
def wrapped():
pass

print(wrapped.__name__)
#inner_function

为什么会这样子?不是应该返回 func 吗?

这也不难理解,因为上边执行 func 和下边 decorator(func) 是等价的,所以上面


func.__name__ 是等价于下面 decorator(func).__name__ 的,那当然名字是 inner_function

def wrapper(func):
def inner_function():
pass
return inner_function

def wrapped():
pass
print(wrapper(wrapped).__name__)
#inner_function

目前,我们可以看到当一个函数被装饰器装饰过后,它的签名信息会发生变化(譬如上面看到的函
数名)

那如何避免这种情况的产生?

解决方案就是使用我们前面所说的 functools .wraps 装饰器。

它的作用就是将 被修饰的函数(wrapped) 的一些属性值赋值给 修饰器函数(wrapper) ,最终让属性


的显示更符合我们的直觉。

from functools import wraps

def wrapper(func):
@wraps(func)
def inner_function():
pass
return inner_function

@wrapper
def wrapped():
pass

print(wrapped.__name__)
## wrapped

那么问题就来了,我们使用了 decorator 之后,是否还会存在这种签名的问题呢?

写个例子来验证一下就知道啦

from decorator import decorator

@decorator
def deco(func, *args, **kw):
print("Ready to run task")
func(*args, **kw)
print("Successful to run task")

@deco
def myfunc():
print("Running the task")

print(myfunc.__name__)

输出的结果是 myfunc ,说明 decorator 已经默认帮我们处理了一切可预见的问题。


5. 总结一下

是一个提高装饰器编码效率的第三方库,它适用于对装饰器原理感到困惑的新手,可
decorator
以让你很轻易的写出更符合人类直觉的代码。对于带参数装饰器的定义,是非常复杂的,它需要要
写多层的嵌套函数,并且需要你熟悉各个参数的传递路径,才能保证你写出来的装饰器可以正常使
用。这时候,只要用上 decorator 这个库,你就可以很轻松的写出一个带参数的装饰器。同时你
也不用担心他会出现签名问题,这些它都为你妥善的处理好了。

这么棒的一个库,推荐你使用起来。

7.11 国际化模块,让翻译更优雅

国际化与本地化

国际化 (internationalization),简称 i18n

很多人并不知道,为什么要叫 i18n 呢?怎么谐音都不对。

实际上 18 是指在 ”internationalization” 这个单词中,i 和 n之间有18个字母。

而与之相对的,本地化(localization),简称 L10 n,10 就是指在 ”localization”这个单词中,l 和


n 之间有10个字母

本地化是指使一个国际化的软件为了在某个特定地区使用而进行实际翻译的过程。

国际化的软件具备这样一种能力,当软件被移植到不同的语言及地区时,软件本身不用做内部工程
上的改变或修正。
gettext 模块

gettext 是一套 GNU下的国际化工具。主要有工具:

xgettext: 从源码中抽取字符串,生成po文件(portable object)


msgfmt: 将po文件编译成mo文件(machine object)
gettext: 进行翻译,如果找不到gettext命令,或者找不到msgfmt命令。请重新安装一遍gettext
套件。

很多系统中都内置了 gettext 模块

如果你在 ubuntu系统中,可能需要如下命令进行安装

sudo apt-get install gettext

简单示例演示

首先新建一个目录

$ mkdir -p locale/zh_CN/LC_MESSAGES

然后在这个目录下新建一个 hello.po 文件

msgid "hello world"


msgstr " "

然后执行如下一条命令,将 po 文件翻译成 mo文件

$ msgfmt locale/zh_CN/LC_MESSAGES/hello.po -o locale/zh_CN/LC_MESSAGES/hello.mo

然后在 local 同级目录下进入 Console 模式,就可以使用 _ 进行翻译了,为什么 _ 能这么用,原


因是 zh.install() 这个调用将其绑定到了 Python 内建命名空间中,以便在应用程序的所有模块
中轻松访问它。

>>> import gettext


>>> zh = gettext.translation("hello", "locale", languages=["zh_CN"])
>>> zh.install()
>>> _('hello world')
' '

7.12 非常好用的调度模块
Python 自带一个调度器模块 sched ,它能为你实现优先级队列/延迟队列和定时队列。

这个模块的使用非常简单,首先以延迟队列为例:

import sched

def do_work(name):
print(f' {name}')

sch = sched.scheduler()
sch.enter(5, 1, do_work, argument=('iswbm', ))
sch.run()

代码运行以后,会卡在 sch.run() 这里,5秒钟以后执行 do_work('iswbm') ,运行效果如下图所


示:

其中, sch.enter() 的第一个参数为延迟的时间,单位为秒,第二个参数为优先级,数字越小优先


级越高。当两个任务同时要执行时,优先级高的先执行。但需要注意的是,如果你这样写:

import sched

def do_work(name):
print(f' {name}')

sch = sched.scheduler()
sch.enter(5, 2, do_work, argument=('python', ))
sch.enter(5, 1, do_work, argument=('iswbm', ))
sch.run()

那么先打印出来的是 python
为什么这里优先级失效了?1的优先级大于2,应该先运行下面的才对啊。

这是由于,只有当两个任务同时运行的时候,才会去检查优先级。如果两个任务触发的时间一前一
后,那么还轮不到比较优先级。由于延迟队列的 是相对于当前运行这一行代码的时间来计算
的,后一行代码比前一行代码晚了几毫秒,所以实际上产品经理这一行会先到时间,所以就会先运
行。

为了使用绝对的精确时间,我们可以使用另外一个方法:

import sched
import time
import datetime

def do_work(name):
print(f' {name}')

sch = sched.scheduler(time.time, time.sleep)


start_time = datetime.datetime.now() + datetime.timedelta(seconds=10)
start_time_ts = start_time.timestamp()
sch.enterabs(start_time_ts, 2, do_work, argument=('python', ))
sch.enterabs(start_time_ts, 1, do_work, argument=('iswbm', ))
sch.run()

运行效果如下图所示:
的第一个参数是任务开始时间的时间戳,这是一个绝对时间,这个时间可以使用
sch.enterabs()
datetime模块来生成,或者其他你熟悉的方式。后面的参数和 sch.enter() 完全一样。

如果你要运行的函数带有多个参数或者默认参数,那么可以使用下面的方式传入参数:

import sched
import time
import datetime

def do_work(name, place, work=' '):


print(f' {name} {place}{work}')

sch = sched.scheduler(time.time, time.sleep)


start_time = datetime.datetime.now() + datetime.timedelta(seconds=10)
start_time_ts = start_time.timestamp()
sch.enter(5, 2, do_work, argument=(' ', ' '), kwargs={'work': ' '})
sch.enterabs(start_time_ts, 1, do_work, argument=(' ', ' '), kwargs={'
work': ' '})
sch.run()

argument参数对应的元组存放普通参数,kwargs对应的字典存放带参数名的参数。

本文来源于:公众号"未闻Code",作者:kingname

7.13 实现字典的点式操作

字典是 Python 中基础的数据结构之一,字典的使用,可以说是非常的简单粗暴,但即便是这样一


个与世无争的数据结构,仍然有很多人 "用不惯它" 。
也许你并不觉得,但我相信,你看了这篇文章后,一定会和我一样,对原生字典开始有了偏见。

我举个简单的例子吧

当你想访问字典中的某个 key 时,你需要使用字典特定的访问方式,而这种方式需要你键入 一对


中括号 还有 一对引号

>>> profile = dict(name="iswbm")


>>> profile
{'name': 'iswbm'}
>>> profile["name"]
'iswbm'

是不是开始觉得忍无可忍了?

如果可以像调用对象属性一样使用 . 去访问 key 就好了,可以省去很多多余的键盘击入,就像这


样子

>>> profile.name
'iswbm'

是的,今天这篇文章就是跟大家分享一种可以直接使用 . 访问和操作字典的一个黑魔法库 --
munch 。

1. 安装方法

使用如下命令进行安装

$ python -m pip install munch

2. 简单示例

munch 有一个 Munch 类,它继承自原生字典,使用 isinstance 可以验证

>>> from munch import Munch


>>> profile = Munch()
>>> isinstance(profile, dict)
True
>>>

并实现了点式赋值与访问, profile.name 与 profile['name'] 是等价的

>>> profile.name = "iswbm"


>>> profile.age = 18
>>> profile
Munch({'name': 'iswbm', 'age': 18})
>>>
>>> profile.name
'iswbm'
>>> profile["name"]
'iswbm'

3. 兼容字典的所有操作

本身 Munch 继承自 dict,dict 的操作也同样适用于 Munch 对象,不妨再来验证下

首先是:增删改查

##
>>> profile["gender"] = "male"
>>> profile
Munch({'name': 'iswbm', 'age': 18, 'gender': 'male'})

##
>>> profile["gender"] = "female"
>>> profile
Munch({'name': 'iswbm', 'age': 18, 'gender': 'female'})

##
>>> profile.pop("gender")
'female'
>>> profile
Munch({'name': 'iswbm', 'age': 18})
>>>
>>> del profile["age"]
>>> profile
Munch({'name': 'iswbm'})

再者是:一些常用方法

>>> profile.keys()
dict_keys(['name'])
>>>
>>> profile.values()
dict_values(['iswbm'])
>>>
>>> profile.get('name')
'iswbm'
>>> profile.setdefault('gender', 'male')
'male'
>>> profile
Munch({'name': 'iswbm', 'gender': 'male'})

4. 设置返回默认值

当访问一个字典中不存在的 key 时,会报 KeyError 的错误

>>> profile = {}
>>> profile["name"]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 'name'

对于这种情况,通常我们会使用 get 来规避

>>> profile = {}
>>> profile.get("name", "undefined")
'undefined'

当然你在 munch 中仍然可以这么用,不过还有一种更好的方法:使用 DefaultMunch,它会在你访


问不存在的 key 时,给你返回一个设定好的默认值

>>> from munch import DefaultMunch


>>> profile = DefaultMunch("undefined", {"name": "iswbm"})
>>> profile
DefaultMunch('undefined', {'name': 'iswbm'})
>>> profile.age
'undefined'
>>> profile
DefaultMunch('undefined', {'name': 'iswbm'})

5. 工厂函数自动创建key

上面使用 DefaultMunch 仅当你访问不存在的 key 是返回一个默认值,但这个行为并不会修改原


munch 对象的任何内容。

若你想访问不存在的 key 时,自动触发给原 munch 中新增你想要访问的 key ,并为其设置一个默


认值,可以试一下 DefaultFactoryMunch 传入一个工厂函数。

>>> from munch import DefaultFactoryMunch


>>> profile = DefaultFactoryMunch(list, name='iswbm')
>>> profile
DefaultFactoryMunch(list, {'name': 'iswbm'})
>>>
>>> profile.brothers
[]
>>> profile
DefaultFactoryMunch(list, {'name': 'iswbm', 'brothers': []})

6. 序列化的支持

Munch 支持序列化为 JSON 或者 YAML 格式的字符串对象

转换成 JSON

>>> from munch import Munch


>>> munch_obj = Munch(foo=Munch(lol=True), bar=100, msg='hello')
>>>
>>> import json
>>> json.dumps(munch_obj)
'{"foo": {"lol": true}, "bar": 100, "msg": "hello"}'

转换成 YAML

>>> from munch import Munch


>>> munch_obj = Munch(foo=Munch(lol=True), bar=100, msg='hello')
>>> import yaml
>>> yaml.dump(munch_obj)
'!munch.Munch\nbar: 100\nfoo: !munch.Munch\n lol: true\nmsg: hello\n'
>>>
>>> print(yaml.dump(munch_obj))
!munch.Munch
bar: 100
foo: !munch.Munch
lol: true
msg: hello

>>>

建议使用 safe_dump 去掉 !munch.Munch

>>> print(yaml.safe_dump(munch_obj))
bar: 100
foo:
lol: true
msg: hello

7. 说说局限性

以上就是关于 munch 的使用全解,munch 的进一步封装使得数据的访问及操作更得更加 Pythonic


,替换原生字典在大部分场景下都不会有太大问题。

但同时也不得不承认,munch 在一些场景下无法达到原生字典的效果,比如我想字典里的 key 为


"1.2" 的时候,原生字典能很好的表示它。

>>> dict_obj = {"1.2": "hello"}


>>> dict_obj["1.2"]
'hello'

切换到 munch ,你会发现无法在初始化 munch 对象的时候,传入 1.2 的 key

>>> from munch import Munch


>>> dict_obj = Munch(1.2="hello")
File "<stdin>", line 1
dict_obj = Munch(1.2="hello")
^
SyntaxError: expression cannot contain assignment, perhaps you meant "=="?

就算你用原生的字典的方式添加了这个 key-value,也根本无法使用 . 的方式取到 1.2 对应的


value。

>>> from munch import Munch


>>> dict_obj = Munch()
>>> dict_obj["1.2"]="hello"
>>> dict_obj
Munch({'1.2': 'hello'})
>>> dict_obj.1.2
File "<stdin>", line 1
dict_obj.1.2
^
SyntaxError: invalid syntax

也正是因为这样,原生字典至今还是不可替代的存在。

赞赏作者

原创不易,请个咖啡,交个朋友

You might also like