【学习笔记】python
1. 基础
1.1 基础
- python是高级语言,CPU并不认识python代码,需要运行的时候才会动态翻译成cpu指令:python源码经过编译后,变成了一个个的字节码文件:.pyc,是一个二进制文件,然后不断的取出字节码的指令解释执行;
- python的数值类型三种:整型(int):包括正和负,其次boolean是整型的子类; 浮点型(float):小数部分和整数部分组成,其次也可以用科学计数法表示:2.5e2 = 2.5 * 10^2 = 250; 复数(complex):a+bj,或者complex(a, b),实部和虚部都是浮点型;
- isinstance(obj, classA) 会检查对象是否是目标类型的实例,子父类也会满足;hasattr(obj, x) 会检查对象是否有属性x
- 优先级:not>and>or; 乘方>按位取反>四则运算>位移>位运算>比较运算>逻辑运算
- for循环变量的作用域是在for结束后依旧有效,但是列表推导式中的变量在列表外就失效了;
- print()每执行一次默认在最后加上了\n,也就是默认会换行;if想要不换行则在后面加上end,例如 print(i, end = '')
- 逻辑操作符都是短路操作,优先级not>and>or;逻辑操作的返回值是最终能确定逻辑表达式值的最后一个变量:
3 and 4 # 结果为4
3 or 4 # 结果为3,3就可以直接确定逻辑表达式为真了,后面就不执行了;
- assert:断言,简单的理解就是断言为假的时候就会触发AssertionError异常
- if知道数组的大小,那创建list的时候就指定大小:lst = [None] * n;不断的append会不断地创建新的副本,if内容不变的时候,用tuple代替list节约内存;
- False,0,'',[],{},(),set()都可以视为False,但不是None。
1.2 关于命名
- 类和异常的命名采用每个单词首字母大写的方法(MyClass);常量采用全部字母大写加下划线的方法(FILE_SIZE);
- 其余所有(包、模块、变量、方法)都采用小写字母的方法(my_method);
- if方法或者成员属性为私有的,则加单下划线(self._member),if父类想要防止和子类重名,可以加__双下划线,这时候会被解释权自动改名,加上类名为前缀;
- 单下划线表示的是protected的属性,只允许本身和子类进行访问,一般约定单下划线开头的函数时模块私有的,所有from module import * 时不会进行导入,同时不建议直接在子类中访问父类中protected的属性,可以通过方法的方式(get_attr)来访问;
- 双下划线表示的是private的属性,只能是允许当前类进行访问,连子类也不可以,这类属性在运行时会自动改名:_类名__属性
- 使用双下划线开头的属性,实际上依然可以在外部访问,所以这种方式我们一般不用,(因为其实就是python将其自动改名了)
1.3 关于False
在python中,以下对象会被看作是False:
- 常量None和False
- 值为0的类型:0, 0.0, 0j, decimal.Decimal(0), fractions.Fraction(0,1)
- 空的序列和容器:’‘, (), {}, [], range(0), range(8,5)等
- 该对象的类定义了双下划线bool方法,且该方法返回False
- 该对象的类定义了双下划线len方法,且该方法返回0
1.4 关于比较
- 任何两个对象都可以比较,但是在python3中,字符串不允许和任何类型比较; 链式比较:4>33 # True 4>3 and 33(优先级:比较高于等于)
- is判断是否是指向同一个对象(判断两个对象的id是否相等),== 会调用双下划线eq方法判断是否相等(判断两个对象的值是否相等)
- 空不可变对象is空不可变对象 为True;空可变对象==空可变对象 为True; 不可变对象的id是相同的,可变对象的id是不同的;
a, b = 1, 1
print(a is b) # True,相同值的不可变对象赋值的地址相同
a, b = [], []
print(a is b) # False,可变对象的id是不同的;
- 字典间的比较时:只要是同样的键值对,用==比较时,就是true
# 下面是不同的字典初始化的方法
a = dict(one=1, two=2, three=3)
b = {'one': 1, 'two': 2, 'three': 3}
c = dict(zip(['one', 'two', 'three'], [1, 2, 3]))
d = dict([('two', 2), ('one', 1), ('three', 3)])
e = dict({'three': 3, 'one': 1, 'two': 2})
f = dict({'one': 1, 'three': 3}, two=2)
a == b == c == d == e == f # True
1.5 关于赋值
- python中的赋值语句有很多需要注意的地方,例如最常见的交换
众所周知,在python中是可以直接交换两个变量的值的,不需要像其他语言一样引入第三个变量
a, b = b, a # 这其实是利用了解包,当后面的b,a这样写的时候其实是装包成为了元组(b,a),然后进行赋值的时候其实就是将这个元组解包赋值给了a,b
# 但是需要注意的是,等号左边的两个不能有依赖关系
x = [0, 1]
i = 0
i, x[i] = 1, 2 # 0,2 因为前面两者有依赖关系,所以当i更新后,变更的是x[1],而不是x[0]!!
- 赋值中= 和+= 的区别:
L = L+L相当于重新赋值,L的id会发生改变
L = [1,2]
M = L
L = L + L
L.append(1)
print(L, M)
-------
2197624871872
2197625196800
[1, 2, 1, 2, 1] [1, 2]
L += L 是可变对象的修改,L的id不会发生改变
L = [1,2]
M = L
L += L
L.append(1)
print(L, M)
------------
[1, 2, 1, 2, 1] [1, 2, 1, 2, 1]
1.6 关于条件分支
- 条件分支在不指明指定的返回值时,会默认返回None,在写的时候,建议函数或者方法要么都有显式的返回值,要么都没有,不要混用,此外对于显式返回的分支,每个分支的返回值类型和个数尽量相同;
- break可以立即退出循环语句,包括else;pass是用来在循环或者判断中进行占位的(比如暂时没想好功能该怎么写,就先占位);
- 在for或者while循环中,if在这个循环外面有else,那么只有当整个循环过程中没有任何一个满足if的条件时,才会在执行结束后执行else后的,否则不会执行
for i in range(4):
if i == 2:
print(i, "found")
break
print(i, "not what we want")
else:
print("not found")
# 输出:只要有满足if的那else就不会执行
0 not what we want
1 not what we want
2 found
1.7 深拷贝浅拷贝
- 对于不可变对象(字符串、元祖、数值类型,bool),浅拷贝和深拷贝都是和赋值操作一样,对象的id与原来的一样;
- 对于可变对象(列表、字典、集合),深拷贝是拷贝了原来对象的全部元素,包含多层的嵌套;浅拷贝顾名思义,就是只是简单的拷贝了一层,对于里面的复杂结构(比如列表里的嵌套列表),同样只是引用,所以修改原来的值会影响;
- 通俗点理解:赋值就是跟原来对象完全一样(因为只是得到了引用);浅拷贝对于无复杂结构进行了完全拷贝,对复杂结构得到了引用;深拷贝彻底进行了完全拷贝;
a = [1,2]
import copy
b = copy.deepcopy(a)
print(id(a) == id(b))
print(id(a[0]) == id(b[0]))
---------
False
True # 一般的解释器会把相同的数值类型赋予相同的地址;
1.8 可变对象和不可变对象
每个对象都保存了三个数据:
id(标识/地址) type(类型) value(值)
- 可变对象:列表、字典、集合
- 不可变对象:数值类型、字符串、元祖、bool
a[0] = 10 # 这个是在改对象,不会改变变量的指向,只是指向的变量的值变了;
a = [4,5,6] # 这个是在改变量,会改变变量指向的对象;
# 可变对象和不可变对象作为函数参数:
>>> def myfunc(l): # l实际上是得到了实参的引用,然后对对象进行了修改;
... l.append(1)
... print(l)
...
>>> l = [1, 2, 3]
>>> myfunc(l)
[1, 2, 3, 1]
>>> l
[1, 2, 3, 1]
>>> def myfunc(a): # 对于不可变对象,形参的a实际上是一个新的地址,而全局上的a值时没有改变的;
... a += 1
... print(a)
...
>>> a = 2
>>> myfunc(a)
3
>>> a
2
2. 序列
序列就是用来保存一组有序的数据,所有的数据在序列中都有唯一的索引;
-
可变序列:列表(list);
-
不可变序列:字符串(str),元祖(tuple);
-
if seq 原理:
返回的是bool(seq),会调用seq.__bool__,if seq对应的类没有__bool__,则调用__len__,if也没有__len__且非None,会默认返回true
2.1 列表(list)
- 列表就是存储对象的对象;
# 数组的创建
a = [] # 创建数组
---
# 数组的增删改查
a.append(1) # 在末尾添加元素1;
a.append([1,2,3]) # 在末尾添加一个列表,最后返回就是[1,[1,2,3]],整体插入
a.extend([1,2,3]) # 扩展a序列,最后返回时[1,1,2,3],化整为零;list的方法append是整建制的追加,而extend是个体化的扩编
a.insert(2,1) # 在索引2添加元素1;
a.pop() # 删除最后索引的元素[1,1,2]
a.pop(1) # 删除索引1的元素[1,2,3],有返回值
del a[1] # 删除索引1的元素
a.remove(3) # 删除元素3,[1,1,2],if有多个3,只删除第一个
a.clear() # 清空列表
a[1] = 4 # [1,4,2,3] 更改元素
a[1] # 4,查看索引1的元素
index = a.index(4) # 1,查看元素4的索引
---
# 数组的其他方法
size = len(a) # 获取a的长度
a.sort() # 数组升序排列,无返回值
temp = sorted(a) # 数组升序排列,有返回值,原始序列不变
a.count(3) # 获取元素3在数组中个数
a.reverse() # 反转数组
---
# 数组的切片
a[-1] # 3,数组的最后一个
a[1:3] # 左闭右开
a[1:3:-1] # 步长为负,从后开始
a[1:3] = [1,2,3,4] # 在给切片进行赋值时,只能使用序列,序列的个数可以和切片的长度不一致,意思是替代指定的切片;
a[1:3] = 'abc' # [1.'a','b','c',3],if左边的是切片,会将右边的转变为列表,然后替代左边切片里的元素,所以会改变原始列表长度
a[::-1] # 数组反转
---
for i in a: # 遍历数组的三种方法,尽量用1,2,避免出现下标
print(i)
for index, element in enumerate(a):
print("index at ",index, "is :",element)
for i in range(0, len(a)):
print("i:" , i, "element : ", a[i])
- 列表切片的注意
x = [4,1,0,3,5]
x[2] = []
print(x) # [4,1,[],3,5]
x[2:3] = []
print(X) # [4,1,3,5],这种方法会改变list的长度
x[2:3] = [2,2,2]
print(X) # [4,1,2,2,2,3,5],序列的个数可以和切片的个数不一样;
x[2:3] = 'abc'
print(x) # [4,1,'a','b','c',3,5] if左边是切片,会将又边的转化为列表,然后代替左边切片里的元素,所以会改变长度
- 需要注意的是:列表起始位置的内存地址不变,每次删除一个元素时会将后面的元素向前
data_lst = [1,2,3,4,5]
for data in data_lst:
data_lst.remove(data) #第一次删除1,然后列表变为[2,3,4,5],2在1的内存位置了,相当于向前移动了!!
# 但是for循环遍历时是位置,所以第二次删除的是3,第三次删除的是5,最后剩下了[2,4]
- 列表和字符串的长度是不一样的
a = ["1222",
"111",
"222"]
print(len(a)) # 3
b = """1
22"""
print(len(b)) # 4
2.2 元组(tuple)
- 元祖就是不可变的列表
t = () # 创建空元组;
t = 1,2,3,4 # 当元组不为空时,括号可以不写;
t = 1, # if元组不为空,至少要有一个逗号;if没有括号就是单变量!!
a,b,c,d = t # 元组的解包,这时候a=1,b=2;
# 所以if想要交换两个元素,其实可以直接
a,b = b,a # b,a就是一个元组,进行了解包;
# 在对一个元组进行解包时,变量的数量必须和元组中的元素的数量一致
# 也可以在变量前边添加一个*,这样变量将会获取元组中所有剩余的元素
a , b , *c = t # c = [3,4];
# 解包是对所有序列都可以的,不仅是元组;
# 但是注意集合本身是无序的,所以解包时容易出现问题;
2.3 字典(dict)
- 列表存储数据很好,但是查询的性能很差;而字典查询效率很高;
- 字典中每个对象都有一个名字(key),而每个对象就是value;这样一个键值对称为一项(item);
- 字典的值可以是任意对象;字典的键可以是任意的不可变对象(int、str、bool、tuple ...),但是一般我们都会使用str
- 字典的键必须是可哈希的,python中的不可变对象都是可哈希的,可变容器(list、dict)是不可哈希的,也就是不可以作为字典的key;
# 创建哈希表
hashtable = [] #通过数组创建,key即为数组下标;
hashtable[2] = "liming" #添加元素/修改元素
mapping = {} #通过字典创建;
d = {'name':a, 'age':18}
d = dict(name='孙悟空',age=18,gender='男') # 这些都可以创建
# 这样的时候if有key1不在哈希表里直接查询mapping[key1]会报错
from collections import defaultdict
mapping1 = defaultdict(int) #参数是一个函数,可以是int,str,float,list等,意思就是每个元素会默认的是当前的类型的默认值
# 比如现在就是默认的是0, list就会默认的是[]
# 另外,Counter这个函数对可迭代对象统计个数后,返回也是一个字典,并且这个字典if有元素不在,然后直接count1[a],这样不会有错。
---
# 字典的增删改查
mapping[key1] = value1 # 增加键值对
mapping.update(map2) #将map2中的键值对添加到mapping中,if有相同的key会进行替代;
mapping.pop("key1") #删除元素,有返回值
del mapping[key1] #删除键值对
mapping.clear() # 清空字典
mapping["key1"] = "liming" # 修改键值对;
mapping[key1] # 获得value1
mapping.get(key, default = None) # 第二个参数在key不存在时可以指定一个返回值;例如 mapping.get(key, []) key不存在的时候会返回一个空列表
---
# 字典的其他方法
len(mapping)
---
# 字典的遍历
for k in d.keys():
print(d[k]) #通过keys()遍历;
for v in d.values():
print(v) #通过values()遍历:
for k,v in d.items():
print(k, '=', v) #items()会返回字典中所有的项;
# 会返回一个序列,序列中是元组,[('name',a),('age',18)],这时候赋值其实是解包
# 在python3.6之后,dict是按照添加元素的顺序进行返回的;
# if想要dict是有序的,可以用OrderedDict
from collections import OrderedDict
dict1 = OrderedDict() # 则是按照元素的插入顺序;
---
# 其次注意字典的key只能是不可变对象;
2.4 集合(set)
- 集合和列表非常相似
- 不同点:
1.集合中只能存储不可变对象 ( s= {[1,2,3]},这就是错误的,因为列表是可变的)
2.集合中存储的对象是无序(不是按照元素的插入顺序保存)
3.集合中不能出现重复的元素
# 创建集合
s = set() #创建一个集合
s = {1,3,4} # 创建一个集合
---
# 集合的增删改查
s.add(value1) # 向集合中添加元素value1
s.update(s2) # 将s2中的元素添加到s中
s.pop() # 因为集合是无序的,所以随机删除一个元素
s.remove(a) # 删除集合中的a,if a 不存在会报错
s.discard(a) # 删除集合中的a, if a 不存在不会报错
s.clear() # 清空集合
---
# 集合的运算:这些操作最后返回的仍然是集合;
s1 & s2 # 集合的交集;
s1 | s2 # 集合的并集;s1
s1 - s2 # 集合的差集;
s1 ^ s2 # 集合的异或集:获取只在一个集合中出现的元素;
s1 <= s2 # s1 是否是s2的子集 s1.issubset(s2)等同
s1 < s2 # s1 是否是s2的真子集
s1.issuperset(s2)
s1.isdisjoint(s2) # s1和s2的不相交集
# 注意集合没有+
2.5 字符串
2.5.1 字符串转义
print("a'b") # a'b
print("""a'b""") # a'b
print("a\'b") # a'b
print("a'b") # a'b
print('a''b'"c") # abc
2.5.2 字符串格式化
- 字符串格式化的速度:f-string > + > % > format
- f-string格式
# 冒号后面为width指定宽度
a = 123.456
f"{a:10}" #' 123.456'
f"{a:010}" #'000123.456',最高位用0填充
f"{a:8.2f}"#' 123.46',注意8是指全部长度是8,不是小数点前是8
f"{a:4.2f}"#'123.46' width比实际小,则不用管了
- format格式
# {}里面的数字指的是在format参数元祖中的位置,0代表format的第一个参数
'{0}-{1}-{2}'.format(1,2,3) # 1-2-3,后面的1,2,3是一个元祖,可以理解为一种装包;
'{0}'.format([1,2,3]) #[1,2,3] 整个list是第一个参数
# :后面是格式,f为浮点,d为整数,前面一位数字表示字符串总长度;
# *是指从后面的元祖中读取宽度或精度
print('pi=%*.*f' %(8,3,pi) # pi= 3.142
print('%+f' %pi) # 显示正负号 +3.14159
# 假设数字是5
{:0>2d} #左填充 05
{:x<4d} #右填充 5xxx
3. 函数
3.1 函数基础
- 函数也是对象;函数可以用来保存一些可执行的代码,并且可以在需要时,对这些语句进行多次的调用
def fn(a,b):
代码块 #fn是函数对象,fn()调用函数;
# 函数在调用时,解析器不会检查实参的类型;实参可以传递任意类型;比如列表甚至是函数都行(fn(fn1));
def fn1(*a,b):
代码块 #可变参数,会将所有的实参保存到一个元组中(装包)
# 可变参数不是必须写在最后,但是注意,带*的参数后的所有参数,必须以关键字参数的形式传递
# 注意函数的参数默认值是在函数声明的时候初始化一次,之后就不会再改变了;
x = 12
def f1(a, b=x)
print(a,b)
x = 15
f1(4) # 4,12 # 在函数声明的时候b就已经被初始化成12了,之后不会再更改;
# if是可变变量作为参数入参,那只会在程序执行期间初始化一次,只要调用过程不主动指定入参,就会一直使用一开始初始化过的那个变量
class Test(object):
def process_data(self, data=[]):
data.sort()
data.append("End")
return data
test1 = Test()
print(test1.process_data()) # ['end']
test2 = Test()
print(test2.process_data()) # ['end','end'] 还是刚才的那个
----------------------------
# *形参不能接收关键字参数;
# **形参可以接收关键字参数,会将这些参数统一保存到一个字典中;**参数必须在所有参数的最后;(**kwargs的kw是key-word,也就是键值对)
# *形参是将参数打包成元祖,而**是将参数打包成字典;同时*还可以用来解包
----------------------------
t = (1,2)
fn(*t)
#传递实参时,可以在序列类型的参数前添加星号,这样他会自动将序列中的元素依次作为参数传递(解包),序列的个数必须和参数个数相同;
# 同样,也可以用**来对字典进行解包,字典的key和参数形参名一样;
----------------------------
return 后面可以跟任意的值,甚至可以是函数;
# 文档字符串
def fn(a: int,b: bool,c: str='hello') -> int:
#这些里面的类型没有任何意义,仅仅是指示说明;
···
可以写一些说明文字
···
----------------------------
if 想在函数内部修改全局变量的值,要加关键字 global;
scope = locals() # locals() 获取当前作用域的命名空间;
# 命名空间其实就是一个字典,是一个专门用来存储变量的字典;所以scope是一个字典,这个字典就是当前作用域中所有的变量;
scope['c'] = 1000 # 向字典中添加键值对就相当于在全局中创建了一个变量c;
globals_scope = globals() #globals()这个函数可以在任意位置获取全局的命名空间;
3.2 高阶函数
- 将函数作为参数,或者将函数作为返回值的函数是高阶函数;当将一个函数作为参数时,实际上是将指定的代码传递给了目标函数;
- 这样的话就可以定制一些函数,然后将函数作为一种“规则”传递给目标函数,然后目标函数根据这种“规则”对参数(原料)做出相应的处理;
1.将函数作为参数传递:
3.2.1 map()
- 参数:
1.函数
2.序列
将序列中的每个元素作用于函数,然后返回一个map对象,所以一般在外面加list,返回一个列表;
list(map(str, [1,2,3,4,5])) # ['1','2','3','4','5']
3.2.2 reduce()
reduce把一个函数作用在一个序列上,这个函数必须接受两个参数,然后把结果继续和序列的下一个元素做累计计算。
from functools import reduce
def fn(x, y):
return x*10 + y
reduce(fn, [1,3,5,7,9]) # 13579
3.2.3 filter()
filter()可以从序列中过滤出符合条件的元素,保存到一个新的序列中
- 参数:
1.函数,根据该函数来过滤序列(可迭代的结构)
2.需要过滤的序列(可迭代的结构) - 返回值:
过滤后的新序列(可迭代的结构) - filter函数是把元素作用在函数上,然后根据函数的true还是false来决定是保留还是丢弃元素,注意和map的区分;
# 筛选出序列为奇数的元素
def is_odd(n):
return n%2 == 1
lst1 = filter(is_odd, [1,2,3,4])
3.2.4 sorted()
sorted()函数对序列进行排序,第一个参数是序列,第二个参数key可以指定一个函数,意思是将序列的元素作用在这个函数上然后再进行排序;
sorted([36, 5, -12, 9, -21], key=abs) # 将每个元素取绝对值后再排序
[5, 9, -12, -21, 36]
3.2.5 lambda函数表达式
有时候一个函数用一次就再也不用了,就可以用lambda表达式;匿名函数一般都是作为参数使用的;
语法:lambda 参数列表 : 返回值
r = filter(lambda i : i > 5 , l)
3.2.6 列表推导式和生成器表达式
# 列表推导式:
lst1 = [n for n in names if len(n) > 4] #提取出names中长度大于4的
# 当然也可以filter和lambda来代替
lst2 = list(filter(lambda n: len(n) > 4, names))
def fun1():
return [lambda x: i * x for i in range(4)] # 这其实是返回一个列表,列表里的元素是lambda函数,一共有四个
[<function <listcomp>.<lambda> at 0x000001BABCA720D0>, <function <listcomp>.<lambda> at 0x000001BABCB15048>, <function <listcomp>.<lambda> at 0x000001BABCB150D0>, <function <listcomp>.<lambda> at 0x000001BABCB15158>]
lst = [lambda x: x*i for i in range(4)]
res = [m(2) for m in lst]
print res #[6,6,6,6]
# 原因就在于上面的列表推导式其实是相当于一个闭包,和下面的等同:
def func():
fun_list = []
for i in range(4):
def foo(x):
return x*i
fun_list.append(foo)
return fun_list
for m in func():
print m(2)
# func是一个四个函数的列表,但是当运行m(2)时,跳转到foo函数里,这时候没有i就会去函数外部寻找,但是这时候外面的for已经循环完毕了,所以i直接就是3
# 注意函数没有调用,就只是简单的定义,并不会把i值传递给函数里面,函数调用和函数定义是有区别的;
def func():
fun_list = []
for i in range(4):
def foo(x, i=i): #当有了这个默认值后,在定义的时候就已经把i的值传给了函数,也就是说默认值参数只在函数初始化的时候初始化一次,之后不变;
return x*i
fun_list.append(foo)
return fun_list
for m in func():
print m(2) #[0,2,4,6]
# 生成器表达式:
a = (n for n in names if len(n) > 4) # 将中括号换成圆括号;但是a会是一个生成器对象,并不是列表这种直接用的东西;
<generator object <genexpr> at 0x0000020CE2EC4C48
# 所以当想要生成器开始迭代的时候,可以用for
for t in a:
print(t)
2.将函数作为返回值
3.3 闭包
- 将函数作为返回值返回,这就是一种闭包,通过闭包可以创建一些只有当前函数能访问的变量,可以将一些私有的数据藏到闭包里;
闭包顾名思义就是一种封闭的包裹,当调用了一个函数A时,这个函数A会返回一个函数B给我们,这个返回的函数B就是闭包,在调用函数A的时候传递的参数叫做自由变量;
形成闭包的条件:
1.函数嵌套;
2.将内部函数作为返回值返回;
3.内部函数必须要使用到外部函数的变量;
def fn(name):
a = 10
# 函数内部再定义一个函数
def inner():
print('我是fn2', a, name)
# 将内部函数 inner作为返回值返回
return inner
r = fn()
# inner就是一个闭包,name和a就是自由变量,当fn这个函数的声明周期结束后,name和a这两个变量依然是存在的,因为它们被闭包引用了,不会被回收;(被引用的自由变量会和闭包一起存在,其实已经离开了创造它的环境)
# r是一个函数,是调用fn()后返回的函数,也就是inner
# 这个函数是在fn()内部定义,并不是全局函数
# 所以这个函数总是能访问到fn()函数内的变量
# 变量是里面能看到外面的,但是外面看不到里面的;通过闭包的方式就能够使r接触到fn内部的变量,也就是外面能看到里面的了;
- 闭包的最大特点就是将父函数(即外部函数)的变量和内部函数(闭包)绑定,并且返回绑定变量后的函数(即闭包),这样即便生成闭包的环境(父函数)已经释放,闭包仍然存在;
参考闭包
3.4 装饰器
3.4.1 装饰器
在写程序的时候,if我们想扩展一个函数,但是我们要尽量不去动原始的函数,而是要遵从开闭原则,即开放对程序的扩展,而关闭对程序的修改;
装饰器其实就是一个函数,这个函数什么功能呢?它接收一个旧函数作为参数,然后在装饰器里对它进行装饰包装,然后以一个新函数作为返回;这样就可以在不修改原始函数的情况下对函数进行扩展;
- 装饰器就是闭包的一种,只不过传递的变量是函数;
例如:
在上图中,b为被装饰的函数,a为装饰器@a对应的函数,装饰后返回一个新的函数b
流程:
1.python解释器发现了装饰器@a,就去调用对应的函数a
2.函数a需要有一个参数,接着传入@a下面修饰的函数作为参数,也就是b
3.a()函数执行,返回一个新的函数b = a(b)
可以直接在旧函数上声明@装饰器函数
可以给函数多个装饰器,装饰的时候从内向外;
3.4.2 多个装饰器
def AA(func):
print('AA 1')
def func_a(*args, **kwargs):
print('AA 2')
return func(*args, **kwargs)
return func_a
def BB(func):
print('BB 1')
def func_b(*args, **kwargs):
print('BB 2')
return func(*args, **kwargs)
return func_b
@BB
@AA
def f(x):
print('F')
return x * 10
print(f(1))
-----
AA 1
BB 1
BB 2
AA 2
F
10
- 注意函数和函数调用是不一样的,f是一个函数,f()是函数调用;
- 装饰器刚装饰的时候就会立即执行,从内向外,所以先执行AA,也即f = AA(f),返回了func_a,此时的f已经是func_a,然后再装饰,f = BB(f) = BB(func_a),f变为了func_b
- 然后函数执行,f(1)也就是func_b(1),返回闭包的参数,也就是func_a,再执行func_a,返回闭包的参数f,最后再执行f;
3.4.3 @property和@setter
- @property是一个装饰器,其对应的函数为property,if我们想要隐藏类的属性,那可以在__init__的时候将属性设置为_下划线,然后提供get和set方法,当有了这两个方法后,我们可以通过property函数直接将其变为一个属性;
例如 score = property(get_score, set_score); - 当然也可以用更简单的方法就是直接使用装饰器@property ; @x.setter
- 使用@property装饰器来创建只读属性,@property装饰器会将方法转换为相同名称的只读属性,可以与所定义的属性配合使用,这样可以防止属性被修改。
class Student():
def __init__(self):
self._score = None
@property
def score(self):
return self._score
@score.setter
def score(self, value):
if value < 0 or value > 100:
raise ValueError('score must between 0 ~ 100!')
self._score = value
if __name__ == "__main__":
s = Student()
s.score = 66
print(s.score)
- 像上面一样,可以直接像利用属性一样来操作了,我们为两个同名函数(score)配置了装饰器,当把函数作为变量读取时会调用property;当把函数作为变量赋值时会调用setter对应的函数,
- 注意前提是@.setter装饰器必须在@property装饰器的后面,且两个被修饰的函数的名称必须保持一致, 即为函数名称。
3.5 nolocal与global
在python的函数里,可以直接引用外部变量,但是不能操作外部变量,这样会报错;
- 变量在寻找时,是按照局部作用域、闭包、全局作用域、内嵌这样的顺序去查找的;
- 函数内可以读取到在函数外的变量(因为在局部搜不到后会去全局进行搜索),但是当想要修改时(例如x = x+1),就会报错,因为函数会把这个变量当做局部变量去使用,但是函数内又没有定义这个局部变量,就会报错(报错的内容就是说在定义前用了变量),那该怎么做呢,就是用global关键字,在函数内部声明这个变量是全局的;
- if想要在函数内部修改全局变量,可以使用global关键字,禁止在函数内使用global直接创建全局变量,当只需要读全局变量时,不需要global声明; 注意在函数外使用global关键字是无效的
- if在函数内部出现了全局变量名字一样的变量并且没有用global声明,其实就相当于直接在函数内部直接定义了一个新变量;
- 对于python来说,全局变量的使用是比较谨慎的,也不被提倡,所以有了一个新的关键字,nonlocal;nonlocal用在封装函数中,也就是闭包里,在外部函数中先声明变量,然后在内部函数中声明nonlocal,然后在闭包和外部函数中的变量就是同一个变量了;
count = 1
def a():
count = 'a函数里面' #如果不事先声明,那么函数b中的nonlocal就会报错(因为nonlocal不会去全局中搜索,所以不会找到最外面的count);if在这里直接声明nonlocal count 也是会报错;
def b():
nonlocal count
print(count)
count = 2
b()
print(count)
if __name__ == '__main__':
a()
print(count)
#最后的执行结果是:
a函数里面
2
1
# 第一行里的count是全局变量,而a函数里面的count是局部变量,是两个变量,在b里声明nonlocal,证明b和a中的是同一个变量
4. 类和对象
4.1 类基础
- 类用大驼峰来命名:MyClass; 类也是对象,类就是一个用来创建对象的对象,一切皆对象!
- 如果是函数调用,则调用时传几个参数,就会有几个实参;但是如果是方法调用,默认传递一个参数,这个实参是解析器自动传的,所以方法中至少要定义一个形参(self),这个默认传递的实参其实就是调用方法的对象本身!if是p1调的,那第一个参数就是p1,if是p2调的,那第一个参数就是p2;所以我们把这个参数命名为self,这个self就是指的是谁调用这个方法了,这个谁就是self;self就是当前对象
- 当我们调用一个对象的属性时,解析器会先在当前对象中寻找是否含有该属性,
如果有,则直接返回当前的对象的属性值,
如果没有,则去当前对象的类对象中去寻找,如果有则返回类对象的属性值,如果类对象中依然没有,则报错! - 类对象和实例对象中都可以保存属性(方法)
- 如果这个属性(方法)是所有的实例共享的,则应该将其保存到类对象中
- 如果这个属性 (方法)是某个实例独有,则应该保存到实例对象中
- python中一切都是对象,它们要么是类的实例,要么是元类的实例,元类就是类的类,可以通过type()来创建元类,在定义类时,可以通过meteclass参数来指定此类的元类;
p1 = Person()的运行流程
1.创建一个变量p1
2.在内存中创建一个新变量,这个变量的类型是Person
3.init(self)方法执行
4.将对象的id(地址)赋值给变量
- 继承,在类的括号里写父类,if没写,就是继承自object.
issubclass(a,b) 检查a是否是b的子类
isinstance(a,A) 检查a是否是A的实例
父类中所有的方法和属性都可以被继承
# 父类中的所有方法都会被子类继承,包括特殊方法,也可以重写特殊方法
class Dog(Animal):
def __init__(self,name,age):
# 希望可以直接调用父类的__init__来初始化父类中定义的属性
# super() 可以用来获取当前类的父类,
# 并且通过super()返回对象调用父类方法时,不需要传递self
super().__init__(name)
self._age = age
- if子类没有定义自己的初始化方法,父类的初始化方法会被默认调用;if子类有自己的初始化方法,没有显式的调用父类的初始化方法,则父类的属性不会被初始化;
- python中可以多重继承:
class C(A,B):
pass
# C继承自两个父类,AB,ifA,B中有同名的方法,会先调用A的,if C(B,A)那就会先调用B的,
- 多态:在函数的参数中,if要传入一个对象,其实不关注对象是什么类型的,只关注这个对象是否有某些属性和方法,只要有某些属性和方法,那就可以传递进去,这样保证了程序的灵活性;
4.2 类方法和类属性
- 类属性可以通过A.属性和a.属性访问,实例属性只能通过a.属性访问,类.属性无法访问;在类中以self作为第一个参数的方法是实例方法,实例方法可以通过类和实例调用,注意实例方也能通过类调用;通过实例调用时,会自动将当前对象传递给self,但是通过类调用时,不会自动传,所以,A.fangfa(a) = a.fangfa()
4.2.1 类属性
- 类变量是类的变量,当这个类有实例化的对象时,会把类变量拷贝一份给对象,对象拿到的只是类变量的副本,所以通过对象.属性来修改类变量的值并不会影响类自身和其他对象;
class TestClass(object):
#类变量
val1 = 100
def __init__(self):
#成员变量
self.val2 = 200
-----
a.val1 = 200
b.val1 # 100
TestClass.val1 = 300
b.val1 # 300 没有经过修改,跟类变量保持一致
a.val1 # 200 经过了修改,跟类变量没有关系了
4.2.2 类方法classmethod和静态方法staticmethod
- 在一个类里,出现最多的就是三种方法:被classmethod装饰的类方法;被staticmethod装饰的静态方法;用的最多的不带装饰器的实例方法;
class A(object):
def m1(self, n):
print("self:", self)
@classmethod
def m2(cls, n):
print("cls:", cls)
@staticmethod
def m3(n):
pass
# A.m1是一个还没有传实例的实例方法,它必须显式的传入一个实例对象进去,而a.m1会自动把当前实例对象传递给self。
# A.m1(a, 1) 其实和a.m1(1)是等价的
# m2是类方法,A.m2会自动将当前类对象A传递给cls;a.m2也可以通过实例对象a找到所属的类A,然后将当前类绑定到cls上
# A.m2(1) 和 a.m2(1)是等价的;
# m3是一个静态方法,这其实和函数是没有区别的,它与类和实例都没有进行绑定,只不过是在类里面罢了;所以通过类和实例都是可以引用的
# A.m3(1) 和 a.m3(1) 都能够引用;
# 静态方法其实就相当于定义了一个局部函数来专门为这个类服务;
子类会继承父类的类方法,但是不会继承父类的静态方法,静态方法的作用域仅在当前类中;
a = A()
a.m1(1) # self: <__main__.A object at 0x000001E596E41A90>
A.m2(1) # cls: <class '__main__.A'>
A.m3(1)
当程序开始运行时,一共发生了下面的几件事情:
1.开始创建一个class A对象,这个类也是一个对象(类对象),同时初始化类里的属性和方法,注意此时并没有实例!这时候其实是变量A指向了真正的类对象;
2.3.执行a=A(),这时候系统自动调用类里的构造器,构造出来了实例对象,变量a指向了实例对象;
4.调用a.m1(1):m1是实例方法,系统会自动把实例对象传递给self参数进行绑定,所以self和a都指向了实例对象;
5.调用A.m2(1):m2是类方法,系统会把类对象传递给cls参数,所以cls和A都指向了类对象;
左边都是变量名,右边才是真正的对象;
# 类方法
# 在类内部使用 @classmethod 来修饰的方法属于类方法
# 当被classmethod装饰时,函数相当于是被借调到子类中去作为一个独立方法,因此会影响到类变量的值;
# 而被staticmethod装饰时,函数作用域仅在当前类,子类中的变量不受影响;
# 类方法的第一个参数是cls,也会被自动传递,cls就是当前的类对象
# 类方法和实例方法的区别,实例方法的第一个参数是self,而类方法的第一个参数是cls
# 类方法可以通过类去调用,也可以通过实例调用,没有区别
@classmethod
def test_2(cls):
print('这是test_2方法,他是一个类方法~~~ ',cls)
print(cls.count)
# 静态方法
# 在类中使用 @staticmethod 来修饰的方法属于静态方法
# 静态方法不需要指定任何的默认参数,静态方法可以通过类和实例去调用
# 静态方法,基本上是一个和当前类无关的方法,它只是一个保存到当前类中的函数
# if不用staticmethod,可以通过类方法来调用,但是实例方法会报错;
# 静态方法一般都是一些工具方法,和当前类无关
---
# 当被staticmethod修饰时,函数作用域仅在当前类中,子类中的变量不会受影响;
class Spam:
num_instances = 0
@staticmethod
def count(cls):
cls.num_instances += 1
def __init__(self):
self.count()
class Sub(Spam):
num_instances = 0
class Other(Spam):
num_instances = 0
x = Spam()
y1, y2 = Sub(), Sub() # 子类没有,所以会调用父类的__init__初始化方法,但是cls指的是spam类;
z1, z2, z3 = Other(), Other(), Other()
x.num_instances, y1.num_instances, z1.num_instances # (6, 0, 0)
Spam.num_instances, Sub.num_instances, Other.num_instances # (6, 0, 0)
---
# 当被classmethod修饰时,函数相当于被借调到一个子类中作为一个独立方法
class Spam:
num_instances = 0
@classmethod
def count(cls): # 相当于子类中也有了count方法,自然调用的也就是子类中的num
cls.num_instances += 1
def __init__(self):
self.count() # 将self.__class__传给count方法
class Sub(Spam):
num_instances = 0
class Other(Spam):
num_instances = 0
x = Spam()
y1, y2 = Sub(), Sub()
z1, z2, z3 = Other(), Other(), Other()
x.num_instances, y1.num_instances, z1.num_instances # (1, 2, 3)
Spam.num_instances, Sub.num_instances, Other.num_instances # (1, 2, 3)
--
class Spam(object):
num_instances = 0
@staticmethod
def count():
Spam.num_instances += 1
def __init__(self):
self.count()
class Sub(Spam):
pass
# num_instances = 0
class Other(Spam):
pass
# num_instances = 0
x = Spam()
y1, y2 = Sub(), Sub()
z1, z2, z3 = Other(), Other(), Other() # 继承父类中的变量
print(x.num_instances, y1.num_instances, z1.num_instances) # 6,6 ,6
print(Spam.num_instances, Sub.num_instances, Other.num_instances) 6 6 6
---
class Spam(object):
num_instances = 0
@classmethod
def count():
Spam.num_instances += 1
def __init__(self):
self.count()
class Sub(Spam):
pass
# num_instances = 0
class Other(Spam):
pass
# num_instances = 0
x = Spam()
y1, y2 = Sub(), Sub()
z1, z2, z3 = Other(), Other(), Other() # 继承父类中的变量
print(x.num_instances, y1.num_instances, z1.num_instances) # 1,3,4
print(Spam.num_instances, Sub.num_instances, Other.num_instances) 1,3,4
------
class Foo:
count = 0
def __init__(self):
self.count = 0
def incr_one(self):
self.count += 1
@staticmethod
def incr_two():
Foo.count += 1
@classmethod
def incr_three(cls):
cls.count += 1
class Bar(Foo):
pass
foo = Foo()
bar = Bar()
foo.incr_one() # 注意实例变量和类变量不是一回事,现在foo.count = 1
bar.incr_one() #bar.count = 1
foo.incr_two() # Foo.count = 1,注意Bar自己没有count,所以是继承Foo的count,而且Bar还没有修改,所以Bar.count = 1!!!
bar.incr_three() # Bar.count = 2,这时候Bar已经更改了,所以和Foo的count没有关系了
Foo.incr_two() # Foo.count = 2
Bar.incr_three() # Bar.count = 3
Foo.incr_three() # Foo.count = 3
4.2.3 new__和__init
new是创建一个实例,而init是初始化一个实例
__new__方法就是类方法,所以第一个参数是cls。是类在实例化的时候要调用的,__new__方法返回什么,实例化的对象就是什么;
__init__方法是在__new__方法基础上完成初始化动作,不需要返回值,if__new__没有正确的返回当前cls类的实例,那__init__就不会被调用;
class A(object)
def __new__(cls):
'重写__new__方法'
return 'abc'
a = A()
print(a) #abc,实例化对象取决于__new__方法;
---
class B():
def __new__(cls):
print ("__new__方法被执行")
def __init__(self):
print ("__init__方法被执行")
b=B() # __new__方法被执行;因为没有正确的返回值,所以init方法不会被调用;
4.3 特殊方法
- 特殊方法也叫魔法方法,一般不需要手动调用,在一些特殊的时候会自动执行,以双下划线开始,
__init__和__new__ :在初始化和对象创建的时候调用;
__del__ 在结束时调用,进行垃圾回收;
__name__ 可以通过__name__属性获取到模块的名字
- 对于所有的类都有以下的属性,用dir()可以查看
__name__:类的名字(字符串)
__doc__:类的文档字符串
__bases__:类的所有父类组成的元祖
__dict__:类的属性组成的字典
__module__:类所属的模块
__class__:类对象的类型
4.3.1 all
在python中使用from 模块 import * 来进行导入时,能导入一个模块中不以下划线(_或者双下划线)开头的成员,if是以下划线开头,那就不会导入
除此之外,可以使用双下划线all来声明哪些成员可以被导入;这个变量的值时一个列表,元素是str,存放的是当前模块的一些成员,当其他文件以from module import * 进行导入(注意只在这种导入时起作用)时,只能导入双下划线all里的成员,不在这里面的是没有办法导入的;
__all__ = ['say','laugh'] # say和laugh都是方法
4.3.2 dict
__dict__是一个字典,用来返回对象的属性;
a.__dict__ # 返回一个字典,key是实例属性,value是对应的值,并不返回所有的属性,只返回和实例关联的实例属性;
A.__dict__ # 返回一个字典,返回所有实例共享的类属性和方法,但并不包括父类的属性;
dir(A) # 会返回一个对象的所有属性(包括从父类中继承的; 所以__dict__是dir的子集;
许多内嵌类型比如list并不拥有__dict__属性,所以要用dir
4.3.3 del
当内存不需要的时候调用这个删除方法,这是属于类的,也就是当这个类没用的时候,python解释器会自动调用;
4.3.4 call
__call__的作用是能够使一个类实例像函数一样去调用,if想要达到这个目的,我们需要在类中实现__call__方法;
例如x是X的一个实例,那么调用x.__call__(1, 2)就等同于x(1,2),这个类实例就像函数一样;
4.3.5 eq
当比较两个对象的时候(==),实际上是调用的对象里的双下划线eq方法,所以if想要自定义一些相等的时候,可以重写双下划线eq方法;
- 子类和父类进行比较时,都是会去调用子类的__eq__
- 当两个对象进行比较时,是去调用__eq__方法,而__eq__方法是按照对象的id进行比较的;
4.3.6 slots
python是动态语言,在创建了一个类的实例后,可以给该实例绑定任何属性和方法;如下面的例子:
class Student(object):
pass
# 定义了一个类
s = Student() # 类的实例
s.name = 'liming' # 给当前实例动态的绑定一个name属性;
def set_age(self, age):
self.age = age
from types import MethodType
s.set_age = Method(set_age, s) # 给实例绑定一个方法;
# 注意对当前实例绑定的属性和方法只对当前实例起作用;
# if想要对所有实例都起作用;可以给类绑定;
Student.set_age = set_age
- slots是用来限制实例的属性的,只允许添加slots里面的属性;
class Student(object):
__slots__ = ('name', 'age') # 用tuple定义允许绑定的属性名称
s = Students
s.name = 'limimg'
s.age = 24
s.score = 99 # AttributeError: 'Student' object has no attribute 'score'
4.4 可迭代对象和迭代器
- 可迭代对象包含迭代器;
- 可迭代对象类必须实现__iter__方法,迭代器类必须实现__iter__和next方法;
- __iter__方法返回的是迭代器类的实例; next方法返回迭代的每一步(当前迭代值);
参考:迭代器
class MyList(object): # 定义可迭代对象类
def __init__(self, num):
self.data = num # 上边界
def __iter__(self):
return MyListIterator(self.data) # 返回该可迭代对象的迭代器类的实例
class MyListIterator(object): # 定义迭代器类,其是MyList可迭代对象的迭代器类
def __init__(self, data):
self.data = data # 上边界
self.now = 0 # 当前迭代值,初始为0
def __iter__(self):
return self # 返回该对象的迭代器类的实例;因为自己就是迭代器,所以返回self
def next(self): # 迭代器类必须实现的方法
while self.now < self.data:
self.now += 1
return self.now - 1 # 返回当前迭代值
raise StopIteration # 超出上边界,抛出异常,必须有
# 在python3.7以上,生成器退出时的StopIteration会被解释器转换成RuntimeError,所以在这之后要用return代替raise StopIteration
my_list = MyList(5) # 得到一个可迭代对象
print type(my_list) # 返回该对象的类型
my_list_iter = iter(my_list) # 得到该对象的迭代器实例,
for i in my_list: # 迭代
print i #0,1,2,3,4
# iter有两种用法,一是接收函数iter(函数,参数),会不断的执行函数,直到返回值与参数相同或者stopiteration
# 二是iter与__iter__联系紧密,iter()是直接调用该对象的__iter__,所以其功能常用作创建迭代器;
例如:lst = [1,2,3,4] # lst就是一个可迭代对象
l1 = iter(lst) # 创建迭代器对象
print(next(l1) # 1
print(next(l2) # 2
print type(my_list_iter)
# 除了使用next以外,还可以使用for循环进行遍历
list=[1,2,3,4]
it = iter(list) # 创建迭代器对象
for x in it:
print (x, end=" ") # 1 2 3 4
# 判断一个对象是否是可迭代对象;
dir会检查对象含有什么方法,列出然后看是否有__iter__即可
print('__iter__' in dir([1,2,3]))
- 生成器(yield)是一种特殊的迭代器,其自动实现了__iter__和next方法,不需要手动实现;生成器往往是一种无限的,例如斐波那契数列
- 只要函数使用了yield那就不再是一个函数,而是一个生成器,这个函数会返回一个迭代器对象,只能用于迭代使用;
- 每次遇到yield时函数会暂停并保存当前所有的运行信息,返回yield的值;
def foo():
print("starting...")
while True:
res = yield 4
print("res:",res)
g = foo()
print(next(g))
print("*"*20)
print(next(g))
# 输出:
starting...
4
********************
res: None
4
# 1.程序运行开始后,因为有yield,所以foo函数并不会真正执行,而是得到一个生成器g;
# 2.直到调用next方法后,生成器开始迭代,先执行print,然后进入while循环
# 3.程序遇到yield后,直接将yiled想象成return后,return一个4,程序暂停,这时候并没有执行赋值操作,也就是res并不是4,然后next执行完毕;
# 4.然后遇到下一个next后,从刚才暂停的地方开始,但是要注意的是:res赋值右边是没有值的,因为刚才那个已经return4了,所以这时候res的值是None,然后在下一次循环后再次yield 4,然后程序再次停止;
4.5 元类(metaclass)
- 在Python里,万物皆对象,类A也是一个对象;
- 在python中,type除了可以用来查看一个对象的类型外,还可以用来动态的创建类;
type(类名, 父类的元祖(可以为空), 包含属性和方法的字典(名称和值))
# 假设这是之前的类:
class Foo(object):
foo = True
def greet(self):
print 'hello world'
print self.foo
# 当用type来创建这个类时:
def greet(self):
print 'hello world'
print self.foo
Foo = type('Foo', (object, ), {'foo': True, 'greet': greet})
# 这和上面是等价的:
f = Foo()
f.foo # True
f.greet() # hello world True
- 元类就是用来创建类的东西,可以这么理解:实例对象是类的实例,而类是元类的实例;
- 元类主要是为了控制类的创建行为;
- 元类主要做了三件事:拦截类的创建;修改类的定义;返回修改后的类;
class Foo(object):
name = 'foo'
def bar(self):
print 'bar'
# if想给这个类的每个方法和属性前面加上my_,然后再加上一个echo方法,就可以使用到元类;
# 1.定义一个元类,从type继承,因为元类是用来创建类的,而type也是;
class PrefixMetaclass(type):
def __new__(cls, name, bases, attrs):
'''cls:当前准备创建的类;
name:类的名字;在这里就是Foo
bases:类的父类集合;这里是object
attrs:类的属性和方法,是一个字典;'''
# 给所有属性和方法前面加上前缀 my_
_attrs = (('my_' + name, value) for name, value in attrs.items())
_attrs = dict((name, value) for name, value in _attrs) # 转化为字典
_attrs['echo'] = lambda self, phrase: phrase # 增加了一个 echo 方法
return type.__new__(cls, name, bases, _attrs) # 返回创建后的类
# 2.指示Foo使用PrefixMetaclass来定制类
class Foo(metaclass=PrefixMetaclass):
name = 'foo'
def bar(self):
print 'bar'
------
f = Foo()
f.name # 会报错,因为没有那么属性了
f.my_name # bar
- 元类就是类的类,常用在类工厂中;
- 在定义类的时候,可以通过metaclass参数来指定当前类的元类,python语句在执行时,会先查找类本身的metaclass属性,if没找到,会继续在父类中查找,if还没找到,在模块中查找,最后再用内置的type来创建类;
5. 异常
try:
代码块(可能出现错误的语句)
except 异常类型 as 异常名:
代码块(出现错误以后的处理方式)
else:
代码块(没出错时要执行的语句))
finally:
代码块(该代码块总会执行)
-----
def divide(x, y):
try:
return x/y
except Exception:
return 0
finally:
return -1
print(divide(1, 0)) # -1 只会返回-1,原因在于一个函数只能有一个return,会执行到return 0,但是finally总会执行,最后就返回-1
# 所以需要注意finally不要有return、break等操作,可能会导致异常的操作不能够正常执行返回;
-
异常也是一个对象,比如 : ZeroDivisionError类的对象专门用来表示除0的异常
NameError类的对象专门用来处理变量错误的异常 -
BaseException是所有异常类的基类,包括键盘中断、进程退出等异常,而Exception是所有不需要进程退出的异常基类,其关系如下:
- BaseException |- KeyboardInterrupt |- SystemExit |- GeneratorExit |- Exception |- (all other current built-in exceptions)
自定义异常类必须继承自Exception,不需要去关注键盘中断等这些异常;
-
不要直接使用except:这种语句,这种会捕获所有的异常,不方便之后的工作,except后要跟明确的异常类,并且按照从小到大的原则,BaseException和Exception放在最后一个;
-
可以使用 raise 语句来抛出异常,
raise语句后需要跟一个异常类 或 异常的实例,if后面啥也不跟,那会抛出 RuntimeError,丢失了原始的异常信息; -
OSerror 是操作系统错误,包括I/O操作失败,例如‘文件未找到’或者‘磁盘已满’等,不包括非法参数或其他偶然性错误;
6. 文件
模式 | 可做操作 | if文件不存在 | 是否覆盖 |
---|---|---|---|
r | 只读 | 报错 | 是 |
r+ | 可读可写 | 报错 | 是 |
w | 只能写 | 创建 | 是 |
w+ | 可读可写 | 创建 | 是 |
a | 只能写 | 创建 | 否,追加写 |
w+ | 可读可写 | 创建 | 否,追加写 |
- 打开文件,opne(file_name)会返回一个对象,if当前文件和目标文件在同一级目录下,则直接写名字就可以,其他的时候就必须用路径了。可以使用..来向上返回一级目录
. 是指当前目录; .. 是指当前目录的上一级目录
- read()用来读,会将内容保存为一个字符串返回;
- 关闭文件;对象.close(),这时候其实经常是忘记的,所以有了with..as语句
- rb方式打开的文档,读的是byte不是str,if文件内容是中文,r可以打开,rb会报错;
with open(file_name) as file_obj: #这其实就是和file_obj = open(file_name)一样,当出去这个with后,会自动关闭;
#打开file_name并返回一个变量file_obj
file_obj.read()
with open(file_name , 'x' , encoding='utf-8') as file_obj:
# 使用open()打开文件时必须要指定打开文件所要做的操作(读、写、追加)
# 如果不指定操作类型,则默认是 读取文件 , 而读取文件时是不能向文件中写入的
# r 表示只读的
# w 表示是可写的,使用w来写入文件时,如果文件不存在会创建文件,如果文件存在则会截断文件
# 截断文件指删除原来文件中的所有内容
# a 表示追加内容,如果文件不存在会创建文件,如果文件存在则会向文件中追加内容
# x 用来新建文件,如果文件不存在则创建,存在则报错
# + 为操作符增加功能
# r+ 即可读又可写,文件不存在会报错
# seek() 可以修改当前读取的位置
seek()需要两个参数
第一个 是要切换到的位置
第二个 计算位置方式
可选值:
0 从头计算,默认值
1 从当前位置计算
2 从最后位置开始计算
# tell() 方法用来查看当前读取的位置
7. 模块和包
- 模块是一段代码,表现为将写的代码保存为文件,这个文件就是一个模块;包是一个有层次的文件目录结构,由多个模块或者子包组成,就是一个包含双下划线__init.py__文件的目录,这个目录下有这个文件和其他模块;库是能够完成一定功能的代码的集合;
- 当导入模块的时候,python解释器会先去搜索具有该名称的内置模块,if没有找到,会在sys.path给出的目录列表中搜索名为导入名字.py的文件;sys.path会从PYTHONPATH初始化;
- 整个文件的顺序:文档字符串 -》 __future_模块的导入 -》 双下划线开头结尾的全局变量 -》import导入 -》全局变量
"""This is a module
Functions of this module
"""
from __future__ import print_function
__all__ = ['hello', 'world']
__version__ = 'V1.0'
import os
import sys
from time import sleep
sample_global_variable = 0
M_SAMPLE_GLOBAL_CONSTANT = 0
- 每行只能导入一个模块 import os,sys 不对; if从一个模块导入多个对象可以:from sys import stdin, stdout
- 一个py文件就是一个模块,模块也是一个对象!在每一个模块内部都有一个__name_属性,通过这个属性可以获取到模块的名字_name_属性值为 _main_的模块是主模块,一个程序中只会有一个主模块,主模块就是我们直接通过 python 执行的模块
- 一个文件夹就是一个包,一个包里可以有多个模块,包中必须含有一个__init__.py文件,一个包里会有__pycache_文件夹,这是模块的缓存文件, py代码在执行前,需要被解析器先转换为机器码,然后再执行,所以我们在使用模块(包)时,也需要将模块的代码先转换为机器码然后再交由计算机执行
而为了提高程序运行的性能,python会在编译过一次以后,将代码保存到一个缓存文件中,这样在下次加载这个模块(包)时,就可以不再重新编译而是直接加载缓存中编译好的代码即可;python中的package必须包含一个_init.py__的文件,可以为空,只要它存在就说明该目录应该被当做为一个package处理!!
- 在python中使用from 模块 import * 来进行导入时,能导入一个模块中不以下划线开头的成员,if是以下划线开头,那就不会导入
除此之外,可以使用__all_来声明哪些成员可以被导入;这个变量的值是一个列表,元素是str,存放的是当前模块的一些成员,当其他文件以from module import * 进行导入(**注意只在这种导入时起作用**)时,只能导入__all__里的成员,不在这里面的是没有办法导入的;
__all__ = ['say','laugh'] # say和laugh都是方法
- 导入的顺序按照标准库-》第三方标准库-》自定义库的顺序导入,每个类别之间有空行;同时对于系统库和第三方库使用绝对路径导入,对于项目库内的代码,可以使用相对路径
from sys_lib import module_a # 系统库用绝对路径
from third_lib import module_b # 第三方库用绝对路径
from .module_c.module_cc import classA # 本项目库可以使用相对路径
8. 日志
- 一般的日志等级从小到大,debug、info、notice、warning、error、critical。显示的信息越来越少;
日志等级 | 描述 |
---|---|
debug | 最详细的日志信息,典型应用场景是 问题诊断 |
info | 信息详细程度仅次于DEBUG,通常只记录关键节点信息,用于确认一切都是按照我们预期的那样进行工作 |
warning | 当某些不期望的事情发生时记录的信息(如,磁盘可用空间较低),但是此时应用程序还是正常运行的 |
error | 由于一个更严重的问题导致某些功能不能正常运行时记录的信息 |
CRITICAL |
logging.log(logging.ERROR, "this is a error log") # 会直接打印在输出台,python里默认的是>=warning的会打印;
# ERROR:root:This is a error log.
LOG_FORMAT = "%(asctime)s - %(levelname)s - %(message)s"
logging.basicConfig(filename='my.log', level=logging.DEBUG, format=LOG_FORMAT) # 使用logging.basicConfig进行配置,这个配置只在第一次配置的时候有用
# 在代码的相同目录下会生成my.log文件,里面的内容是:2017-05-08 14:29:53,784 - ERROR - This is a error log.这样的格式
- 日志采用 ‘%s, %s’,(str1,str2)这样的格式,采用logging模块来记录来而不是用print,不要直接使用外部数据来记录日志;
9. 调试
9.1 pdb
pdb是python自带的调试工具,有两种用法:
- 非侵入方法
# 无需修改源代码,在命令行下直接运行就能调试
python -m pdb filename.py
- 侵入方法
# 需要在被调试的代码中添加一行然后再正常运行;
import pdb
pdb.set_trace()
# 当在命令行下看到这个提示符时,说明已经正确打开了pdb
(Pdb)
# 然后就可以开始输入pdb命令了
命令 | 解释 |
---|---|
break 或 b | 设置断点 |
continue 或 c | 继续执行程序 |
list 或 l | 查看当前行的代码段 |
step 或 s | 进入函数 |
return 或 r | 执行代码直到从当前函数返回 |
exit 或 q | 中止并退出 |
next 或 n | 执行下一行 |
pp | 打印变量的值 |
- pdb一行可以设置多个断点,断点可以设置在import位置,不可以设置在空行;
- 进入pdb模式后,代码执行位置会停留在第一行有效非注释代码的位置;
- 有一些bug很难用pdb进行debug,此时可以用gdb,比如段错误(无法捕捉的python异常)、卡住的进程(无法用pdb进行跟踪)、控制之外的后台处理守护进程
9.2 代码检查
- 常用的代码检查工具:timeit、profile、cProfile、line_profiler、memory_profiler
- cprofile比profile快
- pylint:是一个python代码分析工具,能够查找不符合代码风格标准,默认的代码风格是PEP 8;
- too many instance attributes规定类属性不得多于7个
- too many arguments告警可以通过dict class等方式消除
- deprecated-lambda告警的消除做法是用列表推导代替map和filter
- 遍历设计范围和索引时尽量使用enumerate和range
10 并行
- ython并不能实现真正意义上的多线程,造成这种现象的原因是GIL(全局解释器锁),但是注意它并不是python的问题,而是目前使用最为广泛的使用c语言实现的python解释器CPython上。multithreading因为上述原因,同时只能由一个线程运行,if要利用多核心,需要用multiprocessing;
- 在python中一些看起来很简单的命令并不能保证原子性,例如下面的非线程安全的操作:
i = i + 1
L.append(L[-1])
L[i] = L[j]
可以使用threading.Lock()来加锁;
- python的多线程适用于阻塞式IO的场景,不适用于并行计算的场景,在阻塞式IO场景下,线程在执行IO操作时并不需要占用CPU时间,此时阻塞IO的线程可以被挂起的同时继续执行IO操作,而让出CPU时间给其他线程执行非IO操作。这样一来,多线程并行IO操作就可以起到提高运行效率的作用了。使用协程来处理并发场景。
- 使用subprocess模块管理子进程的时候,给communicate方法传入timeout参数(等待一定时间无响应就抛异常),以避免子进程死锁或者失去响应;
- 并行是多个CPU,是真正意义上的同时执行多个任务;并发是看起来是多个任务同时执行,实际上是采用的时间切片的方式,CPU来回切换;线程是CPU执行的最基本单元;进程是系统进行资源分配的基本单元;多线程相比多进程,无需重复申请资源,子线程和父线程是共享资源的,所以线程间的通信速度要快于多个进程;协程:又叫做微线程,协程的特点是只有一个线程在执行,只有当子程序内部发生阻塞或者IO时,才会交出执行权,线程的优点是:省去了线程间的切换开销;由于是单线程的不需要加锁,所以执行效率更高;进程间的通信方式:管道、共享存储器系统、消息传递系统、信号量;
- 关于python中的协程
asynico.run()用来运行最高层级的入口点main()函数;
await 后接一个可等待对象;并不会在开启并发;
asyncio.create_task()才是真正用来并发运行asyncio任务的多个协程;当一个协程通过这个函数被打包成一个任务时,该协程会自动排入日程准备立即运行;但是当前task并不会真正执行,而是等待await后才可以执行,也就是当前线程上有任务的时候task才能开始执行;
import asyncio
async def say_after(delay, what):
await asyncio.sleep(delay)
print(what)
async def main():
task1 = asyncio.create_task(say_after(1, "hello1"))
task2 = asyncio.create_task(say_after(2, "world1"))
await say_after(1, "hello2")
await say_after(2, "world2")
await task1 # 即使没有这两句也会执行hello1和world1,当上面创建任务的时候就已经准备执行了,只要有一个await,就会开始执行了;
await task2 # 直接用await是顺序执行,用create的是并行的,然后两者之间也是并行的;
asyncio.run(main())
# 输出:
hello2
hello1
world1
world2