Professional Documents
Culture Documents
作者:王炳明
版本: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中,一切皆对象,省略号也不例外。
>>> ...
Ellipsis
>>> type(...)
<class '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
如果你真的想用,也不是没有办法,具体你看下面这个例子。
__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))
执行后,输出如下
那么这个zip 是如何制作的呢,请看下面的示例。
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 ~]#
1、在行尾时,用做续行符
2、在字符串中,用做转义字符,可以将普通字符转化为有特殊含义的字符。
>>> str1='\nhello'
>>> print(str1)
hello
>>> str2='\thello' tab
>>> print(str2)
hello
但是如果你用单 \ 结尾是会报语法错误的
>>> str3="\"
File "<stdin>", line 1
str3="\"
^
SyntaxError: EOL while scanning string literal
>>> str3=r"\"
File "<stdin>", line 1
str3=r"\"
^
SyntaxError: EOL while scanning string literal
1.5 如何修改解释器提示符
这个当做今天的一个小彩蛋吧。应该算是比较冷门的,估计知道的人很少了吧。
1.6 简洁而优雅的链式比较
先给你看一个示例:
你知道这个表达式为什么会会返回 False 吗?
它的运行原理与下面这个类似,是不是有点头绪了:
如果你还是不明白,那我再给你整个第一个例子的等价写法。
这个用法叫做链式比较。
当一个 or 表达式中所有值都为真,Python会选择第一个值
示例如下:
1.8 连接多个列表最极客的方式
>>> a = [1,2]
>>> b = [3,4]
>>> c = [5,6]
>>>
>>> sum((a,b,c), [])
[1, 2, 3, 4, 5, 6]
1.9 字典居然是可以排序的?
## 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 哪些情况下不需要续行符?
在写代码时,为了代码的可读性,代码的排版是尤为重要的。
为了实现高可读性的代码,我们常常使用到的就是续行符 \ 。
那有哪些情况下,是不需要写续行符的呢?
经过总结,在这些符号中间的代码换行可以省略掉续行符: [] , () , {}
>>> my_list=[1,2,3,
... 4,5,6]
>>> my_tuple=(1,2,3,
... 4,5,6)
但是这种写法回车会自动转化为 \n
1.11 用户无感知的小整数池
>>> a = -6
>>> b = -6
>>> a is b
False
>>> a = 256
>>> b = 256
>>> a is b
True
>>> a = 257
>>> b = 257
>>> a is b
False
问题又来了:最后一个示例,为啥是True?
因为当你在同一行里,同时给两个变量赋同一值时,解释器知道这个对象已经生成,那么它就会引
用到同一个对象。如果分成两行的话,解释器并不知道这个对象已经存在了,就会重新申请内存存
放这个对象。
字符串类型作为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
它们有什么区别呢?
但若要严格再进行区分,它们实际上还有各自的叫法
parameter:形参(formal parameter),体现在函数内部,作用域是这个函数体。
argument :实参(actual parameter),调用函数实际传递的参数。
def output_msg(msg):
print(msg)
output_msg("error")
我们经常会在别人的脚本或者项目的入口文件里看到第一行是下面这样
#!/usr/bin/python
或者这样
#!/usr/bin/env python
这两者有什么区别呢?
那么加和不加有什么区别呢?
有没有一种方式?可以省去每次都加 python 呢?
具体演示过程,你可以看下面。
那么对于这两者,我们应该使用哪个呢?
很多人会想当然的认为二者是等同的,但实际情况却不是这样的。
使用 timeit 模块,可以轻松测出这个结果
那为什么会这样呢?
探究这个过程,可以使用 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(),会多了个调用函数的过程,而这个过程会有进出栈的操作,相对更加耗时。
Hello World
Python之禅
反地心引力漫画
就会自动打开一个网页。
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能够
执行。
那问题就来了,以上这两种规则,如果同时存在,Python 解释器会如何选择?哪个优先级更高?
写个示例验证一下,就明白啦
还是写个 示例来验证一下:
1.20 字符串里的缝隙是什么?
在Python中求一个字符串里,某子字符(串)出现的次数。
>>> "aabb".count("a")
2
>>> "aabb".count("b")
2
>>> "aabb".count("ab")
1
但是如果我想计算空字符串的个数呢?
>>> "aabb".count("")
5
奇怪了吧?
不是应该返回 0 吗?怎么会返回 5?
理解了这个“缝隙” 的概念后,以下这些就好理解了。
但是其实并不是这样的。
在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 字母也玩起了障眼法
在Python 2.x 中
>>> valuе = 32
File "<stdin>", line 1
valuе = 32
^
SyntaxError: invalid syntax
在Python 3.x 中
>>> valuе = 32
>>> value
11
什么?没有截图你不信?
如果你在自己的电脑上尝试一下,结果可能是这样的
怎么又好了呢?
如果你想复现的话,请复制我这边给出的代码: valuе = 32
这是为什么呢?
细思恐极,在这里可千万不要得罪同事们,万一离职的时候,对方把你项目里的 e 全局替换成 e
,到时候你就哭去吧,肉眼根本看不出来嘛。
1.23 数值与字符串的比较
在 Python2 中,数字可以与字符串直接比较。结果是数值永远比字符串小。
但在 Python3 中,却不行。
>>> 100000000 < ""
TypeError: '<' not supported between instances of 'int' and 'str'
1.24 时有时无的切片异常
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
什么情况下,这个等式会不成立呢?
1.27 += 不等同于=+
因此会有如下两者的差异。
## =+
>>> 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
回顾一下,什么是局部变量?在非全局下定义声明的变量都是局部变量。
因此报错是正常的。
理解了上面的例子,给你留个思考题。为什么下面的代码不会报错呢?
$ cat demo.py
a = 1
def output():
print(a)
output()
$ python demo.py
1
同时我们又知道:在上下文管理器中,被包裹的程序主体代码结束会运行上下文管理器中的一段代
码(通常是资源的释放)。
但如果把上下文管理器放在一个循环体中,而在这个上下文管理器中执行了 break ,是否会直接跳
出循环呢?
这个问题其实不难,只要做一下试验都能轻易地得出答案,难就难在很多对这个答案都是半猜半
疑,无法肯定的回答。
试验代码如下:
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
start: a = 1
end: a = 1
start: a = 2
end: a = 2
另外还有几个与此类似的问题,我这里也直接给出答案,不再细说了
与我预想的结果不符,多个空格会被分割多次。
有两种方法。
第一种方法
不加参数,这种只适用于将多个空格当成一个空格处理,如果不是以空格为分隔符的场景,这种就
不适用了。
第二种方法
>>> msg='hello----world'
>>> msg.split('-')
['hello', '', '', '', 'world']
>>>
>>> filter(None, msg.split('-'))
['hello', 'world']
>>> 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
## 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 最快查看包搜索路径的方式
那有没有更快的方式呢?
你可能会想到这种,但这本质上与上面并无区别
/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
这里我要介绍的是比上面两种都方便的多的方法,一行命令即可解决
假设现在你需要查看你机器上的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"}
其实还有一种更简单的方法,比如我要计算一个字符串的md5
2.5 用调试模式执行脚本
除此之外,还有一种方法,就是使用 -m pdb
搭建FTP,或者是搭建网络文件系统,这些方法都能够实现Linux的目录共享。但是FTP和网络文件
系统的功能都过于强大,因此它们都有一些不够方便的地方。比如你想快速共享Linux系统的某个
目录给整个项目团队,还想在一分钟内做到,怎么办?很简单,使用Python中的
SimpleHTTPServer。
## python2
python -m SimpleHTTPServer 8888
## python3
python3 -m http.server 8888
SimpleHTTPServer有一个特性,如果待共享的目录下有index.html,那么index.html文件会被视为默
认主页;如果不存在index.html文件,那么就会显示整个目录列表。
当你不知道一个内置模块如何使用时,会怎么做呢?
百度?Google?
是的,你没有听错。
就算没有外网网络也能学习 Python 模块.
帮助文档的效果如下
2.8 最正确且优雅的装包方法
但若你使用这样的命令去安装,就没有了这样的烦恼了
## 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.10 让脚本报错后立即进入调试模式
这个时候,我们通常会去排查是什么原因导致的程序崩溃。
复现有时候很难,有时候虽然简单,但是要伪造各种数据,相当麻烦。
如果有一种方法能在程序崩溃后,立马进入调试模式该有多好啊?
明哥都这么问了,那肯定是带着解决方案来的。
具体演示如下:
需要注意的是:脚本执行完毕,有两种情况:
1. 正常退出
2. 异常退出
如果希望脚本正确完成时自动推出,可以在脚本最后加上一行 __import__("os")._exit(0)
这有点像,你上爱某艺看视频吧,都要先看个 90 秒的广告。
2.12 在执行任意代码前自动念一段平安经
最近的"平安经"可谓是引起了不小的风波啊。
作为一个正儿八经的程序员,最害怕的就是自己的代码上线出现各种各样的 BUG。
做好心理准备了嘛?
我要开始作妖了,噢不,是开始念经了。
你一定很想知道这是如何的吧?
当然是有,只不过相对来说,会麻烦一点了。
这是如何做到的呢?
很简单,只要做两件事
$ export PYTHONSTARTUP=/Users/MING/startup.py
这样就可以了。
如果要在脚本中实现这种效果,我目前想到最粗糙我笨拙的方法了 --
2.14 把模块当做脚本来执行 7 种方法及原理
1. 用法举例
前面的文章里其实分享过不少类似的用法。比如:
1、 快速搭建一个 HTTP 服务
## python2
$ python -m SimpleHTTPServer 8888
## python3
$ python3 -m http.server 8888
4、最优雅且正确的包安装方法
6、快速打印包的搜索路径
$ python -m site
7、用于快速计算程序执行时长
2. 原理剖析
上面的诸多命令,都有一个特点,在命令中都含有 -m 参数选项,而参数的值,
SimpleHTTPServer, http.server, pydoc,pdb,pip, json.tool,site ,timeit这些都是模块或者
包。
通常来说模块或者包,都是用做工具包由其他模块导入使用,而很少直接使用命令来执行(脚本除
外)。
Python 给我们提供了一种方法,可以让我们将模块里的部分功能抽取出来,直接用于命令行式的
调用。效果就是前面你所看到的。
那这是如何实现的呢?
这就很奇怪了。
if __name__ == '__main__':
main()
这下思路清晰了。
想要使用 -m 的方式执行模块,有两种方式:
if __name__ == '__main__':
main()
上面我将 -m 的使用情况分为两种,但是实际上,只有一种,对于第一种,你完全可以将
-m <package> 理解为 -m <package.__main__> 的简写形式。
3. 实践一下
$ export PATH=${PATH}:`pwd`
先来验证一下第一种方法。
## __main__.py
print("hello, world")
然后直接执行如下命令,立马就能看到效果。
$ python3 -m demo
hello,world
执行过程如下:
再来验证一下使用第二种方法。
## foobar.py
def main():
print("hello, world")
if __name__ == "__main__":
main()
最后执行一下如下命令,输出与预期相符
$ python3 -m demo.foobar
hello, foobar
4. -m 存在的意义
那么问题就来了,那我直接执行不就行啦,何必多此一举再加个 -m 呢?
这个问题很有意思,值得一提。
当我们使用一个模块的时候,往往只需要记住模块名,然后使用 import 去导入它就行了。
如此一对比,哪个更方便?你心里应该有数了。
使用的效果如下
如果你不加文件的路径,默认会打开 idle 的 shell 模式
对字符串编码和解码
对一个字符串进行 base64 编码 和 解码(加上 -d 参数即可)
效果如下
对文件进行编码和解码
在命令后面直接加文件的路径
##
$ 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 脚本
识别压缩文件
$ python -m sysconfig
信息包括:
你当前的操作系统平台
Python 的具体版本
包的搜索路径
以及各种环境变量
2.19 快速解压和压缩文件
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 格式压缩包
$ 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
$ 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 格式压缩包
$ 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
$ rm -rf demo
$ ls -l | grep demo
-rw-r--r-- 1 MING staff 74890 5 4 12:55 demo.zip
$ 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
pyc是一种二进制文件,是由py文件经过编译后,生成的文件,是一种byte code,py文件变成pyc文
件后,加载的速度会有所提高。因此在一些场景下,可以预先编译成 pyc 文件,来提高加载速度。
编译的命令非常的简单,示例如下
$ tree demo
demo
"## main.py
$ tree demo
demo
$## __pycache__
% "## main.cpython-39.opt-1.pyc
"## main.py
检查 192.168.56.200 上的 22 端口有没有开放。
2.22 快速将项目打包成应用程序
具体演示过程如下
2.23 快速打印函数的调用栈
在使用pdb时,手动打印调用栈
import traceback
traceback.print_stack(file=sys.stdout)
(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、最直观的相加
使用 + 对多个列表进行相加,你应该懂,不多说了。
2、借助 itertools
3、使用 * 解包
使用 * 可以解包列表,解包后再合并。
示例如下:
5、使用列表推导式
当然可以,具体示例代码如下:
6、使用 heapq
它的效果等价于下面这行代码:
sorted(itertools.chain(*iterables))
如果你希望得到一个始终有序的列表,那请第一时间想到 heapq.merge,因为它采用堆排序,效率
非常高。但若你不希望得到一个排过序的列表,就不要使用它了。
7、借助魔法方法
所以以下两种方法其实是等价的
8. 使用 yield from
因此,我们可以像下面这样自定义一个合并列表的工具函数。
1、最简单的原地更新
2、先解包再合并字典
3、借助 itertools
在 Python 里有一个非常强大的内置模块,它专门用于操作可迭代对象。
4、借助 ChainMap
如果可以引入一个辅助包,那我就再提一个, ChainMap 也可以达到和 itertools 同样的效果。
使用 ChainMap 有一点需要注意,当字典间有重复的键时,只会取第一个值,排在后面的键值并不
会更新掉前面的(使用 itertools 就不会有这个问题)。
5、使用dict.items() 合并
利用这一点,也可以将它用于字典的合并,只不过得绕个弯子,有点不好理解。
6、最酷炫的字典解析式
当然可以,具体示例代码如下:
除了 | 操作符之外,还有另外一个操作符 |= ,类似于原地更新。
3.3 花式导包的八种方法
1. 直接 import
人尽皆知的方法,直接导入即可
>>> import os
>>> os.getcwd()
'/home/wangbm'
与此类似的还有,不再细讲
import ...
import ... as ...
from ... import ...
from ... import ... as ...
但是在一些特殊场景中,可能还需要其他的导入方式。
下面我会一一地给你介绍。
2. 使用 __import__
参数介绍:
使用示例如下:
>>> os = __import__('os')
>>> os.getcwd()
'/home/wangbm'
>>> __builtins__.__dict__['__import__']('os').getcwd()
'/home/wangbm'
3. 使用 importlib 模块
它的简单示例:
从 python 3 开始,内建的 reload 函数被移到了 imp 模块中。而从 Python 3.4 开始,imp 模块被否
决,不再建议使用,其包含的功能被移到了 importlib 模块下。即从 Python 3.4 开始,importlib 模
块是之前 imp 模块和 importlib 模块的合集。
5. 使用 execfile
语法如下:
参数有这么几个:
filename:文件名。
globals:变量作用域,全局命名空间,如果被提供,则必须是一个字典对象。
locals:变量作用域,局部命名空间,如果被提供,可以是任何映射对象。
>>> execfile("/usr/lib64/python2.7/os.py")
>>>
>>> getcwd()
'/home/wangbm'
6. 使用 exec 执行
示例如下:
7. import_from_github_com
这个包使用了PEP 302中新的引入钩子,允许你可以从github上引入包。这个包实际做的就是安装
这个包并将它添加到本地。你需要 Python 3.2 或者更高的版本,并且 git 和 pip 都已经安装才能使
用这个包。
pip 要保证是较新版本,如果不是请执行如下命令进行升级。
示例如下
看了 import_from_github_com的源码后,你会注意到它并没有使用importlib。实际上,它的原理就
是使用 pip 来安装那些没有安装的包,然后使用Python的 __import__() 函数来引入新安装的模
块。
8、远程导入模块
具体内容非常的多,你可以点击这个链接进行深入学习。
示例代码如下:
## 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
try:
loader = UrlMetaLoader(baseurl)
return loader
except Exception:
return None
class UrlMetaLoader(importlib.abc.SourceLoader):
def __init__(self, baseurl):
self.baseurl = baseurl
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)
一切准备好,验证开始。
3.4 条件语句的七种写法
第一种:原代码
这是一段非常简单的通过年龄判断一个人是否成年的代码,由于代码行数过多,有些人就不太愿意
这样写,因为这体现不出自己多年的 Python 功力。
if age > 18:
return " "
else:
return " "
下面我列举了六种这段代码的变异写法,一个比一个还 6 ,单独拿出来比较好理解,放在工程代码
里,没用过这些学法的人,一定会看得一脸懵逼,理解了之后,又不经意大呼:卧槽,还可以这样
写?,而后就要开始骂街了:这是给人看的代码? (除了第一种之外)
第二种
语法:
例子
>>> age1 = 20
>>> age2 = 17
>>>
>>>
>>> msg1 = " " if age1 > 18 else " "
>>> print msg1
>>>
>>> msg2 = " " if age2 > 18 else " "
>>> print msg2
>>>
第三种
语法
例子
>>>
>>> print(msg2)
第四种
语法
(<on_false>, <on_true>)[condition]
例子
>>>
>>>
>>> msg2 = (" ", " ")[age2 > 18]
>>> print(msg2)
第五种
语法
例子
>>>
>>> msg2 = (lambda:" ", lambda:" ")[age2 > 18]()
>>> print(msg2)
第六种
语法:
>>> msg1 = {True: " ", False: " "}[age1 > 18]
>>> print(msg1)
>>>
>>> msg2 = {True: " ", False: " "}[age2 > 18]
>>> print(msg2)
第七种
语法
例子
>>> msg1 = ((age1 > 18) and (" ",) or (" ",))[0]
>>> print(msg1)
>>>
>>> msg2 = ((age2 > 18) and (" ",) or (" ",))[0]
>>> print(msg2)
以上代码,都比较简单,仔细看都能看懂,我就不做解释了。
1、使用 in 和 not in
使用这两个成员运算符,可以很让我们很直观清晰的判断一个对象是否在另一个对象中,示例如
下:
2、使用 find 方法
4、使用 count 方法
只要判断结果大于 0 就说明子串存在于字符串中。
5、通过魔法方法
示例如下;
operator模块是python中内置的操作符函数接口,它定义了一些算术和比较内置操作的函数。
operator模块是用c实现的,所以执行速度比 python 代码快。
7、使用正则匹配
说到查找功能,那正则绝对可以说是专业的工具,多复杂的查找规则,都能满足你。
对于判断字符串是否存在于另一个字符串中的这个需求,使用正则简直就是大材小用。
import re
3.6 海象运算符的三种用法
可能有朋友是第一次接触这个新特性,所以还是简单的介绍一下这个海象运算符有什么用?
import "fmt"
func main() {
if age := 20;age > 18 {
fmt.Println(" ")
}
}
age = 20
if age > 18:
print(" ")
第二个用法:while
但有了海象运算符之后,你可以这样
比如,实现一个需要命令行交互输入密码并检验的代码,你也许会这样子写
while True:
p = input("Enter the password: ")
if p == "youpassword":
break
有了海象运算符之后,这样子写更为舒服
第三个用法:推导式
这个系列的文章,几乎每篇都能看到推导式的身影,这一篇依旧如此。
在编码过程中,我很喜欢使用推导式,在简单的应用场景下,它简洁且不失高效。
如下这段代码中,我会使用列表推导式得出所有会员中过于肥胖的人的 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
height = info["height"]
weight = info["weight"]
## bmi
fat_bmis = [get_bmi(m) for m in members if get_bmi(m) > 24]
print(fat_bmis)
输出如下
1
2
3
4
[25.88057063502083]
如果所有会员都是过于肥胖的,那最终将执行 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 模块重载的五种方法
环境准备
$ tree foo
foo
"## bar.py
0 directories, 1 file
print("successful to be imported")
禁止重复导入
重复导入方法一
重复导入方法二
重复导入方法三
重复导入方法四
如果你对包的加载器有所了解(详细可以翻阅我以前写的文章:https://iswbm.com/84.html)
还可以使用下面的方法
重复导入方法五
既然影响我们重复导入的是 sys.modules,那我们只要将已导入的包从其中移除是不是就好了呢?
这应该算是一个小坑,不知道的人,会掉入坑中爬不出来。
1. 为什么要有转义?
为了能将那些特殊字符都能写入到字符串变量中,就规定了一个用于转义的字符 \ ,有了这个字
符,你在字符串中看的字符,print 出来后就不一定你原来看到的了。
举个例子
是不是有点神奇?变成阶梯状的输出了。
那个 \013 又是什么意思呢?
\ 是转义符号,上面已经说过
把八进制的 13 转成 10 进制后是 11
对照查看 ASCII 码表,11 对应的是一个垂直定位符号,这就能解释,为什么是阶梯状的输出字符
串。
2. 转义的 5 种表示法
## 8
>>> msg = "hello\012world"
>>> print(msg)
hello
world
>>>
## 16
>>> msg = "hello\x0aworld"
>>> print(msg)
hello
world
>>>
因此对于一些常用并且比较特殊字符,我们习惯用另一种类似别名的方式,比如使用 \n 表示换
行,它与 \012 、 \x0a 是等价的。
与此类似的表示法,还有如下这些
于是,要实现 hello + 回车 + world ,就有了第三种方法
##
>>> msg = "hello\nworld"
>>> print(msg)
hello
world
>>>
到目前为止,我们掌握了 三种转义的表示法。
已经非常难得了,让我们的脑洞再大一点吧,接下来再介绍两种。
>>> print("\u4E2D")
## unicode \u000a
>>> print('hello\u000aworld')
hello
world
看到这里,你是不是以为要结束啦?
不,还没有。下面还有一种。
## unicode \U0000000A
>>> print('hello\U0000000Aworld')
hello
world
好啦,目前我们掌握了五种转义的表示法。
总结一下:
为什么标题说,转义也可以炫技呢?
五种转义的表示法到这里就介绍完成,接下来是更多转义相关的内容,也是非常有意思的内容,有
兴趣的可以继续往下看。
3. raw 字符串
>>> print(r"hello\nworld")
hello\nworld
>>>
>>> print(R"hello\nworld")
hello\nworld
然而,不是所有时候都可以加 r 的,比如当你的字符串是由某个程序/函数返回给你的,而不是你
自己生成的
## "hello\nworld"
>>> body = spider()
>>> print(body)
hello
world
这个时候打印它, \n 就是换行打印。
4. 使用 repr
1. 将 \ 变为了 \\
>>> body="hello\nworld"
>>> repr(body)
"'hello\\nworld'"
>>>
>>>
>>> body='hello\nworld'
>>> repr(body)
"'hello\\nworld'"
5. 使用 string_escape
如果你还在使用 Python 2 ,其实还可以使用另一种方法。
>>> "hello\nworld".encode('string_escape')
'hello\\nworld'
>>>
6. 查看原生字符串
综上,想查看原生字符串有两种方法:
>>> body="hello\nworld"
>>>
>>> body
'hello\nworld'
>>>
>>> print(repr(body))
'hello\nworld'
>>>
7. 恢复转义:转成原字符串
答案是:有。
>>> body="hello\\nworld"
>>>
>>> body
'hello\\nworld'
>>>
>>> body.decode('string_escape')
'hello\nworld'
>>>
>>> body="hello\\nworld"
>>>
>>> body
'hello\\nworld'
>>>
>>> bytes(body, "utf-8").decode("unicode_escape")
'hello\nworld'
>>>
如果本文对你有些许帮助,不如给明哥 来个四连 ~ 比心
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 (如果该
包存在的话)。
## 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
3. 使用 pipx
由于它是一个第三方工具,因此在使用它之前,需要先安装
##
$ pipx install pkg
4. 使用 setup.py
##
$ python setup.py install
5. 使用 yum
## rpm
$ python setup.py bdist_rpm
## yum
$ yum install pkg
## rpm
$ rpm -ivh pkg
6. 使用 pipenv
7. 使用 poetry
##
$ poetry add pytest --dev
8. 使用 curl + 管道
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. 第三种:不带参数的类装饰器
以上都是基于函数实现的装饰器,在阅读别人代码时,还可以时常发现还有基于类实现的装饰器。
还是以日志打印这个简单的例子为例
class logger(object):
def __init__(self, func):
self.func = func
@logger
def say(something):
print("say {}!".format(something))
say("hello")
执行一下,看看输出
4. 第四种:带参数的类装饰器
带参数和不带参数的类装饰器有很大的不同。
__init__ :不再接收被装饰函数,而是接收传入参数。
__call__ :接收被装饰函数,实现装饰逻辑。
class logger(object):
def __init__(self, level='INFO'):
self.level = level
@logger(level='WARNING')
def say(something):
print("say {}!".format(something))
say("hello")
我们指定 WARNING 级别,运行一下,来看看输出。
5. 第五种:使用偏函数与类实现装饰器
绝大多数装饰器都是基于函数和闭包实现的,但这并非制造装饰器的唯一方式。
接下来就来说说,如何使用 类和偏函数结合实现一个与众不同的装饰器。
import time
import functools
class DelayFunc:
def __init__(self, duration, func):
self.duration = duration
self.func = func
def delay(duration):
"""
.eager_call
"""
#
# functools.partial DelayFunc
return functools.partial(DelayFunc, duration)
我们的业务函数很简单,就是相加
@delay(duration=2)
def add(a, b):
return a+b
来看一下执行过程
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
其实例化的过程,你可以参考我这里的调试过程,加以理解。
第一种:使用 open
常规操作
with open('data.txt') as fp:
content = fp.readlines()
第二种:使用 fileinput
使用内置库 fileinput
import fileinput
第三种:使用 filecache
使用内置库 filecache,你可以用它来指定读取具体某一行,或者某几行,不指定就读取全部行。
import linecache
content = linecache.getlines('werobot.toml')
第四种:使用 codecs
使用 codecs.open 来读取
import codecs
file=codecs.open("README.md", 'r')
file.read()
第五种:使用 io 模块
使用 io 模块的 open 函数
import io
file=io.open("README.md")
file.read()
第六种:使用 os 模块
>>> 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()
方法二:使用偏函数来执行
power_2=partial(power, n=2)
power_2(2) # output: 4
power_2(3) # output: 9
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:]
运行效果如下
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()
方法五:使用类本身的字典
我相信很多人都会答错。
上面我们定义的是静态方法,静态方法并没有与实例进行绑定,因此静态方法是属于类的,但是不
是属于实例的,实例虽然有使用权(可以调用),但是并没有拥有权。
import sys
class Task:
@staticmethod
def pre_task():
print("running pre_task")
func = Task.__dict__.get("pre_task")
func.__func__()
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:]
方法七:从文本中编译运行
pre_task = """
print("running pre_task")
"""
exec(compile(pre_task, '<string>', 'exec'))
with open('source.txt') as f:
source = f.read()
exec(compile(source, 'source.txt', 'exec'))
方法八:使用 attrgetter 获取执行
class People:
def speak(self, dest):
print("Hello, %s" %dest)
p = People()
caller = attrgetter("speak")
caller(p)(" ")
方法九:使用 methodcaller 执行
class People:
def speak(self, dest):
print("Hello, %s" %dest)
第四章:魔法进阶扫盲
4.1 精通上下文管理器
with 这个关键字,对于每一学习Python的人,都不会陌生。
with open('test.txt') as f:
print(f.readlines())
what context manager?
基本语法
先理清几个概念
1. with open('test.txt') as f:
2. open('test.txt')
3. f
要自己实现这样一个上下文管理,要先知道上下文管理协议。
例如这个示例:
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===')
我们执行一下,通过日志的打印顺序。可以知道其执行过程。
===connect to resource===
===in operation===
===close resource connection===
学习时多问自己几个为什么,养成对一些细节的思考,有助于加深对知识点的理解。
为什么要使用上下文管理器?
1. 可以以一种更加优雅的方式,操作(创建/获取/释放)资源,如文件操作、数据库连接;
2. 可以以一种更加优雅的方式,处理异常;
第一种,我们上面已经以资源的连接为例讲过了。
而第二种,会被大多数人所忽略。这里会重点讲一下。
class Resource():
def __enter__(self):
print('===connect to resource===')
return self
def operate(self):
1/0
运行一下,惊奇地发现,居然不会报错。
在 写 __exit__ 函数时,需要注意的事,它必须要有这三个参数:
exc_type:异常类型
exc_val:异常值
exc_tb:异常的错误栈信息
当主逻辑代码没有报异常时,这三个参数将都为None。
how contextlib?
在上面的例子中,我们只是为了构建一个上下文管理器,却写了一个类。如果只是要实现一个简单
的功能,写一个类未免有点过于繁杂。这时候,我们就想,如果只写一个函数就可以实现上下文管
理器就好了。
这个点Python早就想到了。它给我们提供了一个装饰器,你只要按照它的代码协议来实现函数内
容,就可以将这个函数对象变成一个上下文管理器。
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
在被装饰函数里,必须是一个生成器(带有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
好像只要讲到上下文管理器,大多数人都会谈到打开文件这个经典的例子。
但是在实际开发中,可以使用到上下文管理器的例子也不少。我这边举个我自己的例子。
在OpenStack中,给一个虚拟机创建快照时,需要先创建一个临时文件夹,来存放这个本地快照镜
像,等到本地快照镜像创建完成后,再将这个镜像上传到Glance。然后删除这个临时目录。
虽然代码量很少,逻辑也不复杂,但是“ ”这个功能,在一个项目
中很多地方都需要用到,如果可以将这段逻辑处理写成一个工具函数作为一个上下文管理器,那代
码的复用率也大大提高。
代码是这样的
总结起来,使用上下文管理器有三个好处:
1. 提高代码的复用率;
2. 提高代码的优雅度;
3. 提高代码的可读性;
4.2 深入理解描述符
当你点进这篇文章时
你也许没学过描述符,甚至没听过描述符。
或者你对描述符只是一知半解
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
)
看起来一切都很合理
但是程序并不像人那么智能,不会自动根据使用场景判断数据的合法性,如果老师在录入成绩的时
候,不小心录入了将成绩录成了负数,或者超过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]")
def __repr__(self):
return "<Student: {}, math:{}, chinese: {}, english:{}>".format(
self.name, self.math, self.chinese, self.english
)
这下程序稍微有点人工智能了,能够自己明辨是非了。
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__ :控制删除操作。不会返回内容。
对描述符有了大概的了解后,你开始重写上面的方法。
class Score:
def __init__(self, default=0):
self._score = default
self._score = value
def __delete__(self):
del self._score
class Student:
math = Score(0)
chinese = Score(0)
english = Score(0)
实现的效果和前面的一样,可以对数据的合法性进行有效控制(字段类型、数值区间等)
到这里,你需要记住的只有一点,就是描述符给我们带来的编码上的便利,它在实现
、 的基本功能,同时有大大提高代码的复用率。
2. 描述符的访问规则
描述符分两种:
你一定会问,他们有什么区别呢?网上的讲解,我看过几个,很多都把一个简单的东西讲得复杂
了。
其实就一句话,数据描述器和非数据描述器的区别在于:它们相对于实例的字典的优先级不同。
如果实例字典中有与描述符同名的属性,那么:
描述符是数据描述符的话,优先使用数据描述符
描述符是非数据描述符的话,优先使用字典中的属性。
这边还是以上节的成绩管理的例子来说明,方便你理解。
##
class DataDes:
def __init__(self, default=0):
self._score = default
##
class NoDataDes:
def __init__(self, default=0):
self._score = default
class Student:
math = DataDes(0)
chinese = NoDataDes(0)
def __repr__(self):
return "<Student: {}, math:{}, chinese: {},>".format(
self.name, self.math, self.chinese)
讲完了数据描述符和非数据描述符,我们还需要了解的对象属性的查找规律。
3. 基于描述符如何实现property
经过上面的讲解,我们已经知道如何定义描述符,且明白了描述符是如何工作的。
正常人所见过的描述符的用法就是上面提到的那些,我想说的是那只是描述符协议最常见的应用之
一,或许你还不知道,其实有很多 Python 的特性的底层实现机制都是基于 的,比如我
们熟悉的 @property 、 @classmethod 、 @staticmethod 和 super 等。
先来说说 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]")
不过,从这份伪源码的魔法函数结构组成,可以大体知道其实现逻辑。
代码如下:
class TestProperty(object):
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]")
为了尽量让你少产生一点疑惑,我这里做两点说明:
说了这么多,还是运行一下,更加直观一点。
## TestProperty math
in setter
>>>
>>> s1.math = 90
in __set__
>>> s1.math
in __get__
90
4. 基于描述符如何实现staticmethod
class Test:
@staticmethod
def myfunc():
print("hello")
##
class Test:
def myfunc():
print("hello")
#
myfunc = staticmethod(myfunc)
@TestProperty
def math(self):
return self._math
math = TestProperty(fget=math)
调用这个方法可以知道,每调用一次,它都会经过描述符类的 __get__ 。
>>> Test.myfunc()
in staticmethod __get__
hello
>>> Test().myfunc()
in staticmethod __get__
hello
5. 基于描述符如何实现classmethod
class classmethod(object):
def __init__(self, f):
self.f = f
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
6. 所有实例共享描述符
通过以上内容的学习,你是不是觉得自己已经对描述符足够了解了呢?
可在这里,我想说以上的描述符代码都有问题。
问题在哪里呢?请看下面这个例子。
class Score:
def __init__(self, default=0):
self._value = default
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 里没有像前面那样写了构造函数,但是关键不在这儿,没写只是因为没必要写。
然后来看一下会出现什么样的问题呢
问题是不是来了?小明和小强的分数怎么可能是绑定的呢?这很明显与实际业务不符。
使用描述符给我们制造了便利,却无形中给我们带来了麻烦,难道这也是描述符的特性吗?
描述符是个很好用的特性,会出现这个问题,是由于我们之前写的描述符代码都是错误的。
描述符的机制,在我看来,只是抢占了访问顺序,而具体的逻辑却要因地制宜,视情况而定。
class Score:
def __init__(self, subject):
self.name = subject
class Student:
math = Score("math")
chinese = Score("chinese")
english = Score("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创建类
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(type)
<class 'type'>
>>> type(object)
<class 'type'>
>>> type(int)
<class 'type'>
>>> type(str)
<class 'type'>
如果要形象的来理解的话,就看下面这三行话。
str:用来创建字符串对象的类。
int:是用来创建整数对象的类。
type:是用来创建类对象的类。
反过来看
一个实例的类型,是类
一个类的类型,是元类
一个元类的类型,是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__
在这里可以借助 「单例的实现」举一个例子,你就清楚了
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%的开发人员是不需要
动态修改类的,因为这应该是框架才需要考虑的事。
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。
和 IntField 在这里的用法,叫做
StrField 。
简单来说呢, 可以实现对属性值的类型,范围等一切做约束,意思就是说变量id只能是
int类型,变量name只能是str类型,否则将会抛出异常。
那如何实现这两个 呢?请看代码。
import numbers
class Field:
pass
class IntField(Field):
def __init__(self, name):
self.name = name
self._value = None
class StrField(Field):
def __init__(self, name):
self.name = name
self._value = None
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 实例的创建过程:
这里会有几个问题。
init的参数是User实例时传入的,所以传入的id是int类型,name是str类型。看起来没啥问题,
若是这样,我上面的数据描述符就失效了,不能起约束作用。所以我们希望init接收到的id是
IntField类型,name是StrField类型。
同时,我们希望这些字段属性,能够自动归类到fields变量中。因为,做为BaseModel,它可不
是专门为User类服务的,它还要兼容各种各样的表。不同的表,表里有不同数量,不同属性的
字段,这些都要能自动类别并归类整理到一起。这是一个ORM框架最基本的。
我们希望对表名有两种选择,一个是User中若指定Meta信息,比如表名,就以此为表名,若未
指定就以类名的小写 做为表名。虽然BaseModel可以直接取到User的db_table属性,但是如果在
数据库业务逻辑中,加入这段复杂的逻辑,显然是很不优雅的。
下面就来看看,如何用元类来解决这些问题呢?请看代码。
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__ 有什么用?
class User:
def __new__(cls, *args, **kwargs):
print("in BaseClass")
return super().__new__(cls)
使用如下
>>> u = User('wangbm')
in BaseClass
in User
>>> u.name
'wangbm'
附录:参考文章
第五章:魔法开发技巧
5.1 嵌套上下文管理的另类写法
当我们要写一个嵌套的上下文管理器时,可能会这样写
import contextlib
@contextlib.contextmanager
def test_context(name):
print('enter, my name is {}'.format(name))
yield
with test_context('aaa'):
with test_context('bbb'):
print('========== in main ============')
输出结果如下
除此之外,你可知道,还有另一种嵌套写法
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)
这样的代码,可读性非常的差,很多人不想这么写,可又没有更好的写法。
输出如下
$ python demo.py
12
13
13
14
13
14
14
15
你会写吗?
如果你还说简单,你可以自己试一下。
...
如果你尝试后,仍然写不出来,那我给出自己的做法。
是不是傻了?iter 还有这种用法?这为啥是个死循环?
关于这个问题,你如果看中文网站,可能找不到相关资料。
原来iter有两种使用方法。
通常我们的认知是第一种,将一个列表转化为一个迭代器。
这又是一个知识点,int 是一个内建方法。通过看注释,可以看出它是有默认值0的。你可以在
console 模式下输入 int() 看看是不是返回0。
5.4 如何关闭异常自动关联上下文?
当你在处理异常时,由于处理不当或者其他问题,再次抛出另一个异常时,往外抛出的异常也会携
带原始的异常信息。
就像这样子。
try:
print(1 / 0)
except Exception as exc:
raise RuntimeError("Something bad happened")
从输出可以看到两个异常信息
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:
try:
print(1 / 0)
except Exception as exc:
raise RuntimeError("bad thing").with_traceback(exc)
最后,如果我想彻底关闭这个自动关联异常上下文的机制?有什么办法呢?
$ 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 自带的缓存机制不用白不用
缓存是一种将定量数据加以保存,以备迎合后续获取需求的处理方式,旨在加快数据获取的速度。
数据的生成过程可能需要经过计算,规整,远程获取等操作,如果是同一份数据需要多次使用,每
次都重新生成会大大浪费时间。所以,如果将计算或者远程请求等操作获得的数据缓存下来,会加
快后续的数据获取需求。
@functools.lru_cache(maxsize=None, typed=False)
参数解读:
maxsize:最多可以缓存多少个此函数的调用结果,如果为None,则无限制,设置为 2 的幂
时,性能最佳
typed:若为 True,则不同参数类型的调用将分别缓存。
举个例子
@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("big_file.txt", "r") as fp:
content = fp.read()
def read_from_file(filename):
with open(filename, "r") as fp:
yield fp.readline()
可如果这个文件内容就一行呢,一行就 10个G,其实你还是会一次性读取全部内容。
yield chunk
上面的代码,功能上已经没有问题了,但是代码看起来代码还是有些臃肿。
import "fmt"
func myfunc() {
fmt.Println("B")
}
func main() {
defer myfunc()
fmt.Println("A")
}
A
B
def callback():
print('B')
输出如下
A
B
5.8 如何快速计算函数运行时间
计算一个函数的运行时间,你可能会这样子做
import time
start = time.time()
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 重定向标准输出到日志
假设你有一个脚本,会执行一些任务,比如说集群健康情况的检查。
如果代码有问题,导致异常处理不足,最终检查失败,是很有可能将一些错误异常栈输出到标准错
误或标准输出上。
如何避免这种情况的发生呢?
我们可以这样做,把你的标准错误输出到日志文件中。
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 快速定位错误进入调试模式
当你在写一个程序时,最初的程序一定遇到不少零零散散的错误,这时候就免不了调试一波。
如此反复这样一个过程,直到最后程序没有异常。
你应该能够感受到这个过程有多繁锁,令人崩溃。
接下来介绍一种,可以让你不需要修改源代码,就可以在异常抛出时,快速切换到调试模式,进入
『案发现场』排查问题。
方法很简单,只需要你在执行脚本时,加入 -i 参考
如果你的程序没有任何问题,加上 -i 后又会有什么不一样呢?
5.11 在程序退出前执行代码的技巧
使用 atexit 这个内置模块,可以很方便的注册退出函数。
不管你在哪个地方导致程序崩溃,都会执行那些你注册过的函数。
示例如下
如果 clean() 函数有参数,那么你可以不用装饰器,而是直接调用
atexit.register(clean_1, 1, 2, 3='xxx') 。
如果程序是被你没有处理过的系统信号杀死的,那么注册的函数无法正常执行。
如果发生了严重的 Python 内部错误,你注册的函数无法正常执行。
如果你手动调用了 os._exit() ,你注册的函数无法正常执行。
5.12 逗号也有它的独特用法
第一个用法
元组的转化
print(func())
[root@localhost ~]# python3 demo.py
('ok',)
第二个用法r
print 的取消换行
5.13 如何在运行状态查看源代码?
那如果没有 IDE 呢?
当我们想使用一个函数时,如何知道这个函数需要接收哪些参数呢?
当我们在使用函数时出现问题的时候,如何通过阅读源代码来排查问题所在呢?
## demo.py
import inspect
print("===================")
print(inspect.getsource(add))
运行结果如下
$ python demo.py
===================
def add(x, y):
return x + y
5.14 单分派泛函数如何写?
在Python中只能实现基于单个(第一个)参数的数据类型来选择具体的实现方式,官方名称 是
single-dispatch 。你或许听不懂,说人话,就是可以实现第一个参数的数据类型不同,其调用的
函数也就不同。
单分派:根据一个参数的类型,以不同方式执行相同的操作的行为。
多分派:可根据多个参数的类型选择专门的函数的行为。
泛函数:多个函数绑在一起组合成一个泛函数。
这边举个简单的例子。
@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.
你可能会问,它有什么用呢?实际上真没什么用,你不用它或者不认识它也完全不影响你编码。
我这里举个例子,你可以感受一下。
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 让我爱不释手的用户环境
## 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
5.16 字符串的分割技巧
当我们对字符串进行分割时,且分割符是 \n ,有可能会出现这样一个窘境:
>>> 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]
在这里,介绍一种最优雅的反转方式,使用切片,不管你是字符串,还是列表,简直通杀。
$ cat test.log
hello, python
5.19 改变默认递归次数限制
上面才提到递归,大家都知道使用递归是有风险的,递归深度过深容易导致堆栈的溢出。如果你这
字符串太长啦,使用递归方式反转,就会出现问题。
那到底,默认递归次数限制是多少呢?
可以查,当然也可以自定义修改次数,退出即失效。
>>> sys.setrecursionlimit(2000)
>>> sys.getrecursionlimit()
2000
if else 用法可以说最基础的语法表达式之一,但是今天不是讲这个的。
else:
print("Does not exist")
给几个例子,你体会一下。
同样来几个例子。当不传参数时,就抛出异常。
test_try_else()
## Exception occurred...
test_try_else("ming")
## No Exception occurred...
可以看出,没有 try 里面的代码块没有抛出异常的,会正常走else。
5.21 字典访问不存在的key时不再报错
>>> profile={}
>>> profile["age"]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 'age'
defaultdict 接收一个工厂方法,工厂方法返回的对象就是字典的默认值。
常用的工厂方法有,我们常见的 int,str,bool 等
>>> a=int()
>>> a
0
>>>
>>> b=str()
>>> b
''
>>>
>>> c=bool()
>>> c
False
因为 defaultdict 可以这样子用。
5.22 如何实现函数的连续调用?
现在我想写一个函数可以实现把所有的数进行求和,并且可以达到反复调用的目的。
比如这样子。
>>> add(2)(3)(4)(5)(6)(7)
27
当只调用一次时,也必须适用。
>>> add(2)
2
5.23 如何实现字典的多级排序
在一个列表中,每个元素都是一个字典,里面的每个字典结构都是一样的。
>>> 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'}]
如果两名同学的成绩一样,那谁排在前面呢?
那就再额外再定个第二指标呗,成绩一样,就再看年龄,年龄小的,成绩还能一样,那不是更历害
嘛。
规则定下了:先按成绩降序,如果成绩一致,再按年龄降序。
问题来了,这样的规则,代码该如何实现呢?
>>> 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'}]
那如果一个降序,而另一个是升序,那又该怎么写呢?
还是以上面为例,我现在要实现先按成绩降序,如果成绩一致,再按年龄升序。可以这样写
>>> 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 '
>>>
对齐的思想其实就是在不足的位自动给你补上空格。
如果不想使用空格,可以指定你想要的字符进行填充,比如下面我用 0 来补全。
左对齐
>>> "a".ljust(10)
'a '
>>>
右对齐
>>> "a".rjust(10)
' a'
>>>
居中
>>> "a".center(10)
' a '
>>>
如果不想使用空格,而改用 0 来补齐呢?可以这样
在 Python 中,参数的种类,大概可以分为四种:
1. ,也叫 ,调用函数时一定指定的参数,并且在传参的时候必须按函数定义时的
顺序来
2. ,也叫 ,调用函数时,可以指定也可以不指定,不指定就默认的参数值来。
3. ,就是参数个数可变,可以是 0 个或者任意个,但是传参时不能指定参数名,通常使
用 *args 来表示。
4. ,就是参数个数可变,可以是 0 个或者任意个,但是传参时必须指定参数名,通常
使用 **kw 来表示
使用单独的 * ,可以后面的位置参数变成关键字参数,关键字参数在你传参时,必须要写参数
名,不然会报错。
比如有下面这样一个函数
使用 inspect 可以直接获取
利用 inspect 还可以检查传参是否匹配签名
5.27 如何进行版本的比较
使用 distutils
使用 packaging
5.28 如何捕获警告?(注意不是捕获异常)
1. 警告不是异常
你是不是经常在使用一些系统库或者第三方模块的时候,会出现一些既不是异常也不是错误的警告
信息?
这些警告信息,有时候非常多,对于新手容易造成一些误判,以为是程序出错了。
实则不然,异常和错误,都是程序出现了一些问题,但是警告不同,他的紧急程度非常之低,以致
于大多数的警告都是可以直接忽略的。
2. 警告能捕获吗
能捕获的只有错误异常,但是通过一系列的操作后,你可以将这些警告转化为异常。
这样一来,你就可以像异常一样去捕获他们了。
在不进行任何设置的情况下,警告会直接打印在终端上。
3. 捕获警告方法一
在 warnings 中有一系列的过滤器。
值 处置
"default" 为发出警告的每个位置(模块+行号)打印第一个匹配警告
"error" 将匹配警告转换为异常
"ignore" 从不打印匹配的警告
"always" 总是打印匹配的警告
"module" 为发出警告的每个模块打印第一次匹配警告(无论行号如何)
"once" 无论位置如何,仅打印第一次出现的匹配警告
之后你就可以通过异常的方式去捕获警告了。
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)
5. 捕获警告方法三
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 如何禁止对象深拷贝?
但是有的时候,我们希望基于我们的类实例化后对象,禁止被深拷贝,这时候就要用到 Python 的
魔法方法了。
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
此时你如果对它进行深度拷贝的话,会发现返回的永远都是原来的对象
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 = []
附上 :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 参数
p = People()
print(p.speak)
p.speak = speak
print(p.speak)
输出结果如下,区别非常明显
这种方法,只能用在替换不与实例绑定的静态方法上,不然你每次调用的时候,就得手动传入实例
本身,但这样调用就会变得非常怪异。
思路二:利用 im_func
思路三:非常危险的字节码替换
表层不行,但这个方法在字节码层面却是可行的
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()
5.32 如何动态创建函数?
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 默认参数最好不为可变对象
函数的参数分三种
可变参数
默认参数
关键字参数
当你在传递默认参数时,有新手很容易踩雷的一个坑。
先来看一个示例
func('iphone')
func('xiaomi', item_list=['oppo','vivo'])
func('huawei')
在这里,你可以暂停一下,思考一下会输出什么?
思考过后,你的答案是否和下面的一致呢
['iphone']
['oppo', 'vivo', 'xiaomi']
['iphone', 'huawei']
如果是,那你可以跳过这部分内容,如果不是,请接着往下看,这里来分析一下。
诸如 += 和 *= 这些运算符,叫做 增量赋值运算符。
这里使用用 += 举例,以下两种写法,在效果上是等价的。
##
a = 1 ; a += 1
##
a = 1; a = a + 1
这两种写法有什么区别呢?
所以在能使用增量赋值的时候尽量使用它。
1. 吐槽问题
pprint 你应该很熟悉了吧?
随便在搜索引擎上搜索如何打印漂亮的字典或者格式化字符串时,大部分人都会推荐你使用这货
。
[{"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
"}]
好像有点效果,真的是 “神器”呀。
确实是有好点了,但是看到下面这些,我崩溃了,我哪里知道这是什么鬼,难道是我太菜了吗?当
我是计算机呀?
u'\u6597\u9c7c271934\u8d70\u8fc7\u8def\u8fc7\u4e0d\u8981\u9519\u8fc7\uff0c\u8fd9\u91
cc\u6709\u6700\u597d\u7684\u9e21\u513f'
除此之外,我们知道 json 的严格要求必须使用 双引号,而我定义字典时,也使用了双引号了,为
什么打印出来的为什么是 单引号?我也太难了吧,我连自己的代码都无法控制了吗?
2. 解决问题
打印中文
## 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,本来我可以选择
不用的,因为有更好的替代方案(这个后面会讲)。
但是我出于猎奇,正好前两天不是写过一篇关于 编码 的文章吗,我自认为自己对于 编码还是掌握
比较熟练的,就想着来解决一下这个问题。
以下是我的解决方案,供你参考:
## 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)
MyPrettyPrinter().pprint(info)
输出如下,已经解决了中文的显示问题:
打印双引号
现在我们要修改输出的内容,也就是将输出的单引号替换成双引号。
有了思路,就可以开始写代码了,如下:
## 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('\'', '"')
尝试执行了下,我的天,怎么是这样子的。
[
{
"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
}
]
就像下面这样。
知道了问题所在,再修改下代码
## 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('\'', '"'),
MyPrettyPrinter(stream=MyStream()).pprint(info)
终于成功了,太不容易了吧。
3. 何必折腾
代价就是我整整花费了两个小时,才得以实现,而对于小白来说,可能没有信心,也没有耐心去做
这样的事情。
打印中文
其实无法打印中文,是 Python 2 引来的大坑,并不能全怪 pprint 。
具体的代码示例如下:
json.dumps 的关键参数有两个:
indent=4:以 4 个空格缩进单位
ensure_ascii=False:接收非 ASCII 编码的字符,这样才能使用中文
4. 总结一下
本文的分享就到这里,阅读本文,我认为你可以获取到三个知识点
6.5 变量名与保留关键冲突怎么办?
所有的编程语言都有一些保留关键字,这是代码得以编译/解释的基础。
有了这些关键字就组成了语法,当你的变量名和这些保留关键字冲突时,该怎么办呢?
使用这些关键字来做为变量名,是会报语法错误的。
有了 PEP8 做为指导,我们可以这样子写了
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
如果有一些属性,是父类自己独有的,不想被子类继承,该怎么写呢?
可以在属性前面加两个下划线,两个类的定义如下
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 不要直接调用类的私有方法)我们知道了私有的变量或函数,是不能
直接调用的,需要用这样的形式才能访问 _ __ 。
验证过程如下:
6.7 利用 any 代替 for 循环
在某些场景下,我们需要判断是否满足某一组集合中任意一个条件
found = False
for thing in things:
if thing == other_thing:
found = True
break
6.8 不同条件分支里应减少重合度
如下是一个简单的条件语句模型
if A:
<code_block_1>
elif B:
<code_block_2>
else:
<code_block_3>
这边举个例子,下面这段代码中
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()
字典是由一个个的键值对组成的,如果你遍历字典时只需要访问键,用不到值,有很多同学会用下
面这种方式:
在这种情况下,不需要调用keys(),因为遍历字典时的默认行为是遍历键。
现在,该代码更加简洁,易于阅读,并且避免调用函数会带来性能改进。
简化序列比较
我们经常要做的是在尝试对列表或序列进行操作之前检查列表或序列是否包含元素。
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 给模块的私有属性上保险
保护对象
对于这些变量或者对象,就可以在前面其名字前加上下划线,只要在变量名前加上下划线,就属于
"保护对象"。
_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']
另一种是导入时指定“受保护属性”名。
6.12 变量不能与保留关键字重名
在 Python 中有很多的保留关键字,这些关键字的使用,不需要我们定义,也不需要我们导入,只
要你进入到了 Python 的环境中,就可以立即使用。
而很尴尬的是,如果你在日常编码中,不经意地用到其中的一些关键字,就会产生冲突。
比如说 class,这个有类别的意思,可能你也想使用它来作为变量名,如果直接使用,会发生冲突
更好的做法是,使用下划线来避免冲突
def type_obj_class(name,class_):
pass
def tag(name,*content,class_):
pass
第七章:神奇魔法模块
7.1 远程登陆服务器的最佳利器
在 shell 环境中,我们是这样子做的。
然后你会发现,你的输出有很多你并不需要,但是又不去不掉的一些信息(也许有方法,请留言交
流),类似这样
1. 使用 subprocess
用 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...>
痛点一:需要额外安装 sshpass(如果不免密的话)
痛点二:干扰信息太多,数据清理、格式化相当麻烦
痛点三:代码实现不够优雅(有点土),可读性太差
痛点四:ssh 连接不能复用,一次连接仅能执行一次
痛点五:代码无法全平台,仅能在 Linux 和 OSX 上使用
还真的被我找到了两个
sh.ssh
Paramiko
2. 使用 sh.ssh
首先来介绍第一个, sh.ssh
sh是一个可以让你通过函数的调用来完成 Linxu/OSX 系统命令的一个库,非常好用,关于它有机
会也写篇介绍。
今天只介绍它其中的一个函数: ssh
通常两台机器互访,为了方便,可设置免密登陆,这样就不需要输入密码。
这段代码可以实现免密登陆,并执行我们的命令 ls -l
但有可能 ,我们并不想设置互信免密,为了使这段代码更通用,我假定我们没有设置免密,只能
使用密码进行登陆。
这就好办了呀。
完整代码如下:
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")
这是官方文档(http://amoffat.github.io/sh/tutorials/interacting_with_processes.html?highlight=ssh
)给的一些信息,写的一个demo。
尝试运行后,发现程序会一直在运行中,永远不会返回,不会退出,回调函数也永远不会进入。
以上这个问题,只有在需要输入密码才会出现,如果设置了机器互信是没有问题的。
##
print(my_server.ls())
## sleep top
time.sleep(5)
## +1 -1
print(my_server.ifconfig())
至此,我离 “卒”,就差最后一根稻草了。
3. 使用 paramiko
你可以通过如下命令去安装它
$ python3 -m pip install paramiko
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()
方法1 是传统的连接服务器、执行命令、关闭的一个操作,多个操作需要连接多次,无法复用连接
[痛点四]。
有时候需要登录上服务器执行多个操作,比如执行命令、上传/下载文件,方法1 则无法实现,那
就可以使用 transport 的方法。
import paramiko
##
trans = paramiko.Transport(("xx.xx.xx.xx", 22))
trans.connect(username="root", password="you_passwd")
##
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()
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()
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)
##
trans.close()
以上四种方法,可以帮助你实现远程登陆服务器执行命令,如果需要复用连接:一次连接执行多次
命令,可以使用 方法二 和 方法四
用完后,记得关闭连接。
实现 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
import paramiko
import paramiko
## read()
print(stdout.read())
ssh.close()
4. 写在最后
就像这样子,天呐,密集恐惧症要犯了都
上面这段 traceback
只有黑白两个颜色,无法像代码高亮那样,对肉眼实现太不友好了
无法直接显示报错的代码,排查问题慢人一步,效率太低
那有没有一种办法,可以解决这些问题呢?
通过这条命令你可以安装它
1. 环境要求
2. 效果对比
3. 配置全局可用
答案是显而易见的。
pretty_errors 和其他库不太一样,在一定程度上(如果你使用全局配置的话),它并不是开箱即用
的,你在使用它之前可能需要做一下配置。
$ python3 -m pretty_errors
配置完成后,你再运行任何脚本,traceback 都会自动美化了。
在 PyCharm 中也会
唯一的缺点就是,原先在 PyCharm 中的 traceback 可以直接点击 直接跳转到对应错误文
件代码行,而你如果是在 VSCode 可以使用 下面自定义配置的方案解决这个问题(下面会讲到,参
数是: display_link )。
那怎么取消之前的配置呢?
4. 单文件中使用
取消全局可用后,你可以根据自己需要,在你需要使用 pretty-errors 的脚本文件中导入
pretty_errors ,即可使用
import pretty_errors
就像这样
import pretty_errors
def foo():
1/0
if __name__ == "__main__":
foo()
值得一提的是,使用这种方式,若是你的脚本中,出现语法错误,则输出的异常信息还是按照之前
的方式展示,并不会被美化。
5. 自定义设置
比如
它并没有展示报错文件的绝对路径,这将使我们很难定位到是哪个文件里的代码出现错误。
如果能把具体报错的代码,给我们展示在终端屏幕上,就不需要我们再到源码文件中排查原因
了。
它足够开放,支持自定义配置,可以由你选择你需要展示哪些信息,怎么展示?
这里举一个例子
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()
5.1 设置颜色
header_color :设置标题行的颜色。
timestamp_color :设置时间戳颜色
default_color :设置默认的颜色
filename_color :设置文件名颜色
line_number_color :设置行号颜色。
function_color :设置函数颜色。
link_color :设置链接的颜色。
BLACK :黑色
GREY :灰色
RED :红色
GREEN :绿色
YELLOW :黄色
BLUE :蓝色
MAGENTA :品红色
CYAN :蓝绿色
WHITE :白色
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
为了避免由于一些网络或等其他不可控因素,而引起的功能性问题。比如在发送请求时,会因为网
络不稳定,往往会有请求超时的问题。
这种情况下,我们通常会在代码中加入重试的代码。重试的代码本身不难实现,但如何写得优雅、
易用,是我们要考虑的问题。
1. 在什么情况下才进行重试?
2. 重试几次呢?
3. 重试多久后结束?
4. 每次重试的间隔多长呢?
5. 重试失败后的回调?
在使用它之前 ,先要安装它
最基本的重试
无条件重试,重试之间无间隔
@retry
def test_retry():
print(" ...")
raise Exception
test_retry()
无条件重试,但是在重试之前要等待 2 秒
@retry(wait=wait_fixed(2))
def test_retry():
print(" ...")
raise Exception
test_retry()
设置停止基本条件
只重试7 次
@retry(stop=stop_after_attempt(7))
def test_retry():
print(" ...")
raise Exception
test_retry()
重试 10 秒后不再重试
@retry(stop=stop_after_delay(10))
def test_retry():
print(" ...")
raise Exception
test_retry()
或者上面两个条件满足一个就结束重试
@retry(stop=(stop_after_delay(10) | stop_after_attempt(7)))
def test_retry():
print(" ...")
raise Exception
test_retry()
设置何时进行重试
在出现特定错误/异常(比如请求超时)的情况下,再进行重试
@retry(retry=retry_if_exception_type(exceptions.Timeout))
def test_retry():
print(" ...")
raise exceptions.Timeout
test_retry()
在满足自定义条件时,再进行重试。
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()
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,而不是最根本的原因。
@retry(stop=stop_after_attempt(7), reraise=True)
def test_retry():
print(" ...")
raise Exception
test_retry()
设置回调函数
当最后一次重试失败后,可以执行一个回调函数
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 规整字符串提取数据的神器
从一段指定的字符串中,取得期望的数据,正常人都会想到正则表达式吧?
写过正则表达式的人都知道,正则表达式入门不难,写起来也容易。
但是正则表达式几乎没有可读性可言,维护起来,真的会让人抓狂,别以为这段正则是你写的就可
以驾驭它,过个一个月你可能就不认识它了。
1. 真实案例
先以逗号分隔开来,再以等号分隔取出值来?
你不防可以尝试一下,写出来的代码应该和我想象的一样,没有一丝美感而言。
我来给你展示一下,我是怎么做的?
2. parse 的结果
parse 的结果只有两种结果:
1. 没有匹配上,parse 的值为None
>>> 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'
>>> 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
你可以这样写。
除了将其转为 整型,还有其他格式吗?
内置的格式还有很多,比如
匹配时间
更多类型请参考官方文档:
s Whitespace str
S Non-whitespace str
D Non-digit str
te RFC2822 e-mail format date/time e.g. Mon, 20 Jan 1972 10:21:36 +1000 datetime
5. 提取时去除空格
去除两边空格
去除左边空格
去除右边空格
>>> parse('hello {:<} , hello python', 'hello world , hello python')
<Result (' world',) {}>
6. 大小写敏感开关
如果你需要区分大小写,那可以加个参数,演示如下:
7. 匹配字符数
精确匹配:指定最大字符数
模糊匹配:指定最小字符数
若要在精准/模糊匹配的模式下,再进行格式转换,可以这样写
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. 自定义类型的转换
匹配到的字符串,会做为参数传入对应的函数
比如我们之前讲过的,将字符串转整型
其等价于
10 总结一下
parse 库在字符串解析处理场景中提供的便利,肉眼可见,上手简单。
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))
结果:
我们来加一行代码,再看看结果:
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))
结果:
是不是快了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所产生的函数
只能对指定的类型的参数进行运算:
如果希望JIT能针对所有类型的参数进行运算,可以使用autojit:
%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所支持的所有类型:
工作原理
numba的通过meta模块解析Python函数的ast语法树,对各个变量添加相应的类型信息。然后调用
llvmpy生成机器码,最后再生成机器码的Python调用接口。
meta模块
通过研究numba的工作原理,我们可以找到许多有用的工具。例如meta模块可在程序源码、ast语
法树以及Python二进制码之间进行相互转换。下面看一个例子:
decompile_func能将函数的代码对象反编译成ast语法树,而str_ast能直观地显示ast语法树,使用这
两个工具学习Python的ast语法树是很有帮助的。
而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。
下面调用decompile_pyc将tmp.pyc显示为源代码:
llvmpy模块
LLVM是一个动态编译器,llvmpy则可以通过Python调用LLVM动态地创建机器码。直接通过llvmpy
创建机器码是比较繁琐的,例如下面的程序创建一个计算两个整数之和的函数,并调用它计算结
果。
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
对于每个程序开发者来说,调试几乎是必备技能。
代码写到一半卡住了,不知道这个函数执行完的返回结果是怎样的?调试一下看看
代码运行到一半报错了,什么情况?怎么跟预期的不一样?调试一下看看
调试的方法多种多样,不同的调试方法适合不同的场景和人群。
除了以上,今天明哥再给你介绍一款非常好用的调试工具,它能在一些场景下,大幅度提高调试的
效率, 那就是 PySnooper ,它在 Github 上已经收到了 13k 的 star,获得大家的一致好评。
1. 快速安装
执行下面这些命令进行安装 PySnooper
$ python3 -m pip install pysnooper
##
$ conda install -c conda-forge pysnooper
##
$ yay -S python-pysnooper
2. 简单案例
import pysnooper
@pysnooper.snoop()
def demo_func():
profile = {}
profile["name"] = " "
profile["age"] = 27
profile["gender"] = "male"
return profile
def main():
profile = demo_func()
main()
现在我使用终端命令行的方式来运行它
代码的片段、行号等信息,以及每一行代码是何时调用的?
函数内局部变量的值如何变化的?何时新增了变量,何时修改了变量。
函数的返回值是什么?
运行函数消耗了多少时间?
而作为开发者,要得到这些如此详细的调试信息,你需要做的非常简单,只要给你想要调试的函数
上带上一顶帽子(装饰器) -- @pysnooper.snoop() 即可。
3. 详细使用
2.1 重定向到日志文件
@pysnooper.snoop() 不加任何参数时,会默认将调试的信息输出到标准输出。
这种情况下,你可以将调试信息重定向输出到某一日志文件中,方便追溯排查。
@pysnooper.snoop(output='/var/log/debug.log')
def demo_func():
...
2.2 跟踪非局部变量值
PySnooper 是以函数为单位进行调试的,它默认只会跟踪函数体内的局部变量,若想跟踪全局变
量,可以给 @pysnooper.snoop() 加上 watch 参数
@pysnooper.snoop(watch=('out["foo"]'))
def demo_func():
...
@pysnooper.snoop(watch_explode=('foo', 'bar'))
def demo_func():
...
2.3 设置跟踪函数的深度
@pysnooper.snoop(depth=2)
def demo_func():
...
2.4 设置调试日志的前缀
当你在使用 PySnooper 跟踪多个函数时,调试的日志会显得杂乱无章,不方便查看。
在这种情况下,PySnooper 提供了一个参数,方便你为不同的函数设置不同的标志,方便你在查看
日志时进行区分。
效果如下
2.5 设置最大的输出长度
当然你也可以通过指定参数 进行修改
@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.snoop(thread_info=True)
def demo_func():
...
效果如下
2.7 自定义对象的格式输出
在这个元组里,你可以指定特定类型的对象以特定格式进行输出。
这边我举个例子。
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_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():
...
7.7 比open更好用、更优雅的读取文件
1. 从标准输入中读取
## demo.py
import fileinput
效果如下,不管你输入什么,程序会自动读取并再打印一次,像个复读机似的。
$ python demo.py
hello
hello
python
python
2. 单独打开一个文件
import fileinput
hello
world
执行后就会输出如下
$ python demo.py
a.txt 1 : hello
a.txt 2 : world
3. 批量打开多个文件
import fileinput
$ cat a.txt
hello
world
$ cat b.txt
hello
python
$ python demo.py
a.txt 1 : hello
a.txt 2 : world
b.txt 3 : hello
b.txt 4 : python
如果想要在读取多个文件的时候,也能读取原文件的真实行号,可以使用
fileinput.filelineno() 方法
import fileinput
运行后,输出如下
$ python demo.py
a.txt 1 : hello
a.txt 2 : world
b.txt 1 : hello
b.txt 2 : python
import fileinput
import glob
运行效果如下
$ python demo.py
-------------------- Reading b.txt... --------------------
1: HELLO
2: PYTHON
-------------------- Reading a.txt... --------------------
3: HELLO
4: WORLD
4. 读取的同时备份文件
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. 标准输出重定向替换
请看如下一段测试代码
import fileinput
$ 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
6. 不得不介绍的方法
fileinput.filenam()
返回当前被读取的文件名。 在第一行被读取之前,返回 None 。
fileinput.fileno()
返回以整数表示的当前文件“文件描述符”。 当未打开文件时(处在第一行和文件之间),返回
-1 。
fileinput.lineno()
返回已被读取的累计行号。 在第一行被读取之前,返回 0 。 在最后一个文件的最后一行被读
取之后,返回该行的行号。
fileinput.filelineno()
返回当前文件中的行号。 在第一行被读取之前,返回 0 。 在最后一个文件的最后一行被读取
之后,返回此文件中该行的行号。
7. 进阶一点的玩法
fileinput 为我们内置了两种勾子供你使用
1. fileinput.hook_compressed(*filename*, *mode*)
2. fileinput.hook_encoded(*encoding*, *errors=None*)
如果你自己的场景比较特殊,以上的三种勾子都不能满足你的要求,你也可以自定义。
这边我举个例子来抛砖引玉下
直接将这个函数传给 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="")
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
案例三:利用fileinput将CRLF文件转为LF
import sys
import fileinput
案例四:配合 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
#--- ---
=> 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)
9. 写在最后
7.8 像操作路径一样,操作嵌套字典
下边是一个简单的使用案例
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}}}
print(dpath.util.values(data, "/foo/bar/*"))
## output: [10, 20, [], ['red', 'buggy', 'bumpers']]
7.9 读取文件中任意行的数据
它允许从任何文件中获取任意行,同时尝试使用缓存进行内部优化,这是一种常见的情况,即从单
个文件读取多行。它被 traceback 模块用来检索包含在格式化回溯中的源代码行。
这是一个简单的例子。
如果你指定的行数超过了文件原有的行数,该函数也不会抛出错误,而是返回空字符串。
>>>
7.10 让你的装饰器写得更轻松的神库
本篇文章会为你介绍的是一个已经存在十三年,但是依旧不红的库 decorator,好像很少有人知道
他的存在一样。
这个库可以帮你做什么呢 ?
本篇文章不会过多的向你介绍装饰器的基本知识,我会默认你知道什么是装饰器,并且懂得如何写
一个简单的装饰器。
不了解装饰器的可以先去阅读我之前写的文章,非常全且详细的介绍了装饰器的各种实现方法。
1. 常规的装饰器
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()
装饰器使用起来,似乎有些高端和魔幻,对于一些重复性的功能,往往我们会封装成一个装饰器函
数。
在定义一个装饰器的时候,我们都需要像上面一样机械性的写一个嵌套的函数,对装饰器原理理解
不深的初学者,往往过段时间就会忘记如何定义装饰器。
2. 使用神库
这里,明哥要教你一个更加简单的方法,使用这个方法呢,你需要先安装一个库 : decorator ,
使用 pip 可以很轻易地去安装它
从库的名称不难看出,这是一个专门用来解决装饰器问题的第三方库。
有了它之后,你会惊奇的发现,以后自己定义的装饰器,就再也不需要写嵌套的函数了
@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()
这种写法,不得不说,更加符合直觉,代码的逻辑也更容易理解。
3. 带参数的装饰器可用?
装饰器根据有没有携带参数,可以分为两种
第一种:不带参数,最简单的示例,上面已经举例
def decorator(func):
def wrapper(*args, **kw):
func(*args, **kw)
return wrapper
第二种:带参数,这就相对复杂了,理解起来了也不是那么容易。
下面是一个官方的示例
@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
可以看到
不难推断,只要你在装饰函数中第二个参数开始,使用了非可变参数的写法,这些参数就可以做为
装饰器调用时的参数。
4. 签名问题有解决?
先来看一个例子
def wrapper(func):
def inner_function():
pass
return inner_function
@wrapper
def wrapped():
pass
print(wrapped.__name__)
#inner_function
为什么会这样子?不是应该返回 func 吗?
def wrapper(func):
def inner_function():
pass
return inner_function
def wrapped():
pass
print(wrapper(wrapped).__name__)
#inner_function
目前,我们可以看到当一个函数被装饰器装饰过后,它的签名信息会发生变化(譬如上面看到的函
数名)
那如何避免这种情况的产生?
def wrapper(func):
@wraps(func)
def inner_function():
pass
return inner_function
@wrapper
def wrapped():
pass
print(wrapped.__name__)
## wrapped
写个例子来验证一下就知道啦
@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__)
是一个提高装饰器编码效率的第三方库,它适用于对装饰器原理感到困惑的新手,可
decorator
以让你很轻易的写出更符合人类直觉的代码。对于带参数装饰器的定义,是非常复杂的,它需要要
写多层的嵌套函数,并且需要你熟悉各个参数的传递路径,才能保证你写出来的装饰器可以正常使
用。这时候,只要用上 decorator 这个库,你就可以很轻松的写出一个带参数的装饰器。同时你
也不用担心他会出现签名问题,这些它都为你妥善的处理好了。
这么棒的一个库,推荐你使用起来。
7.11 国际化模块,让翻译更优雅
国际化与本地化
本地化是指使一个国际化的软件为了在某个特定地区使用而进行实际翻译的过程。
国际化的软件具备这样一种能力,当软件被移植到不同的语言及地区时,软件本身不用做内部工程
上的改变或修正。
gettext 模块
很多系统中都内置了 gettext 模块
如果你在 ubuntu系统中,可能需要如下命令进行安装
简单示例演示
首先新建一个目录
$ mkdir -p locale/zh_CN/LC_MESSAGES
然后在这个目录下新建一个 hello.po 文件
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()
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.enterabs()
datetime模块来生成,或者其他你熟悉的方式。后面的参数和 sch.enter() 完全一样。
如果你要运行的函数带有多个参数或者默认参数,那么可以使用下面的方式传入参数:
import sched
import time
import datetime
argument参数对应的元组存放普通参数,kwargs对应的字典存放带参数名的参数。
本文来源于:公众号"未闻Code",作者:kingname
7.13 实现字典的点式操作
我举个简单的例子吧
是不是开始觉得忍无可忍了?
>>> profile.name
'iswbm'
是的,今天这篇文章就是跟大家分享一种可以直接使用 . 访问和操作字典的一个黑魔法库 --
munch 。
1. 安装方法
使用如下命令进行安装
2. 简单示例
3. 兼容字典的所有操作
首先是:增删改查
##
>>> 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. 设置返回默认值
>>> profile = {}
>>> profile["name"]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 'name'
>>> profile = {}
>>> profile.get("name", "undefined")
'undefined'
5. 工厂函数自动创建key
6. 序列化的支持
转换成 JSON
转换成 YAML
>>>
>>> print(yaml.safe_dump(munch_obj))
bar: 100
foo:
lol: true
msg: hello
7. 说说局限性
也正是因为这样,原生字典至今还是不可替代的存在。
赞赏作者
原创不易,请个咖啡,交个朋友