├── Difference_between_DeepCopy_and_ShallowCopy.md ├── Efficient_Pandas_Skills.md ├── Interpret_PEP8.md ├── Learn_Data_Science_In_Python.md ├── My_Python_Notebook ├── 01Python基础.md ├── 02函数.md ├── 03高级特性.md ├── 04函数式编程.md ├── 05模块.md ├── 06面向对象编程.md ├── 07面向对象高级编程.md ├── 08错误、调试与测试.md ├── 09IO编程.md ├── 10进程和线程.md ├── 11正则表达式.md ├── 12常用内建模块(上).md ├── 12常用内建模块(下).md ├── 13常用第三方模块.md ├── 14virtualenv.md ├── 15图形界面.md ├── 16网络编程.md ├── 17电子邮件.md ├── 18访问数据库.md ├── 19Web开发.md ├── 20异步IO.md ├── README.md └── Res │ └── Our Events _ Python.org.html ├── Numpy_Learning.md ├── Pandas_learning.md ├── Python_Libraries_For_Building_Recommender_Systems.md ├── README.md ├── Turtle_Diagram_of_Python_Learning.md └── graph ├── Excecl_Pivot_Table.jpg ├── Turtle_Diagram_of_Python_Learning.png ├── pandas_box_1.png ├── pandas_box_2.png ├── pandas_box_3.png └── pandas_hist.png /Difference_between_DeepCopy_and_ShallowCopy.md: -------------------------------------------------------------------------------- 1 | # 深拷贝与浅拷贝的区别 2 | 3 | 关于Python中的深拷贝和浅拷贝,有一篇很好的文章:[图解Python深拷贝和浅拷贝](http://www.cnblogs.com/wilber2013/p/4645353.html),这篇文章用示例代码和图解很好地阐释了两者的区别。这里自己简单地归纳一下。 4 | 5 | ## 直接赋值 6 | 7 | 首先,如果我们不进行拷贝,而是直接赋值,很有可能会出现意料之外的结果。比如a是一个列表,b=a,那么修改a的同时,b也会同样被修改,因为Python对象的赋值都是进行引用(内存地址)传递的,实际上a和b指向的都是同一个对象。 8 | 9 | ``` 10 | >>> a = [1,2,3] 11 | >>> b = a 12 | >>> a[2] = 4 13 | >>> a 14 | [1, 2, 4] 15 | >>> b 16 | [1, 2, 4] 17 | >>> a is b 18 | True 19 | ``` 20 | 21 | ## 浅拷贝 22 | 23 | 为了避免这种情况发生,我们可以使用浅拷贝: 24 | 25 | ``` 26 | >>> a = [1,2,3] 27 | >>> import copy 28 | >>> b = copy.copy(a) 29 | >>> b is a 30 | False 31 | >>> a[0] is b[0] 32 | True 33 | ``` 34 | 35 | 浅拷贝会创建一个新的对象,然后把生成的新对象赋值给新变量。注意这句话的意思,上面这个例子中1,2,3这三个int型对象并没有创建新的,新的对象是指copy创建了一个新的**列表对象**,这样a和b这两个变量指向的列表对象就不是同一个,但和两个列表**对象里面的元素依然是按引用传递**的,所以a列表中的对象1和b列表中的对象1是同一个。 但是这时修改a列表的**不可变**对象,b列表不会受到影响: 36 | 37 | ``` 38 | >>> a[0] = 4 39 | >>> a 40 | [4, 2, 3] 41 | >>> b 42 | [1, 2, 3] 43 | ``` 44 | 45 | 由于浅拷贝时,对于对象中的元素,浅拷贝只会使用原始元素的引用(内存地址),所以如果对象中的元素是可变对象,浅拷贝就没辙了。比方说列表中包含一个列表,这时改动a,浅拷贝的b依然可能受影响: 46 | 47 | ``` 48 | >>> a = [1,2,[3,]] 49 | >>> b = copy.copy(a) 50 | >>> a is b 51 | False 52 | >>> a[2].append(4) 53 | >>> a 54 | [1, 2, [3, 4]] 55 | >>> b 56 | [1, 2, [3, 4]] 57 | ``` 58 | 59 | 可以产生浅拷贝的操作有以下几种: 60 | 61 | - 使用切片[:]操作 62 | 63 | - 使用[工厂函数](https://www.zhihu.com/question/20670869)(如list/dir/set) 64 | > 工厂函数看上去像函数,实质上是类,调用时实际上是生成了该类型的一个实例,就像工厂生产货物一样. 65 | 66 | - 使用copy模块中的copy()函数 67 | 68 | ## 深拷贝 69 | 70 | 对于这个问题,又引入了深拷贝机制,这时不仅创建了新的对象,连对象中的元素都是新的,深拷贝都会重新生成一份,而不是简单的使用原始元素的引用(内存地址)。注意了,对象中的元素,不可变对象还是使用引用,因为没有重新生成的必要,变量改动时会自动生成另一个不可变对象,然后改变引用的地址。但可变对象的内容是可变的,改动后不会产生新的对象,也不会改变引用地址,所以需要重新生成。 71 | 72 | ``` 73 | >>> a = [1,2,[3,]] 74 | >>> b = copy.deepcopy(a) 75 | >>> a is b 76 | False 77 | >>> a[0] is b[0] 78 | True 79 | >>> a[2] is b[2] 80 | False 81 | ``` 82 | 83 | 这时再改变a中的元素对b就完全没有影响了: 84 | 85 | ``` 86 | [1, 2, [3]] 87 | >>> a = [1,2,[3,]] 88 | >>> b = copy.deepcopy(a) 89 | >>> a[2].append(4) 90 | >>> a 91 | [1, 2, [3, 4]] 92 | >>> b 93 | [1, 2, [3]] 94 | ``` 95 | 96 | ## 特殊情况 97 | 98 | - 对于非容器类型(如数字、字符串、和其他'原子'类型的对象)是没有拷贝这个说法的。 99 | 100 | ``` 101 | >>> a = 'hello' 102 | >>> b = copy.copy(a) 103 | >>> c = copy.deepcopy(a) 104 | >>> a is b 105 | True 106 | >>> a is c 107 | True 108 | ``` 109 | 110 | 对于这种类型的对象,无论是浅拷贝还是深拷贝都不会创建新的对象。 111 | 112 | - 如果元祖变量只包含[原子类型](http://www.cnblogs.com/nzbbody/p/3551862.html)对象,则不能深拷贝: 113 | 114 | >原子类型指所有的数值类型以及字符串 115 | 116 | ``` 117 | >>> a=(1,2,3) 118 | >>> a = (1,2,3) 119 | >>> b = copy.copy(a) 120 | >>> c = copy.deepcopy(a) 121 | >>> a is b 122 | True 123 | >>> a is c 124 | True 125 | ``` 126 | 127 | 元组本身是不可变对象,如果元组里的元素也是不可变对象,就没有进行拷贝的必要了。实测如果元组里面的元素是只包含原子类型对象的元组,则也属于这个范畴。 128 | 129 | ``` 130 | >>> a = (1,2,(3,)) 131 | >>> b = copy.copy(a) 132 | >>> c = copy.deepcopy(a) 133 | >>> a is c 134 | True 135 | >>> a is b 136 | True 137 | ``` 138 | 139 | ## 总结 140 | 141 | - Python中对象的赋值都是进行对象引用(内存地址)传递 142 | 143 | - 使用copy.copy(),可以进行对象的浅拷贝,它复制了对象,但对于对象中的元素,依然使用原始的引用. 144 | 145 | - 如果需要复制一个容器对象,以及它里面的所有元素(包含元素的子元素),可以使用copy.deepcopy()进行深拷贝 146 | 147 | - 非容器类型(如数字、字符串、和其他'原子'类型的对象)不存在拷贝。 148 | 149 | - 只包含原子类型对象的元组变量不存在拷贝。 150 | -------------------------------------------------------------------------------- /Learn_Data_Science_In_Python.md: -------------------------------------------------------------------------------- 1 | #Learn Data Science in Python 2 | 3 | 这篇笔记取自一个帖子:[(Python环境) 信息图:用Python做数据科学](http://dataunion.org/23862.html)。 帖子提供了一份信息图表,讲述利用Python学习数据科学的学习路径,这里进行记录并翻译,也为自己进一步学习定下目标。 4 | 5 | 更好的翻译版本可以参见博文:[数据科学的完整学习路径—Python版](https://www.zybuluo.com/chanvee/note/87377) 6 | 7 | ## Beginner Level 8 | 9 | ###STEP 0 10 | Before beginning you should have the answers for: 11 | 12 | - Why should I learn Python? 13 | 14 | - How would Python be useful? 15 | 16 | 在正式开始前,你应该对下列两个问题给出答案: 17 | 18 | - 你为什么要学习Python? 19 | 20 | - Python能帮到你什么? 21 | 22 | **Ans**:常常听说一句话 **`人生苦短,我用Python`**,其实并非假话,在学习过C,C++,Java以后,使用Python的第一感觉就是简洁!快!这个快不是只运行速度哈,而是指编写代码非常短,别的语言10几20几行的代码才能实现的功能,Python只用1,2行代码就实现了。我们可以更多地关心程序的思路而非代码语法。就这点而言,使用Python给人一种无与伦比的愉悦感。 23 | 24 | 当然,光是这样是不够的,关键的是Python还有许多成熟的库供我们调用,Numpy,Pandas,Matplotlib等等,各种神器可以供我们调用,在数据科学这一领域为我们提供了强大的社区支持。所以使用Python来学习数据科学是非常友好的。 25 | 26 | ###STEP 1 27 | 28 | Set up your Machine 29 | 30 | - Download and Install Anaconda 31 | 32 | 配置机器 33 | 34 | - 下载和安装Anaconda 35 | 36 | 为什么不直接使用Python而是用[Anaconda](http://seisman.info/anaconda-scientific-python-distribution.html)这个科学计算发行版呢?因为进行科学计算我们要安装很多库,而这些库之间存在依赖关系,新手要正确配置好可能还是有些麻烦,而Anaconda就为我们解决了这个问题,大部分数据科学需要用到的库都已经安装好了,我们可以直接import使用。 37 | 38 | ###STEP 2 39 | 40 | Get a grip on basics of Python 41 | 42 | - Begin with Codecademy 43 | 44 | - Properly understand Lists, Tuples Dictionaries, List comprehensions, Dictionary comprehensions. 45 | 46 | - Start solving Python assignments 47 | 48 | 掌握Python的基础知识 49 | 50 | - 使用[Codecademy](https://www.codecademy.com/zh/)进行入门练习 51 | 52 | - 理解Python中列表,元组和字典的使用。 53 | 54 | - 着手解决一些Python习题 55 | 56 | ##Intermediate Level 57 | 58 | ###STEP 3 59 | 60 | Master the use of regular expressions in Python 61 | 62 | - Take google class on Python 63 | 64 | - Do 'baby names' excercise 65 | 66 | - Understand about Text Mining 67 | 68 | 69 | 掌握Python中正则的使用方式 70 | 71 | - 学习Google提供的Python课程 72 | 73 | - 完成 ['baby names'](https://developers.google.com/edu/python/exercises/baby-names) 练习,这是一个使用正则匹配婴儿名字的练习。 74 | 75 | - 理解文本清理,可以浏览这个[教程](http://www.analyticsvidhya.com/blog/2014/11/text-data-cleaning-steps-python/)。 76 | 77 | ###STEP 4 78 | 79 | Gain mastery on Scientific Libraries in Python 80 | 81 | - NumPy, SciPy, Matplotlib, Pandas 82 | 83 | - Focus on NumPy Arrays 84 | 85 | - Go through tutorials of NumPy, SciPy, Pandas 86 | 87 | 掌握Python的科学计算库 88 | 89 | - NumPy, SciPy, Matplotlib, Pandas 90 | 91 | - 特别注重练习NumPy的Arrays,这是一个非常强大高效的数组结构。 92 | 93 | - 过一遍NumPy, SciPy, Pandas的入门教程。 94 | 95 | ###STEP 5 96 | 97 | Data Visualization in Pyton 98 | 99 | - Harvard's tutorial on DV 100 | 101 | - Practice Assignment 102 | 103 | 数据可视化 104 | 105 | - 学习[哈佛大学的课程](http://cm.dce.harvard.edu/2015/01/14328/L03/screen_H264LargeTalkingHead-16x9.shtml) 106 | 107 | - 然后完成这个[练习](http://nbviewer.jupyter.org/github/cs109/2014/blob/master/homework/HW2.ipynb)。 108 | 109 | ##Advanced Level 110 | 111 | ###STEP 6 112 | 113 | Time to conquer SciKit Learn and Machine Learning 114 | 115 | - Refer to Havard's CS109 course 116 | 117 | - Machine Learning course by Andrew Ng 118 | 119 | - Practice Assignments 120 | 121 | 学习机器学习以及相应的库SciKit Learn 122 | 123 | - 参照[哈佛的CS109课程](http://cs109.github.io/2014/) 124 | 125 | - 学习吴恩达的机器学习课程(此人是机器学习领域的超级大牛,现在百度的首席科学家) 126 | 127 | - 练习练习练习! 128 | 129 | ###STEP 7 130 | 131 | Practice makes a man perfect 132 | 133 | - Practice the above steps religously 134 | 135 | 只要功夫深铁杵磨成针,到这一步,基本已经学习完毕了,剩下的就是保持不断的练习,使得自己能熟练运用前面学到的知识。可以在[Kaggle](http://www.kaggle.com/)上参加比赛进行实战。 136 | 137 | ##Expert Level 138 | 139 | ###STEP 8 140 | 141 | Dive deep into DeepLearning 142 | 143 | - Course by Geoff Hinton 144 | 145 | 关注深度学习 146 | 147 | - 学习 [Geoff Hinton的课程](https://www.coursera.org/course/neuralnets),此人是当今深度学习领域三大牛之一! 148 | -------------------------------------------------------------------------------- /My_Python_Notebook/01Python基础.md: -------------------------------------------------------------------------------- 1 | # Python基础 2 | 3 | ## 目录 4 | 5 | 6 | 7 | - [数据类型和变量](#数据类型和变量) 8 | - [数据类型](#数据类型) 9 | - [变量](#变量) 10 | - [常量](#常量) 11 | - [使用list和tuple](#使用list和tuple) 12 | - [list](#list) 13 | - [tuple](#tuple) 14 | - [使用dict和set](#使用dict和set) 15 | - [dict](#dict) 16 | - [set](#set) 17 | - [再议不可变对象](#再议不可变对象) 18 | - [条件判断](#条件判断) 19 | - [条件判断](#条件判断-1) 20 | - [再议input](#再议input) 21 | - [循环](#循环) 22 | - [字符串和编码](#字符串和编码) 23 | - [字符编码](#字符编码) 24 | - [字符串](#字符串) 25 | - [格式化](#格式化) 26 | 27 | 28 | 29 | 30 | ## 数据类型和变量 31 | 32 | ### 数据类型 33 | 34 | Python可以直接表达的数据类型包括:整数,浮点数,复数,字符串,布尔值和空值。 35 | 36 | ##### 整数 int 37 | 38 | 任意大小,正负皆可。并且可以用0b、0o、0x分别表示二进制、八进制和十六进制数: 39 | 40 | ```python 41 | >>> 0b10 # 也可以是 0B10 42 | 2 43 | >>> 0o10 # 也可以是 0O10 44 | 8 45 | >>> 0x10 # 也可以是 0X10 46 | 16 47 | 48 | # 十进制转其他进制 49 | >>> bin(2) 50 | '0b10' 51 | >>> oct(8) 52 | '0o10' 53 | >>> hex(16) 54 | '0x10' 55 | 56 | # 其他进制转十进制(也可将字符串转换为十进制) 57 | >>> int(0b10) # 也可以是 int('10', 2) 58 | 2 59 | >>> int(0o10) # 也可以是 int('10', 8) 60 | 8 61 | >>> int(0x10) # 也可以是 int('10', 16) 62 | 16 63 | ``` 64 | 65 | ##### 浮点数 66 | 67 | 除了普通的写法之外,也可使**用科学记数法**,即1.23x10^9就是1.23e9 ,0.000012可以写成1.2e-5,等等。 68 | 69 | **整数运算永远是精确的(结果也是整数),浮点数运算则有四舍五入的误差**。 70 | 71 | Python提供两种除法+求余运算: 72 | 73 | | 符号 | 用途 | 74 | | :-: | :-: | 75 | |**`/`**|结果为浮点数,即使两数整除,**结果也是浮点数**| 76 | |**`//`**|结果为整数,也称地板除,对**结果向下取整**| 77 | |**`%`**|结果为整数,用于计算余数| 78 | 79 | ##### 复数 80 | 81 | Python中复数用j表示虚数部分,比如说 `x=2.4 + 5.6j`, 并且可以通过复数的类属性 `real` 取得实部,类属性 `imag` 取得虚部,通过类方法 `conjugate()` 获得共轭复数。 82 | 83 | ```python 84 | >>> x = 2.4 + 5.6j 85 | >>> x.real 86 | 2.4 87 | >>> x.imag 88 | 5.6 89 | >>> x.conjugate() 90 | (2.4-5.6j) 91 | ``` 92 | 93 | ##### 字符串 94 | 95 | 使用单引号或双引号括起。而引号本身需要使用转义符` \ `来表达。 用` \' `来表示` ' `。 96 | 97 | 转义符还可以用来转义很多字符,如` \n `表示换行。` \t `表示制表符。 ` \ `本身也需要转义用 **双斜杠** 来代替。 98 | 99 | 如果一个字符串中很多字符都要转义就会很麻烦,所以Python又提供一种简便的写法,**` r'' `表示两个引号之间的内容是不需要转义的**。 对于多行的字符串,为了避免加入` \n `的不方便,可以使用` '''something''' `的格式,即**用三个引号来括起字符串,换行会被保留**。 100 | 101 | ```python 102 | # 没使用三引号 103 | >>> a = '123 104 | File "", line 1 105 | a = '123 106 | ^ 107 | SyntaxError: EOL while scanning string literal 108 | 109 | # 使用了三引号 110 | >>> a = '''123 111 | ... 456 112 | ... 789''' 113 | >>> a 114 | '123\n456\n789' 115 | ``` 116 | 117 | ##### 布尔值 118 | 119 | 只有True和False两种,首字母大写。如果输入一个判断式,则Python会给出布尔值的结果。比方说输入3>2,运行后会得到True。 对于布尔值有三个逻辑运算符,分别是and,or和not。 **本质上True用1存储,False用0存储**。 120 | 121 | ```python 122 | >>> 1 == 1 123 | True 124 | >>> 1 == True 125 | True 126 | >>> 0 != 0 127 | False 128 | >>> 0 == False 129 | True 130 | ``` 131 | 132 | ##### 空值 133 | 134 | Python中用None表示,**不同于数值0**。数值0是有意义的,而None是一个特殊的空值。可以将None赋值给变量,但无法创建None以外的其他NoneType对象。 135 | 136 | ```python 137 | >>> type(None) 138 | 139 | >>> a = None 140 | >>> type(a) 141 | 142 | >>> None == a 143 | True 144 | >>> None == '' 145 | False 146 | >>> None == 0 147 | False 148 | ``` 149 | 150 | --- 151 | 152 | ### 变量 153 | 154 | **Python是一门动态类型语言**,这意味着在Python中,**可以反复把任意数据类型的对象赋值给同一个变量**,相比起静态语言更加地灵活。 155 | 156 | 在Python中变量名不可以以数字开头,构成可以包括大小写英文,数字及下划线。 157 | 158 | 当我们写:a=’ABC’ 时,Python解释器干了两件事情: 159 | 160 | 1. 在内存中创建了一个值为`'ABC'`的字符串对象; 161 | 2. 在内存中创建了一个名为a的变量,并把它指向对象`'ABC'`。 162 | 163 | --- 164 | 165 | ### 常量 166 | 167 | **Python中没有常量**,因为变量都可以重复赋值。但是一般**约定用全部字母大写的单词来表示一个常量**,如:PI=3.14。 168 | 169 | --- 170 | 171 |
172 | 173 | ## 使用list和tuple 174 | 175 | ### list 176 | 177 | list是一种Python内置的数据类型,表示**有序集合**,可动态删除和插入。通过索引可以访问列表元素,**索引从0开始**,即访问第一个列表元素。并且列表是循环的,**可以通过索引-1访问最尾的元素**,索引-2访问倒数第二个元素。例如: 178 | 179 | ```python 180 | >>> classmates = ['Michael', 'Bob', 'Tracy'] 181 | >>> classmates 182 | ['Michael', 'Bob', 'Tracy'] 183 | >>> classmates[0] 184 | 'Michael' 185 | >>> classmates[-1] 186 | 'Tracy' 187 | >>> classmates[-2] 188 | 'Bob' 189 | ``` 190 | 191 | 另外,还可以用 `len()` 函数获取列表的元素个数。 192 | 193 | list 是一个**可变的**有序表,所以,可以往 list 中追加元素到末尾: 194 | 195 | ```python 196 | >>> classmates.append('Adam') 197 | >>> classmates 198 | ['Michael', 'Bob', 'Tracy', 'Adam'] 199 | ``` 200 | 201 | 也可以把元素插入到指定的位置,比如索引号为 1 的位置: 202 | 203 | ```python 204 | >>> classmates.insert(1, 'Jack') 205 | >>> classmates 206 | ['Michael', 'Jack', 'Bob', 'Tracy', 'Adam'] 207 | ``` 208 | 209 | 要删除 list 末尾的元素,用 pop() 方法: 210 | 211 | ```python 212 | >>> classmates.pop() 213 | 'Adam' 214 | >>> classmates 215 | ['Michael', 'Jack', 'Bob', 'Tracy'] 216 | ``` 217 | 218 | 要删除指定位置的元素,用 pop(i) 方法,其中 i 是索引位置: 219 | 220 | ```python 221 | >>> classmates.pop(1) 222 | 'Jack' 223 | >>> classmates 224 | ['Michael', 'Bob', 'Tracy'] 225 | ``` 226 | 227 | 要把某个元素替换成别的元素,可以直接赋值给对应的索引位置: 228 | 229 | ```python 230 | >>> classmates[1] = 'Sarah' 231 | >>> classmates 232 | ['Michael', 'Sarah', 'Tracy'] 233 | ``` 234 | 235 | list 里面的元素的数据类型可以不同,比如: 236 | 237 | ```python 238 | >>> L = ['Apple', 123, True] 239 | ``` 240 | 241 | list 里面的元素也可以是另一个 list,比如: 242 | 243 | ```python 244 | >>> s = ['python', 'java', ['asp', 'php'], 'scheme'] # 四个元素,其中第三个元素是一个列表 245 | >>> len(s) 246 | 4 247 | 248 | >>> s[0] 249 | 'python' 250 | >>> s[2] 251 | ['asp', 'php'] 252 | >>> s[2][0] 253 | 'asp' 254 | >>> s[2][1] 255 | 'php' 256 | ``` 257 | 258 | 要注意s只有4个元素,s[2]作为一个list类型的元素。要拿到'php'可以用 `s[2][1]`,即把s看作一个二维数组,这样的嵌套可以有很多层。 259 | 260 | 如果一个 list 中一个元素也没有,就是一个空的 list,它的长度为 0: 261 | 262 | ```python 263 | >>> L = [] 264 | >>> len(L) 265 | 0 266 | ``` 267 | 268 | --- 269 | 270 | ### tuple 271 | 272 | tuple也是一种**有序列表**,但**tuple一旦初始化就无法再修改**,也因为这个特性,所以代码更安全。和list不同,tuple用小括号来括起。例如: 273 | 274 | ```python 275 | >>> classmates = ('Michael', 'Bob', 'Tracy') 276 | ``` 277 | 278 | 定义空的tuple如下如下: 279 | 280 | ```python 281 | >>> t = () 282 | >>> t 283 | () 284 | ``` 285 | 286 | 但是,要**定义一个只有1个元素的 tuple**,就要注意一下,如果使用t = (1)来定义,则得到的不是一个tuple,而是整数1,因为括号既可以表示tuple又可以表示数学公式中的小括号。这种情况下默认为后者。要定义1个元素的tuple格式如下,使用一个逗号进行区分: 287 | 288 | ```python 289 | >>> t = (1) 290 | >>> t 291 | 1 292 | 293 | >>> t = (1,) 294 | >>> t 295 | (1,) 296 | ``` 297 | 298 | tuple也有 "可变" 的例子,如果tuple的其中一个元素是list,则这个list元素的内容是可以修改的,如下: 299 | 300 | ```python 301 | >>> t = ('a', 'b', ['A', 'B']) 302 | >>> t[2][0] = 'X' 303 | >>> t[2][1] = 'Y' 304 | >>> t 305 | ('a', 'b', ['X', 'Y']) 306 | ``` 307 | 308 | 这个例子实际修改的是list而不是tuple,tuple指向的位置不会变,而list指向的位置可变,上面的例子实际上是创建了字符串X和Y,然后让tuple的第三个元素也即list元素指向这两个新的字符串。 309 | 310 | --- 311 | 312 |
313 | 314 | ## 使用dict和set 315 | 316 | ### dict 317 | 318 | dict即字典,用于存储键值对,查找速度极快。如果使用list来存键值对就需要两个list,要先从key list找出key,再从value list找到对应项的值,因此list越长,耗时越长。用dict实现则可以直接根据key来找value。格式如下: 319 | 320 | ```python 321 | >>> d = {'Michael': 95, 'Bob': 75, 'Tracy': 85} 322 | >>> d['Michael'] 323 | 95 324 | ``` 325 | 326 | dict速度快是因为Python内部像字典一样建立了索引,字典有部首表,Python内部也会根据不同key算出一个存放的「页码」(哈希算法),所以速度非常快。除了初始化赋值还可以对同一个key进行多次赋值,会覆盖原来的value,如果key不存在就会对dict插入一个新的键值对: 327 | 328 | ```python 329 | >>> d = {'Michael': 95, 'Bob': 75, 'Tracy': 85} 330 | >>> d 331 | {'Tracy': 85, 'Michael': 95, 'Bob': 75} 332 | 333 | >>> d['Michael'] = 20 334 | >>> d 335 | {'Tracy': 85, 'Michael': 20, 'Bob': 75} 336 | 337 | >>> d['Lincoln'] = 100 338 | >>> d 339 | {'Tracy': 85, 'Michael': 20, 'Bob': 75, 'Lincoln': 100} 340 | ``` 341 | 342 | 要判断key是否在dict里面有两种方法: 343 | 344 | 1.使用in关键字,有则返回True,无则返回False 345 | 346 | ```python 347 | >>> 'Thomas' in d 348 | False 349 | ``` 350 | 351 | 2.使用dict提供的get方法,有则返回key对应的value,无则返回空值None或者自己指定的值。 352 | 353 | ```python 354 | >>> d.get('Thomas') 355 | None 356 | >>> d.get('Thomas', -1) 357 | -1 358 | ``` 359 | 360 | 删除一个key则对应的value也会从dict中删除,使用pop方法来实现: 361 | 362 | ```python 363 | >>> d.pop('Bob') 364 | 75 365 | >>> d 366 | {'Tracy': 85, 'Michael': 20, 'Lincoln': 100} 367 | ``` 368 | 369 | **dict的插入和查找速度极快,不会随着key的增加而增加,但需要占用大量的内存**,内存浪费多。list则相反,插入和查找时间随元素增加而增加,但占用空间少。所以dict是一种用空间换时间的方法,注意**dict的key必须是不可变对象**,无法修改key,不然dict就混乱了。**字符串和整数等都可以作为key,list无法作为key**。 370 | 371 | --- 372 | 373 | ###set 374 | 375 | set和dict的原理是一样的,同样**不可以放入可变对象做key**,唯一的区别是**set只有key没有value**。显然**set里面是没有重复的元素的**,不然哈希时会出错。**set是有序的,需要使用列表/元组做初始化**,定义方式如下: 376 | 377 | ```python 378 | >>> s = set([1, 2, 3]) 379 | >>> s 380 | {1, 2, 3} 381 | 382 | >>> s = set((1,2,3)) 383 | >>> s 384 | {1, 2, 3} 385 | ``` 386 | 387 | **列表中有重复元素时set会自动被过滤**,添加可以使用add方法,如s.add(4)。删除则用remove方法,如s.remove(4)。 388 | 389 | 集合可以看成数学意义上无序和无重复的集合,**可以做交集、并集、差集等操作**。 390 | 391 | ```python 392 | >>> s1 = set([1, 2, 3]) 393 | >>> s2 = set([2, 3, 4]) 394 | >>> s1 & s2 395 | {2, 3} 396 | >>> s1 | s2 397 | {1, 2, 3, 4} 398 | >>> s1-s2 399 | {1} 400 | ``` 401 | 402 | --- 403 | 404 | ### 再议不可变对象 405 | 406 | Python中一切皆对象,而对象又分可变对象和不可变两类对象两类,具体来说,它们的差别就是**对象的内容是否可变**。不可变对象包括int, float, string, tuple,空值等,可变对象包括list, dict, set等。要注意对象和变量的关系,**在Python里,变量都是对对象的引用**。举个例子: 407 | 408 | ```python 409 | >>> a = 'abc' 410 | >>> a[0] 411 | 'a' 412 | >>> a[0]='b' # 字符串对象本身是不可变的 413 | Traceback (most recent call last): 414 | File "", line 1, in 415 | TypeError: 'str' object does not support item assignment 416 | 417 | # 变量可以指向另一个字符串对象 418 | # 但字符串对象'abc'并没有改变,它依然存在于内存中 419 | >>> a = 'bbc' 420 | >>> a 421 | 'bbc' 422 | ``` 423 | 424 | 关于参数传递,可以简单总结为以下两点: 425 | 426 | - 当一个可变对象作为函数参数时,函数内对参数修改会生效。 427 | - 当一个不可变对象作为函数参数时,函数内对参数修改无效,因为实际上函数是创建一个新的对象并返回而已,没有修改传入的对象。例如: 428 | 429 | ```python 430 | >>> a = ['c', 'b', 'a'] 431 | >>> a.sort() 432 | >> a 433 | ['a', 'b', 'c'] 434 | ``` 435 | 436 | list是可变对象,使用函数操作,内容会变化,注意区分一下**变量**和**对象**,这里a是一个变量,它指向一个列表对象,这个列表对象的内容才是['c', 'b', 'a']。例如: 437 | 438 | ```python 439 | >>> a = 'abc' 440 | >>> b = a.replace('a', 'A') 441 | >>> b 442 | 'Abc' 443 | >>> a 444 | 'abc' 445 | ``` 446 | 447 | 字符串是不可变对象,所以replace函数并不会修改变量a指向的对象,实际上调用不可变对象的任意方法都不改变该对象自身的内容,只会创建新对象并返回,如果使用一个新的变量来接收返回的对象,就能得到操作结果。不接收则直接打印,**原变量指向的对象不会变化**。 448 | 449 | 注意,虽然tuple是不可变对象,但是如果使用tuple作为dict或者set的key,还是有可能产生错误,因为tuple允许元素中包含列表,列表内容可变。如果使用了带有列表元素的tuple作为key就会报 **`TypeError: unhashable type: 'list'`** 的错误。 450 | 451 | --- 452 | 453 |
454 | 455 | ## 条件判断 456 | 457 | ### 条件判断 458 | 459 | Python中代码块是以缩进作区分的,**if-else条件判断注意要在判定条件后写冒号,并且代码块都正确对齐。多个判断条件可以使用多个elif来实现**。例如: 460 | 461 | ```python 462 | age = 20 463 | if age >= 6: 464 | print('teenager') 465 | elif age >= 18: 466 | print('adult') 467 | else: 468 | print('kid') 469 | ``` 470 | 471 | 判断条件并不一定要是一个判断式,可以简写为一个变量,**当变量为非零数值,非空字符串,非空list等时,判断为True,否则为False**。 472 | 473 | --- 474 | 475 | ### 再议input 476 | 477 | 有时会采取 **`input('提示语句')`** 的方式读取用户输入,作为判定条件。要注意**用户输入属于字符串类型**,要进行数值比较必须先转换为对应的数据类型,否则会报错。 478 | 479 | Python提供int(), float(), str()等方法进行数据类型的转换。 480 | 481 | ```python 482 | # 输入1表示Yes,输入0表示No 483 | >>> choose = input('If you choose yes, please input 1. Otherwise, input 0: ') 484 | If you choose yes, please input 1. Otherwise, input 0: 0 485 | 486 | >>> if choose: # 没有进行转换 487 | ... print('Yes') 488 | ... else: 489 | ... print('No') 490 | ... 491 | Yes 492 | 493 | >>> if int(choose): # 进行了转换 494 | ... print('Yes') 495 | ... else: 496 | ... print('No') 497 | ... 498 | No 499 | ``` 500 | 501 | --- 502 | 503 | ### 循环 504 | 505 | Python提供两种循环写法,一种是 **`for...in...`** 循环,一种是 **`while...`** 循环。for循环依次把list或tuple中的每个元素赋值给目标标识符,格式如下: 506 | 507 | ```python 508 | >>> names = ['Michael', 'Bob', 'Tracy'] 509 | >>> for name in names: 510 | ... print(name) 511 | ... 512 | Michael 513 | Bob 514 | Tracy 515 | ``` 516 | 517 | 当列表为连续数值时,可以用range方法生成,格式如下: 518 | 519 | ```python 520 | >>> sum = 0 521 | >>> for x in range(5): 522 | ... sum = sum + x 523 | ... print(sum) 524 | ... 525 | 0 526 | 1 527 | 3 528 | 6 529 | 10 530 | ``` 531 | 532 | while循环的写法和if-else很相似,也是在判定条件后面加一个冒号。例如: 533 | 534 | ```python 535 | >>> sum = 0 536 | >>> n = 11 537 | >>> while n > 0: 538 | ... sum = sum + n 539 | ... n = n - 2 540 | ... print(sum) 541 | ... 542 | 11 543 | 20 544 | 27 545 | 32 546 | 35 547 | 36 548 | ``` 549 | 550 | 另外,对于死循环的程序,可以通过**Ctrl+C** 强制终止。 551 | 552 | --- 553 | 554 |
555 | 556 | ## 字符串和编码 557 | 558 | ### 字符编码 559 | 560 | - ASCII 561 | > 最早的编码,包含大小写英文,数字及一些符号。大写A编码为65,小写a编码为97。字符0编码为48。使用8位二进制组合(1个字节),可表示128个不同字符(最高位用于校验)。 562 | 563 | - GB2312 564 | > 处理中文时一个字节显然不足,至少需要用两个字节,并且不与ASCII冲突,因此有了中国自己制定的GB2312编码,可用于编码中文。 565 | 566 | - Shift_JIS和Euc-kr 567 | > 日本编码为Shift_JIS,韩文编码为Euc-kr。这样各个国家都用自己不同的标准和编码就很容易在多语言的环境产生冲突,变成乱码。因此又有了通用的一套编码。 568 | 569 | - Unicode 570 | > Unicode将所有语言统一到一套编码中,一般使用2个字节,部分非常偏僻的字符会用到4个字节。但是使用Unicode编码,如果在大量英文的环境又非常浪费空间,因为存储和传输时大小是使用ASCII编码的两倍,不划算,于是又有了新的方式。 571 | 572 | - UTF-8 573 | > UTF-8是可变长编码,对Unicode字符不同的数字大小编码成1-6个字节,英文字母为1个字节,汉字一般是3个字节。需要用到4-6字节的字符非常少,这样比较节省空间。 574 | 575 | **Example**: 576 | 577 | | 字符 | ASCII | Unicode | UTF-8 | 578 | |:------:|:------:|:------:|:------:| 579 | | A | 01000001 | 00000000 01000001 | 01000001 | 580 | | 中 | (超出范围) | 01001110 00101101 | 11100100 10111000 10101101 | 581 | 582 | #### 插播一段课外知识 583 | 584 | 摘录整理自知乎提问[为什么不少网站使用 UTF-8 编码?](https://www.zhihu.com/question/19817672)。 585 | 586 | 为什么要分Unicode和UTF-8,它们两者有什么区别和关联呢? 587 | 588 | 其实最早的时候,由于各地区使用的编码方式不同,使用不同语言的人交流就成了一个大问题。国际标准组织ISO制订了统一的**通用字符集(Universal Character Set,UCS)**,也**简称为Unicode**。**早期的Unicode版本对应于UCS-2编码方式**,使用16位的编码空间。也就是每个字符占用2个字节。这样理论上一共最多可以表示 2^16 也即 65536 个字符,基本满足各种语言的使用。根据维基的说法,当前最新版本的Unicode标准收录了128,000个字符,已经远远超出两字节所能表示的65536了。所以**把Unicode视作UCS-2是一种过时的说法,Unicode标准存在多于两字节的字符**。 589 | 590 | **Unicode有多种字符编码实现方式**。一个Unicode字符的code point是确定的。但是在实际传输过程中,由于不同系统平台的设计不一定一致,以及出于节省空间的目的,**会使用不同的字符编码来实现Unicode**。**这些字符编码统称为Unicode转换格式(Unicode Transformation Format,简称为UTF)**。 591 | 592 | 例如,对于一个仅包含ASCII字符(只需要7位)的Unicode文件,如果每个字符都使用2字节的**原Unicode编码**传输,其第一字节的首位始终为0。这就造成了比较大的浪费。对于这种情况,可以使用**UTF-8编码,这是一种变长编码,它将ASCII字符仍用7位编码表示,占用一个字节(首位补0)**。而遇到与其他Unicode字符混合的情况,将按一定算法转换,每个字符使用1-3个字节编码,并利用首位为0或1进行识别。这样对以ASCII字符为主的西文文档就大大节省了编码长度。 593 | 594 | 但是,**现在所说的Unicode一般都指的是字符集而非编码方式**,廖雪峰老师在教程里提到的Unicode编码实际上指的是UCS-2这种编码方式。又因为以前UCS-2和UTF-16是等价的,所以微软的文档习惯把UTF16称为Unicode,这也导致很多开发者容易对此产生误会。 595 | 596 | 以上内容虽然有部分针对廖雪峰老师教程中描述不准确的地方进行了修改,但可能还是存在一些让人迷惑的说法,更详细的讲解可以看我的博文[字符集与编码的恩怨情仇,以及Python3中的编码问题](http://2wildkids.com/2016/10/12/%E5%AD%97%E7%AC%A6%E9%9B%86%E4%B8%8E%E7%BC%96%E7%A0%81%E7%9A%84%E6%81%A9%E6%80%A8%E6%83%85%E4%BB%87%EF%BC%8C%E4%BB%A5%E5%8F%8APython3%E4%B8%AD%E7%9A%84%E7%BC%96%E7%A0%81%E9%97%AE%E9%A2%98/)。 597 | 598 | #### 其他内容 599 | 600 | 在计算机存储中,内存统一使用Unicode编码,而硬盘保存或者传输数据就用UTF-8编码。比方说打开记事本时,数据会从硬盘中UTF-8编码格式转换为Unicode格式,保存时则相反。 601 | 602 | 浏览网页时,服务器端动态生成的内容是Unicode编码,而传输时则会变为传输UTF-8编码格式的网页。所以常常在网页源码中看到 ``的语句,表示该网页正是用的UTF-8编码。 603 | 604 | **按我的理解,可以这样总结:在传输/储存时,为了节省带宽/空间,会使用UTF-8这样的Unicode字符集的实现方式;而在计算机要处理的时候,应当使用原Unicode编码方式,也即每个字符都是固定两字节,这样字符长度统一更便于操作。** 605 | 606 | 特别注意,在涉及到字节流和字符串转换的程序里,我们**应始终坚持使用UTF-8编码进行转换**,从而避免乱码问题。 607 | 608 | 另外,代码本身也是文本,所以也有编码相关的问题。在编写Python脚本时,一般会在文件头部加入以下两句注释: 609 | 610 | ```python 611 | #!/usr/bin/env python3 612 | # -*- coding: utf-8 -*- 613 | ``` 614 | 615 | 第一行注释是为了告诉Linux/OS X系统,这是一个Python可执行程序(这里说得不准确,具体看我第5章[05模块](https://github.com/familyld/learnpython/blob/master/My_Python_Notebook/05%E6%A8%A1%E5%9D%97.md)的笔记),Windows系统则会忽略这个注释; 616 | 617 | 第二行注释是为了告诉**Python解释器,按照UTF-8编码读取源代码**,否则,如果源代码中有中文的话,就会出现乱码了。 618 | 619 | 注意,这里**仅仅是声明了读取时采用UTF-8编码,但文件本身的编码不一定是UTF-8**,要确定把代码文件编码为UTF-8格式,我们需要对写代码的IDE/文本编辑器进行相应的设置。廖老师提到,**要确保使用UTF-8 without BOM编码**,**那么BOM和without BOM有什么区别呢?**暂不在此进行讨论。 620 | 621 | 关于Python3中编码的信息,可以再看看[官方文档](https://docs.python.org/3/howto/unicode.html#introduction-to-unicode)。 622 | 623 | --- 624 | 625 | ### 字符串 626 | 627 | Python中,**字符串类型中的每个字符采用的是两字节的Unicode编码**,支持多语言。 628 | 629 | #### ord函数 630 | 631 | ord函数可以**获取字符的十进制整数表示**: 632 | 633 | ```python 634 | >>> ord('中') 635 | 20013 636 | >>> ord('文') 637 | 25991 638 | ``` 639 | 注意只能传入**单个字符**。 640 | 641 | #### chr函数 642 | 643 | chr函数可以**将十进制整数转换为Unicode编码下对应的字符**。 644 | 645 | ```python 646 | >>> chr(20013) 647 | '中' 648 | >>> chr(25991) 649 | '文' 650 | ``` 651 | 652 | 此外,如果知道了字符的十进制编码,可以将其转换为十六进制,然后**使用`\u`转义得到对应的Unicode字符**: 653 | 654 | ```python 655 | >>> hex(20013) 656 | '0x4e2d' 657 | >>> '\u4e2d' 658 | '中' 659 | >>> hex(25991) 660 | '0x6587' 661 | >>> '\u6587' 662 | '文' 663 | >>> '\u4e2d\u6587' # 多个字符 664 | '中文' 665 | ``` 666 | 667 | 补充一下,Unicode编码是多语言的,不仅限于中文和英文,还可以使用多种语言的字符及符号: 668 | 669 | ```python 670 | In [1]: ord('한') # 韩语 671 | Out[1]: 54620 672 | In [2]: hex(54620) 673 | Out[2]: '0xd55c' 674 | In [3]: '\ud55c' 675 | Out[3]: '한' 676 | 677 | In [4]: ord('に') # 日语 678 | Out[4]: 12395 679 | In [5]: hex(12395) 680 | Out[5]: '0x306b' 681 | In [6]: '\u306b' 682 | Out[6]: 'に' 683 | 684 | In [7]: ord('a') # 英语 685 | Out[7]: 97 686 | In [8]: hex(97) 687 | Out[8]: '0x61' 688 | In [9]: '\u0061' 689 | Out[9]: 'a' 690 | 691 | In [10]: ord('1') # 数字 692 | Out[10]: 49 693 | In [11]: hex(49) 694 | Out[11]: '0x31' 695 | In [12]: '\u0031' 696 | Out[12]: '1' 697 | 698 | In [13]: ord(',') # 符号 699 | Out[13]: 44 700 | In [14]: hex(44) 701 | Out[14]: '0x2c' 702 | In [15]: '\u002c' 703 | Out[15]: ',' 704 | ``` 705 | 706 | 从以上代码可以看到,这里的Unicode编码中**每个字符都是固定用两字节表示(一个字符用4位16进制数表示,每个16进制数需要4bit,所以一共是4*4 = 16bits = 2bytes)**,**和廖老师教程中所说的若干个字节不一致,是否真的有多于两字节的Unicode字符呢?有待考究。** 707 | 708 | #### bytes类型 709 | 710 | 因为Python程序执行时,字符串类型是用Unicode编码的,一个字符对应若干字节(**若干字节还是固定两个字节呢?**),要在网络中传输或存储到硬盘中就要把字符串变为bytes类型(**因为传输和存储都是以字节为单位的,所以需要进行转换。又因为要节省资源,所以会使用别的编码方式如utf-8**)。 711 | 712 | Python显示bytes类型的数据会用b作前缀,要注意 **`b'ABC'`** 和 **`'ABC'`** 的差别,尽管内容一样,但前者的每个字符都只占1个字节,而后者在Python中以Unicode进行编码,每个字符占两个字节。也即: 713 | 714 | ```python 715 | >>> b'\x41\x42\x43' # bytes类型,每个英文字符占1个字节 716 | b'ABC' 717 | >>> '\u0041\u0042\u0043' # Unicode类型,每个英文字符占2个字节,前8bit用0填充 718 | 'ABC' 719 | ``` 720 | 721 | Unicode字符串可以通过 **`encode()`** 方法编码为指定的bytes,如ASCII编码,utf-8编码etc,bytes类型的数据如果字符不属于ASCII码的范围,就用 **`'\x##`** 的格式表示。 相应地,如果读取字节流的数据,就要用 **`decode()`** 方法解码。例如: 722 | 723 | ```python 724 | >>> 'ABC'.encode('ascii') 725 | b'ABC' 726 | >>> '中文'.encode('utf-8') 727 | b'\xe4\xb8\xad\xe6\x96\x87' 728 | 729 | >>> b'ABC'.decode('ascii') 730 | 'ABC' 731 | >>> b'\xe4\xb8\xad\xe6\x96\x87'.decode('utf-8') 732 | '中文' 733 | ``` 734 | 735 | #### len函数 736 | 737 | **对字符串使用len函数会得到字符的数目**,而**对字节流使用len函数则会得到有多少字节**。例如: 738 | 739 | ```python 740 | >>> len('中文') # 字符串'中文'包含两个字符 741 | 2 742 | >>> len('\u4e2d\u6587') # 用4个字节表示字符串'中文' 743 | 2 744 | >>> len('中文'.encode('utf-8')) # 字符串'中文'编码为utf-u格式后,占6个字节 745 | 6 746 | >>> len(b'\xe4\xb8\xad\xe6\x96\x87') # 用6个字节表示字节流'中文' 747 | 6 748 | 749 | >>> len('ABC') # 字符串'ABC'包含三个字符 750 | 3 751 | >>> len('\u0041\u0042\u0043') # 用6个字节表示字符串'ABC' 752 | 3 753 | >>> len('ABC'.encode('ascii')) # 字符串'ABC'编码为ASCII格式后,占3个字符 754 | 3 755 | >>> len(b'\x41\x42\x43') # 用3个字节表示字节流'ABC' 756 | 3 757 | ``` 758 | 759 | --- 760 | 761 | ### 格式化 762 | 763 | 和C语言类似,Python中使用百分号 `%` 占位符实现格式化的功能。`%s`表示用字符串替换,`%d`,`%f`,`%x`分别表示用整数,浮点数和十六进制数替换。其中`%s`可以将任意数据类型转换为字符串。而`%d`和`%f`则可以指定整数和小数部分的位数。例如: 764 | 765 | ```python 766 | # 普通的格式化 767 | >>> '%s %d %f %x' % ('a',1,1.2,0x2) 768 | 'a 1 1.200000 2' 769 | 770 | # %s可以转换任意数据类型 771 | >>> '%s %s %s %s' % ('a',1,1.2,0x2) 772 | 'a 1 1.2 2' 773 | 774 | # %d可以控制整数部分的位数,不足的位数默认以空格填充 775 | >>> '%4d' % 5 776 | ' 5' 777 | 778 | # 除了用空格填充之外,也可以使用0填充不足的位数 779 | >>> '%04d' % 5 780 | '0005' 781 | 782 | # %f也可以控制整数和小数部分的位数 783 | >>> '%f - %.2f - %2.2f - %02.2f' % (1.2, 1.2, 1.2, 1.2) 784 | '1.200000 - 1.20 - 1.20 - 1.20' 785 | ``` 786 | 787 | **注意**: 788 | 789 | * 只有一个占位符时,后面变量不需要用括号括起。 790 | * 如果字符串本身包含` % `,则需要转义,用` %% `来表示百分号。例如: 791 | 792 | ```python 793 | >>> 'growth rate: %d %' % 7 # 没有转义百分号 794 | Traceback (most recent call last): 795 | File "", line 1, in 796 | ValueError: incomplete format 797 | 798 | >>> 'growth rate: %d %%' % 7 # 转义了百分号 799 | 'growth rate: 7 %' 800 | ``` 801 | 802 | --- 803 | -------------------------------------------------------------------------------- /My_Python_Notebook/02函数.md: -------------------------------------------------------------------------------- 1 | # 函数 2 | 3 | ## 目录 4 | 5 | 6 | 7 | - [调用函数](#调用函数) 8 | - [定义函数](#定义函数) 9 | - [空函数](#空函数) 10 | - [参数检查](#参数检查) 11 | - [返回多个值](#返回多个值) 12 | - [函数的参数](#函数的参数) 13 | - [位置参数](#位置参数) 14 | - [默认参数](#默认参数) 15 | - [可变参数](#可变参数) 16 | - [关键字参数](#关键字参数) 17 | - [命名关键字参数](#命名关键字参数) 18 | - [参数组合](#参数组合) 19 | - [小结](#小结) 20 | - [递归函数](#递归函数) 21 | - [汉诺塔](#汉诺塔) 22 | 23 | 24 | 25 | ## 调用函数 26 | 27 | Python支持函数,不仅可以灵活地自定义函数,而且本身也内置了很多有用的函数。 28 | 29 | 除了可以使用help(函数名)查看内置函数(**built-in function, BIF**)的用法和用途,也可以直接查看[官方文档](https://docs.python.org/3/library/functions.html#abs)。 30 | 31 | 函数名其实就是指向一个函数对象的引用,完全可以把函数名赋给一个变量,相当于给这个函数起了一个**别名**。 32 | 33 | ```python 34 | >>> a = abs # 变量a指向abs函数 35 | >>> a(-1) # 所以也可以通过a调用abs函数 36 | 1 37 | ``` 38 | 39 | --- 40 | 41 |
42 | 43 | ## 定义函数 44 | 45 | 定义函数要使用 `def` 语句,依次写出 *函数名*、*括号*、*括号中的参数* 和*冒号*,然后,在缩进块中编写函数体,函数的返回值用 `return` 语句返回。例如: 46 | 47 | ```python 48 | def my_abs(x): 49 | if x >= 0: 50 | return x 51 | else: 52 | return -x 53 | ``` 54 | 55 | 注意,如果没有return语句,则函数执行完毕也会返回 `None`,如果想要函数返回 `None`,除了写 `return None` 之外还可以直接写 `return`。 56 | 57 | 我们既可以直接在命令行定义函数,也可以把函数放在 `.py` 文件中定义。若采用后者,则使用函数时要先把工作目录跳转到文件保存的目录,再启动Python,然后用 `from 文件名 import 函数名` 即可导入函数。(这里文件名不需要包含文件扩展名 `.py`) 58 | 59 | 比方说把上面的 `my_abs` 函数写入到 `my_abs.py` 文件中,保存在桌面,使用该函数需要先 `cd` 到桌面目录,然后再导入和使用: 60 | 61 | ```python 62 | C:\Users\Administrator>cd Desktop 63 | 64 | C:\Users\Administrator\Desktop>python 65 | Python 3.5.1 |Anaconda 4.0.0 (64-bit)| (default, Feb 16 2016, 09:49:46) [MSC v.1900 64 bit (AMD64)] on win32 66 | Type "help", "copyright", "credits" or "license" for more information. 67 | >>> from my_abs import my_abs 68 | >>> my_abs(5) 69 | 5 70 | >>> my_abs(0) 71 | 0 72 | >>> my_abs(-5) 73 | 5 74 | ``` 75 | 76 | --- 77 | 78 | ### 空函数 79 | 80 | 如果还没想好怎么写一个函数,可以用 `pass` 语句来实现一个空函数,如: 81 | 82 | ```python 83 | def nop(): 84 | pass 85 | ``` 86 | 87 | `pass` 语句什么都不做,但可以用来做占位符。用在其他语句中也可以,如: 88 | 89 | ```python 90 | if age >= 18: 91 | pass 92 | ``` 93 | 94 | --- 95 | 96 | ### 参数检查 97 | 98 | 当**参数个数不对**时,Python解释器会抛出 `TypeError` 错误,但是当**参数类型错误**时,如果函数里面没有给出对应的方法,Python解释器就无法抛出正确的错误提示信息。 99 | 100 | 上面实现的 `my_abs` 函数还不够完善,使用Python的内置函数 `isinstance()` 和 `raise` 语句来实现类型检查并报错的功能,如下: 101 | 102 | ```python 103 | def my_abs(x): 104 | if not isinstance(x, (int, float)): 105 | raise TypeError('bad operand type') 106 | if x >= 0: 107 | return x 108 | else: 109 | return -x 110 | ``` 111 | 112 | 效果: 113 | 114 | ```python 115 | >>> my_abs('a') # 参数类型错误 116 | Traceback (most recent call last): 117 | File "", line 1, in 118 | File "", line 3, in my_abs 119 | TypeError: bad operand type 120 | >>> my_abs(1,2) # 参数个数错误 121 | Traceback (most recent call last): 122 | File "", line 1, in 123 | TypeError: my_abs() takes 1 positional argument but 2 were given 124 | ``` 125 | 126 | --- 127 | 128 | ### 返回多个值 129 | 130 | 举一个返回坐标点的例子: 131 | 132 | ```python 133 | import math 134 | 135 | def move(x, y, step, angle=0): 136 | nx = x + step * math.cos(angle) 137 | ny = y - step * math.sin(angle) 138 | return nx, ny 139 | ``` 140 | 141 | 这里用到math包的函数 `cos` 和 `sin`,返回坐标点的两个维度的值。接收时: 142 | 143 | ```python 144 | >>> x, y = move(100, 100, 60, math.pi / 6) 145 | >>> print(x) 146 | 151.96152422706632 147 | >>> print(y) 148 | 70.0 149 | ``` 150 | 151 | 或者: 152 | 153 | ```python 154 | >>> r = move(100, 100, 60, math.pi / 6) 155 | >>> print(r) 156 | (151.96152422706632, 70.0) 157 | ``` 158 | 159 | 实际上,在Python中,函数返回的仍然是一个变量,但**在返回多个值时,Python会将它们合并为一个tuple返回**,又因为语法上返回一个tuple可以省略括号,所以可以直接写成返回多个值的形式。 特别地,我们可以使用多个变量来接收一个返回的tuple,Python会按位置顺序来赋对应的值。 160 | 161 | --- 162 | 163 |
164 | 165 | ## 函数的参数 166 | 167 | 定义函数的时候,我们把参数的名字和位置确定下来,函数的接口定义就完成了。 168 | 169 | Python的函数定义非常简单,但灵活度却非常大。除了正常定义的必选参数外,还可以使用默认参数、可变参数和关键字参数。 170 | 171 | ### 位置参数 172 | 173 | 传入值按位置顺序依次赋给参数。 174 | 175 | ```python 176 | def power(x, n): 177 | s = 1 178 | while n > 0: 179 | n = n - 1 180 | s = s * x 181 | return s 182 | ``` 183 | 184 | 如这个幂函数,调用时使用 `power(5,2)` 这样的格式即可,5和2会按位置顺序分别被赋给变量x和n。 185 | 186 | --- 187 | 188 | ### 默认参数 189 | 190 | 有时候我们希望函数带有默认设置,比方说令幂函数默认计算平方,这样就不需要每次都传入参数n了。 可以使用默认参数来实现这样的功能: 191 | 192 | ```python 193 | def power(x, n=2): 194 | s = 1 195 | while n > 0: 196 | n = n - 1 197 | s = s * x 198 | return s 199 | ``` 200 | 201 | 此时使用 `power(5)` 也能调用幂函数,计算的是5的平方。 202 | 203 | 在编写函数的参数列表时,应当注意: 204 | 205 | 1. 必选参数在前,默认参数在后,否则Python的解释器会报错。 206 | 2. 有多个参数时,把变化大的参数放前面,变化小的参数放后面。这样我们可以把变化小的参数设为默认参数,调用的时候就不需要每次都填写这个参数了。 207 | 208 | **例子**: 209 | 210 | ```python 211 | def enroll(name, gender, age=6, city='Beijing'): 212 | print('name:', name) 213 | print('gender:', gender) 214 | print('age:', age) 215 | print('city:', city) 216 | ``` 217 | 218 | age和city是默认参数,调用时可以不提供。并且提供默认参数时既可以按顺序也可以不按顺序: 219 | 220 | ```python 221 | enroll('Bob', 'M', 7) 222 | enroll('Adam', 'M', city='Tianjin') 223 | ``` 224 | 225 | 按顺序不需指定参数名,**不按顺序时则必须提供参数名,这样其他未提供的参数依然使用默认参数的值**。 226 | 227 | 注意**默认参数必须指向不可变对象**!举个例子: 228 | 229 | ```python 230 | def add_end(L=[]): 231 | L.append('END') 232 | return L 233 | ``` 234 | 235 | 多次使用默认参数时: 236 | 237 | ```python 238 | >>> add_end() 239 | ['END'] 240 | >>> add_end() 241 | ['END', 'END'] 242 | >>> add_end() 243 | ['END', 'END', 'END'] 244 | ``` 245 | 246 | 可以看到这里默认参数的内容改变了,因为L是可变对象,每次调用add_end,函数会修改默认参数的内容。 所以切记默认参数要指向不可变对象,要实现同样的功能,使用None就可以了。 247 | 248 | ```python 249 | def add_end(L=None): 250 | if L is None: 251 | L = [] 252 | L.append('END') 253 | return L 254 | ``` 255 | 256 | 使用不可变对象做参数,在多任务环境下读取对象不需要加锁,同时读没有问题。因此能使用不可变对象就尽量用不可变对象。 257 | 258 | --- 259 | 260 | ### 可变参数 261 | 262 | 可变参数即**传入的参数个数可变,传入任意个参数都可以** 。先看一个例子: 263 | 264 | ```python 265 | def calc(numbers): 266 | sum = 0 267 | for n in numbers: 268 | sum = sum + n * n 269 | return sum 270 | ``` 271 | 272 | 这个求和函数只有一个参数,必须传入一个list或者tuple才行,即 `calc([1, 2, 3,7])` 或者 `calc((1, 3, 5, 7))`。如果使用可变参数,则: 273 | 274 | ```python 275 | def calc(*numbers): 276 | sum = 0 277 | for n in numbers: 278 | sum = sum + n * n 279 | return sum 280 | ``` 281 | 282 | 只是在参数前面加了一个 `*` 号,函数内容不需要改变。这样定义的函数可以使用任意个数的参数,包括0个。 283 | 284 | ```python 285 | >>> calc(1, 2) 286 | 5 287 | >>> calc() 288 | 0 289 | ``` 290 | 291 | 传入参数时不需要构建list或者tuple,**函数接收参数时会自动构建为一个tuple**。 如果已经有一个list或者tuple要调用可变参数也很方便,**将它变成可变参数**就可以了。 292 | 293 | ```python 294 | >>> nums = [1, 2, 3] 295 | >>> calc(*nums) # 在列表前面加上一个星号即可完成转换 296 | 14 297 | ``` 298 | 299 | 同样只需要加一个 `*` 号即可完成转换。 300 | 301 | args是一个tuple类型的对象,**没有传入时就是一个空的tuple**。 302 | 303 | --- 304 | 305 | ### 关键字参数 306 | 307 | 可变参数允许传入0个或任一个参数,这些可变参数会自动组装为一个tuple。 而**关键字参数允许传入0个或任意个含参数名的参数,这些关键字参数会自动组装为一个dict**。 308 | 309 | ```python 310 | def person(name, age, **kw): 311 | print('name:', name, 'age:', age, 'other:', kw) 312 | ``` 313 | 314 | 调用时: 315 | 316 | ```python 317 | >>> person('Michael', 30) 318 | name: Michael age: 30 other: {} 319 | >>> person('Bob', 35, city='Beijing') 320 | name: Bob age: 35 other: {'city': 'Beijing'} 321 | >>> person('Adam', 45, gender='M', job='Engineer') 322 | name: Adam age: 45 other: {'gender': 'M', 'job': 'Engineer'} 323 | ``` 324 | 325 | kw是一个dict类型的对象,**没有传入时就是一个空的dict**。 和可变参数类似,先组装一个dict然后再传入也是可以的。 326 | 327 | ```python 328 | >>> extra = {'city': 'Beijing', 'job': 'Engineer'} 329 | >>> person('Jack', 24, city=extra['city'], job=extra['job']) 330 | name: Jack age: 24 other: {'city': 'Beijing', 'job': 'Engineer'} 331 | ``` 332 | 333 | 或者进行转换: 334 | 335 | ```python 336 | >>> extra = {'city': 'Beijing', 'job': 'Engineer'} 337 | >>> person('Jack', 24, **extra) # 在字典前面加上两个星号即可完成转换 338 | name: Jack age: 24 other: {'city': 'Beijing', 'job': 'Engineer'} 339 | ``` 340 | 341 | 这里使用 `**` 转换的实质是把extra拷贝一份,然后令kw指向这个拷贝,所以**函数内的操作不会对函数外的extra有任何影响**。 342 | 343 | --- 344 | 345 | ### 命名关键字参数 346 | 347 | 关键字参数的自由度很大,但有时我们需要限制用户可以传入哪些参数,这时就需要用到命名关键字参数。 348 | 349 | ```python 350 | def person(name, age, *, city, job): 351 | print(name, age, city, job) 352 | ``` 353 | 354 | 和关键字参数不同,这里**采用一个 `*` 号作为分隔符**,`*` 号后面的参数被视为关键字参数。 调用如下: 355 | 356 | ```python 357 | >>> person('Jack', 24, city='Beijing', job='Engineer') 358 | Jack 24 Beijing Engineer 359 | ``` 360 | 361 | #### 错误举例 362 | 363 | **1.没有给参数名** 364 | 365 | ```python 366 | >>> person('Jack', 24, 'Beijing', 'Engineer') 367 | Traceback (most recent call last): 368 | File "", line 1, in 369 | TypeError: person() takes 2 positional arguments but 4 were given 370 | ``` 371 | 372 | 命名关键字参数必须传入参数名,如果没有参数名,Python解释器会视其为位置参数,从而报参数个数超出的错误。 373 | 374 | **2.没有传入参数** 375 | 376 | ```python 377 | >>> person('Jack', 24) 378 | Traceback (most recent call last): 379 | File "", line 1, in 380 | person('Jack', 24) 381 | TypeError: person() missing 2 required keyword-only arguments: 'city' and 'job' 382 | ``` 383 | 384 | **命名关键字参数若没有定义默认值则被视为必选参数**。 可以为命名关键字参数设置默认值, 比如 `def person(name, age, *, city='Beijing', job):`,这样即使不传入也不会报错了。 385 | 386 | **3.传入没有定义的参数** 387 | 388 | ```python 389 | >>> person('Jack', 24, city='Beijing', joc='Engineer') 390 | Traceback (most recent call last): 391 | File "", line 1, in 392 | person('Jack', 24, city='Beijing', joc='Engineer') 393 | TypeError: person() got an unexpected keyword argument 'joc' 394 | ``` 395 | 396 | **命名关键字参数限制了可以传入怎样的参数,如果传入参数的参数名不在其中也会报错。** 397 | 398 | --- 399 | 400 | ###参数组合 401 | 402 | 在Python中定义函数除了**可变参数和命名关键字参数无法混合**,可以任意组合必选参数、默认参数、可变参数、关键字参数和命名关键字参数。 403 | 404 | 注意!**参数定义的顺序必须是: `必选参数 -> 默认参数 -> 可变参数/命名关键字参数 -> 关键字参数`**。 405 | 406 | **例子**: 407 | 408 | ```python 409 | def f1(a, b, c=0, *args, **kw): 410 | print('a =', a, 'b =', b, 'c =', c, 'args =', args, 'kw =', kw) 411 | 412 | def f2(a, b, c=0, *, d, **kw): 413 | print('a =', a, 'b =', b, 'c =', c, 'd =', d, 'kw =', kw) 414 | ``` 415 | 416 | 调用: 417 | 418 | ```python 419 | >>> f1(1, 2) 420 | a = 1 b = 2 c = 0 args = () kw = {} 421 | >>> f1(1, 2, c=3) 422 | a = 1 b = 2 c = 3 args = () kw = {} 423 | >>> f1(1, 2, 3, 'a', 'b') 424 | a = 1 b = 2 c = 3 args = ('a', 'b') kw = {} 425 | >>> f1(1, 2, 3, 'a', 'b', x=99) 426 | a = 1 b = 2 c = 3 args = ('a', 'b') kw = {'x': 99} 427 | >>> f2(1, 2, d=99, ext=None) 428 | a = 1 b = 2 c = 0 d = 99 kw = {'ext': None} 429 | ``` 430 | 431 | 除了这种普通的调用方式,通过tuple和dict也可以很神奇地调用! 432 | 433 | ```python 434 | >>> args = (1, 2, 3, 4) 435 | >>> kw = {'d': 99, 'x': '#'} 436 | >>> f1(*args, **kw) 437 | a = 1 b = 2 c = 3 args = (4,) kw = {'d': 99, 'x': '#'} 438 | >>> args = (1, 2, 3) 439 | >>> kw = {'d': 88, 'x': '#'} 440 | >>> f2(*args, **kw) 441 | a = 1 b = 2 c = 3 d = 88 kw = {'x': '#'} 442 | ``` 443 | 444 | 赋值是按照上面的固定顺序来进行的!**对于任意函数,都可以通过类似 `func(*args, **kw)` 的形式调用它,无论它的参数是如何定义的**。 445 | 446 | --- 447 | 448 | ###小结 449 | 450 | Python的函数具有非常灵活的参数形态,既可以实现简单的调用,又可以传入非常复杂的参数。 451 | 452 | **默认参数一定要用不可变对象**,如果是可变对象,程序运行时会有逻辑错误! 453 | 454 | 要注意定义可变参数和关键字参数的语法: 455 | 456 | `*args` 是可变参数,`*args` 接收的是一个**tuple**; 457 | 458 | `**kw` 是关键字参数,`**kw` 接收的是一个**dict**。 459 | 460 | 以及调用函数时如何传入可变参数和关键字参数的语法: 461 | 462 | **可变参数既可以直接传入**:func(1, 2, 3),**又可以先组装list或tuple**,再通过 `*args` 传入: `func(*(1, 2, 3))`; 463 | 464 | **关键字参数既可以直接传入**:func(a=1, b=2),**又可以先组装dict**,再通过 `**kw` 传入: `func(**{'a': 1, 'b': 2})`。 465 | 466 | 使用 `*args` 和 `**kw` 这两个名字是Python的习惯写法,当然也可以用其他参数名,但最好使用习惯用法。 467 | 468 | 命名关键字参数是为了限制调用者可以传入的参数名,并且我们可以为其提供默认值。 469 | 470 | **定义命名关键字参数不要忘了写分隔符 `*` ,否则定义的将是位置参数。 471 | 472 | --- 473 | 474 |
475 | 476 | ## 递归函数 477 | 478 | 若一个函数在函数内部调用自身,则该函数是一个递归函数。如: 479 | 480 | ```python 481 | def fact(n): 482 | if n==1: 483 | return 1 484 | return n * fact(n - 1) 485 | ``` 486 | 487 | 阶乘函数就是一个递归函数,**使用递归函数需要注意防止栈溢出**。在计算机中,函数调用是通过**栈(stack)**这种数据结构实现的。 488 | 489 | 每当进入一个函数调用,栈就会加一层栈帧,每当函数返回,栈就会减一层栈帧。**由于栈的大小不是无限的,所以,递归调用的次数过多,会导致栈溢出**。 490 | 491 | **解决递归调用栈溢出的方法是通过尾递归优化**,尾递归和循环效果一样,实际上可以把循环看作特殊的尾递归函数。 492 | 493 | **尾递归要求函数返回时调用自身本身而不能包含表达式**。这样编译器或解释器就可以把尾递归进行优化,无论递归了多少次都只占用一个栈帧。 494 | 495 | ```python 496 | def fact(n): 497 | return fact_iter(n, 1) 498 | 499 | def fact_iter(num, product): 500 | if num == 1: 501 | return product 502 | return fact_iter(num - 1, num * product) 503 | ``` 504 | 505 | 之前的函数定义有乘法表达式,所以不是尾递归。 506 | 507 | 计算过程如下: 508 | 509 | ```python 510 | ===> fact(5) 511 | ===> 5 * fact(4) 512 | ===> 5 * (4 * fact(3)) 513 | ===> 5 * (4 * (3 * fact(2))) 514 | ===> 5 * (4 * (3 * (2 * fact(1)))) 515 | ===> 5 * (4 * (3 * (2 * 1))) 516 | ===> 5 * (4 * (3 * 2)) 517 | ===> 5 * (4 * 6) 518 | ===> 5 * 24 519 | ===> 120 520 | ``` 521 | 522 | 这里改为在函数调用前先计算product,每次递归仅调用函数本身就可以了。 523 | 524 | 计算过程如下: 525 | 526 | ```python 527 | ===> fact_iter(5, 1) 528 | ===> fact_iter(4, 5) 529 | ===> fact_iter(3, 20) 530 | ===> fact_iter(2, 60) 531 | ===> fact_iter(1, 120) 532 | ===> 120 533 | ``` 534 | 535 | 可惜**Python标准的解释器没有对尾递归做优化,所以即使改为尾递归的写法还是有可能产生栈溢出**。 536 | 537 | ### 汉诺塔 538 | 539 | a有n个盘子(从上到下由轻到重),要求只借助a,b,c三个支架,把所有盘子移动到c。并且重的盘子不可以在轻的盘子上。 540 | 541 | ```python 542 | def move(n, a, b, c): 543 | if n == 1: 544 | print(a,' --> ',c) 545 | else: 546 | move(n-1,a,c,b) #将前n-1个盘子从A移动到B上 547 | print(a,' --> ',c) #将最底下的最后一个盘子从A移动到C上 548 | move(n-1,b,a,c) #将B上的n-1个盘子移动到C上 549 | ``` 550 | 551 | 代码很短,思路很清晰,基于规则,每次只能把余下盘子中最重的移到c上。 这里通过改变传入参数的顺序可以灵活使用三个支架。 a在一次移动中可能充当b的角色,b,c也可能充当a的角色。 552 | 553 | 但总的来说,我们都是希望把充当a的支架上n-1个盘子先移到充当b的支架上,再把a的剩下的最重的一个盘子移动到充当c的支架上,然后递归,这时充当b的支架就变成a,充当a的支架就变成b,直到最后完成所有移动。 554 | 555 | --- 556 | -------------------------------------------------------------------------------- /My_Python_Notebook/03高级特性.md: -------------------------------------------------------------------------------- 1 | # 高级特性 2 | 3 | ## 目录 4 | 5 | 6 | 7 | - [切片](#切片) 8 | - [迭代](#迭代) 9 | - [列表生成式](#列表生成式) 10 | - [生成器](#生成器) 11 | - [杨辉三角](#杨辉三角) 12 | - [迭代器](#迭代器) 13 | - [小结](#小结) 14 | 15 | 16 | 17 | 在Python中,代码越少越简单约好。基于这一思想,后面的几个篇章介绍Python一些非常有用的高级特性。 18 | 19 | 比方说构造一个1~99的奇数列表,可以简单地用循环实现: 20 | 21 | ```python 22 | L = [] 23 | n = 1 24 | while n <= 99: 25 | L.append(n) 26 | n = n + 2 27 | ``` 28 | 29 | ## 切片 30 | 31 | 切片即取一个list或tuple部分元素的操作。 当我们需要取列表前n个元素,即索引0~**N-1**的元素时,有两种方法: 32 | 33 | 1.方法1是用循环 34 | 35 | ```python 36 | >>> L = ['Michael', 'Sarah', 'Tracy', 'Bob', 'Jack'] 37 | >>> r = [] 38 | >>> n = 3 39 | >>> for i in range(n): 40 | ... r.append(L[i]) 41 | ... 42 | >>> r 43 | ['Michael', 'Sarah', 'Tracy'] 44 | ``` 45 | 46 | 2.方法2是利用**切片操作符** 47 | 48 | ```python 49 | >>> L[0:3] 50 | ['Michael', 'Sarah', 'Tracy'] 51 | ``` 52 | 53 | 如果经常要取指定的索引范围,用循环就显得太过繁琐了,Python提供了切片操作来简化这个过程。注意,**切片操作的索引左闭右开**。 54 | 55 | 如果索引从0开始,还可以改写为 `L[:3]`。 如果索引到列表最后结束,同样可以简略写为 `L[0:]`。 56 | 57 | 此外,Python还支持**倒数切片**。**列表最后一项的索引在倒数中为-1**。 58 | 59 | ```python 60 | >>> L[-2:] 61 | ['Bob', 'Jack'] 62 | >>> L[-2:-1] 63 | ['Bob'] 64 | ``` 65 | 66 | 特别地,切片操作还支持每隔k个元素取1个这样的操作。先创建一个0~99的整数列表: 67 | 68 | ```python 69 | >>> L = list(range(100)) 70 | >>> L 71 | [0, 1, 2, 3, ..., 99] 72 | ``` 73 | 74 | 取后10个只需起始索引为-10即可: 75 | 76 | ```python 77 | >>> L[-10:] 78 | [90, 91, 92, 93, 94, 95, 96, 97, 98, 99] 79 | ``` 80 | 81 | 前十个数隔两个取一个: 82 | 83 | ```python 84 | >>> L[:10:2] 85 | [0, 2, 4, 6, 8] 86 | ``` 87 | 88 | 所有数,隔五个取一个: 89 | 90 | ```python 91 | >>> L[::5] 92 | [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95] 93 | ``` 94 | 95 | 注意!**对list进行切片操作得到的还是list;对tuple进行切片操作得到的还是tuple**。 特别地,**字符串也可看为一种list,同样可以使用切片操作**。 96 | 97 | --- 98 | 99 |
100 | 101 | ## 迭代 102 | 103 | Python中可迭代的对象包括字符串,list,tuple,dict,set和文件等等。 对这些可迭代对象可以使用 `for...in` 循环来遍历。Python对for循环的抽象程度高于Java和C,所以即使没有下标也能迭代。 104 | 105 | 比如循环遍历一个dict: 106 | 107 | ```python 108 | >>> d = {'a': 1, 'b': 2, 'c': 3} 109 | >>> for key in d: 110 | ... print(key, d[key]) 111 | ... 112 | a 1 113 | c 3 114 | b 2 115 | ``` 116 | 117 | 直接打印key会打印所有dict中的key,更改迭代的写法为 `for value in d.values()` 就变为迭代dict中所有的value。 如果同时要访问key和value,还可以使用 `for k, v in d.items()`。 118 | 119 | 字符串同样可以用for循环迭代: 120 | 121 | ```python 122 | >>> for ch in 'ABC': 123 | ... print(ch) 124 | ... 125 | A 126 | B 127 | C 128 | ``` 129 | 130 | 要**判断一个对象是否可迭代对象可以通过collections模块的 `Iterable` 类型**来判断: 131 | 132 | ```python 133 | >>> from collections import Iterable 134 | >>> isinstance('abc', Iterable) # str是否可迭代 135 | True 136 | >>> isinstance([1,2,3], Iterable) # list是否可迭代 137 | True 138 | >>> isinstance(123, Iterable) # 整数是否可迭代 139 | False 140 | ``` 141 | 142 | 正如上面迭代dict一样,for循环可以同时引用两个甚至多个变量: 143 | 144 | ```python 145 | >>> for i, value in enumerate(['A', 'B', 'C']): 146 | ... print(i, value) 147 | ... 148 | 0 A 149 | 1 B 150 | 2 C 151 | 152 | >>> for x, y in [(1, 1), (2, 4), (3, 9)]: 153 | ... print(x, y) 154 | ... 155 | 1 1 156 | 2 4 157 | 3 9 158 | ``` 159 | 160 | 例子里的 `enumerate` 方法通过[enumerate官方文档](https://docs.python.org/3/library/functions.html#enumerate)了解,它返回一个枚举对象,并且传入参数可迭代时它就是一个可迭代的对象。 161 | 162 | **可以用 `list(enumerate(可迭代对象))` 把一个可迭代对象变为元素为tuple类型的list**,每个tuple有两个元素,形式如:`(序号,原可迭代对象内容)`。 163 | 164 | 并且使用enumerate时可以指定开始的序号,`enumerate(iterable, start=0)`,不写时默认参数为0,即序号从0开始。 可以自己指定为其他数。 165 | 166 | --- 167 | 168 |
169 | 170 | ## 列表生成式 171 | 172 | 列表生成式即List Comprehensions,是Python内置的用于创建list的方式。 173 | 174 | 比方说生成1到10,可以: 175 | 176 | ```python 177 | >>> list(range(1, 11)) 178 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 179 | ``` 180 | 181 | 要生成 `[1x1, 2x2, 3x3, ..., 10x10]`平方序列,笨办法是用循环: 182 | 183 | ```python 184 | >>> L = [] 185 | >>> for x in range(1, 11): 186 | ... L.append(x * x) 187 | ... 188 | >>> L 189 | [1, 4, 9, 16, 25, 36, 49, 64, 81, 100] 190 | ``` 191 | 192 | 用列表生成式只用一个语句就可以了: 193 | 194 | ```python 195 | >>> [x * x for x in range(1, 11)] 196 | [1, 4, 9, 16, 25, 36, 49, 64, 81, 100] 197 | ``` 198 | 199 | 写列表生成式时,把要生成的元素 `x * x` 放到前面,后面跟for循环,就可以把list创建出来。 200 | 201 | 在for循环后面还可以加上if判断,比方说这个例子用于筛选出偶数的平方数: 202 | 203 | ```python 204 | >>> [x * x for x in range(1, 11) if x % 2 == 0] 205 | [4, 16, 36, 64, 100] 206 | ``` 207 | 208 | 使用两层循环还可以生成全排列: 209 | 210 | ```python 211 | >>> [m + n for m in 'ABC' for n in 'XYZ'] 212 | ['AX', 'AY', 'AZ', 'BX', 'BY', 'BZ', 'CX', 'CY', 'CZ'] 213 | ``` 214 | 215 | 列出当前目录下所有文件和目录名也非常简单: 216 | 217 | ```python 218 | >>> import os # 导入os模块,模块的概念后面讲到 219 | >>> [d for d in os.listdir('.')] # os.listdir可以列出文件和目录 220 | ['.emacs.d', '.Trash', 'Applications', 'Desktop', 'Documents'] 221 | ``` 222 | 223 | 前面一节提到for循环迭代可以同时用两个变量,这里列表生成式同样可以用两个变量来生成list。 224 | 225 | ```python 226 | >>> d = {'x': 'A', 'y': 'B', 'z': 'C' } 227 | >>> [k + '=' + v for k, v in d.items()] 228 | ['y=B', 'x=A', 'z=C'] 229 | ``` 230 | 231 | 把list中所有字符串的大写字母换成小写: 232 | 233 | ```python 234 | >>> L = ['Hello', 'World', 'IBM', 'Apple'] 235 | >>> [s.lower() for s in L] 236 | ['hello', 'world', 'ibm', 'apple'] 237 | ``` 238 | 239 | --- 240 | 241 |
242 | 243 | ### 生成器 244 | 245 | 通过列表生成式可以简单地创建列表,但受到内存限制,列表容量是有限的。如果列表元素很多,而我们仅需访问前面一部分元素,则会造成很大的存储空间的浪费。 246 | 247 | **生成器(generator)**就意在解决这个问题,允许在循环过程中不断推算出后续元素,而不用创建完整的list。在Python中,这种边循环边计算的机制称为生成器。 248 | 249 | 和列表生成式的区别很简单,仅仅是把外层的**[]方括号**换成**()圆括号**。 250 | 251 | ```python 252 | >>> L = [x * x for x in range(5)] 253 | >>> L 254 | [0, 1, 4, 9, 16] 255 | >>> g = (x * x for x in range(10)) 256 | >>> g 257 | at 0x1022ef630> 258 | ``` 259 | 260 | 生成器无法通过索引访问,因为它**保存的是算法**,要遍历生成器需要通过 `next()` 函数。 261 | 262 | ```python 263 | >>> next(g) 264 | 0 265 | >>> next(g) 266 | 1 267 | >>> next(g) 268 | 4 269 | >>> next(g) 270 | 9 271 | >>> next(g) 272 | 16 273 | >>> next(g) 274 | Traceback (most recent call last): 275 | File "", line 1, in 276 | StopIteration 277 | ``` 278 | 279 | 当到达最后一个元素时,再使用 `next()` 就会出现 `StopIteration` 错误。 当然,实际遍历生成器时不会这样一个一个用 `next()` 方法遍历,用for循环进行迭代即可。 280 | 281 | ```python 282 | >>> g = (x * x for x in range(5)) 283 | >>> for n in g: 284 | ... print(n) 285 | ... 286 | 0 287 | 1 288 | 4 289 | 9 290 | 16 291 | ``` 292 | 293 | 当算法比较复杂,用简单for循环无法写出来时,还可以通过函数来实现: 294 | 295 | ```python 296 | def fib(max): 297 | n, a, b = 0, 0, 1 298 | while n < max: 299 | print(b) 300 | a, b = b, a + b 301 | n = n + 1 302 | return 'done' 303 | ``` 304 | 305 | 比方说这个计算斐波那契数列的函数,稍微改写一下即可变成generator: 306 | 307 | ```python 308 | def fib(max): 309 | n, a, b = 0, 0, 1 310 | while n < max: 311 | yield b #只修改这里 312 | a, b = b, a + b 313 | n = n + 1 314 | return 'done' 315 | ``` 316 | 317 | 这是定义generator的另一种方法,**如果一个函数定义中包含yield关键字,则该函数就变为一个generator**。 318 | 319 | ```python 320 | >>> f = fib(6) 321 | >>> f 322 | 323 | ``` 324 | 325 | 函数是顺序执行,遇到return语句或到达最后一行函数语句就返回。而变成generator的函数,在每次调用 `next()` 的时候执行,**遇到yield就返回**,下次执行会从yield的地方开始。 326 | 327 | ```python 328 | >>> for n in fib(6): 329 | ... print(n) 330 | ... 331 | 1 332 | 1 333 | 2 334 | 3 335 | 5 336 | 8 337 | ``` 338 | 339 | 同样地,把函数改成generator后,我们不需要用next()方法获取写一个返回值,而是只借用for循环进行迭代。 340 | 341 | 但是这样就拿不到fib函数return语句的值(即字符串done),要获取这个值必须捕获 `StopIteration` 这个错误,它的value就是我们返回的值: 342 | 343 | ```python 344 | >>> g = fib(6) 345 | >>> while True: 346 | ... try: 347 | ... x = next(g) 348 | ... print('g:', x) 349 | ... except StopIteration as e: 350 | ... print('Generator return value:', e.value) 351 | ... break 352 | ... 353 | g: 1 354 | g: 1 355 | g: 2 356 | g: 3 357 | g: 5 358 | g: 8 359 | Generator return value: done 360 | ``` 361 | 362 | 生成器的工作原理是:在for循环的过程中不断计算出下一个元素,并在适当的条件结束for循环。 363 | 364 | 对于函数改成的generator来说,**遇到return语句或者执行到函数体最后一行语句就结束generator**,for循环随之结束。 365 | 366 | 普通函数和生成器函数可以通过调用进行区分,调用普通函数会直接返回结果,调用生成器函数则会返回一个生成器对象。 367 | 368 | --- 369 | 370 | ### 杨辉三角 371 | 372 | 要求使用生成器生成1~10行的杨辉三角。 提示:把每一行当作一个list。 373 | 374 | ```python 375 | def triangles(max): 376 | n = 0 377 | b = [1] 378 | while n < max: 379 | yield b 380 | b = [1] + [ b[i] + b[i + 1] for i in range(len(b) - 1)] + [1] 381 | n = n + 1 382 | ``` 383 | 384 | 这段代码非常短,但是已经充分实现了题目要求,值得欣赏! 385 | 386 | ```python 387 | >>> for L in triangles(6): 388 | ... L 389 | ... 390 | [1] 391 | [1, 1] 392 | [1, 2, 1] 393 | [1, 3, 3, 1] 394 | [1, 4, 6, 4, 1] 395 | [1, 5, 10, 10, 5, 1] 396 | ``` 397 | 398 | 代码里面有两个窍门,一是列表相加,注意不是列表元素相加。 列表相加相当于把后一个列表的元素全部append到前一个列表。如: 399 | 400 | ```python 401 | >>> L = [1,2] 402 | >>> R = [3,4] 403 | >>> L+R 404 | [1, 2, 3, 4] 405 | ``` 406 | 407 | 上面代码中的b即把每一行当作一个list,因为每一行的开头结尾都是1,所以可以每一行的list看作三个list的相加,一头一尾两个list是只有1个元素1的list,中间的list用列表生成式生成。 408 | 409 | 另一个窍门就是这里的列表生成式。 注意这里计算时还没赋值,引用列表b的内容是上一行的信息,所以能巧妙地借助上一行计算相邻两数之和,最终得到含有n-2项的中间列表。 410 | 411 | 补充解析一下代码执行的过程: 412 | 413 | ```python 414 | b = [1], n = 0 415 | b = [1] + [1] = [1,1], n = 1 # 无中间列表 416 | b = [1] + [1+1] + [1], n = 2 # 中间列表包含1个元素 417 | b = [1] + [1+2, 2+1] + [1], n = 3 # 中间列表包含2个元素 418 | b = [1] + [1+3, 3+3, 3+1] + [1], n = 4 # 中间列表包含4个元素 419 | ... ... 420 | ``` 421 | 422 | --- 423 | 424 |
425 | 426 | ## 迭代器 427 | 428 | 迭代器即**Iterator**, 前面说到可以通过**collections模块**的**Iterable类型**来判断一个对象是否可迭代对象。 这里引入Iterator的概念,可以通过类似的方式判断。 429 | 430 | **list,dict,str虽然都Iterable,却不是Iterator**。 生成器都是Iterator。**Iterator的特性允许对象通过next()函数不断返回下一个值**。 431 | 432 | ```python 433 | >>> from collections import Iterator 434 | >>> isinstance((x for x in range(10)), Iterator) 435 | True 436 | >>> isinstance([], Iterator) 437 | False 438 | >>> isinstance({}, Iterator) 439 | False 440 | >>> isinstance('abc', Iterator) 441 | False 442 | ``` 443 | 444 | **要把list,dict,str变为Iterator可以使用 `iter()` 函数**: 445 | 446 | ```python 447 | >>> isinstance(iter([]), Iterator) 448 | True 449 | >>> isinstance(iter('abc'), Iterator) 450 | True 451 | ``` 452 | 453 | Python的Iterator对象表示的其实是一个**数据流**,Iterator对象可以被 `next()` 函数调用并不断返回下一个数据,直到没有数据时抛出 `StopIteration` 错误。 454 | 455 | 可以把这个数据流看做是一个有序序列,但我们却不能提前知道序列的长度,只能不断通过 `next()` 函数实现按需计算下一个数据,所以 `Iterator` 的计算是惰性的,只有在需要返回下一个数据时它才会计算,也因此能够节省空间。 456 | 457 | Iterator甚至可以表示一个**无限大的数据流**,例如全体自然数。而使用list是永远不可能存储全体自然数的。 458 | 459 | --- 460 | 461 | ### 小结 462 | 463 | 凡是可作用于for循环的对象都是 `Iterable` 类型; 464 | 465 | 凡是可作用于 `next()` 函数的对象都是 `Iterator` 类型,它们表示一个惰性计算的序列; 466 | 467 | 集合数据类型如list、dict、str等是 `Iterable` 但不是 `Iterator`,不过可以通过 `iter()` 函数获得一个 `Iterator` 对象。 468 | 469 | Python的**for循环本质上就是通过不断调用 `next()` 函数实现的**,例如: 470 | 471 | ```python 472 | for x in [1, 2, 3, 4, 5]: 473 | pass 474 | ``` 475 | 476 | 实际上完全等价于: 477 | 478 | ```python 479 | # 首先获得Iterator对象: 480 | it = iter([1, 2, 3, 4, 5]) 481 | # 循环: 482 | while True: 483 | try: 484 | # 获得下一个值: 485 | x = next(it) 486 | except StopIteration: 487 | # 遇到StopIteration就退出循环 488 | break 489 | ``` 490 | 491 | --- 492 | 493 | -------------------------------------------------------------------------------- /My_Python_Notebook/04函数式编程.md: -------------------------------------------------------------------------------- 1 | # 函数式编程 2 | 3 | **函数**是Python内建支持的一种封装,我们通过把大段代码拆成函数,通过一层一层的函数调用,就可以把复杂任务分解成简单的任务,这种分解可以称之为面向过程的程序设计。函数就是面向过程的程序设计的基本单元。 4 | 5 | **函数式编程**(请注意多了一个**“式”**字)——Functional Programming,虽然也可以归结到面向过程的程序设计,但其**思想更接近数学计算**。 6 | 7 | 我们首先要搞明白**计算机(Computer)**和**计算(Compute)**的概念。 8 | 9 | - 在计算机的层次上,CPU执行的是加减乘除的指令代码,以及各种条件判断和跳转指令,所以汇编语言是最贴近计算机的语言。 10 | 11 | - 计算则是指数学意义上的计算,越是抽象的计算,离计算机硬件越远。 12 | 13 | 对应到编程语言,就是越低级的语言,越贴近计算机,抽象程度低,执行效率高,比如C语言;越高级的语言,越贴近计算,抽象程度高,执行效率低,比如Lisp语言。 14 | 15 | **归纳一下**: 16 | 17 | | | 低级语言 | 高级语言 | 18 | |:-:|:-:|:-:| 19 | | 特点 | 贴近计算机 | 贴近计算(数学意义上) | 20 | | 抽象程度 | 低 | 高 | 21 | | 执行效率 | 高 | 低 | 22 | | 例子 | 汇编和C | Lisp | 23 | 24 | 函数式编程就是一种**抽象程度很高的编程范式**,纯粹的函数式编程语言编写的函数**没有变量**,因此,任意一个函数,只要输入是确定的,输出就是确定的,这种纯函数我们称之为**没有副作用**。而允许使用变量的程序设计语言,由于函数内部的**变量状态不确定**,同样的输入,可能得到不同的输出,因此,这种函数是有副作用的。 25 | 26 | 函数式编程的一个特点就是,**允许把函数本身作为参数传入另一个函数,还允许返回一个函数**! 27 | 28 | Python**仅对函数式编程提供部分支持**。由于Python允许使用变量,因此,**Python不是纯函数式编程语言**。 29 | 30 | ## 目录 31 | 32 | 33 | 34 | - [函数式编程的三大特性](#函数式编程的三大特性) 35 | - [函数式编程的几个技术](#函数式编程的几个技术) 36 | - [函数式编程的几个好处](#函数式编程的几个好处) 37 | - [简单举例](#简单举例) 38 | - [高阶函数](#高阶函数) 39 | - [变量可以指向函数](#变量可以指向函数) 40 | - [函数名也是变量](#函数名也是变量) 41 | - [传入函数](#传入函数) 42 | - [map/reduce](#mapreduce) 43 | - [filter](#filter) 44 | - [sorted](#sorted) 45 | - [返回函数](#返回函数) 46 | - [函数作为返回值](#函数作为返回值) 47 | - [闭包](#闭包) 48 | - [匿名函数](#匿名函数) 49 | - [装饰器](#装饰器) 50 | - [带参数的decorator](#带参数的decorator) 51 | - [属性复制](#属性复制) 52 | - [练习](#练习) 53 | - [偏函数](#偏函数) 54 | 55 | 56 | 57 | 58 | ## 函数式编程的三大特性 59 | 60 | 1. **immutable data**
61 | 变量不可变,或者说没有变量,只有常量。 函数式编程输入确定时输出就是确定的,函数内部的变量和函数外部的没有关系,不会受到外部操作的影响。 62 | 63 | 2. **first class functions**
64 | 第一类函数(也称高阶函数),意思是函数可以向变量一样用,可以像变量一样创建、修改、传递和返回。 这就允许我们把大段代码拆成函数一层层地调用,这种面向过程的写法相比循环更加直观。 65 | 66 | 3. **尾递归优化**
67 | 之前一章的递归函数中已经提及过了,就是递归时返回函数本身而非表达式。 可惜Python中没有这个特性。 68 | 69 | --- 70 | 71 |
72 | 73 | ## 函数式编程的几个技术 74 | 75 | 1. **map & reduce**
76 | 函数式编程最常见的技术就是对一个集合做Map和Reduce操作。这比起传统的面向过程的写法来说,在代码上要更容易阅读(不需要使用一堆for、while循环来倒腾数据,而是使用更抽象的Map函数和Reduce函数)。 77 | 78 | 2. **pipeline**
79 | 这个技术的意思是把函数实例成一个一个的action,然后把一组action放到一个数组或是列表中组成一个action list,然后把数据传给这个action list,数据就像通过一个pipeline一样顺序地被各个函数所操作,最终得到我们想要的结果。 80 | 81 | 3. **recursing**
82 | 递归最大的好处就简化代码,它可以把一个复杂的问题用很简单的代码描述出来。注意:递归的精髓是描述问题,而这正是函数式编程的精髓。 83 | 84 | 4. **currying**
85 | 把一个函数的多个参数分解成多个函数, 然后把函数多层封装起来,每层函数都返回一个函数去接收下一个参数这样,可以简化函数的多个参数(减少函数的参数数目)。 86 | 87 | 5. **higher order function**
88 | 高阶函数:所谓高阶函数就是函数当参数,把传入的函数做一个封装,然后返回这个封装函数。现象上就是函数传进传出。 89 | 90 | 对currying进行一点补充,举个例子: 91 | 92 | ```python 93 | def pow(i, j): 94 | return i**j 95 | 96 | def square(i): 97 | return pow(i, 2) 98 | ``` 99 | 100 | 这里就是把原本平方函数`square`的参数j分解了,它返回幂函数`pow`函数,把幂次封装在里面,从而减少了求平方时所需用到的参数。 101 | 102 | 关于函数式编程的一些概念理解可以看[傻瓜函数式编程](http://www.kancloud.cn/kancloud/functional-programm-for-rest/56931)或者英文原版的[Functional Programming For The Rest of Us](http://www.defmacro.org/ramblings/fp.html)。 103 | 104 | --- 105 | 106 |
107 | 108 | ## 函数式编程的几个好处 109 | 110 | 1. **parallelization 并行**
111 | 在并行环境下,各个线程之间不需要同步或互斥(变量都是内部的,不需要共享)。 112 | 113 | 2. **lazy evaluation 惰性求值**
114 | 表达式不在它被绑定到变量之后就立即求值,而是在该值被取用的时候求值。 115 | 116 | 3. **determinism 确定性**
117 | 输入是确定的,输出就是确定的。 118 | 119 | ### 简单举例 120 | 121 | 以往面向过程式的编程需要引入额外的逻辑变量以及使用循环: 122 | 123 | ```python 124 | upname =['HAO', 'CHEN', 'COOLSHELL'] 125 | lowname =[] 126 | for i in range(len(upname)): 127 | lowname.append( upname[i].lower() ) 128 | ``` 129 | 130 | 而函数式编程则非常简洁易懂: 131 | 132 | ```python 133 | def toUpper(item): 134 | return item.upper() 135 | 136 | upper_name = map(toUpper, ["hao", "chen", "coolshell"]) 137 | print upper_name 138 | ``` 139 | 140 | 再看一个计算一个列表中所有正数的平均数的例子: 141 | 142 | ```python 143 | num =[2, -5, 9, 7, -2, 5, 3, 1, 0, -3, 8] 144 | positive_num_cnt = 0 145 | positive_num_sum = 0 146 | for i in range(len(num)): 147 | if num[i] > 0: 148 | positive_num_cnt += 1 149 | positive_num_sum += num[i] 150 | 151 | if positive_num_cnt > 0: 152 | average = positive_num_sum / positive_num_cnt 153 | 154 | print average 155 | ``` 156 | 157 | 如果采用函数式编程: 158 | 159 | ```python 160 | positive_num = filter(lambda x: x>0, num) 161 | average = reduce(lambda x,y: x+y, positive_num) / len( positive_num ) 162 | ``` 163 | 164 | 可以看到**函数式编程减少了变量的使用,也就减少了出Bug的可能,维护更加方便。可读性更高,代码更简洁**。 165 | 166 | 更多的例子和解析详见[函数式编程](http://coolshell.cn/articles/10822.html)。 167 | 168 | --- 169 | 170 |
171 | 172 | ## 高阶函数 173 | 174 | 前面已经提到了函数式编程中的高阶函数特性,这一节将针对Python中的使用方式进行更详细的描述。 175 | 176 | ### 变量可以指向函数 177 | 178 | ```python 179 | >>> abs 180 | 181 | >>> f = abs 182 | >>> f 183 | 184 | >>> f(-10) 185 | 10 186 | ``` 187 | 188 | 这个例子表明在Python中变量是可以指向函数的,并且这样赋值的变量能够作为函数的别名使用。 189 | 190 | --- 191 | 192 | ### 函数名也是变量 193 | 194 | ```python 195 | >>> abs = 10 196 | >>> abs(-10) 197 | Traceback (most recent call last): 198 | File "", line 1, in 199 | TypeError: 'int' object is not callable 200 | ``` 201 | 202 | 这里把abs函数赋值为10,这样赋值以后abs就变成一个整形变量,指向int型对象10而不指向原本的函数了。所以无法再作为函数使用。 203 | 204 | 想恢复abs函数要重启Python交互环境。 abs函数定义在 `__builtin__` 模块中,要让修改abs变量的指向在其它模块也生效,用 `__builtin__.abs = 10` 就可以了。 当然**实际写代码绝对不应该这样做**.. 205 | 206 | --- 207 | 208 | ### 传入函数 209 | 210 | 函数能够作为参数传递,接收这样的参数的函数就称为高阶函数。 简单举例: 211 | 212 | ```python 213 | def add(x, y, f): 214 | return f(x) + f(y) 215 | 216 | >>> add(-5, 6, abs) 217 | 11 218 | ``` 219 | 220 | 这里abs函数可以作为一个参数传入我们编写的add函数中,add函数就是一个高阶函数。 221 | 222 | --- 223 | 224 | ### map/reduce 225 | 226 | map()函数和reduce()函数是Python的两个内建函数(BIF)。 227 | 228 | #### map函数 229 | 230 | map()函数接收两个参数,一个是函数,一个是**Iterable对象**,map将传入的函数依次作用到序列的每个元素,并把结果作为**Iterator对象**(惰性序列,可以用list转换为列表输出)返回。例如: 231 | 232 | ```python 233 | >>> def f(x): 234 | ... return x * x 235 | ... 236 | >>> r = map(f, [1, 2, 3, 4, 5, 6, 7, 8, 9]) 237 | >>> list(r) 238 | [1, 4, 9, 16, 25, 36, 49, 64, 81] 239 | ``` 240 | 241 | 这里直接使用list()函数将迭代器对象转换为一个列表。 242 | 243 | 写循环也能达到同样效果,但是显然没有map()函数直观。 map()函数作为高阶函数,大大简化了代码,更易理解。 244 | 245 | ```python 246 | >>> list(map(str, [1, 2, 3, 4, 5, 6, 7, 8, 9])) 247 | ['1', '2', '3', '4', '5', '6', '7', '8', '9'] 248 | ``` 249 | 250 | 将一个整数列表转换为字符列表仅仅需要一行代码。 251 | 252 | #### reduce函数 253 | 254 | reduce接收两个参数,一个是函数(假设该函数称为f),一个是Iterable对象(假设是l)。函数f必须接收两个参数,reduce函数每次会把上一次函数f返回的值和l的下一个元素传入到f中,直到l中所有元素都参与过运算时返回函数f最后返回的值(第一次传入时传入l的头两个元素)。 255 | 256 | ```python 257 | >>> from functools import reduce 258 | >>> def add(x, y): 259 | ... return x + y 260 | ... 261 | >>> reduce(add, [1, 3, 5, 7, 9]) 262 | 25 263 | ``` 264 | 265 | 这里举了一个最简单的序列求和作例子(当然实际上我们直接用sum()函数更方便,这里只是为了举例子)。 这里reduce函数每次将add**作用于序列的前两个元素**,并**把结果返回序列的头部**,直到序列只剩下1个元素就返回结果(这样理解可能更直观一些)。 266 | 267 | ```python 268 | >>> from functools import reduce 269 | >>> def fn(x, y): 270 | ... return x * 10 + y 271 | ... 272 | >>> def char2num(s): 273 | ... return {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9}[s] #字符对应整数的dict,返回传入字符对应的整数 274 | ... 275 | >>> reduce(fn, map(char2num, '13579')) 276 | 13579 277 | ``` 278 | 279 | 可以整理一下,作为一个整体的str2int函数: 280 | 281 | ```python 282 | def str2int(s): 283 | def fn(x, y): 284 | return x * 10 + y 285 | def char2num(s): 286 | return {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9}[s] 287 | return reduce(fn, map(char2num, s)) 288 | ``` 289 | 290 | 使用lambda匿名函数还可以进一步简化: 291 | 292 | ```python 293 | def char2num(s): 294 | return {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9}[s] 295 | 296 | def str2int(s): 297 | return reduce(lambda x, y: x * 10 + y, map(char2num, s)) 298 | ``` 299 | 300 | #### 练习 301 | 302 | > 1.利用map()函数,把不规范的英文名字变为首字母大写其他字母小写的规范名字。 303 | 304 | **Hint**: 305 | 306 | - 字符串支持切片操作,并且可以用加号做字符串拼接。 307 | - 转换大写用upper函数,转换小写用lower函数。 308 | 309 | ```python 310 | def normalize(name): 311 | return name[0].upper()+name[1:].lower() 312 | 313 | L1 = ['adam', 'LISA', 'barT'] 314 | L2 = list(map(normalize, L1)) 315 | print(L2) 316 | ``` 317 | 318 | > 2.编写一个prod()函数,可以接受一个list并利用reduce()求积。 319 | 320 | **Hint**: 321 | 322 | - 用匿名函数做两数相乘 323 | - 用reduce函数做归约,得到列表元素连乘之积。 324 | 325 | ```python 326 | from functools import reduce 327 | def prod(L): 328 | return reduce(lambda x,y: x*y,L) 329 | 330 | print('3 * 5 * 7 * 9 =', prod([3, 5, 7, 9])) 331 | ``` 332 | 333 | > 3.利用map和reduce编写一个str2float函数,把字符串'123.456'转换成浮点数123.45。 334 | 335 | **Hint**: 336 | 337 | - 这题的思路是找到小数点的位置i(从个位开始数i个数字之后),然后让转换出的整数除以10的i次方。 338 | - 另外一种思路是在转换时遇到小数点后,改变转换的方式由 `num*10+当前数字` 变为 `num+当前数字/point`。 point初始为1,每次加入新数字前除以10。 339 | 340 | ```python 341 | from functools import reduce 342 | from math import pow 343 | 344 | def chr2num(s): 345 | return {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9}[s] 346 | 347 | def str2float(s): 348 | return reduce(lambda x,y:x*10+y,map(chr2num,s.replace('.',''))) / pow(10,len(s)-s.find('.')-1) 349 | 350 | print(str2float('985.64785')) 351 | ``` 352 | 353 | --- 354 | 355 | ### filter 356 | 357 | filter()函数同样是内建函数,用于过滤序列。 filter()接收一个函数和一个Iterable对象。 和map()不同的时,filter()把传入的函数依次作用于每个元素,然后**根据函数返回值是True还是False决定保留还是丢弃该元素**。 358 | 359 | 简单的奇数筛选例子: 360 | 361 | ```python 362 | def is_odd(n): 363 | return n % 2 == 1 364 | 365 | list(filter(is_odd, [1, 2, 4, 5, 6, 9, 10, 15])) 366 | # 结果: [1, 5, 9, 15] 367 | ``` 368 | 369 | 筛掉列表的空字符串: 370 | 371 | ```python 372 | def not_empty(s): 373 | return s and s.strip() 374 | 375 | list(filter(not_empty, ['A', '', 'B', None, 'C', ' '])) 376 | # 结果: ['A', 'B', 'C'] 377 | ``` 378 | 379 | 其中,`strip` 函数用于删除字符串中特定字符,格式为:`s.strip(rm)`,删除s字符串中开头、结尾处的包含在rm删除序列中的字符。 rm为空时默认删除空白符(包括'\n', '\r', '\t', ' ')。 380 | 381 | 注意到 `filter()` 函数返回的是一个 **Iterator对象**,也就是一个**惰性序列**,所以要强迫 `filter()` 完成计算结果,需要用 `list()` 函数获得所有结果并返回list。 382 | 383 | filter函数最重要的一点就是正确地定义一个**筛选函数**(即传入filter作为参数的那个函数)。 384 | 385 | #### 练习 386 | 387 | > 1.用filter筛选素数 388 | 389 | 这里使用**埃氏筛法**。 390 | 391 | > 首先,列出从2开始的所有自然数,构造一个序列: 392 | 393 | 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, ... 394 | 395 | > 取序列的第一个数2,它一定是素数,然后用2把序列的2的倍数筛掉: 396 | 397 | 3, 5, 7, 9, 11, 13, 15, 17, 19, ... 398 | 399 | > 取新序列的第一个数3,它一定是素数,然后用3把序列的3的倍数筛掉: 400 | 401 | 5, 7, 11, 13, 17, 19, ... 402 | 403 | > 以此类推... 404 | 405 | 首先构造一个生成器,输出3开始的奇数序列: 406 | 407 | ```python 408 | def _odd_iter(): 409 | n = 1 410 | while True: 411 | n = n + 2 412 | yield n 413 | ``` 414 | 415 | 然后定义一个筛选函数,传入n,判断x能否除尽n: 416 | 417 | ```python 418 | def _not_divisible(n): 419 | return lambda x: x % n > 0 420 | ``` 421 | 422 | 这里x是匿名函数的参数,**由外部提供**。 423 | 424 | 然后就是定义返回素数的生成器了。 425 | 426 | - 首先输出素数2,然后初始化奇数队列,每次输出队首(必然是素数,因为前一轮的过滤已经排除了比当前队首小且非素数的数)。 427 | 428 | - 构造新的队列,每次用当前序列最小的数作为除数,检验后面的数是否素数。 429 | 430 | 定义如下: 431 | 432 | ```python 433 | def primes(): 434 | yield 2 435 | it = _odd_iter() # 初始序列 436 | while True: 437 | n = next(it) # 返回序列的第一个数 438 | yield n 439 | it = filter(_not_divisible(n), it) # 构造新序列 440 | ``` 441 | 442 | 这里因为it是一个迭代器,每次使用next就得到队列的下一个元素,实际上就类似队列的出列操作,挤掉队首,不用担心重复。 443 | 444 | 然后这里filter的原理,就是把当前it队列的每个数都放进_not_divisible(n)中检测一下,注意**不是作为参数n传入而是作为匿名函数的参数x传入**! 445 | 446 | `_not_divisible(n)` 实际是**作为一个整体来看**的,它返回一个**自带参数n的函数**(也即那个匿名函数),然后filter再把列表每一个元素一一传返回的匿名函数中。一定要搞清楚这一点。 447 | 448 | - 最后,因为primes产生的也是一个无限长的惰性序列,我们一般不需要求那么多,简单写个循环用作退出即可: 449 | 450 | ```python 451 | # 打印1000以内的素数: 452 | for n in primes(): 453 | if n < 1000: 454 | print(n) 455 | else: 456 | break 457 | ``` 458 | 459 |
460 | 461 | > 2.用filter筛选回文数 462 | 463 | **Hint**: 464 | 465 | - str可以把整数转换为字符串 466 | - [::-1]可以得到逆序的列表。 467 | 468 | ```python 469 | def is_palindrome(n): 470 | return str(n) == str(n)[::-1] 471 | 472 | print(list(filter(is_palindrome, range(0,1001)))) 473 | ``` 474 | 475 | --- 476 | 477 | ### sorted 478 | 479 | Python内置的 `sorted()` 函数就可以对list进行排序: 480 | 481 | ```python 482 | >>> sorted([36, 5, -12, 9, -21]) 483 | [-21, -12, 5, 9, 36] 484 | ``` 485 | 486 | 并且 `sorted()` 作为**高阶函数**还允许接受一个key函数用于自定义排序,例如: 487 | 488 | ```python 489 | >>> sorted([36, 5, -12, 9, -21], key=abs) 490 | [5, 9, -12, -21, 36] 491 | ``` 492 | 493 | key指定的函数将作用于list的每一个元素上,并**根据key函数返回(映射)的结果进行排序**,最后**对应回列表中的元素**进行输出。 494 | 495 | 再看一个字符串排序例子: 496 | 497 | ```python 498 | >>> sorted(['bob', 'about', 'Zoo', 'Credit']) 499 | ['Credit', 'Zoo', 'about', 'bob'] 500 | ``` 501 | 502 | 默认情况下是按ASCII码排序的,但我们往往希望按照字典序来排,思路就是把字符串变为全小写/全大写再排: 503 | 504 | ```python 505 | >>> sorted(['bob', 'about', 'Zoo', 'Credit'], key=str.lower) 506 | ['about', 'bob', 'Credit', 'Zoo'] 507 | ``` 508 | 509 | 默认排序是由小到大,要反相排序只需把reverse参数设为True。 温习前面的知识,这里reverse参数是一个**命名关键字参数**。 510 | 511 | ```python 512 | >>> sorted(['bob', 'about', 'Zoo', 'Credit'], key=str.lower, reverse=True) 513 | ['Zoo', 'Credit', 'bob', 'about'] 514 | ``` 515 | 516 | 使用好sorted函数的关键就是定义好一个映射函数。 517 | 518 | #### 练习 519 | 520 | > 给出成绩表,分别按姓名和成绩进行排序。 521 | 522 | ```python 523 | >>> L = [('Bob', 75), ('Adam', 92), ('Bart', 66), ('Lisa', 88)] 524 | >>> L2 = sorted(L, key = lambda x:x[0]) #按姓名排序 525 | >>> L2 526 | [('Adam', 92), ('Bart', 66), ('Bob', 75), ('Lisa', 88)] 527 | >>> L3 = sorted(L, key = lambda x:x[1]) #按成绩排序 528 | >>> L3 529 | [('Bart', 66), ('Bob', 75), ('Lisa', 88), ('Adam', 92)] 530 | ``` 531 | 532 | --- 533 | 534 |
535 | 536 | ## 返回函数 537 | 538 | ### 函数作为返回值 539 | 540 | 高阶函数除了可以接受函数作为参数外,还可以把函数作为结果值返回。 541 | 542 | 比方说我们想实现一个对可变参数求和的函数,可以这样写: 543 | 544 | ```python 545 | def calc_sum(*args): 546 | ax = 0 547 | for n in args: 548 | ax = ax + n 549 | return ax 550 | ``` 551 | 552 | 调用时可以传入任意个数字,并得到这些数字的和。而如果我们不需要立即求和,而是后面再根据需要来进行求和,可以写为返回求和函数的形式: 553 | 554 | ```python 555 | def lazy_sum(*args): 556 | def sum(): 557 | ax = 0 558 | for n in args: 559 | ax = ax + n 560 | return ax 561 | return sum 562 | ``` 563 | 564 | 在调用 `lazy_sum` 时,返回一个sum函数,但sum函数内部的求和代码没有执行: 565 | 566 | ```python 567 | >>> f = lazy_sum(1, 3, 5, 7, 9) 568 | >>> f 569 | .sum at 0x101c6ed90> 570 | ``` 571 | 572 | 当我们再调用返回的这个sum函数时,就能得到和值了: 573 | 574 | ```python 575 | >>> f() 576 | 25 577 | ``` 578 | 579 | 注意!每一次调用 `lazy_sum` 返回的函数都是不同的!**即使传入相同的参数,返回函数也是不同的**!举个例子: 580 | 581 | ```python 582 | >>> f1 = lazy_sum(1, 3, 5, 7, 9) 583 | >>> f2 = lazy_sum(1, 3, 5, 7, 9) 584 | >>> f1==f2 585 | False 586 | ``` 587 | 588 | f1和f2是不同的两个函数,虽然调用它们得到同样的结果,但它们是互不影响的。 589 | 590 | --- 591 | 592 | ### 闭包 593 | 594 | 在Python中,从表现形式上来讲,闭包可以定义为:如果在一个内部函数里,对外部作用域(**非全局作用域**)的变量进行了引用,那么这个**内部函数就被认为是闭包**(closure)。 如上面例子中的f就是一个闭包,它调用了变量i,变量i属于外面的循环体而不是全局变量。 595 | 596 | 看一个例子: 597 | 598 | ```python 599 | def count(): 600 | 601 | fs = [] 602 | for i in range(1, 4): 603 | def f(): 604 | return i*i 605 | fs.append(f) 606 | 607 | return fs 608 | 609 | f1, f2, f3 = count() 610 | ``` 611 | 612 | 三个返回函数的调用结果是: 613 | 614 | ```python 615 | >>> f1() 616 | 9 617 | >>> f2() 618 | 9 619 | >>> f3() 620 | 9 621 | ``` 622 | 623 | 解析一下,这里count函数是一个返回三个函数的函数,里面使用了一个循环体来产生要返回的三个函数。从i为1开始到i等于3,每次产生一个函数f,返回i的平方值。如果按照平常的思路来看,可能会觉得返回的三个函数f1、f2、f3应该分别输出1、4、9。 624 | 625 | 但实际上并不是这样的,这是因为**返回一个函数时,函数内部的代码是没有执行的! 只有在调用这个返回的函数时才会执行**! 626 | 627 | 调用count函数时,实际上返回了3个新的函数,循环变量i的值也变为3。在调用这3个返回的函数时,它们的代码才会执行,这时引用的i的值就都是3。 628 | 629 | 如果一定要在闭包中用到外部的循环变量,要怎么办呢? 我们先定义一个函数,**用它的参数绑定循环变量**,然后再在它的里面定义要返回的函数。 这样无论后面循环变量怎么变,**已经绑定到参数的值是不会变**的,就能得到我们期望的结果了。也即把上面的例子改写为: 630 | 631 | ```python 632 | def count(): 633 | 634 | def f(j): 635 | def g(): 636 | return j*j 637 | return g 638 | 639 | fs = [] 640 | for i in range(1, 4): 641 | fs.append(f(i)) # f(i)立刻被执行,因此i的当前值被传入f() 642 | return fs 643 | ``` 644 | 645 | 调用结果: 646 | 647 | ```python 648 | >>> f1, f2, f3 = count() 649 | >>> f1() 650 | 1 651 | >>> f2() 652 | 4 653 | >>> f3() 654 | 9 655 | ``` 656 | 657 | 这里闭包g用到的变量j是**外部作用域f**的,并且**j作为参数绑定在f中不再改变**,不同于外部作用域count函数中的变量i。 所以执行count返回的3个函数,每个的结果都不同。 658 | 659 | #### 总结 660 | 661 | - 返回闭包时,**不要在闭包的代码中引用外部作用域的循环变量或者外部作用域中会变化的变量**。 662 | 663 | - **不应该在闭包中修改外部作用域的局部变量**。 664 | 665 | --- 666 | 667 |
668 | 669 | ## 匿名函数 670 | 671 | 当我们在使用函数作为参数时,有些时候,**不需要预先显式地定义函数**,直接传入一个匿名函数更方便。 672 | 673 | 举个简单例子,计算 `f(x)=x²` 时,不需要显示定义f(x),使用匿名函数可以直接一行解决,这样写非常简洁。 674 | 675 | ```python 676 | >>> list(map(lambda x: x * x, [1, 2, 3, 4, 5, 6, 7, 8, 9])) 677 | [1, 4, 9, 16, 25, 36, 49, 64, 81] 678 | ``` 679 | 680 | **关键字lambda**表示要定义一个匿名函数,**冒号前面**的x表示函数的参数。 681 | 682 | 匿名函数有个限制,就是**只能包含一个表达式**,不用写return,**返回值就是该表达式的结果**。 683 | 684 | 所以上面这个匿名函数就是:x作为参数传入,返回 `x*x` 的结果。 685 | 686 | 用匿名函数有个好处,因为函数没有名字,**不必担心函数名冲突**。此外,匿名函数也是一个**函数对象**,也可以把匿名函数赋值给一个变量,再利用变量来调用该函数: 687 | 688 | ```python 689 | >>> f = lambda x: x * x 690 | >>> f 691 | at 0x101c6ef28> 692 | >>> f(5) 693 | 25 694 | ``` 695 | 696 | 并且匿名函数作为一个函数对象,也能被函数返回(像上一节那样): 697 | 698 | ```python 699 | def build(x, y): 700 | return lambda: x * x + y * y 701 | ``` 702 | 703 | 这里返回的函数是一个匿名函数,它没有参数,里面调用的变量x和变量y是绑定在外部作用域build中的参数。所以调用build时会根据使用的参数返回这个匿名函数,调用返回的这个函数时使用的变量x和变量y就是调用build时用的参数。 704 | 705 | --- 706 | 707 |
708 | 709 | ## 装饰器 710 | 711 | 有时候我们希望为函数增加一些额外的功能,比如在调用函数的前后自动打印某些信息,但又**不希望修改定义函数的代码**。这时就可以使用**装饰器(Decorator)**了,它是python中对[装饰器模式](http://en.wikipedia.org/wiki/Decorator_pattern)的实现,可以**在代码运行期间动态增加功能**(装饰器的代码和要装饰的函数的代码还是要写好,这里只是说对要装饰的函数使用装饰器后,在运行时要装饰的函数会被重新包装一遍,使得它具有了装饰器中定义的功能)。 712 | 713 | 比方说我们要实现一个打印函数名的额外功能,它是通过调用函数对象的 `__name__` 属性获得的: 714 | 715 | ```python 716 | >>> def now(): 717 | ... print('2015-3-25') 718 | ... 719 | >>> now.__name__ 720 | 'now' 721 | ``` 722 | 723 | 如果我们不想在每个函数中都重复写实现这个功能的代码,可以把它写为装饰器的形式,然后为每个函数添加这个装饰器。**装饰器本身也是一个函数**,这个例子可以写为: 724 | 725 | ```python 726 | def log(func): 727 | def wrapper(*args, **kw): 728 | print('call %s():' % func.__name__) 729 | return func(*args, **kw) 730 | return wrapper 731 | ``` 732 | 733 | 它**像正常函数一样定义**(没有特别的语法),接收一个函数作为参数,并且返回一个函数 `wrapper`(Python中函数也是对象,既可以作为参数,也能被返回)。回顾前一节的内容,可以看出 `wrapper` 函数是一个闭包,它本身接收可变参数 `*args` 和关键字参数 `**kw`,并且引用了外部作用域中绑定在 `log` 函数参数中的 `func` 变量。 734 | 735 | 使用装饰器时,借助Python的**@语法**,把装饰器放在函数定义的上一行即可: 736 | 737 | ```python 738 | @log 739 | def now(): 740 | print('2016-2-10') 741 | ``` 742 | 743 | 运行时: 744 | 745 | ```python 746 | >>> now() 747 | call now(): 748 | 2015-3-25 749 | ``` 750 | 751 | 原理是这样的,把 `@log` 放在 `now()` 函数的定义前,运行代码的时候,实际上是在函数定义后执行了: 752 | 753 | ```python 754 | now = log(now) 755 | ``` 756 | 757 | 也即执行了 `log` 函数,并把 `now` 这个变量名赋值为 `log(now)` 返回的 `wrapper(*args, **kw)` 函数(也即 `now` 引用的函数对象变了)。此时查看 `now` 变量指向的函数对象的名字,会发现已经变成了 `wrapper`: 758 | 759 | ```python 760 | >>> now.__name__ 761 | 'wrapper' 762 | ``` 763 | 764 | 此时我们调用这个新的 `now()` 函数时,实际上执行的就是 `wrapper` 函数中的代码,打印出函数信息,然后再**调用原来的 `now` 函数**。要注意 **`wrapper` 调用的 `now` 函数和我们调用的 `now` 函数是不同的两个函数**,我们调用的 `now` 函数已经变成了 `wrapper` 函数,而 `wrapper` 函数调用的则是绑定在 `log` 函数参数中的原本的 `now` 函数。 765 | 766 | **简单归纳一下**: 767 | 768 | - 装饰器也是一个函数 769 | - 装饰器实际上是把传入的函数进行一层包装,返回一个新函数 770 | - 要为函数添加装饰器时,在函数定义前使用 `@装饰器名` 即可 771 | 772 | 装饰器的原理部分如果还有不清楚的,不妨看看知乎上[李冬](https://www.zhihu.com/question/26930016/answer/105175177)的答案,讲得比较浅显和清楚。 773 | 774 | --- 775 | 776 | ### 带参数的decorator 777 | 778 | 前面提到**装饰器可以用于为函数提供增强功能而无须修改函数本身的代码**,在装饰器函数中,闭包 `wrapper` 接收的参数就是函数的参数。但是,如果我们希望在使用装饰器时可以更灵活一些,为不同的函数添加功能类似但又略有不同的装饰器呢?这时我们可以使用**带参数的装饰器**来实现(装饰器本身也是函数,是可以传入参数的)。 779 | 780 | 比方说要实现一个自定义打印文本的功能: 781 | 782 | ```python 783 | def log(text): 784 | def decorator(func): 785 | def wrapper(*args, **kw): 786 | print('%s %s():' % (text, func.__name__)) 787 | return func(*args, **kw) 788 | return wrapper 789 | return decorator 790 | ``` 791 | 792 | 注意到这里在 `wrapper` 和 `log` 之间又套了一层函数,现在变为了 `log` 接收参数 `text` 并返回一个装饰器 `decorator`。这个 `decorator` 接收一个函数对象,输出文本 `text` 和函数对象的名字,理解起来其实不难。 793 | 794 | 使用这个装饰器: 795 | 796 | ```python 797 | @log('execute') 798 | def now(): 799 | print('2015-3-25') 800 | ``` 801 | 802 | 执行结果如下: 803 | 804 | ```python 805 | >>> now() 806 | execute now(): 807 | 2015-3-25 808 | ``` 809 | 810 | 事实上,把 `@log` 放在 `now()` 函数的定义前,运行代码时实际上在函数定义后执行了: 811 | 812 | ```python 813 | >>> now = log('execute')(now) 814 | ``` 815 | 816 | 也即是先调用 `log` 函数,传入参数 `log('execute')`,这时返回了 `decorator` 这个装饰器,然后传入了 `now` 函数,最后返回包装好的 `now` 函数(也即 `wrapper` 函数)。 817 | 818 | --- 819 | 820 | ### 属性复制 821 | 822 | 前面已经提到使用 **`@语法`** 之后,now变量指向的函数名字等属性都改变了,变成了 `wrapper` 函数的,实际上,我们希望变量 `now` 的属性依然是原本 `now()` 函数的属性,这时就需要进行**属性复制**。 823 | 824 | 我们不需要编写类似 `wrapper.__name__ = func.__name__` 这样的代码来逐个把原函数的属性复制给 `wrapper`,Python内置的 `functools.wraps` 装饰器可以满足我们的需求。方法如下: 825 | 826 | ```python 827 | import functools 828 | 829 | def log(func): 830 | @functools.wraps(func) 831 | def wrapper(*args, **kw): 832 | print('call %s():' % func.__name__) 833 | return func(*args, **kw) 834 | return wrapper 835 | ``` 836 | 837 | 和原来定义装饰器的代码对比,唯一修改的就是加上了 `@functools.wraps(func)` 这一句。 当然,还要注意先导入functools模块。 838 | 839 | --- 840 | 841 | ### 练习 842 | 843 | #### 习题1 844 | 845 | > 编写一个decorator,能在函数调用的前后分别打印出 `'begin call'` 和 `'end call'` 的日志。 846 | 847 | **解析:** 848 | 849 | 这题很简单,在 `wrapper` 调用原函数之前,各编写一条打印语句就可以了。 850 | 851 | **代码:** 852 | 853 | ```python 854 | def decorator(func): 855 | def wrapper(*args,**kw): 856 | print("begin call") 857 | a = func(*args,**kw) 858 | print("end call") 859 | return a 860 | return wrapper 861 | 862 | @decorator 863 | def now(): 864 | print("haha") 865 | ``` 866 | 867 | **执行结果:** 868 | 869 | ```python 870 | >>> now # now是一个函数 871 | .wrapper at 0x00000254B45D8EA0> 872 | >>> now() # 调用now函数 873 | begin call 874 | haha 875 | end call 876 | ``` 877 | 878 | #### 习题2 879 | 880 | > 写出一个@log的decorator,使它既支持: 881 | 882 | ```python 883 | @log 884 | def f(): 885 | pass 886 | ``` 887 | 888 | > 又支持: 889 | 890 | ```python 891 | @log('execute') 892 | def f(): 893 | pass 894 | ``` 895 | 896 | **解析**: 897 | 898 | 思路很简单,我们知道使用不带参数的装饰器时,传入装饰器函数(即这里的 `log`)的参数就是要装饰的函数(比方说 `now` 函数);而带参数的装饰器接收的参数则不是要装饰的函数而是别的(比方说一个字符串)。所以呀,我们可以依然使用带参数的装饰器作为原型,但在里面加入对参数类型的判断,如果接收到字符串参数则表示这次调用的是有参数的装饰器,否则就是调用不带参数的装饰器。 899 | 900 | **代码:** 901 | 902 | ```python 903 | import functools 904 | def log(text): # 默认参数,没有参数时,text就是空字符串 905 | def decorator(func): 906 | @functools.wraps(func) # 属性复制 907 | def wrapper(*args,**kw): 908 | if isinstance(text, str): 909 | print('%s %s():' % (text, func.__name__)) 910 | else: 911 | print('%s():' % func.__name__) 912 | return func(*args, **kw) 913 | return wrapper 914 | if isinstance(text, str): # 接收到字符串后返回decorator函数 915 | return decorator 916 | else: 917 | return decorator(text) # 接收到函数则直接返回wrapper函数 918 | 919 | @log('execute') # 带参数text的decorator 920 | def now1(): 921 | print('2016-2-10') 922 | 923 | @log # 不带参数text的decorator 924 | def now2(): 925 | print('2016-2-10') 926 | ``` 927 | 928 | **执行结果:** 929 | 930 | ```python 931 | >>> now1() 932 | execute now1(): 933 | 2016-2-10 934 | >>> now2() 935 | now2(): 936 | 2016-2-10 937 | ``` 938 | 939 | --- 940 | 941 |
942 | 943 | ## 偏函数 944 | 945 | Python的functools模块提供了很多有用的功能,其中一个就是**偏函数(Partial function)**。 946 | 947 | `functools.partial(f, *args, **kw)` 的作用就是创建一个偏函数,**把一个函数的某些参数给固定住**(也就是设置默认值),返回一个新的函数,调用这个新函数会更简单。 948 | 949 | 举个例子,字符串转整型数的函数int,可以使用**关键字参数base**,指定字符串的进制是多少,然后转换为int的时候会按照base进行进制转换,把字符串**转换成十进制整数**。 如: 950 | 951 | ```python 952 | >>> int('1000000', base=2) 953 | 64 954 | ``` 955 | 956 | 如果有大量的二进制字符串要转换,每次都写base=2很麻烦,我们就会希望定义一个新函数,把base参数固定为2,无须每次指定: 957 | 958 | ```python 959 | def int2(x, base=2): 960 | return int(x, base) 961 | ``` 962 | 963 | 实际上我们不需要自己定义,使用 `functools.partial` 就可以轻松创建偏函数: 964 | 965 | ```python 966 | >>> import functools 967 | >>> int2 = functools.partial(int, base=2) 968 | >>> int2('1000000') 969 | 64 970 | >>> int2('1010101') 971 | 85 972 | ``` 973 | 974 | 运行int('1000000')实际相当于: 975 | 976 | ```python 977 | kw = { 'base': 2 } 978 | int('1000000', **kw) 979 | ``` 980 | 981 | **Notice**: 982 | 983 | 这里创建偏函数只是设定了默认值为2,**调用偏函数时依然可以把参数设置为其他值**。 984 | 985 | ```python 986 | >>> int2('1000000', base=10) 987 | 1000000 988 | ``` 989 | 990 | `functools.partial` **不但可以接收关键字参数,还可以接收可变参数** `*args`,如: 991 | 992 | ```python 993 | >>> max2 = functools.partial(max, 10) 994 | >>> max2(5, 6, 7) 995 | 10 996 | ``` 997 | 998 | 相当于max函数每次接收到若干数字时,都默认再放入一个整数10,然后找其中的最大值。 999 | 1000 | --- 1001 | -------------------------------------------------------------------------------- /My_Python_Notebook/05模块.md: -------------------------------------------------------------------------------- 1 | # 模块 2 | 3 | 在开发过程中,一个文件里代码越长就越不容易维护。 4 | 5 | 为了编写易于维护的代码,我们把很多函数分组,分别放到不同的文件里,这样,每个文件包含的代码就相对较少,很多编程语言都采用这种组织代码的方式。 6 | 7 | 在Python中,**一个.py文件就称之为一个模块(Module)**。 8 | 9 | ## 目录 10 | 11 | 12 | 13 | - [使用模块的几个好处](#使用模块的几个好处) 14 | - [模块名和包名](#模块名和包名) 15 | - [使用模块](#使用模块) 16 | - [例子解析](#例子解析) 17 | - [命名规范和作用域](#命名规范和作用域) 18 | - [安装第三方库](#安装第三方库) 19 | - [模块搜索路径](#模块搜索路径) 20 | - [文件搜索路径](#文件搜索路径) 21 | 22 | 23 | 24 | 25 | ## 使用模块的几个好处 26 | 27 | - 大大提高了代码的可维护性。 28 | - 编写代码不必从零开始。当一个模块编写完毕,就可以被其他模块引用。 29 | - 避免函数名和变量名冲突。相同名字的函数和变量完全可以分别存在不同的模块中。 但还是要注意**变量名尽量不要与BIF(built-in functions,内建函数)名字冲突**。 30 | 31 | --- 32 | 33 |
34 | 35 | ## 模块名和包名 36 | 37 | 由于不同的人编写的模块名可能会相同,为了**避免模块名冲突**,Python又引入了**按目录来组织模块**的方法,称为**包(Package)**。 38 | 39 | ![图片](http://oe0e8k1nf.bkt.clouddn.com/My_Python_Notebook_Module0.png) 40 | 41 | 假设图中abc和xyz两个模块的名字和外面其他模块名字冲突了,我们可以通过包来组织模块,避免冲突。 只要**顶层包名(也即这里的mycompany文件夹)不同即可**。 42 | 43 | 此时abc的**模块名**变为 `mycompany.abc`, xyz的**模块名**变为 `mycompany.xyz`。 44 | 45 | **Notice**: 46 | 47 | - **注意区分模块和模块名**!两者不一定相同!(比如上面的模块 `abc` 和它的模块名 `mycompany.abc`) 48 | 49 | - **每个包目录下都必须有一个 `__init__.py` 文件**,否则Python就不会把这个文件夹当作一个包。 `__init__.py`可以是空文件,也可以有Python代码,它本身就是一个模块,并且**它的模块名就是包名**(这里是 `mycompany`)。 50 | 51 | ![图片](http://oe0e8k1nf.bkt.clouddn.com/My_Python_Notebook_Module1.png) 52 | 53 | **包结构可以是多级的**。比方说这里www.py的**模块名**就是 `mycompany.web.www` 。 两个utils.py的模块名分别是 `mycompany.utils` 和 `mycompany.web.utils`,它们不会冲突 。 `mycompany.web` 这个模块名对应的就是web目录下的 `__init__.py` 模块。 54 | 55 | **Notice**: 56 | 57 | 自己创建的模块的模块名**不要和Python自带的模块的模块名冲突**! 比如系统自带sys模块,自己的模块就不要命名sys.py,否则无法会无法正确import自带的sys模块。 58 | 59 | --- 60 | 61 |
62 | 63 | ## 使用模块 64 | 65 | Python本身就内置了很多模块可以直接import使用。 下面我们自己编写一个Hello模块作为例子: 66 | 67 | ```python 68 | #!/usr/bin/env python3 69 | # -*- coding: utf-8 -*- 70 | 71 | 'This is the docstring(document comment) of this module ' 72 | 73 | __author__ = 'Lincoln Deng' 74 | 75 | import sys 76 | 77 | def test(): 78 | args = sys.argv 79 | if len(args)==1: 80 | print('No argument is passed.') 81 | elif len(args)==2: 82 | print('Hello, %s!' % args[1]) 83 | else: 84 | print('Too many arguments!') 85 | 86 | if __name__=='__main__': 87 | test() 88 | ``` 89 | 90 | ### 例子解析 91 | 92 | #### Python模块的标准文件模板 93 | 94 | 文件的第1行和第2行是**标准注释**: 95 | 96 | - 第1行注释是用于**声明使用什么程序来执行这个脚本**,它可以**使得这个脚本能在Unix/Linux/Mac上直接运行**(也即**可以使用 `./Hello.py` 的形式执行**而不是 `python Hello.py` 的形式)。如果系统装了多个版本的python,`#!/usr/bin/env python3` 会保证调用环境变量 `$PATH` 中的第一个叫python3的程序来执行脚本。又因为在一些系统中python能被重定向到python3,也即默认使用python3,所以直接用 `#!/usr/bin/env python` 也是可以的。还有一种写法是 `#!/usr/bin/python`,这样写就是指定一个路径,兼容性不如使用env的写法好。注意,如果我们使用 `python Hello.py` 或者 `python3 Hello.py` 的方式**直接指定解释器来执行的话,这句注释就没用了**。另外,在Windows下**这句注释也是被忽略的**,但**为了代码的兼容性最好还是写上**。 97 | 98 | - 第2行注释表示(解释器和编辑器)**应使用UTF-8编码来读取这个.py脚本文件中的代码文本**。**在Python2中,默认源代码编码方式为ASCII**,要用到中文就得采用很别扭的escape写法,直接写中文会出错。后来有了[PEP 0263标准](https://www.python.org/dev/peps/pep-0263/),规定了**显式声明代码文本编码**的方法。一般有三种格式,包括:能被大部分编辑器识别的 `# -*- coding: utf-8 -*-`,最简单的 `# coding=utf-8` 以及vim的 `# vim: set fileencoding=utf-8` (其实还可以写成别的方式,主要看编辑器怎样正则匹配这一行)。当然这里的utf-8可以换为其他编码。注意**编码声明必须放在代码文件的第一行或第二行**。另外,这里只是**声明读取时采用的编码方式,保存代码时用什么方式要自己设置编辑器**。实测由于**Python3默认用utf-8编码**,所以只要我们正确使用utf-8编码保存代码文件,那么读取时不声明也没关系。当然,**为了代码的兼容性最好还是写上**。 99 | 100 | 文件的第4行是一个字符串,表示模块的文档注释,**任何模块的第一个字符串都被视为模块的文档注释;** 101 | 102 | 文件的第6行使用 `__author__` 变量记录模块作者的名字。 103 | 104 | 以上就是**Python模块的标准文件模板**,在Windows下使用Python3时,其实不写也没关系,但养成良好的书写习惯更好。 105 | 106 | #### 正式代码部分 107 | 108 | 1) **导入sys模块** 109 | 110 | 如果想使用Python内置的sys模块,就要先导入该模块: `import sys`。 导入之后,相当于**创建了一个变量`sys`,该变量指向sys模块,通过这个变量可以访问sys模块的全部功能**。 111 | 112 | 2) **使用argv变量获取参数列表** 113 | 114 | **sys模块有一个 `argv` 变量**,这个变量属于list类型,存储着**命令行的所有参数**(即我们在命令行执行该脚本时使用的参数)。`argv` 列表中至少包含一个元素,因为**第一个参数永远是该脚本文件的名称**,例如: 115 | 116 | 在命令行执行 `python3 Hello.py` 获得的 `sys.argv` 就是 `['Hello.py']`; 117 | 118 | 在命令行执行 `python3 Hello.py Michael` 获得的 `sys.argv` 就是 `['Hello.py', 'Michael]`。 要获取字符串 `'Michael'` 只需要调用 `sys.argv[1]`。 119 | 120 | 3) **条件判断** 121 | 122 | ```python 123 | if __name__=='__main__': 124 | test() 125 | ``` 126 | 127 | 在Hello模块中我们定义了一个test函数,这个条件判断的意思就是,如果程序执行到这里,`__name__` 变量的值是 `'__main__'` 的话就执行 `test` 函数。 128 | 129 | 其中 `__name__` 变量是个特殊变量,当我们**在命令行执行**Hello.py时,Python解释器就会把 `__name__` 变量赋值为 `__main__`。 130 | 131 | 但**如果在别的文件中导入模块,`__name__` 的值就是模块的名字**而非 `__main__`,此时if判断结果为 `False`。 借助这个特性,我们可以**在if判断中编写一些额外的代码**,用于测试模块的功能,而在使用(导入)模块时,if判断里的代码不会被执行。 132 | 133 | #### 在命令行下执行 134 | 135 | 保存代码文件后,打开命令行,先把路径切换到保存 `Hello.py` 的目录,然后执行: 136 | 137 | ``` 138 | C:\Users\Administrator\Desktop>python Hello.py 139 | No argument is passed. 140 | 141 | C:\Users\Administrator\Desktop>python Hello.py Lincoln 142 | Hello, Lincoln! 143 | ``` 144 | 145 | #### 在交互环境下执行 146 | 147 | 比方说使用IDLE或者在命令行中输入python进入: 148 | 149 | ```python 150 | C:\Users\Administrator\Desktop>python 151 | Python 3.5.1 |Anaconda 4.0.0 (64-bit)| (default, Feb 16 2016, 09:49:46) [MSC v.1900 64 bit (AMD64)] on win32 152 | Type "help", "copyright", "credits" or "license" for more information. 153 | >>> import Hello 154 | >>> Hello.__name__ 155 | 'Hello' 156 | >>> Hello.test() 157 | No argument is passed. 158 | ``` 159 | 160 | 导入Hello模块时,它的 `__name__` 变量会被赋值为模块名 `Hello`,而不是 `__main__`,所以if判断为 `False`,if判断里的代码不会被执行。如果要使用 `test` 函数,就要通过 `模块名.函数名` 的方式进行调用,使用模块内的其他变量/函数同理。 161 | 162 | --- 163 | 164 | ### 命名规范和作用域 165 | 166 | 在模块中我们会定义很多函数和变量,有些是希望给别人用的,有些则希望仅仅在模块内部使用。 167 | 168 | #### 公开(public)的变量和函数 169 | 170 | 命名格式如: `abc`,`x123`,`PI`, 如果是有特殊用途则**在名称前后各加上两个下划线**,如:`__author__`,`__name__`。 171 | 172 | ```python 173 | >>> Hello.__doc__ 174 | 'This is the docstring(document comment) of this module ' 175 | >>> Hello.__name__ 176 | 'Hello' 177 | >>> Hello.__author__ 178 | 'Lincoln Deng' 179 | >>> Hello.__file__ 180 | 'C:\\Users\\Administrator\\Desktop\\Hello.py' 181 | ``` 182 | 183 | 可以看到 `__doc__` 变量返回了我们前面模块例子的代码中第一个字符串,也即**文档注释(DocString)**。什么是文档注释呢?其实就是一个**模块/类/函数/方法的定义中第一个声明的字符串**,使用这些对象的 `.doc` 属性即可访问。关于文档注释的书写标准可以查看[PEP 0257](https://www.python.org/dev/peps/pep-0257/)。 184 | 185 | #### 私有(private)的变量和函数 186 | 187 | 命名格式是在**名称前加一个或两个下划线**,如 `_xxx` 和 `__xxx`。这样的函数或变量**不应该被外部直接引用(即通过 `模块名.变量名` 的方式调用)**。比方说下面定义的 `_private_1` 函数和 `_private_2` 函数,我们不希望使用这个模块的人调用它们: 188 | 189 | ```python 190 | def _private_1(name): 191 | return 'Hello, %s' % name 192 | 193 | def _private_2(name): 194 | return 'Hi, %s' % name 195 | 196 | def greeting(name): 197 | if len(name) > 3: 198 | return _private_1(name) 199 | else: 200 | return _private_2(name) 201 | ``` 202 | 203 | 虽然不希望用户调用私有函数,但我们**可以暴露给用户一个接口(公开的函数)**,也即这里的 `greeting` 函数,把调用两个私有函数的代码和逻辑封装在里面,用户直接调用 `greeting` 函数,然后让 `greeting` 函数决定怎样调用私有函数。 204 | 205 | 当用户使用模块时不需要关心私有的变量和函数,直接使用公开的变量和函数就可以了。 这是一种常用的**代码封装和抽象**的方法。 206 | 207 | **注意:** 208 | 209 | 这里说私有函数和变量 **不应该被直接引用**,而不是 **不能被直接引用**。 因为Python没有方法可以限制用户调用私有函数和变量(没有),所以这样命名**只是一种约定的编程习惯**,使用者怎么做就要看他自己怎么决定了。 210 | 211 | 良好的习惯是**外部不需要引用的函数全部定义成private,只有外部需要引用的函数才定义为public**。 212 | 213 | --- 214 | 215 |
216 | 217 | ## 安装第三方库 218 | 219 | 在Python中,安装第三方库(从而使用第三方库中提供的第三方模块),可以通过**包管理工具pip**(也有其他的包管理工具)完成。 220 | 221 | 安装Python时选择了安装pip的话,就可以直接在命令行中使用pip工具了。 222 | 223 | ``` 224 | pip install Pillow 225 | ``` 226 | 227 | 在命令行键入 `pip install 第三方库的库名` 后, pip就会自动帮用户下载并安装第三方库。 228 | 229 | ![安装1](http://imglf0.nosdn.127.net/img/dnpRZUpJZlB5VUQ2UTdlUDluSm9RZEZVQ0hEMjZrWjJReENQRmwvNVp4U0tnN3VUV004RE1BPT0.png?imageView&thumbnail=500x0&quality=96&stripmeta=0&type=jpg) 230 | 231 | ![安装2](http://imglf2.nosdn.127.net/img/dnpRZUpJZlB5VUQ2UTdlUDluSm9RZU9hUFRtT0dMaFRWTENkaW1SREVrN25rd3VXSlVBKy9nPT0.png?imageView&thumbnail=500x0&quality=96&stripmeta=0&type=jpg) 232 | 233 | 这里安装的是**Python Imaging Library**这个第三方库,是一个Python下非常强大的图像处理工具库。 因为PIL只支持到Python2.7,所以这里用的是基于PIL开发的支持Python3的**Pillow**。 234 | 235 | 安装完成后打开 `F:\Python35\Lib\site-packages` 文件夹(具体路径看安装Python的位置而定)就会发现多了两个文件夹,一个是 `Pillow-3.1.1.dist-info`, 另一个是 `PIL`。 前者包含该库的一些基本信息,后者就是我们需要用到的包了,里面是有 `__init__.py` 文件的。 236 | 237 | 安装好的包我们可以在文件中直接用 `from 包名 import 模块名` 来导入要使用的模块,而不需要先转到模块所在的目录下再导入,也不需要把模块复制到我们的工程文件夹中。 238 | 239 | 举一个使用Pillow包中利用Image模块生成图片缩略图的例子: 240 | 241 | ```python 242 | >>> from PIL import Image 243 | >>> im = Image.open('C:/Users/Administrator/Desktop/test.png') # 打开指定路径下的一张照片 244 | >>> print(im.format, im.size, im.mode) # 打印照片的文件格式&尺寸&颜色模式 245 | PNG (400, 300) RGB 246 | >>> im.thumbnail((200, 100)) # 创建缩略图 247 | >>> im.save('thumb.jpg', 'JPEG') # 保存缩略图 248 | >>> im.show() # 查看图片 249 | ``` 250 | 251 | 其他常用的第三方库还有MySQL的驱动:mysql-connector-python,用于科学计算的NumPy库:numpy,用于生成文本的模板工具Jinja2,等等。 252 | 253 | --- 254 | 255 |
256 | 257 | ## 模块搜索路径 258 | 259 | 在Python中导入模块时,Python解释器会从指定好的路径中进行搜索。我们可以使用sys模块的变量 `path` 来查看模块的搜索路径,导入模块时会从这些路径中查找.py文件: 260 | 261 | ```python 262 | C:\Users\Administrator>python 263 | Python 3.5.1 |Anaconda 4.0.0 (64-bit)| (default, Feb 16 2016, 09:49:46) [MSC v.1900 64 bit (AMD64)] on win32 264 | Type "help", "copyright", "credits" or "license" for more information. 265 | >>> import sys 266 | >>> sys.path 267 | ['', 'F:\\Anaconda3\\python35.zip', 'F:\\Anaconda3\\DLLs', 'F:\\Anaconda3\\lib', 'F:\\Anaconda3', 'F:\\Anaconda3\\lib\\site-packages', 268 | 'F:\\Anaconda3\\lib\\site-packages\\Sphinx-1.3.5-py3.5.egg', 269 | 'F:\\Anaconda3\\lib\\site-packages\\win32', 'F:\\Anaconda3\\lib\\site-packages\\win32\\lib', 270 | 'F:\\Anaconda3\\lib\\site-packages\\Pythonwin', 'F:\\Anaconda3\\lib\\site-packages\\setuptools-20.3-py3.5.egg'] 271 | ``` 272 | 273 | sys模块的 `path` 变量是一个列表,它会**在启动Python时被初始化**,初始赋值(按顺序)由三个部分组成,一是**当前目录**(即列表中的空字符串),二是**环境变量 `PYTHONPATH` 中的路径**,三是一些**默认的路径**(包含内置模块和一些通过pip安装的模块)。由于我没有设置环境变量 `PYTHONPATH`,所以上面只有一和三两部分。尝试新建一个名为 `PYTHONPATH` 的环境变量,添加一条路径指向F盘,**重新启动Python程序**,此时就会发现 `path` 变量的初始赋值中多了F盘的路径了: 274 | 275 | ```python 276 | C:\Users\Administrator>python 277 | Python 3.5.1 |Anaconda 4.0.0 (64-bit)| (default, Feb 16 2016, 09:49:46) [MSC v.1900 64 bit (AMD64)] on win32 278 | Type "help", "copyright", "credits" or "license" for more information. 279 | >>> import sys 280 | >>> sys.path 281 | ['', 'F:\\', 'F:\\Anaconda3\\python35.zip', 'F:\\Anaconda3\\DLLs', 'F:\\Anaconda3\\lib', 'F:\\Anaconda3', 'F:\\Anaconda3\\lib\\site-packages', 282 | 'F:\\Anaconda3\\lib\\site-packages\\Sphinx-1.3.5-py3.5.egg', 283 | 'F:\\Anaconda3\\lib\\site-packages\\win32', 'F:\\Anaconda3\\lib\\site-packages\\win32\\lib', 284 | 'F:\\Anaconda3\\lib\\site-packages\\Pythonwin', 'F:\\Anaconda3\\lib\\site-packages\\setuptools-20.3-py3.5.egg'] 285 | ``` 286 | 287 | 如果需要使用自己编写的模块,可以把它们放到这些目录中。 也可以自己**增加搜索路径**。具体来说分为两种方法: 288 | 289 | - 方法一:**直接使用 `apeend` 往 `sys.path` 列表添加搜索路径**,这种方法**只在该次运行时有效**,重启Python交互环境后会恢复原来的路径: 290 | 291 | ```python 292 | >>> import sys 293 | >>> sys.path.append('C:/Users/Administrator/Desktop') 294 | ``` 295 | 296 | - 方法二:**配置环境变量PYTHONPATH**,只需要增加自己的搜索路径,默认的路径是不会被覆盖掉的,使用这种方法就**不需要每次都修改 `sys.path` **了。 297 | 298 | 关于 `sys.path` 的详情可以查看[官方文档](https://docs.python.org/3/library/sys.html#sys.path)。 299 | 300 | --- 301 | 302 |
303 | 304 | ## 文件搜索路径 305 | 306 | 这一节与模块无关,但是觉得有必要区分好**文件搜索路径**和**模块搜索路径**! 文件搜索路径是当前工作目录,如果我们不指定路径,直接使用文件名访问文件的时候,Python会从当前路径中进行查找;模块搜索路径则是像上一节提及的那样,指 `sys.path` 列表中包含的路径。 307 | 308 | 要获取当前工作路径可以使用os模块的 `getcwd` 函数,也即**get current work directory**: 309 | 310 | ```python 311 | >>> import os 312 | >>> os.getcwd() 313 | 'C:\\Users\\Administrator\\Desktop' 314 | ``` 315 | 316 | 要调用的文件如果放在当前工作路径上就可以直接用 `文件名.文件格式` 指定,比方说我在桌面放了一张 `test.jpg`,那么访问时直接用 `im = Image.open('test.jpg')` 就可以打开了,无须使用完整的路径,也即 `im = Image.open('C:/Users/Administrator/Desktop/test.jpg')`。 317 | 318 | 注意Python中**路径字符串里斜杠的使用**, 如果**使用反斜杠划分就必须转义**,也即写作双反斜杠 `\\`;如果**使用左斜杠 `/` 则不需要进行转义**。Python默认路径字符串都使用双反斜杠。 319 | 320 | 如果文件不在当前工作路径,那么我们写路径时可以有两种写法: 321 | 322 | - 使用**绝对路径** 323 | 324 | ```python 325 | im = Image.open('C:/Users/Administrator/Desktop/test.jpg') 326 | ``` 327 | 328 | 这里使用了左斜杠,所以不需要转义。 329 | 330 | - 使用**相对路径** 331 | 332 | ```python 333 | im = Image.open('./test.jpg') 334 | ``` 335 | 336 | 相对路径即相对当前工作路径而言的路径,这是为了避免路径过长而设计的,可以**用 `.` 符来代替当前工作路径**。 337 | -------------------------------------------------------------------------------- /My_Python_Notebook/06面向对象编程.md: -------------------------------------------------------------------------------- 1 | # 面向对象编程 2 | 3 | ## 目录 4 | 5 | 6 | 7 | - [基本概念](#基本概念) 8 | - [对比面向过程与面向对象](#对比面向过程与面向对象) 9 | - [五种编程范式的区分](#五种编程范式的区分) 10 | - [面向对象编程的三大特点](#面向对象编程的三大特点) 11 | - [类和实例](#类和实例) 12 | - [数据封装](#数据封装) 13 | - [访问限制](#访问限制) 14 | - [获取和修改限制访问的属性](#获取和修改限制访问的属性) 15 | - [继承和多态](#继承和多态) 16 | - [基本概念](#基本概念-1) 17 | - [实例的数据类型](#实例的数据类型) 18 | - [多态的好处](#多态的好处) 19 | - [开闭原则](#开闭原则) 20 | - [静态语言 VS 动态语言](#静态语言-vs-动态语言) 21 | - [小结](#小结) 22 | - [获取对象信息](#获取对象信息) 23 | - [type函数](#type函数) 24 | - [isinstance函数](#isinstance函数) 25 | - [dir函数](#dir函数) 26 | - [hasattr函数、setattr函数、getattr函数](#hasattr函数、setattr函数、getattr函数) 27 | - [实例属性和类属性](#实例属性和类属性) 28 | 29 | 30 | 31 | 32 | ## 基本概念 33 | 34 | 面向对象编程 —— Object Oriented Programming,简称OOP,是一种编程思想。OOP把对象作为程序的基本单元,**一个对象不仅包含数据还包含操作数据的函数**。 35 | 36 | ### 对比面向过程与面向对象 37 | 38 | - **面向过程编程(Procedural programming)**:把计算机程序视为**一系列子程序的集合**。为了简化程序设计,面向过程把子程序继续切分为更小的子程序,也即把大的功能分为若干小的功能进行实现,从而降低系统的复杂度,这种做法也称为**模块化(Modularity)**。 39 | 40 | - **面向对象编程(Object-oriented programming)**:把计算机程序视为**一组对象的集合**,而每个对象都可以接收其他对象发过来的消息,并处理这些消息,计算机程序的执行就是一系列消息在各个对象之间传递。 41 | 42 | 下面以保存和打印学生成绩表为例,分别展示面向过程编程和面向对象编程的不同: 43 | 44 | **面向过程编程**: 45 | 46 | ```python 47 | def save_score(name, score): 48 | return {'name':name, 'score':score} 49 | 50 | def print_score(std): 51 | print('%s: %s' % (std['name'], std['score'])) 52 | 53 | bart = save_score('Michael', 98) 54 | lisa = save_score('Bob', 81) 55 | 56 | print_score(bart) 57 | print_score(lisa) 58 | ``` 59 | 60 | 面向过程编程其实就是细分功能并逐步实现,这里分出了保存成绩和打印成绩两个细的功能,并分别封装成子程序(或者说函数),然后通过调用各个子程序来实现程序的最终目标(保存并打印成绩)。 61 | 62 | **面向对象编程**: 63 | 64 | ```python 65 | class Student(object): 66 | def __init__(self, name, score): 67 | self.name = name 68 | self.score = score 69 | def print_score(self): 70 | print('%s: %s' % (self.name, self.score)) 71 | 72 | bart = Student('Bart Simpson', 59) 73 | lisa = Student('Lisa Simpson', 87) 74 | bart.print_score() 75 | lisa.print_score() 76 | ``` 77 | 78 | 面向对象编程强调程序的主体是对象,这里我们把学生抽象成一个**类(class)**,这个类的对象拥有 `name` 和 `score` 这两个**属性(Property)**,那么保存成绩就是把学生类实例化为**对象(object)**,打印成绩就是给每个学生对象发送一个 `print_score` 的**消息(message)**,让对象自己打印自己的属性。这个发送消息的过程又称为调用对象的**方法(method)**,注意区分函数和方法。 79 | 80 | --- 81 | 82 | ### 五种编程范式的区分 83 | 84 | 这一小节是额外加上的,因为之前第4章[函数式编程](https://github.com/familyld/learnpython/blob/master/My_Python_Notebook/04%E5%87%BD%E6%95%B0%E5%BC%8F%E7%BC%96%E7%A8%8B.md)也讲了一种编程范式,这章又引入了面向过程编程和面向对象编程的概念,所以就在这里整理一下。也可以直接查看维基词条:[Wikipedia - Procedural programming](https://en.wikipedia.org/wiki/Procedural_programming#Comparison_with_imperative_programming) 以及引用的文章,讲得比较细致和清晰。更详细的可以查找专门讲编程范式的书籍来浏览。 85 | 86 | #### 面向过程编程(Procedural programming) 87 | 88 | 面向过程就是拆分和逐步实现,前一小节已经说过了,这里不再累述。 89 | 90 | #### 命令式编程(Imperative programming) 91 | 92 | 有时候面向过程编程又称为命令式编程,两者经常混用,但它们之间还是有一点差别的。面向过程编程依赖于**块**和**域**,比方说有 `while`,`for` 等保留字;而命令式编程则没有这样的特征,一般采用 `goto` 或者分支来实现。 93 | 94 | #### 面向对象编程(Object-oriented programming) 95 | 96 | 面向对象也在上一小节简单介绍过了,它比面向过程编程抽象程度更高,面向过程将一个编程任务划分为若干变量、数据结构和子程序的组合,而面向对象则是划分为对象,对象的行为(方法)、使用对象的数据(成员/属性)的接口。面向过程编程使用子程序去操作数据,而面向对象则把这两者结合为对象,每一个对象的方法作用在自身上。 97 | 98 | #### 函数式编程(Functional programming) 99 | 100 | 在模块化和代码复用上,函数式编程和面向过程编程是很像的。但函数式编程中不再强调指令(赋值语句)。面向过程编写出的程序是一组指令的集合,这些指令可能会隐式地修改了一些公用的状态,而函数式编程则规避了这一点,**每一个语句都是一个表达式,只依赖于自己而不依赖外部状态**,因为我们现在所用的计算机都是基于指令运作的,所以函数式编程的效率会稍低一些,但是函数式编程一个很大的好处就是,既然每个语句都是独立的,那么就**很容易实现并行化**了。 101 | 102 | #### 逻辑编程(Logic programming) 103 | 104 | 逻辑编程是一种我们现在比较少接触的变成范式,它关注于表达问题是什么是不是怎样解决问题。感兴趣的话可以了解一下Prolog语言。 105 | 106 | --- 107 | 108 | ### 面向对象编程的三大特点 109 | 110 | 1. **数据封装** 111 | 2. **继承** 112 | 3. **多态** 113 | 114 | --- 115 | 116 |
117 | 118 | ## 类和实例 119 | 120 | 在Python中定义类是通过 `class` 关键字完成的,像前面例子一样: 121 | 122 | ```python 123 | class Student(object): 124 | pass 125 | ``` 126 | 127 | 定义类的格式是 `class 类名(继承的类名)` 。 **类名一般用大写字母开头**,关于继承的知识会留在后续的章节里详述,**object类是所有类的父类**,在Python3中不写也行(既可以写作 `class Student:` 或 `class Student():`),会自动继承,详情可以看[python class inherits object](http://stackoverflow.com/questions/4015417/python-class-inherits-object)。 128 | 129 | 定义类以后,即使没有定义构造函数和属性,我们也可以实例化对象: 130 | 131 | ```python 132 | >>> bart = Student() 133 | >>> bart 134 | <__main__.Student object at 0x10a67a590> 135 | >>> Student 136 | 137 | ``` 138 | 139 | 可以看到变量bart指向的就是一个Student类的实例,**每个实例的地址是不一样的**。 140 | 141 | 与静态语言不同,Python作为动态语言,我们可以自由地给一个实例变量绑定属性: 142 | 143 | ```python 144 | >>> bart.name = 'Bart Simpson' 145 | >>> bart.name 146 | 'Bart Simpson' 147 | ``` 148 | 149 | **绑定的属性在定义类时无须给出**,随时都可以给一个实例绑定新属性。 但是!**这样绑定的新属性仅仅绑定在这个实例上**,别的实例是没有的! 也就是说,**同一个类的多个实例例拥有的属性可能不同**! 150 | 151 | 对于我们认为**必须绑定的属性**,可以通过定义特殊的 `__init__` 方法进行初始化,在创建实例时就进行绑定: 152 | 153 | ```python 154 | class Student(object): 155 | 156 | def __init__(self, name, score): 157 | self.name = name 158 | self.score = score 159 | ``` 160 | 161 | 使用 `__init__` 方法要注意: 162 | 163 | 1. 第一个参数永远是 `self`,**指向创建出的实例本身**。 164 | 2. **有了 `__init__` 方法,创建实例时就不能传入空的参数,必须传入与 `__init__` 方法匹配的参数**,参数self不用传,Python解释器会自动传入。 165 | 166 | ```python 167 | >>> bart = Student('Bart Simpson', 59) 168 | >>> bart.name 169 | 'Bart Simpson' 170 | >>> bart.score 171 | 59 172 | ``` 173 | 174 | ### 数据封装 175 | 176 | 数据封装是面向对象编程特点之一,对于类的每个实例而言,属性访问可以通过函数来实现。 既然**实例本身拥有属性数据**,那么访问实例的数据就不需要通过外面的函数实现,可以**直接在类的内部定义访问数据的函数**。 177 | 178 | **利用内部定义的函数,就把数据封装起来了**,这些函数和类本身是关联的,称为**类的方法**。 179 | 180 | ```python 181 | class Student(object): 182 | 183 | def __init__(self, name, score): 184 | self.name = name 185 | self.score = score 186 | 187 | def print_score(self): 188 | print('%s: %s' % (self.name, self.score)) 189 | ``` 190 | 191 | 依然是前面的例子,可以看到在类中定义方法和外面定义的函数**唯一区别就是方法的第一个参数永远是实例变量 `self`**。其他一致,仍然可以用默认参数,可变参数,关键字参数和命名关键字参数等参数形式。和创建实例一样,调用方法时不需传入self。 192 | 193 | 对于外部,**类的方法实现细节不用了解**,只需要知道怎样调用,能返回什么就可以了。 194 | 195 | --- 196 | 197 | ### 访问限制 198 | 199 | 尽管前一节中我们把数据用方法进行了封装,但事实上,实例化对象后,我们依然可以直接通过属性名来访问一个实例的属性值,并且自由地修改属性值。要让实例的内部属性不被外部访问,只需要在属性的名称前加上两个下划线 `__` 就可以了,此时属性就转换成了**私有属性**。 200 | 201 | ```python 202 | >>> class Student(): 203 | ... def __init__(self, name, score): 204 | ... self.__name = name # 绑定私有属性__name 205 | ... self.__score = score # 绑定私有属性__score 206 | ... def print_score(self): # 通过方法访问私有属性 207 | ... print(self.__name, self.__score) 208 | ... 209 | >>> bart = Student('Bart', 98) # 创建一个Student类的实例bart 210 | >>> bart.__name # 由于访问限制的保护,对外部而言,实例bart是没有__name这个属性的 211 | Traceback (most recent call last): 212 | File "", line 1, in 213 | AttributeError: 'Student' object has no attribute '__name' 214 | >>> bart.__score # 由于访问限制的保护,对外部而言,实例bart是没有__score这个属性的 215 | Traceback (most recent call last): 216 | File "", line 1, in 217 | AttributeError: 'Student' object has no attribute '__score' 218 | >>> bart.print_score() # 但是通过print_score方法可以访问该实例的__name属性和__score属性 219 | Bart 98 220 | ``` 221 | 222 | 这是怎么实现的呢?其实呀,不能访问的实质是Python解释器**对外给私有变量添加了前缀** `_类名`,比如把 `__name` 会被改成 `_Student__name` 。所以我们在外部(即不是通过类的方法)访问时,`__name` 属性是不存在的,但访问 `_Student__name` 属性就可以了: 223 | 224 | ```python 225 | >>> bart._Student__name 226 | 'Bart' 227 | ``` 228 | 229 | 所以说,**即使有访问限制,外部依然可以访问和修改内部属性**,Python没有任何机制预防这一点,只有靠使用者自己注意了。 230 | 231 | 还有一点必须明白。**使用访问限制跟绑定属性是不冲突的**,所以虽然对内而言存在 `__name` 属性,但对外而言这个属性不存在,我们依然可以给实例绑定一个 `__name` 属性: 232 | 233 | ```python 234 | >>> bart.__name = 'Alice' # 外部绑定__name属性 235 | >>> bart.__name # 现在外部可以访问__name属性了 236 | 'Alice' 237 | >>> bart.print_score() # 但对内部方法来说,__name属性依然是原来的 238 | Bart 98 239 | ``` 240 | 241 | Python同样**没有任何机制防止我们给实例绑定一个和私有属性同名的属性**,从外部是可以访问这样绑定的属性的,但对内部方法而言,这种**绑定的赋值不会覆盖私有属性原来的值**,所以极容易出错,只有靠使用者自己注意了。 242 | 243 | 通过 `dir(bart)` 可以查看实例包含的所有变量(属性和方法): 244 | 245 | ```python 246 | >>> dir(bart) 247 | ['_Student__name', '_Student__score', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', 248 | '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__le__', '__lt__', '__module__', 249 | '__name', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', 250 | '__subclasshook__', '__weakref__', 'print_score'] 251 | ``` 252 | 253 | 在Python中,变量名类似 `__xxx__` 的,也就是**以双下划线开头,并且以双下划线结尾**的,是**特殊变量**,特殊变量允许直接访问,**注意私有变量不要这样取名**。有时会看到以**一个下划线开头**的变量名,比如 `_name`,这样的变量外部是可以访问的,不属于访问限制,但是按照约定,这样的变量我们应**视为私有变量,不应在外部直接访问**。 254 | 255 | --- 256 | 257 | ### 获取和修改限制访问的属性 258 | 259 | 对于限制访问的属性,外部代码还是需要进行访问和修改的,我们可以在类中定义对应的get方法和set方法: 260 | 261 | ```python 262 | >>> class Student(): 263 | ... def __init__(self, name, score): 264 | ... self.__name = name # 绑定私有属性__name 265 | ... self.__score = score # 绑定私有属性__score 266 | ... def print_score(self): # 通过方法访问私有属性 267 | ... print(self.__name, self.__score) 268 | ... def get_name(self): 269 | ... return self.__name 270 | ... def get_score(self): 271 | ... return self.__score 272 | ... def set_score(self, score): 273 | ... if 0 <= score <= 100: 274 | ... self.__score = score 275 | ... else: 276 | ... raise ValueError('bad score') 277 | ``` 278 | 279 | 通过类的set方法修改属性值,而不直接在外部修改有一个明显的好处,我们**可以在类的方法中对参数做检查,避免传入无效的参数**。 比如这里可以限制修改成绩时成绩的范围必须是0~100,超出就报错。 280 | 281 | --- 282 | 283 |
284 | 285 | ## 继承和多态 286 | 287 | ### 基本概念 288 | 289 | 在OOP程序设计中,当我们定义一个类的时候,可以从某个现有的类继承,新的类称为**子类(Subclass)**,被继承的类则称为**基类/父类/超类(Base class/Super class)**。比方说我们创建一个Animal类,该类有一个run方法: 290 | 291 | ```python 292 | >>> class Animal(object): 293 | def run(self): 294 | print('Animal is running...') 295 | ``` 296 | 297 | 定义一个Dog类继承Animal类,尽管我们没有为它编写任何方法,但它却可以**获得父类的全部功能**: 298 | 299 | ```python 300 | >>> class Dog(Animal): 301 | pass 302 | 303 | >>> dog=Dog() 304 | >>> dog.run() 305 | Animal is running... 306 | ``` 307 | 308 | 我们也可以为子类增加新的方法: 309 | 310 | ```python 311 | >>> class Dog(Animal): 312 | ... def run(self): 313 | ... print('Dog is running...') 314 | ... def eat(self): 315 | ... print('Eating meat...') 316 | ... 317 | >>> dog.run() 318 | Dog is running 319 | >>> dog.eat() 320 | Eating meat 321 | ``` 322 | 323 | 这里我们除了定义一个新的 `eat` 方法之外,还定义了一个和父类方法同名的 `run` 方法,它会覆盖父类的方法,当调用子类实例的 `run` 方法时调用的就会是子类定义的 `run` 方法而不是父类的 `run` 方法了,这个特点称作**多态**。总结一下,**如果子类也定义一个和父类相同的方法,则执行时总是调用子类的方法**。 324 | 325 | --- 326 | 327 | ### 实例的数据类型 328 | 329 | 我们**定义一个类,实际就是定义了一种数据类型**,和 `list`,`str` 等没有什么区别,要判断一个变量是否属于某种数据类型可以使用 `isinstance()` 方法: 330 | 331 | ```python 332 | >>> isinstance(dog,Animal) 333 | True 334 | >>> isinstance(dog,Dog) 335 | True 336 | ``` 337 | 338 | 实例化子类的对象**既属于子类数据类型也属于父类数据类型**!但是反过来就不可以,实例化父类的对象不属于子类数据类型。 339 | 340 | --- 341 | 342 | ### 多态的好处 343 | 344 | 比方说在外部编写一个函数,接收含有 `run` 方法的变量作参数: 345 | 346 | ```python 347 | def run_twice(animal): 348 | animal.run() 349 | animal.run() 350 | ``` 351 | 352 | 当传入Animal类的实例时就执行Animal类的 `run` 方法,当传入Dog类的实例时也能执行Dog类的 `run` 方法,非常方便: 353 | 354 | ```python 355 | >>> run_twice(Animal()) 356 | Animal is running... 357 | Animal is running... 358 | >>> run_twice(Dog()) 359 | Dog is running... 360 | Dog is running... 361 | ``` 362 | 363 | 有了多态的特性: 364 | 365 | 1. 实现同样的功能时,不需要为每个子类都在外部重写一个函数 366 | 2. 只要一个外部函数能**接收父类实例作参数,则任何子类实例都能直接使用这个外部函数** 367 | 3. 传入的任意子类实例在调用方法时,调用的都是子类中定义的方法(如果子类没有定义的话就调用父类的) 368 | 369 | --- 370 | 371 | ### 开闭原则 372 | 373 | 所谓开闭原则,指的是**对扩展开放**:允许新增 `Animal` 类的子类;**对修改封闭**:父类 `Animal` 可以调用的外部函数其子类也能直接调用,不需要对外部函数进行修改。 374 | 375 | 多态真正的威力:**调用方只需要关注调用函数的对象,不需要关注所调用的函数内部的细节**。当我们新增一种Animal的子类时,只要确保子类的 `run()` 方法编写正确即可,无须修改要调用的函数。对任意一个对象,我们**只需要知道它属于父类类型,无需确切知道它的子类类型具体是什么**,就可以放心地调用外部函数,而函数内部调用的方法是属于Animal类、Dog类、还是Cat类,**由运行时该对象的确切类型决定**。 376 | 377 | --- 378 | 379 | ### 静态语言 VS 动态语言 380 | 381 | 对于静态语言(如:Java),如果函数需要传入 `Animal` 类型,则传入的参数必须是 `Animal` 类型或者它的子类类型,否则无法调用 `run()` 方法。 382 | 383 | 对于动态语言而言,则不一定要传入Animal类型。**只要保证传入的对象有 `run()` 方法就可以了**。 这个特性又称"**鸭子类型**",即一个对象只需要 "看起来像鸭子,能像鸭子那样走" 就可以了。 384 | 385 | ```python 386 | class Timer(object): 387 | def run(self): 388 | print('Start...') 389 | ``` 390 | 391 | 比方说这里的Timer类既不属于Animal类型也不继承自Animal类,但它的实例依然可以传入 `run_twice()` 函数并且执行自己的 `run()` 方法。 392 | 393 | Python的 `file-like object` 就是一种鸭子类型。真正的文件对象有一个 `read()` 方法,能返回其内容。 但是只要一个类中定义有 `read()` 方法,它的实例就可以被视为 `file-like object`。许多函数接收的参数都是 `file-like object`,不一定要传入真正的文件对象,传入任何实现了 `read()` 方法的对象都可以。 394 | 395 | --- 396 | 397 | ### 小结 398 | 399 | - 继承可以把父类的所有功能都赋予子类,这样编写子类就**不必从零做起**,子类只需要**新增自己特有的方法,把父类不合适的方法覆盖重写**就可以了。 400 | 401 | - 动态语言的**鸭子类型**特点决定了**继承不像静态语言那样是必须的**。 402 | 403 | --- 404 | 405 |
406 | 407 | ## 获取对象信息 408 | 409 | 这一小节主要介绍给定一个对象,如何了解对象的类型以及有哪些方法。 410 | 411 | ### type函数 412 | 413 | 使用type函数获得各种变量的类型: 414 | 415 | ```python 416 | >>> type('str') 417 | 418 | >>> type(None) 419 | 420 | >>> type(abs) 421 | 422 | >>> animal = Animal() 423 | >>> type(animal) 424 | 425 | >>> dog = Dog() 426 | >>> type(dog) 427 | 428 | ``` 429 | 430 | `type()` 函数返回值是参数所属的类型,而这个返回值本身则属于type类型: 431 | 432 | ```python 433 | >>> type(type('123')) 434 | 435 | ``` 436 | 437 | 可以用来进行类型判断: 438 | 439 | ```python 440 | >>> type('123')==str 441 | True 442 | >>> type('123') == type(123) 443 | False 444 | ``` 445 | 446 | 导入内建的 `types` 模块还可以做更多更强大的类型判断: 447 | 448 | ```python 449 | >>> import types 450 | >>> def fn(): 451 | ... pass 452 | ... 453 | >>> type(fn)==types.FunctionType # 判断变量是否函数 454 | True 455 | >>> type(abs)==types.BuiltinFunctionType # 判断变量是否内建函数 456 | True 457 | >>> type(lambda x: x)==types.LambdaType # 判断变量是否匿名函数 458 | True 459 | >>> type((x for x in range(10)))==types.GeneratorType # 判断变量是否生成器 460 | True 461 | ``` 462 | 463 | --- 464 | 465 | ### isinstance函数 466 | 467 | 判断类型除了使用 `type()` 函数之外,使用 `isinstance()` 函数也能达到一样的效果: 468 | 469 | ```python 470 | >>> isinstance('a', str) 471 | True 472 | >>> isinstance(123, int) 473 | True 474 | >>> isinstance(b'a', bytes) 475 | True 476 | >>> isinstance([1, 2, 3], (list, tuple)) 477 | True 478 | ``` 479 | 480 | 并且 `isinstance()` 函数的参数二还可以是一个tuple,此时 `isinstance()` 函数将**判断参数一是否属于参数二tuple中所有类型中的其中一种,只要符合其中一种则返回 `True`**。 481 | 482 | 对于类的继承关系来说,`type()` 函数不太合适,因为我们没办法知道一个子类是否属于继承自某个类,使用 `isinstance()` 函数就可以解决这个问题了。**子类的实例也是父类的实例**。 483 | 484 | ```python 485 | >>> animal = Animal() 486 | >>> dog = Dog() 487 | >>> isinstance(animal, Animal) 488 | True 489 | >>> isinstance(animal, Dog) # 父类实例不是子类实例 490 | False 491 | >>> isinstance(dog, Animal) # 子类实例同时也是父类的实例 492 | True 493 | >>> isinstance(dog, Dog) 494 | True 495 | ``` 496 | 497 | --- 498 | 499 | ### dir函数 500 | 501 | `dir()` 函数**返回一个对象的所有属性和方法**: 502 | 503 | ```python 504 | >>> dir('ABC') 505 | ['__add__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', 506 | '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__gt__', '__hash__', '__init__', 507 | '__iter__', '__le__', '__len__', '__lt__', '__mod__', '__mul__', '__ne__', '__new__', '__reduce__', 508 | '__reduce_ex__', '__repr__', '__rmod__', '__rmul__', '__setattr__', '__sizeof__', '__str__', 509 | '__subclasshook__', 'capitalize', 'casefold', 'center', 'count', 'encode', 'endswith', 'expandtabs', 510 | 'find', 'format', 'format_map', 'index', 'isalnum', 'isalpha', 'isdecimal', 'isdigit', 'isidentifier', 511 | 'islower', 'isnumeric', 'isprintable', 'isspace', 'istitle', 'isupper', 'join', 'ljust', 'lower', 512 | 'lstrip', 'maketrans', 'partition', 'replace', 'rfind', 'rindex', 'rjust', 'rpartition', 'rsplit', 513 | 'rstrip', 'split', 'splitlines', 'startswith', 'strip', 'swapcase', 'title', 'translate', 'upper', 514 | 'zfill'] 515 | ``` 516 | 517 | 形如 `__xxx__` 的属性和方法都是有特殊用途的,比如 `__len__` 方法会返回对象长度,不过我们一般直接调用内建函数 `len()` 获取一个对象的长度。而事实上,`len()` 函数内部就是通过调用对象的 `__len__()` 方法来获取长度的。两种写法都可以: 518 | 519 | ```python 520 | >>> len('ABC') 521 | 3 522 | >>> 'ABC'.__len__() 523 | 3 524 | ``` 525 | 526 | 如果自己写的类希望能用 `len()` 函数获取对象的长度,可以在定义类时实现一个 `__len__()` 方法,例如: 527 | 528 | ```python 529 | >>> class MyDog(object): 530 | ... def __len__(self): 531 | ... return 100 532 | ... 533 | >>> dog = MyDog() 534 | >>> len(dog) 535 | 100 536 | ``` 537 | 538 | --- 539 | 540 | ### hasattr函数、setattr函数、getattr函数 541 | 542 | 首先定义一个类,并创建一个该类的实例 `obj`: 543 | 544 | ```python 545 | >>> class MyObject(object): 546 | ... def __init__(self): 547 | ... self.x = 9 548 | ... def power(self): 549 | ... return self.x * self.x 550 | ... 551 | >>> obj = MyObject() 552 | ``` 553 | 554 | 利用 `hasattr()` 函数、`setattr()` 函数、`getattr()` 函数可以分别实现**判断属性/方法是否存在**,**绑定/赋值属性/方法**,以及**获取属性值/方法**的功能: 555 | 556 | ```python 557 | >>> hasattr(obj, 'x') # 有属性x吗? 558 | True 559 | >>> obj.x # 直接访问属性x 560 | 9 561 | >>> setattr(obj, 'x', 10) # 为属性x设置新的值 562 | >>> obj.x # 直接访问属性x 563 | 10 564 | >>> hasattr(obj, 'y') # 有属性y吗? 565 | False 566 | >>> setattr(obj, 'y', 19) # 绑定一个新属性y 567 | >>> hasattr(obj, 'y') # 有属性y吗? 568 | True 569 | >>> obj.y # 直接访问属性y 570 | 19 571 | >>> getattr(obj, 'y') # 获取属性y 572 | 19 573 | ``` 574 | 575 | 如果试图获取不存在的属性,会抛出 `AttributeError` 的错误: 576 | 577 | ```python 578 | >>> obj.z # 直接访问属性z 579 | Traceback (most recent call last): 580 | File "", line 1, in 581 | AttributeError: 'MyObject' object has no attribute 'z' 582 | >>> getattr(obj, 'z') # 获取属性z 583 | Traceback (most recent call last): 584 | File "", line 1, in 585 | AttributeError: 'MyObject' object has no attribute 'z' 586 | ``` 587 | 588 | 特别地,我们可以**为 `getattr()` 函数传入一个额外参数表示默认值**,这样**当属性不存在时就会返回默认值**,而不是抛出错误了,但要注意,**getattr()` 并不会把这个默认值绑定到对象**: 589 | 590 | ```python 591 | >>> getattr(obj, 'z', 404) # 获取属性z,如果不存在,返回默认值404 592 | 404 593 | >>> obj.z # 没有进行绑定,所以obj仍然没有属性z 594 | Traceback (most recent call last): 595 | File "", line 1, in 596 | AttributeError: 'MyObject' object has no attribute 'z' 597 | ``` 598 | 599 | `getattr()` 函数除了可以获取属性之外,也可以用来获取方法并且赋值到变量,然后再通过变量使用: 600 | 601 | ```python 602 | >>> fn = getattr(obj, 'power') # 获取方法 power() 并赋值给变量fn 603 | >>> fn # fn指向obj.power 604 | > 605 | >>> fn() # 调用fn 606 | 100 607 | >>> obj.power() # 结果和调用obj.power是一样的 608 | 100 609 | ``` 610 | 611 | 看到这里也许有一些疑问,为什么我们明明可以通过 `obj.x = 10` 的方式设置属性值,还要用 `setattr(obj, 'x', 10)` 呢? 为什么我们明明可以通过 `obj.y` 直接访问属性值,还要用 `getattr(obj, 'x')` 呢?显然后者的写法要繁琐得多。确实,前面举得例子中,我们都没有任何必要这样写,也不应该这样写。`setattr()` 函数和 `getattr()` 函数是为了一些更特别的情况而创造的,例如: 612 | 613 | ```python 614 | >>> attrname = 'x' 615 | >>> getattr(obj, attrname) 616 | 10 617 | >>> obj.attrname 618 | Traceback (most recent call last): 619 | File "", line 1, in 620 | AttributeError: 'MyObject' object has no attribute 'attrname' 621 | ``` 622 | 623 | 当**属性名绑定在一个变量上**时,显然直接访问就没有办法使用了,但 `getattr()` 函数则不存在这方面的问题。并且前面也提到了 `getattr()` 函数允许我们**设置默认返回值**,这时直接访问无法做到的。又例如: 624 | 625 | ```python 626 | >>> attr = {'x':9, 'y':19, 'z':29} 627 | >>> attr.items() 628 | dict_items([('y', 19), ('z', 29), ('x', 9)]) 629 | >>> for k,v in attr.items(): 630 | ... setattr(obj, k, v) 631 | ... 632 | >>> for k in attr.keys(): 633 | ... getattr(obj, k) 634 | ... 635 | 19 636 | 29 637 | 9 638 | ``` 639 | 640 | 我们可以非常方便地把属性名和属性值存储在一个dict里面,然后利用循环进行赋值,而无需显式地写出 `obj.x = 9`,`obj.y = 19`,和 `obj.z = 29`,当我们需要批量赋值大量属性时,好处就体现出来了。同样地,我们也可以利用循环来读取需要的每个属性的值,而无需显式地逐个属性进行访问。 641 | 642 | 当然,如果可以直接写 `sum = obj.x + obj.y` 就不要写: `sum = getattr(obj, 'x') + getattr(obj, 'y')`,属性少的时候完全没有必要给自己添麻烦,对编写和阅读代码都不友好。 643 | 644 | 最后举个使用 `hasattr()` 函数的例子,比方说读取对象fp,我们可以首先判断fp是否有 `read()` 方法,有则进行读取,无则直接返回空值: 645 | 646 | ```python 647 | def readSomething(fp): 648 | if hasattr(fp, 'read'): 649 | return readData(fp) 650 | return None 651 | ``` 652 | 653 | --- 654 | 655 |
656 | 657 | ## 实例属性和类属性 658 | 659 | 由于Python是动态语言,根据类创建的实例可以任意绑定属性。 660 | 661 | 要给实例绑定属性除了通过实例变量之外,也可以通过self变量来完成绑定: 662 | 663 | ```python 664 | class Student(object): 665 | def __init__(self, name): 666 | self.name = name # 通过self变量绑定属性 667 | 668 | s = Student('Bob') 669 | s.score = 90 # 通过实例变量绑定属性 670 | ``` 671 | 672 | 但是,如果Student类本身需要绑定一个属性呢?可以直接在类中定义属性,这种属性是**类属性**,归Student类所有: 673 | 674 | ```python 675 | class Student(object): 676 | name = 'Student' 677 | ``` 678 | 679 | 当我们定义了一个类属性后,这个**类属性虽然归类所有,但类的所有实例都可以访问到**: 680 | 681 | ```python 682 | >>> class Student(object): 683 | ... name = 'Student' 684 | ... 685 | >>> s = Student() # 创建实例s 686 | >>> print(s.name) # 打印name属性,因为实例并没有name属性,所以会继续查找class的name属性 687 | Student 688 | >>> print(Student.name) # 打印类的name属性 689 | Student 690 | >>> s.name = 'Michael' # 给实例绑定name属性 691 | >>> print(s.name) # 由于实例属性优先级比类属性高,因此,它会屏蔽掉类的name属性 692 | Michael 693 | >>> print(Student.name) # 但是类属性并未消失,用Student.name仍然可以访问 694 | Student 695 | >>> del s.name # 删除实例的name属性 696 | >>> print(s.name) # 再次调用s.name,由于实例的name属性没有找到,类的name属性就显示出来了 697 | Student 698 | ``` 699 | 700 | 从上面的例子可以看出,在编写程序的时候,千万不要给实例属性和类属性设置相同的名字,因为**相同名称的实例属性将屏蔽掉类属性**。但是删除实例属性后,再使用相同的名称进行访问,返回的就会变回类属性了。 701 | 702 | -------------------------------------------------------------------------------- /My_Python_Notebook/09IO编程.md: -------------------------------------------------------------------------------- 1 | # IO编程 2 | 3 | ## 目录 4 | 5 | 6 | 7 | - [什么是IO](#什么是io) 8 | - [文件读写](#文件读写) 9 | - [读文件](#读文件) 10 | - [写文件](#写文件) 11 | - [file-like Object](#file-like-object) 12 | - [二进制文件](#二进制文件) 13 | - [字符编码](#字符编码) 14 | - [小结](#小结) 15 | - [StringIO和BytesIO](#stringio和bytesio) 16 | - [StringIO](#stringio) 17 | - [BytesIO](#bytesio) 18 | - [为什么使用StringIO和BytesIO](#为什么使用stringio和bytesio) 19 | - [读写IO需要注意的地方](#读写io需要注意的地方) 20 | - [小结](#小结-1) 21 | - [操作目录和文件](#操作目录和文件) 22 | - [简述](#简述) 23 | - [环境变量](#环境变量) 24 | - [操作目录和文件](#操作目录和文件-1) 25 | - [小结](#小结-2) 26 | - [练习](#练习) 27 | - [序列化](#序列化) 28 | - [序列化和反序列化](#序列化和反序列化) 29 | - [序列化(pickle)](#序列化(pickle)) 30 | - [JSON](#json) 31 | - [JSON进阶](#json进阶) 32 | - [小结](#小结-3) 33 | 34 | 35 | 36 | 37 | ## 什么是IO 38 | 39 | IO在计算机中指**输入和输出(Input/Output)**。由于程序运行时,数据是在内存中驻留,并由CPU这个超快的计算核心来进行处理的(处理时会把数据从内存载入到CPU的高速缓存中),而涉及到数据交换的操作,比如磁盘读写、网络传输等的时候,就需要使用IO接口来协调了。 40 | 41 | 比如你打开浏览器,访问新浪首页,浏览器这个程序就需要通过网络IO获取新浪的网页。浏览器首先会发送数据给新浪服务器,告诉它我想要首页的HTML,这个动作是往外发数据,叫Output,随后新浪服务器把网页发过来,这个动作是从外面接收数据,叫Input。所以,通常,程序完成IO操作会有Input和Output两个数据流。当然也有只用一个的情况,比如,从磁盘读取文件到内存,就只有Input操作,反过来,把数据写到磁盘文件里,就只是一个Output操作。 42 | 43 | IO编程中,**流(Stream)是**一个很重要的概念,可以把流想象成一个水管,**数据就是水管里的水,但是只能单向流动**。Input Stream就是数据从外面(磁盘、网络)流进内存,Output Stream就是数据从内存流到外面去。对于浏览网页来说,浏览器程序和新浪服务器之间至少需要建立两根水管,才可以既能发数据,又能收数据。 44 | 45 | 由于CPU和内存的速度远远高于外设的速度,所以,在IO编程中,就存在**速度严重不匹配**的问题。举个例子来说,比如要把100M的数据写入磁盘,CPU输出100M的数据只需要0.01秒,可是磁盘要接收这100M数据可能需要10秒,怎么办呢?有两种办法: 46 | 47 | - 第一种方法是让CPU等待,也就是程序暂停执行后续代码,等100M的数据在10秒后写入磁盘,再接着往下执行,这种模式称为**同步IO**; 48 | 49 | - 第二种方法是CPU不等待,只是告诉磁盘,“您老慢慢写,不着急,我接着干别的事去了”,于是,后续代码可以继续执行,这种模式称为**异步IO**。 50 | 51 | 同步和异步的区别就在于是否等待IO执行的结果。好比你去麦当劳点餐,你说“来个汉堡”,服务员告诉你,对不起,汉堡要现做,需要等5分钟,于是你站在收银台前面等了5分钟,拿到汉堡再去逛商场,这是同步IO。 52 | 53 | 你说“来个汉堡”,服务员告诉你,汉堡需要等5分钟,你可以先去逛商场,**等做好了,我们再通知你**,这样你可以立刻去干别的事情(逛商场),这是异步IO。 54 | 55 | 很明显,使用异步IO来编写程序性能会远远高于同步IO,但是异步IO的缺点是编程模型复杂。想想看,你得知道什么时候通知你“汉堡做好了”,而通知你的方法也各不相同。如果是服务员亲自跑过来找到你,这是**回调模式**,如果服务员发短信通知你,你就得不停地检查手机,这是**轮询模式**。总之,异步IO的复杂度远远高于同步IO。 56 | 57 | **操作IO的能力都是由操作系统提供的**,**编程语言所做的只是把操作系统提供的低级C接口封装起来方便使用**,Python也不例外。后面的小节中会详细讨论Python的IO编程接口。 58 | 59 | 注意,本章的IO编程都是同步模式,异步IO由于复杂度太高,后续涉及到服务器端程序开发时会再作讨论。 60 | 61 | --- 62 | 63 |
64 | 65 | ## 文件读写 66 | 67 | 读写文件是最常见的IO操作。Python内置了读写文件的函数,用法和C是兼容的。 68 | 69 | 读写文件前,我们先必须了解一下,**在磁盘上读写文件的功能都是由操作系统提供的**,现代操作系统不允许普通的程序直接操作磁盘,所以,读写文件就是请求操作系统打开一个文件对象(通常称为文件描述符),然后,通过操作系统提供的接口从这个文件对象中读取数据(读文件),或者把数据写入这个文件对象(写文件)。 70 | 71 | --- 72 | 73 | ### 读文件 74 | 75 | 要以读文件的模式打开一个文件对象,可以使用Python内置的 `open()` 函数,传入文件名(如果文件和代码文件在相同文件夹下就可以省略路径)和标示符 `'r'`: 76 | 77 | ```python 78 | >>> f = open('/Users/michael/test.txt', 'r') 79 | ``` 80 | 81 | 标示符 `'r'` 表示读,这样,我们就成功地打开了一个文件。 82 | 83 | 如果文件不存在,`open()` 函数就会抛出一个 `IOError` 的错误,并且给出错误码和详细的信息告诉你文件不存在: 84 | 85 | ```python 86 | >>> f=open('/Users/michael/notfound.txt', 'r') 87 | Traceback (most recent call last): 88 | File "", line 1, in 89 | FileNotFoundError: [Errno 2] No such file or directory: '/Users/michael/notfound.txt' 90 | ``` 91 | 92 | 如果文件打开成功,我们就可以调用 `read()` 方法来一次读取文件的全部内容,Python会把为文件的内容读到内存,返回的是一个 `str` 对象: 93 | 94 | ```python 95 | >>> f.read() 96 | 'Hello, world!' 97 | ``` 98 | 99 | 读取完毕后,如果不需要继续操作文件对象,我们就应当调用 `close()` 方法来关闭它。因为**文件对象会占用操作系统的资源,并且操作系统同一时间能打开的文件数量也是有限的**: 100 | 101 | ```python 102 | >>> f.close() 103 | ``` 104 | 105 | 由于读写文件都有可能产生 `IOError`,一旦出错,后面的 `f.close()` 就不会调用。所以,为了**保证无论是否出错都能正确地关闭文件**,我们可以使用 `try ... finally` 来实现: 106 | 107 | ```python 108 | try: 109 | f = open('/path/to/file', 'r') 110 | print(f.read()) 111 | finally: 112 | if f: 113 | f.close() 114 | ``` 115 | 116 | 但是每次都这么写实在太繁琐,所以,Python引入了 `with` 语句来自动帮我们调用 `close()` 方法((在[上一章](https://github.com/familyld/learnpython/blob/master/My_Python_Notebook/08%E9%94%99%E8%AF%AF%E3%80%81%E8%B0%83%E8%AF%95%E4%B8%8E%E6%B5%8B%E8%AF%95.md)中有 `with` 语句使用及原理的介绍)): 117 | 118 | ```python 119 | with open('/path/to/file', 'r') as f: 120 | print(f.read()) 121 | ``` 122 | 123 | 这和前面的 `try ... finally` 实现的效果是一样的,但是代码更简洁,并且我们不必调用 `f.close()` 方法。 124 | 125 | 调用 `read()` 方法可以一次性读取文件的全部内容。但如果文件有10G,内存就爆了,所以,为了保险起见,我们可以多次调用 `read(size)` 方法,每次最多读取size个**字节**的内容。 126 | 127 | 但是有时候文件不一定有严格的格式,比方说读取一篇文章,这时按字节读取就不太合适了。但我们可以调用 `readline()` 方法,`readline()` 方法每次读取文件的一行内容。而调用 `readlines()` 方法则会一次读取文件的所有内容并按行返回一个 `list` 对象。我们可以: 128 | 129 | ```python 130 | for line in f.readlines(): 131 | print(line.strip()) # 把末尾的换行符'\n'删掉再打印 132 | ``` 133 | 134 | --- 135 | 136 | ### 写文件 137 | 138 | 写文件和读文件是一样的,唯一区别是调用 `open()`函数时,传入标识符 `'w'` 或者 `'wb'` 表示写文本文件或写二进制文件: 139 | 140 | ```python 141 | >>> f = open('/Users/michael/test.txt', 'w') 142 | >>> f.write('Hello, world!') 143 | >>> f.close() 144 | ``` 145 | 146 | 你可以多次调用 `write()` 来写入文件,但是**最后一定要调用 `f.close()` 来关闭文件**。当我们写文件时,操作系统往往不会立刻把数据写入磁盘,而是**放在内存中缓存起来,空闲的时候再慢慢写入**。只有调用 `close()` 方法时,操作系统才会保证把没有写入的数据全部写入磁盘。忘记 `close()` 的后果是**数据可能只有一部分写到了磁盘,剩下的丢失了**。为了避免这样的情况发生,类似上一节所介绍的,我们可以使用 `with` 语句自动管理上下文: 147 | 148 | ```python 149 | with open('/Users/michael/test.txt', 'w') as f: 150 | f.write('Hello, world!') 151 | ``` 152 | 153 | 如果要写入特定编码的文本文件,还可以给 `open()` 函数传入 `encoding` 参数,将要写入的字符串自动转换成指定编码。 154 | 155 | --- 156 | 157 | ### file-like Object 158 | 159 | 在Python中,除了文件对象之外,内存中的字节流,网络流,自定义流等等,拥有 `read()` 方法的对象统称为 **`file-like Object`**。`file-like Object` 不需要继承自特定的类,只要有 `read()` 方法就行(文件对象的其他方法不一定都需要实现,可以看看[官方说明](https://docs.python.org/2.4/lib/bltin-file-objects.html)),这得益于Python鸭子类型的实现。`StringIO` 就是在内存中创建的 `file-like Object`,常用作临时缓冲。 160 | 161 | --- 162 | 163 | ### 二进制文件 164 | 165 | 前面讲的默认都是读取文本文件,并且是UTF-8编码的文本文件。要读取二进制文件,比如图片、视频等等,用 `'rb'` 模式打开文件即可: 166 | 167 | ```python 168 | >>> f = open('/Users/michael/test.jpg', 'rb') 169 | >>> f.read() 170 | b'\xff\xd8\xff\xe1\x00\x18Exif\x00\x00...' # 十六进制表示的字节 171 | ``` 172 | 173 | --- 174 | 175 | ### 字符编码 176 | 177 | 要读取非UTF-8编码的文本文件,可以给 `open()` 函数传入 `encoding` 参数,例如,读取GBK编码的文件: 178 | 179 | ```python 180 | >>> f = open('/Users/michael/gbk.txt', 'r', encoding='gbk') 181 | >>> f.read() 182 | '测试' 183 | ``` 184 | 185 | 遇到有些编码不规范的文件,你可能会遇到 `UnicodeDecodeError`,因为在文本文件中可能夹杂了一些非法编码的字符。遇到这种情况,`open()` 函数还接收一个 `errors` 参数,表示如果遇到编码错误后如何处理。最简单的方式是直接忽略: 186 | 187 | ```python 188 | >>> f = open('/Users/michael/gbk.txt', 'r', encoding='gbk', errors='ignore') 189 | ``` 190 | 191 | --- 192 | 193 | ### 小结 194 | 195 | 在Python中,文件读写是通过 `open()` 函数打开的文件对象完成的。使用 `with` 语句操作文件IO是个好习惯。 196 | 197 | --- 198 | 199 |
200 | 201 | ## StringIO和BytesIO 202 | 203 | ### StringIO 204 | 205 | 很多时候,数据读写不一定是对文件进行的,我们也可以在内存中进行读写操作。 206 | 207 | `StringIO` 顾名思义就是**在内存中读写 `str`**。 208 | 209 | 要把 `str` 写入 `StringIO`,我们需要先创建一个 `StringIO` 对象,然后,像文件一样写入即可: 210 | 211 | ```python 212 | >>> from io import StringIO 213 | >>> f = StringIO() 214 | >>> f.write('hello') 215 | 5 216 | >>> f.write(' ') 217 | 1 218 | >>> f.write('world!') 219 | 6 220 | >>> print(f.getvalue()) 221 | hello world! 222 | ``` 223 | 224 | `getvalue()` 方法用于**获得IO流中的全部内容**。 225 | 226 | 读取 `StringIO` 的方法也和读文件类似: 227 | 228 | ```python 229 | >>> from io import StringIO 230 | >>> f = StringIO('Hello!\nHi!\nGoodbye!') # 用一个str初始化StringIO 231 | >>> while True: 232 | ... s = f.readline() 233 | ... if s == '': # 读取完毕,跳出循环 234 | ... break 235 | ... print(s.strip()) # 去掉当前行首尾的空格再打印 236 | ... 237 | Hello! 238 | Hi! 239 | Goodbye! 240 | ``` 241 | 242 | --- 243 | 244 | ### BytesIO 245 | 246 | `StringIO` 操作的只能是 `str`,如果要操作二进制数据,就需要使用 `BytesIO`。 247 | 248 | `BytesIO` 实现了在内存中读写 `bytes`,我们创建一个 `BytesIO`,然后写入一些 `bytes`: 249 | 250 | ```python 251 | >>> from io import BytesIO 252 | >>> f = BytesIO() 253 | >>> f.write('中文'.encode('utf-8')) 254 | 6 255 | >>> print(f.getvalue()) 256 | b'\xe4\xb8\xad\xe6\x96\x87' 257 | ``` 258 | 259 | 请注意,写入的不是 `str`,而是经过UTF-8编码的 `bytes`。 260 | 261 | 和 `StringIO` 类似,读取 `BytesIO`: 262 | 263 | ```python 264 | >>> from io import BytesIO 265 | >>> f = BytesIO(b'\xe4\xb8\xad\xe6\x96\x87') # 用一个bytes初始化BytesIO 266 | >>> f.read() 267 | b'\xe4\xb8\xad\xe6\x96\x87' 268 | ``` 269 | 270 | --- 271 | 272 | ### 为什么使用StringIO和BytesIO 273 | 274 | 这个问题在Stackoverflow上有[回答](http://stackoverflow.com/questions/4733693/when-is-stringio-used),为什么我们不直接使用 `str` 和 `bytes`,而要这么别扭地在内存中使用 `StringIO` 和 `BytesIO` 呢?其实呀,主要是因为 **`StringIO` 和 `BytesIO` 都是 `file-like Object`**,可以像文件对象那样使用,当某些库的函数支持文件对象时,我们可以传入 `StringIO` 和 `BytesIO`,也能使用,这一点 `str` 和 `bytes` 是没法做到的。 275 | 276 | --- 277 | 278 | ### 读写IO需要注意的地方 279 | 280 | 以 `StringIO` 为例,前面我们将到读取 `StringIO` 时,是**先使用字符串进行初始化,然后再读取**: 281 | 282 | ```python 283 | >>> f = StringIO('Hello!\nHi!\nGoodbye!') # 用一个str初始化StringIO 284 | >>> f.readlines() 285 | ['Hello!\n', 'Hi!\n', 'Goodbye!'] 286 | ``` 287 | 288 | 但如果我们**没有进行初始化,而是对一个空的 `StringIO` 进行写入,然后再读取**呢?这时就会像下面这样: 289 | 290 | ```python 291 | >>> f = StringIO() 292 | >>> f.write('Hello!\n') 293 | 7 294 | >>> f.write('Hi!\n') 295 | 4 296 | >>> f.write('Goodbye!') 297 | 8 298 | >>> f.readlines() 299 | [] 300 | ``` 301 | 302 | 我们发现此时读取不到写入的字符串了,这是为什么呢?其实呀,这时因为**当前所处流的位置(Stream Position)在末尾**,所以读取不到东西了。那怎么知道当前处在流的什么位置呢?我们可以使用 `tell()` 方法。对比一下: 303 | 304 | 使用字符串初始化 `StringIO`: 305 | 306 | ```python 307 | >>> f = StringIO('Hello!\nHi!\nGoodbye!') 308 | >>> f.tell() 309 | 0 310 | ``` 311 | 312 | 写入 `StringIO`: 313 | 314 | ```python 315 | >>> f = StringIO() 316 | >>> f.write('Hello!\n') 317 | 7 318 | >>> f.write('Hi!\n') 319 | 4 320 | >>> f.write('Goodbye!') 321 | 8 322 | >>> f.tell() 323 | 19 324 | ``` 325 | 326 | 可以发现,使用字符串初始化时,位置会保持在流的开头,而使用 `write()` 方法对流进行写入操作后,位置会移动到**写入结束的地方**。那有没有办法在写入以后进行读取呢?有!可以使用前面提到的 `getvalue()` 方法读取IO流中的全部内容,另外也可以使用 `seek()` 方法回到前面的某一位置,然后读取该位置后的内容: 327 | 328 | ```python 329 | >>> f.tell() 330 | 19 331 | >>> f.seek(0) # 回到流的开头位置 332 | 0 333 | >>> f.tell() 334 | 0 335 | ``` 336 | 337 | 有时候呀,我们可能会在初始化一个 `StringIO` 之后,想要对其进行写入操作,这时会发生一个问题: 338 | 339 | ```python 340 | >>> f = StringIO('Hello!') 341 | >>> f.getvalue() 342 | 'Hello!' 343 | >>> f.write('Hi!') 344 | 3 345 | >>> f.getvalue() 346 | 'Hi!lo!' 347 | ``` 348 | 349 | 可以看到初始化的内容被写入的内容覆盖了,这显然不是我们所希望的。为什么会这样呢?其实呀,跟前面说的问题是一样的,举一反三,都是因为Stream Position引起的。初始化一个 `StringIO` 后,位置在流的开头,此时写入就会从流的开头写入,而不是像我们所希望的那样从流的末尾写入,稍微改动一下就好了: 350 | 351 | ```python 352 | >>> f = StringIO('Hello!') 353 | >>> f.seek(0, 2) # 移动到流的末尾 354 | 6 355 | >>> f.write('Hi!') 356 | 3 357 | >>> f.getvalue() 358 | 'Hello!Hi!' 359 | ``` 360 | 361 | 除了移动到流的末尾,也能移动到某个位置,看看 `seek()` 方法的描述: 362 | 363 | ```python 364 | Help on built-in function seek: 365 | 366 | seek(pos, whence=0, /) method of _io.StringIO instance 367 | Change stream position. 368 | 369 | Seek to character offset pos relative to position indicated by whence: 370 | 0 Start of stream (the default). pos should be >= 0; 371 | 1 Current position - pos must be 0; 372 | 2 End of stream - pos must be 0. 373 | Returns the new absolute position. 374 | ``` 375 | 376 | 可以看到 `seek()` 方法有必选参数 `pos` 和 可选参数 `whence`,前者是移动多少,后者是从哪里开始移动。`whence` 默认为0,也即默认从流的开头移动 `pos` 个位置。 377 | 378 | --- 379 | 380 | ### 小结 381 | 382 | `StringIO` 和 `BytesIO` 是在内存中操作 `str` 和 `bytes` 的方法,和读写文件具有一致的接口。 383 | 384 | --- 385 | 386 |
387 | 388 | ## 操作目录和文件 389 | 390 | ### 简述 391 | 392 | 在命令行下,我们可以通过输入操作系统提供的各种命令,比如dir、cp等,来操作目录和文件。这些命令的本质其实就是简单地调用了**操作系统提供的接口函数**。 393 | 394 | 那如果想在Python程序中操作目录和文件该怎么办呢?Python内置的 `os` 模块同样给与我们调用操作系统提供的接口函数的能力。 395 | 396 | 打开Python交互式命令行,首先看看如何使用 `os` 模块的基本功能: 397 | 398 | ```python 399 | >>> import os 400 | >>> os.name 401 | 'posix' 402 | ``` 403 | 404 | Linux、Unix和Mac OS X系统返回的是 `posix`,Windows系统返回的则是 `nt`。 405 | 406 | 要获取详细的系统信息,可以调用 `uname()` 函数: 407 | 408 | ```python 409 | >>> os.uname() 410 | posix.uname_result(sysname='Darwin', nodename='MichaelMacPro.local', release='14.3.0', version='Darwin Kernel Version 14.3.0: Mon Mar 23 11:59:05 PDT 2015; root:xnu-2782.20.48~5/RELEASE_X86_64', machine='x86_64') 411 | ``` 412 | 413 | 注意,`uname()` 函数在Windows系统上不提供,也就是说,**`os` 模块的能否使用某些函数取决于使用者的操作系统**。 414 | 415 | --- 416 | 417 | ### 环境变量 418 | 419 | 在操作系统中定义的环境变量,全部保存在 `os.environ` 变量中。我们可以直接查看操作系统的所有环境变量: 420 | 421 | ```python 422 | >>> os.environ 423 | 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', ...}) 424 | ``` 425 | 426 | 要获取某个环境变量的值,可以调用使用 `os.environ.get('key')` 的方式: 427 | 428 | ```python 429 | >>> os.environ.get('PATH') 430 | '/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin:/opt/X11/bin:/usr/local/mysql/bin' 431 | >>> os.environ.get('x', 'default') 432 | 'default' 433 | ``` 434 | 435 | 传入某个环境变量的名称,得到对应的路径。除此之外还可以传入一个字符串作为默认路径(**没有可返回的路径时**会返回默认路径)。 436 | 437 | --- 438 | 439 | ### 操作目录和文件 440 | 441 | 除了前面的 `os` 模块中,操作目录和文件的函数还有一部分放在 `os.path` 模块中。比方说用于生成绝对路径的 `abspath()` 函数: 442 | 443 | ```python 444 | >>> os.path.abspath('.') # 点符代表当前工作路径 445 | 'F:\\Python35' 446 | >>> os.path.abspath('Tools\\demos') 447 | 'F:\\Python35\\Tools\\demos' 448 | ``` 449 | 450 | 在[05模块](https://github.com/familyld/learnpython/blob/master/My_Python_Notebook/05%E6%A8%A1%E5%9D%97.md)中归纳过文件搜索路径的一些知识,当我们在程序中需要用到某个文件时,可以使用两种方式来让程序查找到这个文件: 451 | 452 | - 一是使用绝对路径,也即完整的路径; 453 | - 二是使用相对路径(相对当前工作路径而言的路径),并且可以使用点符 `.` 来替代当前工作路径。 454 | 455 | 注意,**使用相对路径时是可以不使用点符的**,所以上面代码中,为 `Tools\\demos` 生成绝对路径也同样可行。 456 | 457 | 接下来我们试试创建目录和删除目录: 458 | 459 | ```python 460 | # 在某个目录下创建一个新目录,首先生成新目录的完整路径: 461 | >>> os.path.join('/Users/michael', 'testdir') 462 | '/Users/michael/testdir' 463 | # 然后创建一个目录: 464 | >>> os.mkdir('/Users/michael/testdir') 465 | # 删掉一个目录: 466 | >>> os.rmdir('/Users/michael/testdir') 467 | ``` 468 | 469 | **把两个路径合成一个时,不要直接拼字符串**,而要通过 `os.path.join()` 函数,这样可以**正确处理不同操作系统的路径分隔符**。在Linux/Unix/Mac下,`os.path.join('part1','part2')` 返回这样的字符串: 470 | 471 | ```python 472 | part-1/part-2 473 | ``` 474 | 475 | 而Windows下会返回这样的字符串: 476 | 477 | ```python 478 | part-1\part-2 479 | ``` 480 | 481 | 同样的道理,**要拆分路径时,也不要直接去拆字符串**,而要通过 `os.path.split()` 函数,这样可以把一个路径拆分为两部分,后一部分总是**最后级别的目录或文件名**: 482 | 483 | ```python 484 | >>> os.path.split('/Users/michael/testdir/file.txt') 485 | ('/Users/michael/testdir', 'file.txt') 486 | ``` 487 | 488 | `os.path.splitext()` 函数可以用来**获取文件扩展名**,很多时候非常方便: 489 | 490 | ```python 491 | >>> os.path.splitext('/path/to/file.txt') 492 | ('/path/to/file', '.txt') 493 | ``` 494 | 495 | 这些合并、拆分路径的函数**并不要求目录和文件真实存在**,它们只是对字符串进行操作。 496 | 497 | 文件操作使用下面的函数。假定当前目录下有一个 `test.txt` 文件: 498 | 499 | ```python 500 | # 对文件重命名: 501 | >>> os.rename('test.txt', 'test.py') 502 | # 删掉文件: 503 | >>> os.remove('test.py') 504 | ``` 505 | 506 | 但是 `os` 模块中不存在复制文件的函数!原因是**复制文件并非是由操作系统提供的系统调用**。理论上讲,我们通过上一节的读写文件可以完成文件复制,只不过要多写很多代码。 507 | 508 | 幸运的是 `shutil` 模块提供了 `copyfile()` 的函数,你还可以在 `shutil` 模块中找到很多实用函数,它们可以看做是对 `os` 模块的补充。 509 | 510 | 最后看看如何利用Python的特性来过滤文件。比如我们要列出当前目录下的所有目录,只需要一行代码: 511 | 512 | ```python 513 | >>> [x for x in os.listdir('.') if os.path.isdir(x)] 514 | ['.lein', '.local', '.m2', '.npm', '.ssh', '.Trash', '.vim', 'Applications', 'Desktop', ...] 515 | ``` 516 | 517 | 要列出所有的 `.py` 文件,也只需一行代码: 518 | 519 | ```python 520 | >>> [x for x in os.listdir('.') if os.path.isfile(x) and os.path.splitext(x)[1]=='.py'] 521 | ['apis.py', 'config.py', 'models.py', 'pymonitor.py', 'test_db.py', 'urls.py', 'wsgiapp.py'] 522 | ``` 523 | 524 | 是不是非常简洁? 525 | 526 | --- 527 | 528 | ### 小结 529 | 530 | Python的 `os` 模块封装了操作系统的目录和文件操作,要注意这些函数有的在 `os` 模块中,有的在 `os.path` 模块中。 531 | 532 | ### 练习 533 | 534 | #### 习题1 535 | 536 | > 利用 `os` 模块编写一个能实现 `ls -l` 输出的程序。 537 | 538 | 先看看 `ls -l` 做的是什么: 539 | 540 | ```shell 541 | ubuntu@ubuntu:~/HumanFaceRecognitionWithNN$ ls -l 542 | total 344 543 | -rw-rw-r-- 1 ubuntu ubuntu 10301 Dec 14 22:28 face_recognition.py 544 | drwxrwxr-x 3 ubuntu ubuntu 4096 Dec 21 15:50 test 545 | -rw-rw-r-- 1 ubuntu ubuntu 328506 Dec 10 15:39 yaleB_face_dataset.mat 546 | ``` 547 | 548 | 注意这是在Linux上执行的,我们想查看当前路径下有什么文件和文件夹可以使用 `ls` 或者 `dir` 命令,而如果我们想了解更详细的信息则可以用 `ls -l` 或者`dir -l` 命令。 549 | 550 | 这里稍微解析一下返回的信息吧,以下面这一条为例: 551 | 552 | | field1 | field2 | field3 | field4 | field5 | field6 | field7 | field8 | field9 | field10 | 553 | |:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:| 554 | | - | rw- | rw- | r-- | 1 | ubuntu | ubuntu | 10301 | Dec 14 22:28 | face_recognition.py | 555 | 556 | - field1 是 `File type flag`,**标识文件类型**,如果是 `-` 则表明是普通文件,是 `d` 则表明是一个目录; 557 | - field2、field3、field4 依次是拥有者、拥有者所在的用户组以及其他用户对**该文件/文件夹的操作权限**,`r` 表示可读,`w` 表示可写, `x` 表示可执行。 558 | - field5 是**所含链接数**,如果该项是一个文件,则链接数为1;如果该项是一个目录,则一般为**该目录下子文件夹数+2**。为什么呢?因为当前目录(该项的父目录)有一条指向该项的链接,而对文件夹来说,除了父目录的链接之外,它本身还有一条 `.` 链接指向自身,并且它的子目录都有一条 `..` 链接指向它; 559 | - field6 是**拥有者的名字**; 560 | - field7 是**拥有者所在的用户组**; 561 | - field8 是**该项的大小**(多少bytes); 562 | - field9 是**最后修改该项的日期和时间**; 563 | - field10 是**该项的名字**。 564 | 565 | 我们注意到,除了每一项的详细信息之外,最前面还有一行输出 `total 344`,这个344是什么呢?它指的是当前目录所有文件和文件夹所使用的块(block)的数目,块是一个操作系统的概念,这里不详细展开。如果我们想知道当前目录下每一项所使用的块的数目,可以使用 `ls -s`命令: 566 | 567 | ```shell 568 | ubuntu@VM-173-69-ubuntu:~/HumanFaceRecognitionWithNN$ ls -s 569 | total 344 570 | 12 face_recognition.py 4 test 328 yaleB_face_dataset.mat 571 | ``` 572 | 573 | 加起来总数正是 `344`。 574 | 575 | 以上内容参考了以下几个链接: 576 | 577 | - [What is that “total” in the very first line after ls -l?](http://stackoverflow.com/questions/7401704/what-is-that-total-in-the-very-first-line-after-ls-l) 578 | - [command ls -l output explained](http://go2linux.garron.me/command-ls-file-permissions/) 579 | - [What do the fields in ls -al output mean?](http://unix.stackexchange.com/questions/103114/what-do-the-fields-in-ls-al-output-mean) 580 | - [What does each part of the `ls -la` output mean?](http://askubuntu.com/questions/710905/what-does-each-part-of-the-ls-la-output-mean) 581 | 582 | 题目要求实现Python版的 `ls -l`,理论上应该是可行的,但上面的内容只涉及到 `os` 和 `os.path` 模块中很少的函数,其他的还有待发掘。我暂时没有时间去琢磨,所以先略过这一题。 583 | 584 | #### 习题2 585 | 586 | > 编写一个程序,能在当前目录以及当前目录的所有子目录下查找文件名包含指定字符串的文件,并打印出相对路径。 587 | 588 | ```python 589 | import os 590 | 591 | def search(s, path=os.getcwd()): 592 | filelst = [x for x in os.listdir(path)] 593 | for filename in filelst: 594 | filepath = os.path.join(path, filename) 595 | # print('Searching: ', path, '\nWith: ', filepath) 596 | if os.path.isfile(filepath): 597 | if s in filename: 598 | print(os.path.relpath(filepath)) 599 | elif os.path.isdir(filepath): 600 | search(s, filepath) 601 | 602 | if __name__ == '__main__': 603 | s = input('Enter the string: ') 604 | search(s) 605 | ``` 606 | 607 | 这题还是挺有意思的,用户自己定义搜索的字符串,我们不仅要找出当前目录下包含该字符串的文件,还要搜索所有的子目录。我们可以把搜索一个目录的过程封装为 `search` 函数,并采用递归的方式来实现其子目录的搜索。思路如下: 608 | 609 | 1. 获取当前目录的所有文件&目录名,使用 `os.listdir()` 函数可以实现,把这些名称放在一个列表里保存; 610 | 611 | 2. 接下来逐个遍历并判断列表中的元素是文件还是目录,可以使用 `os.path.isfile()` 和 `os.path.isdir()` 函数; 612 | - 如果当前遍历到的元素是文件,则使用 `os.path.relpath()` 函数输出文件的相对路径; 613 | - 如果当前遍历到的元素是目录,则将该目录的路径传入 `search` 函数。 614 | 615 | 特别地,我们要注意这些函数应该输入什么和会输出什么。`os.path.relpath()` 函数接收一条完整的绝对路径,并输出**相对于当前工作路径(在命令行中执行该Python文件时所处的路径)的相对路径**,所以我们要先构造出正确的绝对路径,才能获取正确的相对路径。 616 | 617 | `os.path.isdir()` 可以接收相对路径也可以接收绝对路径,因为我们使用 `os.listdir()` 只能获得文件或目录的名称,在搜索子目录时,这些名称并不是相对于当前工作路径的相对路径,所以不能直接传入 `os.listdir()` 中,必须先构造绝对路径,然后再判断。 618 | 619 | 为什么不使用 `os.path.abspath()` 函数来生成绝对路径呢?因为**传入 `os.path.abspath()` 函数的必须是一条正确的相对路径,才会得到正确的相对路径**。举个例子,当前工作路径是 `C:\Users\Administrator\Desktop`,其子目录 `test1` 中有一个文件 `test2.py`,如果我们使用 `os.path.abspath('test2.py')`,那么得到的绝对路径就变成了 `C:\Users\Administrator\Desktop\test2.py`,显然是不对的。 620 | 621 | --- 622 | 623 |
624 | 625 | ## 序列化 626 | 627 | ### 序列化和反序列化 628 | 629 | 在程序运行的过程中,所有的变量都保存在内存中,而一旦程序结束,变量所占用的内存就会被操作系统全部回收。但是,有时候,我们希望通过程序修改了某个变量的值之后,能够让另一个程序能调用这个变量。比方说在程序1中定义了一个 `list`,并且经过某些高开销的操作修改了这个 `list` 的值。如果我们想在程序2中使用程序1中修改后的 `list`,按之前的做法就只能把程序1作为一个模块,在程序2中执行程序1的代码,这样一来,就必须重复执行高开销的操作了。有没有解决这个问题的方法呢?有的,答案就是**序列化**。 630 | 631 | 我们把**将变量从内存中保存的格式变成可存储或可传输的格式这个过程称之为序列化**,在Python中叫 `pickling`,在其他语言中也被称之为 `serialization`,`marshalling`,`flattening` 等等,都是一个意思。经过序列化之后,内存中的变量就由原来的格式(某种数据结构/类型)转换为特定的格式,从而可以被存储或传输,这样另一个程序需要用到时就可以直接读取,而不必重复计算了。 632 | 633 | 反过来,把**将变量内容从序列化的对象重新读到内存里还原为原来的格式这一过程称之为反序列化**,即 `unpickling`。 634 | 635 | --- 636 | 637 | ### 序列化(pickle) 638 | 639 | 在[01Python基础](https://github.com/familyld/learnpython/blob/master/My_Python_Notebook/01Python%E5%9F%BA%E7%A1%80.md)中,我们就知道**传输和存储都是以字节(bytes)为单位的**,所以这节首先介绍一种将变量序列化为 `bytes` 对象的方法,Python提供了 `pickle` 模块来实现这一功能。 640 | 641 | 以 `dict` 为例,将一个 `dict` 类型的对象序列化并写入文件: 642 | 643 | ```python 644 | >>> import pickle 645 | >>> d = dict(name='Bob', age=20, score=88) 646 | >>> pickle.dumps(d) 647 | 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.' 648 | ``` 649 | 650 | `pickle.dumps()` 函数可以把任意变量序列化为一个 `bytes` 对象。我们可以把这个 `bytes` 对象写入文件。此外,我们也可以用 `pickle.dump()` 函数直接把对象序列化后**写入一个 `file-like Object`**: 651 | 652 | ```python 653 | >>> f = open('dump.txt', 'wb') 654 | >>> pickle.dump(d, f) 655 | >>> f.close() 656 | ``` 657 | 658 | 打开 `dump.txt` 文件,我们会看到一堆乱七八糟无法阅读的内容,这些都是Python保存的对象信息。 659 | 660 | 当我们需要还原变量,也即把对象从磁盘读到内存时,可以先把内容读入到一个 `bytes` 对象中,然后用 `pickle.loads()` 方法反序列化出对象,也可以直接用 `pickle.load()` 方法从一个 `file-like Object` 中直接反序列化出对象。打开另一个Python命令行,试试反序列化刚才保存的对象: 661 | 662 | ```python 663 | >>> f = open('dump.txt', 'rb') 664 | >>> d = pickle.load(f) 665 | >>> f.close() 666 | >>> d 667 | {'age': 20, 'score': 88, 'name': 'Bob'} 668 | ``` 669 | 670 | 可以看到我们成功地还原了这个 `dict` 类型的对象。 671 | 672 | 有了 `Pickle` 之后,我们可以方便地在Python中进行序列化和反序列化。但是,和所有其他编程语言的序列化问题一样,`Pickle` 是一种Python特有的序列化解决方案,它只能用于Python,甚至不同版本的Python彼此都可能不兼容。如果我们使用Python写程序,而别人使用其他语言,比如Java,C++等,它们没有 `Pickle` 模块也就没办法进行反序列化了。 673 | 674 | --- 675 | 676 | ### JSON 677 | 678 | 如果我们要**在不同的编程语言之间传递对象**,就必须把对象序列化为通用的**标准格式**,比如序列化**XML(Extensible Markup Language,可扩展标记语言)**。但更好的方法是序列化为**JSON(JavaScript Object Notation,JavaScript对象表示法)**。因为JSON表示出来就是一个字符串,可以被所有语言读取,也可以方便地存储到磁盘或者通过网络传输。JSON不仅是标准格式,并且比XML更快,还可以直接在Web页面中读取,非常方便。 679 | 680 | 比较一下Python内置的数据类型和JSON中的表示方式: 681 | 682 | | Python类型 | JSON表示 | 683 | |:-:|:-:| 684 | | dict | {} | 685 | | list | [] | 686 | | str | "string" | 687 | | int, float | 10, 1234.56 | 688 | | True/False | true/false | 689 | | None | null | 690 | 691 | Python内置的 `json` 模块提供了非常完善的Python对象到JSON格式的转换方法。同样对一个 `dict` 进行序列化,方法如下: 692 | 693 | ```python 694 | >>> import json 695 | >>> d = dict(name='Bob', age=20, score=88) 696 | >>> json.dumps(d) 697 | '{"age": 20, "score": 88, "name": "Bob"}' 698 | ``` 699 | 700 | `dumps()` 方法**返回一个 `str`**,内容就是标准的JSON。类似的,`dump()` 方法可以直接把JSON写入一个 `file-like Object`。 701 | 702 | 要把JSON反序列化为Python对象,用 `loads()` 或者对应的 `load()`方法,前者把JSON的字符串反序列化,后者从 `file-like Object` 中读取字符串并反序列化: 703 | 704 | ```python 705 | >>> json_str = '{"age": 20, "score": 88, "name": "Bob"}' 706 | >>> json.loads(json_str) 707 | {'age': 20, 'score': 88, 'name': 'Bob'} 708 | ``` 709 | 710 | 由于JSON标准规定**JSON编码是UTF-8**,所以我们总是能正确地在Python的 `str` 与JSON之间的转换。 711 | 712 | --- 713 | 714 | ### JSON进阶 715 | 716 | Python的 `dict` 对象可以直接序列化为JSON的 `{}`,不过很多时候,Python自带的数据结构并不足以实现我们的需求,此时我们会**使用自定义的类来表示对象**。比如定义一个Student类,并尝试序列化该类的实例: 717 | 718 | ```python 719 | import json 720 | 721 | class Student(object): 722 | def __init__(self, name, age, score): 723 | self.name = name 724 | self.age = age 725 | self.score = score 726 | 727 | s = Student('Bob', 20, 88) 728 | print(json.dumps(s)) 729 | ``` 730 | 731 | 运行代码,毫不留情地得到一个 `TypeError`: 732 | 733 | ```python 734 | Traceback (most recent call last): 735 | ... 736 | TypeError: <__main__.Student object at 0x10603cc50> is not JSON serializable 737 | ``` 738 | 739 | 错误的原因是Student对象**不是一个可序列化为JSON的对象**。 740 | 741 | 这样看来还是不够实用呀,别急,我们再仔细看看 `dumps()` 方法的参数列表,可以发现,除了第一个必须的 `obj` 参数外,`dumps()` 方法还提供了一大堆的可选参数: 742 | 743 | ```python 744 | >>> help(json.dumps) 745 | Help on function dumps in module json: 746 | 747 | dumps(obj, skipkeys=False, ensure_ascii=True, check_circular=True, allow_nan=True, cls=None, 748 | indent=None, separators=None, default=None, sort_keys=False, **kw) 749 | 750 | ... 751 | ``` 752 | 753 | 这些可选参数就是允许我们对JSON序列化进行定制的。前面的代码之所以无法把Student类实例序列化为JSON,是因为默认情况下,`dumps()` 方法不知道如何将Student实例变为一个JSON的 `{}` 对象。 754 | 755 | 可选参数 `default` 允许我们传入一个可以把传入对象变得可序列化的函数,我们只需要为Student专门写一个转换函数,再把函数传进去即可,例如定义: 756 | 757 | ```python 758 | def student2dict(std): 759 | return { 760 | 'name': std.name, 761 | 'age': std.age, 762 | 'score': std.score 763 | } 764 | ``` 765 | 766 | 这样,Student实例首先被 `student2dict()` 函数转换成 `dict`,然后再被序列化为JSON: 767 | 768 | ```python 769 | >>> print(json.dumps(s, default=student2dict)) 770 | {"age": 20, "name": "Bob", "score": 88} 771 | ``` 772 | 773 | 不过,下次如果遇到一个Teacher类的实例,我们还是无法把Teacher类的实例序列化为JSON。有没有更方便的做法呢?有的,我们可以偷个懒,利用 `__dict__` 属性即可: 774 | 775 | ```python 776 | print(json.dumps(s, default=lambda obj: obj.__dict__)) 777 | ``` 778 | 779 | 通常类的实例都有一个 `__dict__` 属性,它就是一个 `dict`。但也有少数例外,比如定义了 `__slots__` 的类(这样的类没有 `__dict__` 属性)。 780 | 781 | 同样的道理,如果我们要把JSON反序列化为一个Student对象实例,`loads()`方法会首先转换出一个 `dict` 对象,然后,参数 `object_hook` 则允许我们传入一个函数,负责把 `dict` 转换为Student实例: 782 | 783 | ```python 784 | def dict2student(d): 785 | return Student(d['name'], d['age'], d['score']) 786 | ``` 787 | 788 | 运行结果如下: 789 | 790 | ```python 791 | >>> json_str = '{"age": 20, "score": 88, "name": "Bob"}' 792 | >>> print(json.loads(json_str, object_hook=dict2student)) 793 | <__main__.Student object at 0x10cd3c190> 794 | ``` 795 | 796 | 打印出的是反序列化后的Student实例对象。 797 | 798 | --- 799 | 800 | ### 小结 801 | 802 | Python语言特定的序列化模块是 `pickle`,但如果想把序列化做得更通用、更符合Web标准,可以使用 `json` 模块。 803 | 804 | `json` 模块的 `dumps()` 和 `loads()` 函数是定义得非常好的接口的典范。当我们使用时,只需要传入一个必须的参数。但是,当默认的序列化或反序列机制不满足我们的要求时,我们又可以传入更多的参数来定制序列化或反序列化的规则,既做到了接口简单易用,又做到了充分的扩展性和灵活性。 805 | -------------------------------------------------------------------------------- /My_Python_Notebook/10进程和线程.md: -------------------------------------------------------------------------------- 1 | # 进程和线程 2 | 3 | ## 目录 4 | 5 | 6 | 7 | - [简介](#简介) 8 | - [多任务](#多任务) 9 | - [进程和线程的含义](#进程和线程的含义) 10 | - [小结](#小结) 11 | - [多进程](#多进程) 12 | - [fork函数](#fork函数) 13 | - [multiprocessing](#multiprocessing) 14 | - [进程池](#进程池) 15 | - [子进程的输入和输出](#子进程的输入和输出) 16 | - [进程间通信](#进程间通信) 17 | - [小结](#小结-1) 18 | - [多线程](#多线程) 19 | - [多线程简单实现](#多线程简单实现) 20 | - [线程锁](#线程锁) 21 | - [死循环与多核CPU](#死循环与多核cpu) 22 | - [小结](#小结-2) 23 | - [ThreadLocal](#threadlocal) 24 | - [局部变量的传递](#局部变量的传递) 25 | - [ThreadLocal的用法](#threadlocal的用法) 26 | - [小结](#小结-3) 27 | - [进程 vs. 线程](#进程-vs-线程) 28 | - [比较多进程和多线程](#比较多进程和多线程) 29 | - [效率问题](#效率问题) 30 | - [计算密集型 vs. IO密集型](#计算密集型-vs-io密集型) 31 | - [异步IO](#异步io) 32 | - [分布式进程](#分布式进程) 33 | - [简介](#简介-1) 34 | - [实现方法](#实现方法) 35 | - [小结](#小结-4) 36 | 37 | 38 | 39 | 40 | ## 简介 41 | 42 | ### 多任务 43 | 44 | 现代操作系统包括Mac OS X,UNIX,Linux,Windows等,它们都是支持“多任务”的操作系统。 45 | 46 | 什么叫“多任务”呢?简单地说,就是操作系统可以同时运行多个任务。打个比方,你一边在用浏览器上网,一边在听MP3,一边在用Word赶作业,这就是多任务,至少同时有3个任务正在运行。还有很多任务悄悄地在后台同时运行着,只是桌面上没有显示而已。 47 | 48 | 现在多核CPU已经非常普及了,使用不同的核执行不同任务自然也不是什么难事。但是,即使是过去的单核CPU,也可以执行多任务。我们知道,CPU执行代码都是顺序执行的,那么**单核CPU是怎么执行多任务的呢**? 49 | 50 | 答案就是**操作系统轮流让各个任务交替执行**,任务1执行0.01秒,切换到任务2,任务2执行0.01秒,再切换到任务3,执行0.01秒…… 这样反复执行下去。表面上每个任务都是交替执行的,但是,由于CPU的执行速度实在是太快了,我们实际感觉到的就变成了所有任务都在同时执行。 51 | 52 | 真正的并行执行多任务只能在多核CPU上实现,但是,由于**任务数量往往是远多于CPU的核心数量的**,要给每个任务都分配一个CPU内核是不现实的,所以实际上操作系统会自动把很多任务轮流调度到每个核心上执行。 53 | 54 | --- 55 | 56 | ### 进程和线程的含义 57 | 58 | 对于操作系统来说,一个任务就是一个**进程(Process)**,比如打开一个浏览器就是启动一个浏览器进程,打开一个记事本就启动了一个记事本进程,打开两个记事本就启动了两个记事本进程,打开一个Word就启动了一个Word进程。 59 | 60 | 有些进程还不止同时干一件事,比如Word,它可以同时进行打字、拼写检查、打印等事情。在一个进程内部,要同时干多件事,就需要同时运行多个“子任务”,我们把**进程内的“子任务”称为线程(Thread)**。 61 | 62 | 由于每个进程至少要干一件事,所以,一个进程至少有一个线程。当然,像Word这种复杂的进程可以有多个线程,**多个线程可以同时执行**,多线程的执行方式和多进程是一样的,也是由操作系统在多个线程之间快速切换,让每个线程都短暂地交替运行,看起来就像同时执行一样。当然,真正同时执行多线程需要多核CPU才可能实现。 63 | 64 | 我们前面编写的所有的Python程序,都是执行单任务的进程,并且只有一个线程。如果我们要同时执行多个任务怎么办?有三种解决方案: 65 | 66 | - **多进程模式**:启动多个进程,每个进程只有一个线程,但多个进程可以一块执行多个任务; 67 | - **多线程模式**:启动一个进程,在一个进程内启动多个线程,多个线程可以一块执行多个任务; 68 | - **多进程+多线程模式**:启动多个进程,每个进程再启动多个线程,这样同时执行的任务就更多了,当然这种模型更复杂,实际很少采用。 69 | 70 | 注意,**同时执行的多个任务之间可能是关联的,需要相互通信和协调**。比方说,任务1必须先暂停等待任务2完成后才能继续执行,而任务3和任务4需要操作同一个文件所以不能同时执行。因此,多进程和多线程的程序的复杂度要远远高于我们前面写的单进程单线程的程序。 71 | 72 | 因为复杂度高,调试困难,所以,不是迫不得已,我们也不想编写多任务。但是,有很多时候,没有多任务还真不行。想想在电脑上看电影,就必须由一个线程播放视频,另一个线程播放音频,否则,单线程实现的话就只能先把视频播放完再播放音频,或者先把音频播放完再播放视频,这显然是不行的。 73 | 74 | Python既支持多进程,又支持多线程,在接下来的这一章,我们会讨论如何编写这两种多任务程序。 75 | 76 | --- 77 | 78 | ### 小结 79 | 80 | **线程是最小的执行单元,进程由至少一个线程组成**。如何调度进程和线程,完全由操作系统决定,程序自己不能决定自己什么时候被执行,执行多长时间。 81 | 82 | 多进程和多线程的程序涉及到同步、数据共享等问题,编写起来更复杂。 83 | 84 | --- 85 | 86 |
87 | 88 | ## 多进程 89 | 90 | ### fork函数 91 | 92 | 要让Python程序实现**多进程(multiprocessing)**,我们先得了解操作系统的相关知识。 93 | 94 | Unix/Linux操作系统提供了一个 `fork()` 系统调用函数,它非常特殊。普通的函数在被调用时,调用一次只会返回一次。但是 `fork()` 函数调用一次会返回两次,因为此时操作系统会自动把当前进程(称为父进程)复制一份(称为子进程),然后分别在父进程和子进程内进行返回。 95 | 96 | **子进程永远返回0,而父进程返回子进程的ID**。这样做的理由是,一个父进程可以fork出很多子进程,所以,父进程要记下每个子进程的ID,而子进程只需要调用 `getppid()` 就可以拿到父进程的ID。 97 | 98 | Python的 `os` 模块封装了常见的系统调用,其中就包括fork,可以在Python程序中轻松创建子进程: 99 | 100 | ```python 101 | import os 102 | 103 | print('Process (%s) start...' % os.getpid()) 104 | # Only works on Unix/Linux/Mac: 105 | pid = os.fork() 106 | if pid == 0: 107 | print('I am child process (%s) and my parent is %s.' % (os.getpid(), os.getppid())) 108 | else: 109 | print('I (%s) just created a child process (%s).' % (os.getpid(), pid)) 110 | ``` 111 | 112 | 运行结果如下: 113 | 114 | ```python 115 | Process (876) start... 116 | I (876) just created a child process (877). 117 | I am child process (877) and my parent is 876. 118 | ``` 119 | 120 | 这段代码在执行第一个print时只有一个进程(876),因此只打印一次。然后在执行 `fork()` 之后,当前进程(876)被复制出一个子进程(877)。当前进程会先返回,返回子进程ID(877),然后往下走进入if-else代码块,进入else语句进行打印;其后子进程(877)返回0,同样往下走,进入if-else代码块,进入if语句进行打印。 121 | 122 | 由于Windows没有fork调用,上面的代码在Windows上无法运行。而Mac系统是基于BSD(Unix的一种)内核,所以,在Mac下运行是没有问题的~ 123 | 124 | 有了fork调用,一个进程在接到新任务时就可以复制出一个子进程来处理新任务,常见的Apache服务器就是由父进程监听端口,每当有新的http请求时,就fork出子进程来处理新的http请求。 125 | 126 | --- 127 | 128 | ### multiprocessing 129 | 130 | 如果你打算编写多进程的服务程序,Unix/Linux无疑是正确的选择。由于Windows没有fork调用,难道在Windows上无法用Python编写多进程的程序? 131 | 132 | 由于Python是跨平台的,自然也应该提供跨平台的多进程支持。`multiprocessing` 模块就是跨平台版本的多进程模块。 133 | 134 | `multiprocessing` 模块提供了一个 `Process` 类来代表一个进程对象,下面的例子演示了启动一个子进程并等待其结束: 135 | 136 | ```python 137 | from multiprocessing import Process 138 | import os 139 | 140 | # 子进程要执行的代码 141 | def run_proc(name): 142 | print('Run child process %s (%s)...' % (name, os.getpid())) 143 | 144 | if __name__=='__main__': 145 | print('Parent process %s.' % os.getpid()) 146 | p = Process(target=run_proc, args=('test',)) 147 | print('Child process will start.') 148 | p.start() 149 | p.join() 150 | print('Child process end.') 151 | ``` 152 | 153 | 执行结果如下: 154 | 155 | ```python 156 | Parent process 928. 157 | Process will start. 158 | Run child process test (929)... 159 | Process end. 160 | ``` 161 | 162 | 创建子进程时,只需要传入一个执行函数和该函数的参数就可以创建一个 `Process` 实例。用 `start()` 方法就可以启动这个子进程,这样创建进程要比 `fork()` 更简单和灵活。 163 | 164 | `join()` 方法可以**等待子进程结束后再继续往下运行**,通常用于进程间的同步。 165 | 166 | --- 167 | 168 | ### 进程池 169 | 170 | 如果要启动大量的子进程,我们可以用**进程池**的方式来**批量创建子进程**: 171 | 172 | ```python 173 | from multiprocessing import Pool 174 | import os, time, random 175 | 176 | # 子进程执行的任务 177 | def long_time_task(name): 178 | print('Run task %s (%s)...' % (name, os.getpid())) 179 | start = time.time() 180 | time.sleep(random.random() * 3) 181 | end = time.time() 182 | print('Task %s runs %0.2f seconds.' % (name, (end - start))) 183 | 184 | if __name__=='__main__': 185 | print('Parent process %s.' % os.getpid()) 186 | p = Pool(4) # 创建一个大小为4的进程池 187 | for i in range(5): # 依次创建5个子进程 188 | p.apply_async(long_time_task, args=(i,)) 189 | print('Waiting for all subprocesses done...') 190 | p.close() 191 | p.join() 192 | print('All subprocesses done.') 193 | ``` 194 | 195 | 执行结果如下: 196 | 197 | ```python 198 | Parent process 669. 199 | Waiting for all subprocesses done... 200 | Run task 0 (671)... 201 | Run task 1 (672)... 202 | Run task 2 (673)... 203 | Run task 3 (674)... 204 | Task 2 runs 0.14 seconds. 205 | Run task 4 (673)... 206 | Task 1 runs 0.27 seconds. 207 | Task 3 runs 0.86 seconds. 208 | Task 0 runs 1.41 seconds. 209 | Task 4 runs 1.91 seconds. 210 | All subprocesses done. 211 | ``` 212 | 213 | 代码解读: 214 | 215 | 对 `Pool` 对象调用 `join()` 方法会等待所有子进程执行完毕,**调用 `join()` 之前必须先调用 `close()`,调用 `close()` 之后就不能往进程池中继续添加新的 `Process` 了**。 216 | 217 | 请注意输出的结果,task 0,1,2,3这四个子进程是立刻执行的,而task 4要等待前面某个task完成后才执行,这是因为我们定义进程池时定义了大小为4,也即最多同时执行4个进程,所以第5个进程就要等进程池里有空位了才能开始。如果改成: 218 | 219 | ```python 220 | p = Pool(5) 221 | ``` 222 | 223 | 就可以同时跑5个进程。 224 | 225 | 当没有传入参数时,**Pool的默认大小是CPU的核数**,所以如果电脑是4核CPU则默认进程池大小为4,如果电脑是8核CPU则默认进程池大小为8。 226 | 227 | --- 228 | 229 | ### 子进程的输入和输出 230 | 231 | 很多时候,子进程和父进程要执行的不是同一个任务。我们创建了子进程后,还需要控制子进程的输入和输出。 232 | 233 | `subprocess` 模块可以让我们非常方便地启动一个子进程,并且控制其输入和输出。 234 | 235 | 下面的例子演示了如何在Python代码中运行命令 `nslookup www.python.org`,这和命令行直接运行的效果是一样的: 236 | 237 | ```python 238 | import subprocess 239 | 240 | print('$ nslookup www.python.org') 241 | r = subprocess.call(['nslookup', 'www.python.org']) 242 | print('Exit code:', r) 243 | ``` 244 | 245 | 运行结果: 246 | 247 | ```python 248 | $ nslookup www.python.org 249 | Server: 192.168.19.4 250 | Address: 192.168.19.4#53 251 | 252 | Non-authoritative answer: 253 | www.python.org canonical name = python.map.fastly.net. 254 | Name: python.map.fastly.net 255 | Address: 199.27.79.223 256 | 257 | Exit code: 0 258 | ``` 259 | 260 | 看看帮助文档中 `subprocess` 模块的 `call()` 函数的描述: 261 | 262 | > Run command with arguments. Wait for command to complete or timeout, then return the returncode attribute. 263 | 264 | 它可以帮助我们建立一个子进程来执行系统调用函数,并且允许传入函数,调用后会等待运行结束并最终返回调用函数的返回值,之后才继续执行后续代码。可以只传入一个列表,列表第一个元素为系统调用的名称,第二个元素为参数。 265 | 266 | 如果子进程**执行的过程中**还需要其他输入,我们使用 `subprocess` 模块的 `Popen` 类初始化子进程,并通过它的 `communicate()` 方法来实现进程执行过程中的多次输入: 267 | 268 | ```python 269 | import subprocess 270 | 271 | print('$ nslookup') 272 | p = subprocess.Popen(['nslookup'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 273 | output, err = p.communicate(b'set q=mx\npython.org\nexit\n') # 每次输入之间用换行符隔开 274 | print(output.decode('utf-8')) # 返回的是字节流,所以要先解码 275 | print('Exit code:', p.returncode) 276 | ``` 277 | 278 | 上面的代码相当于在命令行执行命令 `nslookup`,然后手动输入: 279 | 280 | ```python 281 | set q=mx 282 | python.org 283 | exit 284 | ``` 285 | 286 | 运行结果如下: 287 | 288 | ```python 289 | $ nslookup 290 | Server: 192.168.19.4 291 | Address: 192.168.19.4#53 292 | 293 | Non-authoritative answer: 294 | python.org mail exchanger = 50 mail.python.org. 295 | 296 | Authoritative answers can be found from: 297 | mail.python.org internet address = 82.94.164.166 298 | mail.python.org has AAAA address 2001:888:2000:d::a6 299 | 300 | 301 | Exit code: 0 302 | ``` 303 | 304 | --- 305 | 306 | ### 进程间通信 307 | 308 | Process之间肯定是需要通信的,操作系统提供了很多机制来实现进程间的通信。Python的 `multiprocessing` 模块包装了底层的机制,提供了 `Queue`、`Pipes` 等多种方式来交换数据。 309 | 310 | 我们以 `Queue` 为例,在父进程中创建两个子进程,第一个子进程往 `Queue` 里写数据,第二个子进程从 `Queue` 里读数据: 311 | 312 | ```python 313 | from multiprocessing import Process, Queue 314 | import os, time, random 315 | 316 | # 写数据进程执行的代码: 317 | def write(q): 318 | print('Process to write: %s' % os.getpid()) 319 | for value in ['A', 'B', 'C']: 320 | print('Put %s to queue...' % value) 321 | q.put(value) 322 | time.sleep(random.random()) 323 | 324 | # 读数据进程执行的代码: 325 | def read(q): 326 | print('Process to read: %s' % os.getpid()) 327 | while True: 328 | value = q.get(True) 329 | print('Get %s from queue.' % value) 330 | 331 | if __name__=='__main__': 332 | # 父进程创建Queue,并传给各个子进程: 333 | q = Queue() 334 | pw = Process(target=write, args=(q,)) 335 | pr = Process(target=read, args=(q,)) 336 | # 启动子进程pw,写入: 337 | pw.start() 338 | # 启动子进程pr,读取: 339 | pr.start() 340 | # 等待pw结束: 341 | pw.join() 342 | # pr进程里是死循环,无法等待其结束,只能强行终止: 343 | pr.terminate() 344 | ``` 345 | 346 | 运行结果如下: 347 | 348 | ```python 349 | Process to write: 50563 350 | Put A to queue... 351 | Process to read: 50564 352 | Get A from queue. 353 | Put B to queue... 354 | Get B from queue. 355 | Put C to queue... 356 | Get C from queue. 357 | ``` 358 | 359 | 在Unix/Linux下,`multiprocessing` 模块封装了 `fork()` 调用,使我们不需要关注 `fork()` 的细节。由于Windows没有fork调用,因此,`multiprocessing` 需要“模拟”出fork的效果,**父进程的所有Python对象都必须先通过 `pickle` 序列化再传到子进程去**。所以,如果 `multiprocessing` 在Windows下调用失败了,就要先考虑是不是序列化失败了。 360 | 361 | --- 362 | 363 | ### 小结 364 | 365 | 在Unix/Linux下,可以使用 `fork()` 调用实现多进程。 366 | 367 | 要实现跨平台的多进程,可以使用 `multiprocessing` 模块。 368 | 369 | 进程间通信可以通过 `Queue`、`Pipes` 等实现的。 370 | 371 | --- 372 | 373 |
374 | 375 | ## 多线程 376 | 377 | ### 多线程简单实现 378 | 379 | 多任务可以由多进程完成,也可以由一个进程内的多线程完成。 380 | 381 | 我们前面提到了进程是由若干线程组成的,一个进程至少有一个线程。 382 | 383 | 由于线程是操作系统直接支持的执行单元,因此,高级语言通常都内置多线程的支持,Python也不例外,并且,Python的线程是真正的Posix Thread,而不是模拟出来的线程。 384 | 385 | Python的标准库提供了两个模块:`_thread` 和 `threading`,`_thread` 是低级模块,threading是高级模块,对 `_thread` 进行了封装。绝大多数情况下,我们只需要使用 `threading` 这个高级模块。 386 | 387 | 使用 `threading` 时,我们可以创建 `Thread` 实例来建立新的线程,然后调用 `start()` 启动该线程: 388 | 389 | ```python 390 | import time, threading 391 | 392 | # 新线程执行的代码: 393 | def loop(): 394 | print('thread %s is running...' % threading.current_thread().name) 395 | n = 0 396 | while n < 5: 397 | n = n + 1 398 | print('thread %s >>> %s' % (threading.current_thread().name, n)) 399 | time.sleep(1) 400 | print('thread %s ended.' % threading.current_thread().name) 401 | 402 | print('thread %s is running...' % threading.current_thread().name) 403 | t = threading.Thread(target=loop, name='LoopThread') 404 | t.start() 405 | t.join() 406 | print('thread %s ended.' % threading.current_thread().name) 407 | ``` 408 | 409 | 执行结果如下: 410 | 411 | ```python 412 | thread MainThread is running... 413 | thread LoopThread is running... 414 | thread LoopThread >>> 1 415 | thread LoopThread >>> 2 416 | thread LoopThread >>> 3 417 | thread LoopThread >>> 4 418 | thread LoopThread >>> 5 419 | thread LoopThread ended. 420 | thread MainThread ended. 421 | ``` 422 | 423 | 由于任何进程默认就会启动一个线程,我们把该线程称为主线程,主线程又可以启动新的线程,Python的 `threading` 模块有个 `current_thread()` 函数,它永远返回当前线程的实例。**主线程实例的名字叫 `MainThread`,子线程的名字在创建时指定**,我们用 `LoopThread` 命名子线程。名字仅仅在打印时用来显示,没有其他意义,如果不起名字Python就会自动给线程命名为 `Thread-1`,`Thread-2`…… 424 | 425 | --- 426 | 427 | ### 线程锁 428 | 429 | 多线程和多进程最大的不同在于,多进程中,同一个变量,各自有一份拷贝存在于每个进程中,互不影响。而**多线程中,所有变量都由所有线程共享**,所以,任何一个变量都可以被任何一个线程修改,因此,线程之间共享数据最大的危险在于多个线程**同时改一个变量**,把内容给改乱了。 430 | 431 | 来看看多个线程同时操作一个变量怎么把内容给改乱了: 432 | 433 | ```python 434 | import time, threading 435 | 436 | # 假定这是你的银行存款: 437 | balance = 0 438 | 439 | def change_it(n): 440 | # 先加后减,结果应该为0: 441 | global balance 442 | balance = balance + n 443 | balance = balance - n 444 | 445 | def run_thread(n): 446 | for i in range(100000): 447 | change_it(n) 448 | 449 | t1 = threading.Thread(target=run_thread, args=(5,)) 450 | t2 = threading.Thread(target=run_thread, args=(8,)) 451 | t1.start() 452 | t2.start() 453 | t1.join() 454 | t2.join() 455 | print(balance) 456 | ``` 457 | 458 | 注意,`change_it()` 函数中,使用了global关键字来声明使用的 `balance` 变量是在函数定义外部的,这样在函数内部的修改也会反映到函数外部。 459 | 460 | `balance`,初始值为0,并且启动两个线程,先加后减,理论上结果应该为0,但是,由于线程的调度是由操作系统决定的,当t1、t2交替执行时,只要循环次数足够多,在某个时刻,线程t1和t2同时对 `balance` 变量进行修改,那么最终结果就不一定是0了。比方说: 461 | 462 | | 时间 | 线程 | balance | 操作 | 463 | |:-:|:-:|:-:|:-:| 464 | | 00 | t1 | 0 | +5 | 465 | | 01 | t1 | 5 | -5 | 466 | | 02 | t2 | 0 | +8 | 467 | | 03 | t2 | 8 | -8 | 468 | | 04 | t1 | 0 | +5 | 469 | | 05 | t2 | 5 | +8 | 470 | | 06 | t1 | 13 | -8 | 471 | | 07 | t2 | 5 | -5 | 472 | | 08 | t1,t2 | 0 | +5,+8 | 473 | | 09 | t1 | 8 | -5 | 474 | | 10 | t2 | 3 | -8 | 475 | | 11 | t1 | -5 | +5 | 476 | | 12 | t2 | 0 | +8 | 477 | | 13 | t1 | 8 | -5 | 478 | | 14 | t2 | 3 | -8 | 479 | 480 | 假设在08时刻,t1和t2同时执行 `balance = balance + n` 这个语句,右边的 `balance` 都取0,那么t1执行完该语句时 `balance` 为5,而t2执行完时 `balance` 为8,假设t2后执行完毕,那么 `balance` 就取8,这时就产生错误了,并且错误会继续累积,当循环次数达到一定规模时,这种错误的累积会非常可怕。 481 | 482 | 为什么会产生这样的错误呢?其实 呀,这时因为高级语言的一条语句在CPU执行时需要转换为多条汇编指令,即使一个简单的计算: 483 | 484 | ```python 485 | balance = balance + n 486 | ``` 487 | 488 | 也需要分为两步: 489 | 490 | 1. 计算 `balance + n`,存入临时变量中; 491 | 2. 将临时变量的值赋给 `balance`。 492 | 493 | 可以看成: 494 | 495 | ```python 496 | x = balance + n 497 | balance = x 498 | ``` 499 | 500 | 临时变量 `x` 是一个**局部变量**,两个线程各自都有自己的 `x`,所以当代码正常执行时: 501 | 502 | 初始值 `balance = 0` 503 | 504 | ```python 505 | t1: x1 = balance + 5 # x1 = 0 + 5 = 5 506 | t1: balance = x1 # balance = 5 507 | t1: x1 = balance - 5 # x1 = 5 - 5 = 0 508 | t1: balance = x1 # balance = 0 509 | 510 | t2: x2 = balance + 8 # x2 = 0 + 8 = 8 511 | t2: balance = x2 # balance = 8 512 | t2: x2 = balance - 8 # x2 = 8 - 8 = 0 513 | t2: balance = x2 # balance = 0 514 | ``` 515 | 516 | 结果 `balance = 0` 517 | 518 | 但是t1和t2是交替运行的,如果操作系统以下面的顺序执行t1、t2: 519 | 520 | 初始值 `balance = 0` 521 | 522 | ```python 523 | t1: x1 = balance + 5 # x1 = 0 + 5 = 5 524 | 525 | t2: x2 = balance + 8 # x2 = 0 + 8 = 8 526 | t2: balance = x2 # balance = 8 527 | 528 | t1: balance = x1 # balance = 5 529 | t1: x1 = balance - 5 # x1 = 5 - 5 = 0 530 | t1: balance = x1 # balance = 0 531 | 532 | t2: x2 = balance - 8 # x2 = 0 - 8 = -8 533 | t2: balance = x2 # balance = -8 534 | ``` 535 | 536 | 结果 `balance = -8`,自然就不对了 537 | 538 | 究其原因,是因为修改 `balance` 需要多条语句,而执行这几条语句时,线程可能中断,其他线程可能也会对 `balance` 进行了修改,从而导致多个线程把同一个对象的内容改乱了。所以,我们必须确保一个线程在修改 `balance` 的时候,别的线程一定不能改。 539 | 540 | 如果我们要确保 `balance` 计算正确,就要给 `change_it()` 上一把锁,当某个线程开始执行 `change_it()` 时,我们说,该线程获得了锁,因此其他线程不能同时执行 `change_it()`,只能等待锁被释放,线程获得该锁以后才能改。由于锁只有一个,无论多少线程,同一时刻最多只有一个线程持有该锁,所以,不会造成修改的冲突。创建一个锁可以通过 `threading.Lock()` 来实现: 541 | 542 | ```python 543 | balance = 0 544 | lock = threading.Lock() 545 | 546 | def run_thread(n): 547 | for i in range(100000): 548 | # 先要获取锁: 549 | lock.acquire() 550 | try: 551 | # 放心地改吧: 552 | change_it(n) 553 | finally: 554 | # 改完了一定要释放锁: 555 | lock.release() 556 | ``` 557 | 558 | 当多个线程同时执行 `lock.acquire()` 时,只有一个线程能成功地获取锁,然后继续执行代码,其他线程就继续等待直到获得锁为止。 559 | 560 | **获得锁的线程用完后一定要释放锁**,否则那些苦苦等待锁的线程将永远等待下去,成为死线程。所以我们**用 `try...finally` 来确保锁一定会被释放**。 561 | 562 | 锁的好处就是确保了某段关键代码只能由一个线程从头到尾完整地执行,坏处当然也很多,首先是阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了。其次,由于可以构造多个不同的锁,**不同的线程持有不同的锁,在试图获取对方持有的锁时,可能会造成死锁,导致多个线程全部挂起,既不能执行,也无法结束,只能靠操作系统强制终止**。 563 | 564 | --- 565 | 566 | ### 死循环与多核CPU 567 | 568 | 如果你拥有一个多核CPU,肯定就会想到不同的核应该可以同时执行不同的多个线程。 569 | 570 | 如果写一个死循环的话,会出现什么情况呢? 571 | 572 | 打开Mac OS X的Activity Monitor,或者Windows的Task Manager,都可以监控某个进程的CPU使用率。我们可以监控到一个死循环线程会100%占用一个CPU。如果有两个死循环线程,在多核CPU中,可以监控到会占用200%的CPU,也就是占用两个CPU核心。要想把N核CPU的核心全部跑满,就必须启动N个死循环线程。 573 | 574 | 试试用Python写个死循环: 575 | 576 | ```python 577 | import threading, multiprocessing 578 | 579 | def loop(): 580 | x = 0 581 | while True: 582 | x = x ^ 1 583 | 584 | for i in range(multiprocessing.cpu_count()): 585 | t = threading.Thread(target=loop) 586 | t.start() 587 | ``` 588 | 589 | 启动与CPU核心数量相同的N个线程,可以监控到CPU占用率仅有102%,也就是仅使用了一核。但是用C、C++或Java来改写相同的死循环,直接可以把全部核心跑满,4核就跑到400%,8核就跑到800%,为什么Python不行呢? 590 | 591 | 因为Python的线程虽然是真正的线程,但解释器执行代码时,有一个**GIL锁:Global Interpreter Lock,任何Python线程执行前,必须先获得GIL锁**,然后,每执行100条字节码,解释器就自动释放GIL锁,让别的线程有机会执行。这个GIL全局锁实际上把所有线程的执行代码都给上了锁,所以,**多线程在Python中只能交替执行**,即使100个线程跑在100核CPU上,也只能用到1个核。 592 | 593 | GIL是Python解释器设计的历史遗留问题,通常我们用的解释器是官方实现的CPython,要真正利用多核,除非重写一个不带GIL的解释器。 594 | 595 | 所以,在Python中,可以使用多线程,但不要指望能有效利用多核。如果一定要通过多线程利用多核,那只能通过C扩展来实现,不过这样就失去了Python简单易用的特点。 596 | 597 | 不过,也不用过于担心,Python虽然不能利用多线程实现多核任务,但**可以通过多进程实现多核任务。多个Python进程有各自独立的GIL锁,互不影响**。 598 | 599 | --- 600 | 601 | ### 小结 602 | 603 | 多线程编程,模型复杂,容易发生冲突,必须用锁加以隔离,同时,又要小心死锁的发生。 604 | 605 | Python解释器由于设计时有GIL全局锁,导致了多线程无法利用多核。多线程并发在Python中就是一个美丽的梦。 606 | 607 | --- 608 | 609 |
610 | 611 | ## ThreadLocal 612 | 613 | ### 局部变量的传递 614 | 615 | 在多线程环境下,每个线程都有自己的数据。一个线程使用自己的局部变量比使用全局变量好,因为局部变量只有线程自己能看见,不会影响其他线程,而全局变量的修改必须加锁。 616 | 617 | 但是局部变量也有问题,就是在函数调用的时候,传递起来很麻烦: 618 | 619 | ```python 620 | def process_student(name): 621 | std = Student(name) 622 | # std是局部变量,但是每个函数都要用它,因此必须传进去: 623 | do_task_1(std) 624 | do_task_2(std) 625 | 626 | def do_task_1(std): 627 | do_subtask_1(std) 628 | do_subtask_2(std) 629 | 630 | def do_task_2(std): 631 | do_subtask_2(std) 632 | do_subtask_2(std) 633 | ``` 634 | 635 | 每个函数一层一层调用都这么传参数那还得了?用全局变量?也不行,因为每个线程处理不同的 `Student` 对象,所以不能共享。 636 | 637 | 如果用一个全局的 `dict` 存放所有的 `Student` 对象,以线程自身作为 `key` 和存取对应的 `Student` 对象如何呢? 638 | 639 | ```python 640 | global_dict = {} 641 | 642 | def std_thread(name): 643 | std = Student(name) 644 | # 把std放到全局变量global_dict中: 645 | global_dict[threading.current_thread()] = std 646 | do_task_1() 647 | do_task_2() 648 | 649 | def do_task_1(): 650 | # 不传入std,而是根据当前线程查找: 651 | std = global_dict[threading.current_thread()] 652 | ... 653 | 654 | def do_task_2(): 655 | # 任何函数都可以查找出当前线程的std变量: 656 | std = global_dict[threading.current_thread()] 657 | ... 658 | ``` 659 | 660 | 这种方式理论上是可行的,它最大的优点是消除了 `std` 对象在每层函数中的传递问题,但是,每个函数获取 `std` 的代码有点丑。 661 | 662 | --- 663 | 664 | ### ThreadLocal的用法 665 | 666 | 有没有更简单的方式?`ThreadLocal` 应运而生,不用查找 `dict`,`ThreadLocal` 可以自动帮我们做这件事: 667 | 668 | ```python 669 | import threading 670 | 671 | # 创建全局ThreadLocal对象: 672 | local_school = threading.local() 673 | 674 | def process_student(): 675 | # 获取当前线程关联的student: 676 | std = local_school.student 677 | print('Hello, %s (in %s)' % (std, threading.current_thread().name)) 678 | 679 | def process_thread(name): 680 | # 绑定ThreadLocal的student: 681 | local_school.student = name 682 | process_student() 683 | 684 | t1 = threading.Thread(target= process_thread, args=('Alice',), name='Thread-A') 685 | t2 = threading.Thread(target= process_thread, args=('Bob',), name='Thread-B') 686 | t1.start() 687 | t2.start() 688 | t1.join() 689 | t2.join() 690 | ``` 691 | 692 | 执行结果: 693 | 694 | ```python 695 | Hello, Alice (in Thread-A) 696 | Hello, Bob (in Thread-B) 697 | ``` 698 | 699 | 全局变量 `local_school` 就是一个 `ThreadLocal` 对象,每个线程都可以对它进行读写,线程之间互不影响。你可以把 `local_school` 看成全局变量,但每个属性如 `local_school.student` 都是线程的局部变量,可以任意读写而互不干扰,也不用管理锁的问题,`ThreadLocal` 内部会处理。也可以把 `local_school` 理解为一个全局的 `dict`,不但可以绑定 `local_school.student`,还可以绑定其他变量,如 `local_school.teacher` 等等。 700 | 701 | `ThreadLocal` 最常用的地方就是为每个线程绑定一个不同的数据库连接,HTTP请求,用户身份信息等,这样一个线程的所有调用到的处理函数都可以非常方便地访问这些资源。 702 | 703 | --- 704 | 705 | ### 小结 706 | 707 | `ThreadLocal` 虽然是全局的,但每个线程都只能读写自己线程的独立副本,互不干扰。`ThreadLocal`解决了参数在一个线程中各个函数之间互相传递的问题。 708 | 709 | --- 710 | 711 |
712 | 713 | ## 进程 vs. 线程 714 | 715 | ### 比较多进程和多线程 716 | 717 | 我们介绍了多进程和多线程,这是实现多任务最常用的两种方式。现在,我们来讨论一下这两种方式的优缺点。 718 | 719 | 首先,要实现多任务,通常我们会设计**主从模式(Master-Worker模式)**,Master负责分配任务,Worker负责执行任务,因此,多任务环境下,通常是一个Master,多个Worker。 720 | 721 | - 如果用多进程实现Master-Worker,主进程就是Master,其他进程就是Worker。 722 | - 如果用多线程实现Master-Worker,主线程就是Master,其他线程就是Worker。 723 | 724 | 多进程模式最大的优点就是**稳定性高**,因为一个子进程崩溃了,不会影响主进程和其他子进程。(当然主进程挂了所有进程就全挂了,但是Master进程只负责分配任务,挂掉的概率低)著名的Apache最早就是采用多进程模式。 725 | 726 | 多进程模式的缺点是**创建进程的代价大**,在Unix/Linux系统下,用fork调用还行,在Windows下创建进程开销巨大。另外,**操作系统能同时运行的进程数也是有限的**,在内存和CPU的限制下,如果有几千个进程同时运行,操作系统连调度都会成问题。 727 | 728 | 多线程模式通常比多进程快一点,但是也快不到哪去,而且,多线程模式致命的缺点就是**任何一个线程挂掉都可能直接造成整个进程崩溃**,因为所有线程共享进程的内存。在Windows上,如果一个线程执行的代码出了问题,你经常可以看到这样的提示:“该程序执行了非法操作,即将关闭”,其实往往是某个线程出了问题,但是操作系统会强制结束整个进程。 729 | 730 | **在Windows下,多线程的效率比多进程要高**,所以微软的IIS服务器默认采用多线程模式。由于多线程存在稳定性的问题,IIS的稳定性就不如Apache。为了缓解这个问题,IIS和Apache现在又有多进程+多线程的混合模式,真是把问题越搞越复杂。 731 | 732 | --- 733 | 734 | ### 效率问题 735 | 736 | 无论是多进程还是多线程,只要数量一多,效率肯定上不去,为什么呢? 737 | 738 | 我们打个比方,假设你不幸正在准备中考,每天晚上需要做语文、数学、英语、物理、化学这5科的作业,每项作业耗时1小时。 739 | 740 | 如果你先花1小时做语文作业,做完了,再花1小时做数学作业,这样,依次全部做完,一共花5小时。这种每次只完成一个任务的方式称为**单任务模型**,或者**批处理任务模型**。 741 | 742 | 假设你打算切换到多任务模型,可以先做1分钟语文,再切换到数学作业,做1分钟,再切换到英语,以此类推,只要切换速度足够快,这种方式就和单核CPU执行多任务是一样的了,以幼儿园小朋友的眼光来看,你就正在同时写5科作业。 743 | 744 | 但是,切换作业是有代价的,比如从语文切到数学,要先收拾桌子上的语文书本、钢笔(这叫**保存现场**),然后,打开数学课本、找出圆规直尺(这叫**准备新环境**),才能开始做数学作业。操作系统在切换进程或者线程时也是一样的,它需要先保存当前执行的现场环境(CPU寄存器状态、内存页等),然后,把新任务的执行环境准备好(恢复上次的寄存器状态,切换内存页等),才能开始执行。这个切换过程虽然很快,但是也需要耗费时间。如果有几千个任务同时进行,操作系统可能就主要忙着切换任务,根本没有多少时间去执行任务了,这种情况最常见的就是硬盘狂响,点窗口无反应,系统处于假死状态。 745 | 746 | 所以,多任务一旦多到一个限度,就会消耗掉系统所有的资源,结果效率急剧下降,所有任务都做不好。 747 | 748 | --- 749 | 750 | ### 计算密集型 vs. IO密集型 751 | 752 | 是否采用多任务的第二个考虑是任务的类型。我们可以把任务分为计算密集型和IO密集型。 753 | 754 | 计算密集型任务的特点是要进行大量的计算,消耗CPU资源,比如计算圆周率、对视频进行高清解码等等,全靠CPU的运算能力。这种计算密集型任务虽然也可以用多任务完成,但是任务越多,花在任务切换的时间就越多,CPU执行任务的效率就越低,所以,**要最高效地利用CPU,计算密集型任务同时进行的数量应当等于CPU的核心数**。 755 | 756 | 计算密集型任务由于主要消耗CPU资源,因此,代码运行效率至关重要。**Python这样的脚本语言运行效率很低,完全不适合计算密集型任务。对于计算密集型任务,最好用C语言编写**。 757 | 758 | 第二种任务的类型是IO密集型,涉及到网络、磁盘IO的任务都是IO密集型任务,这类任务的特点是CPU消耗很少,任务的大部分时间都在等待IO操作完成(因为IO的速度远远低于CPU和内存的速度)。对于**IO密集型任务,任务越多,CPU效率越高,但也有一个限度**。常见的大部分任务都是IO密集型任务,比如Web应用。 759 | 760 | IO密集型任务执行期间,99%的时间都花在IO上,花在CPU上的时间很少,因此,用运行速度极快的C语言替换用Python这样运行速度极低的脚本语言,完全无法提升运行效率。对于IO密集型任务,最合适的语言就是开发效率最高(代码量最少)的语言,脚本语言是首选,C语言最差。 761 | 762 | --- 763 | 764 | ### 异步IO 765 | 766 | 考虑到CPU和IO之间巨大的速度差异,一个任务在执行的过程中大部分时间都在等待IO操作,单进程单线程模型会导致别的任务无法并行执行,因此,我们才需要多进程模型或者多线程模型来支持多任务并发执行。 767 | 768 | 现代操作系统对IO操作已经做了巨大的改进,最大的特点就是支持**异步IO**。如果充分利用操作系统提供的异步IO支持,就可以**用单进程单线程模型来执行多任务**,这种全新的模型称为**事件驱动模型**,Nginx就是支持异步IO的Web服务器,它在单核CPU上采用单进程模型就可以高效地支持多任务。在多核CPU上,可以运行多个进程(数量与CPU核心数相同),充分利用多核CPU。由于系统总的进程数量十分有限,因此操作系统调度非常高效。用异步IO编程模型来实现多任务是一个主要的趋势。 769 | 770 | 所谓异步IO,其实这个异步是指使用者发起IO请求后并不马上得到结果。比方说有一些需要处理的数据放在磁盘上,使用者预先知道这些数据的位置,所以预先发起异步IO读请求。而等到真正需要用到这些数据的时候,再等待异步IO完成,对数据进行处理。这样做的话,在发起IO请求到实际使用数据这段时间内,程序还可以继续做其他事情。 771 | 772 | 对应到Python语言,**单进程的异步编程模型称为协程**,有了协程的支持,就可以基于事件驱动编写高效的多任务程序。我们会在后面讨论如何编写协程。 773 | 774 | --- 775 | 776 |
777 | 778 | ## 分布式进程 779 | 780 | ### 简介 781 | 782 | 在线程和进程中,应当**优先选择进程**,因为进程更稳定,而且,**进程可以分布到多台机器上,而线程最多只能分布到同一台机器的多个CPU上**。 783 | 784 | Python的 `multiprocessing` 模块不但支持多进程,其中 `managers` 子模块还支持把多进程分布到多台机器上。一个服务进程可以作为调度者,将任务分布到其他多个进程中,依靠网络通信。由于 `managers` 模块封装很好,我们不必了解网络通信的细节,就可以很容易地编写分布式多进程程序。 785 | 786 | 举个例子:如果我们已经有一个通过 `Queue` 通信的多进程程序在同一台机器上运行,现在,由于处理任务的进程任务繁重,希望把发送任务的进程和处理任务的进程分布到两台机器上。怎么用分布式进程实现? 787 | 788 | 原有的 `Queue` 可以继续使用,但是,通过 `managers` 模块把 `Queue` **通过网络暴露出去**,就可以让其他机器的进程访问 `Queue` 了。 789 | 790 | --- 791 | 792 | ### 实现方法 793 | 794 | 我们先看服务进程,服务进程负责启动 `Queue`,把 `Queue` 注册到网络上,然后往 `Queue` 里面写入任务: 795 | 796 | ```python 797 | # task_master.py 798 | 799 | import random, time, queue 800 | from multiprocessing.managers import BaseManager 801 | 802 | # 发送任务的队列: 803 | task_queue = queue.Queue() 804 | # 接收结果的队列: 805 | result_queue = queue.Queue() 806 | 807 | # 从BaseManager继承的QueueManager: 808 | class QueueManager(BaseManager): 809 | pass 810 | 811 | # 把两个Queue都注册到网络上, callable参数关联了Queue对象: 812 | QueueManager.register('get_task_queue', callable=lambda: task_queue) 813 | QueueManager.register('get_result_queue', callable=lambda: result_queue) 814 | # 绑定端口5000, 设置验证码'abc': 815 | manager = QueueManager(address=('', 5000), authkey=b'abc') 816 | # 启动Queue: 817 | manager.start() 818 | # 获得通过网络访问的Queue对象: 819 | task = manager.get_task_queue() 820 | result = manager.get_result_queue() 821 | # 放几个任务进去: 822 | for i in range(10): 823 | n = random.randint(0, 10000) 824 | print('Put task %d...' % n) 825 | task.put(n) 826 | # 从result队列读取结果: 827 | print('Try get results...') 828 | for i in range(10): 829 | r = result.get(timeout=10) 830 | print('Result: %s' % r) 831 | # 关闭: 832 | manager.shutdown() 833 | print('master exit.') 834 | ``` 835 | 836 | 请注意,当我们在一台机器上写多进程程序时,创建的 `Queue` 可以直接拿来用,但是,在分布式多进程环境下,添加任务到 `Queue` **不可以直接对原始的 `task_queue` 进行操作**,那样就绕过了 `QueueManager` 的封装,必须通过 `manager.get_task_queue()` 获得的 `Queue` 接口添加任务。 837 | 838 | 然后,在另一台机器上启动任务进程(本机上启动也可以): 839 | 840 | ```python 841 | # task_worker.py 842 | 843 | import time, sys, queue 844 | from multiprocessing.managers import BaseManager 845 | 846 | # 创建类似的QueueManager: 847 | class QueueManager(BaseManager): 848 | pass 849 | 850 | # 由于这个QueueManager只从网络上获取Queue,所以注册时只提供名字: 851 | QueueManager.register('get_task_queue') 852 | QueueManager.register('get_result_queue') 853 | 854 | # 连接到服务器,也就是运行task_master.py的机器: 855 | server_addr = '127.0.0.1' # 这里因为worker用的也是本机,所以写回送地址就可以了 856 | print('Connect to server %s...' % server_addr) 857 | # 端口和验证码注意保持与task_master.py设置的完全一致: 858 | m = QueueManager(address=(server_addr, 5000), authkey=b'abc') 859 | # 从网络连接: 860 | m.connect() 861 | # 获取Queue的对象: 862 | task = m.get_task_queue() 863 | result = m.get_result_queue() 864 | # 从task队列取任务,并把结果写入result队列: 865 | for i in range(10): 866 | try: 867 | n = task.get(timeout=1) 868 | print('run task %d * %d...' % (n, n)) 869 | r = '%d * %d = %d' % (n, n, n*n) 870 | time.sleep(1) 871 | result.put(r) 872 | except Queue.Empty: 873 | print('task queue is empty.') 874 | # 处理结束: 875 | print('worker exit.') 876 | ``` 877 | 878 | 任务进程要通过网络连接到服务进程,所以要指定服务进程的IP。 879 | 880 | 现在,可以试试分布式进程的工作效果了。先启动 `task_master.py` 服务进程: 881 | 882 | ```python 883 | $ python3 task_master.py 884 | Put task 3411... 885 | Put task 1605... 886 | Put task 1398... 887 | Put task 4729... 888 | Put task 5300... 889 | Put task 7471... 890 | Put task 68... 891 | Put task 4219... 892 | Put task 339... 893 | Put task 7866... 894 | Try get results... 895 | ``` 896 | 897 | `task_master.py` 进程发送完任务后,开始等待 `result` 队列的结果。现在启动 `task_worker.py` 进程: 898 | 899 | ```python 900 | $ python3 task_worker.py 901 | Connect to server 127.0.0.1... 902 | run task 3411 * 3411... 903 | run task 1605 * 1605... 904 | run task 1398 * 1398... 905 | run task 4729 * 4729... 906 | run task 5300 * 5300... 907 | run task 7471 * 7471... 908 | run task 68 * 68... 909 | run task 4219 * 4219... 910 | run task 339 * 339... 911 | run task 7866 * 7866... 912 | worker exit. 913 | ``` 914 | 915 | `task_worker.py` 进程结束,在 `task_master.py` 进程中会继续打印出结果: 916 | 917 | ```python 918 | Result: 3411 * 3411 = 11634921 919 | Result: 1605 * 1605 = 2576025 920 | Result: 1398 * 1398 = 1954404 921 | Result: 4729 * 4729 = 22363441 922 | Result: 5300 * 5300 = 28090000 923 | Result: 7471 * 7471 = 55815841 924 | Result: 68 * 68 = 4624 925 | Result: 4219 * 4219 = 17799961 926 | Result: 339 * 339 = 114921 927 | Result: 7866 * 7866 = 61873956 928 | ``` 929 | 930 | 这个简单的Master/Worker模型有什么用?其实这就是一个简单但真正的分布式计算,把代码稍加改造,启动多个worker,就可以把任务分布到几台甚至几十台机器上,比如把计算 `n*n` 的代码换成发送邮件,就实现了邮件队列的异步发送。 931 | 932 | `Queue` 对象存储在哪?注意到 `task_worker.py` 中根本没有创建 `Queue` 的代码,所以,`Queue` 对象是存储在 `task_master.py` 进程中的: 933 | 934 | ![task_master_worker](http://oe0e8k1nf.bkt.clouddn.com/My_Python_Notebook_task_master_worker.png) 935 | 936 | 而 `Queue` 之所以能通过网络访问,就是通过 `QueueManager` 实现的。由于 `QueueManager` 管理的不止一个 `Queue`,所以,要给每个 `Queue` 的网络调用接口起个名字,比如 `get_task_queue`。 937 | 938 | `authkey` 有什么用?这是为了保证两台机器正常通信,不被其他机器恶意干扰。如果 `task_worker.py` 的 `authkey` 和 `task_master.py` 的 `authkey` 不一致,肯定连接不上。 939 | 940 | --- 941 | 942 | ### 小结 943 | 944 | Python的分布式进程接口简单,封装良好,适合需要把繁重任务分布到多台机器的环境下。 945 | 946 | 注意 `Queue` 的作用是用来传递任务和接收结果,每个任务的描述数据要尽量小。比如发送一个处理日志文件的任务,就不要发送几百兆的日志文件本身,而是发送日志文件存放的完整路径,由Worker进程再去共享的磁盘上读取文件。 947 | 948 | --- 949 | 950 |
951 | -------------------------------------------------------------------------------- /My_Python_Notebook/11正则表达式.md: -------------------------------------------------------------------------------- 1 | # 正则表达式 2 | 3 | ## 目录 4 | 5 | 6 | 7 | - [简介](#简介) 8 | - [进阶](#进阶) 9 | - [re模块](#re模块) 10 | - [切分字符串](#切分字符串) 11 | - [分组](#分组) 12 | - [贪婪匹配](#贪婪匹配) 13 | - [编译](#编译) 14 | - [小结](#小结) 15 | - [练习](#练习) 16 | - [习题一](#习题一) 17 | - [习题二](#习题二) 18 | 19 | 20 | 21 | 22 | ## 简介 23 | 24 | 字符串是编程时涉及到的最多的一种数据结构,对字符串进行操作的需求几乎无处不在。比如判断一个字符串是否是合法的Email地址,虽然可以编程提取 `@` 前后的子串,再分别判断是否是单词和域名,但这样做不但麻烦,而且代码难以复用。 25 | 26 | 正则表达式是一种用来**匹配字符串的强大工具**。它的设计思想是**用一种描述性的语言来给字符串定义一个规则,凡是符合规则的字符串,我们就认为它“匹配”了**,否则,该字符串就是不合法的。 27 | 28 | 所以我们判断一个字符串是否是合法的Email的方法是: 29 | 30 | 1. 创建一个匹配Email的正则表达式; 31 | 2. 用该正则表达式去匹配用户的输入来判断是否合法。 32 | 33 | 因为正则表达式也是用字符串表示的,所以,我们要首先了解**如何用字符来描述字符**。 34 | 35 | 在正则表达式中,如果直接给出字符,就是**精确匹配**。用 `\d` 可以**匹配一个数字**,`\w` 可以**匹配一个字母或数字**,`\s` 可以**匹配一个空格(也包括Tab等空白符)**。所以: 36 | 37 | - `'00\d'` 可以匹配 `'007'`,但无法匹配 `'00A'`; 38 | - `'\d\d\d'` 可以匹配 `'010'`; 39 | - `'\w\w\d'` 可以匹配 `'py3'`; 40 | - `.` 可以匹配任意字符,所以: `'py.'` 可以匹配 `'pyc'`、`'pyo'`、`'py!'` 等等。 41 | 42 | 要匹配变长的字符,在正则表达式中,用 `*` 表示**任意个字符(包括0个)**,用 `+` 表示**至少一个字符**,用 `?` 表示**0个或1个字符**,用 `{n}` 表示**n个字符**,用 `{n,m}` 表示**n-m个字符**: 43 | 44 | 来看一个复杂的例子:`\d{3}\s+\d{3,8}` 45 | 46 | 我们来从左到右解读一下: 47 | 48 | - `\d{3}` 表示匹配3个数字,例如 `'010'`; 49 | - `\s+` 表示至少有一个空格,例如匹配 `' '`,`' '` 等; 50 | - `\d{3,8}` 表示3-8个数字,例如 `'1234567'`。 51 | 52 | 综合起来,上面的正则表达式可以匹配以任意个空格隔开的带区号的电话号码。 53 | 54 | 如果要匹配 `'010-12345'` 这样的号码呢?由于 **`'-'` 是特殊字符**,在正则表达式中,**要用 `'\'` 转义**,所以,用于匹配的正则表达式应为 `\d{3}\-\d{3,8}`。 55 | 56 | 但是,上面的方法无法匹配 `'010 - 12345'`,我们需要更复杂的匹配方式。 57 | 58 | --- 59 | 60 |
61 | 62 | ## 进阶 63 | 64 | 要做更精确地匹配,可以用 `[]` 表示一个范围,比如: 65 | 66 | - `[0-9a-zA-Z\_]` 可以匹配一个数字、字母或者下划线; 67 | - `[0-9a-zA-Z\_]+` 可以匹配至少由一个数字、字母或者下划线组成的字符串,比如'a100','0_Z','Py3000'等等; 68 | - `[a-zA-Z\_][0-9a-zA-Z\_]*` 可以匹配由字母或下划线开头,后接任意个由一个数字、字母或者下划线组成的字符串,也就是Python合法的变量; 69 | - `[a-zA-Z\_][0-9a-zA-Z\_]{0, 19}` 更精确地限制了变量的长度是1-20个字符(前面1个字符+后面最多19个字符)。 70 | - `A|B` 可以匹配A或B,所以 `(P|p)ython` 既可以匹配 `'Python'` 又可以匹配 `'python'`。 71 | - `^` 表示行的开头,`^\d` 表示必须以数字开头。 72 | - `$` 表示行的结束,`\d$` 表示必须以数字结束。 73 | 74 | 有趣的是 `py` 也可以用来匹配 `'python'`,但是加上 `^py$` 就变成了整行匹配,只能匹配 `'py'` 了。 75 | 76 | --- 77 | 78 |
79 | 80 | ## re模块 81 | 82 | 有了准备知识,我们就可以在Python中使用正则表达式了。Python提供了 `re` 模块,包含所有正则表达式的功能。注意,在正则表达式中,我们使用 `\` 来表示转义,但在Python的字符串中,`\` 同样是一个转义符。因此,使用时我们就要特别注意字符串中的 `\` 是否能正确地起作用。比方说: 83 | 84 | ```python 85 | s = 'ABC\\-001' 86 | ``` 87 | 88 | 这个字符串中,第一个 `\` 用于为第二个 `\` 转义,所以打印这个字符串实际上得到的是 'ABC\-001',这样用作正则表达式时,就能正确地转义 `-`。而: 89 | 90 | ```python 91 | s = 'ABC\-001' 92 | ``` 93 | 94 | 这个字符串实际上是 `'ABC-001'`,由于 `-` 在正则表达式中是一个特殊字符,而这里我们希望的是它只作为用于匹配字符 `-` 的功能,所以这样得到的正则表达式就出错了。 95 | 96 | 因此在书写正则表达式时,推荐使用Python字符串的 `r` 前缀,这样就不用考虑字符转义的问题了: 97 | 98 | ```python 99 | s = r'ABC\-001' 100 | ``` 101 | 102 | **`r` 前缀表示后面的字符串中无需转义字符。** 103 | 104 | 接下来看看如何判断正则表达式是否与字符串匹配: 105 | 106 | ```python 107 | >>> import re 108 | >>> re.match(r'^\d{3}\-\d{3,8}$', '010-12345') 109 | <_sre.SRE_Match object; span=(0, 9), match='010-12345'> 110 | >>> re.match(r'^\d{3}\-\d{3,8}$', '010 12345') 111 | >>> 112 | ``` 113 | 114 | `re` 模块的 `match()` 方法判断是否匹配,如果匹配成功,返回一个 `Match` 对象,否则返回 `None`。常见的判断方法就是: 115 | 116 | ```python 117 | test = '用户输入的字符串' 118 | if re.match(r'正则表达式', test): 119 | print('ok') 120 | else: 121 | print('failed') 122 | ``` 123 | 124 | --- 125 | 126 |
127 | 128 | ## 切分字符串 129 | 130 | 用正则表达式切分字符串比用固定的字符更灵活,请看正常的切分代码: 131 | 132 | ```python 133 | >>> 'a b c'.split(' ') 134 | ['a', 'b', '', '', 'c'] 135 | ``` 136 | 137 | 嗯,无法识别连续的空格,用正则表达式试试: 138 | 139 | ```python 140 | >>> re.split(r'\s+', 'a b c') # '\s+'表示至少匹配一个空白字符 141 | ['a', 'b', 'c'] 142 | ``` 143 | 144 | 无论多少个空格都可以正常分割。加入 `,` 试试: 145 | 146 | ```python 147 | >>> re.split(r'[\s\,]+', 'a,b, c d') 148 | ['a', 'b', 'c', 'd'] 149 | ``` 150 | 151 | 再加入 `;` 试试: 152 | 153 | ```python 154 | >>> re.split(r'[\s\,\;]+', 'a,b;; c d') 155 | ['a', 'b', 'c', 'd'] 156 | ``` 157 | 158 | 如果用户输入了一组标签,下次记得可以用正则表达式来把不规范的输入转化成正确的数组。 159 | 160 | --- 161 | 162 |
163 | 164 | ## 分组 165 | 166 | 除了简单地判断是否匹配之外,正则表达式还有提取子串的强大功能。用 `()` 表示的就是要提取的**分组(Group)**。比如: 167 | 168 | `^(\d{3})-(\d{3,8})$` 分别定义了两个组,可以直接从匹配的字符串中提取出区号和本地号码: 169 | 170 | ```python 171 | >>> m = re.match(r'^(\d{3})-(\d{3,8})$', '010-12345') 172 | >>> m 173 | <_sre.SRE_Match object; span=(0, 9), match='010-12345'> 174 | >>> m.group(0) 175 | '010-12345' 176 | >>> m.group(1) 177 | '010' 178 | >>> m.group(2) 179 | '12345' 180 | ``` 181 | 182 | 如果正则表达式中定义了组,就可以在 `Match` 对象上用 `group()` 方法提取出某个子串。注意到 `group(0)` 永远是原始字符串,`group(1)`、`group(2)` …… 表示第1、2、……个子串。 183 | 184 | 提取子串非常有用。来看一个更凶残的例子: 185 | 186 | ```python 187 | >>> t = '19:05:30' 188 | >>> m = 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) 189 | >>> m.groups() 190 | ('19', '05', '30') 191 | ``` 192 | 193 | 这个正则表达式可以直接识别合法的时间。但是有些时候,用正则表达式也无法做到完全验证,比如识别日期: 194 | 195 | ```python 196 | '^(0[1-9]|1[0-2]|[0-9])-(0[1-9]|1[0-9]|2[0-9]|3[0-1]|[0-9])$' 197 | ``` 198 | 199 | 对于'2-30','4-31'这样的非法日期,用正则还是识别不了,或者说写出来非常困难,这时就需要编写其他代码来配合识别了。 200 | 201 | --- 202 | 203 |
204 | 205 | ## 贪婪匹配 206 | 207 | 最后需要特别指出的是,**正则匹配默认是贪婪匹配**,也就是匹配尽可能多的字符。举例如下,匹配出数字后面的0: 208 | 209 | ```python 210 | >>> re.match(r'^(\d+)(0*)$', '102300').groups() 211 | ('102300', '') 212 | ``` 213 | 214 | 由于 `\d+` 采用贪婪匹配,直接把后面的0全部匹配了,结果 `0*` 就只能匹配到空字符串了。 215 | 216 | 必须让 `\d+` 采用非贪婪匹配(也就是尽可能少匹配),才能把后面的0匹配出来,这时我们可以使用 `?` 来让 `\d+` 采用非贪婪匹配: 217 | 218 | ```python 219 | >>> re.match(r'^(\d+?)(0*)$', '102300').groups() 220 | ('1023', '00') 221 | ``` 222 | 223 | --- 224 | 225 |
226 | 227 | ## 编译 228 | 229 | 当我们在Python中使用正则表达式时,`re` 模块内部会干两件事情: 230 | 231 | 1. 编译正则表达式,如果正则表达式的字符串本身不合法,会报错; 232 | 2. 用编译后的正则表达式去匹配字符串。 233 | 234 | 如果一个正则表达式要重复使用几千次,出于效率的考虑,我们可以**预编译正则表达式**,接下来**重复使用时就不需要编译这个步骤了**,可以直接匹配: 235 | 236 | ```python 237 | >>> import re 238 | # 编译: 239 | >>> re_telephone = re.compile(r'^(\d{3})-(\d{3,8})$') 240 | # 使用: 241 | >>> re_telephone.match('010-12345').groups() 242 | ('010', '12345') 243 | >>> re_telephone.match('010-8086').groups() 244 | ('010', '8086') 245 | ``` 246 | 247 | 编译后生成 `Regular Expression` 对象,由于该对象自己包含了正则表达式,所以调用对应的方法时不用给出正则字符串。 248 | 249 | --- 250 | 251 |
252 | 253 | ## 小结 254 | 255 | 正则表达式非常强大,要在短短的一节里讲完是不可能的。要讲清楚正则的所有内容,可以写一本厚厚的书了。如果你经常遇到正则表达式的问题,你可能需要一本正则表达式的参考书。 256 | 257 | --- 258 | 259 |
260 | 261 | ## 练习 262 | 263 | ### 习题一 264 | 265 | > 请尝试写一个验证Email地址的正则表达式,可以验证类似以下格式的Email: 266 | 267 | ```python 268 | someone@gmail.com 269 | bill.gates@microsoft.com 270 | ``` 271 | 272 | 代码: 273 | 274 | ```python 275 | import re 276 | 277 | re_email = re.compile(r'^[a-z.]+?@[a-z]+?.com$') 278 | 279 | while True: 280 | test = input('\nPlease input your email address: ') 281 | if re_email.match(test): 282 | print('ok') 283 | else: 284 | print('failed') 285 | ``` 286 | 287 | --- 288 | 289 | ### 习题二 290 | 291 | > 继续上一题,但这次的Email地址带名字,要既能验证地址又能提取出名字: 292 | 293 | ```python 294 | tom@voyager.com 295 | mary@microsoft.com 296 | ``` 297 | 298 | 代码: 299 | 300 | ```python 301 | import re 302 | 303 | re_email = re.compile(r'^<([A-Za-z\s]+?)\s([A-Za-z\s]+?)>\s([a-z.]+?@[a-z]+?.com)$') 304 | 305 | while True: 306 | test = input('\nPlease input your email address: ') 307 | match = re_email.match(test) 308 | if match: 309 | print(match.group(1)+' '+match.group(2)+"'s email address is: "+match.group(3)) 310 | else: 311 | print('failed') 312 | ``` 313 | 314 | --- 315 | 316 |
317 | -------------------------------------------------------------------------------- /My_Python_Notebook/12常用内建模块(上).md: -------------------------------------------------------------------------------- 1 | # 常用内建模块(上) 2 | 3 | Python之所以自称 **“batteries included”**,就是因为内置了许多非常有用的模块,无需额外安装和配置,即可直接使用。 4 | 5 | 本章将介绍一些常用的内建模块。 6 | 7 | ## 目录 8 | 9 | 10 | 11 | - [datetime](#datetime) 12 | - [获取当前日期和时间](#获取当前日期和时间) 13 | - [获取指定日期和时间](#获取指定日期和时间) 14 | - [datetime转换为timestamp](#datetime转换为timestamp) 15 | - [timestamp转换为datetime](#timestamp转换为datetime) 16 | - [str转换为datetime](#str转换为datetime) 17 | - [datetime转换为str](#datetime转换为str) 18 | - [datetime加减](#datetime加减) 19 | - [本地时间转换为UTC时间](#本地时间转换为utc时间) 20 | - [时区转换](#时区转换) 21 | - [小结](#小结) 22 | - [练习](#练习) 23 | - [collections](#collections) 24 | - [简介](#简介) 25 | - [namedtuple](#namedtuple) 26 | - [deque](#deque) 27 | - [defaultdict](#defaultdict) 28 | - [OrderedDict](#ordereddict) 29 | - [Counter](#counter) 30 | - [小结](#小结-1) 31 | - [base64](#base64) 32 | - [简介](#简介-1) 33 | - [原理](#原理) 34 | - [Python中的实现方式](#python中的实现方式) 35 | - [小结](#小结-2) 36 | - [练习](#练习-1) 37 | - [struct](#struct) 38 | - [为何需要struct](#为何需要struct) 39 | - [struct的用法](#struct的用法) 40 | - [使用struct分析bmp文件](#使用struct分析bmp文件) 41 | - [小结](#小结-3) 42 | - [练习](#练习-2) 43 | - [hashlib](#hashlib) 44 | - [摘要算法简介](#摘要算法简介) 45 | - [Python实现](#python实现) 46 | - [摘要算法应用](#摘要算法应用) 47 | - [练习一](#练习一) 48 | - [加盐](#加盐) 49 | - [练习二](#练习二) 50 | - [小结](#小结-4) 51 | 52 | 53 | 54 | 55 | ## datetime 56 | 57 | `datetime` 是Python处理日期和时间的标准库。 58 | 59 | ### 获取当前日期和时间 60 | 61 | 我们先看如何获取当前日期和时间: 62 | 63 | ```python 64 | >>> from datetime import datetime 65 | >>> now = datetime.now() # 获取当前datetime 66 | >>> print(now) 67 | 2015-05-18 16:28:07.198690 68 | >>> print(type(now)) 69 | 70 | ``` 71 | 72 | 注意到,`datetime` 模块包含一个 `datetime`类,我们可以通过 `from datetime import datetime` 来导入 `datetime` 类。如果仅使用 `import datetime` 导入,则必须引用全名 `datetime.datetime` 才能使用这个类。`datetime.now()` 可以返回当前日期和时间,其类型是 `datetime.datetime`。 73 | 74 | --- 75 | 76 | ### 获取指定日期和时间 77 | 78 | 要指定某个日期和时间,我们直接用参数构造一个 `datetime` 类实例: 79 | 80 | ```python 81 | >>> from datetime import datetime 82 | >>> dt = datetime(2015, 4, 19, 12, 20) # 用指定日期时间创建datetime 83 | >>> print(dt) 84 | 2015-04-19 12:20:00 85 | ``` 86 | 87 | --- 88 | 89 | ### datetime转换为timestamp 90 | 91 | 在计算机中,时间实际上是用数字表示的。我们把**1970年1月1日 00:00:00 UTC+00:00时区**的时刻称为**新纪元时间(epoch time)**,记为数字0(1970年以前的时间timestamp为负数),**当前时间就是相对于epoch time的秒数,称为timestamp**。 92 | 93 | 你可以认为: 94 | 95 | ```python 96 | timestamp = 0 # 等价于1970-1-1 00:00:00 UTC+0:00 97 | ``` 98 | 99 | 对应的北京时间是: 100 | 101 | ```python 102 | timestamp = 0 # 等价于1970-1-1 08:00:00 UTC+8:00 103 | ``` 104 | 105 | 也即epoch time是北京时间1970年1月1日的早上8点钟。 106 | 107 | 可见**timestamp的值与时区毫无关系**,无论在哪个时区,同一时刻timestamp的值都相同,实际时间只需要再按时区推算就可以了。这就是为什么计算机存储的当前时间是以timestamp表示的,因为**全球各地的计算机在任意时刻的timestamp都是完全相同的**(假定时间已校准)。 108 | 109 | 把一个 `datetime` 类型转换为timestamp只需要简单调用 `timestamp()` 方法: 110 | 111 | ```python 112 | >>> from datetime import datetime 113 | >>> dt = datetime(2015, 4, 19, 12, 20) # 用指定日期时间创建datetime 114 | >>> dt.timestamp() # 把datetime转换为timestamp 115 | 1429417200.0 116 | ``` 117 | 118 | 注意**Python的timestamp是一个浮点数**。如果有小数位,小数位表示**毫秒数**。 119 | 120 | 某些编程语言(如Java和JavaScript)的timestamp使用整数表示毫秒数,这种情况下只需要把timestamp除以1000就得到Python的浮点表示方法。 121 | 122 | --- 123 | 124 | ### timestamp转换为datetime 125 | 126 | 要把timestamp转换为 `datetime`,使用 `datetime` 提供的 `fromtimestamp()` 方法: 127 | 128 | ```python 129 | >>> from datetime import datetime 130 | >>> t = 1429417200.0 131 | >>> print(datetime.fromtimestamp(t)) 132 | 2015-04-19 12:20:00 133 | ``` 134 | 135 | 注意到timestamp是一个浮点数,它没有时区的概念,而datetime是有时区的。**上述转换是在timestamp和本地时间做转换(本地时间是指当前操作系统设定的时区)**。例如北京时区是东8区,则本地时间: 136 | 137 | ```python 138 | 2015-04-19 12:20:00 139 | ``` 140 | 141 | 实际上就是UTC+8:00时区的时间,也即: 142 | 143 | ```python 144 | 2015-04-19 12:20:00 UTC+8:00 145 | ``` 146 | 147 | 而此刻的格林威治标准时间与北京时间差了8小时,也就是UTC+0:00时区的时间应该是: 148 | 149 | ```python 150 | 2015-04-19 04:20:00 UTC+0:00 151 | ``` 152 | 153 | timestamp也可以直接被转换到UTC标准时区的时间: 154 | 155 | ```python 156 | >>> from datetime import datetime 157 | >>> t = 1429417200.0 158 | >>> print(datetime.fromtimestamp(t)) # 本地时间 159 | 2015-04-19 12:20:00 160 | >>> print(datetime.utcfromtimestamp(t)) # UTC时间 161 | 2015-04-19 04:20:00 162 | ``` 163 | 164 | --- 165 | 166 | ### str转换为datetime 167 | 168 | 很多时候,用户输入的日期和时间是字符串,要处理日期和时间,首先必须把 `str` 转换为 `datetime`。转换方法是通过 `datetime.strptime()` 实现的: 169 | 170 | ```python 171 | >>> from datetime import datetime 172 | >>> cday = datetime.strptime('2015-6-1 18:19:59', '%Y-%m-%d %H:%M:%S') 173 | >>> print(cday) 174 | 2015-06-01 18:19:59 175 | ``` 176 | 177 | 字符串 `'%Y-%m-%d %H:%M:%S'` 规定了日期和时间部分的格式。当然也可以根据实际需要进行修改,详细的说明请参考[Python文档](https://docs.python.org/3/library/datetime.html#strftime-strptime-behavior)。注意,**转换后的 `datetime` 是不带时区信息的**。 178 | 179 | --- 180 | 181 | ### datetime转换为str 182 | 183 | 如果已经有了 `datetime` 对象,要把它格式化为字符串显示给用户,就需要转换为 `str`,转换方法是通过 `strftime()` 实现的: 184 | 185 | ```python 186 | >>> from datetime import datetime 187 | >>> now = datetime.now() 188 | >>> print(now.strftime('%a, %b %d %H:%M')) 189 | Mon, May 05 16:28 190 | ``` 191 | 192 | --- 193 | 194 | ### datetime加减 195 | 196 | 对日期和时间进行加减实际上就是把 `datetime` 往后或往前计算,得到新的 `datetime`。加减可以直接用 `+` 和 `-` 运算符,不过需要导入 `timedelta` 这个类: 197 | 198 | ```python 199 | >>> from datetime import datetime, timedelta 200 | >>> now = datetime.now() 201 | >>> now 202 | datetime.datetime(2015, 5, 18, 16, 57, 3, 540997) 203 | >>> now + timedelta(hours=10) 204 | datetime.datetime(2015, 5, 19, 2, 57, 3, 540997) 205 | >>> now - timedelta(days=1) 206 | datetime.datetime(2015, 5, 17, 16, 57, 3, 540997) 207 | >>> now + timedelta(days=2, hours=12) 208 | datetime.datetime(2015, 5, 21, 4, 57, 3, 540997) 209 | ``` 210 | 211 | 可见,使用 `timedelta` 你可以很容易地算出前几天和后几天的时刻。 212 | 213 | 我们也可以使用 `timedelta` 很方便地计算出两个日期之间的差: 214 | 215 | ```python 216 | >>> f = datetime(2016,8,19) 217 | >>> p = datetime(2017,1,16) 218 | >>> print(p-f) 219 | 150 days, 0:00:00 220 | ``` 221 | 222 | --- 223 | 224 | ### 本地时间转换为UTC时间 225 | 226 | 本地时间是指系统设定时区的时间,例如北京时间是UTC+8:00时区的时间,而**UTC时间指UTC+0:00时区的时间**。`datetime` 类型有一个时区属性 `tzinfo`,但是默认为 `None`,所以无法区分这个 `datetime` 到底是哪个时区,除非强行给 `datetime` 设置一个时区: 227 | 228 | ```python 229 | >>> from datetime import datetime, timedelta, timezone 230 | >>> tz_utc_8 = timezone(timedelta(hours=8)) # 创建时区UTC+8:00 231 | >>> now = datetime.now() 232 | >>> now 233 | datetime.datetime(2015, 5, 18, 17, 2, 10, 871012) 234 | >>> dt = now.replace(tzinfo=tz_utc_8) # 强制设置为UTC+8:00 235 | >>> dt 236 | datetime.datetime(2015, 5, 18, 17, 2, 10, 871012, tzinfo=datetime.timezone(datetime.timedelta(0, 28800))) 237 | >>> print(dt) 238 | 2015-05-18 17:02:10.871012+08:00 239 | ``` 240 | 241 | 如果系统时区恰好是UTC+8:00,那么上述代码就是正确的,否则,不应该强制设置为时区信息。 242 | 243 | --- 244 | 245 | ### 时区转换 246 | 247 | 我们可以先通过 `utcnow()` 拿到当前的UTC时间,再转换为任意时区的时间: 248 | 249 | ```python 250 | # 拿到UTC时间,并强制设置时区为UTC+0:00: 251 | >>> utc_dt = datetime.utcnow().replace(tzinfo=timezone.utc) 252 | >>> print(utc_dt) 253 | 2015-05-18 09:05:12.377316+00:00 254 | # astimezone()将转换时区为北京时间: 255 | >>> bj_dt = utc_dt.astimezone(timezone(timedelta(hours=8))) 256 | >>> print(bj_dt) 257 | 2015-05-18 17:05:12.377316+08:00 258 | # astimezone()将utc_dt转换时区为东京时间: 259 | >>> tokyo_dt = utc_dt.astimezone(timezone(timedelta(hours=9))) 260 | >>> print(tokyo_dt) 261 | 2015-05-18 18:05:12.377316+09:00 262 | # astimezone()将bj_dt转换时区为东京时间: 263 | >>> tokyo_dt2 = bj_dt.astimezone(timezone(timedelta(hours=9))) 264 | >>> print(tokyo_dt2) 265 | 2015-05-18 18:05:12.377316+09:00 266 | ``` 267 | 268 | 注意,上述代码中首先通过 `utcnow()` 方法获取到一个处于UTC时间的 `datetime` 实例(按照系统时间推算出的,所以如果系统时间错则得到的实例也是错的),这个实例本身没有时区信息,所以要通过 `replace()` 方法强制设置时区信息为UTC时区。然后利用这个带时区的 `datetime`,通过 `astimezone()` 方法就可以转换到任意时区了。注意,任何带时区(不必是UTC时区)的 `datetime` 都可以被正确转换到别的时区,例如上述 `bj_dt` 到 `tokyo_dt` 的转换。 269 | 270 | ### 小结 271 | 272 | `datetime` 表示的时间需要时区信息才能确定一个特定的时间,否则只能视为本地时间。 273 | 274 | 如果要存储 `datetime`,最佳方法是将其转换为 `timestamp` 再存储,因为 `timestamp` 的值与时区完全无关。 275 | 276 | --- 277 | 278 | ### 练习 279 | 280 | > 假设你获取了用户输入的日期和时间如 `2015-1-21 9:01:30`,以及一个时区信息如 `UTC+5:00`,均是 `str`,请编写一个函数将其转换为 `timestamp`: 281 | 282 | 代码: 283 | 284 | ```python 285 | # -*- coding:utf-8 -*- 286 | 287 | import re 288 | from datetime import datetime, timezone, timedelta 289 | 290 | def to_timestamp(dt_str, tz_str): 291 | 292 | tz_match = re.match(r'UTC([\+\-]\d+?):00', tz_str) 293 | idate = datetime.strptime(dt_str, '%Y-%m-%d %H:%M:%S') 294 | idate = idate.replace(tzinfo=timezone(timedelta(hours=int(tz_match.group(1))))) 295 | return idate.timestamp() 296 | 297 | # 测试: 298 | 299 | t1 = to_timestamp('2015-6-1 08:10:30', 'UTC+7:00') 300 | if t1 == 1433121030.0: 301 | print('Pass.') 302 | else: 303 | print('Fail.') 304 | 305 | t2 = to_timestamp('2015-5-31 16:10:30', 'UTC-09:00') 306 | if t2 == 1433121030.0: 307 | print('Pass.') 308 | else: 309 | print('Fail.') 310 | ``` 311 | 312 | --- 313 | 314 |
315 | 316 | ## collections 317 | 318 | ### 简介 319 | 320 | `collections` 是Python内建的一个集合模块,提供了许多有用的集合类。 321 | 322 | ### namedtuple 323 | 324 | 我们知道 `tuple` 可以表示不变集合,例如,一个点的二维坐标就可以表示成: 325 | 326 | ```python 327 | >>> p = (1, 2) 328 | ``` 329 | 330 | 但是,代码中使用 `(1, 2)` 的方式来写很难看出这个 `tuple` 是用来表示一个点的。如果我们特地为定义一个“点”类而编写代码,又有点小题大作。怎么办呢?`collections` 模块中的 `namedtuple` 可以帮到我们: 331 | 332 | ```python 333 | >>> from collections import namedtuple 334 | >>> Point = namedtuple('Point', ['x', 'y']) 335 | >>> p = Point(1, 2) 336 | >>> p.x 337 | 1 338 | >>> p.y 339 | 2 340 | ``` 341 | 342 | `namedtuple()` 是一个函数,它可以用来方便地创建一个按需定制的 `tuple` 类的子类,我们可以把这样得到的子类看作一种特殊的元组,可以采用访问属性的方式来引用该元组的元素,使用十分方便。 343 | 344 | 可以验证创建的 `Point` 是 `tuple` 的一种子类: 345 | 346 | ```python 347 | >>> isinstance(p, Point) 348 | True 349 | >>> isinstance(p, tuple) 350 | True 351 | ``` 352 | 353 | 类似的,如果要用坐标和半径表示一个圆,也可以用 `namedtuple` 来创建一个 `Circle` 类: 354 | 355 | ```python 356 | # namedtuple('名称', [属性list]): 357 | Circle = namedtuple('Circle', ['x', 'y', 'r']) 358 | ``` 359 | 360 | --- 361 | 362 | ### deque 363 | 364 | 使用 `list` 存储数据时,按索引访问元素很快,但是插入和删除元素就很慢了,因为 `list` 是线性存储,数据量大的时候,插入和删除效率很低。`deque` 是为了更高效实现插入和删除操作而创造的双向列表,适合用于队列和栈: 365 | 366 | ```python 367 | >>> from collections import deque 368 | >>> q = deque(['a', 'b', 'c']) 369 | >>> q.append('x') 370 | >>> q.appendleft('y') 371 | >>> q 372 | deque(['y', 'a', 'b', 'c', 'x']) 373 | ``` 374 | 375 | `deque` 除了实现 `list` 的 `append()` 和 `pop()` 外,还支持 `appendleft()` 和 `popleft()`,这样就可以非常高效地往头部添加或删除元素。 376 | 377 | --- 378 | 379 | ### defaultdict 380 | 381 | 使用 `dict` 时,如果引用的Key不存在,就会抛出 `KeyError`。如果希望key不存在时也能返回一个默认值,可以使用 `defaultdict`: 382 | 383 | ```python 384 | >>> from collections import defaultdict 385 | >>> dd = defaultdict(lambda: 'N/A') 386 | >>> dd['key1'] = 'abc' 387 | >>> dd['key1'] # key1存在 388 | 'abc' 389 | >>> dd['key2'] # key2不存在,返回默认值 390 | 'N/A' 391 | ``` 392 | 393 | 注意**默认值是调用函数返回的**,而函数在创建 `defaultdict` 对象时传入。除了在Key不存在时返回默认值,`defaultdict` 的其他行为跟 `dict` 是完全一样的。 394 | 395 | --- 396 | 397 | ### OrderedDict 398 | 399 | 使用 `dict` 时,Key是无序的。在对 `dict` 做迭代时,我们无法确定Key的顺序。 400 | 401 | 如果要保持Key的顺序,可以用 `OrderedDict`: 402 | 403 | ```python 404 | >>> from collections import OrderedDict 405 | >>> d = dict([('a', 1), ('b', 2), ('c', 3)]) 406 | >>> d # dict的Key是无序的 407 | {'a': 1, 'c': 3, 'b': 2} 408 | >>> od = OrderedDict([('a', 1), ('b', 2), ('c', 3)]) 409 | >>> od # OrderedDict的Key是有序的 410 | OrderedDict([('a', 1), ('b', 2), ('c', 3)]) 411 | ``` 412 | 413 | 注意,**`OrderedDict` 的Key是按照插入的顺序排列的,不是Key本身排序**: 414 | 415 | ```python 416 | >>> od = OrderedDict() 417 | >>> od['z'] = 1 418 | >>> od['y'] = 2 419 | >>> od['x'] = 3 420 | >>> list(od.keys()) # 按照插入的Key的顺序返回 421 | ['z', 'y', 'x'] 422 | ``` 423 | 424 | `OrderedDict` 可以实现一个FIFO(先进先出)的 `dict`,当容量超出限制时,优先删除最早添加的Key: 425 | 426 | ```python 427 | from collections import OrderedDict 428 | 429 | class LastUpdatedOrderedDict(OrderedDict): 430 | 431 | def __init__(self, capacity): 432 | super(LastUpdatedOrderedDict, self).__init__() 433 | self._capacity = capacity 434 | 435 | def __setitem__(self, key, value): 436 | containsKey = 1 if key in self else 0 437 | if len(self) - containsKey >= self._capacity: 438 | last = self.popitem(last=False) 439 | print('remove:', last) 440 | if containsKey: 441 | del self[key] 442 | print('set:', (key, value)) 443 | else: 444 | print('add:', (key, value)) 445 | OrderedDict.__setitem__(self, key, value) 446 | ``` 447 | 448 | 解析一下上面的例子,我们实现了一个容量有限的先进先出的 `dict` —— `LastUpdatedOrderedDict`,在实例化时,`__init__()` 方法会被调用来初始化要创建的实例。`self` 参数指的便是要创建的实例本身,此外我们传入一个 `capacity` 参数用来表示这个 `dict` 的容量。需要注意,在这个 `__init__()` 方法中,我们使用 `super(LastUpdatedOrderedDict, self).__init__()` 这个语句来进行初始化,它会自动找到 `LastUpdatedOrderedDict` 的父类,把实例 `self` 转换为一个特殊的父类对象,从而可以调用父类的 `__init__()` 方法。`self._capacity = capacity` 这一句则是给实例绑定容量属性,使用 `_capacity` 这个变量名来和传入参数区别开来,前置单下划线表示这个属性应被视作私有属性来使用。 449 | 450 | 再看看 `__setitem__()` 方法,首先判断一下设置的key在 `dict` 里是否存在,如果存在则 `containsKey` 为1否则为0。接下来判断容量是否超出,如果超出则pop掉最前面的一个key-value对。然后再判断key是否存在,是则先删除掉原来的key-value对。最后调用父类的 `__setitem__()` 方法来完成赋值。 451 | 452 | --- 453 | 454 | ### Counter 455 | 456 | `Counter` 是一个简单的计数器,例如,统计字符出现的个数: 457 | 458 | ```python 459 | >>> from collections import Counter 460 | >>> c = Counter() 461 | >>> for ch in 'programming': 462 | ... c[ch] = c[ch] + 1 463 | ... 464 | >>> c 465 | Counter({'g': 2, 'm': 2, 'r': 2, 'a': 1, 'i': 1, 'o': 1, 'n': 1, 'p': 1}) 466 | ``` 467 | 468 | `Counter` 实际上也是 `dict` 的一个子类,上面的结果可以看出,字符 `'g'、'm'、'r'` 各出现了两次,其他字符各出现了一次。 469 | 470 | --- 471 | 472 | ### 小结 473 | 474 | `collections` 模块提供了一些有用的集合类,我们可以根据需要选用。 475 | 476 | --- 477 | 478 |
479 | 480 | ## base64 481 | 482 | ### 简介 483 | 484 | Base64是一种用64个字符来表示任意二进制数据的方法。 485 | 486 | 用记事本打开exe、jpg、pdf这些文件时,我们都会看到一大堆乱码,因为二进制文件包含很多无法显示和打印的字符,所以,如果要让记事本这样的文本处理软件能处理二进制数据,就需要一个二进制到字符串的转换方法。Base64是一种最常见的二进制编码方法。 487 | 488 | --- 489 | 490 | ### 原理 491 | 492 | Base64的原理很简单,首先,它使用总共64个字符进行编码(26个大小写字母+10个数字+加号+左斜杠): 493 | 494 | ```python 495 | ['A', 'B', 'C', ... 'a', 'b', 'c', ... '0', '1', ... '+', '/'] 496 | ``` 497 | 498 | 然后,对二进制数据进行处理时,每3个字节一组,就得到 `3x8=24 bit`,重新划分为4组,每组正好6个bit,有 `2^6=64` 种取值,对应64个字符的一个,: 499 | 500 | ![base64-encode](http://oe0e8k1nf.bkt.clouddn.com/base64-encode.png) 501 | 502 | 这样我们就可以把二进制数据的3个字节使用4个字符来表示,这就是Base64编码。采用Base64编码会把3字节的二进制数据编码为4字节(4个字符所以是4字节)的文本数据,长度会增加33%。但好处是编码后的文本数据可以在邮件正文、网页等直接显示,而不会出现乱码的情况。 503 | 504 | **如果要编码的二进制数据不是3的倍数,最后多出1个或2个字节怎么办呢?** Base64编码采用 `\x00` 字节在末尾补充到3个字节,在编码的末尾会使用 `=` 号表示补了多少字节。解码的时候,会自动去掉用于补足的 `\x00` 字节。 505 | 506 | --- 507 | 508 | ### Python中的实现方式 509 | 510 | Python内置的 `base64` 模块可以直接进行base64的编解码: 511 | 512 | ```python 513 | >>> import base64 514 | >>> base64.b64encode(b'binary\x00string') 515 | b'YmluYXJ5AHN0cmluZw==' 516 | >>> base64.b64decode(b'YmluYXJ5AHN0cmluZw==') 517 | b'binary\x00string' 518 | ``` 519 | 520 | 上面的代码中我们使用了 `b` 把字符串转为二进制,然后传入 `b64encode()` 函数进行编码,字符串长度为13个字节(注意 `\x00` 是一个字符,占1字节)。可以看到编码后所得字符串20个字节,并且使用两个 `=` 号表明编码过程中,由于13无法整除3,所以末尾补充了两个 `\x00`。**注意,`=` 号是算在20个字节(`15÷3×4=20 bit`)里面的,而不是额外放在编码后的字符串后面。** 521 | 522 | 由于标准的Base64编码后可能出现字符 `+` 和 `/`,在URL中就不能直接作为参数,所以又有一种 **"url safe" 的base64编码**,把字符 `+` 和 `/` 分别替换成 `-` 和 `_`: 523 | 524 | ```python 525 | >>> base64.b64encode(b'i\xb7\x1d\xfb\xef\xff') 526 | b'abcd++//' 527 | >>> base64.urlsafe_b64encode(b'i\xb7\x1d\xfb\xef\xff') 528 | b'abcd--__' 529 | >>> base64.urlsafe_b64decode('abcd--__') 530 | b'i\xb7\x1d\xfb\xef\xff' 531 | ``` 532 | 533 | 我们也可以自定义64个字符的排列顺序,也即自定义Base64编码,不过,通常情况下没有必要这样做。注意,**Base64仅仅是一种通过查表进行编码的方法,不能用于加密**,即使使用自定义的编码表也不行(依然能很容易被破解)。Base64**适用于小段内容的编码**,比如数字证书签名、Cookie的内容等。由于 `=` 字符也可能出现在Base64编码中,但 `=` 用在URL、Cookie里面会造成歧义,所以,很多人会在Base64编码后把 `=` 去掉: 534 | 535 | ```python 536 | # 标准Base64: 537 | 'abcd' -> 'YWJjZA==' 538 | # 自动去掉=: 539 | 'abcd' -> 'YWJjZA' 540 | ``` 541 | 542 | 那么去掉 `=` 后解码要怎样完成呢?不用担心,因为Base64是把3个字节变为4个字节,所以,Base64编码的长度一定是4的倍数,解码时我们只需要在末尾加上足够的 `=` 把Base64字符串的长度变回4的倍数,就可以正常解码了。 543 | 544 | --- 545 | 546 | ### 小结 547 | 548 | Base64是一种把任意二进制转换为文本字符串的编码方法,常用于在URL、Cookie、网页中传输少量二进制数据。 549 | 550 | --- 551 | 552 | ### 练习 553 | 554 | > 请写一个能兼容去掉 `=` 的base64编码字符串的解码函数: 555 | 556 | 代码: 557 | 558 | ```python 559 | # -*- coding: utf-8 -*- 560 | 561 | import base64 562 | 563 | def safe_base64_decode(s): 564 | 565 | remainder = len(s) % 4 566 | if remainder == 0: 567 | return base64.b64decode(s) 568 | else: 569 | return base64.b64decode(s+remainder*b'=') 570 | 571 | # 测试: 572 | assert b'abcd' == safe_base64_decode(b'YWJjZA=='), safe_base64_decode('YWJjZA==') 573 | assert b'abcd' == safe_base64_decode(b'YWJjZA'), safe_base64_decode('YWJjZA') 574 | print('Pass') 575 | ``` 576 | 577 | --- 578 | 579 |
580 | 581 | ## struct 582 | 583 | ### 为何需要struct 584 | 585 | 准确地讲,**Python没有专门处理字节的数据类型**。但由于 `b'str'` 可以用来表示字节,所以在Python中可以认为 `字节数组=二进制str`。而在C语言中,我们可以很方便地用 `struct`、`union` 等库来处理字节以及字节和int,float的转换。 586 | 587 | 假设我们要把一个32位无符号整数转换为字节(4个bytes)。在Python中,得这么写: 588 | 589 | ```python 590 | >>> n = 10240099 591 | >>> b1 = (n & 0xff000000) >> 24 592 | >>> b2 = (n & 0xff0000) >> 16 593 | >>> b3 = (n & 0xff00) >> 8 594 | >>> b4 = n & 0xff 595 | >>> bs = bytes([b1, b2, b3, b4]) 596 | >>> bs 597 | b'\x00\x9c@c' 598 | ``` 599 | 600 | 稍微解析一下,十进制数 `10240099` 转换为十六进制是 `0x009c4063`,这里使用与运算和右移来拆分出这个32位无符号整数的每一个byte(注意得到的不是字节数组而是一个整数),其中 `b1=0, b2=156, b3=64, b4=99`,将这四个整数放入一个列表中传入 `bytes()` 函数,就能得到字节数组了。注意 `bs` 中是有4个字节的,`len(bs)` 的值为4。由于对应整数64的十六进制数 `0x40` 属于ASCII码范围,所以用字符 `@` 表示,而对应整数99的十六进制数 `0x63` 则对应字符 `c`。 601 | 602 | 可以看到,这样要逐个byte来拆分,再进行转换实在是非常麻烦。如果要把浮点数转换为字节就无能为力了。幸好,Python提供了一个 `struct` 模块来解决 `bytes` 和其他二进制数据类型的转换问题。 603 | 604 | --- 605 | 606 | ### struct的用法 607 | 608 | `struct` 模块的 `pack` 函数把任意数据类型变成 `bytes`: 609 | 610 | ```python 611 | >>> import struct 612 | >>> struct.pack('>I', 10240099) 613 | b'\x00\x9c@c' 614 | ``` 615 | 616 | `pack()` 的第一个参数是**处理指令**,这里 `'>I'` 的意思分为两部分: 617 | 618 | - 字节顺序:`>` 表示的是使用大端序作为字节顺序。 619 | - 数据类型:`I` 表示的是要转换一个32位无符号整数。可以有多个数据类型从而转换出多个数。 620 | 621 | 第二个参数要注意和处理指令一致。 622 | 623 | `unpack()` 函数与 `pack()` 相反,它是把 `bytes` 转换为相应的数据类型: 624 | 625 | ```python 626 | >>> struct.unpack('>IH', b'\xf0\xf0\xf0\xf0\x80\x80') 627 | (4042322160, 32896) 628 | ``` 629 | 630 | 这里的 `>IH` 表示这个字节数组用的是大端序,并且要依次转换出一个32位无符号整数和16位无符号整数。 631 | 632 | `struct` 模块定义的数据类型可以参考[Python官方文档](https://docs.python.org/3/library/struct.html#format-characters)。关于大端序(big-endian,BE)和小端序(little-endian,LE)的区别可以看看[这篇博文](http://www.cnblogs.com/passingcloudss/archive/2011/05/03/2035273.html),讲解得很清晰。 633 | 634 | --- 635 | 636 | ### 使用struct分析bmp文件 637 | 638 | Windows的位图文件(.bmp)是一种非常简单的文件格式,我们可以使用 `struct` 来分析一下。 639 | 640 | 首先找到一个bmp文件,没有的话用Windows自带的【画图】画一个即可。读入它的前30个字节来分析: 641 | 642 | ```python 643 | >>> 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\x00' 644 | ``` 645 | 646 | BMP格式采用小端序的方式存储数据,文件头的结构按顺序如下: 647 | 648 | - 两个字节:'BM'表示Windows位图,'BA'表示OS/2位图; 649 | - 一个32位无符号整数:表示位图大小; 650 | - 一个32位无符号整数:保留位,始终为0; 651 | - 一个32位无符号整数:实际图像的偏移量; 652 | - 一个32位无符号整数:Header的字节数; 653 | - 一个32位无符号整数:图像宽度(单位为像素); 654 | - 一个32位无符号整数:图像高度(单位为像素); 655 | - 一个16位无符号整数:始终为1; 656 | - 一个16位无符号整数:颜色数。 657 | 658 | 所以,组合起来用unpack读取: 659 | 660 | ```python 661 | >>> struct.unpack(' 请编写一个bmpinfo.py,可以检查任意文件是否是Windows位图文件,如果是,打印出图片大小和颜色数。 678 | 679 | 代码: 680 | 681 | ```python 682 | #-*- coding: utf-8 -*- 683 | 684 | import sys 685 | import struct 686 | 687 | def readBmpFile(file): 688 | 689 | f = open(file, 'rb') 690 | bs = f.read() 691 | f.close() 692 | return bs[0:30] 693 | 694 | def checkBmp(info): 695 | 696 | ts = struct.unpack(' 716 | 717 | ## hashlib 718 | 719 | ### 摘要算法简介 720 | 721 | Python的 `hashlib` 模块提供了常见的摘要算法,如MD5,SHA1等等。 722 | 723 | 什么是摘要算法呢?摘要算法又称哈希算法、散列算法。它通过一个摘要函数(也称哈希函数),**把任意长度的数据转换为一个固定长度的数据串**(称为**摘要(digest)**,通常表示为由16进制数字组成的字符串)。 724 | 725 | 摘要函数应当是一个单向函数,也即计算摘要容易,但通过摘要反推原始数据却非常困难。并且即使仅对原始数据做一个bit的修改也会导致计算出的摘要完全不同。 726 | 727 | --- 728 | 729 | ### Python实现 730 | 731 | 以常见的摘要算法MD5为例,计算一个字符串的MD5值: 732 | 733 | ```python 734 | import hashlib 735 | 736 | md5 = hashlib.md5() 737 | md5.update('how to use md5 in python hashlib?'.encode('utf-8')) 738 | print(md5.hexdigest()) 739 | ``` 740 | 741 | 计算结果如下: 742 | 743 | ```python 744 | d26a53750bc40b38b65a520292f69306 745 | ``` 746 | 747 | 如果数据量很大,我们可以多次调用 `update()` 来传入新数据。只要保证输入一致,那么最后计算的结果一定都是一样的: 748 | 749 | ```python 750 | import hashlib 751 | 752 | md5 = hashlib.md5() 753 | md5.update('how to use md5 in '.encode('utf-8')) 754 | md5.update('python hashlib?'.encode('utf-8')) 755 | print(md5.hexdigest()) 756 | ``` 757 | 758 | 但只要改动一个字母,计算的结果就会完全不同。 759 | 760 | MD5是最常见的摘要算法,速度很快,所得摘要长度为128 bit,通常用一个32位的16进制字符串表示。 761 | 762 | 另一种常见的摘要算法是SHA1,调用SHA1和调用MD5完全类似: 763 | 764 | ```python 765 | import hashlib 766 | 767 | sha1 = hashlib.sha1() 768 | sha1.update('how to use sha1 in '.encode('utf-8')) 769 | sha1.update('python hashlib?'.encode('utf-8')) 770 | print(sha1.hexdigest()) 771 | ``` 772 | 773 | SHA1所得摘要长度为160 bit,通常用一个40位的16进制字符串表示。 774 | 775 | 比SHA1更安全的算法是SHA256和SHA512,不过越安全的算法不仅越慢,而且摘要的长度也更长。 776 | 777 | **有没有可能两个不同的数据通过某个摘要算法得到了相同的摘要?完全有可能。**因为任何摘要算法都是把无限多的数据集合映射到一个有限的集合中。这种情况称为**碰撞(collasion)**。 778 | 779 | --- 780 | 781 | ### 摘要算法应用 782 | 783 | 摘要算法能应用到什么地方?举个常用例子: 784 | 785 | 任何允许用户登录的网站都会存储用户登录的用户名和口令。如何存储用户名和口令呢?方法是存到数据库表中: 786 | 787 | | name | password | 788 | |:-:|:-:| 789 | | michael | 123456 | 790 | | bob | abc999 | 791 | | alice | alice2008 | 792 | 793 | 如果以明文保存用户口令,一旦数据库泄露,所有用户的口令就会落入黑客的手里。此外,网站运维人员是可以访问数据库的,如果他们心有不轨,那么也会很容易地获取到所有用户的口令。 794 | 795 | 正确的保存口令的方式是不存储用户的明文口令,而是存储用户口令的摘要,比如MD5: 796 | 797 | | username | password | 798 | |:-:|:-:| 799 | | michael | e10adc3949ba59abbe56e057f20f883e | 800 | | bob | 878ef96e86145580c38c87f0410ad153 | 801 | | alice | 99b1c2188db85afee403b1536010c2c9 | 802 | 803 | 当用户登录时,首先计算用户输入的明文口令的MD5,然后和数据库存储的MD5对比,如果一致,说明口令输入正确,如果不一致,口令肯定错误。 804 | 805 | --- 806 | 807 | ### 练习一 808 | 809 | > 根据用户输入的口令计算出对应的MD5值,并验证是否与数据库中保存的一致 810 | 811 | 代码: 812 | 813 | ```python 814 | #-*- coding: utf-8 -*- 815 | 816 | import sys 817 | import hashlib 818 | 819 | db = { 820 | 'michael': 'e10adc3949ba59abbe56e057f20f883e', 821 | 'bob': '878ef96e86145580c38c87f0410ad153', 822 | 'alice': '99b1c2188db85afee403b1536010c2c9' 823 | } 824 | 825 | def calc_md5(password): 826 | 827 | md5 = hashlib.md5() 828 | md5.update(password.encode('utf-8')) 829 | return md5.hexdigest() 830 | 831 | def login(user, password): 832 | 833 | digest = calc_md5(password) 834 | if db[user] == digest: 835 | return True 836 | else: 837 | return False 838 | 839 | if __name__=='__main__': 840 | user = input('请输入用户名:') 841 | password = input('请输入密码:') 842 | 843 | if login(user, password): 844 | print('密码正确,登录成功。') 845 | else: 846 | print('密码错误,登录失败。') 847 | ``` 848 | 849 | 采用MD5存储口令是否就一定安全呢?也不一定。假设你是一个黑客,已经拿到了存储MD5口令的数据库,如何通过MD5反推用户的明文口令呢?暴力破解费事费力,真正的黑客不会这么干。 850 | 851 | 考虑这么个情况,很多用户喜欢用123456,888888,password这些简单的口令。于是,黑客可以事先计算出这些常用口令的MD5值,得到一个反推表: 852 | 853 | ```python 854 | 'e10adc3949ba59abbe56e057f20f883e': '123456' 855 | '21218cca77804d2ba1922c33e0151105': '888888' 856 | '5f4dcc3b5aa765d61d8327deb882cf99': 'password' 857 | ``` 858 | 859 | 这样,无需破解,只需要对比数据库的MD5,黑客就获得了使用常用口令的用户账号。 860 | 861 | --- 862 | 863 | ### 加盐 864 | 865 | 对于用户来讲,当然不应该使用过于简单的口令。但是,我们能否**在程序设计上对简单口令加强保护**呢? 866 | 867 | 由于常用口令的MD5值很容易被计算出来,为了加强保护,我们可以对原始口令添加一个复杂字符串(俗称“盐”),这样再计算MD5值就不容易被黑客蒙中了,这种方法俗称**“加盐”**: 868 | 869 | ```python 870 | def calc_md5(password): 871 | 872 | md5 = hashlib.md5() 873 | md5.update(password.encode('utf-8')) 874 | md5.update('the-Salt'.encode('utf-8')) 875 | return md5.hexdigest() 876 | ``` 877 | 878 | 经过加盐处理的MD5口令,只要盐不被黑客知道,即使用户使用简单口令,也很难通过MD5被反推出来。 879 | 880 | 但是如果有两个用户都使用了相同的简单口令比如123456,数据库中就会存储两条相同的MD5值,这说明这两个用户的口令是一样的。有没有办法**让使用相同口令的用户存储不同的MD5**呢?如果假定用户无法修改登录名,就可以通过把登录名作为“盐”的一部分来计算MD5,从而实现相同口令的用户有不同的MD5值。 881 | 882 | --- 883 | 884 | ### 练习二 885 | 886 | > 根据用户输入的登录名和口令模拟用户注册,计算更安全的MD5: 887 | 888 | ```python 889 | db = {} 890 | 891 | def register(username, password): 892 | db[username] = calc_md5(password + username + 'the-Salt') 893 | 894 | def calc_md5(password): 895 | 896 | md5 = hashlib.md5() 897 | md5.update(password.encode('utf-8')) 898 | return md5.hexdigest() 899 | 900 | ``` 901 | 902 | > 根据修改后的MD5算法实现用户登录的验证: 903 | 904 | ```python 905 | def login(user, password): 906 | 907 | digest = calc_md5(password + username + 'the-Salt') 908 | if db[user] == digest: 909 | return True 910 | else: 911 | return False 912 | ``` 913 | 914 | --- 915 | 916 | ### 小结 917 | 918 | 摘要算法在很多地方都有广泛的应用。要注意**摘要算法不是加密算法,不能用于加密**(因为无法通过摘要反推明文),只能用于防篡改,但是它的单向计算特性决定了可以在不存储明文口令的情况下验证用户口令。 919 | 920 | --- 921 | 922 |
923 | 924 | -------------------------------------------------------------------------------- /My_Python_Notebook/13常用第三方模块.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/familyld/learnpython/0ffd63d401c8474415aa9f5a3a18074ac48738d9/My_Python_Notebook/13常用第三方模块.md -------------------------------------------------------------------------------- /My_Python_Notebook/14virtualenv.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/familyld/learnpython/0ffd63d401c8474415aa9f5a3a18074ac48738d9/My_Python_Notebook/14virtualenv.md -------------------------------------------------------------------------------- /My_Python_Notebook/15图形界面.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/familyld/learnpython/0ffd63d401c8474415aa9f5a3a18074ac48738d9/My_Python_Notebook/15图形界面.md -------------------------------------------------------------------------------- /My_Python_Notebook/16网络编程.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/familyld/learnpython/0ffd63d401c8474415aa9f5a3a18074ac48738d9/My_Python_Notebook/16网络编程.md -------------------------------------------------------------------------------- /My_Python_Notebook/17电子邮件.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/familyld/learnpython/0ffd63d401c8474415aa9f5a3a18074ac48738d9/My_Python_Notebook/17电子邮件.md -------------------------------------------------------------------------------- /My_Python_Notebook/18访问数据库.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/familyld/learnpython/0ffd63d401c8474415aa9f5a3a18074ac48738d9/My_Python_Notebook/18访问数据库.md -------------------------------------------------------------------------------- /My_Python_Notebook/19Web开发.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/familyld/learnpython/0ffd63d401c8474415aa9f5a3a18074ac48738d9/My_Python_Notebook/19Web开发.md -------------------------------------------------------------------------------- /My_Python_Notebook/20异步IO.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/familyld/learnpython/0ffd63d401c8474415aa9f5a3a18074ac48738d9/My_Python_Notebook/20异步IO.md -------------------------------------------------------------------------------- /My_Python_Notebook/README.md: -------------------------------------------------------------------------------- 1 | # Python学习笔记 2 | 3 | 该笔记记录的是学习[廖雪峰Python3教程](http://www.liaoxuefeng.com/wiki/0014316089557264a6b348958f449949df42a6d3a2e542c000#0)的过程,摘录了一些重点,重新编排内容,并加入了更丰富的代码示例和对学习过程中所遇到问题的理解。 4 | 5 | #### [01Python基础](https://github.com/familyld/learnpython/blob/master/My_Python_Notebook/01Python基础.md) 6 | 7 | #### [02函数](https://github.com/familyld/learnpython/blob/master/My_Python_Notebook/02函数.md) 8 | 9 | #### [03高级特性](https://github.com/familyld/learnpython/blob/master/My_Python_Notebook/03高级特性.md) 10 | 11 | #### [04函数式编程](https://github.com/familyld/learnpython/blob/master/My_Python_Notebook/04函数式编程.md) 12 | 13 | #### [05模块](https://github.com/familyld/learnpython/blob/master/My_Python_Notebook/05模块.md) 14 | 15 | #### [06面向对象编程](https://github.com/familyld/learnpython/blob/master/My_Python_Notebook/06面向对象编程.md) 16 | 17 | #### [07面向对象高级编程](https://github.com/familyld/learnpython/blob/master/My_Python_Notebook/07面向对象高级编程.md) 18 | 19 | #### [08错误、调试与测试](https://github.com/familyld/learnpython/blob/master/My_Python_Notebook/08错误、调试与测试.md) 20 | 21 | #### [09IO编程](https://github.com/familyld/learnpython/blob/master/My_Python_Notebook/09IO编程.md) 22 | 23 | #### [10进程和线程](https://github.com/familyld/learnpython/blob/master/My_Python_Notebook/10进程和线程.md) 24 | 25 | #### [11正则表达式](https://github.com/familyld/learnpython/blob/master/My_Python_Notebook/11正则表达式.md) 26 | 27 | #### [12常用内建模块](https://github.com/familyld/learnpython/blob/master/My_Python_Notebook/12常用内建模块.md) 28 | 29 | #### [13常用第三方模块](https://github.com/familyld/learnpython/blob/master/My_Python_Notebook/13常用第三方模块.md) 30 | 31 | #### [14virtualenv](https://github.com/familyld/learnpython/blob/master/My_Python_Notebook/14virtualenv.md) 32 | 33 | #### [15图形界面](https://github.com/familyld/learnpython/blob/master/My_Python_Notebook/15图形界面.md) 34 | 35 | #### [16网络编程](https://github.com/familyld/learnpython/blob/master/My_Python_Notebook/16网络编程.md) 36 | 37 | #### [17电子邮件](https://github.com/familyld/learnpython/blob/master/My_Python_Notebook/17电子邮件.md) 38 | 39 | #### [18访问数据库](https://github.com/familyld/learnpython/blob/master/My_Python_Notebook/18访问数据库.md) 40 | 41 | #### [19Web开发](https://github.com/familyld/learnpython/blob/master/My_Python_Notebook/19Web开发.md) 42 | 43 | #### [20异步IO](https://github.com/familyld/learnpython/blob/master/My_Python_Notebook/20异步IO.md) 44 | 45 | 46 | -------------------------------------------------------------------------------- /Numpy_Learning.md: -------------------------------------------------------------------------------- 1 | # Numpy 学习 2 | 3 | 该笔记摘录自微信公众号“每天进步一点点2015”的文章[《Python数据分析之numpy学习(一)》](http://mp.weixin.qq.com/s?__biz=MzIxNjA2ODUzNg==&mid=2651435143&idx=1&sn=fb54e5d9129492a24a14fef71bf05af8#rd)和[《Python数据分析之numpy学习(二)》](http://mp.weixin.qq.com/s?__biz=MzIxNjA2ODUzNg==&mid=2651435144&idx=1&sn=5286ef757ed3acdadd6485d196b931b0#rd)。我对代码和讲解中不够清晰的地方进行了一些改动和补充。 4 | 5 | Python模块中的numpy,这是一个处理数组的强大模块,而该模块也是其他数据分析模块(如pandas和scipy)的核心。下面将从这5个方面来介绍numpu模块的内容: 6 | 7 | 1. 数组的创建 8 | 2. 有关数组的属性和函数 9 | 3. 数组元素的获取--普通索引、切片、布尔索引和花式索引 10 | 4. 统计函数与线性代数运算 11 | 5. 随机数的生成 12 | 13 | ## 数组的创建 14 | 15 | ### 一维数组的创建 16 | 17 | 可以使用numpy中的arange()函数创建一维有序数组,它是内置函数range的扩展版。 18 | 19 | ```python 20 | In [1]: import numpy as np 21 | In [2]: ls1 = range(10) 22 | In [3]: list(ls1) 23 | Out[3]: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] 24 | In [4]: type(ls1) 25 | Out[4]: range 26 | 27 | In [5]: ls2 = np.arange(10) 28 | In [6]: list(ls2) 29 | Out[6]: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] 30 | In [7]: type(ls2) 31 | Out[7]: numpy.ndarray 32 | ``` 33 | 34 | 通过arange生成的序列就不是简简单单的列表类型了,而是一个一维数组。 35 | 36 | 如果一维数组不是一个规律的有序元素,而是人为的输入,就需要array()函数创建了。numpy中**使用array()函数创建数组,array的首个参数一定是一个序列,可以是元组也可以是列表**。 37 | 38 | ```python 39 | In [8]: arr1 = np.array((1,20,13,28,22)) 40 | In [9]: arr1 41 | Out[9]: array([ 1, 20, 13, 28, 22]) 42 | In [10]: type(arr1) 43 | Out[10]: numpy.ndarray 44 | ``` 45 | 46 | 上面是由元组序列构成的一维数组。 47 | 48 | ```python 49 | In [11]: arr2 = np.array([1,1,2,3,5,8,13,21]) 50 | In [12]: arr2 51 | Out[12]: array([ 1, 1, 2, 3, 5, 8, 13, 21]) 52 | In [13]: type(arr2) 53 | Out[13]: numpy.ndarray 54 | ``` 55 | 56 | 上面是由列表序列构成的一维数组。 57 | 58 | ### 二维数组的创建 59 | 60 | 二维数组的创建,其实在就是**列表套列表或元组套元组** 61 | ```python 62 | In [14]: arr3 = np.array(((1,1,2,3),(5,8,13,21),(34,55,89,144))) 63 | In [15]: arr3 64 | Out[15]: 65 | array([[ 1, 1, 2, 3], 66 | [ 5, 8, 13, 21], 67 | [ 34, 55, 89, 144]]) 68 | ``` 69 | 70 | 上面是使用元组套元组的方式。 71 | 72 | ```python 73 | In [16]: arr4 = np.array([[1,2,3,4],[5,6,7,8],[9,10,11,12]]) 74 | In [17]: arr4 75 | Out[17]: 76 | array([[ 1, 2, 3, 4], 77 | [ 5, 6, 7, 8], 78 | [ 9, 10, 11, 12]]) 79 | ``` 80 | 81 | 上面是使用列表套列表的方式。 82 | 83 | 对于高维数组在将来的数据分析中用的比较少,这里关于高维数组的创建就不赘述了,构建方法仍然是使用嵌套的方式。 84 | 85 | 上面所介绍的都是人为设定的一维、二维或高维数组,numpy中也提供了几种特殊的数组,它们是: 86 | 87 | ```python 88 | In [18]: np.ones(3) #返回一维元素全为1的数组 89 | Out[18]: array([ 1., 1., 1.]) 90 | 91 | In [19]: np.ones([3,4]) #返回元素全为1的3×4二维数组 92 | Out[19]: 93 | array([[ 1., 1., 1., 1.], 94 | [ 1., 1., 1., 1.], 95 | [ 1., 1., 1., 1.]]) 96 | 97 | In [20]: np.zeros(3) #返回一维元素全为0的数组 98 | Out[20]: array([ 0., 0., 0.]) 99 | 100 | In [21]: np.zeros([3,4]) #返回元素全为0的3×4二维数组 101 | Out[21]: 102 | array([[ 0., 0., 0., 0.], 103 | [ 0., 0., 0., 0.], 104 | [ 0., 0., 0., 0.]]) 105 | 106 | In [22]: np.empty(3) #返回一维空数组 107 | Out[22]: array([ 1.13224202e+277, 6.00769955e-307, 2.55195390e-303]) 108 | 109 | In [23]: np.empty([3,4]) #返回3×4二维空数组 110 | Out[23]: 111 | array([[ 1.09494983e-311, 8.19904309e-314, 1.09678092e-311, 1.09494983e-311], 112 | [ 2.09290548e-313, 1.09678288e-311, 1.09494983e-311, 3.36629926e-313], 113 | [ 1.09678538e-311, 4.03179200e-313, 1.09678287e-311, 1.09494983e-311]]) 114 | ``` 115 | 116 | 原文中对于`np.empty`的举例不够严谨,这个函数返回的是指定规模未经初始化的数组,速度要比 `ones` 和 `zeros` 快,但里面的值都是随机的,要谨慎使用。 117 | 118 | ## 有关数组的属性和函数 119 | 120 | 当一个数组构建好后,我们看看关于数组本身的操作又有哪些属性和函数: 121 | 122 | ```python 123 | In [24]: arr3 124 | Out[24]: 125 | array([[ 1, 1, 2, 3], 126 | [ 5, 8, 13, 21], 127 | [ 34, 55, 89, 144]]) 128 | 129 | In [25]: arr3.shape #shape方法返回数组的行数和列数 130 | Out[25]: (3, 4) 131 | 132 | In [26]: arr3.dtype #dtype方法返回数组的数据类型 133 | Out[26]: dtype('int32') 134 | 135 | In [27]: a = arr3.ravel() #通过ravel的方法将数组拉直(多维数组降为一维数组) 136 | In [28]: a 137 | Out[28]: array([ 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144]) 138 | 139 | In [29]: b = arr3.flatten() #通过flatten的方法将数组拉直 140 | In [30]: b 141 | Out[30]: array([ 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144]) 142 | ``` 143 | 144 | 两者的区别在于**ravel方法生成的是原数组的视图,无需占有内存空间,但视图的改变会影响到原数组的变化。而flatten方法返回的是真实值,其值的改变并不会影响原数组的更改**。 145 | 146 | 通过下面的例子也许就能明白了: 147 | 148 | ```python 149 | In [31]: b[:3] = 0 150 | In [32]: arr3 151 | Out[32]: 152 | array([[ 1, 1, 2, 3], 153 | [ 5, 8, 13, 21], 154 | [ 34, 55, 89, 144]]) 155 | ``` 156 | 157 | 通过更改b的值,原数组没有变化。 158 | 159 | ```python 160 | In [33]: a[:3] = 0 161 | In [34]: arr3 162 | Out[34]: 163 | array([[ 0, 0, 0, 3], 164 | [ 5, 8, 13, 21], 165 | [ 34, 55, 89, 144]]) 166 | ``` 167 | 168 | a的值变化后,会导致原数组跟着变化。 169 | 170 | ```python 171 | In [35]: arr4 172 | Out[35]: 173 | array([[ 1, 2, 3, 4], 174 | [ 5, 6, 7, 8], 175 | [ 9, 10, 11, 12]]) 176 | 177 | In [36]: arr4.ndim #返回数组的维数(嵌套的层数) 178 | Out[36]: 2 179 | 180 | In [37]: arr4.size #返回数组元素的个数 181 | Out[37]: 12 182 | 183 | In [38]: arr4.T #返回数组的转置结果 184 | Out[38]: 185 | array([[ 1, 5, 9], 186 | [ 2, 6, 10], 187 | [ 3, 7, 11], 188 | [ 4, 8, 12]]) 189 | ``` 190 | 191 | **如果数组的数据类型为复数**的话,real方法可以返回复数的实部,imag方法返回复数的虚部。 192 | 193 | 介绍完数组的一些**方法**后,接下来我们看看数组自身有哪些**函数**可操作: 194 | 195 | ```python 196 | In [39]: len(arr4) #返回数组有多少行 197 | Out[39]: 3 198 | 199 | In [40]: arr3 200 | Out[40]: 201 | array([[ 0, 0, 0, 3], 202 | [ 5, 8, 13, 21], 203 | [ 34, 55, 89, 144]]) 204 | 205 | In [41]: arr4 206 | Out[41]: 207 | array([[ 1, 2, 3, 4], 208 | [ 5, 6, 7, 8], 209 | [ 9, 10, 11, 12]]) 210 | 211 | In [42]: np.hstack((arr3,arr4)) 212 | Out[42]: 213 | array([[ 0, 0, 0, 3, 1, 2, 3, 4], 214 | [ 5, 8, 13, 21, 5, 6, 7, 8], 215 | [ 34, 55, 89, 144, 9, 10, 11, 12]]) 216 | ``` 217 | 218 | **横向拼接**arr3和arr4两个数组,但必须满足两个数组的**行数相同**。 219 | 220 | ```python 221 | In [43]: np.vstack((arr3,arr4)) 222 | Out[43]: 223 | array([[ 0, 0, 0, 3], 224 | [ 5, 8, 13, 21], 225 | [ 34, 55, 89, 144], 226 | [ 1, 2, 3, 4], 227 | [ 5, 6, 7, 8], 228 | [ 9, 10, 11, 12]]) 229 | ``` 230 | 231 | **纵向拼接**arr3和arr4两个数组,但必须满足两个数组的**列数相同**。 232 | 233 | ```python 234 | In [44]: np.column_stack((arr3,arr4)) #与hstack函数具有一样的效果 235 | Out[44]: 236 | array([[ 0, 0, 0, 3, 1, 2, 3, 4], 237 | [ 5, 8, 13, 21, 5, 6, 7, 8], 238 | [ 34, 55, 89, 144, 9, 10, 11, 12]]) 239 | 240 | In [45]: np.row_stack((arr3,arr4)) #与vstack函数具有一样的效果 241 | Out[45]: 242 | array([[ 0, 0, 0, 3], 243 | [ 5, 8, 13, 21], 244 | [ 34, 55, 89, 144], 245 | [ 1, 2, 3, 4], 246 | [ 5, 6, 7, 8], 247 | [ 9, 10, 11, 12]]) 248 | ``` 249 | 250 | reshape()函数和resize()函数可以重新设置数组的行数和列数: 251 | 252 | ```python 253 | In [46]: arr5 = np.arange(24) 254 | In [47]: arr5 #此为一维数组 255 | Out[47]: 256 | array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23]) 257 | 258 | In [48]: a = arr5.reshape(4,6) 259 | In [49]: a 260 | Out[49]: 261 | array([[ 0, 1, 2, 3, 4, 5], 262 | [ 6, 7, 8, 9, 10, 11], 263 | [12, 13, 14, 15, 16, 17], 264 | [18, 19, 20, 21, 22, 23]]) 265 | ``` 266 | 267 | 通过reshape函数将一维数组设置为二维数组,且为4行6列的数组。**reshape函数不会改变原数组**。 268 | 269 | ```python 270 | In [50]: a.resize(6,4) 271 | In [51]: a 272 | Out[51]: 273 | array([[ 0, 1, 2, 3], 274 | [ 4, 5, 6, 7], 275 | [ 8, 9, 10, 11], 276 | [12, 13, 14, 15], 277 | [16, 17, 18, 19], 278 | [20, 21, 22, 23]]) 279 | ``` 280 | 281 | 通过**resize函数会直接改变原数组的形状**。 282 | 283 | 数组转换:tolist将数组转换为列表,astype()强制转换数组的数据类型,下面是两个函数的例子: 284 | 285 | ```python 286 | In [53]: b = a.tolist() 287 | In [54]: b 288 | Out[54]: 289 | [[0, 1, 2, 3], 290 | [4, 5, 6, 7], 291 | [8, 9, 10, 11], 292 | [12, 13, 14, 15], 293 | [16, 17, 18, 19], 294 | [20, 21, 22, 23]] 295 | In [55]: type(b) 296 | Out[55]: list 297 | 298 | In [56]: c = a.astype(float) 299 | In [57]: c 300 | Out[57]: 301 | array([[ 0., 1., 2., 3.], 302 | [ 4., 5., 6., 7.], 303 | [ 8., 9., 10., 11.], 304 | [ 12., 13., 14., 15.], 305 | [ 16., 17., 18., 19.], 306 | [ 20., 21., 22., 23.]]) 307 | 308 | In [58]: a.dtype 309 | Out[58]: dtype('int32') 310 | In [59]: c.dtype 311 | Out[59]: dtype('float64') 312 | ``` 313 | 314 | ## 数组元素的获取 315 | 316 | 通过索引和切片的方式获取数组元素,一维数组元素的获取与列表、元组的获取方式一样: 317 | 318 | ```python 319 | In [60]: arr7 = np.arange(10) 320 | In [61]: arr7 321 | Out[61]: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) 322 | In [62]: arr7[3] #获取第4个元素 323 | Out[62]: 3 324 | 325 | In [63]: arr7[:3] #获取前3个元素 326 | Out[63]: array([0, 1, 2]) 327 | 328 | In [64]: arr7[3:] #获取第4个元素即之后的所有元素 329 | Out[64]: array([3, 4, 5, 6, 7, 8, 9]) 330 | 331 | In [65]: arr7[-2:] #获取末尾的2个元素 332 | Out[65]: array([8, 9]) 333 | 334 | In [66]: arr7[::2] #从第1个元素开始,获取步长为2的所有元素 335 | Out[66]: array([0, 2, 4, 6, 8]) 336 | ``` 337 | 338 | 补充一下,如果要获取多个元素,且它们的索引没有固定步长这样的规则的话,可以**传入一个list作为索引**: 339 | 340 | ```python 341 | >>> a = np.arange(5) 342 | >>> a 343 | array([0, 1, 2, 3, 4]) 344 | >>> a[[0,3,4]] #返回数组的第1,4,5个元素 345 | array([0, 3, 4]) 346 | ``` 347 | 348 | 二维数组元素的获取: 349 | 350 | ```python 351 | In [67]: arr8 = np.arange(12).reshape(3,4) 352 | In [68]: arr8 353 | Out[68]: 354 | array([[ 0, 1, 2, 3], 355 | [ 4, 5, 6, 7], 356 | [ 8, 9, 10, 11]]) 357 | 358 | In [69]: arr8[1] #返回数组的第2行 359 | Out[69]: array([4, 5, 6, 7]) 360 | 361 | In [70]: arr8[:2] #返回数组的前2行 362 | Out[70]: 363 | array([[0, 1, 2, 3], 364 | [4, 5, 6, 7]]) 365 | 366 | In [71]: arr8[[0,2]] #返回指定的第1行和第3行 367 | Out[71]: 368 | array([[ 0, 1, 2, 3], 369 | [ 8, 9, 10, 11]]) 370 | 371 | In [72]: arr8[:,0] #返回数组的第1列 372 | Out[72]: array([0, 4, 8]) 373 | 374 | In [73]: arr8[:,-2:] #返回数组的后2列 375 | Out[73]: 376 | array([[ 2, 3], 377 | [ 6, 7], 378 | [10, 11]]) 379 | 380 | In [74]: arr8[:,[0,2]] #返回数组的第1列和第3列 381 | Out[74]: 382 | array([[ 0, 2], 383 | [ 4, 6], 384 | [ 8, 10]]) 385 | 386 | In [75]: arr8[1,2] #返回数组中第2行第3列对应的元素 387 | Out[75]: 6 388 | ``` 389 | 390 | **布尔索引,即索引值为True和False,需要注意的是布尔索引必须是数组对象。** 391 | 392 | ```python 393 | In [76]: log = np.array([True,False,False,True,True,False]) 394 | In [77]: arr9 = np.arange(24).reshape(6,4) 395 | In [78]: arr9 396 | Out[78]: 397 | array([[ 0, 1, 2, 3], 398 | [ 4, 5, 6, 7], 399 | [ 8, 9, 10, 11], 400 | [12, 13, 14, 15], 401 | [16, 17, 18, 19], 402 | [20, 21, 22, 23]]) 403 | 404 | In [79]: arr9[log] #返回所有为True的对应行 405 | Out[79]: 406 | array([[ 0, 1, 2, 3], 407 | [12, 13, 14, 15], 408 | [16, 17, 18, 19]]) 409 | 410 | In [80]: arr9[-log] #通过负号筛选出所有为False的对应行 411 | Out[80]: 412 | array([[ 4, 5, 6, 7], 413 | [ 8, 9, 10, 11], 414 | [20, 21, 22, 23]]) 415 | ``` 416 | 417 | 举一个场景,一维数组表示区域,二维数组表示观测值,如何选取目标区域的观测? 418 | 419 | ```python 420 | In [81]: area = np.array(['A','B','A','C','A','B','D']) 421 | In [82]: area 422 | Out[82]: 423 | array(['A', 'B', 'A', 'C', 'A', 'B', 'D'], 424 | dtype='>> a = np.arange(10).reshape(2,5) 504 | >>> a 505 | array([[0, 1, 2, 3, 4], 506 | [5, 6, 7, 8, 9]]) 507 | 508 | >>> a[:,[0,4]] 509 | array([[0, 4], 510 | [5, 9]]) 511 | 512 | >>> a[[0,0,1,1],[0,4,0,4]] 513 | array([0, 4, 5, 9]) 514 | ``` 515 | 516 | 如果想使用比较简单的方式**返回指定行与列的二维数组**的话,可以使用ix_()函数 517 | 518 | ```python 519 | In [93]: arr10[np.ix_([4,1,5],[0,2,3])] # 允许我们用指定位置的方式,并且保持返回的数组形状不变 520 | Out[93]: 521 | array([[17, 19, 20], 522 | [ 5, 7, 8], 523 | [21, 23, 24]]) 524 | ``` 525 | 526 | 这与 `arr10[[4,1,5]][:,[0,2,3]]` 返回的结果是一致的。 527 | 528 | ## 统计函数与线性代数运算 529 | 530 | ### 统计函数 531 | 532 | 统计运算中常见的聚合函数有:最小值、最大值、中位数、均值、方差、标准差等。首先来看看数组元素级别的计算: 533 | 534 | ```python 535 | In [94]: arr11 = 5-np.arange(1,13).reshape(4,3) 536 | # randint返回指定区间的随机整数,区间左闭右开,size不指定时为None,返回1个随机整数 537 | In [95]: arr12 = np.random.randint(1,10,size = 12).reshape(4,3) 538 | In [96]: arr11 539 | Out[96]: 540 | array([[ 4, 3, 2], 541 | [ 1, 0, -1], 542 | [-2, -3, -4], 543 | [-5, -6, -7]]) 544 | 545 | In [97]: arr12 546 | Out[97]: 547 | array([[1, 3, 7], 548 | [7, 3, 7], 549 | [3, 7, 4], 550 | [6, 1, 2]]) 551 | 552 | In [98]: arr11 ** 2 #计算每个元素的平方 553 | Out[98]: 554 | array([[16, 9, 4], 555 | [ 1, 0, 1], 556 | [ 4, 9, 16], 557 | [25, 36, 49]]) 558 | 559 | In [99]: np.sqrt(arr11) #计算每个元素的平方根 560 | Out[99]: 561 | array([[ 2. , 1.73205081, 1.41421356], 562 | [ 1. , 0. , nan], 563 | [ nan, nan, nan], 564 | [ nan, nan, nan]]) 565 | ``` 566 | 567 | 由于负值的平方根没有意义,故返回nan。 568 | 569 | ```python 570 | In [100]: np.exp(arr11) #计算每个元素的指数值,exp(a) 即 e^a 571 | Out[100]: 572 | array([[ 5.45981500e+01, 2.00855369e+01, 7.38905610e+00], 573 | [ 2.71828183e+00, 1.00000000e+00, 3.67879441e-01], 574 | [ 1.35335283e-01, 4.97870684e-02, 1.83156389e-02], 575 | [ 6.73794700e-03, 2.47875218e-03, 9.11881966e-04]]) 576 | 577 | In [101]: np.log(arr12) #计算每个元素的自然对数值,log(a) 即 log_e(a),还有log2和log10两个函数 578 | Out[101]: 579 | array([[ 0. , 1.09861229, 1.94591015], 580 | [ 1.94591015, 1.09861229, 1.94591015], 581 | [ 1.09861229, 1.94591015, 1.38629436], 582 | [ 1.79175947, 0. , 0.69314718]]) 583 | 584 | In [102]: np.abs(arr11) #计算每个元素的绝对值 585 | Out[102]: 586 | array([[4, 3, 2], 587 | [1, 0, 1], 588 | [2, 3, 4], 589 | [5, 6, 7]]) 590 | ``` 591 | 592 | 相同形状数组间元素的操作: 593 | 594 | ```python 595 | In [103]: arr11 + arr12 #加 596 | Out[103]: 597 | array([[ 5, 6, 9], 598 | [ 8, 3, 6], 599 | [ 1, 4, 0], 600 | [ 1, -5, -5]]) 601 | 602 | In [104]: arr11 - arr12 #减 603 | Out[104]: 604 | array([[ 3, 0, -5], 605 | [ -6, -3, -8], 606 | [ -5, -10, -8], 607 | [-11, -7, -9]]) 608 | 609 | In [105]: arr11 * arr12 #乘 610 | Out[105]: 611 | array([[ 4, 9, 14], 612 | [ 7, 0, -7], 613 | [ -6, -21, -16], 614 | [-30, -6, -14]]) 615 | 616 | In [106]: arr11 / arr12 #除 617 | Out[106]: 618 | array([[ 4. , 1. , 0.28571429], 619 | [ 0.14285714, 0. , -0.14285714], 620 | [-0.66666667, -0.42857143, -1. ], 621 | [-0.83333333, -6. , -3.5 ]]) 622 | 623 | In [107]: arr11 // arr12 #整除 624 | Out[107]: 625 | array([[ 4, 1, 0], 626 | [ 0, 0, -1], 627 | [-1, -1, -1], 628 | [-1, -6, -4]], dtype=int32) 629 | 630 | In [108]: arr11 % arr12 #取余 631 | Out[108]: 632 | array([[0, 0, 2], 633 | [1, 0, 6], 634 | [1, 4, 0], 635 | [1, 0, 1]], dtype=int32) 636 | ``` 637 | 638 | #### 统计运算函数 639 | 640 | ```python 641 | In [109]: np.sum(arr11) #计算所有元素的和 642 | Out[109]: -18 643 | 644 | In [110]: np.sum(arr11,axis = 0) #对每一列求和,注意axis是0 645 | Out[110]: array([ -2, -6, -10]) 646 | 647 | In [111]: np.sum(arr11, axis = 1) #对每一行求和,注意axis是1 648 | Out[111]: array([ 9, 0, -9, -18]) 649 | 650 | In [112]: np.cumsum(arr11) #对每一个元素求累积和(从上到下,从左到右的元素顺序),即每移动一次就把当前数字加到和值 651 | Out[112]: array([ 4, 7, 9, 10, 10, 9, 7, 4, 0, -5, -11, -18], dtype=int32) 652 | 653 | In [113]: np.cumsum(arr11, axis = 0) #计算每一列的累积和,并返回二维数组 654 | Out[113]: 655 | array([[ 4, 3, 2], 656 | [ 5, 3, 1], 657 | [ 3, 0, -3], 658 | [ -2, -6, -10]], dtype=int32) 659 | 660 | In [114]: np.cumprod(arr11, axis = 1) #计算每一行的累计积,并返回二维数组 661 | Out[114]: 662 | array([[ 4, 12, 24], 663 | [ 1, 0, 0], 664 | [ -2, 6, -24], 665 | [ -5, 30, -210]], dtype=int32) 666 | 667 | In [115]: np.min(arr11) #计算所有元素的最小值 668 | Out[115]: -7 669 | 670 | In [116]: np.max(arr11, axis = 0) #计算每一列的最大值 671 | Out[116]: array([4, 3, 2]) 672 | 673 | In [117]: np.mean(arr11) #计算所有元素的均值 674 | Out[117]: -1.5 675 | 676 | In [118]: np.mean(arr11, axis = 1) #计算每一行的均值 677 | Out[118]: array([ 3., 0., -3., -6.]) 678 | 679 | In [119]: np.median(arr11) #计算所有元素的中位数 680 | Out[119]: -1.5 681 | 682 | In [120]: np.median(arr11, axis = 0) #计算每一列的中位数 683 | Out[120]: array([-0.5, -1.5, -2.5]) 684 | 685 | In [121]: np.var(arr12) #计算所有元素的方差 686 | Out[121]: 5.354166666666667 687 | 688 | In [122]: np.std(arr12, axis = 1) #计算每一行的标准差 689 | Out[122]: array([ 2.49443826, 1.88561808, 1.69967317, 2.1602469 ]) 690 | ``` 691 | 692 | **numpy中的统计函数运算是非常灵活的,既可以计算所有元素的统计值,也可以计算指定行或列的统计指标**。还有其他常用的函数,如符号函数`sign`(将正数、负数、零分别映射为1、-1、0),`ceil`(取>=x的最小整数),`floor`(取<=x的最大整数),`modf`(将浮点数的整数部分与小数部分分别存入两个独立的数组),`cos`,`arccos`,`sin`,`arcsin`,`tan`,`arctan`等。 693 | 694 | 让我很兴奋的一个函数是`where()`,它类似于Excel中的if函数,可以进行灵活的变换: 695 | 696 | ```python 697 | In [123]: arr11 698 | Out[123]: 699 | array([[ 4, 3, 2], 700 | [ 1, 0, -1], 701 | [-2, -3, -4], 702 | [-5, -6, -7]]) 703 | 704 | In [124]: np.where(arr11 < 0, 'negtive','positive') 705 | Out[124]: 706 | array([['positive', 'positive', 'positive'], 707 | ['positive', 'positive', 'negtive'], 708 | ['negtive', 'negtive', 'negtive'], 709 | ['negtive', 'negtive', 'negtive']], 710 | dtype='0,1,-1) 865 | In [160]: position = np.cumsum(step) 866 | In [161]: matplotlib.pylab.plot(np.arange(10000), position) 867 | ``` 868 | 869 | ![graph4](http://mmbiz.qpic.cn/mmbiz/yjFUicxiaClgUXjmGVZtctljErLLnffs9Frp8BIXlMvXMvOR5nPHcicuzrRfSicPBicbApwriaUpD7NFnmy0oTlqrvgg/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1) 870 | 871 | **避免for循环,可以达到同样的效果**。 872 | -------------------------------------------------------------------------------- /Python_Libraries_For_Building_Recommender_Systems.md: -------------------------------------------------------------------------------- 1 | # 用于搭建推荐系统的Python库 2 | 3 | 这是一篇发布在[www.faroba.com](http://www.faroba.com/)的小短文,[原文](http://www.faroba.com/2015/12/03/a-python-libraries-for-building-recommender-systems/)是英文版的,这里简单地进行记录和翻译。 4 | 5 | > Recommender systems or Collaborative filtering is the process of filtering for information using techniques involving collaboration among multiple agents. Applications of collaborative filtering typically involve very large data sets. Collaborative filtering methods have been applied to many different kinds of data including: sensing and monitoring data, such as in mineral exploration, environmental sensing over large areas or multiple sensors; financial data, such as financial service institutions that integrate many financial sources; or in electronic commerce and web applications where the focus is on user data 6 | 7 | 推荐系统,或者说协同过滤,是一个使用包括协同算法等技术进行信息过滤的过程。协同过滤的应用一般都涉及到规模非常大的数据集。 协同过滤算法已经应用到了很多不同类型的数据上,比方说:感知和监管数据,包括矿质勘探、大区域/多传感器环境感知;金融数据,包括集成多种资金来源的金融服务机构;电商和网络应用的用户数据。 8 | 9 | > Here is the list of python libraries for building recommender systems. 10 | 11 | 下面是一些可以用于构建推荐系统的Python库。 12 | 13 | ## [Crab](http://muricoca.github.io/crab/) 14 | 15 | > Crab is a flexible, fast recommender engine for Python that integrates classic information filtering recommendation algorithms in the world of scientific Python packages (numpy, scipy, matplotlib). The engine aims to provide a rich set of components from which you can construct a customized recommender system from a set of algorithms. The tutorial is from official documentation of Crab. 16 | 17 | Crad是一个灵活,快速的Python推荐引荐,继承了经典的信息过滤推荐算法。它的目标是提供一个丰富的组件集合,使得我们可以用它来构建一个定制化的推荐系统。官方文档里面提供了详细的教程。 18 | 19 | 这个库其实是Python里面比较有名的推荐系统库了,开源,代码都可以下载到,只是这个库只支持Python2.x版本,有点跟不上现在的潮流了。之前一直挺想读一遍它的源码,好好研究一下的,写一个兼容Python3.x的版本的,应该会有不错的收获。 20 | 21 | ## [pysuggest 1.0](https://pypi.python.org/pypi/pysuggest/1.0) 22 | 23 | > Python wrapper for the SUGGEST, which is a Top-N recommendation engine that implements a variety of recommendation algorithms for collaborative filtering. SUGGEST is a Top-N recommendation engine that implements a variety of recommendation algorithms. Top-N recommender systems, a personalized information filtering technology, are used to identify a set of N items that will be of interest to a certain user. In recent years, top-N recommender systems have been used in a number of different applications such to recommend products a customer will most likely buy; recommend movies, TV programs, or music a user will find enjoyable; identify web-pages that will be of interest; or even suggest alternate ways of searching for information. 24 | 25 | 这个库是SUGGEST的Python封装,(SUGGEST是)一个实现了多种协同过滤推荐算法的Top-N推荐引擎。Top-N推荐,是一种个性化信息过滤技术,用于识别出特定用户会感兴趣的N件物品。在近几年,top-N推荐系统已经被应用到了很多不同的领域,比方说推荐用户最可能买的产品,推荐电影、电视节目、还有用户最喜欢的音乐,推荐用户感兴趣的网页,甚至在用户使用搜索引擎查询时给出更好的建议。 26 | 27 | ## [unison-recsys](https://github.com/python-recsys/unison-recsys) 28 | 29 | > Unison is the recommendation system behind GroupStreamer, an Android application that recommends music for groups. You can also checkout the source code of the mobile application if you're interested. 30 | 31 | Unison是GroupStreamer所使用的推荐系统,GroupStreamer是一个为群组推荐音乐的Android应用。感兴趣的话还可以看看它们手机应用的源码。 32 | 33 | ## [python-recsys](https://github.com/python-recsys/python-recsys) 34 | 35 | > A python library for implementing a recommender system, for documentation and examples click. 36 | 37 | 一个实现推荐系统的Python库,点入链接可以查看文档和具体的例子。 38 | 39 | ## [PySpark](https://spark.apache.org/docs/1.4.0/api/python/pyspark.mllib.html#module-pyspark.mllib.recommendation) 40 | 41 | > The Spark Python API (PySpark) exposes the Spark programming model to Python. 42 | 43 | 一个Spark的Python API,可以借助它调用Spark的编程模型。 44 | 45 | Spark里面也有很多好玩的处理机器学习问题,处理大数据的组件,希望之后有时间可以多多研究吧~ 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # learnpython 2 | 3 | 该项目用于记录学习Python的过程,从入门到~~出家~~小成。 4 | 5 | ## 大纲 6 | 7 | [Learn_Data_Science_In_Python](https://github.com/familyld/learnpython/blob/master/Learn_Data_Science_In_Python.md)记录了使用Python学习数据科学的路线,以此路线为纲,在本项目中会陆续更新学习笔记和感想,同时也会收录一些Python使用的小技巧及需要注意的地方。 8 | 9 | ## 入门 10 | 11 | 在[Turtle_Diagram_of_Python_Learning](https://github.com/familyld/learnpython/blob/master/Turtle_Diagram_of_Python_Learning.md)里有一张国外程序员画的Python学习神图,国内有人翻译成了中文,非常适合入门者初步了解Python的一些特性和使用方式,我对图中提到的地方进行了一些解析。 12 | 13 | Python基本语法及入门学习的笔记记录在[My_Python_Notebook](https://github.com/familyld/learnpython/blob/master/My_Python_Notebook/README.md)中,该笔记记录的是学习[廖雪峰Python3教程](http://www.liaoxuefeng.com/wiki/0014316089557264a6b348958f449949df42a6d3a2e542c000#0)的过程,摘录了一些重点,重新编排内容,并加入了更丰富的代码示例和对学习过程中所遇到问题的理解。 14 | 15 | 在[Interpret_PEP8](https://github.com/familyld/learnpython/blob/master/Interpret_PEP8.md#a-foolish-consistency-is-the-hobgoblin-of-little-minds)中,我对Python的编程规范PEP8进行了中英对照的翻译,并加入了一些自己的见解和代码解析。 16 | 17 | ## 细节 18 | 19 | ### Python中直接赋值,浅拷贝,深拷贝有什么区别? 20 | 21 | 在[Difference_between_DeepCopy_and_ShallowCopy](https://github.com/familyld/learnpython/blob/master/Difference_between_DeepCopy_and_ShallowCopy.md)中记录了Python中直接赋值,浅拷贝,深拷贝的区别,并且使用具体代码作为例子加深理解。 22 | 23 | ## 库的使用 24 | 25 | ### Pandas高效使用技巧 26 | 27 | 在[Efficient_Pandas_Skills](https://github.com/familyld/learnpython/blob/master/Efficient_Pandas_Skills.md)中收录了Pandas库使用的一些技巧,能够提高工作效率。 28 | -------------------------------------------------------------------------------- /Turtle_Diagram_of_Python_Learning.md: -------------------------------------------------------------------------------- 1 | # Python学习入门神图 2 | 3 | 这是一张国外程序员画出Python学习神图,国内有人翻译成了中文,非常适合入门者初步了解Python的一些特性和使用方式。 4 | 5 | ![Turtle_Diagram_of_Python_Learning](https://github.com/familyld/learnpython/blob/master/graph/Turtle_Diagram_of_Python_Learning.png?raw=true) 6 | 7 | 从上往下过一遍: 8 | 9 | - Python代码文件结尾.py,和c语言的.c,c++的.cpp,java的.java类似,这个没什么好说的。 10 | 11 | - 在代码中如果使用到中文,最好在头部声明`#coding=utf-8`或者`#coding=gbk`,这样存储代码文件时就不会以ASCII码保存而是使用我们指定的编码方式,避免了再次打开时变成乱码。这点在我们使用中文注释时特别有用。在[My_Python_Notebook](https://github.com/familyld/learnpython/blob/master/My_Python_Notebook.md)的`中文注释`这个小节我也有提到。 12 | 13 | - 除了可以用UliPad编辑器之外用Sublime也很不错,我就是使用Sublime文本编辑器进行编写的,可以使用ConvertToUTF8插件来实现,安装好保存加载都使用UTF8格式,支持多种语言的使用。 14 | 15 | - 类似C/C++语言和Java语言,我们可以通过引入头文件来在Python中调用其他代码文件中定义的函数或者结构,通过`import`语句可以实现这一点,还有`from libName import structName`这样的用法,只加载库的某个部分或某个函数。这里说的模块指的就是一个.py文件,为了避免模块名重复,Python还引入了包名的机制,详见[My_Python_Notebook](https://github.com/familyld/learnpython/blob/master/My_Python_Notebook.md)的`模块`章节。 16 | 17 | - Python有一个很友好但也饱受诟病的特点就是代码块用4个空格的缩进表示,缩进相同表示在同一级代码块中,不需要像其他语言那样使用括号,编写时清晰简单,但是有时复制粘贴代码容易因为这点出漏子。 18 | 19 | - 我们可以定义一个main函数,但不像java和c/c++,python脚本中的main函数和其他函数一样是不会自动被调用的,它的命名随我们定,甚至不需要叫做main函数。当我们运行脚本时实际上首先执行的是第一行没有缩进的非def的语句,我们在语句中调用函数才会执行函数。 20 | 21 | - 看到图的底部`if __name__ == '__main__':`这个代码块,其中 `__name__` 变量是个特殊变量,当我们**在命令行执行**这个.py文件时,Python解释器就会把`__name__` 变量赋值为 `__main__`。如果在别的地方导入这个.py文件时,不会有对 `__name__` 赋值的操作,if判断就会失败。 借助这个特性,我们可以**在if判断中编写一些额外的代码**,用于在命令行运行时执行。**运行测试**就需要这样做。 22 | 23 | - 在Python中并没有说单引号是字符双引号是字符串这个说法,都可以用,都视为字符串变量。注意引号内转义符的处理。 24 | 25 | - 在Python中函数声明的顺序是没有要求的,只要能找到就可以了,所以图中可以在main函数中定义调用foo函数,并且foo函数不需要在main函数前面声明。 26 | 27 | - 我们可以使用`模块名.函数名`来调用模块中的函数,注意之前一定要import。 28 | 29 | - Python中的变量是是一门具有强类型(即变量类型是强制要求的)、动态类型(类型可变)、隐式类型(不需要做变量声明)、大小写敏感(var和VAR代表了不同的变量)以及面向对象(一切皆为对象)等特点的编程语言。对象是按引用传递的,所以同一个变量可以先是int型然后变成str型。注意变量调用前先要赋值(实例化)。 30 | 31 | - Python的列表类型很强大,是我们在C/C++中使用的数组的加强版,里面可以放多种不同类型的对象,并且可以利用Python的内嵌函数对列表进行各种操作。 32 | 33 | - 循环写法没有太多好说的,简单好用,i不用提前定义,可以根据in后面的变量来自动对i进行赋值。要产生一个范围可以使用range函数,注意它是左闭右开的。 34 | 35 | - 函数声明,循环声明,条件判断声明都用冒号作为结束。 36 | 37 | - 格式化输出类似C语言,注意有多个变量时使用圆括号()括起,这样就当为一个元组。单变量格式化输出无须括号。在格式化描述和变量间用百分号%隔开。 38 | 39 | - 逻辑运算符直接使用小写英文的and,or,not即可,`&&`,`||`,`~`这些就不使用了。 40 | 41 | - 条件判断时我们常用else if,但是在Python中这个关键字是elif。 42 | 43 | - 注释有多重写法,可以单行也可以多行,具体参照[My_Python_Notebook](https://github.com/familyld/learnpython/blob/master/My_Python_Notebook.md)的`注释`章节。 44 | 45 | - 注意图中使用的是Python2.x版本,所以print是作为一个语句而不是函数,不需要使用括号括起后面的字符串。在Python3.x版本中print是作为一个函数被调用的,注意区别。详细的差别可以看这个项目下的[Difference_between_Py2_and_Py3](https://github.com/familyld/learnpython/blob/master/Difference_between_Py2_and_Py3.md)一文。 46 | -------------------------------------------------------------------------------- /graph/Excecl_Pivot_Table.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/familyld/learnpython/0ffd63d401c8474415aa9f5a3a18074ac48738d9/graph/Excecl_Pivot_Table.jpg -------------------------------------------------------------------------------- /graph/Turtle_Diagram_of_Python_Learning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/familyld/learnpython/0ffd63d401c8474415aa9f5a3a18074ac48738d9/graph/Turtle_Diagram_of_Python_Learning.png -------------------------------------------------------------------------------- /graph/pandas_box_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/familyld/learnpython/0ffd63d401c8474415aa9f5a3a18074ac48738d9/graph/pandas_box_1.png -------------------------------------------------------------------------------- /graph/pandas_box_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/familyld/learnpython/0ffd63d401c8474415aa9f5a3a18074ac48738d9/graph/pandas_box_2.png -------------------------------------------------------------------------------- /graph/pandas_box_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/familyld/learnpython/0ffd63d401c8474415aa9f5a3a18074ac48738d9/graph/pandas_box_3.png -------------------------------------------------------------------------------- /graph/pandas_hist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/familyld/learnpython/0ffd63d401c8474415aa9f5a3a18074ac48738d9/graph/pandas_hist.png --------------------------------------------------------------------------------