建站系统软件有哪些,wordpress微信登录页面模板,合肥哪家公司做网站,广告营销模式Python简介
Python是著名的“龟叔”Guido van Rossum在1989年圣诞节期间#xff0c;为了打发无聊的圣诞节而编写的一个编程语言。
现在#xff0c;全世界差不多有600多种编程语言#xff0c;但流行的编程语言也就那么20来种。如果你听说过TIOBE排行榜#xff0c;你就能知…Python简介
Python是著名的“龟叔”Guido van Rossum在1989年圣诞节期间为了打发无聊的圣诞节而编写的一个编程语言。
现在全世界差不多有600多种编程语言但流行的编程语言也就那么20来种。如果你听说过TIOBE排行榜你就能知道编程语言的大致流行程度。这是最近10年最常用的10种编程语言的变化图 总的来说这几种编程语言各有千秋。C语言是可以用来编写操作系统的贴近硬件的语言所以C语言适合开发那些追求运行速度、充分发挥硬件性能的程序。而Python是用来编写应用程序的高级编程语言。
当你用一种语言开始作真正的软件开发时你除了编写代码外还需要很多基本的已经写好的现成的东西来帮助你加快开发进度。比如说要编写一个电子邮件客户端如果先从最底层开始编写网络协议相关的代码那估计一年半载也开发不出来。高级编程语言通常都会提供一个比较完善的基础代码库让你能直接调用比如针对电子邮件协议的SMTP库针对桌面环境的GUI库在这些已有的代码库的基础上开发一个电子邮件客户端几天就能开发出来。
Python就为我们提供了非常完善的基础代码库覆盖了网络、文件、GUI、数据库、文本等大量内容被形象地称作“内置电池batteries included”。用Python开发许多功能不必从零编写直接使用现成的即可。
除了内置的库外Python还有大量的第三方库也就是别人开发的供你直接使用的东西。当然如果你开发的代码通过很好的封装也可以作为第三方库给别人使用。
许多大型网站就是用Python开发的例如YouTube、Instagram还有国内的豆瓣。很多大公司包括Google、Yahoo等甚至NASA美国航空航天局都大量地使用Python。
龟叔给Python的定位是“优雅”、“明确”、“简单”所以Python程序看上去总是简单易懂初学者学Python不但入门容易而且将来深入下去可以编写那些非常非常复杂的程序。
总的来说Python的哲学就是简单优雅尽量写容易看明白的代码尽量写少的代码。如果一个资深程序员向你炫耀他写的晦涩难懂、动不动就几万行的代码你可以尽情地嘲笑他。
那Python适合开发哪些类型的应用呢
首选是网络应用包括网站、后台服务等等
其次是许多日常需要的小工具包括系统管理员需要的脚本任务等等
另外就是把其他语言开发的程序再包装起来方便使用。
最后说说Python的缺点。
任何编程语言都有缺点Python也不例外。优点说过了那Python有哪些缺点呢
第一个缺点就是运行速度慢和C程序相比非常慢因为Python是解释型语言你的代码在执行时会一行一行地翻译成CPU能理解的机器码这个翻译过程非常耗时所以很慢。而C程序是运行前直接编译成CPU能执行的机器码所以非常快。
但是大量的应用程序不需要这么快的运行速度因为用户根本感觉不出来。例如开发一个下载MP3的网络应用程序C程序的运行时间需要0.001秒而Python程序的运行时间需要0.1秒慢了100倍但由于网络更慢需要等待1秒你想用户能感觉到1.001秒和1.1秒的区别吗这就好比F1赛车和普通的出租车在北京三环路上行驶的道理一样虽然F1赛车理论时速高达400公里但由于三环路堵车的时速只有20公里因此作为乘客你感觉的时速永远是20公里。 第二个缺点就是代码不能加密。如果要发布你的Python程序实际上就是发布源代码这一点跟C语言不同C语言不用发布源代码只需要把编译后的机器码也就是你在Windows上常见的xxx.exe文件发布出去。要从机器码反推出C代码是不可能的所以凡是编译型的语言都没有这个问题而解释型的语言则必须把源码发布出去。
这个缺点仅限于你要编写的软件需要卖给别人挣钱的时候。好消息是目前的互联网时代靠卖软件授权的商业模式越来越少了靠网站和移动应用卖服务的模式越来越多了后一种模式不需要把源码给别人。
再说了现在如火如荼的开源运动和互联网自由开放的精神是一致的互联网上有无数非常优秀的像Linux一样的开源代码我们千万不要高估自己写的代码真的有非常大的“商业价值”。那些大公司的代码不愿意开放的更重要的原因是代码写得太烂了一旦开源就没人敢用他们的产品了。 当然Python还有其他若干小缺点请自行忽略就不一一列举了。
安装Python
因为Python是跨平台的它可以运行在Windows、Mac和各种Linux/Unix系统上。在Windows上写Python程序放到Linux上也是能够运行的。
要开始学习Python编程首先就得把Python安装到你的电脑里。安装后你会得到Python解释器就是负责运行Python程序的一个命令行交互环境还有一个简单的集成开发环境。
安装Python 3.5
目前Python有两个版本一个是2.x版一个是3.x版这两个版本是不兼容的。由于3.x版越来越普及我们的教程将以最新的Python 3.5版本为基础。请确保你的电脑上安装的Python版本是最新的3.5.x这样你才能无痛学习这个教程。
在Mac上安装Python
如果你正在使用Mac系统是OS X 10.8~10.10那么系统自带的Python版本是2.7。要安装最新的Python 3.5有两个方法
方法一从Python官网下载Python 3.5的安装程序网速慢的同学请移步国内镜像双击运行并安装
方法二如果安装了Homebrew直接通过命令brew install python3安装即可。
在Linux上安装Python
如果你正在使用Linux那我可以假定你有Linux系统管理经验自行安装Python 3应该没有问题否则请换回Windows系统。
对于大量的目前仍在使用Windows的同学如果短期内没有打算换Mac就可以继续阅读以下内容。
在Windows上安装Python
首先根据你的Windows版本64位还是32位从Python的官方网站下载Python 3.5对应的64位安装程序或32位安装程序网速慢的同学请移步国内镜像然后运行下载的EXE安装包 特别要注意勾上Add Python 3.5 to PATH然后点“Install Now”即可完成安装。
默认会安装到C:\Python35目录下然后打开命令提示符窗口敲入python后会出现两种情况
情况一 看到上面的画面就说明Python安装成功
你看到提示符就表示我们已经在Python交互式环境中了可以输入任何Python代码回车后会立刻得到执行结果。现在输入exit()并回车就可以退出Python交互式环境直接关掉命令行窗口也可以。
情况二得到一个错误
‘python’ 不是内部或外部命令也不是可运行的程序或批处理文件。这是因为Windows会根据一个Path的环境变量设定的路径去查找python.exe如果没找到就会报错。如果在安装时漏掉了勾选Add Python 3.5 to PATH那就要手动把python.exe所在的路径添加到Path中。
如果你不知道怎么修改环境变量建议把Python安装程序重新运行一遍务必记得勾上Add Python 3.5 to PATH。
小结
学会如何把Python安装到计算机中并且熟练打开和退出Python交互式环境。
在Windows上运行Python时请先启动命令行然后运行python。
在Mac和Linux上运行Python时请打开终端然后运行python3。
Python解释器
当我们编写Python代码时我们得到的是一个包含Python代码的以.py为扩展名的文本文件。要运行代码就需要Python解释器去执行.py文件。
由于整个Python语言从规范到解释器都是开源的所以理论上只要水平够高任何人都可以编写Python解释器来执行Python代码当然难度很大。事实上确实存在多种Python解释器。
CPython
当我们从Python官方网站下载并安装好Python 3.5后我们就直接获得了一个官方版本的解释器CPython。这个解释器是用C语言开发的所以叫CPython。在命令行下运行python就是启动CPython解释器。
CPython是使用最广的Python解释器。教程的所有代码也都在CPython下执行。
IPython
IPython是基于CPython之上的一个交互式解释器也就是说IPython只是在交互方式上有所增强但是执行Python代码的功能和CPython是完全一样的。好比很多国产浏览器虽然外观不同但内核其实都是调用了IE。
CPython用作为提示符而IPython用In [序号]:作为提示符。
PyPy
PyPy是另一个Python解释器它的目标是执行速度。PyPy采用JIT技术对Python代码进行动态编译注意不是解释所以可以显著提高Python代码的执行速度。
绝大部分Python代码都可以在PyPy下运行但是PyPy和CPython有一些是不同的这就导致相同的Python代码在两种解释器下执行可能会有不同的结果。如果你的代码要放到PyPy下执行就需要了解PyPy和CPython的不同点。
Jython
Jython是运行在Java平台上的Python解释器可以直接把Python代码编译成Java字节码执行。
IronPython
IronPython和Jython类似只不过IronPython是运行在微软.Net平台上的Python解释器可以直接把Python代码编译成.Net的字节码。
小结
Python的解释器很多但使用最广泛的还是CPython。如果要和Java或.Net平台交互最好的办法不是用Jython或IronPython而是通过网络调用来交互确保各程序之间的独立性。
本教程的所有代码只确保在CPython 3.5版本下运行。请务必在本地安装CPython也就是从Python官方网站下载的安装程序。
第一个Python程序
现在了解了如何启动和退出Python的交互式环境我们就可以正式开始编写Python代码了。
在写代码之前请千万不要用“复制”-“粘贴”把代码从页面粘贴到你自己的电脑上。写程序也讲究一个感觉你需要一个字母一个字母地把代码自己敲进去在敲代码的过程中初学者经常会敲错代码所以你需要仔细地检查、对照才能以最快的速度掌握如何写程序。 在交互式环境的提示符下直接输入代码按回车就可以立刻得到代码执行结果。现在试试输入100200看看计算结果是不是300 100200
300很简单吧任何有效的数学计算都可以算出来。
如果要让Python打印出指定的文字可以用print()函数然后把希望打印的文字用单引号或者双引号括起来但不能混用单引号和双引号 print(hello, world)
hello, world这种用单引号或者双引号括起来的文本在程序中叫字符串今后我们还会经常遇到。
最后用exit()退出Python我们的第一个Python程序完成唯一的缺憾是没有保存下来下次运行时还要再输入一遍代码。
小结
在Python交互式命令行下可以直接输入代码然后执行并立刻得到结果。
使用文本编辑器
在Python的交互式命令行写程序好处是一下就能得到结果坏处是没法保存下次还想运行的时候还得再敲一遍。
所以实际开发的时候我们总是使用一个文本编辑器来写代码写完了保存为一个文件这样程序就可以反复运行了。
现在我们就把上次的hello, world程序用文本编辑器写出来保存下来。
那么问题来了文本编辑器到底哪家强
推荐两款文本编辑器
一个是Sublime Text免费使用但是不付费会弹出提示框 一个是Notepad免费使用有中文界面 请注意用哪个都行但是绝对不能用Word和Windows自带的记事本。Word保存的不是纯文本文件而记事本会自作聪明地在文件开始的地方加上几个特殊字符UTF-8 BOM结果会导致程序运行出现莫名其妙的错误。
安装好文本编辑器后输入以下代码
print(hello, world)注意print前面不要有任何空格。然后选择一个目录例如C:\work把文件保存为hello.py就可以打开命令行窗口把当前目录切换到hello.py所在目录就可以运行这个程序了
C:\workpython hello.py
hello, world也可以保存为别的名字比如first.py但是必须要以.py结尾其他的都不行。此外文件名只能是英文字母、数字和下划线的组合。
如果当前目录下没有hello.py这个文件运行python hello.py就会报错
C:\Users\IEUserpython hello.py
python: cant open file hello.py: [Errno 2] No such file or directory报错的意思就是无法打开hello.py这个文件因为文件不存在。这个时候就要检查一下当前目录下是否有这个文件了。如果hello.py存放在另外一个目录下要首先用cd命令切换当前目录 命令行模式和Python交互模式
请注意区分命令行模式和Python交互模式。
看到类似C:\是在Windows提供的命令行模式 在命令行模式下可以执行python进入Python交互式环境也可以执行python hello.py运行一个.py文件。
看到是在Python交互式环境下 在Python交互式环境下只能输入Python代码并立刻执行。
此外在命令行模式运行.py文件和在Python交互式环境下直接运行Python代码有所不同。Python交互式环境会把每一行Python代码的结果自动打印出来但是直接运行Python代码却不会。
例如在Python交互式环境下输入 100 200 300
600直接可以看到结果600。
但是写一个calc.py的文件内容如下
100 200 300然后在命令行模式下执行
C:\workpython calc.py发现什么输出都没有。
这是正常的。想要输出结果必须自己用print()打印出来。把calc.py改造一下
print(100 200 300)再执行就可以看到结果
C:\workpython calc.py
600直接运行py文件
还有同学问能不能像.exe文件那样直接运行.py文件呢在Windows上是不行的但是在Mac和Linux上是可以的方法是在.py文件的第一行加上一个特殊的注释
#!/usr/bin/env python3print(hello, world)然后通过命令给hello.py以执行权限
$ chmod ax hello.py就可以直接运行hello.py了比如在Mac下运行 小结
用文本编辑器写Python程序然后保存为后缀为.py的文件就可以用Python直接运行这个程序了。
Python的交互模式和直接运行.py文件有什么区别呢
直接输入python进入交互模式相当于启动了Python解释器但是等待你一行一行地输入源代码每输入一行就执行一行。
直接运行.py文件相当于启动了Python解释器然后一次性把.py文件的源代码给执行了你是没有机会以交互的方式输入源代码的。
用Python开发程序完全可以一边在文本编辑器里写代码一边开一个交互式命令窗口在写代码的过程中把部分代码粘到命令行去验证事半功倍前提是得有个27的超大显示器
参考源码
hello.py
Python代码运行助手
Python代码运行助手可以让你在线输入Python代码然后通过本机运行的一个Python脚本来执行代码。原理如下
在网页输入代码 点击Run按钮代码被发送到本机正在运行的Python代码运行助手 Python代码运行助手将代码保存为临时文件然后调用Python解释器执行代码 网页显示代码执行结果 下载
点击右键目标另存为learning.py
备用下载地址learning.py
运行
在存放learning.py的目录下运行命令
C:\Users\michael\Downloads python learning.py如果看到Ready for Python code on port 39093...表示运行成功不要关闭命令行窗口最小化放到后台运行即可 试试效果
需要支持HTML5的浏览器
IE 9FirefoxChromeSarafi
# 测试代码:
----
print(Hello, world)输入和输出
输出
用print()在括号中加上字符串就可以向屏幕上输出指定的文字。比如输出hello, world用代码实现如下 print(hello, world)print()函数也可以接受多个字符串用逗号“,”隔开就可以连成一串输出 print(The quick brown fox, jumps over, the lazy dog)
The quick brown fox jumps over the lazy dogprint()会依次打印每个字符串遇到逗号“,”会输出一个空格因此输出的字符串是这样拼起来的 print()也可以打印整数或者计算结果 print(300)
300print(100 200)
300因此我们可以把计算100 200的结果打印得更漂亮一点 print(100 200 , 100 200)
100 200 300注意对于100 200Python解释器自动计算出结果300但是100 200 是字符串而非数学公式Python把它视为字符串请自行解释上述打印结果。
输入
现在你已经可以用print()输出你想要的结果了。但是如果要让用户从电脑输入一些字符怎么办Python提供了一个input()可以让用户输入字符串并存放到一个变量里。比如输入用户的名字 name input()
Michael当你输入name input()并按下回车后Python交互式命令行就在等待你的输入了。这时你可以输入任意字符然后按回车后完成输入。
输入完成后不会有任何提示Python交互式命令行又回到状态了。那我们刚才输入的内容到哪去了答案是存放到name变量里了。可以直接输入name查看变量内容 name
Michael什么是变量请回忆初中数学所学的代数基础知识
设正方形的边长为a则正方形的面积为a x a。把边长a看做一个变量我们就可以根据a的值计算正方形的面积比如
若a2则面积为a x a 2 x 2 4
若a3.5则面积为a x a 3.5 x 3.5 12.25。
在计算机程序中变量不仅可以为整数或浮点数还可以是字符串因此name作为一个变量就是一个字符串。
要打印出name变量的内容除了直接写name然后按回车外还可以用print()函数 print(name)
Michael有了输入和输出我们就可以把上次打印hello, world的程序改成有点意义的程序了
name input()
print(hello,, name)运行上面的程序第一行代码会让用户输入任意字符作为自己的名字然后存入name变量中第二行代码会根据用户的名字向用户说hello比如输入Michael
C:\Workspace python hello.py
Michael
hello, Michael但是程序运行的时候没有任何提示信息告诉用户“嘿赶紧输入你的名字”这样显得很不友好。幸好input()可以让你显示一个字符串来提示用户于是我们把代码改成
name input(please enter your name: )
print(hello,, name)再次运行这个程序你会发现程序一运行会首先打印出please enter your name:这样用户就可以根据提示输入名字后得到hello, xxx的输出
C:\Workspace python hello.py
please enter your name: Michael
hello, Michael每次运行该程序根据用户输入的不同输出结果也会不同。
在命令行下输入和输出就是这么简单。
小结
任何计算机程序都是为了执行一个特定的任务有了输入用户才能告诉计算机程序所需的信息有了输出程序运行后才能告诉用户任务的结果。
输入是Input输出是Output因此我们把输入输出统称为Input/Output或者简写为IO。
input()和print()是在命令行下面最基本的输入和输出但是用户也可以通过其他更高级的图形界面完成输入和输出比如在网页上的一个文本框输入自己的名字点击“确定”后在网页上看到输出信息。
练习
请利用print()输出1024 * 768 xxx
# -*- coding: utf-8 -*-
----
print(???)参考源码
do_input.py
Python基础
Python是一种计算机编程语言。计算机编程语言和我们日常使用的自然语言有所不同最大的区别就是自然语言在不同的语境下有不同的理解而计算机要根据编程语言执行任务就必须保证编程语言写出的程序决不能有歧义所以任何一种编程语言都有自己的一套语法编译器或者解释器就是负责把符合语法的程序代码转换成CPU能够执行的机器码然后执行。Python也不例外。
Python的语法比较简单采用缩进方式写出来的代码就像下面的样子
# print absolute value of an integer:
a 100
if a 0:print(a)
else:print(-a)以#开头的语句是注释注释是给人看的可以是任意内容解释器会忽略掉注释。其他每一行都是一个语句当语句以冒号:结尾时缩进的语句视为代码块。
缩进有利有弊。好处是强迫你写出格式化的代码但没有规定缩进是几个空格还是Tab。按照约定俗成的管理应该始终坚持使用4个空格的缩进。
缩进的另一个好处是强迫你写出缩进较少的代码你会倾向于把一段很长的代码拆分成若干函数从而得到缩进较少的代码。
缩进的坏处就是“复制粘贴”功能失效了这是最坑爹的地方。当你重构代码时粘贴过去的代码必须重新检查缩进是否正确。此外IDE很难像格式化Java代码那样格式化Python代码。
最后请务必注意Python程序是大小写敏感的如果写错了大小写程序会报错。
小结
Python使用缩进来组织代码块请务必遵守约定俗成的习惯坚持使用4个空格的缩进。
在文本编辑器中需要设置把Tab自动转换为4个空格确保不混用Tab和空格。
数据类型和变量
数据类型
计算机顾名思义就是可以做数学计算的机器因此计算机程序理所当然地可以处理各种数值。但是计算机能处理的远不止数值还可以处理文本、图形、音频、视频、网页等各种各样的数据不同的数据需要定义不同的数据类型。在Python中能够直接处理的数据类型有以下几种
整数
Python可以处理任意大小的整数当然包括负整数在程序中的表示方法和数学上的写法一模一样例如1100-80800等等。
计算机由于使用二进制所以有时候用十六进制表示整数比较方便十六进制用0x前缀和0-9a-f表示例如0xff000xa5b4c3d2等等。
浮点数
浮点数也就是小数之所以称为浮点数是因为按照科学记数法表示时一个浮点数的小数点位置是可变的比如1.23x109和12.3x108是完全相等的。浮点数可以用数学写法如1.233.14-9.01等等。但是对于很大或很小的浮点数就必须用科学计数法表示把10用e替代1.23x109就是1.23e9或者12.3e80.000012可以写成1.2e-5等等。
整数和浮点数在计算机内部存储的方式是不同的整数运算永远是精确的除法难道也是精确的是的而浮点数运算则可能会有四舍五入的误差。
字符串
字符串是以单引号或双引号括起来的任意文本比如abcxyz等等。请注意或本身只是一种表示方式不是字符串的一部分因此字符串abc只有abc这3个字符。如果本身也是一个字符那就可以用括起来比如Im OK包含的字符是Im空格OK这6个字符。
如果字符串内部既包含又包含怎么办可以用转义字符\来标识比如
I\m \OK\!表示的字符串内容是
Im OK!转义字符\可以转义很多字符比如\n表示换行\t表示制表符字符\本身也要转义所以\\表示的字符就是\可以在Python的交互式命令行用print()打印字符串看看 print(I\m ok.)
Im ok.print(I\m learning\nPython.)
Im learning
Python.print(\\\n\\)
\
\如果字符串里面有很多字符都需要转义就需要加很多\为了简化Python还允许用r表示内部的字符串默认不转义可以自己试试 print(\\\t\\)
\ \print(r\\\t\\)
\\\t\\如果字符串内部有很多换行用\n写在一行里不好阅读为了简化Python允许用...的格式表示多行内容可以自己试试 print(line1
... line2
... line3)
line1
line2
line3上面是在交互式命令行内输入注意在输入多行内容时提示符由变为...提示你可以接着上一行输入。如果写成程序就是
print(line1
line2
line3)多行字符串...还可以在前面加上r使用请自行测试。
布尔值
布尔值和布尔代数的表示完全一致一个布尔值只有True、False两种值要么是True要么是False在Python中可以直接用True、False表示布尔值请注意大小写也可以通过布尔运算计算出来 True
TrueFalse
False3 2
True3 5
False布尔值可以用and、or和not运算。
and运算是与运算只有所有都为Trueand运算结果才是True True and True
TrueTrue and False
FalseFalse and False
False5 3 and 3 1
Trueor运算是或运算只要其中有一个为Trueor运算结果就是True True or True
TrueTrue or False
TrueFalse or False
False5 3 or 1 3
Truenot运算是非运算它是一个单目运算符把True变成FalseFalse变成True not True
Falsenot False
Truenot 1 2
True布尔值经常用在条件判断中比如
if age 18:print(adult)
else:print(teenager)空值
空值是Python里一个特殊的值用None表示。None不能理解为0因为0是有意义的而None是一个特殊的空值。
此外Python还提供了列表、字典等多种数据类型还允许创建自定义数据类型我们后面会继续讲到。
变量
变量的概念基本上和初中代数的方程变量是一致的只是在计算机程序中变量不仅可以是数字还可以是任意数据类型。
变量在程序中就是用一个变量名表示了变量名必须是大小写英文、数字和_的组合且不能用数字开头比如
a 1变量a是一个整数。
t_007 T007变量t_007是一个字符串。
Answer True变量Answer是一个布尔值True。
在Python中等号是赋值语句可以把任意数据类型赋值给变量同一个变量可以反复赋值而且可以是不同类型的变量例如
a 123 # a是整数
print(a)
a ABC # a变为字符串
print(a)这种变量本身类型不固定的语言称之为动态语言与之对应的是静态语言。静态语言在定义变量时必须指定变量类型如果赋值的时候类型不匹配就会报错。例如Java是静态语言赋值语句如下// 表示注释
int a 123; // a是整数类型变量
a ABC; // 错误不能把字符串赋给整型变量和静态语言相比动态语言更灵活就是这个原因。
请不要把赋值语句的等号等同于数学的等号。比如下面的代码
x 10
x x 2如果从数学上理解x x 2那无论如何是不成立的在程序中赋值语句先计算右侧的表达式x 2得到结果12再赋给变量x。由于x之前的值是10重新赋值后x的值变成12。
最后理解变量在计算机内存中的表示也非常重要。当我们写
a ABC时Python解释器干了两件事情 在内存中创建了一个ABC的字符串 在内存中创建了一个名为a的变量并把它指向ABC。
也可以把一个变量a赋值给另一个变量b这个操作实际上是把变量b指向变量a所指向的数据例如下面的代码
a ABC
b a
a XYZ
print(b)最后一行打印出变量b的内容到底是ABC呢还是XYZ如果从数学意义上理解就会错误地得出b和a相同也应该是XYZ但实际上b的值是ABC让我们一行一行地执行代码就可以看到到底发生了什么事
执行a ABC解释器创建了字符串ABC和变量a并把a指向ABC 执行b a解释器创建了变量b并把b指向a指向的字符串ABC 执行a XYZ解释器创建了字符串XYZ并把a的指向改为XYZ但b并没有更改 所以最后打印变量b的结果自然是ABC了。
常量
所谓常量就是不能变的变量比如常用的数学常数π就是一个常量。在Python中通常用全部大写的变量名表示常量
PI 3.14159265359但事实上PI仍然是一个变量Python根本没有任何机制保证PI不会被改变所以用全部大写的变量名表示常量只是一个习惯上的用法如果你一定要改变变量PI的值也没人能拦住你。
最后解释一下整数的除法为什么也是精确的。在Python中有两种除法一种除法是/ 10 / 3
3.3333333333333335/除法计算结果是浮点数即使是两个整数恰好整除结果也是浮点数 9 / 3
3.0还有一种除法是//称为地板除两个整数的除法仍然是整数 10 // 3
3你没有看错整数的地板除//永远是整数即使除不尽。要做精确的除法使用/就可以。
因为//除法只取结果的整数部分所以Python还提供一个余数运算可以得到两个整数相除的余数 10 % 3
1无论整数做//除法还是取余数结果永远是整数所以整数运算结果永远是精确的。
练习
请打印出以下变量的值
n 123
f 456.789
s1 Hello, world
s2 Hello, \Adam\
s3 rHello, Bart
s4 rHello,
Lisa!小结
Python支持多种数据类型在计算机内部可以把任何数据都看成一个“对象”而变量就是在程序中用来指向这些数据对象的对变量赋值就是把数据和变量给关联起来。
注意Python的整数没有大小限制而某些语言的整数根据其存储长度是有大小限制的例如Java对32位整数的范围限制在-2147483648-2147483647。
Python的浮点数也没有大小限制但是超出一定范围就直接表示为inf无限大。
字符串和编码
字符编码
我们已经讲过了字符串也是一种数据类型但是字符串比较特殊的是还有一个编码问题。
因为计算机只能处理数字如果要处理文本就必须先把文本转换为数字才能处理。最早的计算机在设计时采用8个比特bit作为一个字节byte所以一个字节能表示的最大的整数就是255二进制11111111十进制255如果要表示更大的整数就必须用更多的字节。比如两个字节可以表示的最大整数是655354个字节可以表示的最大整数是4294967295。
由于计算机是美国人发明的因此最早只有127个字母被编码到计算机里也就是大小写英文字母、数字和一些符号这个编码表被称为ASCII编码比如大写字母A的编码是65小写字母z的编码是122。
但是要处理中文显然一个字节是不够的至少需要两个字节而且还不能和ASCII编码冲突所以中国制定了GB2312编码用来把中文编进去。
你可以想得到的是全世界有上百种语言日本把日文编到Shift_JIS里韩国把韩文编到Euc-kr里各国有各国的标准就会不可避免地出现冲突结果就是在多语言混合的文本中显示出来会有乱码。 因此Unicode应运而生。Unicode把所有语言都统一到一套编码里这样就不会再有乱码问题了。
Unicode标准也在不断发展但最常用的是用两个字节表示一个字符如果要用到非常偏僻的字符就需要4个字节。现代操作系统和大多数编程语言都直接支持Unicode。
现在捋一捋ASCII编码和Unicode编码的区别ASCII编码是1个字节而Unicode编码通常是2个字节。
字母A用ASCII编码是十进制的65二进制的01000001
字符0用ASCII编码是十进制的48二进制的00110000注意字符0和整数0是不同的
汉字中已经超出了ASCII编码的范围用Unicode编码是十进制的20013二进制的01001110 00101101。
你可以猜测如果把ASCII编码的A用Unicode编码只需要在前面补0就可以因此A的Unicode编码是00000000 01000001。
新的问题又出现了如果统一成Unicode编码乱码问题从此消失了。但是如果你写的文本基本上全部是英文的话用Unicode编码比ASCII编码需要多一倍的存储空间在存储和传输上就十分不划算。
所以本着节约的精神又出现了把Unicode编码转化为“可变长编码”的UTF-8编码。UTF-8编码把一个Unicode字符根据不同的数字大小编码成1-6个字节常用的英文字母被编码成1个字节汉字通常是3个字节只有很生僻的字符才会被编码成4-6个字节。如果你要传输的文本包含大量英文字符用UTF-8编码就能节省空间
字符ASCIIUnicodeUTF-8A0100000100000000 0100000101000001中x01001110 0010110111100100 10111000 10101101
从上面的表格还可以发现UTF-8编码有一个额外的好处就是ASCII编码实际上可以被看成是UTF-8编码的一部分所以大量只支持ASCII编码的历史遗留软件可以在UTF-8编码下继续工作。
搞清楚了ASCII、Unicode和UTF-8的关系我们就可以总结一下现在计算机系统通用的字符编码工作方式
在计算机内存中统一使用Unicode编码当需要保存到硬盘或者需要传输的时候就转换为UTF-8编码。
用记事本编辑的时候从文件读取的UTF-8字符被转换为Unicode字符到内存里编辑完成后保存的时候再把Unicode转换为UTF-8保存到文件 浏览网页的时候服务器会把动态生成的Unicode内容转换为UTF-8再传输到浏览器 所以你看到很多网页的源码上会有类似meta charsetUTF-8 /的信息表示该网页正是用的UTF-8编码。
Python的字符串
搞清楚了令人头疼的字符编码问题后我们再来研究Python的字符串。
在最新的Python 3版本中字符串是以Unicode编码的也就是说Python的字符串支持多语言例如 print(包含中文的str)
包含中文的str对于单个字符的编码Python提供了ord()函数获取字符的整数表示chr()函数把编码转换为对应的字符 ord(A)
65ord(中)
20013chr(66)
Bchr(25991)
文如果知道字符的整数编码还可以用十六进制这么写str \u4e2d\u6587
中文两种写法完全是等价的。
由于Python的字符串类型是str在内存中以Unicode表示一个字符对应若干个字节。如果要在网络上传输或者保存到磁盘上就需要把str变为以字节为单位的bytes。
Python对bytes类型的数据用带b前缀的单引号或双引号表示
x bABC要注意区分ABC和bABC前者是str后者虽然内容显示得和前者一样但bytes的每个字符都只占用一个字节。
以Unicode表示的str通过encode()方法可以编码为指定的bytes例如 ABC.encode(ascii)
bABC中文.encode(utf-8)
b\xe4\xb8\xad\xe6\x96\x87中文.encode(ascii)
Traceback (most recent call last):File stdin, line 1, in module
UnicodeEncodeError: ascii codec cant encode characters in position 0-1: ordinal not in range(128)纯英文的str可以用ASCII编码为bytes内容是一样的含有中文的str可以用UTF-8编码为bytes。含有中文的str无法用ASCII编码因为中文编码的范围超过了ASCII编码的范围Python会报错。
在bytes中无法显示为ASCII字符的字节用\x##显示。
反过来如果我们从网络或磁盘上读取了字节流那么读到的数据就是bytes。要把bytes变为str就需要用decode()方法 bABC.decode(ascii)
ABCb\xe4\xb8\xad\xe6\x96\x87.decode(utf-8)
中文要计算str包含多少个字符可以用len()函数 len(ABC)
3len(中文)
2len()函数计算的是str的字符数如果换成byteslen()函数就计算字节数 len(bABC)
3len(b\xe4\xb8\xad\xe6\x96\x87)
6len(中文.encode(utf-8))
6可见1个中文字符经过UTF-8编码后通常会占用3个字节而1个英文字符只占用1个字节。
在操作字符串时我们经常遇到str和bytes的互相转换。为了避免乱码问题应当始终坚持使用UTF-8编码对str和bytes进行转换。
由于Python源代码也是一个文本文件所以当你的源代码中包含中文的时候在保存源代码时就需要务必指定保存为UTF-8编码。当Python解释器读取源代码时为了让它按UTF-8编码读取我们通常在文件开头写上这两行
#!/usr/bin/env python3
# -*- coding: utf-8 -*-第一行注释是为了告诉Linux/OS X系统这是一个Python可执行程序Windows系统会忽略这个注释
第二行注释是为了告诉Python解释器按照UTF-8编码读取源代码否则你在源代码中写的中文输出可能会有乱码。
申明了UTF-8编码并不意味着你的.py文件就是UTF-8编码的必须并且要确保文本编辑器正在使用UTF-8 without BOM编码 如果.py文件本身使用UTF-8编码并且也申明了# -*- coding: utf-8 -*-打开命令提示符测试就可以正常显示中文 格式化
最后一个常见的问题是如何输出格式化的字符串。我们经常会输出类似亲爱的xxx你好你xx月的话费是xx余额是xx之类的字符串而xxx的内容都是根据变量变化的所以需要一种简便的格式化字符串的方式。 在Python中采用的格式化方式和C语言是一致的用%实现举例如下 Hello, %s % world
Hello, worldHi, %s, you have $%d. % (Michael, 1000000)
Hi, Michael, you have $1000000.你可能猜到了%运算符就是用来格式化字符串的。在字符串内部%s表示用字符串替换%d表示用整数替换有几个%?占位符后面就跟几个变量或者值顺序要对应好。如果只有一个%?括号可以省略。
常见的占位符有
%d整数%f浮点数%s字符串%x十六进制整数
其中格式化整数和浮点数还可以指定是否补0和整数与小数的位数 %2d-%02d % (3, 1)3-01%.2f % 3.1415926
3.14如果你不太确定应该用什么%s永远起作用它会把任何数据类型转换为字符串 Age: %s. Gender: %s % (25, True)
Age: 25. Gender: True有些时候字符串里面的%是一个普通字符怎么办这个时候就需要转义用%%来表示一个% growth rate: %d %% % 7
growth rate: 7 %练习
小明的成绩从去年的72分提升到了今年的85分请计算小明成绩提升的百分点并用字符串格式化显示出xx.x%只保留小数点后1位
# -*- coding: utf-8 -*-s1 72
s2 85
----
r ???
print(??? % r)小结
Python 3的字符串使用Unicode直接支持多语言。
str和bytes互相转换时需要指定编码。最常用的编码是UTF-8。Python当然也支持其他编码方式比如把Unicode编码成GB2312 中文.encode(gb2312)
\xd6\xd0\xce\xc4但这种方式纯属自找麻烦如果没有特殊业务要求请牢记仅使用UTF-8编码。
格式化字符串的时候可以用Python的交互式命令行测试方便快捷。
参考源码
the_string.py
使用list和tuple
list
Python内置的一种数据类型是列表list。list是一种有序的集合可以随时添加和删除其中的元素。
比如列出班里所有同学的名字就可以用一个list表示 classmates [Michael, Bob, Tracy]classmates
[Michael, Bob, Tracy]变量classmates就是一个list。用len()函数可以获得list元素的个数 len(classmates)
3用索引来访问list中每一个位置的元素记得索引是从0开始的 classmates[0]
Michaelclassmates[1]
Bobclassmates[2]
Tracyclassmates[3]
Traceback (most recent call last):File stdin, line 1, in module
IndexError: list index out of range当索引超出了范围时Python会报一个IndexError错误所以要确保索引不要越界记得最后一个元素的索引是len(classmates) - 1。
如果要取最后一个元素除了计算索引位置外还可以用-1做索引直接获取最后一个元素 classmates[-1]
Tracy以此类推可以获取倒数第2个、倒数第3个 classmates[-2]
Bobclassmates[-3]
Michaelclassmates[-4]
Traceback (most recent call last):File stdin, line 1, in module
IndexError: list index out of range当然倒数第4个就越界了。
list是一个可变的有序表所以可以往list中追加元素到末尾 classmates.append(Adam)classmates
[Michael, Bob, Tracy, Adam]也可以把元素插入到指定的位置比如索引号为1的位置 classmates.insert(1, Jack)classmates
[Michael, Jack, Bob, Tracy, Adam]要删除list末尾的元素用pop()方法 classmates.pop()
Adamclassmates
[Michael, Jack, Bob, Tracy]要删除指定位置的元素用pop(i)方法其中i是索引位置 classmates.pop(1)
Jackclassmates
[Michael, Bob, Tracy]要把某个元素替换成别的元素可以直接赋值给对应的索引位置 classmates[1] Sarahclassmates
[Michael, Sarah, Tracy]list里面的元素的数据类型也可以不同比如 L [Apple, 123, True]list元素也可以是另一个list比如 s [python, java, [asp, php], scheme]len(s)
4要注意s只有4个元素其中s[2]又是一个list如果拆开写就更容易理解了 p [asp, php]s [python, java, p, scheme]要拿到php可以写p[1]或者s[2][1]因此s可以看成是一个二维数组类似的还有三维、四维……数组不过很少用到。
如果一个list中一个元素也没有就是一个空的list它的长度为0 L []len(L)
0tuple
另一种有序列表叫元组tuple。tuple和list非常类似但是tuple一旦初始化就不能修改比如同样是列出同学的名字 classmates (Michael, Bob, Tracy)现在classmates这个tuple不能变了它也没有append()insert()这样的方法。其他获取元素的方法和list是一样的你可以正常地使用classmates[0]classmates[-1]但不能赋值成另外的元素。
不可变的tuple有什么意义因为tuple不可变所以代码更安全。如果可能能用tuple代替list就尽量用tuple。
tuple的陷阱当你定义一个tuple时在定义的时候tuple的元素就必须被确定下来比如 t (1, 2)t
(1, 2)如果要定义一个空的tuple可以写成() t ()t
()但是要定义一个只有1个元素的tuple如果你这么定义 t (1)t
1定义的不是tuple是1这个数这是因为括号()既可以表示tuple又可以表示数学公式中的小括号这就产生了歧义因此Python规定这种情况下按小括号进行计算计算结果自然是1。
所以只有1个元素的tuple定义时必须加一个逗号,来消除歧义 t (1,)t
(1,)Python在显示只有1个元素的tuple时也会加一个逗号,以免你误解成数学计算意义上的括号。
最后来看一个“可变的”tuple t (a, b, [A, B])t[2][0] Xt[2][1] Yt
(a, b, [X, Y])这个tuple定义的时候有3个元素分别是ab和一个list。不是说tuple一旦定义后就不可变了吗怎么后来又变了
别急我们先看看定义的时候tuple包含的3个元素 当我们把list的元素A和B修改为X和Y后tuple变为 表面上看tuple的元素确实变了但其实变的不是tuple的元素而是list的元素。tuple一开始指向的list并没有改成别的list所以tuple所谓的“不变”是说tuple的每个元素指向永远不变。即指向a就不能改成指向b指向一个list就不能改成指向其他对象但指向的这个list本身是可变的
理解了“指向不变”后要创建一个内容也不变的tuple怎么做那就必须保证tuple的每一个元素本身也不能变。
练习
请用索引取出下面list的指定元素
# -*- coding: utf-8 -*-L [[Apple, Google, Microsoft],[Java, Python, Ruby, PHP],[Adam, Bart, Lisa]
]
----
# 打印Apple:
print(?)
# 打印Python:
print(?)
# 打印Lisa:
print(?)小结
list和tuple是Python内置的有序集合一个可变一个不可变。根据需要来选择使用它们。
参考源码
the_list.py
the_tuple.py
条件判断
条件判断
计算机之所以能做很多自动化的任务因为它可以自己做条件判断。
比如输入用户年龄根据年龄打印不同的内容在Python程序中用if语句实现
age 20
if age 18:print(your age is, age)print(adult)根据Python的缩进规则如果if语句判断是True就把缩进的两行print语句执行了否则什么也不做。
也可以给if添加一个else语句意思是如果if判断是False不要执行if的内容去把else执行了
age 3
if age 18:print(your age is, age)print(adult)
else:print(your age is, age)print(teenager)注意不要少写了冒号:。
当然上面的判断是很粗略的完全可以用elif做更细致的判断
age 3
if age 18:print(adult)
elif age 6:print(teenager)
else:print(kid)elif是else if的缩写完全可以有多个elif所以if语句的完整形式就是
if 条件判断1:执行1
elif 条件判断2:执行2
elif 条件判断3:执行3
else:执行4if语句执行有个特点它是从上往下判断如果在某个判断上是True把该判断对应的语句执行后就忽略掉剩下的elif和else所以请测试并解释为什么下面的程序打印的是teenager
age 20
if age 6:print(teenager)
elif age 18:print(adult)
else:print(kid)if判断条件还可以简写比如写
if x:print(True)只要x是非零数值、非空字符串、非空list等就判断为True否则为False。
再议 input
最后看一个有问题的条件判断。很多同学会用input()读取用户的输入这样可以自己输入程序运行得更有意思
birth input(birth: )
if birth 2000:print(00前)
else:print(00后)输入1982结果报错
Traceback (most recent call last):File stdin, line 1, in module
TypeError: unorderable types: str() int()这是因为input()返回的数据类型是strstr不能直接和整数比较必须先把str转换成整数。Python提供了int()函数来完成这件事情
s input(birth: )
birth int(s)
if birth 2000:print(00前)
else:print(00后)再次运行就可以得到正确地结果。但是如果输入abc呢又会得到一个错误信息
Traceback (most recent call last):File stdin, line 1, in module
ValueError: invalid literal for int() with base 10: abc原来int()函数发现一个字符串并不是合法的数字时就会报错程序就退出了。
如何检查并捕获程序运行期的错误呢后面的错误和调试会讲到。
练习
小明身高1.75体重80.5kg。请根据BMI公式体重除以身高的平方帮小明计算他的BMI指数并根据BMI指数
低于18.5过轻18.5-25正常25-28过重28-32肥胖高于32严重肥胖
用if-elif判断并打印结果
# -*- coding: utf-8 -*-height 1.75
weight 80.5
----
bmi ???
if ???:pass小结
条件判断可以让计算机自己做选择Python的if...elif...else很灵活。 参考源码
do_if.py
循环
循环
要计算123我们可以直接写表达式 1 2 3
6要计算123...10勉强也能写出来。
但是要计算123...10000直接写表达式就不可能了。
为了让计算机能计算成千上万次的重复运算我们就需要循环语句。
Python的循环有两种一种是for...in循环依次把list或tuple中的每个元素迭代出来看例子
names [Michael, Bob, Tracy]
for name in names:print(name)执行这段代码会依次打印names的每一个元素
Michael
Bob
Tracy所以for x in ...循环就是把每个元素代入变量x然后执行缩进块的语句。
再比如我们想计算1-10的整数之和可以用一个sum变量做累加
sum 0
for x in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]:sum sum x
print(sum)如果要计算1-100的整数之和从1写到100有点困难幸好Python提供一个range()函数可以生成一个整数序列再通过list()函数可以转换为list。比如range(5)生成的序列是从0开始小于5的整数 list(range(5))
[0, 1, 2, 3, 4]range(101)就可以生成0-100的整数序列计算如下
sum 0
for x in range(101):sum sum x
print(sum)请自行运行上述代码看看结果是不是当年高斯同学心算出的5050。
第二种循环是while循环只要条件满足就不断循环条件不满足时退出循环。比如我们要计算100以内所有奇数之和可以用while循环实现
sum 0
n 99
while n 0:sum sum nn n - 2
print(sum)在循环内部变量n不断自减直到变为-1时不再满足while条件循环退出。
练习
请利用循环依次对list中的每个名字打印出Hello, xxx!
# -*- coding: utf-8 -*-
L [Bart, Lisa, Adam]小结
循环是让计算机做重复任务的有效的方法有些时候如果代码写得有问题会让程序陷入“死循环”也就是永远循环下去。这时可以用CtrlC退出程序或者强制结束Python进程。
请试写一个死循环程序。
参考源码
do_for.py
do_while.py
使用dict和set
dict
Python内置了字典dict的支持dict全称dictionary在其他语言中也称为map使用键-值key-value存储具有极快的查找速度。
举个例子假设要根据同学的名字查找对应的成绩如果用list实现需要两个list
names [Michael, Bob, Tracy]
scores [95, 75, 85]给定一个名字要查找对应的成绩就先要在names中找到对应的位置再从scores取出对应的成绩list越长耗时越长。
如果用dict实现只需要一个“名字”-“成绩”的对照表直接根据名字查找成绩无论这个表有多大查找速度都不会变慢。用Python写一个dict如下 d {Michael: 95, Bob: 75, Tracy: 85}d[Michael]
95为什么dict查找速度这么快因为dict的实现原理和查字典是一样的。假设字典包含了1万个汉字我们要查某一个字一个办法是把字典从第一页往后翻直到找到我们想要的字为止这种方法就是在list中查找元素的方法list越大查找越慢。
第二种方法是先在字典的索引表里比如部首表查这个字对应的页码然后直接翻到该页找到这个字。无论找哪个字这种查找速度都非常快不会随着字典大小的增加而变慢。
dict就是第二种实现方式给定一个名字比如Michaeldict在内部就可以直接计算出Michael对应的存放成绩的“页码”也就是95这个数字存放的内存地址直接取出来所以速度非常快。
你可以猜到这种key-value存储方式在放进去的时候必须根据key算出value的存放位置这样取的时候才能根据key直接拿到value。
把数据放入dict的方法除了初始化时指定外还可以通过key放入 d[Adam] 67d[Adam]
67由于一个key只能对应一个value所以多次对一个key放入value后面的值会把前面的值冲掉 d[Jack] 90d[Jack]
90d[Jack] 88d[Jack]
88如果key不存在dict就会报错 d[Thomas]
Traceback (most recent call last):File stdin, line 1, in module
KeyError: Thomas要避免key不存在的错误有两种办法一是通过in判断key是否存在 Thomas in d
False二是通过dict提供的get方法如果key不存在可以返回None或者自己指定的value d.get(Thomas)d.get(Thomas, -1)
-1注意返回None的时候Python的交互式命令行不显示结果。
要删除一个key用pop(key)方法对应的value也会从dict中删除 d.pop(Bob)
75d
{Michael: 95, Tracy: 85}请务必注意dict内部存放的顺序和key放入的顺序是没有关系的。
和list比较dict有以下几个特点
查找和插入的速度极快不会随着key的增加而增加需要占用大量的内存内存浪费多。
而list相反
查找和插入的时间随着元素的增加而增加占用空间小浪费内存很少。
所以dict是用空间来换取时间的一种方法。
dict可以用在需要高速查找的很多地方在Python代码中几乎无处不在正确使用dict非常重要需要牢记的第一条就是dict的key必须是不可变对象。
这是因为dict根据key来计算value的存储位置如果每次计算相同的key得出的结果不同那dict内部就完全混乱了。这个通过key计算位置的算法称为哈希算法Hash。
要保证hash的正确性作为key的对象就不能变。在Python中字符串、整数等都是不可变的因此可以放心地作为key。而list是可变的就不能作为key key [1, 2, 3]d[key] a list
Traceback (most recent call last):File stdin, line 1, in module
TypeError: unhashable type: listset
set和dict类似也是一组key的集合但不存储value。由于key不能重复所以在set中没有重复的key。
要创建一个set需要提供一个list作为输入集合 s set([1, 2, 3])s
{1, 2, 3}注意传入的参数[1, 2, 3]是一个list而显示的{1, 2, 3}只是告诉你这个set内部有123这3个元素显示的顺序也不表示set是有序的。。
重复元素在set中自动被过滤 s set([1, 1, 2, 2, 3, 3])s
{1, 2, 3}通过add(key)方法可以添加元素到set中可以重复添加但不会有效果 s.add(4)s
{1, 2, 3, 4}s.add(4)s
{1, 2, 3, 4}通过remove(key)方法可以删除元素 s.remove(4)s
{1, 2, 3}set可以看成数学意义上的无序和无重复元素的集合因此两个set可以做数学意义上的交集、并集等操作 s1 set([1, 2, 3])s2 set([2, 3, 4])s1 s2
{2, 3}s1 | s2
{1, 2, 3, 4}set和dict的唯一区别仅在于没有存储对应的value但是set的原理和dict一样所以同样不可以放入可变对象因为无法判断两个可变对象是否相等也就无法保证set内部“不会有重复元素”。试试把list放入set看看是否会报错。
再议不可变对象
上面我们讲了str是不变对象而list是可变对象。
对于可变对象比如list对list进行操作list内部的内容是会变化的比如 a [c, b, a]a.sort()a
[a, b, c]而对于不可变对象比如str对str进行操作呢 a abca.replace(a, A)
Abca
abc虽然字符串有个replace()方法也确实变出了Abc但变量a最后仍是abc应该怎么理解呢
我们先把代码改成下面这样 a abcb a.replace(a, A)b
Abca
abc要始终牢记的是a是变量而abc才是字符串对象有些时候我们经常说对象a的内容是abc但其实是指a本身是一个变量它指向的对象的内容才是abc 当我们调用a.replace(a, A)时实际上调用方法replace是作用在字符串对象abc上的而这个方法虽然名字叫replace但却没有改变字符串abc的内容。相反replace方法创建了一个新字符串Abc并返回如果我们用变量b指向该新字符串就容易理解了变量a仍指向原有的字符串abc但变量b却指向新字符串Abc了 所以对于不变对象来说调用对象自身的任意方法也不会改变该对象自身的内容。相反这些方法会创建新的对象并返回这样就保证了不可变对象本身永远是不可变的。
小结
使用key-value存储结构的dict在Python中非常有用选择不可变对象作为key很重要最常用的key是字符串。
tuple虽然是不变对象但试试把(1, 2, 3)和(1, [2, 3])放入dict或set中并解释结果。
参考源码
the_dict.py
the_set.py
函数
我们知道圆的面积计算公式为
S πr2
当我们知道半径r的值时就可以根据公式计算出面积。假设我们需要计算3个不同大小的圆的面积
r1 12.34
r2 9.08
r3 73.1
s1 3.14 * r1 * r1
s2 3.14 * r2 * r2
s3 3.14 * r3 * r3当代码出现有规律的重复的时候你就需要当心了每次写3.14 * x * x不仅很麻烦而且如果要把3.14改成3.14159265359的时候得全部替换。
有了函数我们就不再每次写s 3.14 * x * x而是写成更有意义的函数调用s area_of_circle(x)而函数area_of_circle本身只需要写一次就可以多次调用。
基本上所有的高级语言都支持函数Python也不例外。Python不但能非常灵活地定义函数而且本身内置了很多有用的函数可以直接调用。
抽象
抽象是数学中非常常见的概念。举个例子
计算数列的和比如1 2 3 ... 100写起来十分不方便于是数学家发明了求和符号∑可以把1 2 3 ... 100记作
100
∑n
n1
这种抽象记法非常强大因为我们看到 ∑ 就可以理解成求和而不是还原成低级的加法运算。
而且这种抽象记法是可扩展的比如
100
∑(n21)
n1
还原成加法运算就变成了
(1 x 1 1) (2 x 2 1) (3 x 3 1) ... (100 x 100 1)
可见借助抽象我们才能不关心底层的具体计算过程而直接在更高的层次上思考问题。
写计算机程序也是一样函数就是最基本的一种代码抽象的方式。
调用函数
Python内置了很多有用的函数我们可以直接调用。
要调用一个函数需要知道函数的名称和参数比如求绝对值的函数abs只有一个参数。可以直接从Python的官方网站查看文档
http://docs.python.org/3/library/functions.html#abs
也可以在交互式命令行通过help(abs)查看abs函数的帮助信息。
调用abs函数 abs(100)
100abs(-20)
20abs(12.34)
12.34调用函数的时候如果传入的参数数量不对会报TypeError的错误并且Python会明确地告诉你abs()有且仅有1个参数但给出了两个 abs(1, 2)
Traceback (most recent call last):File stdin, line 1, in module
TypeError: abs() takes exactly one argument (2 given)如果传入的参数数量是对的但参数类型不能被函数所接受也会报TypeError的错误并且给出错误信息str是错误的参数类型 abs(a)
Traceback (most recent call last):File stdin, line 1, in module
TypeError: bad operand type for abs(): str而max函数max()可以接收任意多个参数并返回最大的那个 max(1, 2)
2max(2, 3, 1, -5)
3数据类型转换
Python内置的常用函数还包括数据类型转换函数比如int()函数可以把其他数据类型转换为整数 int(123)
123int(12.34)
12float(12.34)
12.34str(1.23)
1.23str(100)
100bool(1)
Truebool()
False函数名其实就是指向一个函数对象的引用完全可以把函数名赋给一个变量相当于给这个函数起了一个“别名” a abs # 变量a指向abs函数a(-1) # 所以也可以通过a调用abs函数
1练习
请利用Python内置的hex()函数把一个整数转换成十六进制表示的字符串
# -*- coding: utf-8 -*-n1 255
n2 1000
----
print(???)小结
调用Python的函数需要根据函数定义传入正确的参数。如果函数调用出错一定要学会看错误信息所以英文很重要
参考源码
call_func.py
定义函数
在Python中定义一个函数要使用def语句依次写出函数名、括号、括号中的参数和冒号:然后在缩进块中编写函数体函数的返回值用return语句返回。
我们以自定义一个求绝对值的my_abs函数为例
def my_abs(x):if x 0:return xelse:return -x请自行测试并调用my_abs看看返回结果是否正确。
请注意函数体内部的语句在执行时一旦执行到return时函数就执行完毕并将结果返回。因此函数内部通过条件判断和循环可以实现非常复杂的逻辑。
如果没有return语句函数执行完毕后也会返回结果只是结果为None。
return None可以简写为return。
在Python交互环境中定义函数时注意Python会出现...的提示。函数定义结束后需要按两次回车重新回到提示符下 如果你已经把my_abs()的函数定义保存为abstest.py文件了那么可以在该文件的当前目录下启动Python解释器用from abstest import my_abs来导入my_abs()函数注意abstest是文件名不含.py扩展名 import的用法在后续模块一节中会详细介绍。
空函数
如果想定义一个什么事也不做的空函数可以用pass语句
def nop():passpass语句什么都不做那有什么用实际上pass可以用来作为占位符比如现在还没想好怎么写函数的代码就可以先放一个pass让代码能运行起来。
pass还可以用在其他语句里比如
if age 18:pass缺少了pass代码运行就会有语法错误。
参数检查
调用函数时如果参数个数不对Python解释器会自动检查出来并抛出TypeError my_abs(1, 2)
Traceback (most recent call last):File stdin, line 1, in module
TypeError: my_abs() takes 1 positional argument but 2 were given但是如果参数类型不对Python解释器就无法帮我们检查。试试my_abs和内置函数abs的差别 my_abs(A)
Traceback (most recent call last):File stdin, line 1, in moduleFile stdin, line 2, in my_abs
TypeError: unorderable types: str() int()abs(A)
Traceback (most recent call last):File stdin, line 1, in module
TypeError: bad operand type for abs(): str当传入了不恰当的参数时内置函数abs会检查出参数错误而我们定义的my_abs没有参数检查会导致if语句出错出错信息和abs不一样。所以这个函数定义不够完善。
让我们修改一下my_abs的定义对参数类型做检查只允许整数和浮点数类型的参数。数据类型检查可以用内置函数isinstance()实现
def my_abs(x):if not isinstance(x, (int, float)):raise TypeError(bad operand type)if x 0:return xelse:return -x添加了参数检查后如果传入错误的参数类型函数就可以抛出一个错误 my_abs(A)
Traceback (most recent call last):File stdin, line 1, in moduleFile stdin, line 3, in my_abs
TypeError: bad operand type错误和异常处理将在后续讲到。
返回多个值
函数可以返回多个值吗答案是肯定的。
比如在游戏中经常需要从一个点移动到另一个点给出坐标、位移和角度就可以计算出新的新的坐标
import mathdef move(x, y, step, angle0):nx x step * math.cos(angle)ny y - step * math.sin(angle)return nx, nyimport math语句表示导入math包并允许后续代码引用math包里的sin、cos等函数。
然后我们就可以同时获得返回值 x, y move(100, 100, 60, math.pi / 6)print(x, y)
151.96152422706632 70.0但其实这只是一种假象Python函数返回的仍然是单一值 r move(100, 100, 60, math.pi / 6)print(r)
(151.96152422706632, 70.0)原来返回值是一个tuple但是在语法上返回一个tuple可以省略括号而多个变量可以同时接收一个tuple按位置赋给对应的值所以Python的函数返回多值其实就是返回一个tuple但写起来更方便。
小结
定义函数时需要确定函数名和参数个数
如果有必要可以先对参数的数据类型做检查
函数体内部可以用return随时返回函数结果
函数执行完毕也没有return语句时自动return None。
函数可以同时返回多个值但其实就是一个tuple。
练习
请定义一个函数quadratic(a, b, c)接收3个参数返回一元二次方程
ax2 bx c 0
的两个解。
提示计算平方根可以调用math.sqrt()函数 import mathmath.sqrt(2)
1.4142135623730951# -*- coding: utf-8 -*-import mathdef quadratic(a, b, c):
----pass
----
# 测试:
print(quadratic(2, 3, 1)) # (-0.5, -1.0)
print(quadratic(1, 3, -4)) # (1.0, -4.0)参考源码
def_func.py
函数的参数
定义函数的时候我们把参数的名字和位置确定下来函数的接口定义就完成了。对于函数的调用者来说只需要知道如何传递正确的参数以及函数将返回什么样的值就够了函数内部的复杂逻辑被封装起来调用者无需了解。
Python的函数定义非常简单但灵活度却非常大。除了正常定义的必选参数外还可以使用默认参数、可变参数和关键字参数使得函数定义出来的接口不但能处理复杂的参数还可以简化调用者的代码。
位置参数
我们先写一个计算x2的函数
def power(x):return x * x对于power(x)函数参数x就是一个位置参数。
当我们调用power函数时必须传入有且仅有的一个参数x power(5)
25power(15)
225现在如果我们要计算x3怎么办可以再定义一个power3函数但是如果要计算x4、x5……怎么办我们不可能定义无限多个函数。
你也许想到了可以把power(x)修改为power(x, n)用来计算xn说干就干
def power(x, n):s 1while n 0:n n - 1s s * xreturn s对于这个修改后的power(x, n)函数可以计算任意n次方 power(5, 2)
25power(5, 3)
125修改后的power(x, n)函数有两个参数x和n这两个参数都是位置参数调用函数时传入的两个值按照位置顺序依次赋给参数x和n。
默认参数
新的power(x, n)函数定义没有问题但是旧的调用代码失败了原因是我们增加了一个参数导致旧的代码因为缺少一个参数而无法正常调用 power(5)
Traceback (most recent call last):File stdin, line 1, in module
TypeError: power() missing 1 required positional argument: nPython的错误信息很明确调用函数power()缺少了一个位置参数n。
这个时候默认参数就排上用场了。由于我们经常计算x2所以完全可以把第二个参数n的默认值设定为2
def power(x, n2):s 1while n 0:n n - 1s s * xreturn s这样当我们调用power(5)时相当于调用power(5, 2) power(5)
25power(5, 2)
25而对于n 2的其他情况就必须明确地传入n比如power(5, 3)。
从上面的例子可以看出默认参数可以简化函数的调用。设置默认参数时有几点要注意
一是必选参数在前默认参数在后否则Python的解释器会报错思考一下为什么默认参数不能放在必选参数前面
二是如何设置默认参数。
当函数有多个参数时把变化大的参数放前面变化小的参数放后面。变化小的参数就可以作为默认参数。
使用默认参数有什么好处最大的好处是能降低调用函数的难度。
举个例子我们写个一年级小学生注册的函数需要传入name和gender两个参数
def enroll(name, gender):print(name:, name)print(gender:, gender)这样调用enroll()函数只需要传入两个参数 enroll(Sarah, F)
name: Sarah
gender: F如果要继续传入年龄、城市等信息怎么办这样会使得调用函数的复杂度大大增加。
我们可以把年龄和城市设为默认参数
def enroll(name, gender, age6, cityBeijing):print(name:, name)print(gender:, gender)print(age:, age)print(city:, city)这样大多数学生注册时不需要提供年龄和城市只提供必须的两个参数 enroll(Sarah, F)
name: Sarah
gender: F
age: 6
city: Beijing只有与默认参数不符的学生才需要提供额外的信息
enroll(Bob, M, 7)
enroll(Adam, M, cityTianjin)可见默认参数降低了函数调用的难度而一旦需要更复杂的调用时又可以传递更多的参数来实现。无论是简单调用还是复杂调用函数只需要定义一个。
有多个默认参数时调用的时候既可以按顺序提供默认参数比如调用enroll(Bob, M, 7)意思是除了namegender这两个参数外最后1个参数应用在参数age上city参数由于没有提供仍然使用默认值。
也可以不按顺序提供部分默认参数。当不按顺序提供部分默认参数时需要把参数名写上。比如调用enroll(Adam, M, cityTianjin)意思是city参数用传进去的值其他默认参数继续使用默认值。
默认参数很有用但使用不当也会掉坑里。默认参数有个最大的坑演示如下
先定义一个函数传入一个list添加一个END再返回
def add_end(L[]):L.append(END)return L当你正常调用时结果似乎不错 add_end([1, 2, 3])
[1, 2, 3, END]add_end([x, y, z])
[x, y, z, END]当你使用默认参数调用时一开始结果也是对的 add_end()
[END]但是再次调用add_end()时结果就不对了 add_end()
[END, END]add_end()
[END, END, END]很多初学者很疑惑默认参数是[]但是函数似乎每次都“记住了”上次添加了END后的list。
原因解释如下
Python函数在定义的时候默认参数L的值就被计算出来了即[]因为默认参数L也是一个变量它指向对象[]每次调用该函数如果改变了L的内容则下次调用时默认参数的内容就变了不再是函数定义时的[]了。
所以定义默认参数要牢记一点默认参数必须指向不变对象
要修改上面的例子我们可以用None这个不变对象来实现
def add_end(LNone):if L is None:L []L.append(END)return L现在无论调用多少次都不会有问题 add_end()
[END]add_end()
[END]为什么要设计str、None这样的不变对象呢因为不变对象一旦创建对象内部的数据就不能修改这样就减少了由于修改数据导致的错误。此外由于对象不变多任务环境下同时读取对象不需要加锁同时读一点问题都没有。我们在编写程序时如果可以设计一个不变对象那就尽量设计成不变对象。
可变参数
在Python函数中还可以定义可变参数。顾名思义可变参数就是传入的参数个数是可变的可以是1个、2个到任意个还可以是0个。
我们以数学题为例子给定一组数字abc……请计算a2 b2 c2 ……。
要定义出这个函数我们必须确定输入的参数。由于参数个数不确定我们首先想到可以把abc……作为一个list或tuple传进来这样函数可以定义如下
def calc(numbers):sum 0for n in numbers:sum sum n * nreturn sum但是调用的时候需要先组装出一个list或tuple calc([1, 2, 3])
14calc((1, 3, 5, 7))
84如果利用可变参数调用函数的方式可以简化成这样 calc(1, 2, 3)
14calc(1, 3, 5, 7)
84所以我们把函数的参数改为可变参数
def calc(*numbers):sum 0for n in numbers:sum sum n * nreturn sum定义可变参数和定义一个list或tuple参数相比仅仅在参数前面加了一个*号。在函数内部参数numbers接收到的是一个tuple因此函数代码完全不变。但是调用该函数时可以传入任意个参数包括0个参数 calc(1, 2)
5calc()
0如果已经有一个list或者tuple要调用一个可变参数怎么办可以这样做 nums [1, 2, 3]calc(nums[0], nums[1], nums[2])
14这种写法当然是可行的问题是太繁琐所以Python允许你在list或tuple前面加一个*号把list或tuple的元素变成可变参数传进去 nums [1, 2, 3]calc(*nums)
14*nums表示把nums这个list的所有元素作为可变参数传进去。这种写法相当有用而且很常见。
关键字参数
可变参数允许你传入0个或任意个参数这些可变参数在函数调用时自动组装为一个tuple。而关键字参数允许你传入0个或任意个含参数名的参数这些关键字参数在函数内部自动组装为一个dict。请看示例
def person(name, age, **kw):print(name:, name, age:, age, other:, kw)函数person除了必选参数name和age外还接受关键字参数kw。在调用该函数时可以只传入必选参数 person(Michael, 30)
name: Michael age: 30 other: {}也可以传入任意个数的关键字参数 person(Bob, 35, cityBeijing)
name: Bob age: 35 other: {city: Beijing}person(Adam, 45, genderM, jobEngineer)
name: Adam age: 45 other: {gender: M, job: Engineer}关键字参数有什么用它可以扩展函数的功能。比如在person函数里我们保证能接收到name和age这两个参数但是如果调用者愿意提供更多的参数我们也能收到。试想你正在做一个用户注册的功能除了用户名和年龄是必填项外其他都是可选项利用关键字参数来定义这个函数就能满足注册的需求。
和可变参数类似也可以先组装出一个dict然后把该dict转换为关键字参数传进去 extra {city: Beijing, job: Engineer}person(Jack, 24, cityextra[city], jobextra[job])
name: Jack age: 24 other: {city: Beijing, job: Engineer}当然上面复杂的调用可以用简化的写法 extra {city: Beijing, job: Engineer}person(Jack, 24, **extra)
name: Jack age: 24 other: {city: Beijing, job: Engineer}**extra表示把extra这个dict的所有key-value用关键字参数传入到函数的**kw参数kw将获得一个dict注意kw获得的dict是extra的一份拷贝对kw的改动不会影响到函数外的extra。
命名关键字参数
对于关键字参数函数的调用者可以传入任意不受限制的关键字参数。至于到底传入了哪些就需要在函数内部通过kw检查。
仍以person()函数为例我们希望检查是否有city和job参数
def person(name, age, **kw):if city in kw:# 有city参数passif job in kw:# 有job参数passprint(name:, name, age:, age, other:, kw)但是调用者仍可以传入不受限制的关键字参数 person(Jack, 24, cityBeijing, addrChaoyang, zipcode123456)如果要限制关键字参数的名字就可以用命名关键字参数例如只接收city和job作为关键字参数。这种方式定义的函数如下
def person(name, age, *, city, job):print(name, age, city, job)和关键字参数**kw不同命名关键字参数需要一个特殊分隔符**后面的参数被视为命名关键字参数。
调用方式如下 person(Jack, 24, cityBeijing, jobEngineer)
Jack 24 Beijing Engineer命名关键字参数必须传入参数名这和位置参数不同。如果没有传入参数名调用将报错 person(Jack, 24, Beijing, Engineer)
Traceback (most recent call last):File stdin, line 1, in module
TypeError: person() takes 2 positional arguments but 4 were given由于调用时缺少参数名city和jobPython解释器把这4个参数均视为位置参数但person()函数仅接受2个位置参数。
命名关键字参数可以有缺省值从而简化调用
def person(name, age, *, cityBeijing, job):print(name, age, city, job)由于命名关键字参数city具有默认值调用时可不传入city参数 person(Jack, 24, jobEngineer)
Jack 24 Beijing Engineer使用命名关键字参数时要特别注意*不是参数而是特殊分隔符。如果缺少*Python解释器将无法识别位置参数和命名关键字参数
def person(name, age, city, job):# 缺少 *city和job被视为位置参数pass参数组合
在Python中定义函数可以用必选参数、默认参数、可变参数、关键字参数和命名关键字参数这5种参数都可以组合使用除了可变参数无法和命名关键字参数混合。但是请注意参数定义的顺序必须是必选参数、默认参数、可变参数/命名关键字参数和关键字参数。
比如定义一个函数包含上述若干种参数
def f1(a, b, c0, *args, **kw):print(a , a, b , b, c , c, args , args, kw , kw)def f2(a, b, c0, *, d, **kw):print(a , a, b , b, c , c, d , d, kw , kw)在函数调用的时候Python解释器自动按照参数位置和参数名把对应的参数传进去。 f1(1, 2)
a 1 b 2 c 0 args () kw {}f1(1, 2, c3)
a 1 b 2 c 3 args () kw {}f1(1, 2, 3, a, b)
a 1 b 2 c 3 args (a, b) kw {}f1(1, 2, 3, a, b, x99)
a 1 b 2 c 3 args (a, b) kw {x: 99}f2(1, 2, d99, extNone)
a 1 b 2 c 0 d 99 kw {ext: None}最神奇的是通过一个tuple和dict你也可以调用上述函数 args (1, 2, 3, 4)kw {d: 99, x: #}f1(*args, **kw)
a 1 b 2 c 3 args (4,) kw {d: 99, x: #}args (1, 2, 3)kw {d: 88, x: #}f2(*args, **kw)
a 1 b 2 c 3 d 88 kw {x: #}所以对于任意函数都可以通过类似func(*args, **kw)的形式调用它无论它的参数是如何定义的。
小结
Python的函数具有非常灵活的参数形态既可以实现简单的调用又可以传入非常复杂的参数。
默认参数一定要用不可变对象如果是可变对象程序运行时会有逻辑错误
要注意定义可变参数和关键字参数的语法
*args是可变参数args接收的是一个tuple
**kw是关键字参数kw接收的是一个dict。
以及调用函数时如何传入可变参数和关键字参数的语法
可变参数既可以直接传入func(1, 2, 3)又可以先组装list或tuple再通过*args传入func(*(1, 2, 3))
关键字参数既可以直接传入func(a1, b2)又可以先组装dict再通过**kw传入func(**{a: 1, b: 2})。
使用*args和**kw是Python的习惯写法当然也可以用其他参数名但最好使用习惯用法。
命名的关键字参数是为了限制调用者可以传入的参数名同时可以提供默认值。
定义命名的关键字参数不要忘了写分隔符*否则定义的将是位置参数。
参考源码
var_args.py
kw_args.py
递归函数
在函数内部可以调用其他函数。如果一个函数在内部调用自身本身这个函数就是递归函数。
举个例子我们来计算阶乘n! 1 x 2 x 3 x ... x n用函数fact(n)表示可以看出
fact(n) n! 1 x 2 x 3 x ... x (n-1) x n (n-1)! x n fact(n-1) x n
所以fact(n)可以表示为n x fact(n-1)只有n1时需要特殊处理。
于是fact(n)用递归的方式写出来就是
def fact(n):if n1:return 1return n * fact(n - 1)上面就是一个递归函数。可以试试 fact(1)
1fact(5)
120fact(100)
93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000如果我们计算fact(5)可以根据函数定义看到计算过程如下 fact(5)5 * fact(4)5 * (4 * fact(3))5 * (4 * (3 * fact(2)))5 * (4 * (3 * (2 * fact(1))))5 * (4 * (3 * (2 * 1)))5 * (4 * (3 * 2))5 * (4 * 6)5 * 24120递归函数的优点是定义简单逻辑清晰。理论上所有的递归函数都可以写成循环的方式但循环的逻辑不如递归清晰。
使用递归函数需要注意防止栈溢出。在计算机中函数调用是通过栈stack这种数据结构实现的每当进入一个函数调用栈就会加一层栈帧每当函数返回栈就会减一层栈帧。由于栈的大小不是无限的所以递归调用的次数过多会导致栈溢出。可以试试fact(1000) fact(1000)
Traceback (most recent call last):File stdin, line 1, in moduleFile stdin, line 4, in fact...File stdin, line 4, in fact
RuntimeError: maximum recursion depth exceeded in comparison解决递归调用栈溢出的方法是通过尾递归优化事实上尾递归和循环的效果是一样的所以把循环看成是一种特殊的尾递归函数也是可以的。
尾递归是指在函数返回的时候调用自身本身并且return语句不能包含表达式。这样编译器或者解释器就可以把尾递归做优化使递归本身无论调用多少次都只占用一个栈帧不会出现栈溢出的情况。
上面的fact(n)函数由于return n * fact(n - 1)引入了乘法表达式所以就不是尾递归了。要改成尾递归方式需要多一点代码主要是要把每一步的乘积传入到递归函数中
def fact(n):return fact_iter(n, 1)def fact_iter(num, product):if num 1:return productreturn fact_iter(num - 1, num * product)可以看到return fact_iter(num - 1, num * product)仅返回递归函数本身num - 1和num * product在函数调用前就会被计算不影响函数调用。
fact(5)对应的fact_iter(5, 1)的调用如下 fact_iter(5, 1)fact_iter(4, 5)fact_iter(3, 20)fact_iter(2, 60)fact_iter(1, 120)120尾递归调用时如果做了优化栈不会增长因此无论多少次调用也不会导致栈溢出。
遗憾的是大多数编程语言没有针对尾递归做优化Python解释器也没有做优化所以即使把上面的fact(n)函数改成尾递归方式也会导致栈溢出。
小结
使用递归函数的优点是逻辑简单清晰缺点是过深的调用会导致栈溢出。
针对尾递归优化的语言可以通过尾递归防止栈溢出。尾递归事实上和循环是等价的没有循环语句的编程语言只能通过尾递归实现循环。
Python标准的解释器没有针对尾递归做优化任何递归函数都存在栈溢出的问题。
练习
汉诺塔的移动可以用递归函数非常简单地实现。
请编写move(n, a, b, c)函数它接收参数n表示3个柱子A、B、C中第1个柱子A的盘子数量然后打印出把所有盘子从A借助B移动到C的方法例如
def move(n, a, b, c):
----pass
----
# 期待输出:
# A -- C
# A -- B
# C -- B
# A -- C
# B -- A
# B -- C
# A -- C
move(3, A, B, C)参考源码
recur.py
高级特性
掌握了Python的数据类型、语句和函数基本上就可以编写出很多有用的程序了。
比如构造一个1, 3, 5, 7, ..., 99的列表可以通过循环实现
L []
n 1
while n 99:L.append(n)n n 2取list的前一半的元素也可以通过循环实现。
但是在Python中代码不是越多越好而是越少越好。代码不是越复杂越好而是越简单越好。
基于这一思想我们来介绍Python中非常有用的高级特性1行代码能实现的功能决不写5行代码。请始终牢记代码越少开发效率越高。
切片
取一个list或tuple的部分元素是非常常见的操作。比如一个list如下 L [Michael, Sarah, Tracy, Bob, Jack]取前3个元素应该怎么做
笨办法 [L[0], L[1], L[2]]
[Michael, Sarah, Tracy]之所以是笨办法是因为扩展一下取前N个元素就没辙了。
取前N个元素也就是索引为0-(N-1)的元素可以用循环 r []n 3for i in range(n):
... r.append(L[i])
... r
[Michael, Sarah, Tracy]对这种经常取指定索引范围的操作用循环十分繁琐因此Python提供了切片Slice操作符能大大简化这种操作。
对应上面的问题取前3个元素用一行代码就可以完成切片 L[0:3]
[Michael, Sarah, Tracy]L[0:3]表示从索引0开始取直到索引3为止但不包括索引3。即索引012正好是3个元素。
如果第一个索引是0还可以省略 L[:3]
[Michael, Sarah, Tracy]也可以从索引1开始取出2个元素出来 L[1:3]
[Sarah, Tracy]类似的既然Python支持L[-1]取倒数第一个元素那么它同样支持倒数切片试试 L[-2:]
[Bob, Jack]L[-2:-1]
[Bob]记住倒数第一个元素的索引是-1。
切片操作十分有用。我们先创建一个0-99的数列 L list(range(100))L
[0, 1, 2, 3, ..., 99]可以通过切片轻松取出某一段数列。比如前10个数 L[:10]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]后10个数 L[-10:]
[90, 91, 92, 93, 94, 95, 96, 97, 98, 99]前11-20个数 L[10:20]
[10, 11, 12, 13, 14, 15, 16, 17, 18, 19]前10个数每两个取一个 L[:10:2]
[0, 2, 4, 6, 8]所有数每5个取一个 L[::5]
[0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95]甚至什么都不写只写[:]就可以原样复制一个list L[:]
[0, 1, 2, 3, ..., 99]tuple也是一种list唯一区别是tuple不可变。因此tuple也可以用切片操作只是操作的结果仍是tuple (0, 1, 2, 3, 4, 5)[:3]
(0, 1, 2)字符串xxx也可以看成是一种list每个元素就是一个字符。因此字符串也可以用切片操作只是操作结果仍是字符串 ABCDEFG[:3]
ABCABCDEFG[::2]
ACEG在很多编程语言中针对字符串提供了很多各种截取函数例如substring其实目的就是对字符串切片。Python没有针对字符串的截取函数只需要切片一个操作就可以完成非常简单。
小结
有了切片操作很多地方循环就不再需要了。Python的切片非常灵活一行代码就可以实现很多行循环才能完成的操作。
参考源码
do_slice.py
迭代
如果给定一个list或tuple我们可以通过for循环来遍历这个list或tuple这种遍历我们称为迭代Iteration。
在Python中迭代是通过for ... in来完成的而很多语言比如C或者Java迭代list是通过下标完成的比如Java代码
for (i0; ilist.length; i) {n list[i];
}可以看出Python的for循环抽象程度要高于Java的for循环因为Python的for循环不仅可以用在list或tuple上还可以作用在其他可迭代对象上。
list这种数据类型虽然有下标但很多其他数据类型是没有下标的但是只要是可迭代对象无论有无下标都可以迭代比如dict就可以迭代 d {a: 1, b: 2, c: 3}for key in d:
... print(key)
...
a
c
b因为dict的存储不是按照list的方式顺序排列所以迭代出的结果顺序很可能不一样。
默认情况下dict迭代的是key。如果要迭代value可以用for value in d.values()如果要同时迭代key和value可以用for k, v in d.items()。
由于字符串也是可迭代对象因此也可以作用于for循环 for ch in ABC:
... print(ch)
...
A
B
C所以当我们使用for循环时只要作用于一个可迭代对象for循环就可以正常运行而我们不太关心该对象究竟是list还是其他数据类型。
那么如何判断一个对象是可迭代对象呢方法是通过collections模块的Iterable类型判断 from collections import Iterableisinstance(abc, Iterable) # str是否可迭代
Trueisinstance([1,2,3], Iterable) # list是否可迭代
Trueisinstance(123, Iterable) # 整数是否可迭代
False最后一个小问题如果要对list实现类似Java那样的下标循环怎么办Python内置的enumerate函数可以把一个list变成索引-元素对这样就可以在for循环中同时迭代索引和元素本身 for i, value in enumerate([A, B, C]):
... print(i, value)
...
0 A
1 B
2 C上面的for循环里同时引用了两个变量在Python里是很常见的比如下面的代码 for x, y in [(1, 1), (2, 4), (3, 9)]:
... print(x, y)
...
1 1
2 4
3 9小结
任何可迭代对象都可以作用于for循环包括我们自定义的数据类型只要符合迭代条件就可以使用for循环。
参考源码
do_iter.py
列表生成式
列表生成式即List Comprehensions是Python内置的非常简单却强大的可以用来创建list的生成式。
举个例子要生成list [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]可以用list(range(1, 11)) list(range(1, 11))
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]但如果要生成[1x1, 2x2, 3x3, ..., 10x10]怎么做方法一是循环 L []for x in range(1, 11):
... L.append(x * x)
...L
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]但是循环太繁琐而列表生成式则可以用一行语句代替循环生成上面的list [x * x for x in range(1, 11)]
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]写列表生成式时把要生成的元素x * x放到前面后面跟for循环就可以把list创建出来十分有用多写几次很快就可以熟悉这种语法。
for循环后面还可以加上if判断这样我们就可以筛选出仅偶数的平方 [x * x for x in range(1, 11) if x % 2 0]
[4, 16, 36, 64, 100]还可以使用两层循环可以生成全排列 [m n for m in ABC for n in XYZ]
[AX, AY, AZ, BX, BY, BZ, CX, CY, CZ]三层和三层以上的循环就很少用到了。
运用列表生成式可以写出非常简洁的代码。例如列出当前目录下的所有文件和目录名可以通过一行代码实现 import os # 导入os模块模块的概念后面讲到[d for d in os.listdir(.)] # os.listdir可以列出文件和目录
[.emacs.d, .ssh, .Trash, Adlm, Applications, Desktop, Documents, Downloads, Library, Movies, Music, Pictures, Public, VirtualBox VMs, Workspace, XCode]for循环其实可以同时使用两个甚至多个变量比如dict的items()可以同时迭代key和value d {x: A, y: B, z: C }for k, v in d.items():
... print(k, , v)
...
y B
x A
z C因此列表生成式也可以使用两个变量来生成list d {x: A, y: B, z: C }[k v for k, v in d.items()]
[yB, xA, zC]最后把一个list中所有的字符串变成小写 L [Hello, World, IBM, Apple][s.lower() for s in L]
[hello, world, ibm, apple]练习
如果list中既包含字符串又包含整数由于非字符串类型没有lower()方法所以列表生成式会报错 L [Hello, World, 18, Apple, None][s.lower() for s in L]
Traceback (most recent call last):File stdin, line 1, in moduleFile stdin, line 1, in listcomp
AttributeError: int object has no attribute lower使用内建的isinstance函数可以判断一个变量是不是字符串 x abcy 123isinstance(x, str)
Trueisinstance(y, str)
False请修改列表生成式通过添加if语句保证列表生成式能正确地执行
# -*- coding: utf-8 -*-L1 [Hello, World, 18, Apple, None]
----
L2 ???
----
# 期待输出: [hello, world, apple]
print(L2)小结
运用列表生成式可以快速生成list可以通过一个list推导出另一个list而代码却十分简洁。
参考源码
do_listcompr.py
生成器
通过列表生成式我们可以直接创建一个列表。但是受到内存限制列表容量肯定是有限的。而且创建一个包含100万个元素的列表不仅占用很大的存储空间如果我们仅仅需要访问前面几个元素那后面绝大多数元素占用的空间都白白浪费了。
所以如果列表元素可以按照某种算法推算出来那我们是否可以在循环的过程中不断推算出后续的元素呢这样就不必创建完整的list从而节省大量的空间。在Python中这种一边循环一边计算的机制称为生成器generator。
要创建一个generator有很多种方法。第一种方法很简单只要把一个列表生成式的[]改成()就创建了一个generator L [x * x for x in range(10)]L
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]g (x * x for x in range(10))g
generator object genexpr at 0x1022ef630创建L和g的区别仅在于最外层的[]和()L是一个list而g是一个generator。
我们可以直接打印出list的每一个元素但我们怎么打印出generator的每一个元素呢
如果要一个一个打印出来可以通过next()函数获得generator的下一个返回值 next(g)
0next(g)
1next(g)
4next(g)
9next(g)
16next(g)
25next(g)
36next(g)
49next(g)
64next(g)
81next(g)
Traceback (most recent call last):File stdin, line 1, in module
StopIteration我们讲过generator保存的是算法每次调用next(g)就计算出g的下一个元素的值直到计算到最后一个元素没有更多的元素时抛出StopIteration的错误。
当然上面这种不断调用next(g)实在是太变态了正确的方法是使用for循环因为generator也是可迭代对象 g (x * x for x in range(10))for n in g:
... print(n)
...
0
1
4
9
16
25
36
49
64
81所以我们创建了一个generator后基本上永远不会调用next()而是通过for循环来迭代它并且不需要关心StopIteration的错误。
generator非常强大。如果推算的算法比较复杂用类似列表生成式的for循环无法实现的时候还可以用函数来实现。
比如著名的斐波拉契数列Fibonacci除第一个和第二个数外任意一个数都可由前两个数相加得到
1, 1, 2, 3, 5, 8, 13, 21, 34, ...
斐波拉契数列用列表生成式写不出来但是用函数把它打印出来却很容易
def fib(max):n, a, b 0, 0, 1while n max:print(b)a, b b, a bn n 1return done上面的函数可以输出斐波那契数列的前N个数 fib(6)
1
1
2
3
5
8
done仔细观察可以看出fib函数实际上是定义了斐波拉契数列的推算规则可以从第一个元素开始推算出后续任意的元素这种逻辑其实非常类似generator。
也就是说上面的函数和generator仅一步之遥。要把fib函数变成generator只需要把print(b)改为yield b就可以了
def fib(max):n, a, b 0, 0, 1while n max:yield ba, b b, a bn n 1return done这就是定义generator的另一种方法。如果一个函数定义中包含yield关键字那么这个函数就不再是一个普通函数而是一个generator f fib(6)f
generator object fib at 0x104feaaa0这里最难理解的就是generator和函数的执行流程不一样。函数是顺序执行遇到return语句或者最后一行函数语句就返回。而变成generator的函数在每次调用next()的时候执行遇到yield语句返回再次执行时从上次返回的yield语句处继续执行。
举个简单的例子定义一个generator依次返回数字135
def odd():print(step 1)yield 1print(step 2)yield(3)print(step 3)yield(5)调用该generator时首先要生成一个generator对象然后用next()函数不断获得下一个返回值 o odd()next(o)
step 1
1next(o)
step 2
3next(o)
step 3
5next(o)
Traceback (most recent call last):File stdin, line 1, in module
StopIteration可以看到odd不是普通函数而是generator在执行过程中遇到yield就中断下次又继续执行。执行3次yield后已经没有yield可以执行了所以第4次调用next(o)就报错。
回到fib的例子我们在循环过程中不断调用yield就会不断中断。当然要给循环设置一个条件来退出循环不然就会产生一个无限数列出来。
同样的把函数改成generator后我们基本上从来不会用next()来获取下一个返回值而是直接使用for循环来迭代 for n in fib(6):
... print(n)
...
1
1
2
3
5
8但是用for循环调用generator时发现拿不到generator的return语句的返回值。如果想要拿到返回值必须捕获StopIteration错误返回值包含在StopIteration的value中 g fib(6)while True:
... try:
... x next(g)
... print(g:, x)
... except StopIteration as e:
... print(Generator return value:, e.value)
... break
...
g: 1
g: 1
g: 2
g: 3
g: 5
g: 8
Generator return value: done关于如何捕获错误后面的错误处理还会详细讲解。
练习
杨辉三角定义如下 11 11 2 11 3 3 11 4 6 4 1
1 5 10 10 5 1把每一行看做一个list试写一个generator不断输出下一行的list
# -*- coding: utf-8 -*-def triangles():
----pass
----
# 期待输出:
# [1]
# [1, 1]
# [1, 2, 1]
# [1, 3, 3, 1]
# [1, 4, 6, 4, 1]
# [1, 5, 10, 10, 5, 1]
# [1, 6, 15, 20, 15, 6, 1]
# [1, 7, 21, 35, 35, 21, 7, 1]
# [1, 8, 28, 56, 70, 56, 28, 8, 1]
# [1, 9, 36, 84, 126, 126, 84, 36, 9, 1]
n 0
for t in triangles():print(t)n n 1if n 10:break小结
generator是非常强大的工具在Python中可以简单地把列表生成式改成generator也可以通过函数实现复杂逻辑的generator。
要理解generator的工作原理它是在for循环的过程中不断计算出下一个元素并在适当的条件结束for循环。对于函数改成的generator来说遇到return语句或者执行到函数体最后一行语句就是结束generator的指令for循环随之结束。
请注意区分普通函数和generator函数普通函数调用直接返回结果 r abs(6)r
6generator函数的“调用”实际返回一个generator对象 g fib(6)g
generator object fib at 0x1022ef948参考源码
do_generator.py
迭代器
我们已经知道可以直接作用于for循环的数据类型有以下几种
一类是集合数据类型如list、tuple、dict、set、str等
一类是generator包括生成器和带yield的generator function。
这些可以直接作用于for循环的对象统称为可迭代对象Iterable。
可以使用isinstance()判断一个对象是否是Iterable对象 from collections import Iterableisinstance([], Iterable)
Trueisinstance({}, Iterable)
Trueisinstance(abc, Iterable)
Trueisinstance((x for x in range(10)), Iterable)
Trueisinstance(100, Iterable)
False而生成器不但可以作用于for循环还可以被next()函数不断调用并返回下一个值直到最后抛出StopIteration错误表示无法继续返回下一个值了。
可以被next()函数调用并不断返回下一个值的对象称为迭代器Iterator。
可以使用isinstance()判断一个对象是否是Iterator对象 from collections import Iteratorisinstance((x for x in range(10)), Iterator)
Trueisinstance([], Iterator)
Falseisinstance({}, Iterator)
Falseisinstance(abc, Iterator)
False生成器都是Iterator对象但list、dict、str虽然是Iterable却不是Iterator。
把list、dict、str等Iterable变成Iterator可以使用iter()函数 isinstance(iter([]), Iterator)
Trueisinstance(iter(abc), Iterator)
True你可能会问为什么list、dict、str等数据类型不是Iterator
这是因为Python的Iterator对象表示的是一个数据流Iterator对象可以被next()函数调用并不断返回下一个数据直到没有数据时抛出StopIteration错误。可以把这个数据流看做是一个有序序列但我们却不能提前知道序列的长度只能不断通过next()函数实现按需计算下一个数据所以Iterator的计算是惰性的只有在需要返回下一个数据时它才会计算。
Iterator甚至可以表示一个无限大的数据流例如全体自然数。而使用list是永远不可能存储全体自然数的。
小结
凡是可作用于for循环的对象都是Iterable类型
凡是可作用于next()函数的对象都是Iterator类型它们表示一个惰性计算的序列
集合数据类型如list、dict、str等是Iterable但不是Iterator不过可以通过iter()函数获得一个Iterator对象。
Python的for循环本质上就是通过不断调用next()函数实现的例如
for x in [1, 2, 3, 4, 5]:pass实际上完全等价于
# 首先获得Iterator对象:
it iter([1, 2, 3, 4, 5])
# 循环:
while True:try:# 获得下一个值:x next(it)except StopIteration:# 遇到StopIteration就退出循环break参考源码
do_iter.py
函数式编程
函数是Python内建支持的一种封装我们通过把大段代码拆成函数通过一层一层的函数调用就可以把复杂任务分解成简单的任务这种分解可以称之为面向过程的程序设计。函数就是面向过程的程序设计的基本单元。
而函数式编程请注意多了一个“式”字——Functional Programming虽然也可以归结到面向过程的程序设计但其思想更接近数学计算。
我们首先要搞明白计算机Computer和计算Compute的概念。
在计算机的层次上CPU执行的是加减乘除的指令代码以及各种条件判断和跳转指令所以汇编语言是最贴近计算机的语言。
而计算则指数学意义上的计算越是抽象的计算离计算机硬件越远。
对应到编程语言就是越低级的语言越贴近计算机抽象程度低执行效率高比如C语言越高级的语言越贴近计算抽象程度高执行效率低比如Lisp语言。
函数式编程就是一种抽象程度很高的编程范式纯粹的函数式编程语言编写的函数没有变量因此任意一个函数只要输入是确定的输出就是确定的这种纯函数我们称之为没有副作用。而允许使用变量的程序设计语言由于函数内部的变量状态不确定同样的输入可能得到不同的输出因此这种函数是有副作用的。
函数式编程的一个特点就是允许把函数本身作为参数传入另一个函数还允许返回一个函数
Python对函数式编程提供部分支持。由于Python允许使用变量因此Python不是纯函数式编程语言。
高阶函数
高阶函数英文叫Higher-order function。什么是高阶函数我们以实际代码为例子一步一步深入概念。
变量可以指向函数
以Python内置的求绝对值的函数abs()为例调用该函数用以下代码 abs(-10)
10但是如果只写abs呢 abs
built-in function abs可见abs(-10)是函数调用而abs是函数本身。
要获得函数调用结果我们可以把结果赋值给变量 x abs(-10)x
10但是如果把函数本身赋值给变量呢 f absf
built-in function abs结论函数本身也可以赋值给变量即变量可以指向函数。
如果一个变量指向了一个函数那么可否通过该变量来调用这个函数用代码验证一下 f absf(-10)
10成功说明变量f现在已经指向了abs函数本身。直接调用abs()函数和调用变量f()完全相同。
函数名也是变量
那么函数名是什么呢函数名其实就是指向函数的变量对于abs()这个函数完全可以把函数名abs看成变量它指向一个可以计算绝对值的函数
如果把abs指向其他对象会有什么情况发生 abs 10abs(-10)
Traceback (most recent call last):File stdin, line 1, in module
TypeError: int object is not callable把abs指向10后就无法通过abs(-10)调用该函数了因为abs这个变量已经不指向求绝对值函数而是指向一个整数10
当然实际代码绝对不能这么写这里是为了说明函数名也是变量。要恢复abs函数请重启Python交互环境。
注由于abs函数实际上是定义在__builtin__模块中的所以要让修改abs变量的指向在其它模块也生效要用__builtin__.abs 10。
传入函数
既然变量可以指向函数函数的参数能接收变量那么一个函数就可以接收另一个函数作为参数这种函数就称之为高阶函数。
一个最简单的高阶函数
def add(x, y, f):return f(x) f(y)当我们调用add(-5, 6, abs)时参数xy和f分别接收-56和abs根据函数定义我们可以推导计算过程为
x -5
y 6
f abs
f(x) f(y) abs(-5) abs(6) 11
return 11用代码验证一下 add(-5, 6, abs)
11编写高阶函数就是让函数的参数能够接收别的函数。
小结
把函数作为参数传入这样的函数称为高阶函数函数式编程就是指这种高度抽象的编程范式。
map/reduce
Python内建了map()和reduce()函数。
如果你读过Google的那篇大名鼎鼎的论文“MapReduce: Simplified Data Processing on Large Clusters”你就能大概明白map/reduce的概念。
我们先看map。map()函数接收两个参数一个是函数一个是Iterablemap将传入的函数依次作用到序列的每个元素并把结果作为新的Iterator返回。
举例说明比如我们有一个函数f(x)x2要把这个函数作用在一个list [1, 2, 3, 4, 5, 6, 7, 8, 9]上就可以用map()实现如下 现在我们用Python代码实现 def f(x):
... return x * x
...r map(f, [1, 2, 3, 4, 5, 6, 7, 8, 9])list(r)
[1, 4, 9, 16, 25, 36, 49, 64, 81]map()传入的第一个参数是f即函数对象本身。由于结果r是一个IteratorIterator是惰性序列因此通过list()函数让它把整个序列都计算出来并返回一个list。
你可能会想不需要map()函数写一个循环也可以计算出结果
L []
for n in [1, 2, 3, 4, 5, 6, 7, 8, 9]:L.append(f(n))
print(L)的确可以但是从上面的循环代码能一眼看明白“把f(x)作用在list的每一个元素并把结果生成一个新的list”吗
所以map()作为高阶函数事实上它把运算规则抽象了因此我们不但可以计算简单的f(x)x2还可以计算任意复杂的函数比如把这个list所有数字转为字符串 list(map(str, [1, 2, 3, 4, 5, 6, 7, 8, 9]))
[1, 2, 3, 4, 5, 6, 7, 8, 9]只需要一行代码。
再看reduce的用法。reduce把一个函数作用在一个序列[x1, x2, x3, ...]上这个函数必须接收两个参数reduce把结果继续和序列的下一个元素做累积计算其效果就是
reduce(f, [x1, x2, x3, x4]) f(f(f(x1, x2), x3), x4)比方说对一个序列求和就可以用reduce实现 from functools import reducedef add(x, y):
... return x y
...reduce(add, [1, 3, 5, 7, 9])
25当然求和运算可以直接用Python内建函数sum()没必要动用reduce。
但是如果要把序列[1, 3, 5, 7, 9]变换成整数13579reduce就可以派上用场 from functools import reducedef fn(x, y):
... return x * 10 y
...reduce(fn, [1, 3, 5, 7, 9])
13579这个例子本身没多大用处但是如果考虑到字符串str也是一个序列对上面的例子稍加改动配合map()我们就可以写出把str转换为int的函数 from functools import reducedef fn(x, y):
... return x * 10 y
...def char2num(s):
... return {0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7, 8: 8, 9: 9}[s]
...reduce(fn, map(char2num, 13579))
13579整理成一个str2int的函数就是
from functools import reducedef str2int(s):def fn(x, y):return x * 10 ydef char2num(s):return {0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7, 8: 8, 9: 9}[s]return reduce(fn, map(char2num, s))还可以用lambda函数进一步简化成
from functools import reducedef char2num(s):return {0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7, 8: 8, 9: 9}[s]def str2int(s):return reduce(lambda x, y: x * 10 y, map(char2num, s))也就是说假设Python没有提供int()函数你完全可以自己写一个把字符串转化为整数的函数而且只需要几行代码
lambda函数的用法在后面介绍。
练习
利用map()函数把用户输入的不规范的英文名字变为首字母大写其他小写的规范名字。输入[adam, LISA, barT]输出[Adam, Lisa, Bart]
# -*- coding: utf-8 -*-
----
def normalize(name):pass
----
# 测试:
L1 [adam, LISA, barT]
L2 list(map(normalize, L1))
print(L2)Python提供的sum()函数可以接受一个list并求和请编写一个prod()函数可以接受一个list并利用reduce()求积
# -*- coding: utf-8 -*-from functools import reducedef prod(L):
----pass
----
print(3 * 5 * 7 * 9 , prod([3, 5, 7, 9]))利用map和reduce编写一个str2float函数把字符串123.456转换成浮点数123.456
# -*- coding: utf-8 -*-from functools import reducedef str2float(s):
----pass
----
print(str2float(\123.456\) , str2float(123.456))参考代码
do_map.py
do_reduce.py
filter
Python内建的filter()函数用于过滤序列。
和map()类似filter()也接收一个函数和一个序列。和map()不同的时filter()把传入的函数依次作用于每个元素然后根据返回值是True还是False决定保留还是丢弃该元素。
例如在一个list中删掉偶数只保留奇数可以这么写
def is_odd(n):return n % 2 1list(filter(is_odd, [1, 2, 4, 5, 6, 9, 10, 15]))
# 结果: [1, 5, 9, 15]把一个序列中的空字符串删掉可以这么写
def not_empty(s):return s and s.strip()list(filter(not_empty, [A, , B, None, C, ]))
# 结果: [A, B, C]可见用filter()这个高阶函数关键在于正确实现一个“筛选”函数。
注意到filter()函数返回的是一个Iterator也就是一个惰性序列所以要强迫filter()完成计算结果需要用list()函数获得所有结果并返回list。
用filter求素数
计算素数的一个方法是埃氏筛法它的算法理解起来非常简单
首先列出从2开始的所有自然数构造一个序列
2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, ...
取序列的第一个数2它一定是素数然后用2把序列的2的倍数筛掉
3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, ...
取新序列的第一个数3它一定是素数然后用3把序列的3的倍数筛掉
5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, ...
取新序列的第一个数5然后用5把序列的5的倍数筛掉
7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, ...
不断筛下去就可以得到所有的素数。
用Python来实现这个算法可以先构造一个从3开始的奇数序列
def _odd_iter():n 1while True:n n 2yield n注意这是一个生成器并且是一个无限序列。
然后定义一个筛选函数
def _not_divisible(n):return lambda x: x % n 0最后定义一个生成器不断返回下一个素数
def primes():yield 2it _odd_iter() # 初始序列while True:n next(it) # 返回序列的第一个数yield nit filter(_not_divisible(n), it) # 构造新序列这个生成器先返回第一个素数2然后利用filter()不断产生筛选后的新的序列。
由于primes()也是一个无限序列所以调用时需要设置一个退出循环的条件
# 打印1000以内的素数:
for n in primes():if n 1000:print(n)else:break注意到Iterator是惰性计算的序列所以我们可以用Python表示“全体自然数”“全体素数”这样的序列而代码非常简洁。
练习
回数是指从左向右读和从右向左读都是一样的数例如12321909。请利用filter()滤掉非回数
# -*- coding: utf-8 -*-def is_palindrome(n):
----pass
----
# 测试:
output filter(is_palindrome, range(1, 1000))
print(list(output))小结
filter()的作用是从一个序列中筛出符合条件的元素。由于filter()使用了惰性计算所以只有在取filter()结果的时候才会真正筛选并每次返回下一个筛出的元素。
参考源码
do_filter.py
prime_numbers.py
sorted
排序算法
排序也是在程序中经常用到的算法。无论使用冒泡排序还是快速排序排序的核心是比较两个元素的大小。如果是数字我们可以直接比较但如果是字符串或者两个dict呢直接比较数学上的大小是没有意义的因此比较的过程必须通过函数抽象出来。
Python内置的sorted()函数就可以对list进行排序 sorted([36, 5, -12, 9, -21])
[-21, -12, 5, 9, 36]此外sorted()函数也是一个高阶函数它还可以接收一个key函数来实现自定义的排序例如按绝对值大小排序 sorted([36, 5, -12, 9, -21], keyabs)
[5, 9, -12, -21, 36]key指定的函数将作用于list的每一个元素上并根据key函数返回的结果进行排序。对比原始的list和经过keyabs处理过的list
list [36, 5, -12, 9, -21]keys [36, 5, 12, 9, 21]然后sorted()函数按照keys进行排序并按照对应关系返回list相应的元素
keys排序结果 [5, 9, 12, 21, 36]| | | | |
最终结果 [5, 9, -12, -21, 36]我们再看一个字符串排序的例子 sorted([bob, about, Zoo, Credit])
[Credit, Zoo, about, bob]默认情况下对字符串排序是按照ASCII的大小比较的由于Z a结果大写字母Z会排在小写字母a的前面。
现在我们提出排序应该忽略大小写按照字母序排序。要实现这个算法不必对现有代码大加改动只要我们能用一个key函数把字符串映射为忽略大小写排序即可。忽略大小写来比较两个字符串实际上就是先把字符串都变成大写或者都变成小写再比较。
这样我们给sorted传入key函数即可实现忽略大小写的排序 sorted([bob, about, Zoo, Credit], keystr.lower)
[about, bob, Credit, Zoo]要进行反向排序不必改动key函数可以传入第三个参数reverseTrue sorted([bob, about, Zoo, Credit], keystr.lower, reverseTrue)
[Zoo, Credit, bob, about]从上述例子可以看出高阶函数的抽象能力是非常强大的而且核心代码可以保持得非常简洁。
小结
sorted()也是一个高阶函数。用sorted()排序的关键在于实现一个映射函数。
练习
假设我们用一组tuple表示学生名字和成绩
L [(Bob, 75), (Adam, 92), (Bart, 66), (Lisa, 88)]请用sorted()对上述列表分别按名字排序
# -*- coding: utf-8 -*-L [(Bob, 75), (Adam, 92), (Bart, 66), (Lisa, 88)]def by_name(t):
----pass
----
L2 sorted(L, keyby_name)
print(L2)再按成绩从高到低排序
# -*- coding: utf-8 -*-L [(Bob, 75), (Adam, 92), (Bart, 66), (Lisa, 88)]----
def by_score(t):passL2 ???
----
print(L2)参考源码
do_sorted.py
返回函数
函数作为返回值
高阶函数除了可以接受函数作为参数外还可以把函数作为结果值返回。
我们来实现一个可变参数的求和。通常情况下求和的函数是这样定义的
def calc_sum(*args):ax 0for n in args:ax ax nreturn ax但是如果不需要立刻求和而是在后面的代码中根据需要再计算怎么办可以不返回求和的结果而是返回求和的函数
def lazy_sum(*args):def sum():ax 0for n in args:ax ax nreturn axreturn sum当我们调用lazy_sum()时返回的并不是求和结果而是求和函数 f lazy_sum(1, 3, 5, 7, 9)f
function lazy_sum.locals.sum at 0x101c6ed90调用函数f时才真正计算求和的结果 f()
25在这个例子中我们在函数lazy_sum中又定义了函数sum并且内部函数sum可以引用外部函数lazy_sum的参数和局部变量当lazy_sum返回函数sum时相关参数和变量都保存在返回的函数中这种称为“闭包Closure”的程序结构拥有极大的威力。
请再注意一点当我们调用lazy_sum()时每次调用都会返回一个新的函数即使传入相同的参数 f1 lazy_sum(1, 3, 5, 7, 9)f2 lazy_sum(1, 3, 5, 7, 9)f1f2
Falsef1()和f2()的调用结果互不影响。
闭包
注意到返回的函数在其定义内部引用了局部变量args所以当一个函数返回了一个函数后其内部的局部变量还被新函数引用所以闭包用起来简单实现起来可不容易。
另一个需要注意的问题是返回的函数并没有立刻执行而是直到调用了f()才执行。我们来看一个例子
def count():fs []for i in range(1, 4):def f():return i*ifs.append(f)return fsf1, f2, f3 count()在上面的例子中每次循环都创建了一个新的函数然后把创建的3个函数都返回了。
你可能认为调用f1()f2()和f3()结果应该是149但实际结果是 f1()
9f2()
9f3()
9全部都是9原因就在于返回的函数引用了变量i但它并非立刻执行。等到3个函数都返回时它们所引用的变量i已经变成了3因此最终结果为9。
返回闭包时牢记的一点就是返回函数不要引用任何循环变量或者后续会发生变化的变量。
如果一定要引用循环变量怎么办方法是再创建一个函数用该函数的参数绑定循环变量当前的值无论该循环变量后续如何更改已绑定到函数参数的值不变
def count():def f(j):def g():return j*jreturn gfs []for i in range(1, 4):fs.append(f(i)) # f(i)立刻被执行因此i的当前值被传入f()return fs再看看结果 f1, f2, f3 count()f1()
1f2()
4f3()
9缺点是代码较长可利用lambda函数缩短代码。
小结
一个函数可以返回一个计算结果也可以返回一个函数。
返回一个函数时牢记该函数并未执行返回函数中不要引用任何可能会变化的变量。
参考源码
return_func.py
匿名函数
当我们在传入函数时有些时候不需要显式地定义函数直接传入匿名函数更方便。
在Python中对匿名函数提供了有限支持。还是以map()函数为例计算f(x)x2时除了定义一个f(x)的函数外还可以直接传入匿名函数 list(map(lambda x: x * x, [1, 2, 3, 4, 5, 6, 7, 8, 9]))
[1, 4, 9, 16, 25, 36, 49, 64, 81]通过对比可以看出匿名函数lambda x: x * x实际上就是
def f(x):return x * x关键字lambda表示匿名函数冒号前面的x表示函数参数。
匿名函数有个限制就是只能有一个表达式不用写return返回值就是该表达式的结果。
用匿名函数有个好处因为函数没有名字不必担心函数名冲突。此外匿名函数也是一个函数对象也可以把匿名函数赋值给一个变量再利用变量来调用该函数 f lambda x: x * xf
function lambda at 0x101c6ef28f(5)
25同样也可以把匿名函数作为返回值返回比如
def build(x, y):return lambda: x * x y * y小结
Python对匿名函数的支持有限只有一些简单的情况下可以使用匿名函数。
装饰器
由于函数也是一个对象而且函数对象可以被赋值给变量所以通过变量也能调用该函数。 def now():
... print(2015-3-25)
...f nowf()
2015-3-25函数对象有一个__name__属性可以拿到函数的名字 now.__name__
nowf.__name__
now现在假设我们要增强now()函数的功能比如在函数调用前后自动打印日志但又不希望修改now()函数的定义这种在代码运行期间动态增加功能的方式称之为“装饰器”Decorator。
本质上decorator就是一个返回函数的高阶函数。所以我们要定义一个能打印日志的decorator可以定义如下
def log(func):def wrapper(*args, **kw):print(call %s(): % func.__name__)return func(*args, **kw)return wrapper观察上面的log因为它是一个decorator所以接受一个函数作为参数并返回一个函数。我们要借助Python的语法把decorator置于函数的定义处
log
def now():print(2015-3-25)调用now()函数不仅会运行now()函数本身还会在运行now()函数前打印一行日志 now()
call now():
2015-3-25把log放到now()函数的定义处相当于执行了语句
now log(now)由于log()是一个decorator返回一个函数所以原来的now()函数仍然存在只是现在同名的now变量指向了新的函数于是调用now()将执行新函数即在log()函数中返回的wrapper()函数。
wrapper()函数的参数定义是(*args, **kw)因此wrapper()函数可以接受任意参数的调用。在wrapper()函数内首先打印日志再紧接着调用原始函数。
如果decorator本身需要传入参数那就需要编写一个返回decorator的高阶函数写出来会更复杂。比如要自定义log的文本
def log(text):def decorator(func):def wrapper(*args, **kw):print(%s %s(): % (text, func.__name__))return func(*args, **kw)return wrapperreturn decorator这个3层嵌套的decorator用法如下
log(execute)
def now():print(2015-3-25)执行结果如下 now()
execute now():
2015-3-25和两层嵌套的decorator相比3层嵌套的效果是这样的 now log(execute)(now)我们来剖析上面的语句首先执行log(execute)返回的是decorator函数再调用返回的函数参数是now函数返回值最终是wrapper函数。
以上两种decorator的定义都没有问题但还差最后一步。因为我们讲了函数也是对象它有__name__等属性但你去看经过decorator装饰之后的函数它们的__name__已经从原来的now变成了wrapper now.__name__
wrapper因为返回的那个wrapper()函数名字就是wrapper所以需要把原始函数的__name__等属性复制到wrapper()函数中否则有些依赖函数签名的代码执行就会出错。
不需要编写wrapper.__name__ func.__name__这样的代码Python内置的functools.wraps就是干这个事的所以一个完整的decorator的写法如下
import functoolsdef log(func):functools.wraps(func)def wrapper(*args, **kw):print(call %s(): % func.__name__)return func(*args, **kw)return wrapper或者针对带参数的decorator
import functoolsdef log(text):def decorator(func):functools.wraps(func)def wrapper(*args, **kw):print(%s %s(): % (text, func.__name__))return func(*args, **kw)return wrapperreturn decoratorimport functools是导入functools模块。模块的概念稍候讲解。现在只需记住在定义wrapper()的前面加上functools.wraps(func)即可。
小结
在面向对象OOP的设计模式中decorator被称为装饰模式。OOP的装饰模式需要通过继承和组合来实现而Python除了能支持OOP的decorator外直接从语法层次支持decorator。Python的decorator可以用函数实现也可以用类实现。
decorator可以增强函数的功能定义起来虽然有点复杂但使用起来非常灵活和方便。
请编写一个decorator能在函数调用的前后打印出begin call和end call的日志。
再思考一下能否写出一个log的decorator使它既支持
log
def f():pass又支持
log(execute)
def f():pass参考源码
decorator.py
偏函数
Python的functools模块提供了很多有用的功能其中一个就是偏函数Partial function。要注意这里的偏函数和数学意义上的偏函数不一样。
在介绍函数参数的时候我们讲到通过设定参数的默认值可以降低函数调用的难度。而偏函数也可以做到这一点。举例如下
int()函数可以把字符串转换为整数当仅传入字符串时int()函数默认按十进制转换 int(12345)
12345但int()函数还提供额外的base参数默认值为10。如果传入base参数就可以做N进制的转换 int(12345, base8)
5349int(12345, 16)
74565假设要转换大量的二进制字符串每次都传入int(x, base2)非常麻烦于是我们想到可以定义一个int2()的函数默认把base2传进去
def int2(x, base2):return int(x, base)这样我们转换二进制就非常方便了 int2(1000000)
64int2(1010101)
85functools.partial就是帮助我们创建一个偏函数的不需要我们自己定义int2()可以直接使用下面的代码创建一个新的函数int2 import functoolsint2 functools.partial(int, base2)int2(1000000)
64int2(1010101)
85所以简单总结functools.partial的作用就是把一个函数的某些参数给固定住也就是设置默认值返回一个新的函数调用这个新函数会更简单。
注意到上面的新的int2函数仅仅是把base参数重新设定默认值为2但也可以在函数调用时传入其他值 int2(1000000, base10)
1000000最后创建偏函数时实际上可以接收函数对象、*args和**kw这3个参数当传入
int2 functools.partial(int, base2)实际上固定了int()函数的关键字参数base也就是
int2(10010)相当于
kw { base: 2 }
int(10010, **kw)当传入
max2 functools.partial(max, 10)实际上会把10作为*args的一部分自动加到左边也就是
max2(5, 6, 7)相当于
args (10, 5, 6, 7)
max(*args)结果为10。
小结
当函数的参数个数太多需要简化时使用functools.partial可以创建一个新的函数这个新函数可以固定住原函数的部分参数从而在调用时更简单。
参考源码
do_partial.py
模块
在计算机程序的开发过程中随着程序代码越写越多在一个文件里代码就会越来越长越来越不容易维护。
为了编写可维护的代码我们把很多函数分组分别放到不同的文件里这样每个文件包含的代码就相对较少很多编程语言都采用这种组织代码的方式。在Python中一个.py文件就称之为一个模块Module。
使用模块有什么好处
最大的好处是大大提高了代码的可维护性。其次编写代码不必从零开始。当一个模块编写完毕就可以被其他地方引用。我们在编写程序的时候也经常引用其他模块包括Python内置的模块和来自第三方的模块。
使用模块还可以避免函数名和变量名冲突。相同名字的函数和变量完全可以分别存在不同的模块中因此我们自己在编写模块时不必考虑名字会与其他模块冲突。但是也要注意尽量不要与内置函数名字冲突。点这里查看Python的所有内置函数。
你也许还想到如果不同的人编写的模块名相同怎么办为了避免模块名冲突Python又引入了按目录来组织模块的方法称为包Package。
举个例子一个abc.py的文件就是一个名字叫abc的模块一个xyz.py的文件就是一个名字叫xyz的模块。
现在假设我们的abc和xyz这两个模块名字与其他模块冲突了于是我们可以通过包来组织模块避免冲突。方法是选择一个顶层包名比如mycompany按照如下目录存放 引入了包以后只要顶层的包名不与别人冲突那所有模块都不会与别人冲突。现在abc.py模块的名字就变成了mycompany.abc类似的xyz.py的模块名变成了mycompany.xyz。
请注意每一个包目录下面都会有一个__init__.py的文件这个文件是必须存在的否则Python就把这个目录当成普通目录而不是一个包。__init__.py可以是空文件也可以有Python代码因为__init__.py本身就是一个模块而它的模块名就是mycompany。
类似的可以有多级目录组成多级层次的包结构。比如如下的目录结构 文件www.py的模块名就是mycompany.web.www两个文件utils.py的模块名分别是mycompany.utils和mycompany.web.utils。
自己创建模块时要注意命名不能和Python自带的模块名称冲突。例如系统自带了sys模块自己的模块就不可命名为sys.py否则将无法导入系统自带的sys模块。
mycompany.web也是一个模块请指出该模块对应的.py文件。
使用模块
Python本身就内置了很多非常有用的模块只要安装完毕这些模块就可以立刻使用。
我们以内建的sys模块为例编写一个hello的模块
#!/usr/bin/env python3
# -*- coding: utf-8 -*- a test module __author__ Michael Liaoimport sysdef test():args sys.argvif len(args)1:print(Hello, world!)elif len(args)2:print(Hello, %s! % args[1])else:print(Too many arguments!)if __name____main__:test()第1行和第2行是标准注释第1行注释可以让这个hello.py文件直接在Unix/Linux/Mac上运行第2行注释表示.py文件本身使用标准UTF-8编码
第4行是一个字符串表示模块的文档注释任何模块代码的第一个字符串都被视为模块的文档注释
第6行使用__author__变量把作者写进去这样当你公开源代码后别人就可以瞻仰你的大名
以上就是Python模块的标准文件模板当然也可以全部删掉不写但是按标准办事肯定没错。
后面开始就是真正的代码部分。
你可能注意到了使用sys模块的第一步就是导入该模块
import sys导入sys模块后我们就有了变量sys指向该模块利用sys这个变量就可以访问sys模块的所有功能。
sys模块有一个argv变量用list存储了命令行的所有参数。argv至少有一个元素因为第一个参数永远是该.py文件的名称例如
运行python3 hello.py获得的sys.argv就是[hello.py]
运行python3 hello.py Michael获得的sys.argv就是[hello.py, Michael]。
最后注意到这两行代码
if __name____main__:test()当我们在命令行运行hello模块文件时Python解释器把一个特殊变量__name__置为__main__而如果在其他地方导入该hello模块时if判断将失败因此这种if测试可以让一个模块通过命令行运行时执行一些额外的代码最常见的就是运行测试。
我们可以用命令行运行hello.py看看效果
$ python3 hello.py
Hello, world!
$ python hello.py Michael
Hello, Michael!如果启动Python交互环境再导入hello模块
$ python3
Python 3.4.3 (v3.4.3:9b73f1c3e601, Feb 23 2015, 02:52:03)
[GCC 4.2.1 (Apple Inc. build 5666) (dot 3)] on darwin
Type help, copyright, credits or license for more information.import hello导入时没有打印Hello, word!因为没有执行test()函数。
调用hello.test()时才能打印出Hello, word! hello.test()
Hello, world!作用域
在一个模块中我们可能会定义很多函数和变量但有的函数和变量我们希望给别人使用有的函数和变量我们希望仅仅在模块内部使用。在Python中是通过_前缀来实现的。
正常的函数和变量名是公开的public可以被直接引用比如abcx123PI等
类似__xxx__这样的变量是特殊变量可以被直接引用但是有特殊用途比如上面的__author____name__就是特殊变量hello模块定义的文档注释也可以用特殊变量__doc__访问我们自己的变量一般不要用这种变量名
类似_xxx和__xxx这样的函数或变量就是非公开的private不应该被直接引用比如_abc__abc等
之所以我们说private函数和变量“不应该”被直接引用而不是“不能”被直接引用是因为Python并没有一种方法可以完全限制访问private函数或变量但是从编程习惯上不应该引用private函数或变量。
private函数或变量不应该被别人引用那它们有什么用呢请看例子
def _private_1(name):return Hello, %s % namedef _private_2(name):return Hi, %s % namedef greeting(name):if len(name) 3:return _private_1(name)else:return _private_2(name)我们在模块里公开greeting()函数而把内部逻辑用private函数隐藏起来了这样调用greeting()函数不用关心内部的private函数细节这也是一种非常有用的代码封装和抽象的方法即
外部不需要引用的函数全部定义成private只有外部需要引用的函数才定义为public。
安装第三方模块
在Python中安装第三方模块是通过包管理工具pip完成的。
如果你正在使用Mac或Linux安装pip本身这个步骤就可以跳过了。
如果你正在使用Windows请参考安装Python一节的内容确保安装时勾选了pip和Add python.exe to Path。
在命令提示符窗口下尝试运行pip如果Windows提示未找到命令可以重新运行安装程序添加pip。
注意Mac或Linux上有可能并存Python 3.x和Python 2.x因此对应的pip命令是pip3。
现在让我们来安装一个第三方库——Python Imaging Library这是Python下非常强大的处理图像的工具库。不过PIL目前只支持到Python 2.7并且有年头没有更新了因此基于PIL的Pillow项目开发非常活跃并且支持最新的Python 3。
一般来说第三方库都会在Python官方的pypi.python.org网站注册要安装一个第三方库必须先知道该库的名称可以在官网或者pypi上搜索比如Pillow的名称叫Pillow因此安装Pillow的命令就是
pip install Pillow耐心等待下载并安装后就可以使用Pillow了。
有了Pillow处理图片易如反掌。随便找个图片生成缩略图 from PIL import Imageim Image.open(test.png)print(im.format, im.size, im.mode)
PNG (400, 300) RGBim.thumbnail((200, 100))im.save(thumb.jpg, JPEG)其他常用的第三方库还有MySQL的驱动mysql-connector-python用于科学计算的NumPy库numpy用于生成文本的模板工具Jinja2等等。
模块搜索路径
当我们试图加载一个模块时Python会在指定的路径下搜索对应的.py文件如果找不到就会报错 import mymodule
Traceback (most recent call last):File stdin, line 1, in module
ImportError: No module named mymodule默认情况下Python解释器会搜索当前目录、所有已安装的内置模块和第三方模块搜索路径存放在sys模块的path变量中 import syssys.path
[, /Library/Frameworks/Python.framework/Versions/3.4/lib/python34.zip, /Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4, /Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/plat-darwin, /Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/lib-dynload, /Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/site-packages]如果我们要添加自己的搜索目录有两种方法
一是直接修改sys.path添加要搜索的目录 import syssys.path.append(/Users/michael/my_py_scripts)这种方法是在运行时修改运行结束后失效。
第二种方法是设置环境变量PYTHONPATH该环境变量的内容会被自动添加到模块搜索路径中。设置方式与设置Path环境变量类似。注意只需要添加你自己的搜索路径Python自己本身的搜索路径不受影响。
面向对象编程
面向对象编程——Object Oriented Programming简称OOP是一种程序设计思想。OOP把对象作为程序的基本单元一个对象包含了数据和操作数据的函数。
面向过程的程序设计把计算机程序视为一系列的命令集合即一组函数的顺序执行。为了简化程序设计面向过程把函数继续切分为子函数即把大块函数通过切割成小块函数来降低系统的复杂度。
而面向对象的程序设计把计算机程序视为一组对象的集合而每个对象都可以接收其他对象发过来的消息并处理这些消息计算机程序的执行就是一系列消息在各个对象之间传递。
在Python中所有数据类型都可以视为对象当然也可以自定义对象。自定义的对象数据类型就是面向对象中的类Class的概念。
我们以一个例子来说明面向过程和面向对象在程序流程上的不同之处。
假设我们要处理学生的成绩表为了表示一个学生的成绩面向过程的程序可以用一个dict表示
std1 { name: Michael, score: 98 }
std2 { name: Bob, score: 81 }而处理学生成绩可以通过函数实现比如打印学生的成绩
def print_score(std):print(%s: %s % (std[name], std[score]))如果采用面向对象的程序设计思想我们首选思考的不是程序的执行流程而是Student这种数据类型应该被视为一个对象这个对象拥有name和score这两个属性Property。如果要打印一个学生的成绩首先必须创建出这个学生对应的对象然后给对象发一个print_score消息让对象自己把自己的数据打印出来。
class Student(object):def __init__(self, name, score):self.name nameself.score scoredef print_score(self):print(%s: %s % (self.name, self.score))给对象发消息实际上就是调用对象对应的关联函数我们称之为对象的方法Method。面向对象的程序写出来就像这样
bart Student(Bart Simpson, 59)
lisa Student(Lisa Simpson, 87)
bart.print_score()
lisa.print_score()面向对象的设计思想是从自然界中来的因为在自然界中类Class和实例Instance的概念是很自然的。Class是一种抽象概念比如我们定义的Class——Student是指学生这个概念而实例Instance则是一个个具体的Student比如Bart Simpson和Lisa Simpson是两个具体的Student。
所以面向对象的设计思想是抽象出Class根据Class创建Instance。
面向对象的抽象程度又比函数要高因为一个Class既包含数据又包含操作数据的方法。
小结
数据封装、继承和多态是面向对象的三大特点我们后面会详细讲解。
类和实例
面向对象最重要的概念就是类Class和实例Instance必须牢记类是抽象的模板比如Student类而实例是根据类创建出来的一个个具体的“对象”每个对象都拥有相同的方法但各自的数据可能不同。
仍以Student类为例在Python中定义类是通过class关键字
class Student(object):passclass后面紧接着是类名即Student类名通常是大写开头的单词紧接着是(object)表示该类是从哪个类继承下来的继承的概念我们后面再讲通常如果没有合适的继承类就使用object类这是所有类最终都会继承的类。
定义好了Student类就可以根据Student类创建出Student的实例创建实例是通过类名()实现的 bart Student()bart
__main__.Student object at 0x10a67a590Student
class __main__.Student可以看到变量bart指向的就是一个Student的实例后面的0x10a67a590是内存地址每个object的地址都不一样而Student本身则是一个类。
可以自由地给一个实例变量绑定属性比如给实例bart绑定一个name属性 bart.name Bart Simpsonbart.name
Bart Simpson由于类可以起到模板的作用因此可以在创建实例的时候把一些我们认为必须绑定的属性强制填写进去。通过定义一个特殊的__init__方法在创建实例的时候就把namescore等属性绑上去
class Student(object):def __init__(self, name, score):self.name nameself.score score注意到__init__方法的第一个参数永远是self表示创建的实例本身因此在__init__方法内部就可以把各种属性绑定到self因为self就指向创建的实例本身。
有了__init__方法在创建实例的时候就不能传入空的参数了必须传入与__init__方法匹配的参数但self不需要传Python解释器自己会把实例变量传进去 bart Student(Bart Simpson, 59)bart.name
Bart Simpsonbart.score
59和普通的函数相比在类中定义的函数只有一点不同就是第一个参数永远是实例变量self并且调用时不用传递该参数。除此之外类的方法和普通函数没有什么区别所以你仍然可以用默认参数、可变参数、关键字参数和命名关键字参数。
数据封装
面向对象编程的一个重要特点就是数据封装。在上面的Student类中每个实例就拥有各自的name和score这些数据。我们可以通过函数来访问这些数据比如打印一个学生的成绩 def print_score(std):
... print(%s: %s % (std.name, std.score))
...print_score(bart)
Bart Simpson: 59但是既然Student实例本身就拥有这些数据要访问这些数据就没有必要从外面的函数去访问可以直接在Student类的内部定义访问数据的函数这样就把“数据”给封装起来了。这些封装数据的函数是和Student类本身是关联起来的我们称之为类的方法
class Student(object):def __init__(self, name, score):self.name nameself.score scoredef print_score(self):print(%s: %s % (self.name, self.score))要定义一个方法除了第一个参数是self外其他和普通函数一样。要调用一个方法只需要在实例变量上直接调用除了self不用传递其他参数正常传入 bart.print_score()
Bart Simpson: 59这样一来我们从外部看Student类就只需要知道创建实例需要给出name和score而如何打印都是在Student类的内部定义的这些数据和逻辑被“封装”起来了调用很容易但却不用知道内部实现的细节。
封装的另一个好处是可以给Student类增加新的方法比如get_grade
class Student(object):...def get_grade(self):if self.score 90:return Aelif self.score 60:return Belse:return C同样的get_grade方法可以直接在实例变量上调用不需要知道内部实现细节 bart.get_grade()
C小结
类是创建实例的模板而实例则是一个一个具体的对象各个实例拥有的数据都互相独立互不影响
方法就是与实例绑定的函数和普通函数不同方法可以直接访问实例的数据
通过在实例上调用方法我们就直接操作了对象内部的数据但无需知道方法内部的实现细节。
和静态语言不同Python允许对实例变量绑定任何数据也就是说对于两个实例变量虽然它们都是同一个类的不同实例但拥有的变量名称都可能不同 bart Student(Bart Simpson, 59)lisa Student(Lisa Simpson, 87)bart.age 8bart.age
8lisa.age
Traceback (most recent call last):File stdin, line 1, in module
AttributeError: Student object has no attribute age参考源码
student.py
访问限制
在Class内部可以有属性和方法而外部代码可以通过直接调用实例变量的方法来操作数据这样就隐藏了内部的复杂逻辑。
但是从前面Student类的定义来看外部代码还是可以自由地修改一个实例的name、score属性 bart Student(Bart Simpson, 98)bart.score
98bart.score 59bart.score
59如果要让内部属性不被外部访问可以把属性的名称前加上两个下划线__在Python中实例的变量名如果以__开头就变成了一个私有变量private只有内部可以访问外部不能访问所以我们把Student类改一改
class Student(object):def __init__(self, name, score):self.__name nameself.__score scoredef print_score(self):print(%s: %s % (self.__name, self.__score))改完后对于外部代码来说没什么变动但是已经无法从外部访问实例变量.__name和实例变量.__score了 bart Student(Bart Simpson, 98)bart.__name
Traceback (most recent call last):File stdin, line 1, in module
AttributeError: Student object has no attribute __name这样就确保了外部代码不能随意修改对象内部的状态这样通过访问限制的保护代码更加健壮。
但是如果外部代码要获取name和score怎么办可以给Student类增加get_name和get_score这样的方法
class Student(object):...def get_name(self):return self.__namedef get_score(self):return self.__score如果又要允许外部代码修改score怎么办可以再给Student类增加set_score方法
class Student(object):...def set_score(self, score):self.__score score你也许会问原先那种直接通过bart.score 59也可以修改啊为什么要定义一个方法大费周折因为在方法中可以对参数做检查避免传入无效的参数
class Student(object):...def set_score(self, score):if 0 score 100:self.__score scoreelse:raise ValueError(bad score)需要注意的是在Python中变量名类似__xxx__的也就是以双下划线开头并且以双下划线结尾的是特殊变量特殊变量是可以直接访问的不是private变量所以不能用__name__、__score__这样的变量名。
有些时候你会看到以一个下划线开头的实例变量名比如_name这样的实例变量外部是可以访问的但是按照约定俗成的规定当你看到这样的变量时意思就是“虽然我可以被访问但是请把我视为私有变量不要随意访问”。
双下划线开头的实例变量是不是一定不能从外部访问呢其实也不是。不能直接访问__name是因为Python解释器对外把__name变量改成了_Student__name所以仍然可以通过_Student__name来访问__name变量 bart._Student__name
Bart Simpson但是强烈建议你不要这么干因为不同版本的Python解释器可能会把__name改成不同的变量名。
总的来说就是Python本身没有任何机制阻止你干坏事一切全靠自觉。
参考源码
protected_student.py
继承和多态
在OOP程序设计中当我们定义一个class的时候可以从某个现有的class继承新的class称为子类Subclass而被继承的class称为基类、父类或超类Base class、Super class。
比如我们已经编写了一个名为Animal的class有一个run()方法可以直接打印
class Animal(object):def run(self):print(Animal is running...)当我们需要编写Dog和Cat类时就可以直接从Animal类继承
class Dog(Animal):passclass Cat(Animal):pass对于Dog来说Animal就是它的父类对于Animal来说Dog就是它的子类。Cat和Dog类似。
继承有什么好处最大的好处是子类获得了父类的全部功能。由于Animial实现了run()方法因此Dog和Cat作为它的子类什么事也没干就自动拥有了run()方法
dog Dog()
dog.run()cat Cat()
cat.run()运行结果如下
Animal is running...
Animal is running...当然也可以对子类增加一些方法比如Dog类
class Dog(Animal):def run(self):print(Dog is running...)def eat(self):print(Eating meat...)继承的第二个好处需要我们对代码做一点改进。你看到了无论是Dog还是Cat它们run()的时候显示的都是Animal is running...符合逻辑的做法是分别显示Dog is running...和Cat is running...因此对Dog和Cat类改进如下
class Dog(Animal):def run(self):print(Dog is running...)class Cat(Animal):def run(self):print(Cat is running...)再次运行结果如下
Dog is running...
Cat is running...当子类和父类都存在相同的run()方法时我们说子类的run()覆盖了父类的run()在代码运行的时候总是会调用子类的run()。这样我们就获得了继承的另一个好处多态。
要理解什么是多态我们首先要对数据类型再作一点说明。当我们定义一个class的时候我们实际上就定义了一种数据类型。我们定义的数据类型和Python自带的数据类型比如str、list、dict没什么两样
a list() # a是list类型
b Animal() # b是Animal类型
c Dog() # c是Dog类型判断一个变量是否是某个类型可以用isinstance()判断 isinstance(a, list)
Trueisinstance(b, Animal)
Trueisinstance(c, Dog)
True看来a、b、c确实对应着list、Animal、Dog这3种类型。
但是等等试试 isinstance(c, Animal)
True看来c不仅仅是Dogc还是Animal
不过仔细想想这是有道理的因为Dog是从Animal继承下来的当我们创建了一个Dog的实例c时我们认为c的数据类型是Dog没错但c同时也是Animal也没错Dog本来就是Animal的一种
所以在继承关系中如果一个实例的数据类型是某个子类那它的数据类型也可以被看做是父类。但是反过来就不行 b Animal()isinstance(b, Dog)
FalseDog可以看成Animal但Animal不可以看成Dog。
要理解多态的好处我们还需要再编写一个函数这个函数接受一个Animal类型的变量
def run_twice(animal):animal.run()animal.run()当我们传入Animal的实例时run_twice()就打印出 run_twice(Animal())
Animal is running...
Animal is running...当我们传入Dog的实例时run_twice()就打印出 run_twice(Dog())
Dog is running...
Dog is running...当我们传入Cat的实例时run_twice()就打印出 run_twice(Cat())
Cat is running...
Cat is running...看上去没啥意思但是仔细想想现在如果我们再定义一个Tortoise类型也从Animal派生
class Tortoise(Animal):def run(self):print(Tortoise is running slowly...)当我们调用run_twice()时传入Tortoise的实例 run_twice(Tortoise())
Tortoise is running slowly...
Tortoise is running slowly...你会发现新增一个Animal的子类不必对run_twice()做任何修改实际上任何依赖Animal作为参数的函数或者方法都可以不加修改地正常运行原因就在于多态。
多态的好处就是当我们需要传入Dog、Cat、Tortoise……时我们只需要接收Animal类型就可以了因为Dog、Cat、Tortoise……都是Animal类型然后按照Animal类型进行操作即可。由于Animal类型有run()方法因此传入的任意类型只要是Animal类或者子类就会自动调用实际类型的run()方法这就是多态的意思
对于一个变量我们只需要知道它是Animal类型无需确切地知道它的子类型就可以放心地调用run()方法而具体调用的run()方法是作用在Animal、Dog、Cat还是Tortoise对象上由运行时该对象的确切类型决定这就是多态真正的威力调用方只管调用不管细节而当我们新增一种Animal的子类时只要确保run()方法编写正确不用管原来的代码是如何调用的。这就是著名的“开闭”原则
对扩展开放允许新增Animal子类
对修改封闭不需要修改依赖Animal类型的run_twice()等函数。
继承还可以一级一级地继承下来就好比从爷爷到爸爸、再到儿子这样的关系。而任何类最终都可以追溯到根类object这些继承关系看上去就像一颗倒着的树。比如如下的继承树 静态语言 vs 动态语言
对于静态语言例如Java来说如果需要传入Animal类型则传入的对象必须是Animal类型或者它的子类否则将无法调用run()方法。
对于Python这样的动态语言来说则不一定需要传入Animal类型。我们只需要保证传入的对象有一个run()方法就可以了
class Timer(object):def run(self):print(Start...)这就是动态语言的“鸭子类型”它并不要求严格的继承体系一个对象只要“看起来像鸭子走起路来像鸭子”那它就可以被看做是鸭子。
Python的“file-like object“就是一种鸭子类型。对真正的文件对象它有一个read()方法返回其内容。但是许多对象只要有read()方法都被视为“file-like object“。许多函数接收的参数就是“file-like object“你不一定要传入真正的文件对象完全可以传入任何实现了read()方法的对象。
小结
继承可以把父类的所有功能都直接拿过来这样就不必重零做起子类只需要新增自己特有的方法也可以把父类不适合的方法覆盖重写。
动态语言的鸭子类型特点决定了继承不像静态语言那样是必须的。
参考源码
animals.py
获取对象信息
当我们拿到一个对象的引用时如何知道这个对象是什么类型、有哪些方法呢
使用type()
首先我们来判断对象类型使用type()函数
基本类型都可以用type()判断 type(123)
class inttype(str)
class strtype(None)
type(None) NoneType如果一个变量指向函数或者类也可以用type()判断 type(abs)
class builtin_function_or_methodtype(a)
class __main__.Animal但是type()函数返回的是什么类型呢它返回对应的Class类型。如果我们要在if语句中判断就需要比较两个变量的type类型是否相同 type(123)type(456)
Truetype(123)int
Truetype(abc)type(123)
Truetype(abc)str
Truetype(abc)type(123)
False判断基本数据类型可以直接写intstr等但如果要判断一个对象是否是函数怎么办可以使用types模块中定义的常量 import typesdef fn():
... pass
...type(fn)types.FunctionType
Truetype(abs)types.BuiltinFunctionType
Truetype(lambda x: x)types.LambdaType
Truetype((x for x in range(10)))types.GeneratorType
True使用isinstance()
对于class的继承关系来说使用type()就很不方便。我们要判断class的类型可以使用isinstance()函数。
我们回顾上次的例子如果继承关系是
object - Animal - Dog - Husky那么isinstance()就可以告诉我们一个对象是否是某种类型。先创建3种类型的对象 a Animal()d Dog()h Husky()然后判断 isinstance(h, Husky)
True没有问题因为h变量指向的就是Husky对象。
再判断 isinstance(h, Dog)
Trueh虽然自身是Husky类型但由于Husky是从Dog继承下来的所以h也还是Dog类型。换句话说isinstance()判断的是一个对象是否是该类型本身或者位于该类型的父继承链上。
因此我们可以确信h还是Animal类型 isinstance(h, Animal)
True同理实际类型是Dog的d也是Animal类型 isinstance(d, Dog) and isinstance(d, Animal)
True但是d不是Husky类型 isinstance(d, Husky)
False能用type()判断的基本类型也可以用isinstance()判断 isinstance(a, str)
Trueisinstance(123, int)
Trueisinstance(ba, bytes)
True并且还可以判断一个变量是否是某些类型中的一种比如下面的代码就可以判断是否是list或者tuple isinstance([1, 2, 3], (list, tuple))
Trueisinstance((1, 2, 3), (list, tuple))
True使用dir()
如果要获得一个对象的所有属性和方法可以使用dir()函数它返回一个包含字符串的list比如获得一个str对象的所有属性和方法 dir(ABC)
[__add__, __class__, __contains__, __delattr__, __dir__, __doc__, __eq__, __format__, __ge__, __getattribute__, __getitem__, __getnewargs__, __gt__, __hash__, __init__, __iter__, __le__, __len__, __lt__, __mod__, __mul__, __ne__, __new__, __reduce__, __reduce_ex__, __repr__, __rmod__, __rmul__, __setattr__, __sizeof__, __str__, __subclasshook__, capitalize, casefold, center, count, encode, endswith, expandtabs, find, format, format_map, index, isalnum, isalpha, isdecimal, isdigit, isidentifier, islower, isnumeric, isprintable, isspace, istitle, isupper, join, ljust, lower, lstrip, maketrans, partition, replace, rfind, rindex, rjust, rpartition, rsplit, rstrip, split, splitlines, startswith, strip, swapcase, title, translate, upper, zfill]类似__xxx__的属性和方法在Python中都是有特殊用途的比如__len__方法返回长度。在Python中如果你调用len()函数试图获取一个对象的长度实际上在len()函数内部它自动去调用该对象的__len__()方法所以下面的代码是等价的 len(ABC)
3ABC.__len__()
3我们自己写的类如果也想用len(myObj)的话就自己写一个__len__()方法 class MyDog(object):
... def __len__(self):
... return 100
...dog MyDog()len(dog)
100剩下的都是普通属性或方法比如lower()返回小写的字符串 ABC.lower()
abc仅仅把属性和方法列出来是不够的配合getattr()、setattr()以及hasattr()我们可以直接操作一个对象的状态 class MyObject(object):
... def __init__(self):
... self.x 9
... def power(self):
... return self.x * self.x
...obj MyObject()紧接着可以测试该对象的属性 hasattr(obj, x) # 有属性x吗
Trueobj.x
9hasattr(obj, y) # 有属性y吗
Falsesetattr(obj, y, 19) # 设置一个属性yhasattr(obj, y) # 有属性y吗
Truegetattr(obj, y) # 获取属性y
19obj.y # 获取属性y
19如果试图获取不存在的属性会抛出AttributeError的错误 getattr(obj, z) # 获取属性z
Traceback (most recent call last):File stdin, line 1, in module
AttributeError: MyObject object has no attribute z可以传入一个default参数如果属性不存在就返回默认值 getattr(obj, z, 404) # 获取属性z如果不存在返回默认值404
404也可以获得对象的方法 hasattr(obj, power) # 有属性power吗
Truegetattr(obj, power) # 获取属性power
bound method MyObject.power of __main__.MyObject object at 0x10077a6a0fn getattr(obj, power) # 获取属性power并赋值到变量fnfn # fn指向obj.power
bound method MyObject.power of __main__.MyObject object at 0x10077a6a0fn() # 调用fn()与调用obj.power()是一样的
81小结
通过内置的一系列函数我们可以对任意一个Python对象进行剖析拿到其内部的数据。要注意的是只有在不知道对象信息的时候我们才会去获取对象信息。如果可以直接写
sum obj.x obj.y就不要写
sum getattr(obj, x) getattr(obj, y)一个正确的用法的例子如下
def readImage(fp):if hasattr(fp, read):return readData(fp)return None假设我们希望从文件流fp中读取图像我们首先要判断该fp对象是否存在read方法如果存在则该对象是一个流如果不存在则无法读取。hasattr()就派上了用场。
请注意在Python这类动态语言中根据鸭子类型有read()方法不代表该fp对象就是一个文件流它也可能是网络流也可能是内存中的一个字节流但只要read()方法返回的是有效的图像数据就不影响读取图像的功能。
参考源码
get_type.py
attrs.py
实例属性和类属性
由于Python是动态语言根据类创建的实例可以任意绑定属性。
给实例绑定属性的方法是通过实例变量或者通过self变量
class Student(object):def __init__(self, name):self.name names Student(Bob)
s.score 90但是如果Student类本身需要绑定一个属性呢可以直接在class中定义属性这种属性是类属性归Student类所有
class Student(object):name Student当我们定义了一个类属性后这个属性虽然归类所有但类的所有实例都可以访问到。来测试一下 class Student(object):
... name Student
...s Student() # 创建实例sprint(s.name) # 打印name属性因为实例并没有name属性所以会继续查找class的name属性
Studentprint(Student.name) # 打印类的name属性
Students.name Michael # 给实例绑定name属性print(s.name) # 由于实例属性优先级比类属性高因此它会屏蔽掉类的name属性
Michaelprint(Student.name) # 但是类属性并未消失用Student.name仍然可以访问
Studentdel s.name # 如果删除实例的name属性print(s.name) # 再次调用s.name由于实例的name属性没有找到类的name属性就显示出来了
Student从上面的例子可以看出在编写程序的时候千万不要把实例属性和类属性使用相同的名字因为相同名称的实例属性将屏蔽掉类属性但是当你删除实例属性后再使用相同的名称访问到的将是类属性。
面向对象高级编程
数据封装、继承和多态只是面向对象程序设计中最基础的3个概念。在Python中面向对象还有很多高级特性允许我们写出非常强大的功能。
我们会讨论多重继承、定制类、元类等概念。
使用__slots__
正常情况下当我们定义了一个class创建了一个class的实例后我们可以给该实例绑定任何属性和方法这就是动态语言的灵活性。先定义class
class Student(object):pass然后尝试给实例绑定一个属性 s Student()s.name Michael # 动态给实例绑定一个属性print(s.name)
Michael还可以尝试给实例绑定一个方法 def set_age(self, age): # 定义一个函数作为实例方法
... self.age age
...from types import MethodTypes.set_age MethodType(set_age, s) # 给实例绑定一个方法s.set_age(25) # 调用实例方法s.age # 测试结果
25但是给一个实例绑定的方法对另一个实例是不起作用的 s2 Student() # 创建新的实例s2.set_age(25) # 尝试调用方法
Traceback (most recent call last):File stdin, line 1, in module
AttributeError: Student object has no attribute set_age为了给所有实例都绑定方法可以给class绑定方法 def set_score(self, score):
... self.score score
...Student.set_score MethodType(set_score, Student)给class绑定方法后所有实例均可调用 s.set_score(100)s.score
100s2.set_score(99)s2.score
99通常情况下上面的set_score方法可以直接定义在class中但动态绑定允许我们在程序运行的过程中动态给class加上功能这在静态语言中很难实现。
使用__slots__
但是如果我们想要限制实例的属性怎么办比如只允许对Student实例添加name和age属性。
为了达到限制的目的Python允许在定义class的时候定义一个特殊的__slots__变量来限制该class实例能添加的属性
class Student(object):__slots__ (name, age) # 用tuple定义允许绑定的属性名称然后我们试试 s Student() # 创建新的实例s.name Michael # 绑定属性names.age 25 # 绑定属性ages.score 99 # 绑定属性score
Traceback (most recent call last):File stdin, line 1, in module
AttributeError: Student object has no attribute score由于score没有被放到__slots__中所以不能绑定score属性试图绑定score将得到AttributeError的错误。
使用__slots__要注意__slots__定义的属性仅对当前类实例起作用对继承的子类是不起作用的 class GraduateStudent(Student):
... pass
...g GraduateStudent()g.score 9999除非在子类中也定义__slots__这样子类实例允许定义的属性就是自身的__slots__加上父类的__slots__。
参考源码
use_slots.py
使用property
在绑定属性时如果我们直接把属性暴露出去虽然写起来很简单但是没办法检查参数导致可以把成绩随便改
s Student()
s.score 9999这显然不合逻辑。为了限制score的范围可以通过一个set_score()方法来设置成绩再通过一个get_score()来获取成绩这样在set_score()方法里就可以检查参数
class Student(object):def get_score(self):return self._scoredef set_score(self, value):if not isinstance(value, int):raise ValueError(score must be an integer!)if value 0 or value 100:raise ValueError(score must between 0 ~ 100!)self._score value现在对任意的Student实例进行操作就不能随心所欲地设置score了 s Student()s.set_score(60) # ok!s.get_score()
60s.set_score(9999)
Traceback (most recent call last):...
ValueError: score must between 0 ~ 100!但是上面的调用方法又略显复杂没有直接用属性这么直接简单。
有没有既能检查参数又可以用类似属性这样简单的方式来访问类的变量呢对于追求完美的Python程序员来说这是必须要做到的
还记得装饰器decorator可以给函数动态加上功能吗对于类的方法装饰器一样起作用。Python内置的property装饰器就是负责把一个方法变成属性调用的
class Student(object):propertydef score(self):return self._scorescore.setterdef score(self, value):if not isinstance(value, int):raise ValueError(score must be an integer!)if value 0 or value 100:raise ValueError(score must between 0 ~ 100!)self._score valueproperty的实现比较复杂我们先考察如何使用。把一个getter方法变成属性只需要加上property就可以了此时property本身又创建了另一个装饰器score.setter负责把一个setter方法变成属性赋值于是我们就拥有一个可控的属性操作 s Student()s.score 60 # OK实际转化为s.set_score(60)s.score # OK实际转化为s.get_score()
60s.score 9999
Traceback (most recent call last):...
ValueError: score must between 0 ~ 100!注意到这个神奇的property我们在对实例属性操作的时候就知道该属性很可能不是直接暴露的而是通过getter和setter方法来实现的。
还可以定义只读属性只定义getter方法不定义setter方法就是一个只读属性
class Student(object):propertydef birth(self):return self._birthbirth.setterdef birth(self, value):self._birth valuepropertydef age(self):return 2015 - self._birth上面的birth是可读写属性而age就是一个只读属性因为age可以根据birth和当前时间计算出来。
小结
property广泛应用在类的定义中可以让调用者写出简短的代码同时保证对参数进行必要的检查这样程序运行时就减少了出错的可能性。
练习
请利用property给一个Screen对象加上width和height属性以及一个只读属性resolution
# -*- coding: utf-8 -*-class Screen(object):
----pass
----
# test:
s Screen()
s.width 1024
s.height 768
print(s.resolution)
assert s.resolution 786432, 1024 * 768 %d ? % s.resolution参考源码
use_property.py
多重继承
继承是面向对象编程的一个重要的方式因为通过继承子类就可以扩展父类的功能。
回忆一下Animal类层次的设计假设我们要实现以下4种动物
Dog - 狗狗Bat - 蝙蝠Parrot - 鹦鹉Ostrich - 鸵鸟。
如果按照哺乳动物和鸟类归类我们可以设计出这样的类的层次 但是如果按照“能跑”和“能飞”来归类我们就应该设计出这样的类的层次 如果要把上面的两种分类都包含进来我们就得设计更多的层次
哺乳类能跑的哺乳类能飞的哺乳类鸟类能跑的鸟类能飞的鸟类。
这么一来类的层次就复杂了 如果要再增加“宠物类”和“非宠物类”这么搞下去类的数量会呈指数增长很明显这样设计是不行的。
正确的做法是采用多重继承。首先主要的类层次仍按照哺乳类和鸟类设计
class Animal(object):pass# 大类:
class Mammal(Animal):passclass Bird(Animal):pass# 各种动物:
class Dog(Mammal):passclass Bat(Mammal):passclass Parrot(Bird):passclass Ostrich(Bird):pass现在我们要给动物再加上Runnable和Flyable的功能只需要先定义好Runnable和Flyable的类
class Runnable(object):def run(self):print(Running...)class Flyable(object):def fly(self):print(Flying...)对于需要Runnable功能的动物就多继承一个Runnable例如Dog
class Dog(Mammal, Runnable):pass对于需要Flyable功能的动物就多继承一个Flyable例如Bat
class Bat(Mammal, Flyable):pass通过多重继承一个子类就可以同时获得多个父类的所有功能。
MixIn
在设计类的继承关系时通常主线都是单一继承下来的例如Ostrich继承自Bird。但是如果需要“混入”额外的功能通过多重继承就可以实现比如让Ostrich除了继承自Bird外再同时继承Runnable。这种设计通常称之为MixIn。
为了更好地看出继承关系我们把Runnable和Flyable改为RunnableMixIn和FlyableMixIn。类似的你还可以定义出肉食动物CarnivorousMixIn和植食动物HerbivoresMixIn让某个动物同时拥有好几个MixIn
class Dog(Mammal, RunnableMixIn, CarnivorousMixIn):passMixIn的目的就是给一个类增加多个功能这样在设计类的时候我们优先考虑通过多重继承来组合多个MixIn的功能而不是设计多层次的复杂的继承关系。
Python自带的很多库也使用了MixIn。举个例子Python自带了TCPServer和UDPServer这两类网络服务而要同时服务多个用户就必须使用多进程或多线程模型这两种模型由ForkingMixIn和ThreadingMixIn提供。通过组合我们就可以创造出合适的服务来。
比如编写一个多进程模式的TCP服务定义如下
class MyTCPServer(TCPServer, ForkingMixIn):pass编写一个多线程模式的UDP服务定义如下
class MyUDPServer(UDPServer, ThreadingMixIn):pass如果你打算搞一个更先进的协程模型可以编写一个CoroutineMixIn
class MyTCPServer(TCPServer, CoroutineMixIn):pass这样一来我们不需要复杂而庞大的继承链只要选择组合不同的类的功能就可以快速构造出所需的子类。
小结
由于Python允许使用多重继承因此MixIn就是一种常见的设计。
只允许单一继承的语言如Java不能使用MixIn的设计。
定制类
看到类似__slots__这种形如__xxx__的变量或者函数名就要注意这些在Python中是有特殊用途的。
__slots__我们已经知道怎么用了__len__()方法我们也知道是为了能让class作用于len()函数。
除此之外Python的class中还有许多这样有特殊用途的函数可以帮助我们定制类。
__str__
我们先定义一个Student类打印一个实例 class Student(object):
... def __init__(self, name):
... self.name name
...print(Student(Michael))
__main__.Student object at 0x109afb190打印出一堆__main__.Student object at 0x109afb190不好看。
怎么才能打印得好看呢只需要定义好__str__()方法返回一个好看的字符串就可以了 class Student(object):
... def __init__(self, name):
... self.name name
... def __str__(self):
... return Student object (name: %s) % self.name
...print(Student(Michael))
Student object (name: Michael)这样打印出来的实例不但好看而且容易看出实例内部重要的数据。
但是细心的朋友会发现直接敲变量不用print打印出来的实例还是不好看 s Student(Michael)s
__main__.Student object at 0x109afb310这是因为直接显示变量调用的不是__str__()而是__repr__()两者的区别是__str__()返回用户看到的字符串而__repr__()返回程序开发者看到的字符串也就是说__repr__()是为调试服务的。
解决办法是再定义一个__repr__()。但是通常__str__()和__repr__()代码都是一样的所以有个偷懒的写法
class Student(object):def __init__(self, name):self.name namedef __str__(self):return Student object (name%s) % self.name__repr__ __str____iter__
如果一个类想被用于for ... in循环类似list或tuple那样就必须实现一个__iter__()方法该方法返回一个迭代对象然后Python的for循环就会不断调用该迭代对象的__next__()方法拿到循环的下一个值直到遇到StopIteration错误时退出循环。
我们以斐波那契数列为例写一个Fib类可以作用于for循环
class Fib(object):def __init__(self):self.a, self.b 0, 1 # 初始化两个计数器abdef __iter__(self):return self # 实例本身就是迭代对象故返回自己def __next__(self):self.a, self.b self.b, self.a self.b # 计算下一个值if self.a 100000: # 退出循环的条件raise StopIteration();return self.a # 返回下一个值现在试试把Fib实例作用于for循环 for n in Fib():
... print(n)
...
1
1
2
3
5
...
46368
75025__getitem__
Fib实例虽然能作用于for循环看起来和list有点像但是把它当成list来使用还是不行比如取第5个元素 Fib()[5]
Traceback (most recent call last):File stdin, line 1, in module
TypeError: Fib object does not support indexing要表现得像list那样按照下标取出元素需要实现__getitem__()方法
class Fib(object):def __getitem__(self, n):a, b 1, 1for x in range(n):a, b b, a breturn a现在就可以按下标访问数列的任意一项了 f Fib()f[0]
1f[1]
1f[2]
2f[3]
3f[10]
89f[100]
573147844013817084101但是list有个神奇的切片方法 list(range(100))[5:10]
[5, 6, 7, 8, 9]对于Fib却报错。原因是__getitem__()传入的参数可能是一个int也可能是一个切片对象slice所以要做判断
class Fib(object):def __getitem__(self, n):if isinstance(n, int): # n是索引a, b 1, 1for x in range(n):a, b b, a breturn aif isinstance(n, slice): # n是切片start n.startstop n.stopif start is None:start 0a, b 1, 1L []for x in range(stop):if x start:L.append(a)a, b b, a breturn L现在试试Fib的切片 f Fib()f[0:5]
[1, 1, 2, 3, 5]f[:10]
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]但是没有对step参数作处理 f[:10:2]
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]也没有对负数作处理所以要正确实现一个__getitem__()还是有很多工作要做的。
此外如果把对象看成dict__getitem__()的参数也可能是一个可以作key的object例如str。
与之对应的是__setitem__()方法把对象视作list或dict来对集合赋值。最后还有一个__delitem__()方法用于删除某个元素。
总之通过上面的方法我们自己定义的类表现得和Python自带的list、tuple、dict没什么区别这完全归功于动态语言的“鸭子类型”不需要强制继承某个接口。
__getattr__
正常情况下当我们调用类的方法或属性时如果不存在就会报错。比如定义Student类
class Student(object):def __init__(self):self.name Michael调用name属性没问题但是调用不存在的score属性就有问题了 s Student()print(s.name)
Michaelprint(s.score)
Traceback (most recent call last):...
AttributeError: Student object has no attribute score错误信息很清楚地告诉我们没有找到score这个attribute。
要避免这个错误除了可以加上一个score属性外Python还有另一个机制那就是写一个__getattr__()方法动态返回一个属性。修改如下
class Student(object):def __init__(self):self.name Michaeldef __getattr__(self, attr):if attrscore:return 99当调用不存在的属性时比如scorePython解释器会试图调用__getattr__(self, score)来尝试获得属性这样我们就有机会返回score的值 s Student()s.name
Michaels.score
99返回函数也是完全可以的
class Student(object):def __getattr__(self, attr):if attrage:return lambda: 25只是调用方式要变为 s.age()
25注意只有在没有找到属性的情况下才调用__getattr__已有的属性比如name不会在__getattr__中查找。
此外注意到任意调用如s.abc都会返回None这是因为我们定义的__getattr__默认返回就是None。要让class只响应特定的几个属性我们就要按照约定抛出AttributeError的错误
class Student(object):def __getattr__(self, attr):if attrage:return lambda: 25raise AttributeError(\Student\ object has no attribute \%s\ % attr)这实际上可以把一个类的所有属性和方法调用全部动态化处理了不需要任何特殊手段。
这种完全动态调用的特性有什么实际作用呢作用就是可以针对完全动态的情况作调用。
举个例子
现在很多网站都搞REST API比如新浪微博、豆瓣啥的调用API的URL类似
http://api.server/user/friendshttp://api.server/user/timeline/list
如果要写SDK给每个URL对应的API都写一个方法那得累死而且API一旦改动SDK也要改。
利用完全动态的__getattr__我们可以写出一个链式调用
class Chain(object):def __init__(self, path):self._path pathdef __getattr__(self, path):return Chain(%s/%s % (self._path, path))def __str__(self):return self._path__repr__ __str__试试 Chain().status.user.timeline.list
/status/user/timeline/list这样无论API怎么变SDK都可以根据URL实现完全动态的调用而且不随API的增加而改变
还有些REST API会把参数放到URL中比如GitHub的API
GET /users/:user/repos调用时需要把:user替换为实际用户名。如果我们能写出这样的链式调用
Chain().users(michael).repos就可以非常方便地调用API了。有兴趣的童鞋可以试试写出来。
__call__
一个对象实例可以有自己的属性和方法当我们调用实例方法时我们用instance.method()来调用。能不能直接在实例本身上调用呢在Python中答案是肯定的。
任何类只需要定义一个__call__()方法就可以直接对实例进行调用。请看示例
class Student(object):def __init__(self, name):self.name namedef __call__(self):print(My name is %s. % self.name)调用方式如下 s Student(Michael)s() # self参数不要传入
My name is Michael.__call__()还可以定义参数。对实例进行直接调用就好比对一个函数进行调用一样所以你完全可以把对象看成函数把函数看成对象因为这两者之间本来就没啥根本的区别。
如果你把对象看成函数那么函数本身其实也可以在运行期动态创建出来因为类的实例都是运行期创建出来的这么一来我们就模糊了对象和函数的界限。
那么怎么判断一个变量是对象还是函数呢其实更多的时候我们需要判断一个对象是否能被调用能被调用的对象就是一个Callable对象比如函数和我们上面定义的带有__call__()的类实例 callable(Student())
Truecallable(max)
Truecallable([1, 2, 3])
Falsecallable(None)
Falsecallable(str)
False通过callable()函数我们就可以判断一个对象是否是“可调用”对象。
小结
Python的class允许定义许多定制方法可以让我们非常方便地生成特定的类。
本节介绍的是最常用的几个定制方法还有很多可定制的方法请参考Python的官方文档。
参考源码
special_str.py
special_iter.py
special_getitem.py
special_getattr.py
special_call.py
使用枚举类
当我们需要定义常量时一个办法是用大写变量通过整数来定义例如月份
JAN 1
FEB 2
MAR 3
...
NOV 11
DEC 12好处是简单缺点是类型是int并且仍然是变量。
更好的方法是为这样的枚举类型定义一个class类型然后每个常量都是class的一个唯一实例。Python提供了Enum类来实现这个功能
from enum import EnumMonth Enum(Month, (Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec))这样我们就获得了Month类型的枚举类可以直接使用Month.Jan来引用一个常量或者枚举它的所有成员
for name, member in Month.__members__.items():print(name, , member, ,, member.value)value属性则是自动赋给成员的int常量默认从1开始计数。
如果需要更精确地控制枚举类型可以从Enum派生出自定义类
from enum import Enum, uniqueunique
class Weekday(Enum):Sun 0 # Sun的value被设定为0Mon 1Tue 2Wed 3Thu 4Fri 5Sat 6unique装饰器可以帮助我们检查保证没有重复值。
访问这些枚举类型可以有若干种方法 day1 Weekday.Monprint(day1)
Weekday.Monprint(Weekday.Tue)
Weekday.Tueprint(Weekday[Tue])
Weekday.Tueprint(Weekday.Tue.value)
2print(day1 Weekday.Mon)
Trueprint(day1 Weekday.Tue)
Falseprint(Weekday(1))
Weekday.Monprint(day1 Weekday(1))
TrueWeekday(7)
Traceback (most recent call last):...
ValueError: 7 is not a valid Weekdayfor name, member in Weekday.__members__.items():
... print(name, , member)
...
Sun Weekday.Sun
Mon Weekday.Mon
Tue Weekday.Tue
Wed Weekday.Wed
Thu Weekday.Thu
Fri Weekday.Fri
Sat Weekday.Sat可见既可以用成员名称引用枚举常量又可以直接根据value的值获得枚举常量。
小结
Enum可以把一组相关常量定义在一个class中且class不可变而且成员可以直接比较。
参考源码
use_enum.py
使用元类
type()
动态语言和静态语言最大的不同就是函数和类的定义不是编译时定义的而是运行时动态创建的。
比方说我们要定义一个Hello的class就写一个hello.py模块
class Hello(object):def hello(self, nameworld):print(Hello, %s. % name)当Python解释器载入hello模块时就会依次执行该模块的所有语句执行结果就是动态创建出一个Hello的class对象测试如下 from hello import Helloh Hello()h.hello()
Hello, world.print(type(Hello))
class typeprint(type(h))
class hello.Hellotype()函数可以查看一个类型或变量的类型Hello是一个class它的类型就是type而h是一个实例它的类型就是class Hello。
我们说class的定义是运行时动态创建的而创建class的方法就是使用type()函数。
type()函数既可以返回一个对象的类型又可以创建出新的类型比如我们可以通过type()函数创建出Hello类而无需通过class Hello(object)...的定义 def fn(self, nameworld): # 先定义函数
... print(Hello, %s. % name)
...Hello type(Hello, (object,), dict(hellofn)) # 创建Hello classh Hello()h.hello()
Hello, world.print(type(Hello))
class typeprint(type(h))
class __main__.Hello要创建一个class对象type()函数依次传入3个参数
class的名称继承的父类集合注意Python支持多重继承如果只有一个父类别忘了tuple的单元素写法class的方法名称与函数绑定这里我们把函数fn绑定到方法名hello上。
通过type()函数创建的类和直接写class是完全一样的因为Python解释器遇到class定义时仅仅是扫描一下class定义的语法然后调用type()函数创建出class。
正常情况下我们都用class Xxx...来定义类但是type()函数也允许我们动态创建出类来也就是说动态语言本身支持运行期动态创建类这和静态语言有非常大的不同要在静态语言运行期创建类必须构造源代码字符串再调用编译器或者借助一些工具生成字节码实现本质上都是动态编译会非常复杂。
metaclass
除了使用type()动态创建类以外要控制类的创建行为还可以使用metaclass。
metaclass直译为元类简单的解释就是
当我们定义了类以后就可以根据这个类创建出实例所以先定义类然后创建实例。
但是如果我们想创建出类呢那就必须根据metaclass创建出类所以先定义metaclass然后创建类。
连接起来就是先定义metaclass就可以创建类最后创建实例。
所以metaclass允许你创建类或者修改类。换句话说你可以把类看成是metaclass创建出来的“实例”。
metaclass是Python面向对象里最难理解也是最难使用的魔术代码。正常情况下你不会碰到需要使用metaclass的情况所以以下内容看不懂也没关系因为基本上你不会用到。
我们先看一个简单的例子这个metaclass可以给我们自定义的MyList增加一个add方法
定义ListMetaclass按照默认习惯metaclass的类名总是以Metaclass结尾以便清楚地表示这是一个metaclass
# metaclass是类的模板所以必须从type类型派生
class ListMetaclass(type):def __new__(cls, name, bases, attrs):attrs[add] lambda self, value: self.append(value)return type.__new__(cls, name, bases, attrs)有了ListMetaclass我们在定义类的时候还要指示使用ListMetaclass来定制类传入关键字参数metaclass
class MyList(list, metaclassListMetaclass):pass当我们传入关键字参数metaclass时魔术就生效了它指示Python解释器在创建MyList时要通过ListMetaclass.__new__()来创建在此我们可以修改类的定义比如加上新的方法然后返回修改后的定义。
__new__()方法接收到的参数依次是 当前准备创建的类的对象 类的名字 类继承的父类集合 类的方法集合。
测试一下MyList是否可以调用add()方法 L MyList()L.add(1)L
[1]而普通的list没有add()方法 L2 list()L2.add(1)
Traceback (most recent call last):File stdin, line 1, in module
AttributeError: list object has no attribute add动态修改有什么意义直接在MyList定义中写上add()方法不是更简单吗正常情况下确实应该直接写通过metaclass修改纯属变态。
但是总会遇到需要通过metaclass修改类定义的。ORM就是一个典型的例子。
ORM全称“Object Relational Mapping”即对象-关系映射就是把关系数据库的一行映射为一个对象也就是一个类对应一个表这样写代码更简单不用直接操作SQL语句。
要编写一个ORM框架所有的类都只能动态定义因为只有使用者才能根据表的结构定义出对应的类来。
让我们来尝试编写一个ORM框架。
编写底层模块的第一步就是先把调用接口写出来。比如使用者如果使用这个ORM框架想定义一个User类来操作对应的数据库表User我们期待他写出这样的代码
class User(Model):# 定义类的属性到列的映射id IntegerField(id)name StringField(username)email StringField(email)password StringField(password)# 创建一个实例
u User(id12345, nameMichael, emailtestorm.org, passwordmy-pwd)
# 保存到数据库
u.save()其中父类Model和属性类型StringField、IntegerField是由ORM框架提供的剩下的魔术方法比如save()全部由metaclass自动完成。虽然metaclass的编写会比较复杂但ORM的使用者用起来却异常简单。
现在我们就按上面的接口来实现该ORM。
首先来定义Field类它负责保存数据库表的字段名和字段类型
class Field(object):def __init__(self, name, column_type):self.name nameself.column_type column_typedef __str__(self):return %s:%s % (self.__class__.__name__, self.name)在Field的基础上进一步定义各种类型的Field比如StringFieldIntegerField等等
class StringField(Field):def __init__(self, name):super(StringField, self).__init__(name, varchar(100))class IntegerField(Field):def __init__(self, name):super(IntegerField, self).__init__(name, bigint)下一步就是编写最复杂的ModelMetaclass了
class ModelMetaclass(type):def __new__(cls, name, bases, attrs):if nameModel:return type.__new__(cls, name, bases, attrs)print(Found model: %s % name)mappings dict()for k, v in attrs.items():if isinstance(v, Field):print(Found mapping: %s %s % (k, v))mappings[k] vfor k in mappings.keys():attrs.pop(k)attrs[__mappings__] mappings # 保存属性和列的映射关系attrs[__table__] name # 假设表名和类名一致return type.__new__(cls, name, bases, attrs)以及基类Model
class Model(dict, metaclassModelMetaclass):def __init__(self, **kw):super(Model, self).__init__(**kw)def __getattr__(self, key):try:return self[key]except KeyError:raise AttributeError(rModel object has no attribute %s % key)def __setattr__(self, key, value):self[key] valuedef save(self):fields []params []args []for k, v in self.__mappings__.items():fields.append(v.name)params.append(?)args.append(getattr(self, k, None))sql insert into %s (%s) values (%s) % (self.__table__, ,.join(fields), ,.join(params))print(SQL: %s % sql)print(ARGS: %s % str(args))当用户定义一个class User(Model)时Python解释器首先在当前类User的定义中查找metaclass如果没有找到就继续在父类Model中查找metaclass找到了就使用Model中定义的metaclass的ModelMetaclass来创建User类也就是说metaclass可以隐式地继承到子类但子类自己却感觉不到。
在ModelMetaclass中一共做了几件事情 排除掉对Model类的修改 在当前类比如User中查找定义的类的所有属性如果找到一个Field属性就把它保存到一个__mappings__的dict中同时从类属性中删除该Field属性否则容易造成运行时错误实例的属性会遮盖类的同名属性 把表名保存到__table__中这里简化为表名默认为类名。
在Model类中就可以定义各种操作数据库的方法比如save()delete()find()update等等。
我们实现了save()方法把一个实例保存到数据库中。因为有表名属性到字段的映射和属性值的集合就可以构造出INSERT语句。
编写代码试试
u User(id12345, nameMichael, emailtestorm.org, passwordmy-pwd)
u.save()输出如下
Found model: User
Found mapping: email StringField:email
Found mapping: password StringField:password
Found mapping: id IntegerField:uid
Found mapping: name StringField:username
SQL: insert into User (password,email,username,id) values (?,?,?,?)
ARGS: [my-pwd, testorm.org, Michael, 12345]可以看到save()方法已经打印出了可执行的SQL语句以及参数列表只需要真正连接到数据库执行该SQL语句就可以完成真正的功能。
不到100行代码我们就通过metaclass实现了一个精简的ORM框架。
小结
metaclass是Python中非常具有魔术性的对象它可以改变类创建时的行为。这种强大的功能使用起来务必小心。
参考源码
create_class_on_the_fly.py
use_metaclass.py
orm.py
错误、调试和测试
在程序运行过程中总会遇到各种各样的错误。
有的错误是程序编写有问题造成的比如本来应该输出整数结果输出了字符串这种错误我们通常称之为bugbug是必须修复的。
有的错误是用户输入造成的比如让用户输入email地址结果得到一个空字符串这种错误可以通过检查用户输入来做相应的处理。
还有一类错误是完全无法在程序运行过程中预测的比如写入文件的时候磁盘满了写不进去了或者从网络抓取数据网络突然断掉了。这类错误也称为异常在程序中通常是必须处理的否则程序会因为各种问题终止并退出。
Python内置了一套异常处理机制来帮助我们进行错误处理。
此外我们也需要跟踪程序的执行查看变量的值是否正确这个过程称为调试。Python的pdb可以让我们以单步方式执行代码。
最后编写测试也很重要。有了良好的测试就可以在程序修改后反复运行确保程序输出符合我们编写的测试。
错误处理
在程序运行的过程中如果发生了错误可以事先约定返回一个错误代码这样就可以知道是否有错以及出错的原因。在操作系统提供的调用中返回错误码非常常见。比如打开文件的函数open()成功时返回文件描述符就是一个整数出错时返回-1。
用错误码来表示是否出错十分不便因为函数本身应该返回的正常结果和错误码混在一起造成调用者必须用大量的代码来判断是否出错
def foo():r some_function()if r(-1):return (-1)# do somethingreturn rdef bar():r foo()if r(-1):print(Error)else:pass一旦出错还要一级一级上报直到某个函数可以处理该错误比如给用户输出一个错误信息。
所以高级语言通常都内置了一套try...except...finally...的错误处理机制Python也不例外。
try
让我们用一个例子来看看try的机制
try:print(try...)r 10 / 0print(result:, r)
except ZeroDivisionError as e:print(except:, e)
finally:print(finally...)
print(END)当我们认为某些代码可能会出错时就可以用try来运行这段代码如果执行出错则后续代码不会继续执行而是直接跳转至错误处理代码即except语句块执行完except后如果有finally语句块则执行finally语句块至此执行完毕。
上面的代码在计算10 / 0时会产生一个除法运算错误
try...
except: division by zero
finally...
END从输出可以看到当错误发生时后续语句print(result:, r)不会被执行except由于捕获到ZeroDivisionError因此被执行。最后finally语句被执行。然后程序继续按照流程往下走。
如果把除数0改成2则执行结果如下
try...
result: 5
finally...
END由于没有错误发生所以except语句块不会被执行但是finally如果有则一定会被执行可以没有finally语句。
你还可以猜测错误应该有很多种类如果发生了不同类型的错误应该由不同的except语句块处理。没错可以有多个except来捕获不同类型的错误
try:print(try...)r 10 / int(a)print(result:, r)
except ValueError as e:print(ValueError:, e)
except ZeroDivisionError as e:print(ZeroDivisionError:, e)
finally:print(finally...)
print(END)int()函数可能会抛出ValueError所以我们用一个except捕获ValueError用另一个except捕获ZeroDivisionError。
此外如果没有错误发生可以在except语句块后面加一个else当没有错误发生时会自动执行else语句
try:print(try...)r 10 / int(2)print(result:, r)
except ValueError as e:print(ValueError:, e)
except ZeroDivisionError as e:print(ZeroDivisionError:, e)
else:print(no error!)
finally:print(finally...)
print(END)Python的错误其实也是class所有的错误类型都继承自BaseException所以在使用except时需要注意的是它不但捕获该类型的错误还把其子类也“一网打尽”。比如
try:foo()
except ValueError as e:print(ValueError)
except UnicodeError as e:print(UnicodeError)第二个except永远也捕获不到UnicodeError因为UnicodeError是ValueError的子类如果有也被第一个except给捕获了。
Python所有的错误都是从BaseException类派生的常见的错误类型和继承关系看这里
https://docs.python.org/3/library/exceptions.html#exception-hierarchy
使用try...except捕获错误还有一个巨大的好处就是可以跨越多层调用比如函数main()调用foo()foo()调用bar()结果bar()出错了这时只要main()捕获到了就可以处理
def foo(s):return 10 / int(s)def bar(s):return foo(s) * 2def main():try:bar(0)except Exception as e:print(Error:, e)finally:print(finally...)也就是说不需要在每个可能出错的地方去捕获错误只要在合适的层次去捕获错误就可以了。这样一来就大大减少了写try...except...finally的麻烦。
调用堆栈
如果错误没有被捕获它就会一直往上抛最后被Python解释器捕获打印一个错误信息然后程序退出。来看看err.py
# err.py:
def foo(s):return 10 / int(s)def bar(s):return foo(s) * 2def main():bar(0)main()执行结果如下
$ python3 err.py
Traceback (most recent call last):File err.py, line 11, in modulemain()File err.py, line 9, in mainbar(0)File err.py, line 6, in barreturn foo(s) * 2File err.py, line 3, in fooreturn 10 / int(s)
ZeroDivisionError: division by zero出错并不可怕可怕的是不知道哪里出错了。解读错误信息是定位错误的关键。我们从上往下可以看到整个错误的调用函数链
错误信息第1行
Traceback (most recent call last):告诉我们这是错误的跟踪信息。
第2~3行 File err.py, line 11, in modulemain()调用main()出错了在代码文件err.py的第11行代码但原因是第9行 File err.py, line 9, in mainbar(0)调用bar(0)出错了在代码文件err.py的第9行代码但原因是第6行 File err.py, line 6, in barreturn foo(s) * 2原因是return foo(s) * 2这个语句出错了但这还不是最终原因继续往下看 File err.py, line 3, in fooreturn 10 / int(s)原因是return 10 / int(s)这个语句出错了这是错误产生的源头因为下面打印了
ZeroDivisionError: integer division or modulo by zero根据错误类型ZeroDivisionError我们判断int(s)本身并没有出错但是int(s)返回0在计算10 / 0时出错至此找到错误源头。
记录错误
如果不捕获错误自然可以让Python解释器来打印出错误堆栈但程序也被结束了。既然我们能捕获错误就可以把错误堆栈打印出来然后分析错误原因同时让程序继续执行下去。
Python内置的logging模块可以非常容易地记录错误信息
# err_logging.pyimport loggingdef foo(s):return 10 / int(s)def bar(s):return foo(s) * 2def main():try:bar(0)except Exception as e:logging.exception(e)main()
print(END)同样是出错但程序打印完错误信息后会继续执行并正常退出
$ python3 err_logging.py
ERROR:root:division by zero
Traceback (most recent call last):File err_logging.py, line 13, in mainbar(0)File err_logging.py, line 9, in barreturn foo(s) * 2File err_logging.py, line 6, in fooreturn 10 / int(s)
ZeroDivisionError: division by zero
END通过配置logging还可以把错误记录到日志文件里方便事后排查。
抛出错误
因为错误是class捕获一个错误就是捕获到该class的一个实例。因此错误并不是凭空产生的而是有意创建并抛出的。Python的内置函数会抛出很多类型的错误我们自己编写的函数也可以抛出错误。
如果要抛出错误首先根据需要可以定义一个错误的class选择好继承关系然后用raise语句抛出一个错误的实例
# err_raise.py
class FooError(ValueError):passdef foo(s):n int(s)if n0:raise FooError(invalid value: %s % s)return 10 / nfoo(0)执行可以最后跟踪到我们自己定义的错误
$ python3 err_raise.py
Traceback (most recent call last):File err_throw.py, line 11, in modulefoo(0)File err_throw.py, line 8, in fooraise FooError(invalid value: %s % s)
__main__.FooError: invalid value: 0只有在必要的时候才定义我们自己的错误类型。如果可以选择Python已有的内置的错误类型比如ValueErrorTypeError尽量使用Python内置的错误类型。
最后我们来看另一种错误处理的方式
# err_reraise.pydef foo(s):n int(s)if n0:raise ValueError(invalid value: %s % s)return 10 / ndef bar():try:foo(0)except ValueError as e:print(ValueError!)raisebar()在bar()函数中我们明明已经捕获了错误但是打印一个ValueError!后又把错误通过raise语句抛出去了这不有病么
其实这种错误处理方式不但没病而且相当常见。捕获错误目的只是记录一下便于后续追踪。但是由于当前函数不知道应该怎么处理该错误所以最恰当的方式是继续往上抛让顶层调用者去处理。好比一个员工处理不了一个问题时就把问题抛给他的老板如果他的老板也处理不了就一直往上抛最终会抛给CEO去处理。
raise语句如果不带参数就会把当前错误原样抛出。此外在except中raise一个Error还可以把一种类型的错误转化成另一种类型
try:10 / 0
except ZeroDivisionError:raise ValueError(input error!)只要是合理的转换逻辑就可以但是决不应该把一个IOError转换成毫不相干的ValueError。
小结
Python内置的try...except...finally用来处理错误十分方便。出错时会分析错误信息并定位错误发生的代码位置才是最关键的。
程序也可以主动抛出错误让调用者来处理相应的错误。但是应该在文档中写清楚可能会抛出哪些错误以及错误产生的原因。
参考源码
do_try.py
err.py
err_logging.py
err_raise.py
err_reraise.py
调试
程序能一次写完并正常运行的概率很小基本不超过1%。总会有各种各样的bug需要修正。有的bug很简单看看错误信息就知道有的bug很复杂我们需要知道出错时哪些变量的值是正确的哪些变量的值是错误的因此需要一整套调试程序的手段来修复bug。
第一种方法简单直接粗暴有效就是用print()把可能有问题的变量打印出来看看
def foo(s):n int(s)print( n %d % n)return 10 / ndef main():foo(0)main()执行后在输出中查找打印的变量值
$ python3 err.pyn 0
Traceback (most recent call last):...
ZeroDivisionError: integer division or modulo by zero用print()最大的坏处是将来还得删掉它想想程序里到处都是print()运行结果也会包含很多垃圾信息。所以我们又有第二种方法。
断言
凡是用print()来辅助查看的地方都可以用断言assert来替代
def foo(s):n int(s)assert n ! 0, n is zero!return 10 / ndef main():foo(0)assert的意思是表达式n ! 0应该是True否则根据程序运行的逻辑后面的代码肯定会出错。
如果断言失败assert语句本身就会抛出AssertionError
$ python3 err.py
Traceback (most recent call last):...
AssertionError: n is zero!程序中如果到处充斥着assert和print()相比也好不到哪去。不过启动Python解释器时可以用-O参数来关闭assert
$ python3 -O err.py
Traceback (most recent call last):...
ZeroDivisionError: division by zero关闭后你可以把所有的assert语句当成pass来看。
logging
把print()替换为logging是第3种方式和assert比logging不会抛出错误而且可以输出到文件
import loggings 0
n int(s)
logging.info(n %d % n)
print(10 / n)logging.info()就可以输出一段文本。运行发现除了ZeroDivisionError没有任何信息。怎么回事
别急在import logging之后添加一行配置再试试
import logging
logging.basicConfig(levellogging.INFO)看到输出了
$ python3 err.py
INFO:root:n 0
Traceback (most recent call last):File err.py, line 8, in moduleprint(10 / n)
ZeroDivisionError: division by zero这就是logging的好处它允许你指定记录信息的级别有debuginfowarningerror等几个级别当我们指定levelINFO时logging.debug就不起作用了。同理指定levelWARNING后debug和info就不起作用了。这样一来你可以放心地输出不同级别的信息也不用删除最后统一控制输出哪个级别的信息。
logging的另一个好处是通过简单的配置一条语句可以同时输出到不同的地方比如console和文件。
pdb
第4种方式是启动Python的调试器pdb让程序以单步方式运行可以随时查看运行状态。我们先准备好程序
# err.py
s 0
n int(s)
print(10 / n)然后启动
$ python3 -m pdb err.py/Users/michael/Github/learn-python3/samples/debug/err.py(2)module()
- s 0以参数-m pdb启动后pdb定位到下一步要执行的代码- s 0。输入命令l来查看代码
(Pdb) l1 # err.py2 - s 03 n int(s)4 print(10 / n)输入命令n可以单步执行代码
(Pdb) n/Users/michael/Github/learn-python3/samples/debug/err.py(3)module()
- n int(s)
(Pdb) n/Users/michael/Github/learn-python3/samples/debug/err.py(4)module()
- print(10 / n)任何时候都可以输入命令p 变量名来查看变量
(Pdb) p s
0
(Pdb) p n
0输入命令q结束调试退出程序
(Pdb) q这种通过pdb在命令行调试的方法理论上是万能的但实在是太麻烦了如果有一千行代码要运行到第999行得敲多少命令啊。还好我们还有另一种调试方法。
pdb.set_trace()
这个方法也是用pdb但是不需要单步执行我们只需要import pdb然后在可能出错的地方放一个pdb.set_trace()就可以设置一个断点
# err.py
import pdbs 0
n int(s)
pdb.set_trace() # 运行到这里会自动暂停
print(10 / n)运行代码程序会自动在pdb.set_trace()暂停并进入pdb调试环境可以用命令p查看变量或者用命令c继续运行
$ python3 err.py /Users/michael/Github/learn-python3/samples/debug/err.py(7)module()
- print(10 / n)
(Pdb) p n
0
(Pdb) c
Traceback (most recent call last):File err.py, line 7, in moduleprint(10 / n)
ZeroDivisionError: division by zero这个方式比直接启动pdb单步调试效率要高很多但也高不到哪去。
IDE
如果要比较爽地设置断点、单步执行就需要一个支持调试功能的IDE。目前比较好的Python IDE有PyCharm
http://www.jetbrains.com/pycharm/
另外Eclipse加上pydev插件也可以调试Python程序。
小结
写程序最痛苦的事情莫过于调试程序往往会以你意想不到的流程来运行你期待执行的语句其实根本没有执行这时候就需要调试了。
虽然用IDE调试起来比较方便但是最后你会发现logging才是终极武器。
参考源码
do_assert.py
do_logging.py
do_pdb.py
单元测试
如果你听说过“测试驱动开发”TDDTest-Driven Development单元测试就不陌生。
单元测试是用来对一个模块、一个函数或者一个类来进行正确性检验的测试工作。
比如对函数abs()我们可以编写出以下几个测试用例 输入正数比如1、1.2、0.99期待返回值与输入相同 输入负数比如-1、-1.2、-0.99期待返回值与输入相反 输入0期待返回0 输入非数值类型比如None、[]、{}期待抛出TypeError。
把上面的测试用例放到一个测试模块里就是一个完整的单元测试。
如果单元测试通过说明我们测试的这个函数能够正常工作。如果单元测试不通过要么函数有bug要么测试条件输入不正确总之需要修复使单元测试能够通过。
单元测试通过后有什么意义呢如果我们对abs()函数代码做了修改只需要再跑一遍单元测试如果通过说明我们的修改不会对abs()函数原有的行为造成影响如果测试不通过说明我们的修改与原有行为不一致要么修改代码要么修改测试。
这种以测试为驱动的开发模式最大的好处就是确保一个程序模块的行为符合我们设计的测试用例。在将来修改的时候可以极大程度地保证该模块行为仍然是正确的。
我们来编写一个Dict类这个类的行为和dict一致但是可以通过属性来访问用起来就像下面这样 d Dict(a1, b2)d[a]
1d.a
1mydict.py代码如下
class Dict(dict):def __init__(self, **kw):super().__init__(**kw)def __getattr__(self, key):try:return self[key]except KeyError:raise AttributeError(rDict object has no attribute %s % key)def __setattr__(self, key, value):self[key] value为了编写单元测试我们需要引入Python自带的unittest模块编写mydict_test.py如下
import unittestfrom mydict import Dictclass TestDict(unittest.TestCase):def test_init(self):d Dict(a1, btest)self.assertEqual(d.a, 1)self.assertEqual(d.b, test)self.assertTrue(isinstance(d, dict))def test_key(self):d Dict()d[key] valueself.assertEqual(d.key, value)def test_attr(self):d Dict()d.key valueself.assertTrue(key in d)self.assertEqual(d[key], value)def test_keyerror(self):d Dict()with self.assertRaises(KeyError):value d[empty]def test_attrerror(self):d Dict()with self.assertRaises(AttributeError):value d.empty编写单元测试时我们需要编写一个测试类从unittest.TestCase继承。
以test开头的方法就是测试方法不以test开头的方法不被认为是测试方法测试的时候不会被执行。
对每一类测试都需要编写一个test_xxx()方法。由于unittest.TestCase提供了很多内置的条件判断我们只需要调用这些方法就可以断言输出是否是我们所期望的。最常用的断言就是assertEqual()
self.assertEqual(abs(-1), 1) # 断言函数返回的结果与1相等另一种重要的断言就是期待抛出指定类型的Error比如通过d[empty]访问不存在的key时断言会抛出KeyError
with self.assertRaises(KeyError):value d[empty]而通过d.empty访问不存在的key时我们期待抛出AttributeError
with self.assertRaises(AttributeError):value d.empty运行单元测试
一旦编写好单元测试我们就可以运行单元测试。最简单的运行方式是在mydict_test.py的最后加上两行代码
if __name__ __main__:unittest.main()这样就可以把mydict_test.py当做正常的python脚本运行
$ python3 mydict_test.py另一种方法是在命令行通过参数-m unittest直接运行单元测试
$ python3 -m unittest mydict_test
.....
----------------------------------------------------------------------
Ran 5 tests in 0.000sOK这是推荐的做法因为这样可以一次批量运行很多单元测试并且有很多工具可以自动来运行这些单元测试。
setUp与tearDown
可以在单元测试中编写两个特殊的setUp()和tearDown()方法。这两个方法会分别在每调用一个测试方法的前后分别被执行。
setUp()和tearDown()方法有什么用呢设想你的测试需要启动一个数据库这时就可以在setUp()方法中连接数据库在tearDown()方法中关闭数据库这样不必在每个测试方法中重复相同的代码
class TestDict(unittest.TestCase):def setUp(self):print(setUp...)def tearDown(self):print(tearDown...)可以再次运行测试看看每个测试方法调用前后是否会打印出setUp...和tearDown...。
小结
单元测试可以有效地测试某个程序模块的行为是未来重构代码的信心保证。
单元测试的测试用例要覆盖常用的输入组合、边界条件和异常。
单元测试代码要非常简单如果测试代码太复杂那么测试代码本身就可能有bug。
单元测试通过了并不意味着程序就没有bug了但是不通过程序肯定有bug。
参考源码
mydict.py
mydict_test.py
文档测试
如果你经常阅读Python的官方文档可以看到很多文档都有示例代码。比如re模块就带了很多示例代码 import rem re.search((?abc)def, abcdef)m.group(0)
def可以把这些示例代码在Python的交互式环境下输入并执行结果与文档中的示例代码显示的一致。
这些代码与其他说明可以写在注释中然后由一些工具来自动生成文档。既然这些代码本身就可以粘贴出来直接运行那么可不可以自动执行写在注释中的这些代码呢
答案是肯定的。
当我们编写注释时如果写上这样的注释
def abs(n):Function to get absolute value of number.Example: abs(1)1 abs(-1)1 abs(0)0return n if n 0 else (-n)无疑更明确地告诉函数的调用者该函数的期望输入和输出。
并且Python内置的“文档测试”doctest模块可以直接提取注释中的代码并执行测试。
doctest严格按照Python交互式命令行的输入和输出来判断测试结果是否正确。只有测试异常的时候可以用...表示中间一大段烦人的输出。
让我们用doctest来测试上次编写的Dict类
# mydict2.py
class Dict(dict):Simple dict but also support access as x.y style. d1 Dict() d1[x] 100 d1.x100 d1.y 200 d1[y]200 d2 Dict(a1, b2, c3) d2.c3 d2[empty]Traceback (most recent call last):...KeyError: empty d2.emptyTraceback (most recent call last):...AttributeError: Dict object has no attribute emptydef __init__(self, **kw):super(Dict, self).__init__(**kw)def __getattr__(self, key):try:return self[key]except KeyError:raise AttributeError(rDict object has no attribute %s % key)def __setattr__(self, key, value):self[key] valueif __name____main__:import doctestdoctest.testmod()运行python3 mydict2.py
$ python3 mydict2.py什么输出也没有。这说明我们编写的doctest运行都是正确的。如果程序有问题比如把__getattr__()方法注释掉再运行就会报错
$ python3 mydict2.py
**********************************************************************
File /Users/michael/Github/learn-python3/samples/debug/mydict2.py, line 10, in __main__.Dict
Failed example:d1.x
Exception raised:Traceback (most recent call last):...AttributeError: Dict object has no attribute x
**********************************************************************
File /Users/michael/Github/learn-python3/samples/debug/mydict2.py, line 16, in __main__.Dict
Failed example:d2.c
Exception raised:Traceback (most recent call last):...AttributeError: Dict object has no attribute c
**********************************************************************
1 items had failures:2 of 9 in __main__.Dict
***Test Failed*** 2 failures.注意到最后3行代码。当模块正常导入时doctest不会被执行。只有在命令行直接运行时才执行doctest。所以不必担心doctest会在非测试环境下执行。
练习
对函数fact(n)编写doctest并执行
# -*- coding: utf-8 -*-def fact(n):
----
----if n 1:raise ValueError()if n 1:return 1return n * fact(n - 1)if __name__ __main__:import doctestdoctest.testmod()小结
doctest非常有用不但可以用来测试还可以直接作为示例代码。通过某些文档生成工具就可以自动把包含doctest的注释提取出来。用户看文档的时候同时也看到了doctest。
参考源码
mydict2.py
IO编程
IO在计算机中指Input/Output也就是输入和输出。由于程序和运行时数据是在内存中驻留由CPU这个超快的计算核心来执行涉及到数据交换的地方通常是磁盘、网络等就需要IO接口。
比如你打开浏览器访问新浪首页浏览器这个程序就需要通过网络IO获取新浪的网页。浏览器首先会发送数据给新浪服务器告诉它我想要首页的HTML这个动作是往外发数据叫Output随后新浪服务器把网页发过来这个动作是从外面接收数据叫Input。所以通常程序完成IO操作会有Input和Output两个数据流。当然也有只用一个的情况比如从磁盘读取文件到内存就只有Input操作反过来把数据写到磁盘文件里就只是一个Output操作。
IO编程中Stream流是一个很重要的概念可以把流想象成一个水管数据就是水管里的水但是只能单向流动。Input Stream就是数据从外面磁盘、网络流进内存Output Stream就是数据从内存流到外面去。对于浏览网页来说浏览器和新浪服务器之间至少需要建立两根水管才可以既能发数据又能收数据。
由于CPU和内存的速度远远高于外设的速度所以在IO编程中就存在速度严重不匹配的问题。举个例子来说比如要把100M的数据写入磁盘CPU输出100M的数据只需要0.01秒可是磁盘要接收这100M数据可能需要10秒怎么办呢有两种办法
第一种是CPU等着也就是程序暂停执行后续代码等100M的数据在10秒后写入磁盘再接着往下执行这种模式称为同步IO
另一种方法是CPU不等待只是告诉磁盘“您老慢慢写不着急我接着干别的事去了”于是后续代码可以立刻接着执行这种模式称为异步IO。
同步和异步的区别就在于是否等待IO执行的结果。好比你去麦当劳点餐你说“来个汉堡”服务员告诉你对不起汉堡要现做需要等5分钟于是你站在收银台前面等了5分钟拿到汉堡再去逛商场这是同步IO。
你说“来个汉堡”服务员告诉你汉堡需要等5分钟你可以先去逛商场等做好了我们再通知你这样你可以立刻去干别的事情逛商场这是异步IO。
很明显使用异步IO来编写程序性能会远远高于同步IO但是异步IO的缺点是编程模型复杂。想想看你得知道什么时候通知你“汉堡做好了”而通知你的方法也各不相同。如果是服务员跑过来找到你这是回调模式如果服务员发短信通知你你就得不停地检查手机这是轮询模式。总之异步IO的复杂度远远高于同步IO。
操作IO的能力都是由操作系统提供的每一种编程语言都会把操作系统提供的低级C接口封装起来方便使用Python也不例外。我们后面会详细讨论Python的IO编程接口。
注意本章的IO编程都是同步模式异步IO由于复杂度太高后续涉及到服务器端程序开发时我们再讨论。
文件读写
读写文件是最常见的IO操作。Python内置了读写文件的函数用法和C是兼容的。
读写文件前我们先必须了解一下在磁盘上读写文件的功能都是由操作系统提供的现代操作系统不允许普通的程序直接操作磁盘所以读写文件就是请求操作系统打开一个文件对象通常称为文件描述符然后通过操作系统提供的接口从这个文件对象中读取数据读文件或者把数据写入这个文件对象写文件。
读文件
要以读文件的模式打开一个文件对象使用Python内置的open()函数传入文件名和标示符 f open(/Users/michael/test.txt, r)标示符r表示读这样我们就成功地打开了一个文件。
如果文件不存在open()函数就会抛出一个IOError的错误并且给出错误码和详细的信息告诉你文件不存在 fopen(/Users/michael/notfound.txt, r)
Traceback (most recent call last):File stdin, line 1, in module
FileNotFoundError: [Errno 2] No such file or directory: /Users/michael/notfound.txt如果文件打开成功接下来调用read()方法可以一次读取文件的全部内容Python把内容读到内存用一个str对象表示 f.read()
Hello, world!最后一步是调用close()方法关闭文件。文件使用完毕后必须关闭因为文件对象会占用操作系统的资源并且操作系统同一时间能打开的文件数量也是有限的 f.close()由于文件读写时都有可能产生IOError一旦出错后面的f.close()就不会调用。所以为了保证无论是否出错都能正确地关闭文件我们可以使用try ... finally来实现
try:f open(/path/to/file, r)print(f.read())
finally:if f:f.close()但是每次都这么写实在太繁琐所以Python引入了with语句来自动帮我们调用close()方法
with open(/path/to/file, r) as f:print(f.read())这和前面的try ... finally是一样的但是代码更佳简洁并且不必调用f.close()方法。
调用read()会一次性读取文件的全部内容如果文件有10G内存就爆了所以要保险起见可以反复调用read(size)方法每次最多读取size个字节的内容。另外调用readline()可以每次读取一行内容调用readlines()一次读取所有内容并按行返回list。因此要根据需要决定怎么调用。
如果文件很小read()一次性读取最方便如果不能确定文件大小反复调用read(size)比较保险如果是配置文件调用readlines()最方便
for line in f.readlines():print(line.strip()) # 把末尾的\n删掉file-like Object
像open()函数返回的这种有个read()方法的对象在Python中统称为file-like Object。除了file外还可以是内存的字节流网络流自定义流等等。file-like Object不要求从特定类继承只要写个read()方法就行。
StringIO就是在内存中创建的file-like Object常用作临时缓冲。
二进制文件
前面讲的默认都是读取文本文件并且是UTF-8编码的文本文件。要读取二进制文件比如图片、视频等等用rb模式打开文件即可 f open(/Users/michael/test.jpg, rb)f.read()
b\xff\xd8\xff\xe1\x00\x18Exif\x00\x00... # 十六进制表示的字节字符编码
要读取非UTF-8编码的文本文件需要给open()函数传入encoding参数例如读取GBK编码的文件 f open(/Users/michael/gbk.txt, r, encodinggbk)f.read()
测试遇到有些编码不规范的文件你可能会遇到UnicodeDecodeError因为在文本文件中可能夹杂了一些非法编码的字符。遇到这种情况open()函数还接收一个errors参数表示如果遇到编码错误后如何处理。最简单的方式是直接忽略 f open(/Users/michael/gbk.txt, r, encodinggbk, errorsignore)写文件
写文件和读文件是一样的唯一区别是调用open()函数时传入标识符w或者wb表示写文本文件或写二进制文件 f open(/Users/michael/test.txt, w)f.write(Hello, world!)f.close()你可以反复调用write()来写入文件但是务必要调用f.close()来关闭文件。当我们写文件时操作系统往往不会立刻把数据写入磁盘而是放到内存缓存起来空闲的时候再慢慢写入。只有调用close()方法时操作系统才保证把没有写入的数据全部写入磁盘。忘记调用close()的后果是数据可能只写了一部分到磁盘剩下的丢失了。所以还是用with语句来得保险
with open(/Users/michael/test.txt, w) as f:f.write(Hello, world!)要写入特定编码的文本文件请给open()函数传入encoding参数将字符串自动转换成指定编码。
小结
在Python中文件读写是通过open()函数打开的文件对象完成的。使用with语句操作文件IO是个好习惯。
参考源码
with_file.py
StringIO和BytesIO
StringIO
很多时候数据读写不一定是文件也可以在内存中读写。
StringIO顾名思义就是在内存中读写str。
要把str写入StringIO我们需要先创建一个StringIO然后像文件一样写入即可 from io import StringIOf StringIO()f.write(hello)
5f.write( )
1f.write(world!)
6print(f.getvalue())
hello world!getvalue()方法用于获得写入后的str。
要读取StringIO可以用一个str初始化StringIO然后像读文件一样读取 from io import StringIOf StringIO(Hello!\nHi!\nGoodbye!)while True:
... s f.readline()
... if s :
... break
... print(s.strip())
...
Hello!
Hi!
Goodbye!BytesIO
StringIO操作的只能是str如果要操作二进制数据就需要使用BytesIO。
BytesIO实现了在内存中读写bytes我们创建一个BytesIO然后写入一些bytes from io import BytesIOf BytesIO()f.write(中文.encode(utf-8))
6print(f.getvalue())
b\xe4\xb8\xad\xe6\x96\x87请注意写入的不是str而是经过UTF-8编码的bytes。
和StringIO类似可以用一个bytes初始化BytesIO然后像读文件一样读取 from io import StringIOf BytesIO(b\xe4\xb8\xad\xe6\x96\x87)f.read()
b\xe4\xb8\xad\xe6\x96\x87小结
StringIO和BytesIO是在内存中操作str和bytes的方法使得和读写文件具有一致的接口。
参考源码
do_stringio.py
do_bytesio.py
操作文件和目录
如果我们要操作文件、目录可以在命令行下面输入操作系统提供的各种命令来完成。比如dir、cp等命令。
如果要在Python程序中执行这些目录和文件的操作怎么办其实操作系统提供的命令只是简单地调用了操作系统提供的接口函数Python内置的os模块也可以直接调用操作系统提供的接口函数。
打开Python交互式命令行我们来看看如何使用os模块的基本功能 import osos.name # 操作系统类型
posix如果是posix说明系统是Linux、Unix或Mac OS X如果是nt就是Windows系统。
要获取详细的系统信息可以调用uname()函数 os.uname()
posix.uname_result(sysnameDarwin, nodenameMichaelMacPro.local, release14.3.0, versionDarwin Kernel Version 14.3.0: Mon Mar 23 11:59:05 PDT 2015; root:xnu-2782.20.48~5/RELEASE_X86_64, machinex86_64)注意uname()函数在Windows上不提供也就是说os模块的某些函数是跟操作系统相关的。
环境变量
在操作系统中定义的环境变量全部保存在os.environ这个变量中可以直接查看 os.environ
environ({VERSIONER_PYTHON_PREFER_32_BIT: no, TERM_PROGRAM_VERSION: 326, LOGNAME: michael, USER: michael, PATH: /usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin:/opt/X11/bin:/usr/local/mysql/bin, ...})要获取某个环境变量的值可以调用os.environ.get(key) os.environ.get(PATH)
/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin:/opt/X11/bin:/usr/local/mysql/binos.environ.get(x, default)
default操作文件和目录
操作文件和目录的函数一部分放在os模块中一部分放在os.path模块中这一点要注意一下。查看、创建和删除目录可以这么调用
# 查看当前目录的绝对路径:os.path.abspath(.)
/Users/michael
# 在某个目录下创建一个新目录首先把新目录的完整路径表示出来:os.path.join(/Users/michael, testdir)
/Users/michael/testdir
# 然后创建一个目录:os.mkdir(/Users/michael/testdir)
# 删掉一个目录:os.rmdir(/Users/michael/testdir)把两个路径合成一个时不要直接拼字符串而要通过os.path.join()函数这样可以正确处理不同操作系统的路径分隔符。在Linux/Unix/Mac下os.path.join()返回这样的字符串
part-1/part-2而Windows下会返回这样的字符串
part-1\part-2同样的道理要拆分路径时也不要直接去拆字符串而要通过os.path.split()函数这样可以把一个路径拆分为两部分后一部分总是最后级别的目录或文件名 os.path.split(/Users/michael/testdir/file.txt)
(/Users/michael/testdir, file.txt)os.path.splitext()可以直接让你得到文件扩展名很多时候非常方便 os.path.splitext(/path/to/file.txt)
(/path/to/file, .txt)这些合并、拆分路径的函数并不要求目录和文件要真实存在它们只对字符串进行操作。
文件操作使用下面的函数。假定当前目录下有一个test.txt文件
# 对文件重命名:os.rename(test.txt, test.py)
# 删掉文件:os.remove(test.py)但是复制文件的函数居然在os模块中不存在原因是复制文件并非由操作系统提供的系统调用。理论上讲我们通过上一节的读写文件可以完成文件复制只不过要多写很多代码。
幸运的是shutil模块提供了copyfile()的函数你还可以在shutil模块中找到很多实用函数它们可以看做是os模块的补充。
最后看看如何利用Python的特性来过滤文件。比如我们要列出当前目录下的所有目录只需要一行代码 [x for x in os.listdir(.) if os.path.isdir(x)]
[.lein, .local, .m2, .npm, .ssh, .Trash, .vim, Applications, Desktop, ...]要列出所有的.py文件也只需一行代码 [x for x in os.listdir(.) if os.path.isfile(x) and os.path.splitext(x)[1].py]
[apis.py, config.py, models.py, pymonitor.py, test_db.py, urls.py, wsgiapp.py]是不是非常简洁
小结
Python的os模块封装了操作系统的目录和文件操作要注意这些函数有的在os模块中有的在os.path模块中。
练习 利用os模块编写一个能实现dir -l输出的程序。 编写一个程序能在当前目录以及当前目录的所有子目录下查找文件名包含指定字符串的文件并打印出相对路径。
参考源码
do_dir
序列化
在程序运行的过程中所有的变量都是在内存中比如定义一个dict
d dict(nameBob, age20, score88)可以随时修改变量比如把name改成Bill但是一旦程序结束变量所占用的内存就被操作系统全部回收。如果没有把修改后的Bill存储到磁盘上下次重新运行程序变量又被初始化为Bob。
我们把变量从内存中变成可存储或传输的过程称之为序列化在Python中叫pickling在其他语言中也被称之为serializationmarshallingflattening等等都是一个意思。
序列化之后就可以把序列化后的内容写入磁盘或者通过网络传输到别的机器上。
反过来把变量内容从序列化的对象重新读到内存里称之为反序列化即unpickling。
Python提供了pickle模块来实现序列化。
首先我们尝试把一个对象序列化并写入文件 import pickled dict(nameBob, age20, score88)pickle.dumps(d)
b\x80\x03}q\x00(X\x03\x00\x00\x00ageq\x01K\x14X\x05\x00\x00\x00scoreq\x02KXX\x04\x00\x00\x00nameq\x03X\x03\x00\x00\x00Bobq\x04u.pickle.dumps()方法把任意对象序列化成一个bytes然后就可以把这个bytes写入文件。或者用另一个方法pickle.dump()直接把对象序列化后写入一个file-like Object f open(dump.txt, wb)pickle.dump(d, f)f.close()看看写入的dump.txt文件一堆乱七八糟的内容这些都是Python保存的对象内部信息。
当我们要把对象从磁盘读到内存时可以先把内容读到一个bytes然后用pickle.loads()方法反序列化出对象也可以直接用pickle.load()方法从一个file-like Object中直接反序列化出对象。我们打开另一个Python命令行来反序列化刚才保存的对象 f open(dump.txt, rb)d pickle.load(f)f.close()d
{age: 20, score: 88, name: Bob}变量的内容又回来了
当然这个变量和原来的变量是完全不相干的对象它们只是内容相同而已。
Pickle的问题和所有其他编程语言特有的序列化问题一样就是它只能用于Python并且可能不同版本的Python彼此都不兼容因此只能用Pickle保存那些不重要的数据不能成功地反序列化也没关系。
JSON
如果我们要在不同的编程语言之间传递对象就必须把对象序列化为标准格式比如XML但更好的方法是序列化为JSON因为JSON表示出来就是一个字符串可以被所有语言读取也可以方便地存储到磁盘或者通过网络传输。JSON不仅是标准格式并且比XML更快而且可以直接在Web页面中读取非常方便。
JSON表示的对象就是标准的JavaScript语言的对象JSON和Python内置的数据类型对应如下
JSON类型Python类型{}dict[]liststringstr1234.56int或floattrue/falseTrue/FalsenullNone
Python内置的json模块提供了非常完善的Python对象到JSON格式的转换。我们先看看如何把Python对象变成一个JSON import jsond dict(nameBob, age20, score88)json.dumps(d)
{age: 20, score: 88, name: Bob}dumps()方法返回一个str内容就是标准的JSON。类似的dump()方法可以直接把JSON写入一个file-like Object。
要把JSON反序列化为Python对象用loads()或者对应的load()方法前者把JSON的字符串反序列化后者从file-like Object中读取字符串并反序列化 json_str {age: 20, score: 88, name: Bob}json.loads(json_str)
{age: 20, score: 88, name: Bob}由于JSON标准规定JSON编码是UTF-8所以我们总是能正确地在Python的str与JSON的字符串之间转换。
JSON进阶
Python的dict对象可以直接序列化为JSON的{}不过很多时候我们更喜欢用class表示对象比如定义Student类然后序列化
import jsonclass Student(object):def __init__(self, name, age, score):self.name nameself.age ageself.score scores Student(Bob, 20, 88)
print(json.dumps(s))运行代码毫不留情地得到一个TypeError
Traceback (most recent call last):...
TypeError: __main__.Student object at 0x10603cc50 is not JSON serializable错误的原因是Student对象不是一个可序列化为JSON的对象。
如果连class的实例对象都无法序列化为JSON这肯定不合理
别急我们仔细看看dumps()方法的参数列表可以发现除了第一个必须的obj参数外dumps()方法还提供了一大堆的可选参数
https://docs.python.org/3/library/json.html#json.dumps
这些可选参数就是让我们来定制JSON序列化。前面的代码之所以无法把Student类实例序列化为JSON是因为默认情况下dumps()方法不知道如何将Student实例变为一个JSON的{}对象。
可选参数default就是把任意一个对象变成一个可序列为JSON的对象我们只需要为Student专门写一个转换函数再把函数传进去即可
def student2dict(std):return {name: std.name,age: std.age,score: std.score}这样Student实例首先被student2dict()函数转换成dict然后再被顺利序列化为JSON print(json.dumps(s, defaultstudent2dict))
{age: 20, name: Bob, score: 88}不过下次如果遇到一个Teacher类的实例照样无法序列化为JSON。我们可以偷个懒把任意class的实例变为dict
print(json.dumps(s, defaultlambda obj: obj.__dict__))因为通常class的实例都有一个__dict__属性它就是一个dict用来存储实例变量。也有少数例外比如定义了__slots__的class。
同样的道理如果我们要把JSON反序列化为一个Student对象实例loads()方法首先转换出一个dict对象然后我们传入的object_hook函数负责把dict转换为Student实例
def dict2student(d):return Student(d[name], d[age], d[score])运行结果如下 json_str {age: 20, score: 88, name: Bob}print(json.loads(json_str, object_hookdict2student))
__main__.Student object at 0x10cd3c190打印出的是反序列化的Student实例对象。
小结
Python语言特定的序列化模块是pickle但如果要把序列化搞得更通用、更符合Web标准就可以使用json模块。
json模块的dumps()和loads()函数是定义得非常好的接口的典范。当我们使用时只需要传入一个必须的参数。但是当默认的序列化或反序列机制不满足我们的要求时我们又可以传入更多的参数来定制序列化或反序列化的规则既做到了接口简单易用又做到了充分的扩展性和灵活性。
参考源码
use_pickle.py
use_json.py
进程和线程
很多同学都听说过现代操作系统比如Mac OS XUNIXLinuxWindows等都是支持“多任务”的操作系统。
什么叫“多任务”呢简单地说就是操作系统可以同时运行多个任务。打个比方你一边在用浏览器上网一边在听MP3一边在用Word赶作业这就是多任务至少同时有3个任务正在运行。还有很多任务悄悄地在后台同时运行着只是桌面上没有显示而已。
现在多核CPU已经非常普及了但是即使过去的单核CPU也可以执行多任务。由于CPU执行代码都是顺序执行的那么单核CPU是怎么执行多任务的呢
答案就是操作系统轮流让各个任务交替执行任务1执行0.01秒切换到任务2任务2执行0.01秒再切换到任务3执行0.01秒……这样反复执行下去。表面上看每个任务都是交替执行的但是由于CPU的执行速度实在是太快了我们感觉就像所有任务都在同时执行一样。
真正的并行执行多任务只能在多核CPU上实现但是由于任务数量远远多于CPU的核心数量所以操作系统也会自动把很多任务轮流调度到每个核心上执行。
对于操作系统来说一个任务就是一个进程Process比如打开一个浏览器就是启动一个浏览器进程打开一个记事本就启动了一个记事本进程打开两个记事本就启动了两个记事本进程打开一个Word就启动了一个Word进程。
有些进程还不止同时干一件事比如Word它可以同时进行打字、拼写检查、打印等事情。在一个进程内部要同时干多件事就需要同时运行多个“子任务”我们把进程内的这些“子任务”称为线程Thread。
由于每个进程至少要干一件事所以一个进程至少有一个线程。当然像Word这种复杂的进程可以有多个线程多个线程可以同时执行多线程的执行方式和多进程是一样的也是由操作系统在多个线程之间快速切换让每个线程都短暂地交替运行看起来就像同时执行一样。当然真正地同时执行多线程需要多核CPU才可能实现。
我们前面编写的所有的Python程序都是执行单任务的进程也就是只有一个线程。如果我们要同时执行多个任务怎么办
有两种解决方案
一种是启动多个进程每个进程虽然只有一个线程但多个进程可以一块执行多个任务。
还有一种方法是启动一个进程在一个进程内启动多个线程这样多个线程也可以一块执行多个任务。
当然还有第三种方法就是启动多个进程每个进程再启动多个线程这样同时执行的任务就更多了当然这种模型更复杂实际很少采用。
总结一下就是多任务的实现有3种方式
多进程模式多线程模式多进程多线程模式。
同时执行多个任务通常各个任务之间并不是没有关联的而是需要相互通信和协调有时任务1必须暂停等待任务2完成后才能继续执行有时任务3和任务4又不能同时执行所以多进程和多线程的程序的复杂度要远远高于我们前面写的单进程单线程的程序。
因为复杂度高调试困难所以不是迫不得已我们也不想编写多任务。但是有很多时候没有多任务还真不行。想想在电脑上看电影就必须由一个线程播放视频另一个线程播放音频否则单线程实现的话就只能先把视频播放完再播放音频或者先把音频播放完再播放视频这显然是不行的。
Python既支持多进程又支持多线程我们会讨论如何编写这两种多任务程序。
小结
线程是最小的执行单元而进程由至少一个线程组成。如何调度进程和线程完全由操作系统决定程序自己不能决定什么时候执行执行多长时间。
多进程和多线程的程序涉及到同步、数据共享的问题编写起来更复杂。
多进程
要让Python程序实现多进程multiprocessing我们先了解操作系统的相关知识。
Unix/Linux操作系统提供了一个fork()系统调用它非常特殊。普通的函数调用调用一次返回一次但是fork()调用一次返回两次因为操作系统自动把当前进程称为父进程复制了一份称为子进程然后分别在父进程和子进程内返回。
子进程永远返回0而父进程返回子进程的ID。这样做的理由是一个父进程可以fork出很多子进程所以父进程要记下每个子进程的ID而子进程只需要调用getppid()就可以拿到父进程的ID。
Python的os模块封装了常见的系统调用其中就包括fork可以在Python程序中轻松创建子进程
import osprint(Process (%s) start... % os.getpid())
# Only works on Unix/Linux/Mac:
pid os.fork()
if pid 0:print(I am child process (%s) and my parent is %s. % (os.getpid(), os.getppid()))
else:print(I (%s) just created a child process (%s). % (os.getpid(), pid))运行结果如下
Process (876) start...
I (876) just created a child process (877).
I am child process (877) and my parent is 876.由于Windows没有fork调用上面的代码在Windows上无法运行。由于Mac系统是基于BSDUnix的一种内核所以在Mac下运行是没有问题的推荐大家用Mac学Python
有了fork调用一个进程在接到新任务时就可以复制出一个子进程来处理新任务常见的Apache服务器就是由父进程监听端口每当有新的http请求时就fork出子进程来处理新的http请求。
multiprocessing
如果你打算编写多进程的服务程序Unix/Linux无疑是正确的选择。由于Windows没有fork调用难道在Windows上无法用Python编写多进程的程序
由于Python是跨平台的自然也应该提供一个跨平台的多进程支持。multiprocessing模块就是跨平台版本的多进程模块。
multiprocessing模块提供了一个Process类来代表一个进程对象下面的例子演示了启动一个子进程并等待其结束
from multiprocessing import Process
import os# 子进程要执行的代码
def run_proc(name):print(Run child process %s (%s)... % (name, os.getpid()))if __name____main__:print(Parent process %s. % os.getpid())p Process(targetrun_proc, args(test,))print(Child process will start.)p.start()p.join()print(Child process end.)执行结果如下
Parent process 928.
Process will start.
Run child process test (929)...
Process end.创建子进程时只需要传入一个执行函数和函数的参数创建一个Process实例用start()方法启动这样创建进程比fork()还要简单。
join()方法可以等待子进程结束后再继续往下运行通常用于进程间的同步。
Pool
如果要启动大量的子进程可以用进程池的方式批量创建子进程
from multiprocessing import Pool
import os, time, randomdef long_time_task(name):print(Run task %s (%s)... % (name, os.getpid()))start time.time()time.sleep(random.random() * 3)end time.time()print(Task %s runs %0.2f seconds. % (name, (end - start)))if __name____main__:print(Parent process %s. % os.getpid())p Pool(4)for i in range(5):p.apply_async(long_time_task, args(i,))print(Waiting for all subprocesses done...)p.close()p.join()print(All subprocesses done.)执行结果如下
Parent process 669.
Waiting for all subprocesses done...
Run task 0 (671)...
Run task 1 (672)...
Run task 2 (673)...
Run task 3 (674)...
Task 2 runs 0.14 seconds.
Run task 4 (673)...
Task 1 runs 0.27 seconds.
Task 3 runs 0.86 seconds.
Task 0 runs 1.41 seconds.
Task 4 runs 1.91 seconds.
All subprocesses done.代码解读
对Pool对象调用join()方法会等待所有子进程执行完毕调用join()之前必须先调用close()调用close()之后就不能继续添加新的Process了。
请注意输出的结果task 0123是立刻执行的而task 4要等待前面某个task完成后才执行这是因为Pool的默认大小在我的电脑上是4因此最多同时执行4个进程。这是Pool有意设计的限制并不是操作系统的限制。如果改成
p Pool(5)就可以同时跑5个进程。
由于Pool的默认大小是CPU的核数如果你不幸拥有8核CPU你要提交至少9个子进程才能看到上面的等待效果。
子进程
很多时候子进程并不是自身而是一个外部进程。我们创建了子进程后还需要控制子进程的输入和输出。
subprocess模块可以让我们非常方便地启动一个子进程然后控制其输入和输出。
下面的例子演示了如何在Python代码中运行命令nslookup www.python.org这和命令行直接运行的效果是一样的
import subprocessprint($ nslookup www.python.org)
r subprocess.call([nslookup, www.python.org])
print(Exit code:, r)运行结果
$ nslookup www.python.org
Server: 192.168.19.4
Address: 192.168.19.4#53Non-authoritative answer:
www.python.org canonical name python.map.fastly.net.
Name: python.map.fastly.net
Address: 199.27.79.223Exit code: 0如果子进程还需要输入则可以通过communicate()方法输入
import subprocessprint($ nslookup)
p subprocess.Popen([nslookup], stdinsubprocess.PIPE, stdoutsubprocess.PIPE, stderrsubprocess.PIPE)
output, err p.communicate(bset qmx\npython.org\nexit\n)
print(output.decode(utf-8))
print(Exit code:, p.returncode)上面的代码相当于在命令行执行命令nslookup然后手动输入
set qmx
python.org
exit运行结果如下
$ nslookup
Server: 192.168.19.4
Address: 192.168.19.4#53Non-authoritative answer:
python.org mail exchanger 50 mail.python.org.Authoritative answers can be found from:
mail.python.org internet address 82.94.164.166
mail.python.org has AAAA address 2001:888:2000:d::a6Exit code: 0进程间通信
Process之间肯定是需要通信的操作系统提供了很多机制来实现进程间的通信。Python的multiprocessing模块包装了底层的机制提供了Queue、Pipes等多种方式来交换数据。
我们以Queue为例在父进程中创建两个子进程一个往Queue里写数据一个从Queue里读数据
from multiprocessing import Process, Queue
import os, time, random# 写数据进程执行的代码:
def write(q):print(Process to write: %s % os.getpid())for value in [A, B, C]:print(Put %s to queue... % value)q.put(value)time.sleep(random.random())# 读数据进程执行的代码:
def read(q):print(Process to read: %s % os.getpid())while True:value q.get(True)print(Get %s from queue. % value)if __name____main__:# 父进程创建Queue并传给各个子进程q Queue()pw Process(targetwrite, args(q,))pr Process(targetread, args(q,))# 启动子进程pw写入:pw.start()# 启动子进程pr读取:pr.start()# 等待pw结束:pw.join()# pr进程里是死循环无法等待其结束只能强行终止:pr.terminate()运行结果如下
Process to write: 50563
Put A to queue...
Process to read: 50564
Get A from queue.
Put B to queue...
Get B from queue.
Put C to queue...
Get C from queue.在Unix/Linux下multiprocessing模块封装了fork()调用使我们不需要关注fork()的细节。由于Windows没有fork调用因此multiprocessing需要“模拟”出fork的效果父进程所有Python对象都必须通过pickle序列化再传到子进程去所有如果multiprocessing在Windows下调用失败了要先考虑是不是pickle失败了。
小结
在Unix/Linux下可以使用fork()调用实现多进程。
要实现跨平台的多进程可以使用multiprocessing模块。
进程间通信是通过Queue、Pipes等实现的。
参考源码
do_folk.py
multi_processing.py
pooled_processing.py
do_subprocess.py
do_queue.py
多线程
多任务可以由多进程完成也可以由一个进程内的多线程完成。
我们前面提到了进程是由若干线程组成的一个进程至少有一个线程。
由于线程是操作系统直接支持的执行单元因此高级语言通常都内置多线程的支持Python也不例外并且Python的线程是真正的Posix Thread而不是模拟出来的线程。
Python的标准库提供了两个模块_thread和threading_thread是低级模块threading是高级模块对_thread进行了封装。绝大多数情况下我们只需要使用threading这个高级模块。
启动一个线程就是把一个函数传入并创建Thread实例然后调用start()开始执行
import time, threading# 新线程执行的代码:
def loop():print(thread %s is running... % threading.current_thread().name)n 0while n 5:n n 1print(thread %s %s % (threading.current_thread().name, n))time.sleep(1)print(thread %s ended. % threading.current_thread().name)print(thread %s is running... % threading.current_thread().name)
t threading.Thread(targetloop, nameLoopThread)
t.start()
t.join()
print(thread %s ended. % threading.current_thread().name)执行结果如下
thread MainThread is running...
thread LoopThread is running...
thread LoopThread 1
thread LoopThread 2
thread LoopThread 3
thread LoopThread 4
thread LoopThread 5
thread LoopThread ended.
thread MainThread ended.由于任何进程默认就会启动一个线程我们把该线程称为主线程主线程又可以启动新的线程Python的threading模块有个current_thread()函数它永远返回当前线程的实例。主线程实例的名字叫MainThread子线程的名字在创建时指定我们用LoopThread命名子线程。名字仅仅在打印时用来显示完全没有其他意义如果不起名字Python就自动给线程命名为Thread-1Thread-2……
Lock
多线程和多进程最大的不同在于多进程中同一个变量各自有一份拷贝存在于每个进程中互不影响而多线程中所有变量都由所有线程共享所以任何一个变量都可以被任何一个线程修改因此线程之间共享数据最大的危险在于多个线程同时改一个变量把内容给改乱了。
来看看多个线程同时操作一个变量怎么把内容给改乱了
import time, threading# 假定这是你的银行存款:
balance 0def change_it(n):# 先存后取结果应该为0:global balancebalance balance nbalance balance - ndef run_thread(n):for i in range(100000):change_it(n)t1 threading.Thread(targetrun_thread, args(5,))
t2 threading.Thread(targetrun_thread, args(8,))
t1.start()
t2.start()
t1.join()
t2.join()
print(balance)我们定义了一个共享变量balance初始值为0并且启动两个线程先存后取理论上结果应该为0但是由于线程的调度是由操作系统决定的当t1、t2交替执行时只要循环次数足够多balance的结果就不一定是0了。
原因是因为高级语言的一条语句在CPU执行时是若干条语句即使一个简单的计算
balance balance n也分两步
计算balance n存入临时变量中将临时变量的值赋给balance。
也就是可以看成
x balance n
balance x由于x是局部变量两个线程各自都有自己的x当代码正常执行时
初始值 balance 0t1: x1 balance 5 # x1 0 5 5
t1: balance x1 # balance 5
t1: x1 balance - 5 # x1 5 - 5 0
t1: balance x1 # balance 0t2: x2 balance 8 # x2 0 8 8
t2: balance x2 # balance 8
t2: x2 balance - 8 # x2 8 - 8 0
t2: balance x2 # balance 0结果 balance 0但是t1和t2是交替运行的如果操作系统以下面的顺序执行t1、t2
初始值 balance 0t1: x1 balance 5 # x1 0 5 5t2: x2 balance 8 # x2 0 8 8
t2: balance x2 # balance 8t1: balance x1 # balance 5
t1: x1 balance - 5 # x1 5 - 5 0
t1: balance x1 # balance 0t2: x2 balance - 8 # x2 0 - 8 -8
t2: balance x2 # balance -8结果 balance -8究其原因是因为修改balance需要多条语句而执行这几条语句时线程可能中断从而导致多个线程把同一个对象的内容改乱了。
两个线程同时一存一取就可能导致余额不对你肯定不希望你的银行存款莫名其妙地变成了负数所以我们必须确保一个线程在修改balance的时候别的线程一定不能改。
如果我们要确保balance计算正确就要给change_it()上一把锁当某个线程开始执行change_it()时我们说该线程因为获得了锁因此其他线程不能同时执行change_it()只能等待直到锁被释放后获得该锁以后才能改。由于锁只有一个无论多少线程同一时刻最多只有一个线程持有该锁所以不会造成修改的冲突。创建一个锁就是通过threading.Lock()来实现
balance 0
lock threading.Lock()def run_thread(n):for i in range(100000):# 先要获取锁:lock.acquire()try:# 放心地改吧:change_it(n)finally:# 改完了一定要释放锁:lock.release()当多个线程同时执行lock.acquire()时只有一个线程能成功地获取锁然后继续执行代码其他线程就继续等待直到获得锁为止。
获得锁的线程用完后一定要释放锁否则那些苦苦等待锁的线程将永远等待下去成为死线程。所以我们用try...finally来确保锁一定会被释放。
锁的好处就是确保了某段关键代码只能由一个线程从头到尾完整地执行坏处当然也很多首先是阻止了多线程并发执行包含锁的某段代码实际上只能以单线程模式执行效率就大大地下降了。其次由于可以存在多个锁不同的线程持有不同的锁并试图获取对方持有的锁时可能会造成死锁导致多个线程全部挂起既不能执行也无法结束只能靠操作系统强制终止。
多核CPU
如果你不幸拥有一个多核CPU你肯定在想多核应该可以同时执行多个线程。
如果写一个死循环的话会出现什么情况呢
打开Mac OS X的Activity Monitor或者Windows的Task Manager都可以监控某个进程的CPU使用率。
我们可以监控到一个死循环线程会100%占用一个CPU。
如果有两个死循环线程在多核CPU中可以监控到会占用200%的CPU也就是占用两个CPU核心。
要想把N核CPU的核心全部跑满就必须启动N个死循环线程。
试试用Python写个死循环
import threading, multiprocessingdef loop():x 0while True:x x ^ 1for i in range(multiprocessing.cpu_count()):t threading.Thread(targetloop)t.start()启动与CPU核心数量相同的N个线程在4核CPU上可以监控到CPU占用率仅有102%也就是仅使用了一核。
但是用C、C或Java来改写相同的死循环直接可以把全部核心跑满4核就跑到400%8核就跑到800%为什么Python不行呢
因为Python的线程虽然是真正的线程但解释器执行代码时有一个GIL锁Global Interpreter Lock任何Python线程执行前必须先获得GIL锁然后每执行100条字节码解释器就自动释放GIL锁让别的线程有机会执行。这个GIL全局锁实际上把所有线程的执行代码都给上了锁所以多线程在Python中只能交替执行即使100个线程跑在100核CPU上也只能用到1个核。
GIL是Python解释器设计的历史遗留问题通常我们用的解释器是官方实现的CPython要真正利用多核除非重写一个不带GIL的解释器。
所以在Python中可以使用多线程但不要指望能有效利用多核。如果一定要通过多线程利用多核那只能通过C扩展来实现不过这样就失去了Python简单易用的特点。
不过也不用过于担心Python虽然不能利用多线程实现多核任务但可以通过多进程实现多核任务。多个Python进程有各自独立的GIL锁互不影响。
小结
多线程编程模型复杂容易发生冲突必须用锁加以隔离同时又要小心死锁的发生。
Python解释器由于设计时有GIL全局锁导致了多线程无法利用多核。多线程的并发在Python中就是一个美丽的梦。
参考源码
multi_threading.py
do_lock.py
ThreadLocal
在多线程环境下每个线程都有自己的数据。一个线程使用自己的局部变量比使用全局变量好因为局部变量只有线程自己能看见不会影响其他线程而全局变量的修改必须加锁。
但是局部变量也有问题就是在函数调用的时候传递起来很麻烦
def process_student(name):std Student(name)# std是局部变量但是每个函数都要用它因此必须传进去do_task_1(std)do_task_2(std)def do_task_1(std):do_subtask_1(std)do_subtask_2(std)def do_task_2(std):do_subtask_2(std)do_subtask_2(std)每个函数一层一层调用都这么传参数那还得了用全局变量也不行因为每个线程处理不同的Student对象不能共享。
如果用一个全局dict存放所有的Student对象然后以thread自身作为key获得线程对应的Student对象如何
global_dict {}def std_thread(name):std Student(name)# 把std放到全局变量global_dict中global_dict[threading.current_thread()] stddo_task_1()do_task_2()def do_task_1():# 不传入std而是根据当前线程查找std global_dict[threading.current_thread()]...def do_task_2():# 任何函数都可以查找出当前线程的std变量std global_dict[threading.current_thread()]...这种方式理论上是可行的它最大的优点是消除了std对象在每层函数中的传递问题但是每个函数获取std的代码有点丑。
有没有更简单的方式
ThreadLocal应运而生不用查找dictThreadLocal帮你自动做这件事
import threading# 创建全局ThreadLocal对象:
local_school threading.local()def process_student():# 获取当前线程关联的student:std local_school.studentprint(Hello, %s (in %s) % (std, threading.current_thread().name))def process_thread(name):# 绑定ThreadLocal的student:local_school.student nameprocess_student()t1 threading.Thread(target process_thread, args(Alice,), nameThread-A)
t2 threading.Thread(target process_thread, args(Bob,), nameThread-B)
t1.start()
t2.start()
t1.join()
t2.join()执行结果
Hello, Alice (in Thread-A)
Hello, Bob (in Thread-B)全局变量local_school就是一个ThreadLocal对象每个Thread对它都可以读写student属性但互不影响。你可以把local_school看成全局变量但每个属性如local_school.student都是线程的局部变量可以任意读写而互不干扰也不用管理锁的问题ThreadLocal内部会处理。
可以理解为全局变量local_school是一个dict不但可以用local_school.student还可以绑定其他变量如local_school.teacher等等。
ThreadLocal最常用的地方就是为每个线程绑定一个数据库连接HTTP请求用户身份信息等这样一个线程的所有调用到的处理函数都可以非常方便地访问这些资源。
小结
一个ThreadLocal变量虽然是全局变量但每个线程都只能读写自己线程的独立副本互不干扰。ThreadLocal解决了参数在一个线程中各个函数之间互相传递的问题。
参考源码
use_threadlocal.py
进程 vs. 线程
我们介绍了多进程和多线程这是实现多任务最常用的两种方式。现在我们来讨论一下这两种方式的优缺点。
首先要实现多任务通常我们会设计Master-Worker模式Master负责分配任务Worker负责执行任务因此多任务环境下通常是一个Master多个Worker。
如果用多进程实现Master-Worker主进程就是Master其他进程就是Worker。
如果用多线程实现Master-Worker主线程就是Master其他线程就是Worker。
多进程模式最大的优点就是稳定性高因为一个子进程崩溃了不会影响主进程和其他子进程。当然主进程挂了所有进程就全挂了但是Master进程只负责分配任务挂掉的概率低著名的Apache最早就是采用多进程模式。
多进程模式的缺点是创建进程的代价大在Unix/Linux系统下用fork调用还行在Windows下创建进程开销巨大。另外操作系统能同时运行的进程数也是有限的在内存和CPU的限制下如果有几千个进程同时运行操作系统连调度都会成问题。
多线程模式通常比多进程快一点但是也快不到哪去而且多线程模式致命的缺点就是任何一个线程挂掉都可能直接造成整个进程崩溃因为所有线程共享进程的内存。在Windows上如果一个线程执行的代码出了问题你经常可以看到这样的提示“该程序执行了非法操作即将关闭”其实往往是某个线程出了问题但是操作系统会强制结束整个进程。
在Windows下多线程的效率比多进程要高所以微软的IIS服务器默认采用多线程模式。由于多线程存在稳定性的问题IIS的稳定性就不如Apache。为了缓解这个问题IIS和Apache现在又有多进程多线程的混合模式真是把问题越搞越复杂。
线程切换
无论是多进程还是多线程只要数量一多效率肯定上不去为什么呢
我们打个比方假设你不幸正在准备中考每天晚上需要做语文、数学、英语、物理、化学这5科的作业每项作业耗时1小时。
如果你先花1小时做语文作业做完了再花1小时做数学作业这样依次全部做完一共花5小时这种方式称为单任务模型或者批处理任务模型。
假设你打算切换到多任务模型可以先做1分钟语文再切换到数学作业做1分钟再切换到英语以此类推只要切换速度足够快这种方式就和单核CPU执行多任务是一样的了以幼儿园小朋友的眼光来看你就正在同时写5科作业。
但是切换作业是有代价的比如从语文切到数学要先收拾桌子上的语文书本、钢笔这叫保存现场然后打开数学课本、找出圆规直尺这叫准备新环境才能开始做数学作业。操作系统在切换进程或者线程时也是一样的它需要先保存当前执行的现场环境CPU寄存器状态、内存页等然后把新任务的执行环境准备好恢复上次的寄存器状态切换内存页等才能开始执行。这个切换过程虽然很快但是也需要耗费时间。如果有几千个任务同时进行操作系统可能就主要忙着切换任务根本没有多少时间去执行任务了这种情况最常见的就是硬盘狂响点窗口无反应系统处于假死状态。
所以多任务一旦多到一个限度就会消耗掉系统所有的资源结果效率急剧下降所有任务都做不好。
计算密集型 vs. IO密集型
是否采用多任务的第二个考虑是任务的类型。我们可以把任务分为计算密集型和IO密集型。
计算密集型任务的特点是要进行大量的计算消耗CPU资源比如计算圆周率、对视频进行高清解码等等全靠CPU的运算能力。这种计算密集型任务虽然也可以用多任务完成但是任务越多花在任务切换的时间就越多CPU执行任务的效率就越低所以要最高效地利用CPU计算密集型任务同时进行的数量应当等于CPU的核心数。
计算密集型任务由于主要消耗CPU资源因此代码运行效率至关重要。Python这样的脚本语言运行效率很低完全不适合计算密集型任务。对于计算密集型任务最好用C语言编写。
第二种任务的类型是IO密集型涉及到网络、磁盘IO的任务都是IO密集型任务这类任务的特点是CPU消耗很少任务的大部分时间都在等待IO操作完成因为IO的速度远远低于CPU和内存的速度。对于IO密集型任务任务越多CPU效率越高但也有一个限度。常见的大部分任务都是IO密集型任务比如Web应用。
IO密集型任务执行期间99%的时间都花在IO上花在CPU上的时间很少因此用运行速度极快的C语言替换用Python这样运行速度极低的脚本语言完全无法提升运行效率。对于IO密集型任务最合适的语言就是开发效率最高代码量最少的语言脚本语言是首选C语言最差。
异步IO
考虑到CPU和IO之间巨大的速度差异一个任务在执行的过程中大部分时间都在等待IO操作单进程单线程模型会导致别的任务无法并行执行因此我们才需要多进程模型或者多线程模型来支持多任务并发执行。
现代操作系统对IO操作已经做了巨大的改进最大的特点就是支持异步IO。如果充分利用操作系统提供的异步IO支持就可以用单进程单线程模型来执行多任务这种全新的模型称为事件驱动模型Nginx就是支持异步IO的Web服务器它在单核CPU上采用单进程模型就可以高效地支持多任务。在多核CPU上可以运行多个进程数量与CPU核心数相同充分利用多核CPU。由于系统总的进程数量十分有限因此操作系统调度非常高效。用异步IO编程模型来实现多任务是一个主要的趋势。
对应到Python语言单进程的异步编程模型称为协程有了协程的支持就可以基于事件驱动编写高效的多任务程序。我们会在后面讨论如何编写协程。
分布式进程
在Thread和Process中应当优选Process因为Process更稳定而且Process可以分布到多台机器上而Thread最多只能分布到同一台机器的多个CPU上。
Python的multiprocessing模块不但支持多进程其中managers子模块还支持把多进程分布到多台机器上。一个服务进程可以作为调度者将任务分布到其他多个进程中依靠网络通信。由于managers模块封装很好不必了解网络通信的细节就可以很容易地编写分布式多进程程序。
举个例子如果我们已经有一个通过Queue通信的多进程程序在同一台机器上运行现在由于处理任务的进程任务繁重希望把发送任务的进程和处理任务的进程分布到两台机器上。怎么用分布式进程实现
原有的Queue可以继续使用但是通过managers模块把Queue通过网络暴露出去就可以让其他机器的进程访问Queue了。
我们先看服务进程服务进程负责启动Queue把Queue注册到网络上然后往Queue里面写入任务
# task_master.pyimport random, time, queue
from multiprocessing.managers import BaseManager# 发送任务的队列:
task_queue queue.Queue()
# 接收结果的队列:
result_queue queue.Queue()# 从BaseManager继承的QueueManager:
class QueueManager(BaseManager):pass# 把两个Queue都注册到网络上, callable参数关联了Queue对象:
QueueManager.register(get_task_queue, callablelambda: task_queue)
QueueManager.register(get_result_queue, callablelambda: result_queue)
# 绑定端口5000, 设置验证码abc:
manager QueueManager(address(, 5000), authkeybabc)
# 启动Queue:
manager.start()
# 获得通过网络访问的Queue对象:
task manager.get_task_queue()
result manager.get_result_queue()
# 放几个任务进去:
for i in range(10):n random.randint(0, 10000)print(Put task %d... % n)task.put(n)
# 从result队列读取结果:
print(Try get results...)
for i in range(10):r result.get(timeout10)print(Result: %s % r)
# 关闭:
manager.shutdown()
print(master exit.)请注意当我们在一台机器上写多进程程序时创建的Queue可以直接拿来用但是在分布式多进程环境下添加任务到Queue不可以直接对原始的task_queue进行操作那样就绕过了QueueManager的封装必须通过manager.get_task_queue()获得的Queue接口添加。
然后在另一台机器上启动任务进程本机上启动也可以
# task_worker.pyimport time, sys, queue
from multiprocessing.managers import BaseManager# 创建类似的QueueManager:
class QueueManager(BaseManager):pass# 由于这个QueueManager只从网络上获取Queue所以注册时只提供名字:
QueueManager.register(get_task_queue)
QueueManager.register(get_result_queue)# 连接到服务器也就是运行task_master.py的机器:
server_addr 127.0.0.1
print(Connect to server %s... % server_addr)
# 端口和验证码注意保持与task_master.py设置的完全一致:
m QueueManager(address(server_addr, 5000), authkeybabc)
# 从网络连接:
m.connect()
# 获取Queue的对象:
task m.get_task_queue()
result m.get_result_queue()
# 从task队列取任务,并把结果写入result队列:
for i in range(10):try:n task.get(timeout1)print(run task %d * %d... % (n, n))r %d * %d %d % (n, n, n*n)time.sleep(1)result.put(r)except Queue.Empty:print(task queue is empty.)
# 处理结束:
print(worker exit.)任务进程要通过网络连接到服务进程所以要指定服务进程的IP。
现在可以试试分布式进程的工作效果了。先启动task_master.py服务进程
$ python3 task_master.py
Put task 3411...
Put task 1605...
Put task 1398...
Put task 4729...
Put task 5300...
Put task 7471...
Put task 68...
Put task 4219...
Put task 339...
Put task 7866...
Try get results...task_master.py进程发送完任务后开始等待result队列的结果。现在启动task_worker.py进程
$ python3 task_worker.py
Connect to server 127.0.0.1...
run task 3411 * 3411...
run task 1605 * 1605...
run task 1398 * 1398...
run task 4729 * 4729...
run task 5300 * 5300...
run task 7471 * 7471...
run task 68 * 68...
run task 4219 * 4219...
run task 339 * 339...
run task 7866 * 7866...
worker exit.task_worker.py进程结束在task_master.py进程中会继续打印出结果
Result: 3411 * 3411 11634921
Result: 1605 * 1605 2576025
Result: 1398 * 1398 1954404
Result: 4729 * 4729 22363441
Result: 5300 * 5300 28090000
Result: 7471 * 7471 55815841
Result: 68 * 68 4624
Result: 4219 * 4219 17799961
Result: 339 * 339 114921
Result: 7866 * 7866 61873956这个简单的Master/Worker模型有什么用其实这就是一个简单但真正的分布式计算把代码稍加改造启动多个worker就可以把任务分布到几台甚至几十台机器上比如把计算n*n的代码换成发送邮件就实现了邮件队列的异步发送。
Queue对象存储在哪注意到task_worker.py中根本没有创建Queue的代码所以Queue对象存储在task_master.py进程中 而Queue之所以能通过网络访问就是通过QueueManager实现的。由于QueueManager管理的不止一个Queue所以要给每个Queue的网络调用接口起个名字比如get_task_queue。
authkey有什么用这是为了保证两台机器正常通信不被其他机器恶意干扰。如果task_worker.py的authkey和task_master.py的authkey不一致肯定连接不上。
小结
Python的分布式进程接口简单封装良好适合需要把繁重任务分布到多台机器的环境下。
注意Queue的作用是用来传递任务和接收结果每个任务的描述数据量要尽量小。比如发送一个处理日志文件的任务就不要发送几百兆的日志文件本身而是发送日志文件存放的完整路径由Worker进程再去共享的磁盘上读取文件。
参考源码
task_master.py
task_worker.py
正则表达式
字符串是编程时涉及到的最多的一种数据结构对字符串进行操作的需求几乎无处不在。比如判断一个字符串是否是合法的Email地址虽然可以编程提取前后的子串再分别判断是否是单词和域名但这样做不但麻烦而且代码难以复用。
正则表达式是一种用来匹配字符串的强有力的武器。它的设计思想是用一种描述性的语言来给字符串定义一个规则凡是符合规则的字符串我们就认为它“匹配”了否则该字符串就是不合法的。
所以我们判断一个字符串是否是合法的Email的方法是 创建一个匹配Email的正则表达式 用该正则表达式去匹配用户的输入来判断是否合法。
因为正则表达式也是用字符串表示的所以我们要首先了解如何用字符来描述字符。
在正则表达式中如果直接给出字符就是精确匹配。用\d可以匹配一个数字\w可以匹配一个字母或数字所以 00\d可以匹配007但无法匹配00A \d\d\d可以匹配010 \w\w\d可以匹配py3
.可以匹配任意字符所以
py.可以匹配pyc、pyo、py!等等。
要匹配变长的字符在正则表达式中用*表示任意个字符包括0个用表示至少一个字符用?表示0个或1个字符用{n}表示n个字符用{n,m}表示n-m个字符
来看一个复杂的例子\d{3}\s\d{3,8}。
我们来从左到右解读一下 \d{3}表示匹配3个数字例如010 \s可以匹配一个空格也包括Tab等空白符所以\s表示至少有一个空格例如匹配 等 \d{3,8}表示3-8个数字例如1234567。
综合起来上面的正则表达式可以匹配以任意个空格隔开的带区号的电话号码。
如果要匹配010-12345这样的号码呢由于-是特殊字符在正则表达式中要用\转义所以上面的正则是\d{3}\-\d{3,8}。
但是仍然无法匹配010 - 12345因为带有空格。所以我们需要更复杂的匹配方式。
进阶
要做更精确地匹配可以用[]表示范围比如 [0-9a-zA-Z\_]可以匹配一个数字、字母或者下划线 [0-9a-zA-Z\_]可以匹配至少由一个数字、字母或者下划线组成的字符串比如a1000_ZPy3000等等 [a-zA-Z\_][0-9a-zA-Z\_]*可以匹配由字母或下划线开头后接任意个由一个数字、字母或者下划线组成的字符串也就是Python合法的变量 [a-zA-Z\_][0-9a-zA-Z\_]{0, 19}更精确地限制了变量的长度是1-20个字符前面1个字符后面最多19个字符。
A|B可以匹配A或B所以[P|p]ython可以匹配Python或者python。
^表示行的开头^\d表示必须以数字开头。
$表示行的结束\d$表示必须以数字结束。
你可能注意到了py也可以匹配python但是加上^py$就变成了整行匹配就只能匹配py了。
re模块
有了准备知识我们就可以在Python中使用正则表达式了。Python提供re模块包含所有正则表达式的功能。由于Python的字符串本身也用\转义所以要特别注意
s ABC\\-001 # Python的字符串
# 对应的正则表达式字符串变成
# ABC\-001因此我们强烈建议使用Python的r前缀就不用考虑转义的问题了
s rABC\-001 # Python的字符串
# 对应的正则表达式字符串不变
# ABC\-001先看看如何判断正则表达式是否匹配 import rere.match(r^\d{3}\-\d{3,8}$, 010-12345)
_sre.SRE_Match object; span(0, 9), match010-12345re.match(r^\d{3}\-\d{3,8}$, 010 12345)match()方法判断是否匹配如果匹配成功返回一个Match对象否则返回None。常见的判断方法就是
test 用户输入的字符串
if re.match(r正则表达式, test):print(ok)
else:print(failed)切分字符串
用正则表达式切分字符串比用固定的字符更灵活请看正常的切分代码 a b c.split( )
[a, b, , , c]嗯无法识别连续的空格用正则表达式试试 re.split(r\s, a b c)
[a, b, c]无论多少个空格都可以正常分割。加入,试试 re.split(r[\s\,], a,b, c d)
[a, b, c, d]再加入;试试 re.split(r[\s\,\;], a,b;; c d)
[a, b, c, d]如果用户输入了一组标签下次记得用正则表达式来把不规范的输入转化成正确的数组。
分组
除了简单地判断是否匹配之外正则表达式还有提取子串的强大功能。用()表示的就是要提取的分组Group。比如
^(\d{3})-(\d{3,8})$分别定义了两个组可以直接从匹配的字符串中提取出区号和本地号码 m re.match(r^(\d{3})-(\d{3,8})$, 010-12345)m
_sre.SRE_Match object; span(0, 9), match010-12345m.group(0)
010-12345m.group(1)
010m.group(2)
12345如果正则表达式中定义了组就可以在Match对象上用group()方法提取出子串来。
注意到group(0)永远是原始字符串group(1)、group(2)……表示第1、2、……个子串。
提取子串非常有用。来看一个更凶残的例子 t 19:05:30m re.match(r^(0[0-9]|1[0-9]|2[0-3]|[0-9])\:(0[0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9]|[0-9])\:(0[0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9]|[0-9])$, t)m.groups()
(19, 05, 30)这个正则表达式可以直接识别合法的时间。但是有些时候用正则表达式也无法做到完全验证比如识别日期
^(0[1-9]|1[0-2]|[0-9])-(0[1-9]|1[0-9]|2[0-9]|3[0-1]|[0-9])$对于2-304-31这样的非法日期用正则还是识别不了或者说写出来非常困难这时就需要程序配合识别了。
贪婪匹配
最后需要特别指出的是正则匹配默认是贪婪匹配也就是匹配尽可能多的字符。举例如下匹配出数字后面的0 re.match(r^(\d)(0*)$, 102300).groups()
(102300, )由于\d采用贪婪匹配直接把后面的0全部匹配了结果0*只能匹配空字符串了。
必须让\d采用非贪婪匹配也就是尽可能少匹配才能把后面的0匹配出来加个?就可以让\d采用非贪婪匹配 re.match(r^(\d?)(0*)$, 102300).groups()
(1023, 00)编译
当我们在Python中使用正则表达式时re模块内部会干两件事情 编译正则表达式如果正则表达式的字符串本身不合法会报错 用编译后的正则表达式去匹配字符串。
如果一个正则表达式要重复使用几千次出于效率的考虑我们可以预编译该正则表达式接下来重复使用时就不需要编译这个步骤了直接匹配 import re
# 编译:re_telephone re.compile(r^(\d{3})-(\d{3,8})$)
# 使用re_telephone.match(010-12345).groups()
(010, 12345)re_telephone.match(010-8086).groups()
(010, 8086)编译后生成Regular Expression对象由于该对象自己包含了正则表达式所以调用对应的方法时不用给出正则字符串。
小结
正则表达式非常强大要在短短的一节里讲完是不可能的。要讲清楚正则的所有内容可以写一本厚厚的书了。如果你经常遇到正则表达式的问题你可能需要一本正则表达式的参考书。
练习
请尝试写一个验证Email地址的正则表达式。版本一应该可以验证出类似的Email
someonegmail.com
bill.gatesmicrosoft.com版本二可以验证并提取出带名字的Email地址
Tom Paris tomvoyager.org参考源码
regex.py
常用内建模块
Python之所以自称“batteries included”就是因为内置了许多非常有用的模块无需额外安装和配置即可直接使用。
本章将介绍一些常用的内建模块。
datetime
datetime是Python处理日期和时间的标准库。
获取当前日期和时间
我们先看如何获取当前日期和时间 from datetime import datetimenow datetime.now() # 获取当前datetimeprint(now)
2015-05-18 16:28:07.198690print(type(now))
class datetime.datetime注意到datetime是模块datetime模块还包含一个datetime类通过from datetime import datetime导入的才是datetime这个类。
如果仅导入import datetime则必须引用全名datetime.datetime。
datetime.now()返回当前日期和时间其类型是datetime。
获取指定日期和时间
要指定某个日期和时间我们直接用参数构造一个datetime from datetime import datetimedt datetime(2015, 4, 19, 12, 20) # 用指定日期时间创建datetimeprint(dt)
2015-04-19 12:20:00datetime转换为timestamp
在计算机中时间实际上是用数字表示的。我们把1970年1月1日 00:00:00 UTC00:00时区的时刻称为epoch time记为01970年以前的时间timestamp为负数当前时间就是相对于epoch time的秒数称为timestamp。
你可以认为
timestamp 0 1970-1-1 00:00:00 UTC0:00对应的北京时间是
timestamp 0 1970-1-1 08:00:00 UTC8:00可见timestamp的值与时区毫无关系因为timestamp一旦确定其UTC时间就确定了转换到任意时区的时间也是完全确定的这就是为什么计算机存储的当前时间是以timestamp表示的因为全球各地的计算机在任意时刻的timestamp都是完全相同的假定时间已校准。
把一个datetime类型转换为timestamp只需要简单调用timestamp()方法 from datetime import datetimedt datetime(2015, 4, 19, 12, 20) # 用指定日期时间创建datetimedt.timestamp() # 把timestamp转换为datetime
1429417200.0注意Python的timestamp是一个浮点数。如果有小数位小数位表示毫秒数。
某些编程语言如Java和JavaScript的timestamp使用整数表示毫秒数这种情况下只需要把timestamp除以1000就得到Python的浮点表示方法。
timestamp转换为datetime
要把timestamp转换为datetime使用datetime提供的fromtimestamp()方法 from datetime import datetimet 1429417200.0print(datetime.fromtimestamp(t))
2015-04-19 12:20:00注意到timestamp是一个浮点数它没有时区的概念而datetime是有时区的。上述转换是在timestamp和本地时间做转换。
本地时间是指当前操作系统设定的时区。例如北京时区是东8区则本地时间
2015-04-19 12:20:00实际上就是UTC8:00时区的时间
2015-04-19 12:20:00 UTC8:00而此刻的格林威治标准时间与北京时间差了8小时也就是UTC0:00时区的时间应该是
2015-04-19 04:20:00 UTC0:00timestamp也可以直接被转换到UTC标准时区的时间 from datetime import datetimet 1429417200.0print(datetime.fromtimestamp(t)) # 本地时间
2015-04-19 12:20:00print(datetime.utcfromtimestamp(t)) # UTC时间
2015-04-19 04:20:00str转换为datetime
很多时候用户输入的日期和时间是字符串要处理日期和时间首先必须把str转换为datetime。转换方法是通过datetime.strptime()实现需要一个日期和时间的格式化字符串 from datetime import datetimecday datetime.strptime(2015-6-1 18:19:59, %Y-%m-%d %H:%M:%S)print(cday)
2015-06-01 18:19:59字符串%Y-%m-%d %H:%M:%S规定了日期和时间部分的格式。详细的说明请参考Python文档。
注意转换后的datetime是没有时区信息的。
datetime转换为str
如果已经有了datetime对象要把它格式化为字符串显示给用户就需要转换为str转换方法是通过strftime()实现的同样需要一个日期和时间的格式化字符串 from datetime import datetimenow datetime.now()print(now.strftime(%a, %b %d %H:%M))
Mon, May 05 16:28datetime加减
对日期和时间进行加减实际上就是把datetime往后或往前计算得到新的datetime。加减可以直接用和-运算符不过需要导入timedelta这个类 from datetime import datetime, timedeltanow datetime.now()now
datetime.datetime(2015, 5, 18, 16, 57, 3, 540997)now timedelta(hours10)
datetime.datetime(2015, 5, 19, 2, 57, 3, 540997)now - timedelta(days1)
datetime.datetime(2015, 5, 17, 16, 57, 3, 540997)now timedelta(days2, hours12)
datetime.datetime(2015, 5, 21, 4, 57, 3, 540997)可见使用timedelta你可以很容易地算出前几天和后几天的时刻。
本地时间转换为UTC时间
本地时间是指系统设定时区的时间例如北京时间是UTC8:00时区的时间而UTC时间指UTC0:00时区的时间。
一个datetime类型有一个时区属性tzinfo但是默认为None所以无法区分这个datetime到底是哪个时区除非强行给datetime设置一个时区 from datetime import datetime, timedelta, timezonetz_utc_8 timezone(timedelta(hours8)) # 创建时区UTC8:00now datetime.now()now
datetime.datetime(2015, 5, 18, 17, 2, 10, 871012)dt now.replace(tzinfotz_utc_8) # 强制设置为UTC8:00dt
datetime.datetime(2015, 5, 18, 17, 2, 10, 871012, tzinfodatetime.timezone(datetime.timedelta(0, 28800)))如果系统时区恰好是UTC8:00那么上述代码就是正确的否则不能强制设置为UTC8:00时区。
时区转换
我们可以先通过utcnow()拿到当前的UTC时间再转换为任意时区的时间
# 拿到UTC时间并强制设置时区为UTC0:00:utc_dt datetime.utcnow().replace(tzinfotimezone.utc)print(utc_dt)
2015-05-18 09:05:12.37731600:00
# astimezone()将转换时区为北京时间:bj_dt utc_dt.astimezone(timezone(timedelta(hours8)))print(bj_dt)
2015-05-18 17:05:12.37731608:00
# astimezone()将转换时区为东京时间:tokyo_dt utc_dt.astimezone(timezone(timedelta(hours9)))print(tokyo_dt)
2015-05-18 18:05:12.37731609:00
# astimezone()将bj_dt转换时区为东京时间:tokyo_dt2 bj_dt.astimezone(timezone(timedelta(hours9)))print(tokyo_dt2)
2015-05-18 18:05:12.37731609:00时区转换的关键在于拿到一个datetime时要获知其正确的时区然后强制设置时区作为基准时间。
利用带时区的datetime通过astimezone()方法可以转换到任意时区。
注不是必须从UTC0:00时区转换到其他时区任何带时区的datetime都可以正确转换例如上述bj_dt到tokyo_dt的转换。
小结
datetime表示的时间需要时区信息才能确定一个特定的时间否则只能视为本地时间。
如果要存储datetime最佳方法是将其转换为timestamp再存储因为timestamp的值与时区完全无关。
练习
假设你获取了用户输入的日期和时间如2015-1-21 9:01:30以及一个时区信息如UTC5:00均是str请编写一个函数将其转换为timestamp
# -*- coding:utf-8 -*-import re
from datetime import datetime, timezone, timedeltadef to_timestamp(dt_str, tz_str):
----pass
----
# 测试:t1 to_timestamp(2015-6-1 08:10:30, UTC7:00)
assert t1 1433121030.0, t1t2 to_timestamp(2015-5-31 16:10:30, UTC-09:00)
assert t2 1433121030.0, t2print(Pass)参考源码
use_datetime.py
collections
collections是Python内建的一个集合模块提供了许多有用的集合类。
namedtuple
我们知道tuple可以表示不变集合例如一个点的二维坐标就可以表示成 p (1, 2)但是看到(1, 2)很难看出这个tuple是用来表示一个坐标的。
定义一个class又小题大做了这时namedtuple就派上了用场 from collections import namedtuplePoint namedtuple(Point, [x, y])p Point(1, 2)p.x
1p.y
2namedtuple是一个函数它用来创建一个自定义的tuple对象并且规定了tuple元素的个数并可以用属性而不是索引来引用tuple的某个元素。
这样一来我们用namedtuple可以很方便地定义一种数据类型它具备tuple的不变性又可以根据属性来引用使用十分方便。
可以验证创建的Point对象是tuple的一种子类 isinstance(p, Point)
Trueisinstance(p, tuple)
True类似的如果要用坐标和半径表示一个圆也可以用namedtuple定义
# namedtuple(名称, [属性list]):
Circle namedtuple(Circle, [x, y, r])deque
使用list存储数据时按索引访问元素很快但是插入和删除元素就很慢了因为list是线性存储数据量大的时候插入和删除效率很低。
deque是为了高效实现插入和删除操作的双向列表适合用于队列和栈 from collections import dequeq deque([a, b, c])q.append(x)q.appendleft(y)q
deque([y, a, b, c, x])deque除了实现list的append()和pop()外还支持appendleft()和popleft()这样就可以非常高效地往头部添加或删除元素。
defaultdict
使用dict时如果引用的Key不存在就会抛出KeyError。如果希望key不存在时返回一个默认值就可以用defaultdict from collections import defaultdictdd defaultdict(lambda: N/A)dd[key1] abcdd[key1] # key1存在
abcdd[key2] # key2不存在返回默认值
N/A注意默认值是调用函数返回的而函数在创建defaultdict对象时传入。
除了在Key不存在时返回默认值defaultdict的其他行为跟dict是完全一样的。
OrderedDict
使用dict时Key是无序的。在对dict做迭代时我们无法确定Key的顺序。
如果要保持Key的顺序可以用OrderedDict from collections import OrderedDictd dict([(a, 1), (b, 2), (c, 3)])d # dict的Key是无序的
{a: 1, c: 3, b: 2}od OrderedDict([(a, 1), (b, 2), (c, 3)])od # OrderedDict的Key是有序的
OrderedDict([(a, 1), (b, 2), (c, 3)])注意OrderedDict的Key会按照插入的顺序排列不是Key本身排序 od OrderedDict()od[z] 1od[y] 2od[x] 3list(od.keys()) # 按照插入的Key的顺序返回
[z, y, x]OrderedDict可以实现一个FIFO先进先出的dict当容量超出限制时先删除最早添加的Key
from collections import OrderedDictclass LastUpdatedOrderedDict(OrderedDict):def __init__(self, capacity):super(LastUpdatedOrderedDict, self).__init__()self._capacity capacitydef __setitem__(self, key, value):containsKey 1 if key in self else 0if len(self) - containsKey self._capacity:last self.popitem(lastFalse)print(remove:, last)if containsKey:del self[key]print(set:, (key, value))else:print(add:, (key, value))OrderedDict.__setitem__(self, key, value)Counter
Counter是一个简单的计数器例如统计字符出现的个数 from collections import Counterc Counter()for ch in programming:
... c[ch] c[ch] 1
...c
Counter({g: 2, m: 2, r: 2, a: 1, i: 1, o: 1, n: 1, p: 1})Counter实际上也是dict的一个子类上面的结果可以看出字符g、m、r各出现了两次其他字符各出现了一次。
小结
collections模块提供了一些有用的集合类可以根据需要选用。
参考源码
use_collections.py
base64
Base64是一种用64个字符来表示任意二进制数据的方法。
用记事本打开exe、jpg、pdf这些文件时我们都会看到一大堆乱码因为二进制文件包含很多无法显示和打印的字符所以如果要让记事本这样的文本处理软件能处理二进制数据就需要一个二进制到字符串的转换方法。Base64是一种最常见的二进制编码方法。
Base64的原理很简单首先准备一个包含64个字符的数组
[A, B, C, ... a, b, c, ... 0, 1, ... , /]然后对二进制数据进行处理每3个字节一组一共是3x824bit划为4组每组正好6个bit 这样我们得到4个数字作为索引然后查表获得相应的4个字符就是编码后的字符串。
所以Base64编码会把3字节的二进制数据编码为4字节的文本数据长度增加33%好处是编码后的文本数据可以在邮件正文、网页等直接显示。
如果要编码的二进制数据不是3的倍数最后会剩下1个或2个字节怎么办Base64用\x00字节在末尾补足后再在编码的末尾加上1个或2个号表示补了多少字节解码的时候会自动去掉。
Python内置的base64可以直接进行base64的编解码 import base64base64.b64encode(bbinary\x00string)
bYmluYXJ5AHN0cmluZwbase64.b64decode(bYmluYXJ5AHN0cmluZw)
bbinary\x00string由于标准的Base64编码后可能出现字符和/在URL中就不能直接作为参数所以又有一种url safe的base64编码其实就是把字符和/分别变成-和_ base64.b64encode(bi\xb7\x1d\xfb\xef\xff)
babcd//base64.urlsafe_b64encode(bi\xb7\x1d\xfb\xef\xff)
babcd--__base64.urlsafe_b64decode(abcd--__)
bi\xb7\x1d\xfb\xef\xff还可以自己定义64个字符的排列顺序这样就可以自定义Base64编码不过通常情况下完全没有必要。
Base64是一种通过查表的编码方法不能用于加密即使使用自定义的编码表也不行。
Base64适用于小段内容的编码比如数字证书签名、Cookie的内容等。
由于字符也可能出现在Base64编码中但用在URL、Cookie里面会造成歧义所以很多Base64编码后会把去掉
# 标准Base64:
abcd - YWJjZA
# 自动去掉:
abcd - YWJjZA去掉后怎么解码呢因为Base64是把3个字节变为4个字节所以Base64编码的长度永远是4的倍数因此需要加上把Base64字符串的长度变为4的倍数就可以正常解码了。
小结
Base64是一种任意二进制到文本字符串的编码方法常用于在URL、Cookie、网页中传输少量二进制数据。
练习
请写一个能处理去掉的base64解码函数
# -*- coding: utf-8 -*-import base64def safe_base64_decode(s):
----pass
----
# 测试:
assert babcd safe_base64_decode(bYWJjZA), safe_base64_decode(YWJjZA)
assert babcd safe_base64_decode(bYWJjZA), safe_base64_decode(YWJjZA)
print(Pass)参考源码
do_base64.py
struct
准确地讲Python没有专门处理字节的数据类型。但由于str既是字符串又可以表示字节所以字节数组str。而在C语言中我们可以很方便地用struct、union来处理字节以及字节和intfloat的转换。
在Python中比方说要把一个32位无符号整数变成字节也就是4个长度的bytes你得配合位运算符这么写 n 10240099b1 (n 0xff000000) 24b2 (n 0xff0000) 16b3 (n 0xff00) 8b4 n 0xffbs bytes([b1, b2, b3, b4])bs
b\x00\x9cc非常麻烦。如果换成浮点数就无能为力了。
好在Python提供了一个struct模块来解决bytes和其他二进制数据类型的转换。
struct的pack函数把任意数据类型变成bytes import structstruct.pack(I, 10240099)
b\x00\x9ccpack的第一个参数是处理指令I的意思是
表示字节顺序是big-endian也就是网络序I表示4字节无符号整数。
后面的参数个数要和处理指令一致。
unpack把bytes变成相应的数据类型 struct.unpack(IH, b\xf0\xf0\xf0\xf0\x80\x80)
(4042322160, 32896)根据IH的说明后面的bytes依次变为I4字节无符号整数和H2字节无符号整数。
所以尽管Python不适合编写底层操作字节流的代码但在对性能要求不高的地方利用struct就方便多了。
struct模块定义的数据类型可以参考Python官方文档
https://docs.python.org/3/library/struct.html#format-characters
Windows的位图文件.bmp是一种非常简单的文件格式我们来用struct分析一下。
首先找一个bmp文件没有的话用“画图”画一个。
读入前30个字节来分析 s b\x42\x4d\x38\x8c\x0a\x00\x00\x00\x00\x00\x36\x00\x00\x00\x28\x00\x00\x00\x80\x02\x00\x00\x68\x01\x00\x00\x01\x00\x18\x00BMP格式采用小端方式存储数据文件头的结构按顺序如下
两个字节BM表示Windows位图BA表示OS/2位图 一个4字节整数表示位图大小 一个4字节整数保留位始终为0 一个4字节整数实际图像的偏移量 一个4字节整数Header的字节数 一个4字节整数图像宽度 一个4字节整数图像高度 一个2字节整数始终为1 一个2字节整数颜色数。
所以组合起来用unpack读取 struct.unpack(ccIIIIIIHH, s)
(bB, bM, 691256, 0, 54, 40, 640, 360, 1, 24)结果显示bB、bM说明是Windows位图位图大小为640x360颜色数为24。
请编写一个bmpinfo.py可以检查任意文件是否是位图文件如果是打印出图片大小和颜色数。
参考源码
check_bmp.py
hashlib
摘要算法简介
Python的hashlib提供了常见的摘要算法如MD5SHA1等等。
什么是摘要算法呢摘要算法又称哈希算法、散列算法。它通过一个函数把任意长度的数据转换为一个长度固定的数据串通常用16进制的字符串表示。
举个例子你写了一篇文章内容是一个字符串how to use python hashlib - by Michael并附上这篇文章的摘要是2d73d4f15c0db7f5ecb321b6a65e5d6d。如果有人篡改了你的文章并发表为how to use python hashlib - by Bob你可以一下子指出Bob篡改了你的文章因为根据how to use python hashlib - by Bob计算出的摘要不同于原始文章的摘要。
可见摘要算法就是通过摘要函数f()对任意长度的数据data计算出固定长度的摘要digest目的是为了发现原始数据是否被人篡改过。
摘要算法之所以能指出数据是否被篡改过就是因为摘要函数是一个单向函数计算f(data)很容易但通过digest反推data却非常困难。而且对原始数据做一个bit的修改都会导致计算出的摘要完全不同。
我们以常见的摘要算法MD5为例计算出一个字符串的MD5值
import hashlibmd5 hashlib.md5()
md5.update(how to use md5 in python hashlib?.encode(utf-8))
print(md5.hexdigest())计算结果如下
d26a53750bc40b38b65a520292f69306如果数据量很大可以分块多次调用update()最后计算的结果是一样的
import hashlibmd5 hashlib.md5()
md5.update(how to use md5 in .encode(utf-8))
md5.update(python hashlib?.encode(utf-8))
print(md5.hexdigest())试试改动一个字母看看计算的结果是否完全不同。
MD5是最常见的摘要算法速度很快生成结果是固定的128 bit字节通常用一个32位的16进制字符串表示。
另一种常见的摘要算法是SHA1调用SHA1和调用MD5完全类似
import hashlibsha1 hashlib.sha1()
sha1.update(how to use sha1 in .encode(utf-8))
sha1.update(python hashlib?.encode(utf-8))
print(sha1.hexdigest())SHA1的结果是160 bit字节通常用一个40位的16进制字符串表示。
比SHA1更安全的算法是SHA256和SHA512不过越安全的算法不仅越慢而且摘要长度更长。
有没有可能两个不同的数据通过某个摘要算法得到了相同的摘要完全有可能因为任何摘要算法都是把无限多的数据集合映射到一个有限的集合中。这种情况称为碰撞比如Bob试图根据你的摘要反推出一篇文章how to learn hashlib in python - by Bob并且这篇文章的摘要恰好和你的文章完全一致这种情况也并非不可能出现但是非常非常困难。
摘要算法应用
摘要算法能应用到什么地方举个常用例子
任何允许用户登录的网站都会存储用户登录的用户名和口令。如何存储用户名和口令呢方法是存到数据库表中
name | password
------------------
michael | 123456
bob | abc999
alice | alice2008如果以明文保存用户口令如果数据库泄露所有用户的口令就落入黑客的手里。此外网站运维人员是可以访问数据库的也就是能获取到所有用户的口令。
正确的保存口令的方式是不存储用户的明文口令而是存储用户口令的摘要比如MD5
username | password
------------------------------------------
michael | e10adc3949ba59abbe56e057f20f883e
bob | 878ef96e86145580c38c87f0410ad153
alice | 99b1c2188db85afee403b1536010c2c9当用户登录时首先计算用户输入的明文口令的MD5然后和数据库存储的MD5对比如果一致说明口令输入正确如果不一致口令肯定错误。
练习
根据用户输入的口令计算出存储在数据库中的MD5口令
def calc_md5(password):pass存储MD5的好处是即使运维人员能访问数据库也无法获知用户的明文口令。
设计一个验证用户登录的函数根据用户输入的口令是否正确返回True或False
db {michael: e10adc3949ba59abbe56e057f20f883e,bob: 878ef96e86145580c38c87f0410ad153,alice: 99b1c2188db85afee403b1536010c2c9
}def login(user, password):pass采用MD5存储口令是否就一定安全呢也不一定。假设你是一个黑客已经拿到了存储MD5口令的数据库如何通过MD5反推用户的明文口令呢暴力破解费事费力真正的黑客不会这么干。
考虑这么个情况很多用户喜欢用123456888888password这些简单的口令于是黑客可以事先计算出这些常用口令的MD5值得到一个反推表
e10adc3949ba59abbe56e057f20f883e: 123456
21218cca77804d2ba1922c33e0151105: 888888
5f4dcc3b5aa765d61d8327deb882cf99: password这样无需破解只需要对比数据库的MD5黑客就获得了使用常用口令的用户账号。
对于用户来讲当然不要使用过于简单的口令。但是我们能否在程序设计上对简单口令加强保护呢
由于常用口令的MD5值很容易被计算出来所以要确保存储的用户口令不是那些已经被计算出来的常用口令的MD5这一方法通过对原始口令加一个复杂字符串来实现俗称“加盐”
def calc_md5(password):return get_md5(password the-Salt)经过Salt处理的MD5口令只要Salt不被黑客知道即使用户输入简单口令也很难通过MD5反推明文口令。
但是如果有两个用户都使用了相同的简单口令比如123456在数据库中将存储两条相同的MD5值这说明这两个用户的口令是一样的。有没有办法让使用相同口令的用户存储不同的MD5呢
如果假定用户无法修改登录名就可以通过把登录名作为Salt的一部分来计算MD5从而实现相同口令的用户也存储不同的MD5。
练习
根据用户输入的登录名和口令模拟用户注册计算更安全的MD5
db {}def register(username, password):db[username] get_md5(password username the-Salt)然后根据修改后的MD5算法实现用户登录的验证
def login(username, password):pass小结
摘要算法在很多地方都有广泛的应用。要注意摘要算法不是加密算法不能用于加密因为无法通过摘要反推明文只能用于防篡改但是它的单向计算特性决定了可以在不存储明文口令的情况下验证用户口令。
参考源码
use_hashlib.py
itertools
Python的内建模块itertools提供了非常有用的用于操作迭代对象的函数。
首先我们看看itertools提供的几个“无限”迭代器 import itertoolsnatuals itertools.count(1)for n in natuals:
... print(n)
...
1
2
3
...因为count()会创建一个无限的迭代器所以上述代码会打印出自然数序列根本停不下来只能按CtrlC退出。
cycle()会把传入的一个序列无限重复下去 import itertoolscs itertools.cycle(ABC) # 注意字符串也是序列的一种for c in cs:
... print(c)
...
A
B
C
A
B
C
...同样停不下来。
repeat()负责把一个元素无限重复下去不过如果提供第二个参数就可以限定重复次数 ns itertools.repeat(A, 3)for n in ns:
... print(n)
...
A
A
A无限序列只有在for迭代时才会无限地迭代下去如果只是创建了一个迭代对象它不会事先把无限个元素生成出来事实上也不可能在内存中创建无限多个元素。
无限序列虽然可以无限迭代下去但是通常我们会通过takewhile()等函数根据条件判断来截取出一个有限的序列 natuals itertools.count(1)ns itertools.takewhile(lambda x: x 10, natuals)list(ns)
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]itertools提供的几个迭代器操作函数更加有用
chain()
chain()可以把一组迭代对象串联起来形成一个更大的迭代器 for c in itertools.chain(ABC, XYZ):
... print(c)
# 迭代效果A B C X Y Zgroupby()
groupby()把迭代器中相邻的重复元素挑出来放在一起 for key, group in itertools.groupby(AAABBBCCAAA):
... print(key, list(group))
...
A [A, A, A]
B [B, B, B]
C [C, C]
A [A, A, A]实际上挑选规则是通过函数完成的只要作用于函数的两个元素返回的值相等这两个元素就被认为是在一组的而函数返回值作为组的key。如果我们要忽略大小写分组就可以让元素A和a都返回相同的key for key, group in itertools.groupby(AaaBBbcCAAa, lambda c: c.upper()):
... print(key, list(group))
...
A [A, a, a]
B [B, B, b]
C [c, C]
A [A, A, a]小结
itertools模块提供的全部是处理迭代功能的函数它们的返回值不是list而是Iterator只有用for循环迭代的时候才真正计算。
参考源码
use_itertools.py
XML
XML虽然比JSON复杂在Web中应用也不如以前多了不过仍有很多地方在用所以有必要了解如何操作XML。
DOM vs SAX
操作XML有两种方法DOM和SAX。DOM会把整个XML读入内存解析为树因此占用内存大解析慢优点是可以任意遍历树的节点。SAX是流模式边读边解析占用内存小解析快缺点是我们需要自己处理事件。
正常情况下优先考虑SAX因为DOM实在太占内存。
在Python中使用SAX解析XML非常简洁通常我们关心的事件是start_elementend_element和char_data准备好这3个函数然后就可以解析xml了。
举个例子当SAX解析器读到一个节点时
a href/python/a会产生3个事件 start_element事件在读取a href/时 char_data事件在读取python时 end_element事件在读取/a时。
用代码实验一下
from xml.parsers.expat import ParserCreateclass DefaultSaxHandler(object):def start_element(self, name, attrs):print(sax:start_element: %s, attrs: %s % (name, str(attrs)))def end_element(self, name):print(sax:end_element: %s % name)def char_data(self, text):print(sax:char_data: %s % text)xml r?xml version1.0?
ollia href/pythonPython/a/lilia href/rubyRuby/a/li
/ol
handler DefaultSaxHandler()
parser ParserCreate()
parser.StartElementHandler handler.start_element
parser.EndElementHandler handler.end_element
parser.CharacterDataHandler handler.char_data
parser.Parse(xml)需要注意的是读取一大段字符串时CharacterDataHandler可能被多次调用所以需要自己保存起来在EndElementHandler里面再合并。
除了解析XML外如何生成XML呢99%的情况下需要生成的XML结构都是非常简单的因此最简单也是最有效的生成XML的方法是拼接字符串
L []
L.append(r?xml version1.0?)
L.append(rroot)
L.append(encode(some data))
L.append(r/root)
return .join(L)如果要生成复杂的XML呢建议你不要用XML改成JSON。
小结
解析XML时注意找出自己感兴趣的节点响应事件时把节点数据保存起来。解析完毕后就可以处理数据。
练习
请利用SAX编写程序解析Yahoo的XML格式的天气预报获取当天和第二天的天气
http://weather.yahooapis.com/forecastrss?ucw2151330
参数w是城市代码要查询某个城市代码可以在weather.yahoo.com搜索城市浏览器地址栏的URL就包含城市代码。
# -*- coding:utf-8 -*-from xml.parsers.expat import ParserCreate
----
class WeatherSaxHandler(object):passdef parse_weather(xml):return {city: Beijing,country: China,today: {text: Partly Cloudy,low: 20,high: 33},tomorrow: {text: Sunny,low: 21,high: 34}}
----
# 测试:
data r?xml version1.0 encodingUTF-8 standaloneyes ?
rss version2.0 xmlns:yweatherhttp://xml.weather.yahoo.com/ns/rss/1.0 xmlns:geohttp://www.w3.org/2003/01/geo/wgs84_pos#channeltitleYahoo! Weather - Beijing, CN/titlelastBuildDateWed, 27 May 2015 11:00 am CST/lastBuildDateyweather:location cityBeijing region countryChina/yweather:units temperatureC distancekm pressuremb speedkm/h/yweather:wind chill28 direction180 speed14.48 /yweather:atmosphere humidity53 visibility2.61 pressure1006.1 rising0 /yweather:astronomy sunrise4:51 am sunset7:32 pm/itemgeo:lat39.91/geo:latgeo:long116.39/geo:longpubDateWed, 27 May 2015 11:00 am CST/pubDateyweather:condition textHaze code21 temp28 dateWed, 27 May 2015 11:00 am CST /yweather:forecast dayWed date27 May 2015 low20 high33 textPartly Cloudy code30 /yweather:forecast dayThu date28 May 2015 low21 high34 textSunny code32 /yweather:forecast dayFri date29 May 2015 low18 high25 textAM Showers code39 /yweather:forecast daySat date30 May 2015 low18 high32 textSunny code32 /yweather:forecast daySun date31 May 2015 low20 high37 textSunny code32 //item/channel
/rssweather parse_weather(data)
assert weather[city] Beijing, weather[city]
assert weather[country] China, weather[country]
assert weather[today][text] Partly Cloudy, weather[today][text]
assert weather[today][low] 20, weather[today][low]
assert weather[today][high] 33, weather[today][high]
assert weather[tomorrow][text] Sunny, weather[tomorrow][text]
assert weather[tomorrow][low] 21, weather[tomorrow][low]
assert weather[tomorrow][high] 34, weather[tomorrow][high]
print(Weather:, str(weather))参考源码
use_sax.py
HTMLParser
如果我们要编写一个搜索引擎第一步是用爬虫把目标网站的页面抓下来第二步就是解析该HTML页面看看里面的内容到底是新闻、图片还是视频。
假设第一步已经完成了第二步应该如何解析HTML呢
HTML本质上是XML的子集但是HTML的语法没有XML那么严格所以不能用标准的DOM或SAX来解析HTML。
好在Python提供了HTMLParser来非常方便地解析HTML只需简单几行代码
from html.parser import HTMLParser
from html.entities import name2codepointclass MyHTMLParser(HTMLParser):def handle_starttag(self, tag, attrs):print(%s % tag)def handle_endtag(self, tag):print(/%s % tag)def handle_startendtag(self, tag, attrs):print(%s/ % tag)def handle_data(self, data):print(data)def handle_comment(self, data):print(!--, data, --)def handle_entityref(self, name):print(%s; % name)def handle_charref(self, name):print(#%s; % name)parser MyHTMLParser()
parser.feed(html
head/head
body
!-- test html parser --pSome a href\#\html/a HTMLnbsp;tutorial...brEND/p
/body/html)feed()方法可以多次调用也就是不一定一次把整个HTML字符串都塞进去可以一部分一部分塞进去。
特殊字符有两种一种是英文表示的nbsp;一种是数字表示的#1234;这两种字符都可以通过Parser解析出来。
小结
利用HTMLParser可以把网页中的文本、图像等解析出来。
练习
找一个网页例如https://www.python.org/events/python-events/用浏览器查看源码并复制然后尝试解析一下HTML输出Python官网发布的会议时间、名称和地点。
参考源码
use_htmlparser.py
urllib
urllib提供了一系列用于操作URL的功能。
Get
urllib的request模块可以非常方便地抓取URL内容也就是发送一个GET请求到指定的页面然后返回HTTP的响应
例如对豆瓣的一个URLhttps://api.douban.com/v2/book/2129650进行抓取并返回响应
from urllib import requestwith request.urlopen(https://api.douban.com/v2/book/2129650) as f:data f.read()print(Status:, f.status, f.reason)for k, v in f.getheaders():print(%s: %s % (k, v))print(Data:, data.decode(utf-8))可以看到HTTP响应的头和JSON数据
Status: 200 OK
Server: nginx
Date: Tue, 26 May 2015 10:02:27 GMT
Content-Type: application/json; charsetutf-8
Content-Length: 2049
Connection: close
Expires: Sun, 1 Jan 2006 01:00:00 GMT
Pragma: no-cache
Cache-Control: must-revalidate, no-cache, private
X-DAE-Node: pidl1
Data: {rating:{max:10,numRaters:16,average:7.4,min:0},subtitle:,author:[廖雪峰编著],pubdate:2007-6,tags:[{count:20,name:spring,title:spring}...}如果我们要想模拟浏览器发送GET请求就需要使用Request对象通过往Request对象添加HTTP头我们就可以把请求伪装成浏览器。例如模拟iPhone 6去请求豆瓣首页
from urllib import requestreq request.Request(http://www.douban.com/)
req.add_header(User-Agent, Mozilla/6.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/8.0 Mobile/10A5376e Safari/8536.25)
with request.urlopen(req) as f:print(Status:, f.status, f.reason)for k, v in f.getheaders():print(%s: %s % (k, v))print(Data:, f.read().decode(utf-8))这样豆瓣会返回适合iPhone的移动版网页
...meta nameviewport contentwidthdevice-width, user-scalableno, initial-scale1.0, minimum-scale1.0, maximum-scale1.0meta nameformat-detection contenttelephonenolink relapple-touch-icon sizes57x57 hrefhttp://img4.douban.com/pics/cardkit/launcher/57.png /
...Post
如果要以POST发送一个请求只需要把参数data以bytes形式传入。
我们模拟一个微博登录先读取登录的邮箱和口令然后按照weibo.cn的登录页的格式以usernamexxxpasswordxxx的编码传入
from urllib import request, parseprint(Login to weibo.cn...)
email input(Email: )
passwd input(Password: )
login_data parse.urlencode([(username, email),(password, passwd),(entry, mweibo),(client_id, ),(savestate, 1),(ec, ),(pagerefer, https://passport.weibo.cn/signin/welcome?entrymweiborhttp%3A%2F%2Fm.weibo.cn%2F)
])req request.Request(https://passport.weibo.cn/sso/login)
req.add_header(Origin, https://passport.weibo.cn)
req.add_header(User-Agent, Mozilla/6.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/8.0 Mobile/10A5376e Safari/8536.25)
req.add_header(Referer, https://passport.weibo.cn/signin/login?entrymweiboreswelwm3349rhttp%3A%2F%2Fm.weibo.cn%2F)with request.urlopen(req, datalogin_data.encode(utf-8)) as f:print(Status:, f.status, f.reason)for k, v in f.getheaders():print(%s: %s % (k, v))print(Data:, f.read().decode(utf-8))如果登录成功我们获得的响应如下
Status: 200 OK
Server: nginx/1.2.0
...
Set-Cookie: SSOLoginState1432620126; path/; domainweibo.cn
...
Data: {retcode:20000000,msg:,data:{...,uid:1658384301}}如果登录失败我们获得的响应如下
...
Data: {retcode:50011015,msg:\u7528\u6237\u540d\u6216\u5bc6\u7801\u9519\u8bef,data:{username:examplepython.org,errline:536}}Handler
如果还需要更复杂的控制比如通过一个Proxy去访问网站我们需要利用ProxyHandler来处理示例代码如下
proxy_handler urllib.request.ProxyHandler({http: http://www.example.com:3128/})
proxy_auth_handler urllib.request.ProxyBasicAuthHandler()
proxy_auth_handler.add_password(realm, host, username, password)
opener urllib.request.build_opener(proxy_handler, proxy_auth_handler)
with opener.open(http://www.example.com/login.html) as f:pass小结
urllib提供的功能就是利用程序去执行各种HTTP请求。如果要模拟浏览器完成特定功能需要把请求伪装成浏览器。伪装的方法是先监控浏览器发出的请求再根据浏览器的请求头来伪装User-Agent头就是用来标识浏览器的。
练习
利用urllib读取XML将XML一节的数据由硬编码改为由urllib获取
from urllib import request, parsedef fetch_xml(url):
----pass
----
# 测试
print(fetch_xml(http://weather.yahooapis.com/forecastrss?ucw2151330))参考源码
use_urllib.py
常用第三方模块
除了内建的模块外Python还有大量的第三方模块。
基本上所有的第三方模块都会在PyPI - the Python Package Index上注册只要找到对应的模块名字即可用pip安装。
本章介绍常用的第三方模块。
PIL
PILPython Imaging Library已经是Python平台事实上的图像处理标准库了。PIL功能非常强大但API却非常简单易用。
由于PIL仅支持到Python 2.7加上年久失修于是一群志愿者在PIL的基础上创建了兼容的版本名字叫Pillow支持最新Python 3.x又加入了许多新特性因此我们可以直接安装使用Pillow。
安装Pillow
在命令行下直接通过pip安装
$ pip install pillow如果遇到Permission denied安装失败请加上sudo重试。
操作图像
来看看最常见的图像缩放操作只需三四行代码
from PIL import Image# 打开一个jpg图像文件注意是当前路径:
im Image.open(test.jpg)
# 获得图像尺寸:
w, h im.size
print(Original image size: %sx%s % (w, h))
# 缩放到50%:
im.thumbnail((w//2, h//2))
print(Resize image to: %sx%s % (w//2, h//2))
# 把缩放后的图像用jpeg格式保存:
im.save(thumbnail.jpg, jpeg)其他功能如切片、旋转、滤镜、输出文字、调色板等一应俱全。
比如模糊效果也只需几行代码
from PIL import Image, ImageFilter# 打开一个jpg图像文件注意是当前路径:
im Image.open(test.jpg)
# 应用模糊滤镜:
im2 im.filter(ImageFilter.BLUR)
im2.save(blur.jpg, jpeg)效果如下 PIL的ImageDraw提供了一系列绘图方法让我们可以直接绘图。比如要生成字母验证码图片
from PIL import Image, ImageDraw, ImageFont, ImageFilterimport random# 随机字母:
def rndChar():return chr(random.randint(65, 90))# 随机颜色1:
def rndColor():return (random.randint(64, 255), random.randint(64, 255), random.randint(64, 255))# 随机颜色2:
def rndColor2():return (random.randint(32, 127), random.randint(32, 127), random.randint(32, 127))# 240 x 60:
width 60 * 4
height 60
image Image.new(RGB, (width, height), (255, 255, 255))
# 创建Font对象:
font ImageFont.truetype(Arial.ttf, 36)
# 创建Draw对象:
draw ImageDraw.Draw(image)
# 填充每个像素:
for x in range(width):for y in range(height):draw.point((x, y), fillrndColor())
# 输出文字:
for t in range(4):draw.text((60 * t 10, 10), rndChar(), fontfont, fillrndColor2())
# 模糊:
image image.filter(ImageFilter.BLUR)
image.save(code.jpg, jpeg)我们用随机颜色填充背景再画上文字最后对图像进行模糊得到验证码图片如下 如果运行的时候报错
IOError: cannot open resource这是因为PIL无法定位到字体文件的位置可以根据操作系统提供绝对路径比如
/Library/Fonts/Arial.ttf要详细了解PIL的强大功能请请参考Pillow官方文档
https://pillow.readthedocs.org/
小结
PIL提供了操作图像的强大功能可以通过简单的代码完成复杂的图像处理。
参考源码
https://github.com/michaelliao/learn-python3/blob/master/samples/packages/pil/use_pil_resize.py
https://github.com/michaelliao/learn-python3/blob/master/samples/packages/pil/use_pil_blur.py
https://github.com/michaelliao/learn-python3/blob/master/samples/packages/pil/use_pil_draw.py
virtualenv
在开发Python应用程序的时候系统安装的Python3只有一个版本3.4。所有第三方的包都会被pip安装到Python3的site-packages目录下。
如果我们要同时开发多个应用程序那这些应用程序都会共用一个Python就是安装在系统的Python 3。如果应用A需要jinja 2.7而应用B需要jinja 2.6怎么办
这种情况下每个应用可能需要各自拥有一套“独立”的Python运行环境。virtualenv就是用来为一个应用创建一套“隔离”的Python运行环境。
首先我们用pip安装virtualenv
$ pip3 install virtualenv然后假定我们要开发一个新的项目需要一套独立的Python运行环境可以这么做
第一步创建目录
Mac:~ michael$ mkdir myproject
Mac:~ michael$ cd myproject/
Mac:myproject michael$第二步创建一个独立的Python运行环境命名为venv
Mac:myproject michael$ virtualenv --no-site-packages venv
Using base prefix /usr/local/.../Python.framework/Versions/3.4
New python executable in venv/bin/python3.4
Also creating executable in venv/bin/python
Installing setuptools, pip, wheel...done.命令virtualenv就可以创建一个独立的Python运行环境我们还加上了参数--no-site-packages这样已经安装到系统Python环境中的所有第三方包都不会复制过来这样我们就得到了一个不带任何第三方包的“干净”的Python运行环境。
新建的Python环境被放到当前目录下的venv目录。有了venv这个Python环境可以用source进入该环境
Mac:myproject michael$ source venv/bin/activate
(venv)Mac:myproject michael$注意到命令提示符变了有个(venv)前缀表示当前环境是一个名为venv的Python环境。
下面正常安装各种第三方包并运行python命令
(venv)Mac:myproject michael$ pip install jinja2
...
Successfully installed jinja2-2.7.3 markupsafe-0.23
(venv)Mac:myproject michael$ python myapp.py
...在venv环境下用pip安装的包都被安装到venv这个环境下系统Python环境不受任何影响。也就是说venv环境是专门针对myproject这个应用创建的。
退出当前的venv环境使用deactivate命令
(venv)Mac:myproject michael$ deactivate
Mac:myproject michael$此时就回到了正常的环境现在pip或python均是在系统Python环境下执行。
完全可以针对每个应用创建独立的Python运行环境这样就可以对每个应用的Python环境进行隔离。
virtualenv是如何创建“独立”的Python运行环境的呢原理很简单就是把系统Python复制一份到virtualenv的环境用命令source venv/bin/activate进入一个virtualenv环境时virtualenv会修改相关环境变量让命令python和pip均指向当前的virtualenv环境。
小结
virtualenv为应用提供了隔离的Python运行环境解决了不同应用间多版本的冲突问题。
图形界面
Python支持多种图形界面的第三方库包括 Tk wxWidgets Qt GTK
等等。
但是Python自带的库是支持Tk的Tkinter使用Tkinter无需安装任何包就可以直接使用。本章简单介绍如何使用Tkinter进行GUI编程。
Tkinter
我们来梳理一下概念
我们编写的Python代码会调用内置的TkinterTkinter封装了访问Tk的接口
Tk是一个图形库支持多个操作系统使用Tcl语言开发
Tk会调用操作系统提供的本地GUI接口完成最终的GUI。
所以我们的代码只需要调用Tkinter提供的接口就可以了。
第一个GUI程序
使用Tkinter十分简单我们来编写一个GUI版本的“Hello, world!”。
第一步是导入Tkinter包的所有内容
from tkinter import *第二步是从Frame派生一个Application类这是所有Widget的父容器
class Application(Frame):def __init__(self, masterNone):Frame.__init__(self, master)self.pack()self.createWidgets()def createWidgets(self):self.helloLabel Label(self, textHello, world!)self.helloLabel.pack()self.quitButton Button(self, textQuit, commandself.quit)self.quitButton.pack()在GUI中每个Button、Label、输入框等都是一个Widget。Frame则是可以容纳其他Widget的Widget所有的Widget组合起来就是一棵树。
pack()方法把Widget加入到父容器中并实现布局。pack()是最简单的布局grid()可以实现更复杂的布局。
在createWidgets()方法中我们创建一个Label和一个Button当Button被点击时触发self.quit()使程序退出。
第三步实例化Application并启动消息循环
app Application()
# 设置窗口标题:
app.master.title(Hello World)
# 主消息循环:
app.mainloop()GUI程序的主线程负责监听来自操作系统的消息并依次处理每一条消息。因此如果消息处理非常耗时就需要在新线程中处理。
运行这个GUI程序可以看到下面的窗口 点击“Quit”按钮或者窗口的“x”结束程序。
输入文本
我们再对这个GUI程序改进一下加入一个文本框让用户可以输入文本然后点按钮后弹出消息对话框。
from tkinter import *
import tkinter.messagebox as messageboxclass Application(Frame):def __init__(self, masterNone):Frame.__init__(self, master)self.pack()self.createWidgets()def createWidgets(self):self.nameInput Entry(self)self.nameInput.pack()self.alertButton Button(self, textHello, commandself.hello)self.alertButton.pack()def hello(self):name self.nameInput.get() or worldmessagebox.showinfo(Message, Hello, %s % name)app Application()
# 设置窗口标题:
app.master.title(Hello World)
# 主消息循环:
app.mainloop()当用户点击按钮时触发hello()通过self.nameInput.get()获得用户输入的文本后使用tkMessageBox.showinfo()可以弹出消息对话框。
程序运行结果如下 小结
Python内置的Tkinter可以满足基本的GUI程序的要求如果是非常复杂的GUI程序建议用操作系统原生支持的语言和库来编写。
参考源码
hello_gui.py
网络编程
自从互联网诞生以来现在基本上所有的程序都是网络程序很少有单机版的程序了。
计算机网络就是把各个计算机连接到一起让网络中的计算机可以互相通信。网络编程就是如何在程序中实现两台计算机的通信。
举个例子当你使用浏览器访问新浪网时你的计算机就和新浪的某台服务器通过互联网连接起来了然后新浪的服务器把网页内容作为数据通过互联网传输到你的电脑上。
由于你的电脑上可能不止浏览器还有QQ、Skype、Dropbox、邮件客户端等不同的程序连接的别的计算机也会不同所以更确切地说网络通信是两台计算机上的两个进程之间的通信。比如浏览器进程和新浪服务器上的某个Web服务进程在通信而QQ进程是和腾讯的某个服务器上的某个进程在通信。 网络编程对所有开发语言都是一样的Python也不例外。用Python进行网络编程就是在Python程序本身这个进程内连接别的服务器进程的通信端口进行通信。
本章我们将详细介绍Python网络编程的概念和最主要的两种网络类型的编程。
TCP/IP简介
虽然大家现在对互联网很熟悉但是计算机网络的出现比互联网要早很多。
计算机为了联网就必须规定通信协议早期的计算机网络都是由各厂商自己规定一套协议IBM、Apple和Microsoft都有各自的网络协议互不兼容这就好比一群人有的说英语有的说中文有的说德语说同一种语言的人可以交流不同的语言之间就不行了。
为了把全世界的所有不同类型的计算机都连接起来就必须规定一套全球通用的协议为了实现互联网这个目标互联网协议簇Internet Protocol Suite就是通用协议标准。Internet是由inter和net两个单词组合起来的原意就是连接“网络”的网络有了Internet任何私有网络只要支持这个协议就可以联入互联网。
因为互联网协议包含了上百种协议标准但是最重要的两个协议是TCP和IP协议所以大家把互联网的协议简称TCP/IP协议。
通信的时候双方必须知道对方的标识好比发邮件必须知道对方的邮件地址。互联网上每个计算机的唯一标识就是IP地址类似123.123.123.123。如果一台计算机同时接入到两个或更多的网络比如路由器它就会有两个或多个IP地址所以IP地址对应的实际上是计算机的网络接口通常是网卡。
IP协议负责把数据从一台计算机通过网络发送到另一台计算机。数据被分割成一小块一小块然后通过IP包发送出去。由于互联网链路复杂两台计算机之间经常有多条线路因此路由器就负责决定如何把一个IP包转发出去。IP包的特点是按块发送途径多个路由但不保证能到达也不保证顺序到达。 IP地址实际上是一个32位整数称为IPv4以字符串表示的IP地址如192.168.0.1实际上是把32位整数按8位分组后的数字表示目的是便于阅读。
IPv6地址实际上是一个128位整数它是目前使用的IPv4的升级版以字符串表示类似于2001:0db8:85a3:0042:1000:8a2e:0370:7334。
TCP协议则是建立在IP协议之上的。TCP协议负责在两台计算机之间建立可靠连接保证数据包按顺序到达。TCP协议会通过握手建立连接然后对每个IP包编号确保对方按顺序收到如果包丢掉了就自动重发。
许多常用的更高级的协议都是建立在TCP协议基础上的比如用于浏览器的HTTP协议、发送邮件的SMTP协议等。
一个IP包除了包含要传输的数据外还包含源IP地址和目标IP地址源端口和目标端口。
端口有什么作用在两台计算机通信时只发IP地址是不够的因为同一台计算机上跑着多个网络程序。一个IP包来了之后到底是交给浏览器还是QQ就需要端口号来区分。每个网络程序都向操作系统申请唯一的端口号这样两个进程在两台计算机之间建立网络连接就需要各自的IP地址和各自的端口号。
一个进程也可能同时与多个计算机建立链接因此它会申请很多端口。
了解了TCP/IP协议的基本概念IP地址和端口的概念我们就可以开始进行网络编程了。
TCP编程
Socket是网络编程的一个抽象概念。通常我们用一个Socket表示“打开了一个网络链接”而打开一个Socket需要知道目标计算机的IP地址和端口号再指定协议类型即可。
客户端
大多数连接都是可靠的TCP连接。创建TCP连接时主动发起连接的叫客户端被动响应连接的叫服务器。
举个例子当我们在浏览器中访问新浪时我们自己的计算机就是客户端浏览器会主动向新浪的服务器发起连接。如果一切顺利新浪的服务器接受了我们的连接一个TCP连接就建立起来的后面的通信就是发送网页内容了。
所以我们要创建一个基于TCP连接的Socket可以这样做
# 导入socket库:
import socket# 创建一个socket:
s socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 建立连接:
s.connect((www.sina.com.cn, 80))创建Socket时AF_INET指定使用IPv4协议如果要用更先进的IPv6就指定为AF_INET6。SOCK_STREAM指定使用面向流的TCP协议这样一个Socket对象就创建成功但是还没有建立连接。
客户端要主动发起TCP连接必须知道服务器的IP地址和端口号。新浪网站的IP地址可以用域名www.sina.com.cn自动转换到IP地址但是怎么知道新浪服务器的端口号呢
答案是作为服务器提供什么样的服务端口号就必须固定下来。由于我们想要访问网页因此新浪提供网页服务的服务器必须把端口号固定在80端口因为80端口是Web服务的标准端口。其他服务都有对应的标准端口号例如SMTP服务是25端口FTP服务是21端口等等。端口号小于1024的是Internet标准服务的端口端口号大于1024的可以任意使用。
因此我们连接新浪服务器的代码如下
s.connect((www.sina.com.cn, 80))注意参数是一个tuple包含地址和端口号。
建立TCP连接后我们就可以向新浪服务器发送请求要求返回首页的内容
# 发送数据:
s.send(bGET / HTTP/1.1\r\nHost: www.sina.com.cn\r\nConnection: close\r\n\r\n)TCP连接创建的是双向通道双方都可以同时给对方发数据。但是谁先发谁后发怎么协调要根据具体的协议来决定。例如HTTP协议规定客户端必须先发请求给服务器服务器收到后才发数据给客户端。
发送的文本格式必须符合HTTP标准如果格式没问题接下来就可以接收新浪服务器返回的数据了
# 接收数据:
buffer []
while True:# 每次最多接收1k字节:d s.recv(1024)if d:buffer.append(d)else:break
data b.join(buffer)接收数据时调用recv(max)方法一次最多接收指定的字节数因此在一个while循环中反复接收直到recv()返回空数据表示接收完毕退出循环。
当我们接收完数据后调用close()方法关闭Socket这样一次完整的网络通信就结束了
# 关闭连接:
s.close()接收到的数据包括HTTP头和网页本身我们只需要把HTTP头和网页分离一下把HTTP头打印出来网页内容保存到文件
header, html data.split(b\r\n\r\n, 1)
print(header.decode(utf-8))
# 把接收的数据写入文件:
with open(sina.html, wb) as f:f.write(html)现在只需要在浏览器中打开这个sina.html文件就可以看到新浪的首页了。
服务器
和客户端编程相比服务器编程就要复杂一些。
服务器进程首先要绑定一个端口并监听来自其他客户端的连接。如果某个客户端连接过来了服务器就与该客户端建立Socket连接随后的通信就靠这个Socket连接了。
所以服务器会打开固定端口比如80监听每来一个客户端连接就创建该Socket连接。由于服务器会有大量来自客户端的连接所以服务器要能够区分一个Socket连接是和哪个客户端绑定的。一个Socket依赖4项服务器地址、服务器端口、客户端地址、客户端端口来唯一确定一个Socket。
但是服务器还需要同时响应多个客户端的请求所以每个连接都需要一个新的进程或者新的线程来处理否则服务器一次就只能服务一个客户端了。
我们来编写一个简单的服务器程序它接收客户端连接把客户端发过来的字符串加上Hello再发回去。
首先创建一个基于IPv4和TCP协议的Socket
s socket.socket(socket.AF_INET, socket.SOCK_STREAM)然后我们要绑定监听的地址和端口。服务器可能有多块网卡可以绑定到某一块网卡的IP地址上也可以用0.0.0.0绑定到所有的网络地址还可以用127.0.0.1绑定到本机地址。127.0.0.1是一个特殊的IP地址表示本机地址如果绑定到这个地址客户端必须同时在本机运行才能连接也就是说外部的计算机无法连接进来。
端口号需要预先指定。因为我们写的这个服务不是标准服务所以用9999这个端口号。请注意小于1024的端口号必须要有管理员权限才能绑定
# 监听端口:
s.bind((127.0.0.1, 9999))紧接着调用listen()方法开始监听端口传入的参数指定等待连接的最大数量
s.listen(5)
print(Waiting for connection...)接下来服务器程序通过一个永久循环来接受来自客户端的连接accept()会等待并返回一个客户端的连接:
while True:# 接受一个新连接:sock, addr s.accept()# 创建新线程来处理TCP连接:t threading.Thread(targettcplink, args(sock, addr))t.start()每个连接都必须创建新线程或进程来处理否则单线程在处理连接的过程中无法接受其他客户端的连接
def tcplink(sock, addr):print(Accept new connection from %s:%s... % addr)sock.send(bWelcome!)while True:data sock.recv(1024)time.sleep(1)if not data or data.decode(utf-8) exit:breaksock.send((Hello, %s! % data.decode(utf-8)).encode(utf-8))sock.close()print(Connection from %s:%s closed. % addr)连接建立后服务器首先发一条欢迎消息然后等待客户端数据并加上Hello再发送给客户端。如果客户端发送了exit字符串就直接关闭连接。
要测试这个服务器程序我们还需要编写一个客户端程序
s socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 建立连接:
s.connect((127.0.0.1, 9999))
# 接收欢迎消息:
print(s.recv(1024).decode(utf-8))
for data in [bMichael, bTracy, bSarah]:# 发送数据:s.send(data)print(s.recv(1024).decode(utf-8))
s.send(bexit)
s.close()我们需要打开两个命令行窗口一个运行服务器程序另一个运行客户端程序就可以看到效果了 需要注意的是客户端程序运行完毕就退出了而服务器程序会永远运行下去必须按CtrlC退出程序。
小结
用TCP协议进行Socket编程在Python中十分简单对于客户端要主动连接服务器的IP和指定端口对于服务器要首先监听指定端口然后对每一个新的连接创建一个线程或进程来处理。通常服务器程序会无限运行下去。
同一个端口被一个Socket绑定了以后就不能被别的Socket绑定了。
参考源码
do_tcp.py
UDP编程
TCP是建立可靠连接并且通信双方都可以以流的形式发送数据。相对TCPUDP则是面向无连接的协议。
使用UDP协议时不需要建立连接只需要知道对方的IP地址和端口号就可以直接发数据包。但是能不能到达就不知道了。
虽然用UDP传输数据不可靠但它的优点是和TCP比速度快对于不要求可靠到达的数据就可以使用UDP协议。
我们来看看如何通过UDP协议传输数据。和TCP类似使用UDP的通信双方也分为客户端和服务器。服务器首先需要绑定端口
s socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 绑定端口:
s.bind((127.0.0.1, 9999))创建Socket时SOCK_DGRAM指定了这个Socket的类型是UDP。绑定端口和TCP一样但是不需要调用listen()方法而是直接接收来自任何客户端的数据
print(Bind UDP on 9999...)
while True:# 接收数据:data, addr s.recvfrom(1024)print(Received from %s:%s. % addr)s.sendto(bHello, %s! % data, addr)recvfrom()方法返回数据和客户端的地址与端口这样服务器收到数据后直接调用sendto()就可以把数据用UDP发给客户端。
注意这里省掉了多线程因为这个例子很简单。
客户端使用UDP时首先仍然创建基于UDP的Socket然后不需要调用connect()直接通过sendto()给服务器发数据
s socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
for data in [bMichael, bTracy, bSarah]:# 发送数据:s.sendto(data, (127.0.0.1, 9999))# 接收数据:print(s.recv(1024).decode(utf-8))
s.close()从服务器接收数据仍然调用recv()方法。
仍然用两个命令行分别启动服务器和客户端测试结果如下 小结
UDP的使用与TCP类似但是不需要建立连接。此外服务器绑定UDP端口和TCP端口互不冲突也就是说UDP的9999端口与TCP的9999端口可以各自绑定。
参考源码
udp_server.py
udp_client.py
电子邮件
Email的历史比Web还要久远直到现在Email也是互联网上应用非常广泛的服务。
几乎所有的编程语言都支持发送和接收电子邮件但是先等等在我们开始编写代码之前有必要搞清楚电子邮件是如何在互联网上运作的。
我们来看看传统邮件是如何运作的。假设你现在在北京要给一个香港的朋友发一封信怎么做呢
首先你得写好信装进信封写上地址贴上邮票然后就近找个邮局把信仍进去。
信件会从就近的小邮局转运到大邮局再从大邮局往别的城市发比如先发到天津再走海运到达香港也可能走京九线到香港但是你不用关心具体路线你只需要知道一件事就是信件走得很慢至少要几天时间。
信件到达香港的某个邮局也不会直接送到朋友的家里因为邮局的叔叔是很聪明的他怕你的朋友不在家一趟一趟地白跑所以信件会投递到你的朋友的邮箱里邮箱可能在公寓的一层或者家门口直到你的朋友回家的时候检查邮箱发现信件后就可以取到邮件了。
电子邮件的流程基本上也是按上面的方式运作的只不过速度不是按天算而是按秒算。
现在我们回到电子邮件假设我们自己的电子邮件地址是me163.com对方的电子邮件地址是friendsina.com注意地址都是虚构的哈现在我们用Outlook或者Foxmail之类的软件写好邮件填上对方的Email地址点“发送”电子邮件就发出去了。这些电子邮件软件被称为MUAMail User Agent——邮件用户代理。
Email从MUA发出去不是直接到达对方电脑而是发到MTAMail Transfer Agent——邮件传输代理就是那些Email服务提供商比如网易、新浪等等。由于我们自己的电子邮件是163.com所以Email首先被投递到网易提供的MTA再由网易的MTA发到对方服务商也就是新浪的MTA。这个过程中间可能还会经过别的MTA但是我们不关心具体路线我们只关心速度。
Email到达新浪的MTA后由于对方使用的是sina.com的邮箱因此新浪的MTA会把Email投递到邮件的最终目的地MDAMail Delivery Agent——邮件投递代理。Email到达MDA后就静静地躺在新浪的某个服务器上存放在某个文件或特殊的数据库里我们将这个长期保存邮件的地方称之为电子邮箱。
同普通邮件类似Email不会直接到达对方的电脑因为对方电脑不一定开机开机也不一定联网。对方要取到邮件必须通过MUA从MDA上把邮件取到自己的电脑上。
所以一封电子邮件的旅程就是
发件人 - MUA - MTA - MTA - 若干个MTA - MDA - MUA - 收件人有了上述基本概念要编写程序来发送和接收邮件本质上就是 编写MUA把邮件发到MTA 编写MUA从MDA上收邮件。
发邮件时MUA和MTA使用的协议就是SMTPSimple Mail Transfer Protocol后面的MTA到另一个MTA也是用SMTP协议。
收邮件时MUA和MDA使用的协议有两种POPPost Office Protocol目前版本是3俗称POP3IMAPInternet Message Access Protocol目前版本是4优点是不但能取邮件还可以直接操作MDA上存储的邮件比如从收件箱移到垃圾箱等等。
邮件客户端软件在发邮件时会让你先配置SMTP服务器也就是你要发到哪个MTA上。假设你正在使用163的邮箱你就不能直接发到新浪的MTA上因为它只服务新浪的用户所以你得填163提供的SMTP服务器地址smtp.163.com为了证明你是163的用户SMTP服务器还要求你填写邮箱地址和邮箱口令这样MUA才能正常地把Email通过SMTP协议发送到MTA。
类似的从MDA收邮件时MDA服务器也要求验证你的邮箱口令确保不会有人冒充你收取你的邮件所以Outlook之类的邮件客户端会要求你填写POP3或IMAP服务器地址、邮箱地址和口令这样MUA才能顺利地通过POP或IMAP协议从MDA取到邮件。
在使用Python收发邮件前请先准备好至少两个电子邮件如xxx163.comxxxsina.comxxxqq.com等注意两个邮箱不要用同一家邮件服务商。
最后特别注意目前大多数邮件服务商都需要手动打开SMTP发信和POP收信的功能否则只允许在网页登录 SMTP发送邮件
SMTP是发送邮件的协议Python内置对SMTP的支持可以发送纯文本邮件、HTML邮件以及带附件的邮件。
Python对SMTP支持有smtplib和email两个模块email负责构造邮件smtplib负责发送邮件。
首先我们来构造一个最简单的纯文本邮件
from email.mime.text import MIMEText
msg MIMEText(hello, send by Python..., plain, utf-8)注意到构造MIMEText对象时第一个参数就是邮件正文第二个参数是MIME的subtype传入plain表示纯文本最终的MIME就是text/plain最后一定要用utf-8编码保证多语言兼容性。
然后通过SMTP发出去
# 输入Email地址和口令:
from_addr input(From: )
password input(Password: )
# 输入收件人地址:
to_addr input(To: )
# 输入SMTP服务器地址:
smtp_server input(SMTP server: )import smtplib
server smtplib.SMTP(smtp_server, 25) # SMTP协议默认端口是25
server.set_debuglevel(1)
server.login(from_addr, password)
server.sendmail(from_addr, [to_addr], msg.as_string())
server.quit()我们用set_debuglevel(1)就可以打印出和SMTP服务器交互的所有信息。SMTP协议就是简单的文本命令和响应。login()方法用来登录SMTP服务器sendmail()方法就是发邮件由于可以一次发给多个人所以传入一个list邮件正文是一个stras_string()把MIMEText对象变成str。
如果一切顺利就可以在收件人信箱中收到我们刚发送的Email 仔细观察发现如下问题
邮件没有主题收件人的名字没有显示为友好的名字比如Mr Green greenexample.com明明收到了邮件却提示不在收件人中。
这是因为邮件主题、如何显示发件人、收件人等信息并不是通过SMTP协议发给MTA而是包含在发给MTA的文本中的所以我们必须把From、To和Subject添加到MIMEText中才是一封完整的邮件
from email import encoders
from email.header import Header
from email.mime.text import MIMEText
from email.utils import parseaddr, formataddrimport smtplibdef _format_addr(s):name, addr parseaddr(s)return formataddr((Header(name, utf-8).encode(), addr))from_addr input(From: )
password input(Password: )
to_addr input(To: )
smtp_server input(SMTP server: )msg MIMEText(hello, send by Python..., plain, utf-8)
msg[From] _format_addr(Python爱好者 %s % from_addr)
msg[To] _format_addr(管理员 %s % to_addr)
msg[Subject] Header(来自SMTP的问候……, utf-8).encode()server smtplib.SMTP(smtp_server, 25)
server.set_debuglevel(1)
server.login(from_addr, password)
server.sendmail(from_addr, [to_addr], msg.as_string())
server.quit()我们编写了一个函数_format_addr()来格式化一个邮件地址。注意不能简单地传入name addrexample.com因为如果包含中文需要通过Header对象进行编码。
msg[To]接收的是字符串而不是list如果有多个邮件地址用,分隔即可。
再发送一遍邮件就可以在收件人邮箱中看到正确的标题、发件人和收件人 你看到的收件人的名字很可能不是我们传入的管理员因为很多邮件服务商在显示邮件时会把收件人名字自动替换为用户注册的名字但是其他收件人名字的显示不受影响。
如果我们查看Email的原始内容可以看到如下经过编码的邮件头
From: ?utf-8?b?UHl0aG9u54ix5aW96ICF? xxxxxx163.com
To: ?utf-8?b?566h55CG5ZGY? xxxxxxqq.com
Subject: ?utf-8?b?5p2l6IeqU01UUOeahOmXruWAmeKApuKApg?这就是经过Header对象编码的文本包含utf-8编码信息和Base64编码的文本。如果我们自己来手动构造这样的编码文本显然比较复杂。
发送HTML邮件
如果我们要发送HTML邮件而不是普通的纯文本文件怎么办方法很简单在构造MIMEText对象时把HTML字符串传进去再把第二个参数由plain变为html就可以了
msg MIMEText(htmlbodyh1Hello/h1 psend by a hrefhttp://www.python.orgPython/a.../p /body/html, html, utf-8)再发送一遍邮件你将看到以HTML显示的邮件 发送附件
如果Email中要加上附件怎么办带附件的邮件可以看做包含若干部分的邮件文本和各个附件本身所以可以构造一个MIMEMultipart对象代表邮件本身然后往里面加上一个MIMEText作为邮件正文再继续往里面加上表示附件的MIMEBase对象即可
# 邮件对象:
msg MIMEMultipart()
msg[From] _format_addr(Python爱好者 %s % from_addr)
msg[To] _format_addr(管理员 %s % to_addr)
msg[Subject] Header(来自SMTP的问候……, utf-8).encode()# 邮件正文是MIMEText:
msg.attach(MIMEText(send with file..., plain, utf-8))# 添加附件就是加上一个MIMEBase从本地读取一个图片:
with open(/Users/michael/Downloads/test.png, rb) as f:# 设置附件的MIME和文件名这里是png类型:mime MIMEBase(image, png, filenametest.png)# 加上必要的头信息:mime.add_header(Content-Disposition, attachment, filenametest.png)mime.add_header(Content-ID, 0)mime.add_header(X-Attachment-Id, 0)# 把附件的内容读进来:mime.set_payload(f.read())# 用Base64编码:encoders.encode_base64(mime)# 添加到MIMEMultipart:msg.attach(mime)然后按正常发送流程把msg注意类型已变为MIMEMultipart发送出去就可以收到如下带附件的邮件 发送图片
如果要把一个图片嵌入到邮件正文中怎么做直接在HTML邮件中链接图片地址行不行答案是大部分邮件服务商都会自动屏蔽带有外链的图片因为不知道这些链接是否指向恶意网站。
要把图片嵌入到邮件正文中我们只需按照发送附件的方式先把邮件作为附件添加进去然后在HTML中通过引用srccid:0就可以把附件作为图片嵌入了。如果有多个图片给它们依次编号然后引用不同的cid:x即可。
把上面代码加入MIMEMultipart的MIMEText从plain改为html然后在适当的位置引用图片
msg.attach(MIMEText(htmlbodyh1Hello/h1 pimg srccid:0/p /body/html, html, utf-8))再次发送就可以看到图片直接嵌入到邮件正文的效果 同时支持HTML和Plain格式
如果我们发送HTML邮件收件人通过浏览器或者Outlook之类的软件是可以正常浏览邮件内容的但是如果收件人使用的设备太古老查看不了HTML邮件怎么办
办法是在发送HTML的同时再附加一个纯文本如果收件人无法查看HTML格式的邮件就可以自动降级查看纯文本邮件。
利用MIMEMultipart就可以组合一个HTML和Plain要注意指定subtype是alternative
msg MIMEMultipart(alternative)
msg[From] ...
msg[To] ...
msg[Subject] ...msg.attach(MIMEText(hello, plain, utf-8))
msg.attach(MIMEText(htmlbodyh1Hello/h1/body/html, html, utf-8))
# 正常发送msg对象...加密SMTP
使用标准的25端口连接SMTP服务器时使用的是明文传输发送邮件的整个过程可能会被窃听。要更安全地发送邮件可以加密SMTP会话实际上就是先创建SSL安全连接然后再使用SMTP协议发送邮件。
某些邮件服务商例如Gmail提供的SMTP服务必须要加密传输。我们来看看如何通过Gmail提供的安全SMTP发送邮件。
必须知道Gmail的SMTP端口是587因此修改代码如下
smtp_server smtp.gmail.com
smtp_port 587
server smtplib.SMTP(smtp_server, smtp_port)
server.starttls()
# 剩下的代码和前面的一模一样:
server.set_debuglevel(1)
...只需要在创建SMTP对象后立刻调用starttls()方法就创建了安全连接。后面的代码和前面的发送邮件代码完全一样。
如果因为网络问题无法连接Gmail的SMTP服务器请相信我们的代码是没有问题的你需要对你的网络设置做必要的调整。
小结
使用Python的smtplib发送邮件十分简单只要掌握了各种邮件类型的构造方法正确设置好邮件头就可以顺利发出。
构造一个邮件对象就是一个Messag对象如果构造一个MIMEText对象就表示一个文本邮件对象如果构造一个MIMEImage对象就表示一个作为附件的图片要把多个对象组合起来就用MIMEMultipart对象而MIMEBase可以表示任何对象。它们的继承关系如下
Message
- MIMEBase- MIMEMultipart- MIMENonMultipart- MIMEMessage- MIMEText- MIMEImage这种嵌套关系就可以构造出任意复杂的邮件。你可以通过email.mime文档查看它们所在的包以及详细的用法。
参考源码
send_mail.py
POP3收取邮件
SMTP用于发送邮件如果要收取邮件呢
收取邮件就是编写一个MUA作为客户端从MDA把邮件获取到用户的电脑或者手机上。收取邮件最常用的协议是POP协议目前版本号是3俗称POP3。
Python内置一个poplib模块实现了POP3协议可以直接用来收邮件。
注意到POP3协议收取的不是一个已经可以阅读的邮件本身而是邮件的原始文本这和SMTP协议很像SMTP发送的也是经过编码后的一大段文本。
要把POP3收取的文本变成可以阅读的邮件还需要用email模块提供的各种类来解析原始文本变成可阅读的邮件对象。
所以收取邮件分两步
第一步用poplib把邮件的原始文本下载到本地
第二部用email解析原始文本还原为邮件对象。
通过POP3下载邮件
POP3协议本身很简单以下面的代码为例我们来获取最新的一封邮件内容
import poplib# 输入邮件地址, 口令和POP3服务器地址:
email input(Email: )
password input(Password: )
pop3_server input(POP3 server: )# 连接到POP3服务器:
server poplib.POP3(pop3_server)
# 可以打开或关闭调试信息:
server.set_debuglevel(1)
# 可选:打印POP3服务器的欢迎文字:
print(server.getwelcome().decode(utf-8))# 身份认证:
server.user(email)
server.pass_(password)# stat()返回邮件数量和占用空间:
print(Messages: %s. Size: %s % server.stat())
# list()返回所有邮件的编号:
resp, mails, octets server.list()
# 可以查看返回的列表类似[b1 82923, b2 2184, ...]
print(mails)# 获取最新一封邮件, 注意索引号从1开始:
index len(mails)
resp, lines, octets server.retr(index)# lines存储了邮件的原始文本的每一行,
# 可以获得整个邮件的原始文本:
msg_content b\r\n.join(lines).decode(utf-8)
# 稍后解析出邮件:
msg Parser().parsestr(msg_content)# 可以根据邮件索引号直接从服务器删除邮件:
# server.dele(index)
# 关闭连接:
server.quit()用POP3获取邮件其实很简单要获取所有邮件只需要循环使用retr()把每一封邮件内容拿到即可。真正麻烦的是把邮件的原始内容解析为可以阅读的邮件对象。
解析邮件
解析邮件的过程和上一节构造邮件正好相反因此先导入必要的模块
from email.parser import Parser
from email.header import decode_header
from email.utils import parseaddrimport poplib只需要一行代码就可以把邮件内容解析为Message对象
msg Parser().parsestr(msg_content)但是这个Message对象本身可能是一个MIMEMultipart对象即包含嵌套的其他MIMEBase对象嵌套可能还不止一层。
所以我们要递归地打印出Message对象的层次结构
# indent用于缩进显示:
def print_info(msg, indent0):if indent 0:for header in [From, To, Subject]:value msg.get(header, )if value:if headerSubject:value decode_str(value)else:hdr, addr parseaddr(value)name decode_str(hdr)value u%s %s % (name, addr)print(%s%s: %s % ( * indent, header, value))if (msg.is_multipart()):parts msg.get_payload()for n, part in enumerate(parts):print(%spart %s % ( * indent, n))print(%s-------------------- % ( * indent))print_info(part, indent 1)else:content_type msg.get_content_type()if content_typetext/plain or content_typetext/html:content msg.get_payload(decodeTrue)charset guess_charset(msg)if charset:content content.decode(charset)print(%sText: %s % ( * indent, content ...))else:print(%sAttachment: %s % ( * indent, content_type))邮件的Subject或者Email中包含的名字都是经过编码后的str要正常显示就必须decode
def decode_str(s):value, charset decode_header(s)[0]if charset:value value.decode(charset)return valuedecode_header()返回一个list因为像Cc、Bcc这样的字段可能包含多个邮件地址所以解析出来的会有多个元素。上面的代码我们偷了个懒只取了第一个元素。
文本邮件的内容也是str还需要检测编码否则非UTF-8编码的邮件都无法正常显示
def guess_charset(msg):charset msg.get_charset()if charset is None:content_type msg.get(Content-Type, ).lower()pos content_type.find(charset)if pos 0:charset content_type[pos 8:].strip()return charset把上面的代码整理好我们就可以来试试收取一封邮件。先往自己的邮箱发一封邮件然后用浏览器登录邮箱看看邮件收到没如果收到了我们就来用Python程序把它收到本地 运行程序结果如下
OK Welcome to coremail Mail Pop3 Server (163coms[...])
Messages: 126. Size: 27228317From: Test xxxxxxqq.com
To: Python爱好者 xxxxxx163.com
Subject: 用POP3收取邮件
part 0
--------------------part 0--------------------Text: Python可以使用POP3收取邮件……...part 1--------------------Text: Python可以a href...使用POP3/a收取邮件……...
part 1
--------------------Attachment: application/octet-stream我们从打印的结构可以看出这封邮件是一个MIMEMultipart它包含两部分第一部分又是一个MIMEMultipart第二部分是一个附件。而内嵌的MIMEMultipart是一个alternative类型它包含一个纯文本格式的MIMEText和一个HTML格式的MIMEText。
小结
用Python的poplib模块收取邮件分两步第一步是用POP3协议把邮件获取到本地第二步是用email模块把原始邮件解析为Message对象然后用适当的形式把邮件内容展示给用户即可。
参考源码
fetch_mail.py
访问数据库
程序运行的时候数据都是在内存中的。当程序终止的时候通常都需要将数据保存到磁盘上无论是保存到本地磁盘还是通过网络保存到服务器上最终都会将数据写入磁盘文件。
而如何定义数据的存储格式就是一个大问题。如果我们自己来定义存储格式比如保存一个班级所有学生的成绩单
名字成绩Michael99Bob85Bart59Lisa87
你可以用一个文本文件保存一行保存一个学生用,隔开
Michael,99
Bob,85
Bart,59
Lisa,87你还可以用JSON格式保存也是文本文件
[{name:Michael,score:99},{name:Bob,score:85},{name:Bart,score:59},{name:Lisa,score:87}
]你还可以定义各种保存格式但是问题来了
存储和读取需要自己实现JSON还是标准自己定义的格式就各式各样了
不能做快速查询只有把数据全部读到内存中才能自己遍历但有时候数据的大小远远超过了内存比如蓝光电影40GB的数据根本无法全部读入内存。
为了便于程序保存和读取数据而且能直接通过条件快速查询到指定的数据就出现了数据库Database这种专门用于集中存储和查询的软件。
数据库软件诞生的历史非常久远早在1950年数据库就诞生了。经历了网状数据库层次数据库我们现在广泛使用的关系数据库是20世纪70年代基于关系模型的基础上诞生的。
关系模型有一套复杂的数学理论但是从概念上是十分容易理解的。举个学校的例子
假设某个XX省YY市ZZ县第一实验小学有3个年级要表示出这3个年级可以在Excel中用一个表格画出来 每个年级又有若干个班级要把所有班级表示出来可以在Excel中再画一个表格 这两个表格有个映射关系就是根据Grade_ID可以在班级表中查找到对应的所有班级 也就是Grade表的每一行对应Class表的多行在关系数据库中这种基于表Table的一对多的关系就是关系数据库的基础。
根据某个年级的ID就可以查找所有班级的行这种查询语句在关系数据库中称为SQL语句可以写成
SELECT * FROM classes WHERE grade_id 1;结果也是一个表
-----------------------------
grade_id | class_id | name
-----------------------------
1 | 11 | 一年级一班
-----------------------------
1 | 12 | 一年级二班
-----------------------------
1 | 13 | 一年级三班
-----------------------------类似的Class表的一行记录又可以关联到Student表的多行记录 由于本教程不涉及到关系数据库的详细内容如果你想从零学习关系数据库和基本的SQL语句推荐Coursera课程
英文https://www.coursera.org/course/db
中文http://c.open.163.com/coursera/courseIntro.htm?cid12
NoSQL
你也许还听说过NoSQL数据库很多NoSQL宣传其速度和规模远远超过关系数据库所以很多同学觉得有了NoSQL是否就不需要SQL了呢千万不要被他们忽悠了连SQL都不明白怎么可能搞明白NoSQL呢
数据库类别
既然我们要使用关系数据库就必须选择一个关系数据库。目前广泛使用的关系数据库也就这么几种
付费的商用数据库 Oracle典型的高富帅 SQL Server微软自家产品Windows定制专款 DB2IBM的产品听起来挺高端 Sybase曾经跟微软是好基友后来关系破裂现在家境惨淡。
这些数据库都是不开源而且付费的最大的好处是花了钱出了问题可以找厂家解决不过在Web的世界里常常需要部署成千上万的数据库服务器当然不能把大把大把的银子扔给厂家所以无论是Google、Facebook还是国内的BAT无一例外都选择了免费的开源数据库 MySQL大家都在用一般错不了 PostgreSQL学术气息有点重其实挺不错但知名度没有MySQL高 sqlite嵌入式数据库适合桌面和移动应用。
作为Python开发工程师选择哪个免费数据库呢当然是MySQL。因为MySQL普及率最高出了错可以很容易找到解决方法。而且围绕MySQL有一大堆监控和运维的工具安装和使用很方便。
为了能继续后面的学习你需要从MySQL官方网站下载并安装MySQL Community Server 5.6这个版本是免费的其他高级版本是要收钱的请放心收钱的功能我们用不上。
使用SQLite
SQLite是一种嵌入式数据库它的数据库就是一个文件。由于SQLite本身是C写的而且体积很小所以经常被集成到各种应用程序中甚至在iOS和Android的App中都可以集成。
Python就内置了SQLite3所以在Python中使用SQLite不需要安装任何东西直接使用。
在使用SQLite前我们先要搞清楚几个概念
表是数据库中存放关系数据的集合一个数据库里面通常都包含多个表比如学生的表班级的表学校的表等等。表和表之间通过外键关联。
要操作关系数据库首先需要连接到数据库一个数据库连接称为Connection
连接到数据库后需要打开游标称之为Cursor通过Cursor执行SQL语句然后获得执行结果。
Python定义了一套操作数据库的API接口任何数据库要连接到Python只需要提供符合Python标准的数据库驱动即可。
由于SQLite的驱动内置在Python标准库中所以我们可以直接来操作SQLite数据库。
我们在Python交互式命令行实践一下
# 导入SQLite驱动:import sqlite3
# 连接到SQLite数据库
# 数据库文件是test.db
# 如果文件不存在会自动在当前目录创建:conn sqlite3.connect(test.db)
# 创建一个Cursor:cursor conn.cursor()
# 执行一条SQL语句创建user表:cursor.execute(create table user (id varchar(20) primary key, name varchar(20)))
sqlite3.Cursor object at 0x10f8aa260
# 继续执行一条SQL语句插入一条记录:cursor.execute(insert into user (id, name) values (\1\, \Michael\))
sqlite3.Cursor object at 0x10f8aa260
# 通过rowcount获得插入的行数:cursor.rowcount
1
# 关闭Cursor:cursor.close()
# 提交事务:conn.commit()
# 关闭Connection:conn.close()我们再试试查询记录 conn sqlite3.connect(test.db)cursor conn.cursor()
# 执行查询语句:cursor.execute(select * from user where id?, 1)
sqlite3.Cursor object at 0x10f8aa340
# 获得查询结果集:values cursor.fetchall()values
[(1, Michael)]cursor.close()conn.close()使用Python的DB-API时只要搞清楚Connection和Cursor对象打开后一定记得关闭就可以放心地使用。
使用Cursor对象执行insertupdatedelete语句时执行结果由rowcount返回影响的行数就可以拿到执行结果。
使用Cursor对象执行select语句时通过featchall()可以拿到结果集。结果集是一个list每个元素都是一个tuple对应一行记录。
如果SQL语句带有参数那么需要把参数按照位置传递给execute()方法有几个?占位符就必须对应几个参数例如
cursor.execute(select * from user where id?, 1)SQLite支持常见的标准SQL语句以及几种常见的数据类型。具体文档请参阅SQLite官方网站。
小结
在Python中操作数据库时要先导入数据库对应的驱动然后通过Connection对象和Cursor对象操作数据。
要确保打开的Connection对象和Cursor对象都正确地被关闭否则资源就会泄露。
如何才能确保出错的情况下也关闭掉Connection对象和Cursor对象呢请回忆try:...except:...finally:...的用法。
练习
请编写函数在Sqlite中根据分数段查找指定的名字
# -*- coding: utf-8 -*-import os, sqlite3db_file os.path.join(os.path.dirname(__file__), test.db)
if os.path.isfile(db_file):os.remove(db_file)# 初始数据:
conn sqlite3.connect(db_file)
cursor conn.cursor()
cursor.execute(create table user(id varchar(20) primary key, name varchar(20), score int))
cursor.execute(rinsert into user values (A-001, Adam, 95))
cursor.execute(rinsert into user values (A-002, Bart, 62))
cursor.execute(rinsert into user values (A-003, Lisa, 78))
cursor.close()
conn.commit()
conn.close()def get_score_in(low, high): 返回指定分数区间的名字按分数从低到高排序
----pass
----
# 测试:
assert get_score_in(80, 95) [Adam], get_score_in(80, 95)
assert get_score_in(60, 80) [Bart, Lisa], get_score_in(60, 80)
assert get_score_in(60, 100) [Bart, Lisa, Adam], get_score_in(60, 100)print(Pass)参考源码
do_sqlite.py
使用MySQL
MySQL是Web世界中使用最广泛的数据库服务器。SQLite的特点是轻量级、可嵌入但不能承受高并发访问适合桌面和移动应用。而MySQL是为服务器端设计的数据库能承受高并发访问同时占用的内存也远远大于SQLite。
此外MySQL内部有多种数据库引擎最常用的引擎是支持数据库事务的InnoDB。
安装MySQL
可以直接从MySQL官方网站下载最新的Community Server 5.6.x版本。MySQL是跨平台的选择对应的平台下载安装文件安装即可。
安装时MySQL会提示输入root用户的口令请务必记清楚。如果怕记不住就把口令设置为password。
在Windows上安装时请选择UTF-8编码以便正确地处理中文。
在Mac或Linux上需要编辑MySQL的配置文件把数据库默认的编码全部改为UTF-8。MySQL的配置文件默认存放在/etc/my.cnf或者/etc/mysql/my.cnf
[client]
default-character-set utf8[mysqld]
default-storage-engine INNODB
character-set-server utf8
collation-server utf8_general_ci重启MySQL后可以通过MySQL的客户端命令行检查编码
$ mysql -u root -p
Enter password:
Welcome to the MySQL monitor...
...mysql show variables like %char%;
----------------------------------------------------------------------------------
| Variable_name | Value |
----------------------------------------------------------------------------------
| character_set_client | utf8 |
| character_set_connection | utf8 |
| character_set_database | utf8 |
| character_set_filesystem | binary |
| character_set_results | utf8 |
| character_set_server | utf8 |
| character_set_system | utf8 |
| character_sets_dir | /usr/local/mysql-5.1.65-osx10.6-x86_64/share/charsets/ |
----------------------------------------------------------------------------------
8 rows in set (0.00 sec)看到utf8字样就表示编码设置正确。
安装MySQL驱动
由于MySQL服务器以独立的进程运行并通过网络对外服务所以需要支持Python的MySQL驱动来连接到MySQL服务器。MySQL官方提供了mysql-connector-python驱动但是安装的时候需要给pip命令加上参数--allow-external
$ pip install mysql-connector-python --allow-external mysql-connector-python我们演示如何连接到MySQL服务器的test数据库
# 导入MySQL驱动:import mysql.connector
# 注意把password设为你的root口令:conn mysql.connector.connect(userroot, passwordpassword, databasetest)cursor conn.cursor()
# 创建user表:cursor.execute(create table user (id varchar(20) primary key, name varchar(20)))
# 插入一行记录注意MySQL的占位符是%s:cursor.execute(insert into user (id, name) values (%s, %s), [1, Michael])cursor.rowcount
1
# 提交事务:conn.commit()cursor.close()
# 运行查询:cursor conn.cursor()cursor.execute(select * from user where id %s, [1])values cursor.fetchall()values
[(1, Michael)]
# 关闭Cursor和Connection:cursor.close()
Trueconn.close()由于Python的DB-API定义都是通用的所以操作MySQL的数据库代码和SQLite类似。
小结 执行INSERT等操作后要调用commit()提交事务 MySQL的SQL占位符是%s。
参考源码
do_mysql.py
使用SQLAlchemy
数据库表是一个二维表包含多行多列。把一个表的内容用Python的数据结构表示出来的话可以用一个list表示多行list的每一个元素是tuple表示一行记录比如包含id和name的user表
[(1, Michael),(2, Bob),(3, Adam)
]Python的DB-API返回的数据结构就是像上面这样表示的。
但是用tuple表示一行很难看出表的结构。如果把一个tuple用class实例来表示就可以更容易地看出表的结构来
class User(object):def __init__(self, id, name):self.id idself.name name[User(1, Michael),User(2, Bob),User(3, Adam)
]这就是传说中的ORM技术Object-Relational Mapping把关系数据库的表结构映射到对象上。是不是很简单
但是由谁来做这个转换呢所以ORM框架应运而生。
在Python中最有名的ORM框架是SQLAlchemy。我们来看看SQLAlchemy的用法。
首先通过pip安装SQLAlchemy
$ pip install sqlalchemy然后利用上次我们在MySQL的test数据库中创建的user表用SQLAlchemy来试试
第一步导入SQLAlchemy并初始化DBSession
# 导入:
from sqlalchemy import Column, String, create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base# 创建对象的基类:
Base declarative_base()# 定义User对象:
class User(Base):# 表的名字:__tablename__ user# 表的结构:id Column(String(20), primary_keyTrue)name Column(String(20))# 初始化数据库连接:
engine create_engine(mysqlmysqlconnector://root:passwordlocalhost:3306/test)
# 创建DBSession类型:
DBSession sessionmaker(bindengine)以上代码完成SQLAlchemy的初始化和具体每个表的class定义。如果有多个表就继续定义其他class例如School
class School(Base):__tablename__ schoolid ...name ...create_engine()用来初始化数据库连接。SQLAlchemy用一个字符串表示连接信息
数据库类型数据库驱动名称://用户名:口令机器地址:端口号/数据库名你只需要根据需要替换掉用户名、口令等信息即可。
下面我们看看如何向数据库表中添加一行记录。
由于有了ORM我们向数据库表中添加一行记录可以视为添加一个User对象
# 创建session对象:
session DBSession()
# 创建新User对象:
new_user User(id5, nameBob)
# 添加到session:
session.add(new_user)
# 提交即保存到数据库:
session.commit()
# 关闭session:
session.close()可见关键是获取session然后把对象添加到session最后提交并关闭。DBSession对象可视为当前数据库连接。
如何从数据库表中查询数据呢有了ORM查询出来的可以不再是tuple而是User对象。SQLAlchemy提供的查询接口如下
# 创建Session:
session DBSession()
# 创建Query查询filter是where条件最后调用one()返回唯一行如果调用all()则返回所有行:
user session.query(User).filter(User.id5).one()
# 打印类型和对象的name属性:
print(type:, type(user))
print(name:, user.name)
# 关闭Session:
session.close()运行结果如下
type: class __main__.User
name: Bob可见ORM就是把数据库表的行与相应的对象建立关联互相转换。
由于关系数据库的多个表还可以用外键实现一对多、多对多等关联相应地ORM框架也可以提供两个对象之间的一对多、多对多等功能。
例如如果一个User拥有多个Book就可以定义一对多关系如下
class User(Base):__tablename__ userid Column(String(20), primary_keyTrue)name Column(String(20))# 一对多:books relationship(Book)class Book(Base):__tablename__ bookid Column(String(20), primary_keyTrue)name Column(String(20))# “多”的一方的book表是通过外键关联到user表的:user_id Column(String(20), ForeignKey(user.id))当我们查询一个User对象时该对象的books属性将返回一个包含若干个Book对象的list。
小结
ORM框架的作用就是把数据库表的一行记录与一个对象互相做自动转换。
正确使用ORM的前提是了解关系数据库的原理。
参考源码
do_sqlalchemy.py
Web开发
最早的软件都是运行在大型机上的软件使用者通过“哑终端”登陆到大型机上去运行软件。后来随着PC机的兴起软件开始主要运行在桌面上而数据库这样的软件运行在服务器端这种Client/Server模式简称CS架构。
随着互联网的兴起人们发现CS架构不适合Web最大的原因是Web应用程序的修改和升级非常迅速而CS架构需要每个客户端逐个升级桌面App因此Browser/Server模式开始流行简称BS架构。
在BS架构下客户端只需要浏览器应用程序的逻辑和数据都存储在服务器端。浏览器只需要请求服务器获取Web页面并把Web页面展示给用户即可。
当然Web页面也具有极强的交互性。由于Web页面是用HTML编写的而HTML具备超强的表现力并且服务器端升级后客户端无需任何部署就可以使用到新的版本因此BS架构迅速流行起来。
今天除了重量级的软件如OfficePhotoshop等大部分软件都以Web形式提供。比如新浪提供的新闻、博客、微博等服务均是Web应用。
Web应用开发可以说是目前软件开发中最重要的部分。Web开发也经历了好几个阶段 静态Web页面由文本编辑器直接编辑并生成静态的HTML页面如果要修改Web页面的内容就需要再次编辑HTML源文件早期的互联网Web页面就是静态的 CGI由于静态Web页面无法与用户交互比如用户填写了一个注册表单静态Web页面就无法处理。要处理用户发送的动态数据出现了Common Gateway Interface简称CGI用C/C编写。 ASP/JSP/PHP由于Web应用特点是修改频繁用C/C这样的低级语言非常不适合Web开发而脚本语言由于开发效率高与HTML结合紧密因此迅速取代了CGI模式。ASP是微软推出的用VBScript脚本编程的Web开发技术而JSP用Java来编写脚本PHP本身则是开源的脚本语言。 MVC为了解决直接用脚本语言嵌入HTML导致的可维护性差的问题Web应用也引入了Model-View-Controller的模式来简化Web开发。ASP发展为ASP.NetJSP和PHP也有一大堆MVC框架。
目前Web开发技术仍在快速发展中异步开发、新的MVVM前端技术层出不穷。
Python的诞生历史比Web还要早由于Python是一种解释型的脚本语言开发效率高所以非常适合用来做Web开发。
Python有上百种Web开发框架有很多成熟的模板技术选择Python开发Web应用不但开发效率高而且运行速度快。
本章我们会详细讨论Python Web开发技术。
HTTP协议简介
在Web应用中服务器把网页传给浏览器实际上就是把网页的HTML代码发送给浏览器让浏览器显示出来。而浏览器和服务器之间的传输协议是HTTP所以 HTML是一种用来定义网页的文本会HTML就可以编写网页 HTTP是在网络上传输HTML的协议用于浏览器和服务器的通信。
在举例子之前我们需要安装Google的Chrome浏览器。
为什么要使用Chrome浏览器而不是IE呢因为IE实在是太慢了并且IE对于开发和调试Web应用程序完全是一点用也没有。
我们需要在浏览器很方便地调试我们的Web应用而Chrome提供了一套完整地调试工具非常适合Web开发。
安装好Chrome浏览器后打开Chrome在菜单中选择“视图”“开发者”“开发者工具”就可以显示开发者工具 Elements显示网页的结构Network显示浏览器和服务器的通信。我们点Network确保第一个小红灯亮着Chrome就会记录所有浏览器和服务器之间的通信 当我们在地址栏输入www.sina.com.cn时浏览器将显示新浪的首页。在这个过程中浏览器都干了哪些事情呢通过Network的记录我们就可以知道。在Network中定位到第一条记录点击右侧将显示Request Headers点击右侧的view source我们就可以看到浏览器发给新浪服务器的请求 最主要的头两行分析如下第一行
GET / HTTP/1.1GET表示一个读取请求将从服务器获得网页数据/表示URL的路径URL总是以/开头/就表示首页最后的HTTP/1.1指示采用的HTTP协议版本是1.1。目前HTTP协议的版本就是1.1但是大部分服务器也支持1.0版本主要区别在于1.1版本允许多个HTTP请求复用一个TCP连接以加快传输速度。
从第二行开始每一行都类似于Xxx: abcdefg
Host: www.sina.com.cn表示请求的域名是www.sina.com.cn。如果一台服务器有多个网站服务器就需要通过Host来区分浏览器请求的是哪个网站。
继续往下找到Response Headers点击view source显示服务器返回的原始响应数据 HTTP响应分为Header和Body两部分Body是可选项我们在Network中看到的Header最重要的几行如下
200 OK200表示一个成功的响应后面的OK是说明。失败的响应有404 Not Found网页不存在500 Internal Server Error服务器内部出错等等。
Content-Type: text/htmlContent-Type指示响应的内容这里是text/html表示HTML网页。请注意浏览器就是依靠Content-Type来判断响应的内容是网页还是图片是视频还是音乐。浏览器并不靠URL来判断响应的内容所以即使URL是http://example.com/abc.jpg它也不一定就是图片。
HTTP响应的Body就是HTML源码我们在菜单栏选择“视图”“开发者”“查看网页源码”就可以在浏览器中直接查看HTML源码 当浏览器读取到新浪首页的HTML源码后它会解析HTML显示页面然后根据HTML里面的各种链接再发送HTTP请求给新浪服务器拿到相应的图片、视频、Flash、JavaScript脚本、CSS等各种资源最终显示出一个完整的页面。所以我们在Network下面能看到很多额外的HTTP请求。
HTTP请求
跟踪了新浪的首页我们来总结一下HTTP请求的流程
步骤1浏览器首先向服务器发送HTTP请求请求包括
方法GET还是POSTGET仅请求资源POST会附带用户数据
路径/full/url/path
域名由Host头指定Host: www.sina.com.cn
以及其他相关的Header
如果是POST那么请求还包括一个Body包含用户数据。
步骤2服务器向浏览器返回HTTP响应响应包括
响应代码200表示成功3xx表示重定向4xx表示客户端发送的请求有错误5xx表示服务器端处理时发生了错误
响应类型由Content-Type指定
以及其他相关的Header
通常服务器的HTTP响应会携带内容也就是有一个Body包含响应的内容网页的HTML源码就在Body中。
步骤3如果浏览器还需要继续向服务器请求其他资源比如图片就再次发出HTTP请求重复步骤1、2。
Web采用的HTTP协议采用了非常简单的请求-响应模式从而大大简化了开发。当我们编写一个页面时我们只需要在HTTP请求中把HTML发送出去不需要考虑如何附带图片、视频等浏览器如果需要请求图片和视频它会发送另一个HTTP请求因此一个HTTP请求只处理一个资源。
HTTP协议同时具备极强的扩展性虽然浏览器请求的是http://www.sina.com.cn/的首页但是新浪在HTML中可以链入其他服务器的资源比如img srchttp://i1.sinaimg.cn/home/2013/1008/U8455P30DT20131008135420.png从而将请求压力分散到各个服务器上并且一个站点可以链接到其他站点无数个站点互相链接起来就形成了World Wide Web简称WWW。
HTTP格式
每个HTTP请求和响应都遵循相同的格式一个HTTP包含Header和Body两部分其中Body是可选的。
HTTP协议是一种文本协议所以它的格式也非常简单。HTTP GET请求的格式
GET /path HTTP/1.1
Header1: Value1
Header2: Value2
Header3: Value3每个Header一行一个换行符是\r\n。
HTTP POST请求的格式
POST /path HTTP/1.1
Header1: Value1
Header2: Value2
Header3: Value3body data goes here...当遇到连续两个\r\n时Header部分结束后面的数据全部是Body。
HTTP响应的格式
200 OK
Header1: Value1
Header2: Value2
Header3: Value3body data goes here...HTTP响应如果包含body也是通过\r\n\r\n来分隔的。请再次注意Body的数据类型由Content-Type头来确定如果是网页Body就是文本如果是图片Body就是图片的二进制数据。
当存在Content-Encoding时Body数据是被压缩的最常见的压缩方式是gzip所以看到Content-Encoding: gzip时需要将Body数据先解压缩才能得到真正的数据。压缩的目的在于减少Body的大小加快网络传输。
要详细了解HTTP协议推荐“HTTP: The Definitive Guide”一书非常不错有中文译本
HTTP权威指南
HTML简介
网页就是HTML这么理解大概没错。因为网页中不但包含文字还有图片、视频、Flash小游戏有复杂的排版、动画效果所以HTML定义了一套语法规则来告诉浏览器如何把一个丰富多彩的页面显示出来。
HTML长什么样上次我们看了新浪首页的HTML源码如果仔细数数竟然有6000多行
所以学HTML就不要指望从新浪入手了。我们来看看最简单的HTML长什么样
html
headtitleHello/title
/head
bodyh1Hello, world!/h1
/body
/html可以用文本编辑器编写HTML然后保存为hello.html双击或者把文件拖到浏览器中就可以看到效果 HTML文档就是一系列的Tag组成最外层的Tag是html。规范的HTML也包含head.../head和body.../body注意不要和HTTP的Header、Body搞混了由于HTML是富文档模型所以还有一系列的Tag用来表示链接、图片、表格、表单等等。
CSS简介
CSS是Cascading Style Sheets层叠样式表的简称CSS用来控制HTML里的所有元素如何展现比如给标题元素h1加一个样式变成48号字体灰色带阴影
html
headtitleHello/titlestyleh1 {color: #333333;font-size: 48px;text-shadow: 3px 3px 3px #666666;}/style
/head
bodyh1Hello, world!/h1
/body
/html效果如下 JavaScript简介
JavaScript虽然名称有个Java但它和Java真的一点关系没有。JavaScript是为了让HTML具有交互性而作为脚本语言添加的JavaScript既可以内嵌到HTML中也可以从外部链接到HTML中。如果我们希望当用户点击标题时把标题变成红色就必须通过JavaScript来实现
html
headtitleHello/titlestyleh1 {color: #333333;font-size: 48px;text-shadow: 3px 3px 3px #666666;}/stylescriptfunction change() {document.getElementsByTagName(h1)[0].style.color #ff0000;}/script
/head
bodyh1 onclickchange()Hello, world!/h1
/body
/html点击标题后效果如下 小结
如果要学习Web开发首先要对HTML、CSS和JavaScript作一定的了解。HTML定义了页面的内容CSS来控制页面元素的样式而JavaScript负责页面的交互逻辑。
讲解HTML、CSS和JavaScript就可以写3本书对于优秀的Web开发人员来说精通HTML、CSS和JavaScript是必须的这里推荐一个在线学习网站w3schools
http://www.w3schools.com/
以及一个对应的中文版本
http://www.w3school.com.cn/
当我们用Python或者其他语言开发Web应用时我们就是要在服务器端动态创建出HTML这样浏览器就会向不同的用户显示出不同的Web页面。
WSGI接口
了解了HTTP协议和HTML文档我们其实就明白了一个Web应用的本质就是 浏览器发送一个HTTP请求 服务器收到请求生成一个HTML文档 服务器把HTML文档作为HTTP响应的Body发送给浏览器 浏览器收到HTTP响应从HTTP Body取出HTML文档并显示。
所以最简单的Web应用就是先把HTML用文件保存好用一个现成的HTTP服务器软件接收用户请求从文件中读取HTML返回。Apache、Nginx、Lighttpd等这些常见的静态服务器就是干这件事情的。
如果要动态生成HTML就需要把上述步骤自己来实现。不过接受HTTP请求、解析HTTP请求、发送HTTP响应都是苦力活如果我们自己来写这些底层代码还没开始写动态HTML呢就得花个把月去读HTTP规范。
正确的做法是底层代码由专门的服务器软件实现我们用Python专注于生成HTML文档。因为我们不希望接触到TCP连接、HTTP原始请求和响应格式所以需要一个统一的接口让我们专心用Python编写Web业务。
这个接口就是WSGIWeb Server Gateway Interface。
WSGI接口定义非常简单它只要求Web开发者实现一个函数就可以响应HTTP请求。我们来看一个最简单的Web版本的“Hello, web!”
def application(environ, start_response):start_response(200 OK, [(Content-Type, text/html)])return [bh1Hello, web!/h1]上面的application()函数就是符合WSGI标准的一个HTTP处理函数它接收两个参数 environ一个包含所有HTTP请求信息的dict对象 start_response一个发送HTTP响应的函数。
在application()函数中调用
start_response(200 OK, [(Content-Type, text/html)])就发送了HTTP响应的Header注意Header只能发送一次也就是只能调用一次start_response()函数。start_response()函数接收两个参数一个是HTTP响应码一个是一组list表示的HTTP Header每个Header用一个包含两个str的tuple表示。
通常情况下都应该把Content-Type头发送给浏览器。其他很多常用的HTTP Header也应该发送。
然后函数的返回值bh1Hello, web!/h1将作为HTTP响应的Body发送给浏览器。
有了WSGI我们关心的就是如何从environ这个dict对象拿到HTTP请求信息然后构造HTML通过start_response()发送Header最后返回Body。
整个application()函数本身没有涉及到任何解析HTTP的部分也就是说底层代码不需要我们自己编写我们只负责在更高层次上考虑如何响应请求就可以了。
不过等等这个application()函数怎么调用如果我们自己调用两个参数environ和start_response我们没法提供返回的bytes也没法发给浏览器。
所以application()函数必须由WSGI服务器来调用。有很多符合WSGI规范的服务器我们可以挑选一个来用。但是现在我们只想尽快测试一下我们编写的application()函数真的可以把HTML输出到浏览器所以要赶紧找一个最简单的WSGI服务器把我们的Web应用程序跑起来。
好消息是Python内置了一个WSGI服务器这个模块叫wsgiref它是用纯Python编写的WSGI服务器的参考实现。所谓“参考实现”是指该实现完全符合WSGI标准但是不考虑任何运行效率仅供开发和测试使用。
运行WSGI服务
我们先编写hello.py实现Web应用程序的WSGI处理函数
# hello.pydef application(environ, start_response):start_response(200 OK, [(Content-Type, text/html)])return [bh1Hello, web!/h1]然后再编写一个server.py负责启动WSGI服务器加载application()函数
# server.py
# 从wsgiref模块导入:
from wsgiref.simple_server import make_server
# 导入我们自己编写的application函数:
from hello import application# 创建一个服务器IP地址为空端口是8000处理函数是application:
httpd make_server(, 8000, application)
print(Serving HTTP on port 8000...)
# 开始监听HTTP请求:
httpd.serve_forever()确保以上两个文件在同一个目录下然后在命令行输入python server.py来启动WSGI服务器 注意如果8000端口已被其他程序占用启动将失败请修改成其他端口。
启动成功后打开浏览器输入http://localhost:8000/就可以看到结果了 在命令行可以看到wsgiref打印的log信息 按CtrlC终止服务器。
如果你觉得这个Web应用太简单了可以稍微改造一下从environ里读取PATH_INFO这样可以显示更加动态的内容
# hello.pydef application(environ, start_response):start_response(200 OK, [(Content-Type, text/html)])body h1Hello, %s!/h1 % (environ[PATH_INFO][1:] or web)return [body.encode(utf-8)]你可以在地址栏输入用户名作为URL的一部分将返回Hello, xxx! 是不是有点Web App的感觉了
小结
无论多么复杂的Web应用程序入口都是一个WSGI处理函数。HTTP请求的所有输入信息都可以通过environ获得HTTP响应的输出都可以通过start_response()加上函数返回值作为Body。
复杂的Web应用程序光靠一个WSGI函数来处理还是太底层了我们需要在WSGI之上再抽象出Web框架进一步简化Web开发。
参考源码
hello.py
do_wsgi.py
使用Web框架
了解了WSGI框架我们发现其实一个Web App就是写一个WSGI的处理函数针对每个HTTP请求进行响应。
但是如何处理HTTP请求不是问题问题是如何处理100个不同的URL。
每一个URL可以对应GET和POST请求当然还有PUT、DELETE等请求但是我们通常只考虑最常见的GET和POST请求。
一个最简单的想法是从environ变量里取出HTTP请求的信息然后逐个判断
def application(environ, start_response):method environ[REQUEST_METHOD]path environ[PATH_INFO]if methodGET and path/:return handle_home(environ, start_response)if methodPOST and path/signin:return handle_signin(environ, start_response)...只是这么写下去代码是肯定没法维护了。
代码这么写没法维护的原因是因为WSGI提供的接口虽然比HTTP接口高级了不少但和Web App的处理逻辑比还是比较低级我们需要在WSGI接口之上能进一步抽象让我们专注于用一个函数处理一个URL至于URL到函数的映射就交给Web框架来做。
由于用Python开发一个Web框架十分容易所以Python有上百个开源的Web框架。这里我们先不讨论各种Web框架的优缺点直接选择一个比较流行的Web框架——Flask来使用。
用Flask编写Web App比WSGI接口简单这不是废话么要是比WSGI还复杂用框架干嘛我们先用pip安装Flask
$ pip install flask然后写一个app.py处理3个URL分别是 GET /首页返回Home GET /signin登录页显示登录表单 POST /signin处理登录表单显示登录结果。
注意噢同一个URL/signin分别有GET和POST两种请求映射到两个处理函数中。
Flask通过Python的装饰器在内部自动地把URL和函数给关联起来所以我们写出来的代码就像这样
from flask import Flask
from flask import requestapp Flask(__name__)app.route(/, methods[GET, POST])
def home():return h1Home/h1app.route(/signin, methods[GET])
def signin_form():return form action/signin methodpostpinput nameusername/ppinput namepassword typepassword/ppbutton typesubmitSign In/button/p/formapp.route(/signin, methods[POST])
def signin():# 需要从request对象读取表单内容if request.form[username]admin and request.form[password]password:return h3Hello, admin!/h3return h3Bad username or password./h3if __name__ __main__:app.run()运行python app.pyFlask自带的Server在端口5000上监听
$ python app.py * Running on http://127.0.0.1:5000/打开浏览器输入首页地址http://localhost:5000/ 首页显示正确
再在浏览器地址栏输入http://localhost:5000/signin会显示登录表单 输入预设的用户名admin和口令password登录成功 输入其他错误的用户名和口令登录失败 实际的Web App应该拿到用户名和口令后去数据库查询再比对来判断用户是否能登录成功。
除了Flask常见的Python Web框架还有 Django全能型Web框架 web.py一个小巧的Web框架 Bottle和Flask类似的Web框架 TornadoFacebook的开源异步Web框架。
当然了因为开发Python的Web框架也不是什么难事我们后面也会讲到开发Web框架的内容。
小结
有了Web框架我们在编写Web应用时注意力就从WSGI处理函数转移到URL对应的处理函数这样编写Web App就更加简单了。
在编写URL处理函数时除了配置URL外从HTTP请求拿到用户数据也是非常重要的。Web框架都提供了自己的API来实现这些功能。Flask通过request.form[name]来获取表单的内容。
参考源码
do_flask.py
使用模板
Web框架把我们从WSGI中拯救出来了。现在我们只需要不断地编写函数带上URL就可以继续Web App的开发了。
但是Web App不仅仅是处理逻辑展示给用户的页面也非常重要。在函数中返回一个包含HTML的字符串简单的页面还可以但是想想新浪首页的6000多行的HTML你确信能在Python的字符串中正确地写出来么反正我是做不到。
俗话说得好不懂前端的Python工程师不是好的产品经理。有Web开发经验的同学都明白Web App最复杂的部分就在HTML页面。HTML不仅要正确还要通过CSS美化再加上复杂的JavaScript脚本来实现各种交互和动画效果。总之生成HTML页面的难度很大。
由于在Python代码里拼字符串是不现实的所以模板技术出现了。
使用模板我们需要预先准备一个HTML文档这个HTML文档不是普通的HTML而是嵌入了一些变量和指令然后根据我们传入的数据替换后得到最终的HTML发送给用户 这就是传说中的MVCModel-View-Controller中文名“模型-视图-控制器”。
Python处理URL的函数就是CControllerController负责业务逻辑比如检查用户名是否存在取出用户信息等等
包含变量{{ name }}的模板就是VViewView负责显示逻辑通过简单地替换一些变量View最终输出的就是用户看到的HTML。
MVC中的Model在哪Model是用来传给View的这样View在替换变量的时候就可以从Model中取出相应的数据。
上面的例子中Model就是一个dict
{ name: Michael }只是因为Python支持关键字参数很多Web框架允许传入关键字参数然后在框架内部组装出一个dict作为Model。
现在我们把上次直接输出字符串作为HTML的例子用高端大气上档次的MVC模式改写一下
from flask import Flask, request, render_templateapp Flask(__name__)app.route(/, methods[GET, POST])
def home():return render_template(home.html)app.route(/signin, methods[GET])
def signin_form():return render_template(form.html)app.route(/signin, methods[POST])
def signin():username request.form[username]password request.form[password]if usernameadmin and passwordpassword:return render_template(signin-ok.html, usernameusername)return render_template(form.html, messageBad username or password, usernameusername)if __name__ __main__:app.run()Flask通过render_template()函数来实现模板的渲染。和Web框架类似Python的模板也有很多种。Flask默认支持的模板是jinja2所以我们先直接安装jinja2
$ pip install jinja2然后开始编写jinja2模板
home.html
用来显示首页的模板
html
headtitleHome/title
/head
bodyh1 stylefont-style:italicHome/h1
/body
/htmlform.html
用来显示登录表单的模板
html
headtitlePlease Sign In/title
/head
body{% if message %}p stylecolor:red{{ message }}/p{% endif %}form action/signin methodpostlegendPlease sign in:/legendpinput nameusername placeholderUsername value{{ username }}/ppinput namepassword placeholderPassword typepassword/ppbutton typesubmitSign In/button/p/form
/body
/htmlsignin-ok.html
登录成功的模板
html
headtitleWelcome, {{ username }}/title
/head
bodypWelcome, {{ username }}!/p
/body
/html登录失败的模板呢我们在form.html中加了一点条件判断把form.html重用为登录失败的模板。
最后一定要把模板放到正确的templates目录下templates和app.py在同级目录下 启动python app.py看看使用模板的页面效果 通过MVC我们在Python代码中处理MModel和CController而VView是通过模板处理的这样我们就成功地把Python代码和HTML代码最大限度地分离了。
使用模板的另一大好处是模板改起来很方便而且改完保存后刷新浏览器就能看到最新的效果这对于调试HTML、CSS和JavaScript的前端工程师来说实在是太重要了。
在Jinja2模板中我们用{{ name }}表示一个需要替换的变量。很多时候还需要循环、条件判断等指令语句在Jinja2中用{% ... %}表示指令。
比如循环输出页码
{% for i in page_list %}a href/page/{{ i }}{{ i }}/a
{% endfor %}如果page_list是一个list[1, 2, 3, 4, 5]上面的模板将输出5个超链接。
除了Jinja2常见的模板还有 Mako用% ... %和${xxx}的一个模板 Cheetah也是用% ... %和${xxx}的一个模板 DjangoDjango是一站式框架内置一个用{% ... %}和{{ xxx }}的模板。
小结
有了MVC我们就分离了Python代码和HTML代码。HTML代码全部放到模板里写起来更有效率。
源码参考
app.py
异步IO
在IO编程一节中我们已经知道CPU的速度远远快于磁盘、网络等IO。在一个线程中CPU执行代码的速度极快然而一旦遇到IO操作如读写文件、发送网络数据时就需要等待IO操作完成才能继续进行下一步操作。这种情况称为同步IO。
在IO操作的过程中当前线程被挂起而其他需要CPU执行的代码就无法被当前线程执行了。
因为一个IO操作就阻塞了当前线程导致其他代码无法执行所以我们必须使用多线程或者多进程来并发执行代码为多个用户服务。每个用户都会分配一个线程如果遇到IO导致线程被挂起其他用户的线程不受影响。
多线程和多进程的模型虽然解决了并发问题但是系统不能无上限地增加线程。由于系统切换线程的开销也很大所以一旦线程数量过多CPU的时间就花在线程切换上了真正运行代码的时间就少了结果导致性能严重下降。
由于我们要解决的问题是CPU高速执行能力和IO设备的龟速严重不匹配多线程和多进程只是解决这一问题的一种方法。
另一种解决IO问题的方法是异步IO。当代码需要执行一个耗时的IO操作时它只发出IO指令并不等待IO结果然后就去执行其他代码了。一段时间后当IO返回结果时再通知CPU进行处理。
可以想象如果按普通顺序写出的代码实际上是没法完成异步IO的
do_some_code()
f open(/path/to/file, r)
r f.read() # 线程停在此处等待IO操作结果
# IO操作完成后线程才能继续执行:
do_some_code(r)所以同步IO模型的代码是无法实现异步IO模型的。
异步IO模型需要一个消息循环在消息循环中主线程不断地重复“读取消息-处理消息”这一过程
loop get_event_loop()
while True:event loop.get_event()process_event(event)消息模型其实早在应用在桌面应用程序中了。一个GUI程序的主线程就负责不停地读取消息并处理消息。所有的键盘、鼠标等消息都被发送到GUI程序的消息队列中然后由GUI程序的主线程处理。
由于GUI线程处理键盘、鼠标等消息的速度非常快所以用户感觉不到延迟。某些时候GUI线程在一个消息处理的过程中遇到问题导致一次消息处理时间过长此时用户会感觉到整个GUI程序停止响应了敲键盘、点鼠标都没有反应。这种情况说明在消息模型中处理一个消息必须非常迅速否则主线程将无法及时处理消息队列中的其他消息导致程序看上去停止响应。
消息模型是如何解决同步IO必须等待IO操作这一问题的呢当遇到IO操作时代码只负责发出IO请求不等待IO结果然后直接结束本轮消息处理进入下一轮消息处理过程。当IO操作完成后将收到一条“IO完成”的消息处理该消息时就可以直接获取IO操作结果。
在“发出IO请求”到收到“IO完成”的这段时间里同步IO模型下主线程只能挂起但异步IO模型下主线程并没有休息而是在消息循环中继续处理其他消息。这样在异步IO模型下一个线程就可以同时处理多个IO请求并且没有切换线程的操作。对于大多数IO密集型的应用程序使用异步IO将大大提升系统的多任务处理能力。
协程
在学习异步IO模型前我们先来了解协程。
协程又称微线程纤程。英文名Coroutine。
协程的概念很早就提出来了但直到最近几年才在某些语言如Lua中得到广泛应用。
子程序或者称为函数在所有语言中都是层级调用比如A调用BB在执行过程中又调用了CC执行完毕返回B执行完毕返回最后是A执行完毕。
所以子程序调用是通过栈实现的一个线程就是执行一个子程序。
子程序调用总是一个入口一次返回调用顺序是明确的。而协程的调用和子程序不同。
协程看上去也是子程序但执行过程中在子程序内部可中断然后转而执行别的子程序在适当的时候再返回来接着执行。
注意在一个子程序中中断去执行其他子程序不是函数调用有点类似CPU的中断。比如子程序A、B
def A():print(1)print(2)print(3)def B():print(x)print(y)print(z)假设由协程执行在执行A的过程中可以随时中断去执行BB也可能在执行过程中中断再去执行A结果可能是
1
2
x
y
3
z但是在A中是没有调用B的所以协程的调用比函数调用理解起来要难一些。
看起来A、B的执行有点像多线程但协程的特点在于是一个线程执行那和多线程比协程有何优势
最大的优势就是协程极高的执行效率。因为子程序切换不是线程切换而是由程序自身控制因此没有线程切换的开销和多线程比线程数量越多协程的性能优势就越明显。
第二大优势就是不需要多线程的锁机制因为只有一个线程也不存在同时写变量冲突在协程中控制共享资源不加锁只需要判断状态就好了所以执行效率比多线程高很多。
因为协程是一个线程执行那怎么利用多核CPU呢最简单的方法是多进程协程既充分利用多核又充分发挥协程的高效率可获得极高的性能。
Python对协程的支持是通过generator实现的。
在generator中我们不但可以通过for循环来迭代还可以不断调用next()函数获取由yield语句返回的下一个值。
但是Python的yield不但可以返回一个值它还可以接收调用者发出的参数。
来看例子
传统的生产者-消费者模型是一个线程写消息一个线程取消息通过锁机制控制队列和等待但一不小心就可能死锁。
如果改用协程生产者生产消息后直接通过yield跳转到消费者开始执行待消费者执行完毕后切换回生产者继续生产效率极高
def consumer():r while True:n yield rif not n:returnprint([CONSUMER] Consuming %s... % n)r 200 OKdef produce(c):c.send(None)n 0while n 5:n n 1print([PRODUCER] Producing %s... % n)r c.send(n)print([PRODUCER] Consumer return: %s % r)c.close()c consumer()
produce(c)执行结果
[PRODUCER] Producing 1...
[CONSUMER] Consuming 1...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 2...
[CONSUMER] Consuming 2...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 3...
[CONSUMER] Consuming 3...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 4...
[CONSUMER] Consuming 4...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 5...
[CONSUMER] Consuming 5...
[PRODUCER] Consumer return: 200 OK注意到consumer函数是一个generator把一个consumer传入produce后 首先调用c.send(None)启动生成器 然后一旦生产了东西通过c.send(n)切换到consumer执行 consumer通过yield拿到消息处理又通过yield把结果传回 produce拿到consumer处理的结果继续生产下一条消息 produce决定不生产了通过c.close()关闭consumer整个过程结束。
整个流程无锁由一个线程执行produce和consumer协作完成任务所以称为“协程”而非线程的抢占式多任务。
最后套用Donald Knuth的一句话总结协程的特点
“子程序就是协程的一种特例。”
参考源码
coroutine.py
asyncio
asyncio是Python 3.4版本引入的标准库直接内置了对异步IO的支持。
asyncio的编程模型就是一个消息循环。我们从asyncio模块中直接获取一个EventLoop的引用然后把需要执行的协程扔到EventLoop中执行就实现了异步IO。
用asyncio实现Hello world代码如下
import asyncioasyncio.coroutine
def hello():print(Hello world!)# 异步调用asyncio.sleep(1):r yield from asyncio.sleep(1)print(Hello again!)# 获取EventLoop:
loop asyncio.get_event_loop()
# 执行coroutine
loop.run_until_complete(hello())
loop.close()asyncio.coroutine把一个generator标记为coroutine类型然后我们就把这个coroutine扔到EventLoop中执行。
hello()会首先打印出Hello world!然后yield from语法可以让我们方便地调用另一个generator。由于asyncio.sleep()也是一个coroutine所以线程不会等待asyncio.sleep()而是直接中断并执行下一个消息循环。当asyncio.sleep()返回时线程就可以从yield from拿到返回值此处是None然后接着执行下一行语句。
把asyncio.sleep(1)看成是一个耗时1秒的IO操作在此期间主线程并未等待而是去执行EventLoop中其他可以执行的coroutine了因此可以实现并发执行。
我们用Task封装两个coroutine试试
import threading
import asyncioasyncio.coroutine
def hello():print(Hello world! (%s) % threading.currentThread())yield from asyncio.sleep(1)print(Hello again! (%s) % threading.currentThread())loop asyncio.get_event_loop()
tasks [hello(), hello()]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()观察执行过程
Hello world! (_MainThread(MainThread, started 140735195337472))
Hello world! (_MainThread(MainThread, started 140735195337472))
(暂停约1秒)
Hello again! (_MainThread(MainThread, started 140735195337472))
Hello again! (_MainThread(MainThread, started 140735195337472))由打印的当前线程名称可以看出两个coroutine是由同一个线程并发执行的。
如果把asyncio.sleep()换成真正的IO操作则多个coroutine就可以由一个线程并发执行。
我们用asyncio的异步网络连接来获取sina、sohu和163的网站首页
import asyncioasyncio.coroutine
def wget(host):print(wget %s... % host)connect asyncio.open_connection(host, 80)reader, writer yield from connectheader GET / HTTP/1.0\r\nHost: %s\r\n\r\n % hostwriter.write(header.encode(utf-8))yield from writer.drain()while True:line yield from reader.readline()if line b\r\n:breakprint(%s header %s % (host, line.decode(utf-8).rstrip()))# Ignore the body, close the socketwriter.close()loop asyncio.get_event_loop()
tasks [wget(host) for host in [www.sina.com.cn, www.sohu.com, www.163.com]]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()执行结果如下
wget www.sohu.com...
wget www.sina.com.cn...
wget www.163.com...
(等待一段时间)
(打印出sohu的header)
www.sohu.com header HTTP/1.1 200 OK
www.sohu.com header Content-Type: text/html
...
(打印出sina的header)
www.sina.com.cn header HTTP/1.1 200 OK
www.sina.com.cn header Date: Wed, 20 May 2015 04:56:33 GMT
...
(打印出163的header)
www.163.com header HTTP/1.0 302 Moved Temporarily
www.163.com header Server: Cdn Cache Server V2.0
...可见3个连接由一个线程通过coroutine并发完成。
小结
asyncio提供了完善的异步IO支持
异步操作需要在coroutine中通过yield from完成
多个coroutine可以封装成一组Task然后并发执行。
参考源码
async_hello.py
async_wget.py
async/await
用asyncio提供的asyncio.coroutine可以把一个generator标记为coroutine类型然后在coroutine内部用yield from调用另一个coroutine实现异步操作。
为了简化并更好地标识异步IO从Python 3.5开始引入了新的语法async和await可以让coroutine的代码更简洁易读。
请注意async和await是针对coroutine的新语法要使用新的语法只需要做两步简单的替换
把asyncio.coroutine替换为async把yield from替换为await。
让我们对比一下上一节的代码
asyncio.coroutine
def hello():print(Hello world!)r yield from asyncio.sleep(1)print(Hello again!)用新语法重新编写如下
async def hello():print(Hello world!)r await asyncio.sleep(1)print(Hello again!)剩下的代码保持不变。
小结
Python从3.5版本开始为asyncio提供了async和await的新语法
注意新语法只能用在Python 3.5以及后续版本如果使用3.4版本则仍需使用上一节的方案。
练习
将上一节的异步获取sina、sohu和163的网站首页源码用新语法重写并运行。
参考源码
async_hello2.py
async_wget2.py
aiohttp
asyncio可以实现单线程并发IO操作。如果仅用在客户端发挥的威力不大。如果把asyncio用在服务器端例如Web服务器由于HTTP连接就是IO操作因此可以用单线程coroutine实现多用户的高并发支持。
asyncio实现了TCP、UDP、SSL等协议aiohttp则是基于asyncio实现的HTTP框架。
我们先安装aiohttp
pip install aiohttp然后编写一个HTTP服务器分别处理以下URL / - 首页返回bh1Index/h1 /hello/{name} - 根据URL参数返回文本hello, %s!。
代码如下
import asynciofrom aiohttp import webasync def index(request):await asyncio.sleep(0.5)return web.Response(bodybh1Index/h1)async def hello(request):await asyncio.sleep(0.5)text h1hello, %s!/h1 % request.match_info[name]return web.Response(bodytext.encode(utf-8))async def init(loop):app web.Application(looploop)app.router.add_route(GET, /, index)app.router.add_route(GET, /hello/{name}, hello)srv await loop.create_server(app.make_handler(), 127.0.0.1, 8000)print(Server started at http://127.0.0.1:8000...)return srvloop asyncio.get_event_loop()
loop.run_until_complete(init(loop))
loop.run_forever()注意aiohttp的初始化函数init()也是一个coroutineloop.create_server()则利用asyncio创建TCP服务。
参考源码
aio_web.py
实战
看完了教程是不是有这么一种感觉看的时候觉得很简单照着教程敲代码也没啥大问题。
于是准备开始独立写代码就发现不知道从哪开始下手了。
这种情况是完全正常的。好比学写作文学的时候觉得简单写的时候就无从下笔了。
虽然这个教程是面向小白的零基础Python教程但是我们的目标不是学到60分而是学到90分。
所以用Python写一个真正的Web App吧
目标
我们设定的实战目标是一个Blog网站包含日志、用户和评论3大部分。
很多童鞋会想这是不是太简单了
比如webpy.org上就提供了一个Blog的例子目测也就100行代码。
但是这样的页面 你拿得出手么
我们要写出用户真正看得上眼的页面首页长得像这样 评论区 还有极其强大的后台管理页面 是不是一下子变得高端大气上档次了
项目名称
必须是高端大气上档次的名称命名为awesome-python3-webapp。
项目计划
项目计划开发周期为16天。每天你需要完成教程中的内容。如果你觉得编写代码难度实在太大可以参考一下当天在GitHub上的代码。
第N天的代码在https://github.com/michaelliao/awesome-python3-webapp/tree/day-N上。比如第1天就是
https://github.com/michaelliao/awesome-python3-webapp/tree/day-01
以此类推。
要预览awesome-python3-webapp的最终页面效果请猛击 awesome.liaoxuefeng.com
Day 1 - 搭建开发环境
搭建开发环境
首先确认系统安装的Python版本是3.4.x
$ python3 --version
Python 3.4.3然后用pip安装开发Web App需要的第三方库
异步框架aiohttp
$pip3 install aiohttp前端模板引擎jinja2
$ pip3 install jinja2MySQL 5.x数据库从官方网站下载并安装安装完毕后请务必牢记root口令。为避免遗忘口令建议直接把root口令设置为password
MySQL的Python异步驱动程序aiomysql
$ pip3 install aiomysql项目结构
选择一个工作目录然后我们建立如下的目录结构
awesome-python3-webapp/ -- 根目录
|
- backup/ -- 备份目录
|
- conf/ -- 配置文件
|
- dist/ -- 打包目录
|
- www/ -- Web目录存放.py文件
| |
| - static/ -- 存放静态文件
| |
| - templates/ -- 存放模板文件
|
- ios/ -- 存放iOS App工程
|
- LICENSE -- 代码LICENSE创建好项目的目录结构后建议同时建立git仓库并同步至GitHub保证代码修改的安全。
要了解git和GitHub的用法请移步Git教程。
开发工具
自备推荐用Sublime Text请参考使用文本编辑器。
参考源码
day-01
Day 2 - 编写Web App骨架
由于我们的Web App建立在asyncio的基础上因此用aiohttp写一个基本的app.py
import logging; logging.basicConfig(levellogging.INFO)import asyncio, os, json, time
from datetime import datetimefrom aiohttp import webdef index(request):return web.Response(bodybh1Awesome/h1)asyncio.coroutine
def init(loop):app web.Application(looploop)app.router.add_route(GET, /, index)srv yield from loop.create_server(app.make_handler(), 127.0.0.1, 9000)logging.info(server started at http://127.0.0.1:9000...)return srvloop asyncio.get_event_loop()
loop.run_until_complete(init(loop))
loop.run_forever()运行python app.pyWeb App将在9000端口监听HTTP请求并且对首页/进行响应
$ python3 app.py
INFO:root:server started at http://127.0.0.1:9000...这里我们简单地返回一个Awesome字符串在浏览器中可以看到效果 这说明我们的Web App骨架已经搭好了可以进一步往里面添加更多的东西。
参考源码
day-02
Day 3 - 编写ORM
在一个Web App中所有数据包括用户信息、发布的日志、评论等都存储在数据库中。在awesome-python3-webapp中我们选择MySQL作为数据库。
Web App里面有很多地方都要访问数据库。访问数据库需要创建数据库连接、游标对象然后执行SQL语句最后处理异常清理资源。这些访问数据库的代码如果分散到各个函数中势必无法维护也不利于代码复用。
所以我们要首先把常用的SELECT、INSERT、UPDATE和DELETE操作用函数封装起来。
由于Web框架使用了基于asyncio的aiohttp这是基于协程的异步模型。在协程中不能调用普通的同步IO操作因为所有用户都是由一个线程服务的协程的执行速度必须非常快才能处理大量用户的请求。而耗时的IO操作不能在协程中以同步的方式调用否则等待一个IO操作时系统无法响应任何其他用户。
这就是异步编程的一个原则一旦决定使用异步则系统每一层都必须是异步“开弓没有回头箭”。
幸运的是aiomysql为MySQL数据库提供了异步IO的驱动。
创建连接池
我们需要创建一个全局的连接池每个HTTP请求都可以从连接池中直接获取数据库连接。使用连接池的好处是不必频繁地打开和关闭数据库连接而是能复用就尽量复用。
连接池由全局变量__pool存储缺省情况下将编码设置为utf8自动提交事务
asyncio.coroutine
def create_pool(loop, **kw):logging.info(create database connection pool...)global __pool__pool yield from aiomysql.create_pool(hostkw.get(host, localhost),portkw.get(port, 3306),userkw[user],passwordkw[password],dbkw[db],charsetkw.get(charset, utf8),autocommitkw.get(autocommit, True),maxsizekw.get(maxsize, 10),minsizekw.get(minsize, 1),looploop)Select
要执行SELECT语句我们用select函数执行需要传入SQL语句和SQL参数
asyncio.coroutine
def select(sql, args, sizeNone):log(sql, args)global __poolwith (yield from __pool) as conn:cur yield from conn.cursor(aiomysql.DictCursor)yield from cur.execute(sql.replace(?, %s), args or ())if size:rs yield from cur.fetchmany(size)else:rs yield from cur.fetchall()yield from cur.close()logging.info(rows returned: %s % len(rs))return rsSQL语句的占位符是?而MySQL的占位符是%sselect()函数在内部自动替换。注意要始终坚持使用带参数的SQL而不是自己拼接SQL字符串这样可以防止SQL注入攻击。
注意到yield from将调用一个子协程也就是在一个协程中调用另一个协程并直接获得子协程的返回结果。
如果传入size参数就通过fetchmany()获取最多指定数量的记录否则通过fetchall()获取所有记录。
Insert, Update, Delete
要执行INSERT、UPDATE、DELETE语句可以定义一个通用的execute()函数因为这3种SQL的执行都需要相同的参数以及返回一个整数表示影响的行数
asyncio.coroutine
def execute(sql, args):log(sql)with (yield from __pool) as conn:try:cur yield from conn.cursor()yield from cur.execute(sql.replace(?, %s), args)affected cur.rowcountyield from cur.close()except BaseException as e:raisereturn affectedexecute()函数和select()函数所不同的是cursor对象不返回结果集而是通过rowcount返回结果数。
ORM
有了基本的select()和execute()函数我们就可以开始编写一个简单的ORM了。
设计ORM需要从上层调用者角度来设计。
我们先考虑如何定义一个User对象然后把数据库表users和它关联起来。
from orm import Model, StringField, IntegerFieldclass User(Model):__table__ usersid IntegerField(primary_keyTrue)name StringField()注意到定义在User类中的__table__、id和name是类的属性不是实例的属性。所以在类级别上定义的属性用来描述User对象和表的映射关系而实例属性必须通过__init__()方法去初始化所以两者互不干扰
# 创建实例:
user User(id123, nameMichael)
# 存入数据库:
user.insert()
# 查询所有User对象:
users User.findAll()定义Model
首先要定义的是所有ORM映射的基类Model
class Model(dict, metaclassModelMetaclass):def __init__(self, **kw):super(Model, self).__init__(**kw)def __getattr__(self, key):try:return self[key]except KeyError:raise AttributeError(rModel object has no attribute %s % key)def __setattr__(self, key, value):self[key] valuedef getValue(self, key):return getattr(self, key, None)def getValueOrDefault(self, key):value getattr(self, key, None)if value is None:field self.__mappings__[key]if field.default is not None:value field.default() if callable(field.default) else field.defaultlogging.debug(using default value for %s: %s % (key, str(value)))setattr(self, key, value)return valueModel从dict继承所以具备所有dict的功能同时又实现了特殊方法__getattr__()和__setattr__()因此又可以像引用普通字段那样写 user[id]
123user.id
123以及Field和各种Field子类
class Field(object):def __init__(self, name, column_type, primary_key, default):self.name nameself.column_type column_typeself.primary_key primary_keyself.default defaultdef __str__(self):return %s, %s:%s % (self.__class__.__name__, self.column_type, self.name)映射varchar的StringField
class StringField(Field):def __init__(self, nameNone, primary_keyFalse, defaultNone, ddlvarchar(100)):super().__init__(name, ddl, primary_key, default)注意到Model只是一个基类如何将具体的子类如User的映射信息读取出来呢答案就是通过metaclassModelMetaclass
class ModelMetaclass(type):def __new__(cls, name, bases, attrs):# 排除Model类本身:if nameModel:return type.__new__(cls, name, bases, attrs)# 获取table名称:tableName attrs.get(__table__, None) or namelogging.info(found model: %s (table: %s) % (name, tableName))# 获取所有的Field和主键名:mappings dict()fields []primaryKey Nonefor k, v in attrs.items():if isinstance(v, Field):logging.info( found mapping: %s %s % (k, v))mappings[k] vif v.primary_key:# 找到主键:if primaryKey:raise RuntimeError(Duplicate primary key for field: %s % k)primaryKey kelse:fields.append(k)if not primaryKey:raise RuntimeError(Primary key not found.)for k in mappings.keys():attrs.pop(k)escaped_fields list(map(lambda f: %s % f, fields))attrs[__mappings__] mappings # 保存属性和列的映射关系attrs[__table__] tableNameattrs[__primary_key__] primaryKey # 主键属性名attrs[__fields__] fields # 除主键外的属性名# 构造默认的SELECT, INSERT, UPDATE和DELETE语句:attrs[__select__] select %s, %s from %s % (primaryKey, , .join(escaped_fields), tableName)attrs[__insert__] insert into %s (%s, %s) values (%s) % (tableName, , .join(escaped_fields), primaryKey, create_args_string(len(escaped_fields) 1))attrs[__update__] update %s set %s where %s? % (tableName, , .join(map(lambda f: %s? % (mappings.get(f).name or f), fields)), primaryKey)attrs[__delete__] delete from %s where %s? % (tableName, primaryKey)return type.__new__(cls, name, bases, attrs)这样任何继承自Model的类比如User会自动通过ModelMetaclass扫描映射关系并存储到自身的类属性如__table__、__mappings__中。
然后我们往Model类添加class方法就可以让所有子类调用class方法
class Model(dict):...classmethodasyncio.coroutinedef find(cls, pk): find object by primary key. rs yield from select(%s where %s? % (cls.__select__, cls.__primary_key__), [pk], 1)if len(rs) 0:return Nonereturn cls(**rs[0])User类现在就可以通过类方法实现主键查找
user yield from User.find(123)往Model类添加实例方法就可以让所有子类调用实例方法
class Model(dict):...asyncio.coroutinedef save(self):args list(map(self.getValueOrDefault, self.__fields__))args.append(self.getValueOrDefault(self.__primary_key__))rows yield from execute(self.__insert__, args)if rows ! 1:logging.warn(failed to insert record: affected rows: %s % rows)这样就可以把一个User实例存入数据库
user User(id123, nameMichael)
yield from user.save()最后一步是完善ORM对于查找我们可以实现以下方法 findAll() - 根据WHERE条件查找 findNumber() - 根据WHERE条件查找但返回的是整数适用于select count(*)类型的SQL。
以及update()和remove()方法。
所有这些方法都必须用asyncio.coroutine装饰变成一个协程。
调用时需要特别注意
user.save()没有任何效果因为调用save()仅仅是创建了一个协程并没有执行它。一定要用
yield from user.save()才真正执行了INSERT操作。
最后看看我们实现的ORM模块一共多少行代码累计不到300多行。用Python写一个ORM是不是很容易呢
参考源码
day-03
Day 4 - 编写Model
有了ORM我们就可以把Web App需要的3个表用Model表示出来
import time, uuidfrom orm import Model, StringField, BooleanField, FloatField, TextFielddef next_id():return %015d%s000 % (int(time.time() * 1000), uuid.uuid4().hex)class User(Model):__table__ usersid StringField(primary_keyTrue, defaultnext_id, ddlvarchar(50))email StringField(ddlvarchar(50))passwd StringField(ddlvarchar(50))admin BooleanField()name StringField(ddlvarchar(50))image StringField(ddlvarchar(500))created_at FloatField(defaulttime.time)class Blog(Model):__table__ blogsid StringField(primary_keyTrue, defaultnext_id, ddlvarchar(50))user_id StringField(ddlvarchar(50))user_name StringField(ddlvarchar(50))user_image StringField(ddlvarchar(500))name StringField(ddlvarchar(50))summary StringField(ddlvarchar(200))content TextField()created_at FloatField(defaulttime.time)class Comment(Model):__table__ commentsid StringField(primary_keyTrue, defaultnext_id, ddlvarchar(50))blog_id StringField(ddlvarchar(50))user_id StringField(ddlvarchar(50))user_name StringField(ddlvarchar(50))user_image StringField(ddlvarchar(500))content TextField()created_at FloatField(defaulttime.time)在编写ORM时给一个Field增加一个default参数可以让ORM自己填入缺省值非常方便。并且缺省值可以作为函数对象传入在调用save()时自动计算。
例如主键id的缺省值是函数next_id创建时间created_at的缺省值是函数time.time可以自动设置当前日期和时间。
日期和时间用float类型存储在数据库中而不是datetime类型这么做的好处是不必关心数据库的时区以及时区转换问题排序非常简单显示的时候只需要做一个float到str的转换也非常容易。
初始化数据库表
如果表的数量很少可以手写创建表的SQL脚本
-- schema.sqldrop database if exists awesome;create database awesome;use awesome;grant select, insert, update, delete on awesome.* to www-datalocalhost identified by www-data;create table users (id varchar(50) not null,email varchar(50) not null,passwd varchar(50) not null,admin bool not null,name varchar(50) not null,image varchar(500) not null,created_at real not null,unique key idx_email (email),key idx_created_at (created_at),primary key (id)
) engineinnodb default charsetutf8;create table blogs (id varchar(50) not null,user_id varchar(50) not null,user_name varchar(50) not null,user_image varchar(500) not null,name varchar(50) not null,summary varchar(200) not null,content mediumtext not null,created_at real not null,key idx_created_at (created_at),primary key (id)
) engineinnodb default charsetutf8;create table comments (id varchar(50) not null,blog_id varchar(50) not null,user_id varchar(50) not null,user_name varchar(50) not null,user_image varchar(500) not null,content mediumtext not null,created_at real not null,key idx_created_at (created_at),primary key (id)
) engineinnodb default charsetutf8;如果表的数量很多可以从Model对象直接通过脚本自动生成SQL脚本使用更简单。
把SQL脚本放到MySQL命令行里执行
$ mysql -u root -p schema.sql我们就完成了数据库表的初始化。
编写数据访问代码
接下来就可以真正开始编写代码操作对象了。比如对于User对象我们就可以做如下操作
import orm
from models import User, Blog, Commentdef test():yield from orm.create_pool(userwww-data, passwordwww-data, databaseawesome)u User(nameTest, emailtestexample.com, passwd1234567890, imageabout:blank)yield from u.save()for x in test():pass可以在MySQL客户端命令行查询看看数据是不是正常存储到MySQL里面了。
参考源码
day-04
Day 5 - 编写Web框架
在正式开始Web开发前我们需要编写一个Web框架。
aiohttp已经是一个Web框架了为什么我们还需要自己封装一个
原因是从使用者的角度来说aiohttp相对比较底层编写一个URL的处理函数需要这么几步
第一步编写一个用asyncio.coroutine装饰的函数
asyncio.coroutine
def handle_url_xxx(request):pass第二步传入的参数需要自己从request中获取
url_param request.match_info[key]
query_params parse_qs(request.query_string)最后需要自己构造Response对象
text render(template, data)
return web.Response(text.encode(utf-8))这些重复的工作可以由框架完成。例如处理带参数的URL/blog/{id}可以这么写
get(/blog/{id})
def get_blog(id):pass处理query_string参数可以通过关键字参数**kw或者命名关键字参数接收
get(/api/comments)
def api_comments(*, page1):pass对于函数的返回值不一定是web.Response对象可以是str、bytes或dict。
如果希望渲染模板我们可以这么返回一个dict
return {__template__: index.html,data: ...
}因此Web框架的设计是完全从使用者出发目的是让使用者编写尽可能少的代码。
编写简单的函数而非引入request和web.Response还有一个额外的好处就是可以单独测试否则需要模拟一个request才能测试。
get和post
要把一个函数映射为一个URL处理函数我们先定义get()
def get(path):Define decorator get(/path)def decorator(func):functools.wraps(func)def wrapper(*args, **kw):return func(*args, **kw)wrapper.__method__ GETwrapper.__route__ pathreturn wrapperreturn decorator这样一个函数通过get()的装饰就附带了URL信息。
post与get定义类似。
定义RequestHandler
URL处理函数不一定是一个coroutine因此我们用RequestHandler()来封装一个URL处理函数。
RequestHandler是一个类由于定义了__call__()方法因此可以将其实例视为函数。
RequestHandler目的就是从URL函数中分析其需要接收的参数从request中获取必要的参数调用URL函数然后把结果转换为web.Response对象这样就完全符合aiohttp框架的要求
class RequestHandler(object):def __init__(self, app, fn):self._app appself._func fn...asyncio.coroutinedef __call__(self, request):kw ... 获取参数r yield from self._func(**kw)return r再编写一个add_route函数用来注册一个URL处理函数
def add_route(app, fn):method getattr(fn, __method__, None)path getattr(fn, __route__, None)if path is None or method is None:raise ValueError(get or post not defined in %s. % str(fn))if not asyncio.iscoroutinefunction(fn) and not inspect.isgeneratorfunction(fn):fn asyncio.coroutine(fn)logging.info(add route %s %s %s(%s) % (method, path, fn.__name__, , .join(inspect.signature(fn).parameters.keys())))app.router.add_route(method, path, RequestHandler(app, fn))最后一步把很多次add_route()注册的调用
add_route(app, handles.index)
add_route(app, handles.blog)
add_route(app, handles.create_comment)
...变成自动扫描
# 自动把handler模块的所有符合条件的函数注册了:
add_routes(app, handlers)add_routes()定义如下
def add_routes(app, module_name):n module_name.rfind(.)if n (-1):mod __import__(module_name, globals(), locals())else:name module_name[n1:]mod getattr(__import__(module_name[:n], globals(), locals(), [name]), name)for attr in dir(mod):if attr.startswith(_):continuefn getattr(mod, attr)if callable(fn):method getattr(fn, __method__, None)path getattr(fn, __route__, None)if method and path:add_route(app, fn)最后在app.py中加入middleware、jinja2模板和自注册的支持
app web.Application(looploop, middlewares[logger_factory, response_factory
])
init_jinja2(app, filtersdict(datetimedatetime_filter))
add_routes(app, handlers)
add_static(app)middleware
middleware是一种拦截器一个URL在被某个函数处理前可以经过一系列的middleware的处理。
一个middleware可以改变URL的输入、输出甚至可以决定不继续处理而直接返回。middleware的用处就在于把通用的功能从每个URL处理函数中拿出来集中放到一个地方。例如一个记录URL日志的logger可以简单定义如下
asyncio.coroutine
def logger_factory(app, handler):asyncio.coroutinedef logger(request):# 记录日志:logging.info(Request: %s %s % (request.method, request.path))# 继续处理请求:return (yield from handler(request))return logger而response这个middleware把返回值转换为web.Response对象再返回以保证满足aiohttp的要求
asyncio.coroutine
def response_factory(app, handler):asyncio.coroutinedef response(request):# 结果:r yield from handler(request)if isinstance(r, web.StreamResponse):return rif isinstance(r, bytes):resp web.Response(bodyr)resp.content_type application/octet-streamreturn respif isinstance(r, str):resp web.Response(bodyr.encode(utf-8))resp.content_type text/html;charsetutf-8return respif isinstance(r, dict):...有了这些基础设施我们就可以专注地往handlers模块不断添加URL处理函数了可以极大地提高开发效率。
参考源码
day-05
Day 6 - 编写配置文件
有了Web框架和ORM框架我们就可以开始装配App了。
通常一个Web App在运行时都需要读取配置文件比如数据库的用户名、口令等在不同的环境中运行时Web App可以通过读取不同的配置文件来获得正确的配置。
由于Python本身语法简单完全可以直接用Python源代码来实现配置而不需要再解析一个单独的.properties或者.yaml等配置文件。
默认的配置文件应该完全符合本地开发环境这样无需任何设置就可以立刻启动服务器。
我们把默认的配置文件命名为config_default.py
# config_default.pyconfigs {db: {host: 127.0.0.1,port: 3306,user: www-data,password: www-data,database: awesome},session: {secret: AwEsOmE}
}上述配置文件简单明了。但是如果要部署到服务器时通常需要修改数据库的host等信息直接修改config_default.py不是一个好办法更好的方法是编写一个config_override.py用来覆盖某些默认设置
# config_override.pyconfigs {db: {host: 192.168.0.100}
}把config_default.py作为开发环境的标准配置把config_override.py作为生产环境的标准配置我们就可以既方便地在本地开发又可以随时把应用部署到服务器上。
应用程序读取配置文件需要优先从config_override.py读取。为了简化读取配置文件可以把所有配置读取到统一的config.py中
# config.py
configs config_default.configstry:import config_overrideconfigs merge(configs, config_override.configs)
except ImportError:pass这样我们就完成了App的配置。
参考源码
day-06
Day 7 - 编写MVC
现在ORM框架、Web框架和配置都已就绪我们可以开始编写一个最简单的MVC把它们全部启动起来。
通过Web框架的get和ORM框架的Model支持可以很容易地编写一个处理首页URL的函数
get(/)
def index(request):users yield from User.findAll()return {__template__: test.html,users: users}__template__指定的模板文件是test.html其他参数是传递给模板的数据所以我们在模板的根目录templates下创建test.html
!DOCTYPE html
html
headmeta charsetutf-8 /titleTest users - Awesome Python Webapp/title
/head
bodyh1All users/h1{% for u in users %}p{{ u.name }} / {{ u.email }}/p{% endfor %}
/body
/html接下来如果一切顺利可以用命令行启动Web服务器
$ python3 app.py然后在浏览器中访问http://localhost:9000/。
如果数据库的users表什么内容也没有你就无法在浏览器中看到循环输出的内容。可以自己在MySQL的命令行里给users表添加几条记录然后再访问 参考源码
day-07
Day 8 - 构建前端
虽然我们跑通了一个最简单的MVC但是页面效果肯定不会让人满意。
对于复杂的HTML前端页面来说我们需要一套基础的CSS框架来完成页面布局和基本样式。另外jQuery作为操作DOM的JavaScript库也必不可少。
从零开始写CSS不如直接从一个已有的功能完善的CSS框架开始。有很多CSS框架可供选择。我们这次选择uikit这个强大的CSS框架。它具备完善的响应式布局漂亮的UI以及丰富的HTML组件让我们能轻松设计出美观而简洁的页面。
可以从uikit首页下载打包的资源文件。
所有的静态资源文件我们统一放到www/static目录下并按照类别归类
static/
- css/
| - addons/
| | - uikit.addons.min.css
| | - uikit.almost-flat.addons.min.css
| | - uikit.gradient.addons.min.css
| - awesome.css
| - uikit.almost-flat.addons.min.css
| - uikit.gradient.addons.min.css
| - uikit.min.css
- fonts/
| - fontawesome-webfont.eot
| - fontawesome-webfont.ttf
| - fontawesome-webfont.woff
| - FontAwesome.otf
- js/- awesome.js- html5.js- jquery.min.js- uikit.min.js由于前端页面肯定不止首页一个页面每个页面都有相同的页眉和页脚。如果每个页面都是独立的HTML模板那么我们在修改页眉和页脚的时候就需要把每个模板都改一遍这显然是没有效率的。
常见的模板引擎已经考虑到了页面上重复的HTML部分的复用问题。有的模板通过include把页面拆成三部分
html% include fileinc_header.html %% include fileindex_body.html %% include fileinc_footer.html %
/html这样相同的部分inc_header.html和inc_footer.html就可以共享。
但是include方法不利于页面整体结构的维护。jinjia2的模板还有另一种“继承”方式实现模板的复用更简单。
“继承”模板的方式是通过编写一个“父模板”在父模板中定义一些可替换的block块。然后编写多个“子模板”每个子模板都可以只替换父模板定义的block。比如定义一个最简单的父模板
!-- base.html --
htmlheadtitle{% block title%} 这里定义了一个名为title的block {% endblock %}/title/headbody{% block content %} 这里定义了一个名为content的block {% endblock %}/body
/html对于子模板a.html只需要把父模板的title和content替换掉
{% extends base.html %}{% block title %} A {% endblock %}{% block content %}h1Chapter A/h1pblablabla.../p
{% endblock %}对于子模板b.html如法炮制
{% extends base.html %}{% block title %} B {% endblock %}{% block content %}h1Chapter B/h1ullilist 1/lililist 2/li/ul
{% endblock %}这样一旦定义好父模板的整体布局和CSS样式编写子模板就会非常容易。
让我们通过uikit这个CSS框架来完成父模板__base__.html的编写
!DOCTYPE html
html
headmeta charsetutf-8 /{% block meta %}!-- block meta --{% endblock %}title{% block title %} ? {% endblock %} - Awesome Python Webapp/titlelink relstylesheet href/static/css/uikit.min.csslink relstylesheet href/static/css/uikit.gradient.min.csslink relstylesheet href/static/css/awesome.css /script src/static/js/jquery.min.js/scriptscript src/static/js/md5.js/scriptscript src/static/js/uikit.min.js/scriptscript src/static/js/awesome.js/script{% block beforehead %}!-- before head --{% endblock %}
/head
bodynav classuk-navbar uk-navbar-attached uk-margin-bottomdiv classuk-container uk-container-centera href/ classuk-navbar-brandAwesome/aul classuk-navbar-navli data-urlblogsa href/i classuk-icon-home/i 日志/a/lilia target_blank href#i classuk-icon-book/i 教程/a/lilia target_blank href#i classuk-icon-code/i 源码/a/li/uldiv classuk-navbar-flipul classuk-navbar-nav{% if user %}li classuk-parent data-uk-dropdowna href#0i classuk-icon-user/i {{ user.name }}/adiv classuk-dropdown uk-dropdown-navbarul classuk-nav uk-nav-navbarlia href/signouti classuk-icon-sign-out/i 登出/a/li/ul/div/li{% else %}lia href/signini classuk-icon-sign-in/i 登陆/a/lilia href/registeri classuk-icon-edit/i 注册/a/li{% endif %}/ul/div/div/navdiv classuk-container uk-container-centerdiv classuk-grid!-- content --{% block content %}{% endblock %}!-- // content --/div/divdiv classuk-margin-large-top stylebackground-color:#eee; border-top:1px solid #ccc;div classuk-container uk-container-center uk-text-centerdiv classuk-panel uk-margin-top uk-margin-bottompa target_blank href# classuk-icon-button uk-icon-weibo/aa target_blank href# classuk-icon-button uk-icon-github/aa target_blank href# classuk-icon-button uk-icon-linkedin-square/aa target_blank href# classuk-icon-button uk-icon-twitter/a/ppPowered by a href#Awesome Python Webapp/a. Copyright copy; 2014. [a href/manage/ target_blankManage/a]/ppa hrefhttp://www.liaoxuefeng.com/ target_blankwww.liaoxuefeng.com/a. All rights reserved./pa target_blank href#i classuk-icon-html5 stylefont-size:64px; color: #444;/i/a/div/div/div
/body
/html__base__.html定义的几个block作用如下
用于子页面定义一些meta例如rss feed
{% block meta %} ... {% endblock %}覆盖页面的标题
{% block title %} ... {% endblock %}子页面可以在head标签关闭前插入JavaScript代码
{% block beforehead %} ... {% endblock %}子页面的content布局和内容
{% block content %}...
{% endblock %}我们把首页改造一下从__base__.html继承一个blogs.html
{% extends __base__.html %}{% block title %}日志{% endblock %}{% block content %}div classuk-width-medium-3-4{% for blog in blogs %}article classuk-articleh2a href/blog/{{ blog.id }}{{ blog.name }}/a/h2p classuk-article-meta发表于{{ blog.created_at}}/pp{{ blog.summary }}/ppa href/blog/{{ blog.id }}继续阅读 i classuk-icon-angle-double-right/i/a/p/articlehr classuk-article-divider{% endfor %}/divdiv classuk-width-medium-1-4div classuk-panel uk-panel-headerh3 classuk-panel-title友情链接/h3ul classuk-list uk-list-linelii classuk-icon-thumbs-o-up/i a target_blank href#编程/a/lilii classuk-icon-thumbs-o-up/i a target_blank href#读书/a/lilii classuk-icon-thumbs-o-up/i a target_blank href#Python教程/a/lilii classuk-icon-thumbs-o-up/i a target_blank href#Git教程/a/li/ul/div/div{% endblock %}相应地首页URL的处理函数更新如下
get(/)
def index(request):summary Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.blogs [Blog(id1, nameTest Blog, summarysummary, created_attime.time()-120),Blog(id2, nameSomething New, summarysummary, created_attime.time()-3600),Blog(id3, nameLearn Swift, summarysummary, created_attime.time()-7200)]return {__template__: blogs.html,blogs: blogs}Blog的创建日期显示的是一个浮点数因为它是由这段模板渲染出来的
p classuk-article-meta发表于{{ blog.created_at }}/p解决方法是通过jinja2的filter过滤器把一个浮点数转换成日期字符串。我们来编写一个datetime的filter在模板里用法如下
p classuk-article-meta发表于{{ blog.created_at|datetime }}/pfilter需要在初始化jinja2时设置。相关代码如下
def datetime_filter(t):delta int(time.time() - t)if delta 60:return 1分钟前if delta 3600:return %s分钟前 % (delta // 60)if delta 86400:return %s小时前 % (delta // 3600)if delta 604800:return %s天前 % (delta // 86400)dt datetime.fromtimestamp(t)return %s年%s月%s日 % (dt.year, dt.month, dt.day)...
init_jinja2(app, filtersdict(datetimedatetime_filter))
...现在完善的首页显示如下 参考源码
day-08
Day 9 - 编写API
自从Roy Fielding博士在2000年他的博士论文中提出RESTRepresentational State Transfer风格的软件架构模式后REST就基本上迅速取代了复杂而笨重的SOAP成为Web API的标准了。
什么是Web API呢
如果我们想要获取一篇Blog输入http://localhost:9000/blog/123就可以看到id为123的Blog页面但这个结果是HTML页面它同时混合包含了Blog的数据和Blog的展示两个部分。对于用户来说阅读起来没有问题但是如果机器读取就很难从HTML中解析出Blog的数据。
如果一个URL返回的不是HTML而是机器能直接解析的数据这个URL就可以看成是一个Web API。比如读取http://localhost:9000/api/blogs/123如果能直接返回Blog的数据那么机器就可以直接读取。
REST就是一种设计API的模式。最常用的数据格式是JSON。由于JSON能直接被JavaScript读取所以以JSON格式编写的REST风格的API具有简单、易读、易用的特点。
编写API有什么好处呢由于API就是把Web App的功能全部封装了所以通过API操作数据可以极大地把前端和后端的代码隔离使得后端代码易于测试前端代码编写更简单。
一个API也是一个URL的处理函数我们希望能直接通过一个api来把函数变成JSON格式的REST API这样获取注册用户可以用一个API实现如下
get(/api/users)
def api_get_users(*, page1):page_index get_page_index(page)num yield from User.findNumber(count(id))p Page(num, page_index)if num 0:return dict(pagep, users())users yield from User.findAll(orderBycreated_at desc, limit(p.offset, p.limit))for u in users:u.passwd ******return dict(pagep, usersusers)只要返回一个dict后续的response这个middleware就可以把结果序列化为JSON并返回。
我们需要对Error进行处理因此定义一个APIError这种Error是指API调用时发生了逻辑错误比如用户不存在其他的Error视为Bug返回的错误代码为internalerror。
客户端调用API时必须通过错误代码来区分API调用是否成功。错误代码是用来告诉调用者出错的原因。很多API用一个整数表示错误码这种方式很难维护错误码客户端拿到错误码还需要查表得知错误信息。更好的方式是用字符串表示错误代码不需要看文档也能猜到错误原因。
可以在浏览器直接测试API例如输入http://localhost:9000/api/users就可以看到返回的JSON 参考源码
day-09
Day 10 - 用户注册和登录
用户管理是绝大部分Web网站都需要解决的问题。用户管理涉及到用户注册和登录。
用户注册相对简单我们可以先通过API把用户注册这个功能实现了
_RE_EMAIL re.compile(r^[a-z0-9\.\-\_]\[a-z0-9\-\_](\.[a-z0-9\-\_]){1,4}$)
_RE_SHA1 re.compile(r^[0-9a-f]{40}$)post(/api/users)
def api_register_user(*, email, name, passwd):if not name or not name.strip():raise APIValueError(name)if not email or not _RE_EMAIL.match(email):raise APIValueError(email)if not passwd or not _RE_SHA1.match(passwd):raise APIValueError(passwd)users yield from User.findAll(email?, [email])if len(users) 0:raise APIError(register:failed, email, Email is already in use.)uid next_id()sha1_passwd %s:%s % (uid, passwd)user User(iduid, namename.strip(), emailemail, passwdhashlib.sha1(sha1_passwd.encode(utf-8)).hexdigest(), imagehttp://www.gravatar.com/avatar/%s?dmms120 % hashlib.md5(email.encode(utf-8)).hexdigest())yield from user.save()# make session cookie:r web.Response()r.set_cookie(COOKIE_NAME, user2cookie(user, 86400), max_age86400, httponlyTrue)user.passwd ******r.content_type application/jsonr.body json.dumps(user, ensure_asciiFalse).encode(utf-8)return r注意用户口令是客户端传递的经过SHA1计算后的40位Hash字符串所以服务器端并不知道用户的原始口令。
接下来可以创建一个注册页面让用户填写注册表单然后提交数据到注册用户的API
{% extends __base__.html %}{% block title %}注册{% endblock %}{% block beforehead %}script
function validateEmail(email) {var re /^[a-z0-9\.\-\_]\[a-z0-9\-\_](\.[a-z0-9\-\_]){1,4}$/;return re.test(email.toLowerCase());
}
$(function () {var vm new Vue({el: #vm,data: {name: ,email: ,password1: ,password2: },methods: {submit: function (event) {event.preventDefault();var $form $(#vm);if (! this.name.trim()) {return $form.showFormError(请输入名字);}if (! validateEmail(this.email.trim().toLowerCase())) {return $form.showFormError(请输入正确的Email地址);}if (this.password1.length 6) {return $form.showFormError(口令长度至少为6个字符);}if (this.password1 ! this.password2) {return $form.showFormError(两次输入的口令不一致);}var email this.email.trim().toLowerCase();$form.postJSON(/api/users, {name: this.name.trim(),email: email,passwd: CryptoJS.SHA1(email : this.password1).toString()}, function (err, r) {if (err) {return $form.showFormError(err);}return location.assign(/);});}}});$(#vm).show();
});
/script{% endblock %}{% block content %}div classuk-width-2-3h1欢迎注册/h1form idvm v-onsubmit: submit classuk-form uk-form-stackeddiv classuk-alert uk-alert-danger uk-hidden/divdiv classuk-form-rowlabel classuk-form-label名字:/labeldiv classuk-form-controlsinput v-modelname typetext maxlength50 placeholder名字 classuk-width-1-1/div/divdiv classuk-form-rowlabel classuk-form-label电子邮件:/labeldiv classuk-form-controlsinput v-modelemail typetext maxlength50 placeholderyour-nameexample.com classuk-width-1-1/div/divdiv classuk-form-rowlabel classuk-form-label输入口令:/labeldiv classuk-form-controlsinput v-modelpassword1 typepassword maxlength50 placeholder输入口令 classuk-width-1-1/div/divdiv classuk-form-rowlabel classuk-form-label重复口令:/labeldiv classuk-form-controlsinput v-modelpassword2 typepassword maxlength50 placeholder重复口令 classuk-width-1-1/div/divdiv classuk-form-rowbutton typesubmit classuk-button uk-button-primaryi classuk-icon-user/i 注册/button/div/form/div{% endblock %}这样我们就把用户注册的功能完成了 用户登录比用户注册复杂。由于HTTP协议是一种无状态协议而服务器要跟踪用户状态就只能通过cookie实现。大多数Web框架提供了Session功能来封装保存用户状态的cookie。
Session的优点是简单易用可以直接从Session中取出用户登录信息。
Session的缺点是服务器需要在内存中维护一个映射表来存储用户登录信息如果有两台以上服务器就需要对Session做集群因此使用Session的Web App很难扩展。
我们采用直接读取cookie的方式来验证用户登录每次用户访问任意URL都会对cookie进行验证这种方式的好处是保证服务器处理任意的URL都是无状态的可以扩展到多台服务器。
由于登录成功后是由服务器生成一个cookie发送给浏览器所以要保证这个cookie不会被客户端伪造出来。
实现防伪造cookie的关键是通过一个单向算法例如SHA1举例如下
当用户输入了正确的口令登录成功后服务器可以从数据库取到用户的id并按照如下方式计算出一个字符串
用户id 过期时间 SHA1(用户id 用户口令 过期时间 SecretKey)当浏览器发送cookie到服务器端后服务器可以拿到的信息包括 用户id 过期时间 SHA1值
如果未到过期时间服务器就根据用户id查找用户口令并计算
SHA1(用户id 用户口令 过期时间 SecretKey)并与浏览器cookie中的MD5进行比较如果相等则说明用户已登录否则cookie就是伪造的。
这个算法的关键在于SHA1是一种单向算法即可以通过原始字符串计算出SHA1结果但无法通过SHA1结果反推出原始字符串。
所以登录API可以实现如下
post(/api/authenticate)
def authenticate(*, email, passwd):if not email:raise APIValueError(email, Invalid email.)if not passwd:raise APIValueError(passwd, Invalid password.)users yield from User.findAll(email?, [email])if len(users) 0:raise APIValueError(email, Email not exist.)user users[0]# check passwd:sha1 hashlib.sha1()sha1.update(user.id.encode(utf-8))sha1.update(b:)sha1.update(passwd.encode(utf-8))if user.passwd ! sha1.hexdigest():raise APIValueError(passwd, Invalid password.)# authenticate ok, set cookie:r web.Response()r.set_cookie(COOKIE_NAME, user2cookie(user, 86400), max_age86400, httponlyTrue)user.passwd ******r.content_type application/jsonr.body json.dumps(user, ensure_asciiFalse).encode(utf-8)return r# 计算加密cookie:
def user2cookie(user, max_age):# build cookie string by: id-expires-sha1expires str(int(time.time() max_age))s %s-%s-%s-%s % (user.id, user.passwd, expires, _COOKIE_KEY)L [user.id, expires, hashlib.sha1(s.encode(utf-8)).hexdigest()]return -.join(L)对于每个URL处理函数如果我们都去写解析cookie的代码那会导致代码重复很多次。
利用middle在处理URL之前把cookie解析出来并将登录用户绑定到request对象上这样后续的URL处理函数就可以直接拿到登录用户
asyncio.coroutine
def auth_factory(app, handler):asyncio.coroutinedef auth(request):logging.info(check user: %s %s % (request.method, request.path))request.__user__ Nonecookie_str request.cookies.get(COOKIE_NAME)if cookie_str:user yield from cookie2user(cookie_str)if user:logging.info(set current user: %s % user.email)request.__user__ userreturn (yield from handler(request))return auth# 解密cookie:
asyncio.coroutine
def cookie2user(cookie_str):Parse cookie and load user if cookie is valid.if not cookie_str:return Nonetry:L cookie_str.split(-)if len(L) ! 3:return Noneuid, expires, sha1 Lif int(expires) time.time():return Noneuser yield from User.find(uid)if user is None:return Nones %s-%s-%s-%s % (uid, user.passwd, expires, _COOKIE_KEY)if sha1 ! hashlib.sha1(s.encode(utf-8)).hexdigest():logging.info(invalid sha1)return Noneuser.passwd ******return userexcept Exception as e:logging.exception(e)return None这样我们就完成了用户注册和登录的功能。
参考源码
day-10
Day 11 - 编写日志创建页
在Web开发中后端代码写起来其实是相当容易的。
例如我们编写一个REST API用于创建一个Blog
post(/api/blogs)
def api_create_blog(request, *, name, summary, content):check_admin(request)if not name or not name.strip():raise APIValueError(name, name cannot be empty.)if not summary or not summary.strip():raise APIValueError(summary, summary cannot be empty.)if not content or not content.strip():raise APIValueError(content, content cannot be empty.)blog Blog(user_idrequest.__user__.id, user_namerequest.__user__.name, user_imagerequest.__user__.image, namename.strip(), summarysummary.strip(), contentcontent.strip())yield from blog.save()return blog编写后端Python代码不但很简单而且非常容易测试上面的APIapi_create_blog()本身只是一个普通函数。
Web开发真正困难的地方在于编写前端页面。前端页面需要混合HTML、CSS和JavaScript如果对这三者没有深入地掌握编写的前端页面将很快难以维护。
更大的问题在于前端页面通常是动态页面也就是说前端页面往往是由后端代码生成的。
生成前端页面最早的方式是拼接字符串
s htmlheadtitle title /title/headbody body /body/html显然这种方式完全不具备可维护性。所以有第二种模板方式
html
headtitle{{ title }}/title
/head
body{{ body }}
/body
/htmlASP、JSP、PHP等都是用这种模板方式生成前端页面。
如果在页面上大量使用JavaScript事实上大部分页面都会模板方式仍然会导致JavaScript代码与后端代码绑得非常紧密以至于难以维护。其根本原因在于负责显示的HTML DOM模型与负责数据和交互的JavaScript代码没有分割清楚。
要编写可维护的前端代码绝非易事。和后端结合的MVC模式已经无法满足复杂页面逻辑的需要了所以新的MVVMModel View ViewModel模式应运而生。
MVVM最早由微软提出来它借鉴了桌面应用程序的MVC思想在前端页面中把Model用纯JavaScript对象表示
scriptvar blog {name: hello,summary: this is summary,content: this is content...};
/scriptView是纯HTML
form action/api/blogs methodpostinput namenameinput namesummarytextarea namecontent/textareabutton typesubmitOK/button
/form由于Model表示数据View负责显示两者做到了最大限度的分离。
把Model和View关联起来的就是ViewModel。ViewModel负责把Model的数据同步到View显示出来还负责把View的修改同步回Model。
ViewModel如何编写需要用JavaScript编写一个通用的ViewModel这样就可以复用整个MVVM模型了。
好消息是已有许多成熟的MVVM框架例如AngularJSKnockoutJS等。我们选择Vue这个简单易用的MVVM框架来实现创建Blog的页面templates/manage_blog_edit.html
{% extends __base__.html %}{% block title %}编辑日志{% endblock %}{% block beforehead %}script
varID {{ id }},action {{ action }};
function initVM(blog) {var vm new Vue({el: #vm,data: blog,methods: {submit: function (event) {event.preventDefault();var $form $(#vm).find(form);$form.postJSON(action, this.$data, function (err, r) {if (err) {$form.showFormError(err);}else {return location.assign(/api/blogs/ r.id);}});}}});$(#vm).show();
}
$(function () {if (ID) {getJSON(/api/blogs/ ID, function (err, blog) {if (err) {return fatal(err);}$(#loading).hide();initVM(blog);});}else {$(#loading).hide();initVM({name: ,summary: ,content: });}
});
/script{% endblock %}{% block content %}div classuk-width-1-1 uk-margin-bottomdiv classuk-panel uk-panel-boxul classuk-breadcrumblia href/manage/comments评论/a/lilia href/manage/blogs日志/a/lilia href/manage/users用户/a/li/ul/div/divdiv iderror classuk-width-1-1/divdiv idloading classuk-width-1-1 uk-text-centerspani classuk-icon-spinner uk-icon-medium uk-icon-spin/i 正在加载.../span/divdiv idvm classuk-width-2-3form v-onsubmit: submit classuk-form uk-form-stackeddiv classuk-alert uk-alert-danger uk-hidden/divdiv classuk-form-rowlabel classuk-form-label标题:/labeldiv classuk-form-controlsinput v-modelname namename typetext placeholder标题 classuk-width-1-1/div/divdiv classuk-form-rowlabel classuk-form-label摘要:/labeldiv classuk-form-controlstextarea v-modelsummary rows4 namesummary placeholder摘要 classuk-width-1-1 styleresize:none;/textarea/div/divdiv classuk-form-rowlabel classuk-form-label内容:/labeldiv classuk-form-controlstextarea v-modelcontent rows16 namecontent placeholder内容 classuk-width-1-1 styleresize:none;/textarea/div/divdiv classuk-form-rowbutton typesubmit classuk-button uk-button-primaryi classuk-icon-save/i 保存/buttona href/manage/blogs classuk-buttoni classuk-icon-times/i 取消/a/div/form/div{% endblock %}初始化Vue时我们指定3个参数
el根据选择器查找绑定的View这里是#vm就是id为vm的DOM对应的是一个div标签
dataJavaScript对象表示的Model我们初始化为{ name: , summary: , content: }
methodsView可以触发的JavaScript函数submit就是提交表单时触发的函数。
接下来我们在form标签中用几个简单的v-model就可以让Vue把Model和View关联起来
!-- input的value和Model的name关联起来了 --
input v-modelname classuk-width-1-1Form表单通过form v-onsubmit: submit把提交表单的事件关联到submit方法。
需要特别注意的是在MVVM中Model和View是双向绑定的。如果我们在Form中修改了文本框的值可以在Model中立刻拿到新的值。试试在表单中输入文本然后在Chrome浏览器中打开JavaScript控制台可以通过vm.name访问单个属性或者通过vm.$data访问整个Model 如果我们在JavaScript逻辑中修改了Model这个修改会立刻反映到View上。试试在JavaScript控制台输入vm.name MVVM简介可以看到文本框的内容自动被同步了 双向绑定是MVVM框架最大的作用。借助于MVVM我们把复杂的显示逻辑交给框架完成。由于后端编写了独立的REST API所以前端用AJAX提交表单非常容易前后端分离得非常彻底。
参考源码
day-11
Day 12 - 编写日志列表页
MVVM模式不但可用于Form表单在复杂的管理页面中也能大显身手。例如分页显示Blog的功能我们先把后端代码写出来
在apis.py中定义一个Page类用于存储分页信息
class Page(object):def __init__(self, item_count, page_index1, page_size10):self.item_count item_countself.page_size page_sizeself.page_count item_count // page_size (1 if item_count % page_size 0 else 0)if (item_count 0) or (page_index self.page_count):self.offset 0self.limit 0self.page_index 1else:self.page_index page_indexself.offset self.page_size * (page_index - 1)self.limit self.page_sizeself.has_next self.page_index self.page_countself.has_previous self.page_index 1def __str__(self):return item_count: %s, page_count: %s, page_index: %s, page_size: %s, offset: %s, limit: %s % (self.item_count, self.page_count, self.page_index, self.page_size, self.offset, self.limit)__repr__ __str__在handlers.py中实现API
get(/api/blogs)
def api_blogs(*, page1):page_index get_page_index(page)num yield from Blog.findNumber(count(id))p Page(num, page_index)if num 0:return dict(pagep, blogs())blogs yield from Blog.findAll(orderBycreated_at desc, limit(p.offset, p.limit))return dict(pagep, blogsblogs)管理页面
get(/manage/blogs)
def manage_blogs(*, page1):return {__template__: manage_blogs.html,page_index: get_page_index(page)}模板页面首先通过APIGET /api/blogs?page?拿到Model
{page: {has_next: true,page_index: 1,page_count: 2,has_previous: false,item_count: 12},blogs: [...]
}然后通过Vue初始化MVVM
script
function initVM(data) {var vm new Vue({el: #vm,data: {blogs: data.blogs,page: data.page},methods: {edit_blog: function (blog) {location.assign(/manage/blogs/edit?id blog.id);},delete_blog: function (blog) {if (confirm(确认要删除“ blog.name ”删除后不可恢复)) {postJSON(/api/blogs/ blog.id /delete, function (err, r) {if (err) {return alert(err.message || err.error || err);}refresh();});}}}});$(#vm).show();
}
$(function() {getJSON(/api/blogs, {page: {{ page_index }}}, function (err, results) {if (err) {return fatal(err);}$(#loading).hide();initVM(results);});
});
/scriptView的容器是#vm包含一个table我们用v-repeat可以把Model的数组blogs直接变成多行的tr
div idvm classuk-width-1-1a href/manage/blogs/create classuk-button uk-button-primaryi classuk-icon-plus/i 新日志/atable classuk-table uk-table-hovertheadtrth classuk-width-5-10标题 / 摘要/thth classuk-width-2-10作者/thth classuk-width-2-10创建时间/thth classuk-width-1-10操作/th/tr/theadtbodytr v-repeatblog: blogs tda target_blank v-attrhref: /blog/blog.id v-textblog.name/a/tdtda target_blank v-attrhref: /user/blog.user_id v-textblog.user_name/a/tdtdspan v-textblog.created_at.toDateTime()/span/tdtda href#0 v-onclick: edit_blog(blog)i classuk-icon-edit/ia href#0 v-onclick: delete_blog(blog)i classuk-icon-trash-o/i/td/tr/tbody/tablediv v-componentpagination v-withpage/div
/div往Model的blogs数组中增加一个Blog元素table就神奇地增加了一行把blogs数组的某个元素删除table就神奇地减少了一行。所有复杂的Model-View的映射逻辑全部由MVVM框架完成我们只需要在HTML中写上v-repeat指令就什么都不用管了。
可以把v-repeatblog: blogs看成循环代码所以可以在一个tr内部引用循环变量blog。v-text和v-attr指令分别用于生成文本和DOM节点属性。
完整的Blog列表页如下 参考源码
day-12
Day 13 - 提升开发效率
现在我们已经把一个Web App的框架完全搭建好了从后端的API到前端的MVVM流程已经跑通了。
在继续工作前注意到每次修改Python代码都必须在命令行先Ctrl-C停止服务器再重启改动才能生效。
在开发阶段每天都要修改、保存几十次代码每次保存都手动来这么一下非常麻烦严重地降低了我们的开发效率。有没有办法让服务器检测到代码修改后自动重新加载呢
Django的开发环境在Debug模式下就可以做到自动重新加载如果我们编写的服务器也能实现这个功能就能大大提升开发效率。
可惜的是Django没把这个功能独立出来不用Django就享受不到怎么办
其实Python本身提供了重新载入模块的功能但不是所有模块都能被重新载入。另一种思路是检测www目录下的代码改动一旦有改动就自动重启服务器。
按照这个思路我们可以编写一个辅助程序pymonitor.py让它启动wsgiapp.py并时刻监控www目录下的代码改动有改动时先把当前wsgiapp.py进程杀掉再重启就完成了服务器进程的自动重启。
要监控目录文件的变化我们也无需自己手动定时扫描Python的第三方库watchdog可以利用操作系统的API来监控目录文件的变化并发送通知。我们先用pip安装
$ pip3 install watchdog利用watchdog接收文件变化的通知如果是.py文件就自动重启wsgiapp.py进程。
利用Python自带的subprocess实现进程的启动和终止并把输入输出重定向到当前进程的输入输出中
#!/usr/bin/env python3
# -*- coding: utf-8 -*-__author__ Michael Liaoimport os, sys, time, subprocessfrom watchdog.observers import Observer
from watchdog.events import FileSystemEventHandlerdef log(s):print([Monitor] %s % s)class MyFileSystemEventHander(FileSystemEventHandler):def __init__(self, fn):super(MyFileSystemEventHander, self).__init__()self.restart fndef on_any_event(self, event):if event.src_path.endswith(.py):log(Python source file changed: %s % event.src_path)self.restart()command [echo, ok]
process Nonedef kill_process():global processif process:log(Kill process [%s]... % process.pid)process.kill()process.wait()log(Process ended with code %s. % process.returncode)process Nonedef start_process():global process, commandlog(Start process %s... % .join(command))process subprocess.Popen(command, stdinsys.stdin, stdoutsys.stdout, stderrsys.stderr)def restart_process():kill_process()start_process()def start_watch(path, callback):observer Observer()observer.schedule(MyFileSystemEventHander(restart_process), path, recursiveTrue)observer.start()log(Watching directory %s... % path)start_process()try:while True:time.sleep(0.5)except KeyboardInterrupt:observer.stop()observer.join()if __name__ __main__:argv sys.argv[1:]if not argv:print(Usage: ./pymonitor your-script.py)exit(0)if argv[0] ! python3:argv.insert(0, python3)command argvpath os.path.abspath(.)start_watch(path, None)一共70行左右的代码就实现了Debug模式的自动重新加载。用下面的命令启动服务器
$ python3 pymonitor.py wsgiapp.py或者给pymonitor.py加上可执行权限启动服务器
$ ./pymonitor.py app.py在编辑器中打开一个.py文件修改后保存看看命令行输出是不是自动重启了服务器
$ ./pymonitor.py app.py
[Monitor] Watching directory /Users/michael/Github/awesome-python3-webapp/www...
[Monitor] Start process python app.py...
...
INFO:root:application (/Users/michael/Github/awesome-python3-webapp/www) will start at 0.0.0.0:9000...
[Monitor] Python source file changed: /Users/michael/Github/awesome-python-webapp/www/handlers.py
[Monitor] Kill process [2747]...
[Monitor] Process ended with code -9.
[Monitor] Start process python app.py...
...
INFO:root:application (/Users/michael/Github/awesome-python3-webapp/www) will start at 0.0.0.0:9000...现在只要一保存代码就可以刷新浏览器看到效果大大提升了开发效率。
Day 14 - 完成Web App
在Web App框架和基本流程跑通后剩下的工作全部是体力活了在Debug开发模式下完成后端所有API、前端所有页面。我们需要做的事情包括
把当前用户绑定到request上并对URL/manage/进行拦截检查当前用户是否是管理员身份
asyncio.coroutine
def auth_factory(app, handler):asyncio.coroutinedef auth(request):logging.info(check user: %s %s % (request.method, request.path))request.__user__ Nonecookie_str request.cookies.get(COOKIE_NAME)if cookie_str:user yield from cookie2user(cookie_str)if user:logging.info(set current user: %s % user.email)request.__user__ userif request.path.startswith(/manage/) and (request.__user__ is None or not request.__user__.admin):return web.HTTPFound(/signin)return (yield from handler(request))return auth后端API包括 获取日志GET /api/blogs 创建日志POST /api/blogs 修改日志POST /api/blogs/:blog_id 删除日志POST /api/blogs/:blog_id/delete 获取评论GET /api/comments 创建评论POST /api/blogs/:blog_id/comments 删除评论POST /api/comments/:comment_id/delete 创建新用户POST /api/users 获取用户GET /api/users
管理页面包括 评论列表页GET /manage/comments 日志列表页GET /manage/blogs 创建日志页GET /manage/blogs/create 修改日志页GET /manage/blogs/ 用户列表页GET /manage/users
用户浏览页面包括 注册页GET /register 登录页GET /signin 注销页GET /signout 首页GET / 日志详情页GET /blog/:blog_id
把所有的功能实现我们第一个Web App就宣告完成
参考源码
day-14
Day 15 - 部署Web App
作为一个合格的开发者在本地环境下完成开发还远远不够我们需要把Web App部署到远程服务器上这样广大用户才能访问到网站。
很多做开发的同学把部署这件事情看成是运维同学的工作这种看法是完全错误的。首先最近流行DevOps理念就是说开发和运维要变成一个整体。其次运维的难度其实跟开发质量有很大的关系。代码写得垃圾运维再好也架不住天天挂掉。最后DevOps理念需要把运维、监控等功能融入到开发中。你想服务器升级时不中断用户服务那就得在开发时考虑到这一点。
下面我们就来把awesome-python3-webapp部署到Linux服务器。
搭建Linux服务器
要部署到Linux首先得有一台Linux服务器。要在公网上体验的同学可以在Amazon的AWS申请一台EC2虚拟机免费使用1年或者使用国内的一些云服务器一般都提供Ubuntu Server的镜像。想在本地部署的同学请安装虚拟机推荐使用VirtualBox。
我们选择的Linux服务器版本是Ubuntu Server 14.04 LTS原因是apt太简单了。如果你准备使用其他Linux版本也没有问题。
Linux安装完成后请确保ssh服务正在运行否则需要通过apt安装
$ sudo apt-get install openssh-server有了ssh服务就可以从本地连接到服务器上。建议把公钥复制到服务器端用户的.ssh/authorized_keys中这样就可以通过证书实现无密码连接。
部署方式
利用Python自带的asyncio我们已经编写了一个异步高性能服务器。但是我们还需要一个高性能的Web服务器这里选择Nginx它可以处理静态资源同时作为反向代理把动态请求交给Python代码处理。这个模型如下 Nginx负责分发请求 在服务器端我们需要定义好部署的目录结构
/
- srv/- awesome/ -- Web App根目录- www/ -- 存放Python源码| - static/ -- 存放静态资源文件- log/ -- 存放log在服务器上部署要考虑到新版本如果运行不正常需要回退到旧版本时怎么办。每次用新的代码覆盖掉旧的文件是不行的需要一个类似版本控制的机制。由于Linux系统提供了软链接功能所以我们把www作为一个软链接它指向哪个目录哪个目录就是当前运行的版本 而Nginx和gunicorn的配置文件只需要指向www目录即可。
Nginx可以作为服务进程直接启动但gunicorn还不行所以Supervisor登场Supervisor是一个管理进程的工具可以随系统启动而启动服务它还时刻监控服务进程如果服务进程意外退出Supervisor可以自动重启服务。
总结一下我们需要用到的服务有 Nginx高性能Web服务器负责反向代理 Supervisor监控服务进程的工具 MySQL数据库服务。
在Linux服务器上用apt可以直接安装上述服务
$ sudo apt-get install nginx supervisor python3 mysql-server然后再把我们自己的Web App用到的Python库安装了
$ sudo pip3 install jinja2 aiomysql aiohttp在服务器上创建目录/srv/awesome/以及相应的子目录。
在服务器上初始化MySQL数据库把数据库初始化脚本schema.sql复制到服务器上执行
$ mysql -u root -p schema.sql服务器端准备就绪。
部署
用FTP还是SCP还是rsync复制文件如果你需要手动复制用一次两次还行一天如果部署50次不但慢、效率低而且容易出错。
正确的部署方式是使用工具配合脚本完成自动化部署。Fabric就是一个自动化部署工具。由于Fabric是用Python 2.x开发的所以部署脚本要用Python 2.7来编写本机还必须安装Python 2.7版本。
要用Fabric部署需要在本机是开发机器不是Linux服务器安装Fabric
$ easy_install fabricLinux服务器上不需要安装FabricFabric使用SSH直接登录服务器并执行部署命令。
下一步是编写部署脚本。Fabric的部署脚本叫fabfile.py我们把它放到awesome-python-webapp的目录下与www目录平级
awesome-python-webapp/
- fabfile.py
- www/
- ...Fabric的脚本编写很简单首先导入Fabric的API设置部署时的变量
# fabfile.py
import os, re
from datetime import datetime# 导入Fabric API:
from fabric.api import *# 服务器登录用户名:
env.user michael
# sudo用户为root:
env.sudo_user root
# 服务器地址可以有多个依次部署:
env.hosts [192.168.0.3]# 服务器MySQL用户名和口令:
db_user www-data
db_password www-data然后每个Python函数都是一个任务。我们先编写一个打包的任务
_TAR_FILE dist-awesome.tar.gzdef build():includes [static, templates, transwarp, favicon.ico, *.py]excludes [test, .*, *.pyc, *.pyo]local(rm -f dist/%s % _TAR_FILE)with lcd(os.path.join(os.path.abspath(.), www)):cmd [tar, --dereference, -czvf, ../dist/%s % _TAR_FILE]cmd.extend([--exclude\%s\ % ex for ex in excludes])cmd.extend(includes)local( .join(cmd))Fabric提供local(...)来运行本地命令with lcd(path)可以把当前命令的目录设定为lcd()指定的目录注意Fabric只能运行命令行命令Windows下可能需要Cgywin环境。
在awesome-python-webapp目录下运行
$ fab build看看是否在dist目录下创建了dist-awesome.tar.gz的文件。
打包后我们就可以继续编写deploy任务把打包文件上传至服务器解压重置www软链接重启相关服务
_REMOTE_TMP_TAR /tmp/%s % _TAR_FILE
_REMOTE_BASE_DIR /srv/awesomedef deploy():newdir www-%s % datetime.now().strftime(%y-%m-%d_%H.%M.%S)# 删除已有的tar文件:run(rm -f %s % _REMOTE_TMP_TAR)# 上传新的tar文件:put(dist/%s % _TAR_FILE, _REMOTE_TMP_TAR)# 创建新目录:with cd(_REMOTE_BASE_DIR):sudo(mkdir %s % newdir)# 解压到新目录:with cd(%s/%s % (_REMOTE_BASE_DIR, newdir)):sudo(tar -xzvf %s % _REMOTE_TMP_TAR)# 重置软链接:with cd(_REMOTE_BASE_DIR):sudo(rm -f www)sudo(ln -s %s www % newdir)sudo(chown www-data:www-data www)sudo(chown -R www-data:www-data %s % newdir)# 重启Python服务和nginx服务器:with settings(warn_onlyTrue):sudo(supervisorctl stop awesome)sudo(supervisorctl start awesome)sudo(/etc/init.d/nginx reload)注意run()函数执行的命令是在服务器上运行with cd(path)和with lcd(path)类似把当前目录在服务器端设置为cd()指定的目录。如果一个命令需要sudo权限就不能用run()而是用sudo()来执行。
配置Supervisor
上面让Supervisor重启awesome的命令会失败因为我们还没有配置Supervisor呢。
编写一个Supervisor的配置文件awesome.conf存放到/etc/supervisor/conf.d/目录下
[program:awesome]command /srv/awesome/www/app.py
directory /srv/awesome/www
user www-data
startsecs 3redirect_stderr true
stdout_logfile_maxbytes 50MB
stdout_logfile_backups 10
stdout_logfile /srv/awesome/log/app.log配置文件通过[program:awesome]指定服务名为awesomecommand指定启动app.py。
然后重启Supervisor后就可以随时启动和停止Supervisor管理的服务了
$ sudo supervisorctl reload
$ sudo supervisorctl start awesome
$ sudo supervisorctl status
awesome RUNNING pid 1401, uptime 5:01:34配置Nginx
Supervisor只负责运行gunicorn我们还需要配置Nginx。把配置文件awesome放到/etc/nginx/sites-available/目录下
server {listen 80; # 监听80端口root /srv/awesome/www;access_log /srv/awesome/log/access_log;error_log /srv/awesome/log/error_log;# server_name awesome.liaoxuefeng.com; # 配置域名# 处理静态文件/favicon.ico:location /favicon.ico {root /srv/awesome/www;}# 处理静态资源:location ~ ^\/static\/.*$ {root /srv/awesome/www;}# 动态请求转发到9000端口:location / {proxy_pass http://127.0.0.1:9000;proxy_set_header X-Real-IP $remote_addr;proxy_set_header Host $host;proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;}
}然后在/etc/nginx/sites-enabled/目录下创建软链接
$ pwd
/etc/nginx/sites-enabled
$ sudo ln -s /etc/nginx/sites-available/awesome .让Nginx重新加载配置文件不出意外我们的awesome-python3-webapp应该正常运行
$ sudo /etc/init.d/nginx reload如果有任何错误都可以在/srv/awesome/log下查找Nginx和App本身的log。如果Supervisor启动时报错可以在/var/log/supervisor下查看Supervisor的log。
如果一切顺利你可以在浏览器中访问Linux服务器上的awesome-python3-webapp了 如果在开发环境更新了代码只需要在命令行执行
$ fab build
$ fab deploy自动部署完成刷新浏览器就可以看到服务器代码更新后的效果。
友情链接
嫌国外网速慢的童鞋请移步网易和搜狐的镜像站点
http://mirrors.163.com/
http://mirrors.sohu.com/
参考源码
day-15
Day 16 - 编写移动App
网站部署上线后还缺点啥呢
在移动互联网浪潮席卷而来的今天一个网站没有上线移动App出门根本不好意思跟人打招呼。
所以awesome-python3-webapp必须得有一个移动App版本
开发iPhone版本
我们首先来看看如何开发iPhone App。前置条件一台Mac电脑安装XCode和最新的iOS SDK。
在使用MVVM编写前端页面时我们就能感受到用REST API封装网站后台的功能不但能清晰地分离前端页面和后台逻辑现在这个好处更加明显移动App也可以通过REST API从后端拿到数据。
我们来设计一个简化版的iPhone App包含两个屏幕列出最新日志和阅读日志的详细内容 只需要调用API/api/blogs。
在XCode中完成App编写 由于我们的教程是Python关于如何开发iOS请移步Develop Apps for iOS。
点击下载iOS App源码。
如何编写Android App这个当成作业了。
参考源码
day-16
FAQ
常见问题
本节列出常见的一些问题。
如何获取当前路径
当前路径可以用.表示再用os.path.abspath()将其转换为绝对路径
# -*- coding:utf-8 -*-
# test.pyimport osprint(os.path.abspath(.))运行结果
$ python3 test.py
/Users/michael/workspace/testing如何获取当前模块的文件名
可以通过特殊变量__file__获取
# -*- coding:utf-8 -*-
# test.pyprint(__file__)输出
$ python3 test.py
test.py如何获取命令行参数
可以通过sys模块的argv获取
# -*- coding:utf-8 -*-
# test.pyimport sysprint(sys.argv)输出
$ python3 test.py -a -s Hello world
[test.py, -a, -s, Hello world]argv的第一个元素永远是命令行执行的.py文件名。
如何获取当前Python命令的可执行文件路径
sys模块的executable变量就是Python命令可执行文件的路径
# -*- coding:utf-8 -*-
# test.pyimport sysprint(sys.executable)在Mac下的结果
$ python3 test.py
/usr/local/opt/python3/bin/python3.4期末总结
终于到了期末总结的时刻了
经过一段时间的学习相信你对Python已经初步掌握。一开始可能觉得Python上手很容易可是越往后学会越困难有的时候发现理解不了代码这时不妨停下来思考一下先把概念搞清楚代码自然就明白了。
Python非常适合初学者用来进入计算机编程领域。Python属于非常高级的语言掌握了这门高级语言就对计算机编程的核心思想——抽象有了初步理解。如果希望继续深入学习计算机编程可以学习C、JavaScript、Lisp等不同类型的语言只有多掌握不同领域的语言有比较才更有收获。 from: http://www.liaoxuefeng.com/wiki/0014316089557264a6b348958f449949df42a6d3a2e542c000