基础
字符串和编码
字符编码
8个bit(比特)作为一个byte(字节),一个字节能表示的最大整数就是255(二进制11111111=255)
可变长编码的UTF-8
总结: 在计算机内存中,统一使用Unicode编码,当需要保存到硬盘或者需要传输的时候,就转换为UTF-编码。
用记事本编辑的时候,从文件读取的UTF-8字符被转换为Unicode字符到内存里,编辑完成后,保存的时候再把Unicode转换为UTF-8保存到文件
浏览网页的时候,服务器会把动态生成的Unicode内容转换为UTF-8再传输到浏览器
ord()函数: 用于获取字符的整数表示,chr()把编码转换为对应的字符
Python中的字符类型是str,在内存中以Unicode表示,一个字符对应若干个字节。在网络上传输或者保存到磁盘上,就需要将str变为以字节为单位的bytes。
Python对bytes类型的数据用带b前缀的单引号或双引号表示。
以Unicode表示的str通过encode()方法可以编码为指定的bytes。
纯英文的str可以用ascii编码为bytes;含有中文的str可以用utf-8编码为bytes;含有中文的str无法用ascii编码。
要把bytes变为str,可以用到decode()方法。
len()函数计算的是str的字符数,如果换成bytes,len()函数就计算字节流书。
1个中文3个字节,一个英文字符只占用1个字节
当Python解释器读取源代码时,为了让它按UTF-8编码读取,通常在文件开头写上这两行
1 | #!/usr/bin/env python3 |
%x: 十六进制整数
使用%%来表示一个%
使用list和tuple
list
插入: insert(index, content)
删除:
- pop —- 删除末尾的元素
- pop(i)删除指定元素
元素替换: 直接用索引表示
list里面的元素的数据类型也可以不同
list元素也可以是另一个list
tuple: 元组
元组中除开appened(),insert()这样的方法,其他获取元素的方法和list是一样的。
元组不可变所以更安全,如果可能,尽量使用tuple代替list
特别注意:
1 | # 像下面这样定义的不是元组而是一个整数 |
特殊的tuple
使用dict和set
dict
要避免key不存在的错误,有两种方法,一种是通过in判断key是否存在,一种是通过dict提供的get方法,如果key不存在,可以返回None,或者自己指定的value。
注意:返回None的时候Python的交互命令行不显示结果
删除一个key,使用pop(key)
dicet的缺点:需要占用大量的内存,内存浪费多
dict是用空间来换取时间的方法。
关于dict,需要牢记的一条就是dict的key必须是不可变对象
通过key计算位置的算法称为哈希算法
Python中:整数、字符串都是不可变的。而list是可变的
set
set也是一组key的集合,但是不存储value。set中,由于key不能重复,所以没有重复的key
创建一个set,需要提供一个list作为输入集合:
1 | 1, 2, 3, 4, 5]) s = set([ |
想set中添加元素使用add(key),可以重复添加,但是不会有效果。
使用remove(key)方法可以删除元素
注意: set也需要放入不可变的对象
再议不可变对象
高级特性
切片
L[start: end] : 索引包括start但是不包括end
倒数的第一个元素是-1
高级玩法:
L[start:end :space ]
L[:]用于复制一个原样list
tuple仍然可以使用切片,但是因为tuple是不可变的,所以使用切片后得到的还是tuple
字符串也可以使用切片,使用之后得到的仍然是字符串
迭代
给定一个list或者tuple,使用for循环来遍历这个list或tuple,这种遍历我们称之为遍历(lteration)
Python中,迭代是通过for … in 来完成的,相比java,python的for循环抽象程度要更高,因为Python中的for循环不仅可以在list或tuple上,还可以作用在其他可迭代对象上。
只要是可迭代对象,不管它有没有下标,都可以使用迭代:
例如dict就可以迭代,但是dict的存储不是按照list的方式顺序排列,所以,迭代出的结果的顺序很可能不一样。默认情况下,dict迭代的是key,如果要迭代value,可以用for value in d.values(),如果同时迭代key和value,可以使用for k, v in d.items():
如何判断一个对象是可迭代对象呢? 可通过collections模块的lterable类型判断:
1 | from collection.abc import Iterable |
如何让list实现类似java那样的下标循环?Python内置的enumerate函数可以把一个list变成索引-元素对,这样就可以在for循环中同时迭代索引和元素本身:
1 | for i, value in enumerate(["A", 'B','C' ]): |
小结: 任何可迭代对象都可以作用于for循环,包括我们自定义的数据类型
列表生成式
列表生成式即List Comprehensions,是Python内置的非常简单却强大的可以用来创建list的生成式。
生成器
在Python中,这种一边循环一边计算大的机制,称为生成器:generator
创建一个generator有很多种方法,第一种方法就是把一个列表生成式的[]改成()。
使用next()可以获得geneartor的下一个返回值,当没有更多元素时,抛出StopIteration的错误。
Generator 也是可迭代对象
定义generator的另一种方法:
如果一个函数定义中包含yield关键字,那么这个函数就不再是一个普通函数,而是一个generator:
1 | # generator生成的另一种方法 |
较难理解的就是generator和函数的执行流程不一样。generator的函数,在每次调用next()的时候执行,遇到yield语句返回,再次执行时从上次返回的yield语句处继续执行。
使用for循环调用generator时,发现拿不到generator的return语句中的返回值。如果想要拿到返回值,必须捕获到StopIteration错误,返回值包含在StopIteration的value中
1 | # Exercise 写出一个关于杨辉三角形的generator |
迭代器
可以直接作用于for循环的对象统称为可迭代对象: Iterable
可以被next()函数调用并不断返回下一个值的对象称为迭代器: Iterator
可以使用isinstance()判断一个对象是不是Iterator对象
生成器都是Iterator对象,但list、dict、str虽然是Iterable,却不是Iterator。
Python的Iterator对象是一个数据流,Iterator对象可以被next()函数调用并不断返回下一个数据,直到没有数据时抛出StopIteration错误。
可以把数据流看做是一个有序序列,但我们却不能提前知道序列的长度,只能不断通过next()函数实现按需计算下一个数据,所以Iterator的计算是惰性的,在需要返回下一个数据时它才会计算。
Iterator甚至可以表示一个无限大的数据流,例如全体自然数。
函数式编程 - Functional Programming
通过把大段代码拆成函数调用,就可以把复杂任务分解成简单的任务,这种分解可以称之为面向过程的程序设计。函数就是面向过程的程序设计的基本单元
理解计算机和计算的概念
在计算机的层次上,CPU执行的是加减乘除的指令代码,以及各种条件判断和跳转指令,所以,汇编语言是最接近计算机的语言。
而计算则指的是数学意义上的计算,越是抽象的计算,离计算机硬件越远
对应到编程语言,就是越低级的语言,越贴近计算机,抽象程度低,执行效率高,比如C语言;越高级的语言,越贴近计算,抽象程度高,执行效率低,比如Lisp语言。
函数式编程就是一种抽象程度很高的编程范式,纯粹的函数式编程语言编写的函数没有变量,因此,任意一个函数,只要输入是确定的,输出就是确定的,这种纯函数我们称之为没有副作用。而允许使用变量的程序设计语言,由于函数内部的变量状态不确定,同样的输入,可能得到不同的输出,因此,这种函数是有副作用的。
函数式编程的一个特点就是,允许把函数本身作为参数传入另一个函数,还允许返回一个函数!
Python不是纯函数式编程语言。
高阶函数
高阶函数英文名: High-order function
abs(-10)是函数调用,abs是函数本身
1 | f = abs |
函数本身也可以赋值给变量,即:变量可以指向函数
传入函数
一个函数可以接受另一个函数作为参数,这种函数就称之为高阶函数
一个最简单的高阶函数:
1 | def add(x, y, f): |
高阶函数中作为参数的函数只需要函数名,不需要后面的括号
map/reduce
map
map()函数接收两个参数,一个是函数,一个是Iterable,map函数将传入的函数依次作用到序列的每个元素,并把结果作为新的Iterator返回。
map()作为高阶函数,事实上它把运算规则抽象了。
下面将一个list中的所有数字转为字符串:
1 | list(map(str, [1, 2, 3, 4, 5, 6, 7, 9])) |
reduce
reduce把一个函数作用在一个序列[x1, x2, x3, …]上,这个函数必须接收两个参数,ruduce把结果继续和序列的下一个元素做累积计算。
1 | # 关于reduce的用法 |
filter
Python内建的filter()函数用于过滤序列
和map()
类似,filter()
也接收一个函数和一个序列。和map()
不同的时,filter()
把传入的函数依次作用于每个元素,然后根据返回值是True
还是False
决定保留还是丢弃该元素。
filter()函数返回的是一个Iterator,也就是一个惰性序列,所以要强迫filter()完成计算结果,需要用到list()函数获得所有结果并返回一个list
用filter求素数:但是没有怎么看懂
1 | # 用filter求素数 -- 没有怎么看懂 |
匿名函数
Python中对匿名函数提供了有限支持
1 | lambda x : x * x |
匿名函数有一个限制,就是只能有一个表达式,不用谢retrun,返回值就是该表达式的结果
匿名函数没有名字,所以不用担心函数名冲突。匿名函数也是一个对象,也可以把匿名函数赋值给一个变量,再利用变量来调用该函数;
也可以把匿名函数作为一个返回值返回。
sorted – 排序算法
排序的核心是比较两个元素的大小
对于字符串或者两个dict如何比较呢?直接比较数学上的大小是没有意义的,因此,比较的过程必须通过函数抽象出来。
list排序
Python内置的sorted()函数就可以对list进行排序
sorted()是一个高阶函数,它可以接受一个key函数来实现自定义的排序:
key函数使用方法: key指定的函数将作用于list的每一个元素上,并根据key函数返回的结果进行排序。
字符串排序
默认情况下,对字符串进行排序,是按照ASCII的大小进行比较的。
如何实现排序时忽略大小写,按照字母序排序?实现这个算法,只需要将所有字符串都改成大写或者小写
如果要进行反向排序,不必改动key函数,只需要传入第三个参数: reverse=True
下面的练习题虽然我做出来了,但是我还是很蒙,有时间记得复习复习:
1 | # 请用sorted()对上述列表分别按名字排序 |
返回函数
函数作为返回值
高阶函数除了可以接受函数作为参数外,还可以把函数作为结果值返回
对于一个求和函数,如果不需要立刻求和,而是在后面的代码中,根据需要再计算。这时就可以不返回求和的结果,而是返回求和的函数
1 | ''' |
注意 :返回一个函数时,不需要加上函数名后面的括号
何为闭包?
在这个例子中,我们在函数lazy_sum
中又定义了函数sum
,并且,内部函数sum
可以引用外部函数lazy_sum
的参数和局部变量,当lazy_sum
返回函数sum
时,相关参数和变量都保存在返回的函数中,这种称为“闭包(Closure)”的程序结构拥有极大的威力。
请再注意一点,当我们调用lazy_sum()
时,每次调用都会返回一个新的函数,即使用入相同的参数:
1 | f1 = lazy_sum(1, 3, 5, 7, 9) |
f1()
和f2()
的调用结果互不影响。
闭包–Closure
需要注意的是:返回的函数并没有立刻执行,而是直到它被调用之后才执行
返回闭包时牢记的一点就是: 返回函数不要引用任何循环变量,或者后续会发生变化的变量
如果一定要引用循环变量怎么办?方法是再创建一个函数,用该函数的参数绑定循环变量当前的值,无论循环变量后续如何更改,已绑定到函数参数的值不变:
1 | # 在闭包中使用循环变量 |
装饰器
函数对象有一个name属性,可以拿到函数的名字:
什么是装饰器
在代码运行期间动态增加功能的方式,称之为”装饰器”(Decorator)
本质上,decoator就是一个返回函数的高阶函数。
总感觉装饰器decorator有点难
小结
在OPP的设计模式中,decorator被称为装饰模式。OPP的装饰模式需要通过继承和组合来实现,而Python除了能支持OPP的decorator外,直接从语法层次支持decorator。Python的decorator可以用函数实现,也可以用类实现。
*这一小节的练习题答案参见大神做法: *
https://blog.csdn.net/GBA_Eagle/article/details/80764749
偏函数– Partial function
对于函数,通过设定参数的默认值,可以降低函数调用的难度。偏函数也可以做到这点
int()函数
int()函数可以把字符串转换为整数,默认为十进制转换
int()函数中有个base参数,默认为10,修改base可以让int()函数按照你希望的进制进行转换
转换大量二进制字符串的时候,每次都传入int(x, base=2)非常麻烦,这时候,可以自定义一个int2()函数,默认把base=2传进去,像下面这样:
1 | def int2(x, base=2): |
functools.partial就是帮助我们创建一个偏函数的,不需要自定义int2(),可以直接使用下面的代码创建一个新的函数int2():
1 | import functools |
functools.partial的作用就是,把一个函数的某些参数固定住,返回一个新的函数
小结:
当函数的参数个数太多,需要简化时,使用functools.partial
可以创建一个新的函数,这个新函数可以固定住原函数的部分参数,从而在调用时更简单。
模块
sys模块
任何模块的第一个字符串,都被视为模块的文档注释
使用author变量可以添加作者姓名
1 | __author__ = "YourName" |
sys模块有一个argv变量,用list存储了命令行的所有参数。argv至少有一个元素,第一个元素永远是该.py文件的文件名
在一个模块中,我们可能会定义很多函数和变量。对于其中一些函数和变量,我们是想用来给别人使用的,对于另外一些函数和变量,我们只想在模块内部使用。实现上述目标是通过_前缀来实现的。
正常的函数和变量名是公开的(public),可以直接被使用
下面的一段话没有读太懂:
类似**xxx**
这样的变量是特殊变量,可以被直接引用,但是有特殊用途,比如上面的**author**
,**name**
就是特殊变量,hello
模块定义的文档注释也可以用特殊变量**doc**
访问,我们自己的变量一般不要用这种变量名;
类似_abc这样的函数或变量就是非公开的(private),不应该被直接使用。
需要注意的是: private函数和变量是不应该被直接引用,而不是“不能”被直接引用,是因为Python并没有一种方法可以完全限制访问private函数和变量,但是,从编程习惯上不应该引用private函数或变量
小结
外部不需要引用的函数全部定义为private,只有外部需要引用的函数才定义为public
安装第三方模块
安装使用pip或者pip3
模块搜索路径
加载一个模块的时候,Python会在指定的路径下搜索对应的.py文件,如果找不到,就会报错。
默认情况下,搜索路径会存放在sys模块的path变量中
如何添加自己的搜索路径,有两种方法:
直接修改sys.path,在其中添加要搜索的目录
1
2import sys
sys.path.append('') # 在引号中添加要搜索的目录这种方法是在运行时修改,运行结束后失效
第二种方法是设置环境变量PYTHONPATH,该环境变量的内容会自动添加到模块搜索路径中。设置方式与设置Path环境变量类似。注意只需要添加你自己的搜索路径,Python自己本身的搜索路径不受影响
类和实例
面向对象最重要的概念就是类(Class)和实例(Instance),必须牢记类是抽象的模版,而实例是根据类创建出来的一个个具体的“对象”,每个对象都拥有相同的方法,但各自的数据可能不同
可以自由地给一个实例变量绑定属性:
1 | # 给实例bart绑定一个name属性: |
由于类可以起到模版的作用,因此可以在创建实例的时候,把一些我们认为必须绑定的属性强制填写进去,通过定义一个特殊的init方法,可以在创建实例的时候,就把name,score等属性绑定上去
下面的需要重点理解一下:
1 | # 可以在创建实例的时候,把一些我们认为必须绑定的属性强制填写进去。通过定义一个特殊的init方法,可以在创建实例的时候, |
self不需要传,Python解释器自己会把实例变量传进去:
和普通的函数相比,在类中定义的函数只有一点不同,就是第一个参数永远是实例变量self
,并且,调用时,不用传递该参数。除此之外,类的方法和普通函数没有什么区别,所以,你仍然可以用默认参数、可变参数、关键字参数和命名关键字参数。
数据封装
我们可以直接在Student类的内部定义访问数据的函数,这样,就把“数据”封装起来了。这些封装数据的函数是和Student类本身关联起来的,我们称之为类的方法
访问限制
外部代码还是可以自由地修改一个实例的属性。
如果想让内部属性不被外部访问,可以把属性的名字前加上两个下划线,在Python中,实例的变量名如果以双下划线开头,就变成了一个私有变量(private),只有内部可以访问,外部不能访问
以双下划线开头且以双下划线结尾的是特殊变量,特殊变量可以直接访问,不是private变量。
对于前面只有一个下划线的变量,是可以在外部访问到的。但是,按照约定俗成的规定,当你看到这样的变量时,意思是说,“虽然我可以被访问,但是请把我当作私有变量,不要随意访问我”
双下划线开头的实例变量其实也是可以访问到的,因为Python其实并没有严格的private变量。之所以不能访问以双下划线开头的私有变量,是因为Python解释器把
1 | # __name变量变成了_Student__name |
总的来说就是: Python本身并没有任何机制阻止你干坏事,全凭自觉
继承和多态
继承
当子类和父类有相同的方法的时候,子类的方法会覆盖父类的方法。当运行子类的实例的时候,总是会调用子类的方法
多态
创建一个class的时候实际上是创建了一种数据类型。
判断某个变量是否是某种类型,可以使用isinstance()判断
在继承关系中,如果一个实例的数据类型是某个子类,那么它的数据类型也可以被看做是父类。但是反过来就不行
多态的真正威力: 调用方法只管调用,不管细节,而当我们新增一种Animal的子类时,只要确保run()方法编写正确,不用管原来的代码是如何调用的。这就是著名的“开闭”原则:
- 对扩展开放: 允许新增Animal子类;
- 对修改封闭:不需要修改依赖Animal类型的run_twice()等函数
静态语言 VS 动态语言
Python的“file-like object“就是一种鸭子类型。对真正的文件对象,它有一个read()
方法,返回其内容。但是,许多对象,只要有read()
方法,都被视为“file-like object“。许多函数接收的参数就是“file-like object“,你不一定要传入真正的文件对象,完全可以传入任何实现了read()
方法的对象。
获取对象信息
使用type()
使用isinstance
对于class的继承关系,使用isinstance()函数
isinstance()判断的是一个对象是否是该类型本身,或者位于该类型的父继承链上
能用type()判断的基本类型也可以用isinstance()判断
还可以用isinstance判断一个变量是否是某些类型中的一种
1 | # 使用isinstance()判断一个变量是否是某些类型中的一个 |
使用dir()
dir()用于获取一个对象的所有属性和方法。它返回一个包含字符串的list
1 | # 获得一个str对象的所有属性和方法 |
1 | # __len__和len()是等价的,使用后者,它会自动去调用该对象的__len__() |
使用getattr(), setattr(),hasattr()
1 | # 使用getattr(),setattr(),hasattr(),我们可以直接操作一个对象的状态 |
如果获取不存在的属性,会抛出AttributeError(属性)的错误:
使用getattr()时,可以传入一个default参数,如果属性不存在,就返回默认值
1 | # 使用getattr()时,可以传入一个default参数,如果属性不存在,就返回默认值 |
小结
如果知道一个对象的属性和方法,就直接使用,不要使用getattr(),setattr()等等
实例属性和类属性
由于Python是动态语言,根据类创建的实例可以任意绑定属性
给实例绑定属性的方法是通过实例变量,或者通过self
变量
类属性:类本身需要绑定的属性,可以直接在class中定义,这种属性是类属性,归该class所有
1 | # 实例属性和类属性 |
注意:
在编写程序的时候,千万不要把实例属性和类属性使用相同的名字,因为相同名称的实例属性将屏蔽掉类属性,但是当你删除实例属性后,再使用相同的名称,访问到的将是类属性。
面向对象高级编程
使用slots
给实例绑定一个方法:
1 | # 定义一个函数作为实例方法 |
但是给一个实例绑定的方法,对于另一个实例是不起作用的
想要给所有实例都绑定方法,可以给class绑定方法:
1 | def set_score(self, score): |
使用slots
如何才能限制实例的属性,比如,只允许对Student实例添加name和age属性。
为了达到限制的目的,Python允许在定义class的时候,定义一个特殊的slots变量,用来限制该class实例能添加的属性
*使用slots时要注意: *
slots定义的属性仅对当前类实例起作用,对继承的子类是不起作用的;除非,在子类中也定义一个slots,这样,子类实例允许定义的属性就是自身的slots加上父类的slots。
多重继承
类似下面这样就实现了多重继承
1 | class Bat(Mammal, Flyable): |
Mixln
为了让一个类实现更多的类,从而让它再多继承一个类,这种设计通常被称为Mixln
Mixln的目的就是给一个类增加多个功能,这样,在设计类的时候,我们优先考虑通过多重继承来组合多个Mixln的功能,而不是设计多层次的复杂的继承关系
只允许单一继承的语言(如Java)不能使用Mixln的设计
定制类
Python中有很多特殊的变量或函数都有着特殊的用途,可以帮助我们定制类
str()和repr()
1 | # __str__()和__repr__()用于返回有关类和实例的字符串,其中前者返回用户看到的字符串,后者返回程序开发者看到的字符串,后者是为调试服务的 |
iter
如果一个类想被用于for…in循环,类似list或tuple那样,就必须实现个iter()方法,该方法返回一个迭代对象,然后,Python的for循环就会不断调用该迭代对象的next()方法拿到循环的下一个值,知道遇到StopIteration错误时间退出循环
1 | # iter |
getitem
想要FIb实例可以实现按索引取值必须实现getitem方法:
1 | # getitem |
让Fib实现切片操作
1 | # 让Fib实现切片 |
经过上面的操作之后,Fib还没有实现list的所有功能,例如对step参数还没有处理,也没有对含有负数的slice进行处理。要正确实现一个getitem()还是有很多工作要做的
如果想把对象看成一个dict,getitem()的参数也可能是一个可以作key的object,例如str。
与getitem类似的还有setitem以及delitem()方法
getattr
使用getattr()方法可以动态返回一个属性: 即当调用一个不存在的属性时,比如score,Python解释器会试图调用getattr(self, ‘score’)来尝试获得属性
1 | # __getattr__() |
###call
一个对象实例有自己的属性和实例方法,调用实例方法时,用instance.method()来调用。
在Python中,可以直接在实例本身上调用实例方法
1 | # call |
小结
Python的class允许定义许多定制方法,可以让我们非常方便地生成特定的类。
Python官方文档地址:https://docs.python.org/3/reference/datamodel.html#special-method-names
使用枚举类
使用枚举类
定义常量时,更好的方法是为这样的枚举类型定义一个class类型,然后,每个常量都是class的一个唯一实例。Python提供了Enum类来实现这个功能:
1 | # 使用枚举类 |
错误、调试和测试
错误、调试和测试
运行一个程序总会遇到各种各样的错误:
程序编写造成的问题叫做bug,bug是必须修复的
用户输入造成的错误可以通过检查用户的输入来做相应的处理
另一类错误是完全无法在程序运行过程中预测的。这种错误也称为异常,例如:写入文件的时候,磁盘满了,写不进去了。
Python内置有一套异常处理机制。
我们也需要跟踪程序的执行,查看变量的值是否正确,这个过程称为调试。Python的pdb可以让我们以单步方式执行代码。
错误处理
在程序运行的过程中,如果发生了错误,可以事先约定返回一个错误代码,这样,就知道是否有错。
但是用错误码来表示是否出错十分不便,更好的方法是使用try-except-finally的错误处理机制
当我们认为某些代码可能会出错时,就可以用try
来运行这段代码,如果执行出错,则后续代码不会继续执行,而是直接跳转至错误处理代码,即except
语句块,执行完except
后,如果有finally
语句块,则执行finally
语句块,至此,执行完毕。
注意:如果有finally语句,则finally语句则一定会被执行,但是可以没有finally语句
还可以在except语句后面加一个else,当没有错误发生时,会自动执行else语句
Python的错误也是class,所有的错误都继承自BaseExceptioon
常见的错误类型和继承关系:
https://docs.python.org/3/library/exceptions.html#exception-hierarchy
Try-except-finally可以跨越多层调用,所以在使用时,只需要在合适的层次去捕获错误就可以了。
###调用堆栈
如果错误没有被捕获,它就会一直往上抛,直到最后被Python解释器捕获,打印一个错误信息,然后程序退出。
###记录错误
Python内置的logging模块可以非常容易地记录错误信息:
1 | import logging |
抛出错误
错误是class,捕获一个错误就是捕获到该class的一个实例。
错误并不是凭空产生的,而是有意创建并抛出的。Python内置的函数或抛出很多类型的错误,我们自己编写的函数也可以抛出错误。
抛出错误,首先根据需要,可以定义你一个错误的class,选择好继承关系,然后用raise语句抛出一个错误的实例:
1 | class FootError(ValueError): |
只有在有需要的时候才定义我们自己的错误类型。如果可以选择Python已有的内置的错误类型,尽量使用Python内置的错误类型。
raise语句如果不带参数,就会把当前错误原样抛出,在except中raise一个Error,还可以把一种类型的错误转化为另一种类型
程序也可以主动抛出错误,让调用者来处理相应的错误。但是,应该在文档中写清楚可能会抛出哪些错误,以及错误产生的原因。
调试
调试
程序能一次写完并且正常运行的概率很小,基本不超过1%
我们需要知道,出措时,哪些变量的值是正确的,哪些变量的值是错误的。
第一种方法: 使用print()把可能有问题的变量打印出来
第二种方法: 断言
凡是用print()来辅助查看的的地方,都可以用断言(assert)来替代:
1 | # 断言(assert) |
第三种方法: logging
把print()换成logging是第三种方法,和assert相比,logging不会抛出任何错误,而且可以输出到文本
1 | # logging |
*注意: *
1 | 这就是logging的好处,它允许你指定记录信息的级别,有debug,info,warning,error等几个级别,当我们指定level=INFO时,logging.debug就不起作用了。同理,指定level=WARNING后,debug和info就不起作用了。(备注: 使用Debug的时候,logging.info以及logging.debug都是会运行的) |
第四种方法: pdb
pdb是Python的调试器,它可以让程序以单步方式运行。
使用方法:
- 以参数 -m pdb启动后,pdb自动定位到下一步将要执行的代码
- 使用命令参数 l来查看代码
- 使用命令参数 n 可以单步执行代码
- 在任何时候都可以输入命令参数 p 变量名来查看变量
- 输入命令参数 q结束调试
第五种方法:pdb.set_trace()
这个方法也是使用pdb,但是不需要单步执行,我们只需要import pdb,然后在可能出错的地方放一个pdb.set_trace(),就可以设置一个断点。
运行代码时,程序会自动在pdb,set_trace()暂停并进入pdb调试环境,可以使用命令p查看变量,或者用命令c继续运行
1 | import pdb |
最后一种方法: 使用IDE
单元测试
单元测试是用来对一个模块、一个函数或者一个类来进行正确性检验的测试工作,详见:
https://www.nli.cn/read/liaoxuefeng-python30/7cfd1dfdf10276d3.md
setUp与setDown
可以在单元测试中编写两个特殊的setUp()和setDown()方法。这两个方法会分别在每调用一个测试方法的前后分别被执行。
文档测试
Python内置的“文档测试”(doctest)模块可以直接提取注释中的代码并执行测试。
doctest严格按照Python交互式命令行的输入和输出来判断测试结果是否正确。只有测试异常的时候,可以用…
表示中间一大段烦人的输出。
什么输出也没有。这说明我们编写的doctest运行都是正确的。如果程序有问题,比如把**getattr**()
方法注释掉,再运行就会报错
当模块正常导入时,doctest不会被执行。只有在命令行直接运行时,才执行doctest。所以,不必担心doctest会在非测试环境下执行。
doctest非常有用,不但可以用来测试,还可以直接作为示例代码。通过某些文档生成工具,就可以自动把包含doctest的注释提取出来。用户看文档的时候,同时也看到了doctest
IO编程
异步IO和同步IO
在IO编程中,就存在速度严重不匹配的问题。举个例子来说,比如要把100M的数据写入磁盘,CPU输出100M的数据只需要0.01秒,可是磁盘要接收这100M数据可能需要10秒,怎么办呢?有两种办法:
第一种是CPU等着,也就是程序暂停执行后续代码,等100M的数据在10秒后写入磁盘,再接着往下执行,这种模式称为同步IO;
另一种方法是CPU不等待,只是告诉磁盘,“您老慢慢写,不着急,我接着干别的事去了”,于是,后续代码可以立刻接着执行,这种模式称为异步IO。
使用异步IO来编写程序性能会远远高于同步IO,但是异步IO的缺点是编程模型复杂。想想看,你得知道什么时候通知你“汉堡做好了”,而通知你的方法也各不相同。如果是服务员跑过来找到你,这是回调模式,如果服务员发短信通知你,你就得不停地检查手机,这是轮询模式。总之,异步IO的复杂度远远高于同步IO
操作IO的能力都是由操作系统提供的,每一种编程语言都会把操作系统提供的低级C接口封装起来方便使用,Python也不例外。我们后面会详细讨论Python的IO编程接口。
操作IO的能力都是由操作系统提供的,每一种编程语言都会把操作系统提供的低级C接口封装起来方便使用,Python也不例外。
文件读写
文件对象: 通常被称为文件描述
读文件
使用Python内置的open()函数,传入文件名和标识符:
1 | '/User/Test/test.txt', 'r') f = open( |
如果文件不存在,open()函数就会抛出一个IOErro,并且给出错误码和详细的信息告诉你文件不存在。
如果文件打开成功,调用read()方法可以一次读取文件的全部内容,Python把内容读取到内容中,用一个str对象表示
最后一步是调用close()方法关闭文件。文件使用完毕后必须关闭,因为文件对象会占用操作系统的资源,并且操作系统同一时间能打开的文件数量也是有限的
由于文件读写都有可能产生IOError,一旦出错,后面的f.close()就不会调用。所以为了保证无论是否出错都能够正确地关闭文件,我们可以使用try…finally来实现:
1 | try: |
上述操作更好的方法是使用Python的with语句来自动帮助我们调用close()方法:
1 | with open('/Users/bowenkei/Desktop/Test.txt', 'r') as file_object: |
read():会一次性读取文件的所有内容
read(size):每次最多读取size个字节的内容,可以反复调用
readline():每次读取一行内容
readlines():一次读取所有内容并按行返回list。
如果文件很小,read()
一次性读取最方便;如果不能确定文件大小,反复调用read(size)
比较保险;如果是配置文件,调用readlines()
最方便
####file_like object
像open()函数返回的这种有个read()方法的对象,在Python中统称为file-like object。除了file外,还可以是内存中的字节流,网络流,自定义流等等。file-like Object不要求从特定类继承,只要写个read()方法就行。
StringIO就是在内存中创建的file-like object,常用作临时缓冲。
二进制文件
要读取二进制文件,比如图片、视频,用’rb’模式打开即可
字符编码
要读取非UTF-8编码的文本文件,需要给open()函数传入encoding参数,例如GBK编码的文件如下读取:
1 | with open('/Users/bowenkei/Desktop/GBK.txt', encoding='gbk') as file_object: |
遇到有些编码不规范的文件,你可能会遇到UnicodeDecodeError
,因为在文本文件中可能夹杂了一些非法编码的字符。遇到这种情况,open()
函数还接收一个errors
参数,表示如果遇到编码错误后如何处理。最简单的方式是直接忽略:
1 | with open('/Users/bowenkei/Desktop/GBK.txt', encoding='gbk', errors='ignore') as file_object: |
写入文件时,只需要使用相应的’w’和’wb’即可
‘a’模式可以实现往一个文件中添加内容而不清空原有内容
下面是各种模式:
StringIO和BytesIO
StringIO: 在内存中读写str
要把str写入StringIO,我们需要先创建一个StringIO,然后,像文件一样写入即可:
1 | f = StringIO() |
BytesIO: 实现了在内存中读写bytes:
1 | from io import BytesIO |
操作文件和目录
Python内置的os可以直接调用操作系统提供的借口函数
1 | # 导入os模块之后,使用os.name可以获得操作系统类型 |
环境变量
在操作系统中定义的环境变量,全部在os.environ这个变量中,可以直接查看:
1 | # 查看变量 |
操作文件和目录
操作文件和目录的函数一部分放在os模块中,一部分放在o s.path模块中。
查看、创建和删除目录 :
把新目录的完整路径表示出来: os.path.join(‘’, ‘’)
创建一个目录: os.mkdir(‘’)
删除一个目录: os.rmdir(‘’)
拆分路径: os.path.split()
拆分路径之分离文件名和后缀: os.path.splitext()
重命名: os.rename(‘’)
删掉文件: os.remove(‘’)
列出当前目录下的所有文件: os.listdir()
判断是否是目录: os.path.isdir()
把两个路径合为一个时,通过os.path.join()可以正确处理不同操作系统的路径分隔符
拆分路径时,不要直接去拆字符串,要通过os.path.split(),这样就可以baggie路径分成两部分,后一部分总是最后级别的目录或文件名
合并、拆分路径的函数并不是要求目录和文件要真实存在,它们只对字符串尽心操作。
复制文件的函数在os中不存在,因为复制文件并非由操作系统提供的系统调用
shutil模块是os的补充,其中提供了包括copy file()在内的多个函数
序列化
我们把变量从内存中变成可存储或可传输的过程称之为序列化,在Python中称为pickling,在Java中称为serialization。
序列化之后,我们就可以把序列化后的内容写入磁盘或者通过网络传输到别的机器上
把变量内容从序列化的对象重新读取到内存里称之为反序列化,即unpickling.
Python提供了pickle模块来实现序列化。
pickle.dumps()
方法把任意对象序列化成一个bytes
,然后,就可以把这个bytes
写入文件。或者用另一个方法pickle.dump()
直接把对象序列化后写入一个file-like Object
由反序列化得到的变量和原来的变量是完全不相干的对象,它们只是内容相同而已
Pickle的问题和所有其他编程语言特有的序列化问题一样,就是它只能用于Python,并且可能不同版本的Python彼此都不兼容,因此,只能用Pickle保存那些不重要的数据
*小总结: *
pickle.dumps() and pickle.loads()都是将一个byte写入文件
pickle.dump() and pickle.load()都是将一个file-like-Object写入文件
JSON
在不同的编程语言之间传递对象,就必须把对象序列化为标准格式,例如XML,但是JOSN更好。因为JSON表示出来就是一个字符串,可以被所有语言读取,也可以方便地存储到磁盘或者通过网络传输。
JSON和Python内置的数据类型对应如下:
JSON | Python |
---|---|
{} | dict |
[] | list |
“string” | str |
1234.56 | int或float |
true/false | True/False |
null | None |
Python内置了json模块提供了非常完善的Python对象到JSON格式的转换。
1 |
JSON进阶
将class对象序列化为JSON的{} + 将JSON反序列化为class对象:
1 | import pickle |
进程和线程
多进程
Unix/linux操作系统提供了一个fork()系统调用。普通的函数调用,返回一次,fork()调用一次,返回两次,因为操作系统自动把当前进程(称为父进程)复制了一份(称为子进程),然后分别在父进程和子进程内返回。子进程永远返回0,而父进程返回子进程的ID。这样做的理由是,一个父进程可以fork出很多子进程,所以,父进程要记下每个子进程的ID,而子进程只需要调用getppid()
就可以拿到父进程的ID。
Python的os
模块封装了常见的系统调用,其中就包括fork
,可以在Python程序中轻松创建子进程
1 | import os |
####multiprocessing
multiprocessing模块提供了一个Process类来代表一个进程对象
1 | from multiprocessing import Process |
####Pool
用进程池的方式批量创建大量子进程:
1 | from multiprocessing import Pool |
####子进程
很多时候,子进程并不是自身,而是一个外部进程。创建子进程后,还需要控制子进程的输入和输出
subprocess模块用于启动一个子进程,然后控制其输入和输出
1 | import subprocess |
子进程需要输入,可以使用communicate()方法输入(下面代码没有看懂,希望有大神指点指点):
1 | import subprocess |
####进程间通信
Python的multiprocessing模块包装了底层的机制,提供了Queue、Pipes等多种方式来交换数据,以实现Process之间的通信:
1 | import os, time, random |
多线程
多任务可以由多进程完成,也可以由一个进程内的多进程完成。
进程是由若干个线程组成的,一个进程至少有一个线程
线程是操作系统直接支持的执行单元。
Python的线程是真正的Posix Thread,而不是模拟出来的线程。
1 | # Python中 _thread和threading用于多线程。其中_thread是低级模块,threading是高级模块,它对_thread进行了封装。 |
任何进程默认会启动一个线程,这个线程称为主线程(MainThread),Python的threading模块下面的current_thread()函数用于返回当前线程的实例。子线程的名字在创建时指定。名字仅仅在打印时用来显示,完全没有其他任何意义。默认情况下,Python自动给线程命名为Thread-1, Thread-2……
Lock
线程的调度是由操作系统决定的
1 | # 没有使用Lock之前: |
两个线程同时一存一取,就可能导致余额不对,你肯定不希望你的银行存款莫名其妙地变成了负数,所以,我们必须确保一个线程在修改balance
的时候,别的线程一定不能改。
如果我们要确保balance
计算正确,就要给change_it()
上一把锁,当某个线程开始执行change_it()
时,我们说,该线程因为获得了锁,因此其他线程不能同时执行change_it()
,只能等待,直到锁被释放后,获得该锁以后才能改。由于锁只有一个,无论多少线程,同一时刻最多只有一个线程持有该锁,所以,不会造成修改的冲突。创建一个锁就是通过threading.Lock()
来实现:
1 | import threading, time |
使用Lock的优缺点:
锁的好处就是确保了某段关键代码只能由一个线程从头到尾完整地执行,坏处当然也很多,首先是阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了。其次,由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁,导致多个线程全部挂起,既不能执行,也无法结束,只能靠操作系统强制终止。
Python虽然不能利用多线程实现多核任务,但可以通过多进程实现多核任务。多个Python进程有各自独立的GIL锁,互不影响。
Threadlocal
用一个全局dict
存放所有的Student
对象,然后以thread
自身作为key
获得线程对应的Student
对象
1 | # 实现 |
用Threadlocal替代上面自己创建的dict
1 | import threading |
全局变量local_school
就是一个ThreadLocal
对象,每个Thread
对它都可以读写student
属性,但互不影响。你可以把local_school
看成全局变量,但每个属性如local_school.student
都是线程的局部变量,可以任意读写而互不干扰,也不用管理锁的问题,ThreadLocal
内部会处理。
可以理解为全局变量local_school
是一个dict
,不但可以用local_school.student
,还可以绑定其他变量,如local_school.teacher
等等。
ThreadLocal
最常用的地方就是为每个线程绑定一个数据库连接,HTTP请求,用户身份信息等,这样一个线程的所有调用到的处理函数都可以非常方便地访问这些资源。
###进程 VS 线程
Master-Worker模式
Master负责分配任务,Worker负责执行任务,因此,多任务环境下,通常是一个Master,多个Worker。
如果用多进程实现Master-Worker,主进程就是Master,其他进程就是Worker。
如果用多线程实现Master-Worker,主线程就是Master,其他线程就是Worker。
多线程和多进程的优劣
正则表达式
正则表达式是一种用来匹配字符串的有力的武器
在正则表达式中,直接给出字符,就是精确匹配。
- \d:one number
- \w: one letter or one number
- .: everything
- *:任意个字符(包括0个)
- +: 至少一个字符
- ?: 0个或1个字符
- {n}:n个字符
- {n, m}: n-m个字符
- \s: 可以匹配一个空格(也包括Tab等空白符)
进阶
更精确地匹配,使用[]
1 |
|
re模块
1 | import re |
常见的判断方法:
1 | import re |
切分字符串
用正则表达式切分字符串比用固定的字符更灵活。
1 | # 普通的切分代码: |
分组
正则表达式还有提取子串的强大功能。用()表示的就是要提取的分组(Group)。
1 | # 分组 |
贪婪匹配
正则匹配默认是贪婪匹配,也就是匹配尽可能多的字符。
编译
使用正则表达式时,re模块内部会做下面两件事:
- 编译正则表达式
- 用编译后的正则表达式去匹配字符串
如果一个正则表达式要重复使用上千次,出于效率的考虑,可以预编译该正则表达式并保存下来,接下来重复使用的时候就可以跳过编译这个步骤,直接匹配了:
1 | re_name = re.compile(r'bowen[a-zA-Z]{3}') |
小节练习:
1 | ''' |
常用内建模块
datetime
1 | # 获取当前时间 |
下面截图是Python官方关于时间、日期转换的介绍:
小节练习
1 | """ |
collections
Collections 是Python内建的一个集合模块,提供了很多有用的集合类
namedtuple
1 | # namedtuple |
deque
1 | # namedtuple |
defaultdict
1 | # defaultdict |
orderedDict
1 | # OrderedDict |
Counter
1 | # Counter: 一个简单的计数器 |
base64
Base是一种用64个字符来表示任意二进制数据的方法。
要让记事本这样的文本处理软件能处理二进制数据,就需要一个二进制到字符串的转换方法。
Base64 是一种最常见的二进制编码方法
Python内置的base64可以直接进行base64的解编码:
1 | # base64 |
由于标准的Base64编码后可能出现字符+和/,在URL中就不能直接作为参数。
一种叫做“Url safe”的base64编码,其实就是把字符+和/分别编程-和_:
1 | # 使用url-safe的base64编码 |
1 |
|
常见函数
.join()
用于将序列中的元素以指定的字符连接生成一个新的字符
用法:
Str.join(sequence)
- Sequence – 要连接的元素序列
capitalize ()
-capitalize()方法返回字符串的一个副本,只有它的第一个字母大写。对于8位的字符串,这个方法与语言环境相关。
用法如下:
1 | str.capitalize() |
index()
index(content):用于判断给定字符的下标
strip()
用于移除字符串头尾指定的字符(默认为空格或换行符)或字符序列
*注意: *该方法只能删除开头或是结尾的字符,不能删除中间部分的字符
bytes()
描述:
bytes函数返回一个新的bytes对象,该对象是一个0<=x<256区间内的整数不可变序列。它是bytearray的不可变版本。
语法如下:
1 | class bytes([source[, encoding[, errors]]]) |
参数解释:
- 如果source为整数,则返回一个长度为source的初始化数组;
- 如果source为字符串,则按照指定的encoding将字符串转换为字节序列;
- 如果source为可迭代类型,则元素必须为[0.256]中的整数
- 如果source为与buffer接口一致的对象,则此对象也可以被用于初始化bytearray;
- 如果没有输入任何参数,默认就是初始化数组为0个元素
struct
Python提供了一个struct模块来解决bytes和其他二进制数据的转换
struct的pack函数把任意数据类型编程bytes:
1 | # struct |
小节练习
1 | import struct |
hashlib
Python的hashlib提供了常见的摘要算法: 如MD5, SHA1等等
摘要算法: 即哈希算法、散列算法。把任意长度的数据转换为一个长度固定的数据串(通常用16进制的字符串表示)。
摘要算法就是通过摘要函数对任意长度的数据data计算出固定长度的digest,目的是为了发现原始数据是否被篡改过。
以MD5为例,计算出一个字符串的MD5值:
1 | # hashlib |
摘要算法应用
任何允许用户登录的网站都会存储用户登录的用户名和口令。
正确的保存口令的方式是不存储用户的明文口令,而是存储用户口令的摘要
当用户登录时,首先计算用户输入的明文口令的MD5,然后和数据库存储的MD5对比,如果一致,说明口令输入正确,如果不一致,口令肯定错误。
小节练习
1 | import hashlib |
常用口令的MD5很容易被计算出来,所以,要确保存储的用户口令不是那些已经被计算出来的常用口令的MD5,这一方法通过对原始口令加一个复杂字符串来实现,俗称“加盐”
1 | def calc_mad(password): |
这样只要salt不被黑客知道,即使用户输入简单口令,也很难通过MD5反推口令。
练习
1 | import hashlib |
*注意: *
摘要算法不是加密算法,不能用于加密,只能用于防篡改,但是它的单向计算性决定了可以在不存储明文口令的情况下验证用户口令
itertools
Python的内建模块itertools提供了非常有用的用于操作迭代对象的函数
“无限”迭代器
1 | # count()会创建一个无限迭代器。 |
itertools提供的几个迭代器操作函数:
1 | # chain() |
XML(解析)
操作XML有两种方法:DOM和SAX。正常情况下,优先考虑SAX,因为DOM实在太占内存
解析XML时,我们关心三件事情: start_element, end_element和char_data。
1 |
其他知识
Python- 变量前加或者*
➕*
➕ **
Python标准注释
1 | #!/usr/bin/env python3 |
关于切片
对于切片,[:]中:前后两个数字必须满足从小到大的顺序,同时,包括前者,但并不包括后者
关于Dict
Python中,可以使用if判断key是否在dict中,但是不能直接使用value判断其是否在dict中
关于JSON
标准JSON中不能有多余的——逗号(,)
1 | import pickle |
关于全局变量
参见:
https://blog.csdn.net/songyunli1111/article/details/76095971
关于join()
Join()函数并不会改变原有的序列