变量
变量定义
# 什么是变量
用标识符命名的存储单元的地址称为变量,变量是用来存储数据的,通过标识符可以获取变量的值,也可以对变量进行赋值
作用域
创建、改变、查找变量名时,都是在一个保存变量名的空间中进行,该空间称作为作用域
Python中,一个变量的作用域在声明时就决定了,与在何处调用无关
函数作用域的LEGB顺序
L:local 函数内部作用域
E:enclosing 函数内部与内嵌函数之间
G:global 全局作用域
B:build-in 内置作用域
python在函数里面的查找分为4种,称之为LEGB,也正是按照这是顺序来查找的
可变类型和不可变类型
# 可变类型(mutable):列表、字典
当该数据类型的对应变量的值发生了改变,那么它对应的内存地址不发生改变,对于这种数据类,就称可变数据类型
# 不可变类型(immutable):数字、字符串、元组
对于不可变类型来说,变量保存的实际上都是对象的引用,对其进行修改时,会开辟一块内存空间创建了一个新的对象并指向它,然后将原对象的引用计数-1
# 例1
a = 1
print(a)
a = 2
print(a)
# 例2
b = [1, 2, 3]
print(id(b))
b[1] = 1
print(id(b))
is和==区别
is:比较的是两个对象的id值是否相等,也就是比较俩对象是否为同一个实例对象,是否指向同一个内存地址
==:比较的两个对象的内容/值是否相等,默认会调用对象的eq()方法
引用、浅拷贝、深拷贝
import copy
a = 'Allen'
a = b
c = copy.copy(a)
d = copy.deepcopy(a)
# 赋值:传递的a所指对象的引用,如果是可变类型修改后,影响原对象,不可变类型则创建一个新的对象,并指向新的对象,将原对象的应用计数-1
# 浅拷贝:将一个对象的引用拷贝到别一个对象,对拷贝的对象作出改动,会影响到原对象
# 深拷贝:将一个对象拷贝到别一个对象,对拷贝的对象作出改动时,不会影响到原对象
推导式
# 字典推导式
d = {'allen': 'name'}
print({v: k for v, k in d.items()})
# 列表推导式
print([i.upper() for i in 'abcdef'])
# 集合推导式
print({i for i in 'abcdef'})
# 生成器推导式
print((i for i in range(10)))
%:无法同时传递一个变量和元组
c = (250, 250)
s1 = "pos:%s" % c # 报一个异常:TypeError: not all arguments converted during string formatting
c = (250, 250)
s1 = 'pos:%s'(c,)
format:就不会存在上面的问题
c = (250, 250)
s1 = 'pos:{}'.format(c)
# python3.6支持 f-strings
c = (250, 250)
s1 = f'pos:{c}'
连接字符串用join还是+
# +
当用操作符+连接字符串的时候,每执行一次+都会申请一块新的内存,然后复制上一个+操作的结果和本次操作的右操作符到这块内存空间,因此用+连接字符串的时候会涉及好几次内存申请和复制
# join
join在连接字符串的时候,会先计算需要多大的内存存放结果,然后一次性申请所需内存并将字符串复制过去
函数
函数参数传递
# 例1
a = 1
def fun(a):
a = 2
# 由于是不可变类型,在向不可变类型变量赋值时,就是创建新的对象并引用,将原有的对象引用计数-1
fun(a)
print(a) # 结果为1
# 例2
a = []
def fun(a):
a.append(1)
# 因为list是可变类型,可以在原处修改
fun(a)
print a # [1] #
lambda
# lambda
语法:lambda argument_list:expression
lambda是python预留关键字,argument_list和expression有用户自定义;
argument_list:参数列表 # *arg, **kwargs, None
expression:表达式,表达式中出现的参数必须在argument_list中定义,并且表达式只能是单行的
lambda argument_list:expression表示的是一个函数,这个函数叫做lambda函数
# 三个特性
1.lambda函数式匿名的:所谓匿名函数,lambda函数没有名字,引用计数为0,使用一次就释放
2.lambda函数有输入和输出:输入是argument_list中传递的参数,输出是:根据expression计算出来的值
3.lambda函数一般功能简单:单行expression决定了lambda函数不可能完成复杂的逻辑,只能完成非常简单的功能
# 四种用法
1.将lambda函数赋值给一个变量,通过这个变量间接调用该lambda函数
2.将lambda函数赋值给其他函数,从而将其他函数用该lambda函数替换
3.将lambda函数作为其他函数的返回值,返回给调用者
4.将lambda函数作为参数传递给其他函数
函数式编程
纯粹的函数式编程语言编写的函数没有变量,因此,任意一个函数,只要输入是确定的,输出就是确定的,这种纯函数我们称之为没有副作用。而允许使用变量的程序设计语言,由于函数内部的变量状态不确定,同样的输入,可能得到不同的输出,因此,这种函数是有副作用的
函数式编程的一个特点就是,允许把函数本身作为参数传入另一个函数,还允许返回一个函数
Python对函数式编程提供部分支持。由于Python允许使用变量,因此,Python不是纯函数式编程语言
# 函数式编程的三大特性
函数式编程的一个特点就是,允许把函数本身作为参数传入另一个函数,还允许返回一个函数
1.immutable data 不可变数据
2.first class function
3.尾递归优化 # 尾递归优化技术——每次递归时都会重用stack!!!python不支持
函数式编程的几个技术
# filter函数
此时lambda函数用于指定过滤列表元素的条件。例如filter(lambda x: x % 3 == 0, [1, 2, 3])指定将列表[1,2,3]中能够被3整除的元素过滤出来,其结果是[3]
# sorted函数
此时lambda函数用于指定对列表中所有元素进行排序的准则。例如sorted([1, 2, 3, 4, 5, 6, 7, 8, 9], key=lambda x: abs(5-x))将列表[1, 2, 3, 4, 5, 6, 7, 8, 9]按照元素与5距离从小到大进行排序,其结果是[5, 4, 6, 3, 7, 2, 8, 1, 9]
# map函数
此时lambda函数用于指定对列表中每一个元素的共同操作。例如map(lambda x: x+1, [1, 2,3])将列表[1, 2, 3]中的元素分别加1,其结果[2, 3, 4]
# reduce函数
python3中从"functools"中导入
此时lambda函数用于指定列表中两两相邻元素的结合条件。例如reduce(lambda a, b: '{}, {}'.format(a, b), [1, 2, 3, 4, 5, 6, 7, 8, 9])将列表 [1, 2, 3, 4, 5, 6, 7, 8, 9]中的元素从左往右两两以逗号分隔的字符的形式依次结合起来,其结果是'1, 2, 3, 4, 5, 6, 7, 8, 9'
*args和**kwargs
当你不确定你的函数里将要传递多少参数时你可以用*args,它可以接受任意数量的参数:
def f1(*args):
for couter,item in enumerate(args):
print(f'{couter}-{item}')
f1(1, 2, 3, 4)# 我们可以传递任意个数的参数
**kwargs允许你使用没有事先定义的参数名:
def f2(**kwargs):
for key,value in kwagrs.items():
print(key,value)
*args和**kwargs可以同时存在,但是*args必须在**kwargs前面,
# 我们可以在调用函数传递参数是使用*和**
def f3(*args, **kwargs):
print(args,kwargs)
t = (1,2,3)
d = {'name':'allen','age':18}
f3(*t, **d) # *将t拆成一个一个位置参数:1, 2, 3,**将d拆成一个个的关键字参数:name='allen','age'=18
# 缺省参数即是调用该函数时:
# foo(x, y=1)
缺省参数的值若未被传入,则传入默认预设的值
# 多值参数
函数参数列表中,参数前增加一个*可以接收元组,增加两个*可以接收字典。
*args(arguments) 存放元组参数
**kwargs 存放字典参数
函数的工作原理
def foo():
bar()
def bar():
pass
首先要知道当我们执行一个python程序实际上是由python解释器运行的(CPython是运行在C语言之上)
python解释器会用一个叫做PyEval_EvalFrameEx()的C函数去执行foo函数,在C函数执行foo函数时,首先会创建一个栈帧对象(stack frame),栈帧是一个上下文,保存命名空间的全局/局部字典,当前正在执行字节码的信息,索引指针,然后将代码转成字节码对象,字节码是运行在栈帧上下文中的,当foo调用子函数bar时,又会创建一个栈帧对象,在新创建的栈帧对象上下文中执行字节码
# 所有的栈帧时分配在堆内存中的,堆内存的特性就是如果我们分配的堆内存不去释放,那么会一直保存在堆内存中
函数工作的流程图
# 1
PyEval_EvalFrameEx(PyFrameObject *f)
PyFrameObject
f_back # 指向调用的栈帧对象,此时foo没有调用的栈帧对象
f_code # 指向foo的字节码对象
PyCodeObject
foo's bytecode #
# 2
PyEval_EvalFrameEx(PyFrameObject *f)
PyFrameObject
f_back # 指向foo的栈帧对象
f_code # 指向bar的字节码对象
PyCodeObject
bar's bytecode #
迭代器和生成器
迭代器
# 可迭代对象 iterable
对象必须拥有__iter__方法,该方法返回一个迭代器对象
# 迭代器对象
迭代器是访问集合内元素的一种方式,一般用来遍历数据
迭代器和以下标的访问方式不一样,迭代器是不能返回的,迭代器提供了一种惰性访问数据的方式
对象必须拥有__iter__和__next__方法,执行是返回迭代器中的下一项,或者引起StopIteration异常,终止迭代
# 迭代过程
使用iter(iterable object)内置函数,该函数利用可迭代对象的__iter__方法生成一个迭代器对象;每一步调用next(iterator)内置函数调用迭代器对象,利用迭代器的__next__方法生成一个值,直到抛出StopIteration异常,结束迭代
# 自定义可迭代对象
from collections.abc import Iterator
class MyIterator(Iterator):
def __init__(self, iterable):
self.iterable = iterable
self.index = 0
def __next__(self):
try:
result = self.iterable[self.index]
except IndexError:
raise StopIteration
self.index += 1
return result
class MyIterable(object):
def __init__(self, employee):
self.employee = employee
def __iter__(self):
return MyIterator(self.employee)
emp = ['allen', 'kevin', 'collins']
my = MyIterable(emp)
for i in my:
print(i)
----------------------------------------------------------------
# 迭代器是访问集合内元素的一种方式,一般用来遍历数据
# 迭代器和下标获取的方式不同,迭代器不能返回,迭代器提供了一种惰性的访问机制
# 迭代协议
# python是基于协议的编程的
# 一个可迭代对象内部实现一个__iter__魔法函数,调用iter是返回一个迭代器对象,迭代器实现__iter__和__next__的魔法函数
class Iterable(object):
def __init__(self, seq):
self.seq = seq
def __iter__(self):
return Iterator(self.seq)
class Iterator(object):
def __init__(self, seq)
self.seq = seq
self.index = 0
def __next__(object):
try:
ele = self.seq[self.index]
except InderError:
raise StopIteration
index+=1
return ele
生成器
# 生成器
只要在函数中出现yield关键字,他就是一个生成器
生成器可以挂起执行并且保持当前执行的状态
生成器对象,在python编译字节码的时候就产生了
生成器对象,实现了迭代协议,所以我们可以使用for循环来遍历
生成器与函数运行过程有所不同,PyGenObject(生成器对象)封装了一个栈帧对象和字节码对象,而栈帧对象中ilast保存生成器最后执行的位置(刚开始为-1,意味着生成器尚未开始),当我们调用send时(预激),生成器执行到第一个yield暂停,并返回值,基于这一特性生成器可以在任何时候被任何函数恢复执行
函数中声明了 yield 关键字,python解释器在编译字节码的时候,将该函数标记成生成器
# 基于生成器的协程
python3之前没有原声协程,只有基于生成器的协程
pep342赠强了生成器功能
生成器可以通过yield暂停执行和产出数据
同时支持send()向生成器发送数据和throw()向生成器抛出异常
# 基于生成器的协程注意点
协程序需要使用send(None)或者next(coroutine)来"预激"(prime)才能启动
在yield处协程会暂停执行
单独的yield value会产出值给调用方
可以通过coroutine.send(value)来给协程发送值,发送的值会赋值给yield表达式左边的变量,value=yield
协程执行完成后(没有遇到下一个yield语句)会抛出StopIteration异常
# 协程装饰器
from functools import wraps
# 避免每次都要用send预激它
def coroutine(func):
@wraps(func)
def primer(*args,**kwargs): # 这样就不用每次都用send(None)启动了
"""装饰器:向前执行到第一个yield表达式,预激func"""
gen = func(*args, **kwargs)
next(gen)
return gen
return primer
面向切面编程AOP
AOP
# AOP
简言之、这种在运行时,编译时,类和方法加载时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面的编程
我们管切入到指定类指定方法的代码片段称为切面,而切入到哪些类、哪些方法则叫切入点。有了AOP,我们就可以把几个类共有的代码,抽取到一个切片中,等到需要时再切入对象中去,从而改变其原有的行为。
优点是:这样的做法,对原有代码毫无入侵性
闭包
# 内部函数包含对外部作用域而非全局作用域的引用
# 闭包的意义:返回的函数对象,不仅仅是一个函数对象:在该函数外还包裹了一层作用域,这使得,该函数无论在何处调用,优先使用自己外层包裹的作用域
# 应用领域:延迟计算(原来我们是传参,现在我们是包起来)
# 装饰器就是闭包函数的一种应用场景
当一个内嵌函数引用其外部作用域的变量,我们就会得到一个闭包. 总结一下,创建一个闭包必须满足以下几点:
1.必须有一个内嵌函数
2.内嵌函数必须引用外部函数中的变量
3.外部函数的返回值必须是内嵌函数
def index(path):
def get():
fd = open(path)
fd.read()
fd.close()
return get
装饰器
# 装饰器
经常被用于有切面需求的场景,有了装饰器,我们就可以抽离出大量函数中与函数功能本身无关的雷同代码并继续重用
装饰器的原则:
1.不修改被装饰对象的源代码
2.不修改被装饰对象的调用方式
装饰器的目标:为被装饰对象添加上新功能
# 装饰模版
# 装饰器就是把其他函数作为参数的函数
def decorator(func):
# 在函数里面,装饰器在运行中定义函数: 包装.
# 这个函数将被包装在原始函数的外面,所以可以在原始函数之前和之后执行其他代码..
def wrapper(*args, **kwargs):
# 把要在原始函数被调用前的代码放在这里
print "Before the function runs"
# 调用原始函数(用括号)
result = func(*args, **kwargs)
# 把要在原始函数调用后的代码放在这里
print "After the function runs"
# 在这里"a_function_to_decorate" 函数永远不会被执行
# 在这里返回刚才包装过的函数
# 在包装函数里包含要在原始函数前后执行的代码.
return wrapper
# 假如你建了个函数,不想修改了
def foo():
print "I am a stand alone function, don't you dare modify me"
foo()
#输出: I am a stand alone function, don't you dare modify me
# 现在,你可以装饰它来增加它的功能
# 把它传递给装饰器,它就会返回一个被包装过的函数.
new_foo = decorator(foo)
# 执行
nwe_foo()
#输出s:
#Before the function runs
#I am a stand alone function, don't you dare modify me
#After the function runs
# 装饰器语法糖
def decorator(func):
def wrapper(*args, **kwargs):
print('Before func runs')
result =func(*args, **kwargs)
print('After func runs')
return wrapper
@decorator # 其实就是decorator(foo)简写
def foo(*args, **kwargs):
print('Hello')
# 无参装饰器
def decorator(func):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result
return wrapper
# 有参装饰器
def decorator(*arg, **kwargs):
def wrapper(func):
def inner(*args, **kwargs):
result = func(*args, **kwargs)
return result
return inner
return wrapper
# 叠加多个装饰器
1. 加载顺序(outter函数的调用顺序):自下而上
2. 执行顺序(wrapper函数的执行顺序):自上而下
# 案例
def wrapper1(func):
print('loading wrapper1')
def inner1(*args, **kwargs):
print('execute inner1')
result = func(*args, **kwargs)
return result
return inner1
def wrapper2(func):
print('loading wrapper2')
def inner2(*args, **kwargs):
print('execute inner2')
result = func(*args, **kwargs)
return result
return inner2
def wrapper3(func):
print('loading wrapper3')
def inner3(*args, **kwargs):
print('execute inner3')
result = func(*args, **kwargs)
return result
return inner3
@wrapper1
@wrapper2
@wrapper3
def my_func():
print('From my_func')
# 装饰器知识
装饰器使函数调用变慢了.一定要记住.
装饰器不能被取消(有些人把装饰器做成可以移除的但是没有人会用)所以一旦一个函数被装饰了.所有的代码都会被装饰.
Python自身提供了几个装饰器,像property, staticmethod,classmetho
Django用装饰器管理缓存和视图的权限.
Twisted用来修改异步函数的调用.
面向对象编程OOP
OOP
面向对象是相当于面向过程而言的,面向过程语言是一种基于功能分析的,以算法为中心的程序设计方法,而面向对象是一种基于结构分析的,以数据为中心的程序设计思想。在面向对象语言中有一个很重要的东西,叫做类。面向对象有三大特性:封装、继承、多态。
面向对象的基本特征
1.封装
简单来讲: 将现实世界的事物抽象成计算机领域中的对象,对象同时具有属性和行为,这种抽象就是封装.
封装的一个重要特性: 数据隐藏. 对象只对外提供与其它对象交互的必要接口,而将自身的某些属性和实现细节对外隐藏,
通过这种方式,对象对内部数据提供了不同级别的保护,以防止程序中无关的部分意外的改变或错误的使用了对象的私有部分。
这样就在确保正常交互的前提下,保证了安全性.
2.继承
面向对象的一个重要特性是复用性.继承是实现复用性的一个重要手段.
可以在不重复编写以实现的功能的前提下,对功能进行复用和拓展.
继承概念的实现方式有二类:实现继承与接口继承。
*实现继承是指直接使用基类的属性和方法而无需额外编码的能力
*接口继承是指仅使用属性和方法的名称、但是子类必须提供实现的能力
被继承的类叫做父类
继承的类叫做派生类、子类
3.多态
多态就是不同的对象可以调用相同的方法然后得到不同的结果
多态的几个前提
* a:要有继承关系。
* b:要有方法重写。
* c:要有父类引用指向子类对象。
多态的好处
* a:提高了代码的维护性(继承保证)
* b:提高了代码的扩展性(由多态保证)
多态的限制
* 不能使用子类的特有属性和行为。
鸭子类型
# 当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子
# python中的鸭子类型允许我们使用任何提供所需方法的对象,而不需要迫使它成为一个子类。
# 在鸭子类型中,关注的不是对象的类型本身,而是它是如何使用的
python鸭子类型的体现
file StringIO socket 对象都有read方法,所以都叫 like file object
比如定义__iter__魔法方法的对象可以使用for循环迭代它
类变量和实例变量
# 类变量(类创建后在全局是唯一的)
类变量定义在类中且在函数体之外,类变量通常不作为实例变量使用,类变量在整个实例化的对象中是公用的,即类实例都可以访问
# 实例变量
实例化之后,每个实例单独拥有的变量
class Test(object):
country = 'China'
def say(self):
print('Hello%s' % Test.country)
print('Hello%s' % self.country)
t = Test()
print(t.country)
t.country = 'ZH'
print(t.country)
# 实例的作用域里把类变量的引用改变了,就变成了一个实例变量,self.name不再引用Person的类变量name了.
实例方法、类方法、静态方法
class User(object):
def __inin__(self, name):
self.name = name
def intro(self):
print(self.name)
@staticmethod
def welcome():
print('Welcome')
@classmethod
def from_cls(cls, name):
return cls(name)
# 实例方法
绑定到对象的方法:没有被任何装饰器装饰的方法
通过对象来调用该方法,自动将对象当作第一个参数传入
# 类方法
绑定到类的方法:用classmethod装饰器装饰的方法
通过类来调用该方法,调用时自动将类当作第一个参数传入
# 静态方法
不与类或对象绑定,类和对象都可以调用,不会自动传值
单下划线和双下划线
# __foo__
__foo__:双下划线开头双下划线结尾的是一些 Python 的"魔术"对象,如类成员的 __init__、__del__、__add__、__getitem__ 等,以及全局的 __file__、__name__ 等,Python 官方推荐永远不要将这样的命名方式应用于自己的变量或函数,而是按照文档说明来使用
# _foo
_foo:一种约定,用来指定变量私有,程序员用来指定私有变量的一种方式,不能用from module import * 导入,其他方面和公有一样访问
# __foo
__foo:这个有真正的意义:解析器用_classname__foo来代替这个名字,以区别和其他类相同的命名,它无法直接像公有成员一样随便访问,通过对象名._类名__xxx这样的方式可以访问
# foo_
foo_:在Python的官方推荐的代码样式中,还有一种单下划线结尾的样式,这在解析时并没有特别的含义,但通常用于和 Python关键词区分开来,如果我们需要一个变量叫做 class,但 class 是 Python 的关键词,就可以以单下划线结尾写作 class_
重载
函数重载主要是为了解决两个问题
1. 可变参数类型
2. 可变参数个数
另外,一个基本的设计原则是,仅仅当两个函数除了参数类型和参数个数不同以外,其功能是完全相同的,此时才使用函数重载,如果两个函数的功能其实不同,那么不应当使用重载,而应当使用一个名字不同的函数。
对于情况1:函数功能相同,但是参数类型不同,python 如何处理?答案是根本不需要处理,因为 python 可以接受任何类型的参数,如果函数的功能相同,那么不同的参数类型在 python 中很可能是相同的代码,没有必要做成两个不同函数。
对于情况2:函数功能相同,但参数个数不同,python 如何处理?大家知道,答案就是缺省参数。对那些缺少的参数设定为缺省参数即可解决问题。因为你假设函数功能相同,那么那些缺少的参数终归是需要用的
# 缺省参数即是调用该函数时:缺省参数的值若未被传入,则传入默认预设的值
# 多值参数
函数参数列表中,参数前增加一个*可以接收元组,增加两个*可以接收字典。
* args(arguments) 存放元组参数
* * kwargs 存放字典参数
好了,鉴于情况 1 跟 情况 2 都有了解决方案,python 自然就不需要函数重载了。
新式类和旧式类
新式类都从object继承,经典类不需要。
新式类的MRO(method resolution order 基类搜索顺序)算法采用C3算法广度优先搜索,而旧式类的MRO算法是采用深度优先搜索
新式类相同父类只执行一次构造函数,经典类重复执行多次
Python 2.x中默认都是经典类,只有显式继承了object才是新式类
Python 3.x中默认都是新式类,经典类被移除,不必显式的继承object
# python 新式类例子
class A(object):
pass
class B(A):
pass
class C(A):
pass
class D(A):
pass
class E(B, C):
pass
class F(C, D):
pass
class G(D):
pass
class H(E, F):
pass
class I(F, G):
pass
class K(H, I):
pass
# K --> H --> E--> B--> I --> F --> C --> G --> D -->A
魔法函数
class A(object):
def __new__(cls,*args,**kwargs):
print('__new__')
return super().__new__(cls)
def __init__(self,*args,**kwargs):
print('__init__')
a = A()
# __init__
__init__方法做的事情是在对象创建好之后初始化变量
# __new__
__new__方法会返回一个创建的实例,而__init__什么都不返回
当创建一个新实例时调用__new__,初始化一个实例时用__init__
# __call__
当调用实例对象,自动会调用此方法
# __del__
析构函数,当删除一个对象时,则会执行此方法,对象在内存中销毁时,自动会调用此方法
元类
# 类也是对象,元类是创建类的类
# 元类可控制类对象创建的过程
# python中类的实例化过程,会首先寻找metaclass,通过metaclass创建类
# 如果没有找到metaclass,则由type来创建类对象
class MetaClass(type): # 自定义元类继承type
def __new__(cls, *args, **kwargs):
super().__new__(cls, *args, **kwargs)
class User(metaclass=MetaClass):
# _metaclass_ 指定元类
def __init__(self):
pass
自省
# 自省(Introspection):自省是通过一定的机制查询到对象的内部结构
dir(obj)
dir(obj)可以获取一个对象所有的属性与方法,返回为列表(仅有属性或方法名称)
dir()是Python提供的一个API函数,dir()函数会自动寻找一个对象的所有属性(包括从父类中继承的属性和方法)
__dict__
__dict__字典中存储的是对象或类的部分属性,键为属性名,值为属性值
实例对象的__dict__仅存储与该实例相关的实例属性
类的__dict__存储所有实例对象共享的变量和函数(类属性,方法等),类的__dict__并不包含其父类的属性和方法
dir()和__dict__的区别
1.dir()是一个函数,返回的是list,仅有属性名和方法名;
2.__dict__返回是一个字典,键为属性名,值为属性值;
3.dir()用来寻找一个对象的所有属性和方法(包括从父类中继承的属性和方法),包括__dict__中的属性和方法,__dict__是dir()的子集;
注:并不是所有对象都拥有__dict__属性。许多内建类型就没有__dict__属性,如list,此时就需要用dir()来列出对象的所有属性和方法
---------------------------------------------------------------------------------------------
hasattr(object, name)
检查对象是否具体 name 属性。返回 bool.
getattr(object, name, default)
获取对象的name属性。
setattr(object, name, default)
给对象设置name属性
delattr(object, name)
给对象删除name属性
dir([object])
获取对象大部分的属性
isinstance(name, object)
检查name是不是object对象
type(object)
查看对象的类型
callable(object)
判断对象是否是可调用对象
内存管理和垃圾回收
内存管理
Python有内存池机制,用于对内存的申请和释放管理,预先在内存中申请一定数量的,大小相等的内存块留作备用,当有新的内存需求时,就先从内存池中分配内存给这个需求,不够了之后再申请新的内存,这样做最显著的优势就是能够减少内存碎片,提升效率
垃圾回收
垃圾回收机制,Python采用GC作为自动内存管理机制,GC要做的有2件事,一是找到内存中无用的垃圾对象资源,二是清除找到的这些垃圾对象,释放内存给其他对象使用。
Python采用了"引用计数"为主,"标志清除"和"分代回收"为辅助策略
# 引用计数
PyObject是每个对象必有的内容,其中ob_refcnt就是做为引用计数,当一个对象有新的引用时,它的ob_refcnt就会增加,当引用它的对象被删除,它的ob_refcnt就会减少,一旦对象的引用计数为0,该对象立即被回收,对象占用的内存空间将被释放
优点:
简单
实时性
缺点:
需要额外的空间维护引用计数
无法解决循环引用问题
# 循环引用
A和B相互引用而且没有外部引用A与B中的任何一个,也就是对象之间互相应用,导致引用链形成一个环
# 标记清除
标记清除主要是解决循环引用问题
标记清除算法是一种基于追踪回收(tracing GC)技术实现的垃圾回收算法,它分为两个阶段:第一阶段是标记阶段,GC会把所有的活动对象打上标记,第二阶段是把那些没有标记的对象非活动对象进行回收
对象之间通过引用(指针)连在一起,构成一个有向图,对象构成这个有向图的节点,而引用关系构成这个有向图的边,从根对象(root object)出发,沿着有向边遍历对象,可达的(reachable)对象标记为活动对象,不可达的对象就是要被清除的非活动对象,根对象就是全局变量、调用栈、寄存器
优点:
解决类循环引用的问题
缺点:
清除非活动的对象前它必须顺序扫描整个堆内存
# 分代回收
分代回收是一种以空间换时间的操作方式
分代回收思想将对象分为三代(generation 0,1,2) 0代表幼年对象,1代表青年对象,2代表老年对象,随着代数越高脚检测频率越低,每一代都有最大容纳对象的个数,一旦超过容纳个数就会出发垃圾回收机制,老年对象触发将清理所有三代,青年对象触发会清理青年和幼年,幼年对象触发后只会清理自己
内存的结构
# 程序的内存分配
栈(stack):有编译器自动分配和释放,存放函数的参数、局部变量、临时变量、函数返回地址等
堆(heap):一般有程序员分配和释放,如果没有手动释放,在程序结束时可能由操作系统自动释放,稍有不慎会引起内存泄漏
# Python、Go有回收机制的语言,C/CPP必须手动释放开辟的堆内存)
# 申请后系统的响应
栈:只要栈的剩余空间大于所申请的空间,系统将为程序提供内存,否则将报异常提示栈溢出
堆:在记录空闲内存地址的链表中寻找一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统会在这块内存空间的首地址处记录本次分配空间的大小,这样代码中的delete才能正确释放本内存空间,系统会将多余的那部分重新空闲链表中
# 申请大小限制
栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域,这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在linux下默认栈大小为8Mb,如果申请的空间超过栈的剩余空间时,将提示overflow,因此,能从栈获得的空间较小(速度快,无法控制)
堆:堆是向高地址扩展的数据结构,是不连续的内存区域,这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址,
堆的大小受限于计算机系统中有效的虚拟内存,由此可见,堆获得的空间比较灵活,也比较大(速度慢,操作方便)
# 分配效率
栈:由系统自动分配,速度较快,但程序员是无法控制的
堆:一般速度比较慢,而且容易产生内存碎片,不过用起来最方便
# 存储内容
栈:在栈中,第一个进栈的是主函数下一条指令的地址,然后是函数的各个参数,在大多数编译器中,参数是由右往左入栈,然后是函数中的局部变量,注意,静态变量不入栈,出栈则刚好顺序相反
堆:一般在堆的头部用一个字节存放堆的大小,具体内容由程序员安排
# 内存分成5个区
栈:内存由编译器在需要时自动分配和释放,通常用来存储局部变量和函数参数,(为运行函数而分配的局部变量、函数参数、返回地址等存放在栈区),
栈运算分配内置于处理器的指令集中,效率很高,但是分配的内存容量有限
堆:内存使用new进行分配,使用delete或delete[]释放,如果未能对内存进行正确的释放,会造成内存泄漏,但在程序结束时,会由操作系统自动回收
自由存储区:使用malloc进行分配,使用free进行回收,和堆类似
全局/静态存储区:全局变量和静态变量被分配到同一块内存中,C语言中区分初始化和未初始化的,C++中不再区分了(全局变量、静态数据、常量存放在全局数据区)
常量存储区:存储常量,不允许被修改
--
# 内存分为3个区
静态存储区:内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在,它主要存放静态数据、全局数据和常量
栈区:在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放,栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限
堆区:亦称动态内存分配,程序在运行的时候用malloc或new申请任意大小的内存,程序员自己负责在适当的时候用free或 delete释放内存,动态内存的生存期可以由我们决定,
如果我们不释放内存,程序将在最后才释放掉动态内存,但是,良好的编程习惯是:如果某动态内存不再使用,需要将其释放掉,否则,我们认为发生了内存泄漏现象
# 代码示例
void fn()
{
int *p = new int[5]
}
new,分配了一块堆内存,指针p分配的示一块栈内存,所以这句话的意思,就是:在栈内存中存放了一个指向一块堆内存的指针p
栈顶和栈底
ESP:栈指针寄存器(extended stack pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶
EBP:基址指针寄存器(extended base pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的底部
设计模式
单例模式
# __new__
class Singleton(object):
def __new__(cls, *args, **kwargs):
if not hasattr(cls, '_instance'):
instance = super(Singleton, cls).__new__(cls, *args, **kwargs)
# 因为__new__是一个静态方法
setattr(cls, '_instance', instance)
return cls._instance
a = Singleton()
b = Singleton()
c = Singleton()
print(id(a)) # 4325283320
print(id(b)) # 4325283320
print(id(c)) # 4325283320
# 共享属性
创建实例时把所有实例的`__dict__`指向同一个字典,这样它们具有相同的属性和方法.
class Singleton(object):
_local = {'name':'Allen'}
def __new__(cls, *args, **kwargs):
obj = super(Singleton, cls).__new__(cls, *args, **kwargs)
obj.__dict__ = cls._local
return obj
s1 = S()
s2 = S()
print(s1.name)
s1.name = 'Kevin'
print(s2.name)
# 装饰器(类装饰器)
def decorator(cls):
instances = {}
def wrapper(*args,**kwargs):
if cls not in instances:
obj = cls(*args,**kwargs)
instances[cls] = obj
return instances[cls]
return wrapper
@decorator
class MyClass(objcet):
a = 1
m1 = MyClass()
m2 = MyClass()
m3 = MyClass()
print(id(m1))
print(id(m2))
print(id(m3))
# import
# 作为python的模块是天然的单例模式
a.py
class MyClass(object):
a = 1
obj = MyClass()
b.py
import a
o1 = a.obj
o2 = a.obj
o3 = a.obj
网络编程
ISO七层协议
# 物理层:比特
通过光缆、电缆、双绞线、无线电波发送电信号,高电压对应1,低电压对应数字0
# 数据链路层:帧
工作于以太网协议,将一组电信号构成一个数据包,叫做帧,每一组数据帧分成报头head和数据data来个部分(head固定18字节,data最短46字节,最长1500字节)
以太网协议的接入internet的设备都必须具备网卡,发送端和接受端的地址便是mac地址并且该地址为全球唯一的,通过广播的的方式来进行通信,向在同一局域网内的每一个计算机发送消息来确认接受者
# 网络层:报文
引入IP协议,该协议定义的地址成为IP地址,广泛采用ipv4,规定网络定制有32位2进制表示:0.0.0.0-255.255.255.0
ip地址分为两个部分:表示子网,表示主机
子网掩码:通过子网掩码,我们就能判断,任意两个ip地址是否处于同一个子网内,IP地址和子网掩掩码通过and运算得出结果,ip数据包分为head和data部分,无需为ip数据包定义单独的栏位,直接放入以太网包的data部分,head(20-60字节),data(最长65515字节),而以太网数据包的data(数据)部分,最长只有15000字节,因此,如果ip数据包超过了1500字节,他就需要分隔成几个以太网数据包,分开发送,
# 传输层:TPDU--传输协议数据单元
# 会话层:SPDU--会话协议数据单元
# 表示层:SPDU--表示协议数据单元
# 应用层:APDU--应用协议数据单元
wsgi
WSGI是 Web Server Gateway Interface 的缩写。
它是 Python应用程序(application)或框架(如 Django)和 Web服务器之间的一种接口,已经被广泛接受。
它是一种协议,一种规范,其是在 PEP 333提出的,并在 PEP 3333 进行补充(主要是为了支持 Python3.x)。这个协议旨在解决众多 web 框架和web server软件的兼容问题。有了WSGI,你不用再因为你使用的web 框架而去选择特定的 web server软件
WSGI 接口有服务端和应用端两部分,服务端也可以叫网关端,应用端也叫框架端。服务端调用一个由应用端提供的可调用对象。如何提供这个对象,由服务端决定。例如某些服务器或者网关需要应用的部署者写一段脚本,以创建服务器或者网关的实例,并且为这个实例提供一个应用实例。另一些服务器或者网关则可能使用配置文件或其他方法以指定应用实例应该从哪里导入或获取
WSGI 对于 application 对象有如下三点要求
必须是一个可调用的对象
接收两个必选参数environ、start_response
返回值必须是可迭代对象,用来表示http body
一次http请求的过程
一个HTTP请求的过程分为2个阶段
#第一个阶段是从客户端到WSGI server
#第二个阶段是从WSGI server到WSGI application
socket
并发编程
GIL全局解释器锁
# 一个python文件执行的过程
1、首先执行py文件,向操作系统发起请求,操作系统划出了一块内存空间(运行一个python程序只产生一个进程<两份数据:一个py文件数据,一个是python解释器数据>)
2、先把python解释器数据送到内存中,然后在把py文件送到内存中,(他们都在一个进程中传送)
3、最后把py文件当作普通字符串传送到python解释器中,然后解释器对其进解释(py程序脱离了解释器就是普通的字符串)
# GIL的作用
GIL:global interpreter lock(全局解释器锁)
Python中一个线程对应于c语言中的一个线程
GIL使得同一个时刻只有一个线程运行在一个CPU上执行字节码,在同一进程下无法将多个线程映射到多个cpu上执行
GIL会根据执行的字节码行数以及时间片释放GIL,GIL在遇到IO操作时会主动释放
GIL同步线程执行字节码
# GIL的优缺点:
优点:
保证CPython解释器中内存管理的线程安全
缺点:
同一进程下的多线程无法实现并行
# 全局解释器锁(Global Interpreter Lock)
Python为了保证线程安全而采取的独立线程运行的限制,也就是说一个核只能在同一时间运行一个线程,对于io密集型任务,python的多线程起到作用,io不占用cpu资源,但对于cpu密集型任务,python的多线程发挥不出多核优势
# 注意:
GIL锁不能保证数据安全,GIL只是保证解释器数据安全,如果要保证自己的数据安全就需要自己加一把锁
# 解决方法
1.多进程
2.协程(协程也只是单CPU,但是能减小切换代价提升性能)
进程和线程
# 进程:
定义:系统资源分配与调度的基本单位
优点:独占操作系统与计算机资源,尤其是独占内存地址空间,所以多进程场景中的单个进程异常不会让整个应用程序崩溃
缺点:独占资源多,所以进程的创建、销毁、通信及切换的成本都比较高
# 守护进程:
对于主进程来说:运行完毕指的是主进程代码运行完毕
主进程在其代码执行完毕就算结束了(此时回收守护进程),但是主进程会等待所有非守护进程执行完毕并挥手资源才结束(防止产生僵尸进程);
# 线程
定义:处理器调度的基本单位
优点:轻量基本不独占资源,所以线程的创建、销毁、通信及切换的成本都更低,更有利于在多线程场景中提高程序的并发性能
缺点:没有隔离出私有内存,所以单个线程的崩溃可能会导致整个应用程序退出
# 守护线程:
对于主线程来说:主线程所在的进程中所有非守护线程执行完毕,主线程才算执行完毕
主线程在所有非守护线程执行完毕才结束,对主线程来说,主线程的结束意味着主进程的结束,线程是执行单位
# 线程安全
进程间通信
from multiprocessing import Process
from multiprocessing import Manager
from multiprocessing import Queue
from multiprocessing import Pool
from multiprocessing import Pipe
import time
def producer(q):
q.put("Hello")
def consumer(q):
result = q.get()
print(result)
def p(pi):
time.sleep(2)
pi.send("Hello")
def c(pi):
print(pi.recv())
def add(s_dict, key, value, lock):
lock.acquire()
s_dict[key] += value
lock.release()
if __name__ == '__main__':
# 共享变量在多进程不适用,在多线程适用
# q = Queue(10)
# p1 = Process(target=producer, args=(q,))
# p2 = Process(target=consumer, args=(q,))
# p1.start()
# p2.start()
# p1.join()
# p2.join()
# 进程池不能使用Queue,Pool使用Manager中Queue来传递参数
# o = Manager().Queue(10)
# pool = Pool(2)
# pool.apply_async(func=producer, args=(o,))
# pool.apply_async(func=consumer, args=(o,))
# pool.close()
# pool.join()
# pipe的性能要不queue要高
# 通过Pipe来进行进程间通信
# Pipe只能适用于两个进程间的通信
# recv, send = pipe = Pipe() # 返回两个connection,一个发送数据,一个接收数据
# px = Process(target=p, args=(send,))
# cx = Process(target=c, args=(recv,))
# px.start()
# cx.start()
# px.join()
# cx.join()
# Manager()共享内存,使用共享内存在修改数据要保证同步性
shareDict = Manager().dict()
mutex = Manager().Lock()
shareDict["key"] = 0
all_task = [Process(target=add, args=(shareDict, "key", 10000, mutex)) for _ in range(10)]
for t in all_task:
t.start()
for x in all_task:
x.join()
print(shareDict)
并发和并行
1.单线程下实现并发
并发指的是多个任务看起来好像是同时运行的
并发实现的本质:切换+保存状态
并发、并行、串行:
并发:看起来是同时运行,切换+保存状态
并行:真正意义上的同时运行,只有在多cpu的情况下才能实现并行,4个cpu能够并行4个任务
串行:一个任务完完整整的执行完毕才能运行下一个任务
协程:
是单线程下的并发,又称为线程,纤程,英文名(Coroutine)。操作系统不会认协程
#1. python的线程属于内核级别的,即由操作系统控制调度(如单线程遇到io或执行时间过长就会被迫交出cpu执行权限,切换其他线程运行)
#2. 单线程内开启协程,一旦遇到io,就会从应用程序级别(而非操作系统)控制切换,以此来提升效率(!!!非io操作的切换与效率无关)
优点:
#1. 协程的切换开销更小,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级
#2. 单线程内就可以实现并发的效果,最大限度地利用cpu
缺点:
#1. 协程的本质是单线程下,无法利用多核,可以是一个程序开启多个进程,每个进程内开启多个线程,每个线程内开启协程
#2. 协程指的是单个线程,因而一旦协程出现阻塞,将会阻塞整个线程
特点:
必须在只有一个单线程里实现并发
修改共享数据不需加锁
用户程序里自己保存多个控制流的上下文栈
附加:一个协程遇到IO操作自动切换到其它协程(如何实现检测IO,yield、greenlet都无法实现,就用到了gevent模块(select机制)
协程
# 协程(Coroutine):可以被暂停并切换到其他协程运行的函数(协程不同于进程和线程,有我们程序(员)自己调度)
# 协程可以让我们像写同步代码去写异步代码
# 在python有一个特例是可以被暂停的那就是:生成器
# 无需线程上下文切换的开销,协程避免了无意义的调度,由此可以提高性能(但也因此,程序员必须自己承担调度的责任,同时,协程也失去了标准线程使用多CPU的能力)
# 无需原子操作锁定及同步的开销
# 方便切换控制流,简化编程模型
# 高并发+高扩展性+低成本,适合做并发处理
# 进行 阻塞 操作 会阻塞掉整个程序,如是要避免出现阻塞操作发生
def gen_func():
yield 1
yield 2
yield 3
gen = gen_func() # 创建一个生成器对象
v = gen.__next__()
print(v)
# 1.将生成器包装成协程
from tornado.gen import coroutine
@coroutine
def cor1():
yield 1
yield 2
yield 3
# 2.(在python3.5中提供原生协程)就是async/awiat
async def cor2():
# yield和yield from 不可以写在async定义的协程中
await cor1() # 如果想要获取其他协程的结果就要使用await
Queue
# 使用Queue作为线程间通信的媒介,可以不用去考虑资源竞态的问题,他是线程安全的
# Queue内部使用的是deque双端队列(deque是线程安全的)
q.get() # 向队列中取值,默认阻塞,可以设置block=false异步执行
q.put() # 向队列中放置
q.get_nowait() # 内部还是调用get方法,执行传递了一个block=false参数
q.put_nowait()
q.qsize() # 队列的长度
q.empty() # 队列是否为空
q.full() # 队列是否为满
q.join() # 阻塞队列,直到向队列发送一个task_done
q.task_done() # 向队列发出一个信号
线程同步
Lock:互斥锁
RLock:可重入锁
Condition:条件锁
from threading import Condition
from threading import Thread
from threading import Lock
# condition条件变量,用于复杂的线程间同步
# 通过Condition完成协同读诗
class Ai(Thread):
def __init__(self, cond: Condition):
super(Ai, self).__init__(name="小爱同学")
self.cond = cond
def run(self):
with self.cond:
self.cond.wait()
print(f"{self.name}:在")
self.cond.notify()
self.cond.wait()
print(f"{self.name}:好啊")
self.cond.notify()
self.cond.wait()
print(f"{self.name}:君住长江尾")
self.cond.notify()
self.cond.wait()
print(f"{self.name}:共饮长江水")
self.cond.notify()
self.cond.wait()
print(f"{self.name}:此恨何时已")
self.cond.notify()
self.cond.wait()
print(f"{self.name}:定不负相思意")
class TMall(Thread):
def __init__(self, cond: Condition):
super(TMall, self).__init__(name="天猫精灵")
self.cond = cond
def run(self):
with self.cond:
print(f"{self.name}:小爱同学")
self.cond.notify()
self.cond.wait()
print(f"{self.name}:我们来古诗吧")
self.cond.notify()
self.cond.wait()
print(f"{self.name}:我住长江头")
self.cond.notify()
self.cond.wait()
print(f"{self.name}:日日思君不见君")
self.cond.notify()
self.cond.wait()
print(f"{self.name}:此水几时休")
self.cond.notify()
self.cond.wait()
print(f"{self.name}:只愿君心似我心")
self.cond.notify()
if __name__ == '__main__':
lock = Condition()
t = TMall(lock)
a = Ai(lock)
a.start()
t.start()
# 启动顺序很重要
# 在调用with cond(或者cond.acquire)之后才能调用wait或者notify方法 error:cannot notify on un-acquired lock
# condition有两层锁,cond.acquire和waiter,一把底层锁会在线程调用了wait方法的时候释放,上面的锁会在每次调用wait的时候分配一把并放入cond的等待队列中(deque)等待notify方法的唤醒
t.join()
a.join()
Semaphores:信号量
# Semaphore是用于控制进入数量的锁
# 文件,读,写,写一般只是用于一个线程写,读可以允许有多个
import time
from threading import Semaphore
from threading import Thread
def task(index, sem: Semaphore):
time.sleep(2)
print(f"Get success {index}")
sem.release()
def handler(s: Semaphore):
for i in range(20):
s.acquire()
Thread(target=task, args=(i, s)).start()
if __name__ == '__main__':
semaphore = Semaphore(3)
handler(semaphore)
线程池和进程池
同步、异步、阻塞、非阻塞
1、阻塞和非阻塞指的是程序的两种运行状态(运行态、就绪态、阻塞态)
阻塞(就绪态和阻塞态):遇到IO操作就发生阻塞,程序一旦遇到阻塞操作就会停在原地,并且立刻释放CPU资源
非阻塞(运行态):没有遇到IO操作,或者通过某种手段让程序即便是遇到IO操作也不会停在原地,执行其他操作,力求尽可能多的占有CPU
2、同步与异步指的是提交任务的两种方式
同步调用:提交完任务后,就在原地等待,直到任务运行完毕后,拿到任务的返回值,才能继续执行下一行代码
异步调用:提交任务后,不在原地等待,直接执行一下一行代码,结果?
同步(调用/执行/任务/提交),发起任务后必须等待任务结束,拿到一个结果才能继续执行
异步(调用/执行/任务/提交),发起任务后不需要关心任务的执行结果,可以继续往下运行
异步效率高于同步
但是并不是说所有任务都可以异步,判断一个任务是否可以异步的条件是,任务发起是否立即需要执行结果
但使用异步方式发起任务是 任务中可能包含io操作 异步不可能阻塞
同步提交任务 也会卡主程序 但是不等同阻塞,因为任务中可能在做一对计算任务,cpu没走
同步和异步关注的是获取结果的方式,同步是获取到结果之后才进行下一步操作,阻塞非阻塞关注的是调用接口时当前线程的状态,同步可以调用阻塞也可非阻塞,异步是调用非阻塞接口
# 同步是指代码调用IO操作时,必须等待IO操作完成才返回的调用方式
# 异步是指代码调用IO操作时,不必等IO操作完成就返回的调用方式
# 阻塞是指调用函数时候当前线程被挂起
# 阻塞是指调用函数时候当前线程不会被挂起,而是立即返回
select、poll、epoll
select、poll、epoll都是io多路复用的机制,i/o多路复用就是同一种机制,一个进程可以监视多个描述符(可以理解为多个socket),一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作,但是select、poll、epoll本质上都是同步i/o,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步i/o则无需自己负责进行读写,异步i/o的实现会负责把数据从内核拷贝到用户空间
# select
select函数事件的文件描述符分为3类,分别是writefds(可写的文件描述符)、readfds(可读的文件描述符)、exceptfds(异常文件描述符),调用后select函数会阻塞,直到有描述符就绪(可数据可读、可写、有异常),或者超时(timeout指定等待时间,如果立即返回设为None即可),函数返回,当select函数返回后,可以通过遍历fdset,来找到就绪的描述符.
select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点,select的一个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,linux上一般为1024,可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但是这样也会造成效率的降低(select每一个调用文件描述符都会去遍历一次[性能比较低])
# poll
不同与select使用三个位图表示三个fdset的方式,poll使用一个pollfd的指针实现
pollfd结构包含了监视的event和发生的event,不在使用select'参数-值'传递的方式,同时,pollfd并没有最大数量限制(但是数量过大后性能也是会下降),和select函数一样,poll返回后,需要轮训pollfd来获取就绪的描述符
从上面看,select和poll都需要在返回后,通过遍历文件描述符来获取已经就绪的socket,事实上,同时连接的大量客户端在同一时刻只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降
# epoll
epoll是在linux2.6内核中提出的,是之前的select和poll的增强版本,相对于selct和poll来说,epoll更加灵活,没有描述符限制,epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次
FD_ZERO(int fd, fd_set* fds)
FD_SET(int fd, fd_set* fds)
FD_ISSET(int fd, fd_set* fds)
FD_CLR(int fd, fd_set* fds)
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds,
struct timeval *timeout)
# select 机制提供一个fd_set的数据结构,实际上是一个long类型的数组,每一个数组元素能与一个打开的文件描述符建立联系,select中文件描述符分为3类,分别是writefds(可写的文件描述符)、readfds(可读的文件描述符)、exceptfds(异常文件描述符),当调用select时,由内核根据IO状态来修改fd_set的内容,由此来通知执行了select的进程那些文件/socket可读可写,然后遍历fd_set找出可读或可写描述符,从内核空间拷贝到用户空间( 时间复杂度O(n) ),select能够监听的描述符有限通常为1024个
struct pollfd {
int fd; //文件描述符
short events; //要求查询的事件掩码
short revents; //返回的事件掩码
};
int poll(struct pollfd *ufds, unsigned int nfds, int timeout);
# poll 使用pollfd的数据结构来监听一组文件描述符,和select类似无非就是没有对监听描述符的闲置
# epoll 提供三个函数epoll_create(创建一个epoll句柄),epoll_ctl(注册要监听的事件类型)和epoll_wait(等待事件的产生),epoll在每次注册新的事件到epoll句柄中时,会把所有的描述符拷贝进内核空间,而不是在epoll_wait的时候重复拷贝,保证整个过程只拷贝一次,epoll在epoll_ctl(注册监听事件类型的时候)为每一个描述符指定一个回调函数,当描述符就绪时就会调用这个会调用函数,而这个函数会把就绪的fd加入一个就绪链表(rdlist)中,epoll_wait就从就绪链表中查看有没有就绪的描述符,就不需要遍历整个事件表
UNIX五种I/O模型
对于一个Network IO(read举例),他会涉及到两个系统对象,一个是调用这个IO的Process/Thread,另一个就是系统内核(kernel),当一个read操作发生时,经历两个阶段:等待数据准备(wait for data)、将数据从内核空间拷贝到用户空间(copy data from kernel to user)
阻塞式IO
执行recvfrom系统调用,kernel等到数据的的到来,用户进程此时会被阻塞,当kernel准备好数据,他将数据从kernel中拷贝到用户内存,kernel返回结果,用户进程解除阻塞
非阻塞式IO
用户进程发出readfrom系统调用时,如果kernel的数据没有准备好,那么他并不会阻塞而是直接返回一个error,当用户进程判断结果为error时,那么用户可以再次向kernel发出read操作(用户进程不停的向内核询问,知道内核数据准备好了),一旦kernel中的数据准备好了,并且又再次用户的系统调用,就将数据从内核空间拷贝到用户空间,然后返回 # 非阻塞IO的这种轮训方式会大量的消耗CPU资源
IO多路复用
信号驱动式I/O
异步I/O
io多路复用+回调+事件循环
# 例子
import time
from selectors import DefaultSelector
from selectors import EVENT_READ
from selectors import EVENT_WRITE
from urllib.parse import urlparse
import socket
selector = DefaultSelector()
event_status = True
urls = []
class Fetch(object):
def __init__(self, host, path, spider_url):
self.host = host
self.path = path
self.spider_url = spider_url
self.client = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM, proto=0)
self.client.setblocking(False)
self.data = b""
@classmethod
def parse_url(cls, url):
parsed_url = urlparse(url)
host = parsed_url.netloc
path = parsed_url.path
if not path:
path = "/"
return cls(host, path, url)
def connect(self):
try:
self.client.connect((self.host, 80))
except BlockingIOError:
selector.register(self.client.fileno(), EVENT_WRITE, self.writeable)
def writeable(self, key):
selector.unregister(key.fd)
self.client.send(f"GET {self.path} HTTP/1.1
Host:{self.host}
Connection:close
".encode("utf8"))
selector.register(self.client.fileno(), EVENT_READ, self.readable)
def readable(self, key):
data = self.client.recv(1024)
if data:
self.data += data
else:
selector.unregister(key.fd)
html = self.data.decode("utf8").split("
", maxsplit=1)[1]
urls.remove(self.spider_url)
print(html)
if len(urls) == 0:
global event_status
event_status = False
self.client.close()
def even_loop():
while event_status:
ready = selector.select()
for key, events in ready:
callback = key.data
callback(key)
if __name__ == '__main__':
start_time = time.time()
for i in range(1, 21):
u = f"http://shop.projectsedu.com/goods/{i}/"
urls.append(u)
Fetch.parse_url(u).connect()
even_loop()
end_time = time.time()
print(end_time - start_time)
异步编程和回调之痛
异步编程:
# 以进程、线程、协程、函数/方法作为执行任务程序的基本单位,结合回调、事件循环、信号量等机制,以提高程序整体执行效率和并发能力的编程方式
# 如果在某程序的运行时,能根据已经执行的指令准确判断它接下来要进行哪个具体操作,那它是同步程序,反之则为异步程序(无序与有序的区别)
# 同步/异步、阻塞/非阻塞并非水火不容,要看讨论的程序所处的封装级别。例如购物程序在处理多个用户的浏览请求可以是异步的,而更新库存时必须是同步的
异步之难:
# 控制不住“计几”写的程序,因为其执行顺序不可预料,当下正要发生什么事件不可预料,在并行情况下更为复杂和艰难
# 所以,几乎所有的异步框架都将异步编程模型简化:一次只允许处理一个事件,故而有关异步的讨论几乎都集中在了单线程内
# 如果某事件处理程序需要长时间执行,所有其他部分都会被阻塞
# 所以,一旦采取异步编程,每个异步调用必须"足够小",不能耗时太久。如何拆分异步任务成了难题。
程序下一步行为往往依赖上一步执行结果,如何知晓上次异步调用已完成并获取结果?
回调(Callback)成了必然选择,那又需要面临"回调地狱"的折磨
同步代码改为异步代码,必然破坏代码结构
解决问题的逻辑也要转变,不再是一条路走到黑,需要精心安排异步任务
如果回调函数执行不正常该如何
如果回调里面还要嵌套回调怎么办,要嵌套很多层
如果嵌套了多层,其中某个环节出错了会造成什么后果
如果有个数据需要被每个回调用都处理怎么办
怎么使用单前函数中的局部变量
可读性差
共享状态管理困难
异常处理困难
协程
1.回调模式编码复杂度高
2.同步编程的并发性不高
3.多线程编程需要线程间同步 # Lock使用锁会降低性能
1.采用同步的方式去编写异步的代码
2.使用单线程去切换任务
1.线程是有操作系统切换的,单线程切换以为者我们需要程序员自己去调度任务
2.不再需要锁,并发性高,如果但线程内切换函数,性能远高于线程切换,并发性更高
协程:有多个入口的函数,可以暂停的函数,可以暂停的函数(可以向暂停的地方传入值)
传统的函数时调用栈的
协程也是一种IO复用的手段,相比于select + 回调,协程的优势是编码结构同步。
所谓IO复用(以实现高并发),其实本质上是将CPU操作和IO操作分离,在所有的python代码中都只进行CPU操作,将IO操作发送至内核完成,应用层不阻塞。从结果的角度来看这和多线程是类似的(因为python有gil锁,而线程也是遇到IO操作就切换,只不过线程切换是由操作系统完成的,而协程是由程序员完成的,这点bobby老师多次强调!)
为什么传统的函数不可以?
因为函数是基于栈进行调用的,只能运行完,然后退出。不能够暂停(无法由程序员调度任务),然后运行别的程序,再回来!
每个协程都有自己的栈(上下文)
next和send源码
static PyObject *
gen_iternext(PyGenObject *gen)
{
return gen_send_ex(gen, NULL, 0);
}
static PyObject *
gen_send(PyGenObject *gen, PyObject *arg)
{
return gen_send_ex(gen, arg, 0);
}
static PyObject *
gen_send_ex(PyGenObject *gen, PyObject *arg, int exc)
{
PyThreadState *tstate = PyThreadState_GET();
PyFrameObject *f = gen->gi_frame;
PyObject *result;
if (gen->gi_running) { // 判断生成器是否已经运行
PyErr_SetString(PyExc_ValueError,
"generator already executing");
return NULL;
}
if (f==NULL || f->f_stacktop == NULL) { // 如果代码块为空或调用栈为空,
//则抛出StopIteration异常
/* Only set exception if called from send() */
if (arg && !exc)
PyErr_SetNone(PyExc_StopIteration);
return NULL;
}
if (f->f_lasti == -1) { // f_lasti==-1 代表首次执行
if (arg && arg != Py_None) { // 首次执行不允许带有参数
PyErr_SetString(PyExc_TypeError,
"can't send non-None value to a "
"just-started generator");
return NULL;
}
} else {
/* Push arg onto the frame's value stack */
result = arg ? arg : Py_None;
Py_INCREF(result); // 该参数引用计数+1
*(f->f_stacktop++) = result; // 参数压栈
}
/* Generators always return to their most recent caller, not
* necessarily their creator. */
f->f_tstate = tstate;
Py_XINCREF(tstate->frame);
assert(f->f_back == NULL);
f->f_back = tstate->frame;
gen->gi_running = 1; // 修改生成器执行状态
result = PyEval_EvalFrameEx(f, exc); // 执行字节码
gen->gi_running = 0; // 恢复为未执行状态
/* Don't keep the reference to f_back any longer than necessary. It
* may keep a chain of frames alive or it could create a reference
* cycle. */
assert(f->f_back == tstate->frame);
Py_CLEAR(f->f_back);
/* Clear the borrowed reference to the thread state */
f->f_tstate = NULL;
/* If the generator just returned (as opposed to yielding), signal
* that the generator is exhausted. */
if (result == Py_None && f->f_stacktop == NULL) {
Py_DECREF(result);
result = NULL;
/* Set exception if not called by gen_iternext() */
if (arg)
PyErr_SetNone(PyExc_StopIteration);
}
if (!result || f->f_stacktop == NULL) {
/* generator can't be rerun, so release the frame */
Py_DECREF(f);
gen->gi_frame = NULL;
}
return result;
}
生成器send、throw、close
# send
import inspect
# 定义一个生成器函数
def generator_function():
val = yield 1
print(val)
yield 1
return "Allen"
if __name__ == "__main__":
# 创建一个生成器对象
gen = generator_function()
# 在使用生成器之前需要启动(这个动作称为预激)
# 启动生成器的方式有两种next()/send(None)
# gen.send(None) send(None)方法的值必须为None
# 因为生成器还没有执行到第一个yield语句,send一个非None值,他没有办法接受,
# val = yield 1执行是从右到做,一旦遇到yield函数就被挂起,函数就暂停了,
# 也就是说先yield 1给调用方,然后接受调用方传递的值进行赋值,但是yield的特殊性,val = yield 1执行了一半函数就挂起了,等待下一个send(val),来接受调用方传递的值
next(gen)
gen.send("Hello")
# throw
# 通过throw可以向生成器抛出异常
def gen_func():
try:
yield 1
except Exception as e:
print(e)
yield 2
if __name__ == '__main__':
g = gen_func()
print(g.send(None))
print(g.throw(Exception, "Error")) # 向生成器挂起处抛出一个异常,并接受下一个yield语句的值
# close
# 案例一
def gen_func():
try:
yield 1
except GeneratorExit:
pass
if __name__ == '__main__':
g = gen_func()
print(g.send(None))
print(g.close()) # 生成器对象的close方法会在生成器对象方法的挂起处抛出一个GeneratorExit异常
def gen_func():
try:
yield 1
except GeneratorExit:
# GeneratorExit继承BaseException
# 或者抛出 StopIteration close方法就不会报错了
raise StopIteration
yield 2 # 因为生成器已经关闭了
# 一旦产生了GeneratorExit异常,生成器方法后续执行的语句中,不能再有yield语句
# RuntimeError: generator ignored GeneratorExit
yield 3
if __name__ == '__main__':
g = gen_func()
print(g.send(None))
print(g.close()) # 生成器对象的close方法会在生成器对象方法的挂起处抛出一个GeneratorExit异常
# 生成器是有状态的
import inspect
# 生成器是可以暂停的函数
# 生成器是有状态的
def gen_func():
yield 1
return "Allen"
# 1.用同步的方式编写异步的代码,在适当的时候暂停函数并在适当的时候启动函数
if __name__ == '__main__':
gen = gen_func()
print(inspect.getgeneratorstate(gen)) # GEN_CREATED
gen.send(None)
print(inspect.getgeneratorstate(gen)) # GEN_SUSPENDED
try:
gen.__next__()
except StopIteration as e:
print(inspect.getgeneratorstate(gen)) # GEN_CLOSED
print(e.value)
# yield from 后面可以跟一个生成器
def gen_func():
yield 1
yield 2
yield 3
yield 4
yield 5
def g1(gen):
yield from gen
def main():
gen = gen_func()
g = g1(gen)
print(g.send(None))
print(g.send(None))
print(g.send(None))
print(g.send(None))
print(g.send(None))
if __name__ == "__main__":
main()
# main:调用方
# g1:委托生成器
# gen:子生成器
# yield from会在调用方与子生气之间建立一个通道
yield from
# yield from python3.3新增的语法
yield from 后面跟一个可迭代对象/生成器/协程
# 举个例子
from itertools import chain
from collections import Iterable
# chain可将多个可迭代对象合并到一起
my_list=[1,2,3,4,5,6,7,8,9,10]
my_dict={"key1":"value1","key2":"value2","key3":"value3","key4":"value4","key5":"value5"}
for i in chain(my_list, my_dict):
print(i)
# 通过生成器实现chain
def my_chain(*args):
if list(filter(lambda:x:not isinstance(x, Iterable),args))
raise ValueError("must be iterable")
for i in args:
for j in i:
yield j
# 通过yield from来实现
def my_chain(*args):
if list(filter(lambda:x:not isinstance(x, Iterable),args))
raise ValueError("must be iterable")
for i in args:
yield from i
yield from应用
def g1():
total = 0
while 1:
val = yield
if val is None:
break
total += 1
print(val)
return total # 结束后会抛出StopIteration,他会向上抛出异常
def g2():
while 1:
x = yield from g1() # x是g1的返回值
print("g1 return val:", x)
def main():
gen = g2()
gen.send(None)
for i in range(1, 101):
gen.send(i)
gen.send(None)
# g1:子生成器
# g2:委托生成器
# main:调用方
if __name__ == '__main__':
main()
pep380生成器yield from
import sys
"""
子生成器可能只是一个迭代器,并不是一个作为协程的生成器,所以他不支持throw()和close()方法
如果子生成器支持throw()和close()方法,但是在子生成器内部,这两个方法都会抛出异常
调用方让子生成器自己抛出异常
当调用方使用next()或者send(None)时,都要在子生成器调用next()函数,当调用方使用send()发送非None值时,才会调用子生成器的send方法
yield from <expr>
RESULT = yield from EXPR
_i:子生成器
_y:子生成器生产的值
_r:yield from表达式最终值
_s:调用方通过send()发送的值
_e:异常对象
"""
def yield_from(EXPR):
_i = iter(EXPR) # yield from 后面接受一个可迭代对象(迭代器和生成器都是可迭代对象)
try:
_y = next(_i) # 调用_i的__next__方法预激生成器/开始迭代迭代器(_i可以是迭代器也可以是生成器)
except StopIteration as _e:
# 1.当迭代器迭代完会抛出StopIteration生成器return的时候会抛出StopIteration异常
_r = _e.value
# 生成器的return值会传递给StopIteration异常的第一个参数
else:
# 当预激活启动生成器成功执行else
while 1: # 循环,阻塞委托生成器
try:
_s = yield _y
# 将子生成器产出的值yield给调用房,并等待接收调用方传递的值
except GeneratorExit as _e:
# 当关闭委托生成器(即调用了close()方法)或者传递了throw(GeneratorExit)异常
try:
_m = _i.close
# 会调用子生成器的close(首先判断有没有close方法)因为迭代器是没有close方法的
except AttributeError:
pass # 如果是迭代器没有close直接结束了
else:
_m() # 调用子生成器的close方法,关闭生成器
raise _e
except BaseException as _e:
# 除了GeneratorExit,任何异常通过throw传递给子生成器,比如调用方想委托生成器throw异常
_x = sys.exc_info() # BaseException是所有异常的基类,获取异常的信息
try:
_m = _i.throw # 判断是否有throw方法(迭代器是没有throw方法)
except AttributeError:
raise _e # 没有直接raise异常对象
else:
# 如果是_i是生成器,则向子生成器throw捕获的异常
try:
_y = _m(*_x) # exc_info()返回的是一个元组
except StopIteration as _e: # 如果是抛出StopIteration则异常恢复生成器
# 当生成器return是会抛出StopIteratioe,返回值作为StopIteratioe第一个参数传入
_r = _e.value
break
else:
# 当接收调用方传递的值判断是None还是非None
# None调用next()
# 非None调用send()
# _y作为接受值的变量
try:
if _s is None:
_y = next(_i)
else:
_y = _i.send(_s)
except StopIteration as _e:
# 如果生成器yield完了,还继续传递值则会抛出StopIteration
_r = _e.value
break
RESULT = _r # _r就是yield from的结果
"""
1. 子生成器生产的值,都是直接传给调用方的;调用方通过.send()发送的值都是直接传递给子生成器的;如果发送的是 None,会调用子生成器的__next__()方法,如果不是 None,会调用子生成器的.send()方法;
2. 子生成器退出的时候,最后的return EXPR,会触发一个StopIteration(EXPR)异常;
3. yield from表达式的值,是子生成器终止时,传递给StopIteration异常的第一个参数;
4. 如果调用的时候出现StopIteration异常,委托生成器会恢复运行,同时其他的异常会向上 "冒泡";
5. 传入委托生成器的异常里,除了GeneratorExit之外,其他的所有异常全部传递给子生成器的.throw()方法;如果调用.throw()的时候出现了StopIteration异常,那么就恢复委托生成器的运行,其他的异常全部向上 "冒泡";
6. 如果在委托生成器上调用.close()或传入GeneratorExit异常,会调用子生成器的.close()方法,没有的话就不调用。如果在调用.close()的时候抛出了异常,那么就向上 "冒泡",否则的话委托生成器会抛出GeneratorExit异常。#
"""
asyncio
# python3.4引入asyncio异步IO库
# 包含各种特定系统实现的模块化事件循环
# 传输和协议抽象
# 对TCP、UDP、SSL、子进程、延时调用以及其他的具体支持
# 模仿furures模块但适用于事件循环使用的Future类
# 基于yield from的协议和任务,可以让你用顺序的方式编写并发代码
# 必须使用一个将产生阻塞IO的调用时,有接口可以把这个事件转移到线程池
# 模仿threading模块中的同步原语、可以用在单线程内的协程之间
# 事件循环+回调(驱动生成器/协程)+IO多路复用
# asyncio是python用于解决异步IO编程的一整套解决方案
# tornado、gevent、twisted(scrapy、django channel)
# tornado(实现web服务器),django+flask(uwsgi,gunicorn+nginx)
# tornado可以直接部署,nginx+tornado(不能使用同步的数据库驱动)
# event_loop(事件循环):程序开启一个无限的循环,程序员会把一些函数(协程)注册到事件循环上,当满足事件发生的时候,调用相应的协程函数
# coroutine 协程:协程对象,指一个使用async关键字定义的函数,它的调用不会立即执行函数,而是会返回一个协程对象,协程对象需要注册到事件循环,由事件循环调用
# task(任务):一个协程对象就是一个原生可以挂起的函数,任务则是对协程进一步封装,其中包含任务的各种状态,Task对象是Future的子类,它将coroutine和Future联系在一起,将coroutine封装成一个Future对象
# async/await关键字:python3.5 用于定义协程的关键字,async定义一个协程,await用于挂起阻塞的异步调用接口,其作用在一定程度上类似于yield/yield from
import asyncio
# 使用async定义一个原生协程
async def get_html(url):
print("Start get html")
await asyncio.sleep(1) # await后面跟一个awaitable对象/协程对象
print("End get html")
if __name__ == "__main__":
# 创建一个事件循环
loop = asyncio.new_event_loop()
# 也可以直接使用get_event_loop获取,一个线程只有一个事件循环,没有他会自动创建
# loop = get_event_loop()
# 创建一个协程对象
coroutine = get_html("http://xxx.com")
# 将协程对象转成task对象
task = loop.create_task(coroutine)
# 将task注册到事件循环
loop.run_until_complete(task)
yield和await
import asyncio
# python3.5引入async和await关键字
# python为了将语义变得更加明确,就引入async和await关键词用于定义原生的协程
async def coroutine(): # async定义一个协程
# 在async关键字定义的协程中不能使用yield/yield from
await asyncio.sleep(1) # await用于挂起阻塞的异步调用接口,其作用类似yield/yield from,但是await后面只能跟一个Awaitable对象
if __name__ == '__main__':
cor1 = coroutine()
loop = asyncio.new_event_loop()
task = loop.create_task(cor1)
loop.run_until_complete(task)
asyncio:wait&gather
import asyncio
"""
asyncio.wait()
"""
async def coroutine():
print("Start")
await asyncio.sleep(2)
print("End")
if __name__ == '__main__':
tasks = [
asyncio.ensure_future(coroutine()),
asyncio.ensure_future(coroutine()),
asyncio.ensure_future(coroutine()),
asyncio.ensure_future(coroutine()),
]
loop = asyncio.get_event_loop()
# wait是一个协程,wait接受一个迭代对象,可以指定结束的时机,通过wait可以并发执行协程(将多个协程对象放到一个列表中)
# FIRST_COMPLETED、FIRST_EXCEPTION、ALL_COMPLETED:第一次完成、第一次异常、全部完成
loop.run_until_complete(asyncio.wait(tasks))
# gather
import asyncio
async def coroutine():
print("Start")
await asyncio.sleep(1)
print("End")
if __name__ == '__main__':
loop = asyncio.new_event_loop()
task1 = [loop.create_task(coroutine()) for c in range(10)]
task2 = [loop.create_task(coroutine()) for c in range(10)]
g1 = asyncio.gather(*task1)
g2 = asyncio.gather(*task2)
g2.cancel()
loop.run_until_complete(asyncio.gather(g1, g2))
"""
gather对于wait是一个更为高级的抽象
gather可以吧任务分组,并且可以按组取消任务列表
"""
asyncio之task调度过程
import asyncio
async def compute(x, y):
print("Compute %s + %s ..." % (x, y))
await asyncio.sleep(1.0)
return x + y
async def print_sum(x, y):
result = await compute(x, y)
print("%s + %s = %s" % (x, y, result))
if __name__ == '__main__':
loop = asyncio.get_event_loop()
loop.run_until_complete(print_sum(1, 2))
loop.close()
数据结构
数据结构/算法 |
语言内置 |
内置库 |
线性结构 |
list/tuple |
array/collections,namedtuple |
链式结构 |
|
collections.deque |
字典结构 |
dict |
collections.Counter/OrderdDict |
集合结构 |
set/frozenset |
|
排序算法 |
sorted |
|
二分算法 |
|
bisect模块 |
堆算法 |
|
heapq模块 |
缓存算法 |
|
functools.lru_cache(Least Recent Used,python3) |
列表和元组之间的区别
# 都是线形的数据结构,支持下表访问
# 列表是可变对象,元组保存的引用不可变
# list没法作为字典的key,tuple可以(可变对象不可哈希)
dict底层结构
dict底层使用的哈希表
为了支持快速查找使用了哈希表作为底层结构
哈希表平均查找时间复杂度O(1)
CPython解释器使用二次探查解决哈希冲突问题
在所有的线性数据结构中,数组的定位速度最快,因为它可通过数组下标直接定位到相应的数组空间,就不需要一个个查找,而哈希表就是利用数组这个能够快速定位数据的结构的特性
1.取数据元素的关键字key,计算其哈希函数值(地址)。若该地址对应的存储空间还没有被占用,则将该元素存入;否则执行step2解决冲突
2.根据选择的冲突处理方法,计算关键字key的下一个存储地址。若下一个存储地址仍被占用,则继续执行step2,直到找到能用的存储地址为止
# 哈希扩容
# 哈希冲突
LRUCache
Least-Recently-Used替换掉最近最少使用的对象
缓存剔除策略,当缓存空间不够用的时候需要一种方式剔除key
常见的有LRU,LFU等
LRU通过使用一个循环双端队列不断把最新访问的key放到表头实现
# 实现LRUCache
from collections import OrderedDict
class LRUCache(object):
def __init__(self, capacity=128):
self.od = OrderedDict()
self.capacity = capacity
def get(self, key):
if key in self.od:
value = self.od[key]
self.od.move_to_end(key)
return value
else:
return None
def put(self, key, value):
if key in self.od:
del self.od[key]
self.od[key] = value
else:
if len(self.od) > self.capacity:
self.od.popitem(last=False)
self.od[key] = value
算法
排序算法
排序算法 |
最差时间分析 |
平均时间复杂度 |
稳定度 |
空间复制度 |
冒泡排序 |
O(n^2) |
O(n^2) |
稳定 |
O(1) |
选择排序 |
O(n^2) |
O(n^2) |
不稳定 |
O(1) |
插入排序 |
O(n^2) |
O(n^2) |
稳定 |
O(1) |
快速排序 |
O(n^2) |
O(n^log2n) |
不稳定 |
O(log2n)~O(n) |
堆排序 |
O(n^log2n) |
O(n^log2n) |
不稳定 |
O(1) |
什么是排序算法的稳定性
相同大小的元素在排序之后依然保持对象位置不变,就是稳定的
r[i]=r[j]且r[i]在r[j]之前,排序之后r[i]依然在r[j]之前
稳定性对于排序以恶搞复杂结构,并且需要保持原有排序才有意义
快速排序
# 快速排序
快排经常问:分治法,快排三步走
Partition:选择基准分隔数组为来两个子数组,小于基准和大于基准的
对两个子数组分别快排
合并结果
# code 1
def partition(li, left, right):
tmp = li[left]
while left < right:
while left < right and li[right] >= tmp:
right -= 1
li[left] = li[right]
while left < right and li[left] <= tmp:
left += 1
li[right] = li[left]
li[left] = tmp
return left
def quick_sort(li, left, right):
if left < right:
mid =. partition(li, left, right)
quick_sort(li, left, mid-1)
quick_sort(li, mid + 1, right)
# code 2
def quick_sort(array):
# 递归出口
if len(array) < 2:
return array
else:
index = 0
tmp = array[index]
less_part = [
i for i in array[index + 1:] if i <= tmp
]
great_part = [
i for i in array[index + 1:] if i > tmp
]
return quick_sort(less_part) + [tmp] + quick_sort(great_part)
归并排序
# 归并排序(连个有序的列表)
def marge_sorted_list(sorted_a, sorted_b):
length_a, length_b = len(sorted_a), len(sorted_b)
a = b = 0
new_array = []
while a < length_a and b < length_b:
if sorted_a[a] > sorted_b[b]:
new_array.append(sorted_b[b])
b += 1
else:
new_array.append(sorted_a[a])
a += 1
if a > length_a:
new_array.extend(sorted_a[a:])
else:
new_array.extend(sorted_b[b:])
return new_array
# 归并排序
def merge_sorted(seq):
if len(seq) <= 1: # 递归出口
return seq
else:
mid = int(len(seq) / 2)
left = merge_sorted(seq[:mid])
right = merge_sorted(seq[mid:])
new_seq = merge_sorted_list(left, right)
return new_seq
查询算法
二分查找
# 二分查找
def binary_search(array, target):
if not array:
return -1
begin = 0
end = len(array) - 1
while begin <= end:
mid = (begin + end) // 2
if array[mid] > target:
end = mid
elif array[mid] < target:
begin += 1
else:
return mid
return -1
数据库
事务
Transaction
事务数据库并发控制的基本单位
事务可以看作是一系列SQL语句的集合
事务必须要么全部执行成功,要么全部执行失败(回滚)
Atomicity(原子性):一个事务中所有操作全部完成或失败
Consistency(一致性):事务开始和结束之后数据完整性没有被破坏
Isolation(隔离性):允许多个事务同时对数据库修改和读写
Durability(持久性):一旦事务提交,则其所做的修改就会永久保存到数据库中
事务的异常和隔离性
# 四种异常情况:
幻读(phantom read):一个事务第二次查出第一次没有的结果
非重复读(nonrepeatable read):一个事务重复读两次得到不同结果
脏读(dirty read):一个事务读取到别一个事务没有提交的修改
丢失修改(lost update):并发写入造成其中一些修改丢失
# 四种隔离级别
读未提交(read uncommitted):别的事务可以读取到为提交改变
读已提交(read committed):只能读取已经提交的数据
可重复读(repeatable read):同一个事务先后查询结果一样
串行化(serializable):事务完全串行化的执行,隔离级别最高,执行效率最低
# 解决高并发场景下的插入重复
使用数据库的唯一索引
使用队列异步写入
使用redis等实现分布式锁
# 乐观锁和悲观锁
悲观锁是先获取锁再进行操作,一锁二查三更新 select for update
乐观锁先修改,更新的时候发现数据已经变了就回滚(check and set)
使需要根据相应速度、冲突频率、重试代价来判断使用哪一种
数据类型
# 文本
char varchar text tinytext
# 数值
tinyint smallint mediumint int bigint float double decimal
# 日期
date datetime timestamp
存储引擎
MyISAM不支持事务,InnoDB支持事务
MyISAM不支持外键,InnoDB支持外键
MyISAM只支持表锁,InnoDB支持行锁和表锁
数据库索引
索引的原理、类型、结构
创建索引的注意事项、使用原则
如何排查和消除慢查询
为什么需要索引
索引是数据表中一个或者多个列进行排序的数据结构
索引能够大幅提升检索速度
创建、更新索引本身也会耗费空间和时间
B-Tree
线性查找:一个个找,实现简单,太慢
二分查找:有序简单,要求是有序的插入,特别慢
HASH:查询快,占用空间,不太适合存储大规模数据
二叉查找树:插入和查询很快(log(n)),无法村大规模数据,复杂度退化
平衡树:解决bst退化的问题,树是平衡的,节点非常多的时候,依然树高很高,一个父节点最多有两个字节点
多路查找树:一个父亲多个孩子节点(度),节点过多树高不会特别深
# B-Tree
多路平衡查找树(B-Tree):每个节点最多m(m>=2)个孩子,称为m阶或者度
叶子节点具有相同的深度
节点中的数据key从左到右是递增的
# B+Tree
MySQL时机使用的B+Tree作为索引的数据结构
只在叶子节点带有指向记录的指针(为什么?可以增加树的度)
叶子节点通过指针相连,为什么?实现范围查询
数据只存放在叶子节点中
普通索引(create index)
唯一索引(create unique index)
联合索引
主键索引(primary key)
全文索引(FULLTEXT INDEX),InnoDB不支持
什么时候创建索引
经常用做查询条件的字段(where字段)
经常用做表连接的字段
经常出现在 order by,group by 之后的字段
索引注意点
非空字段 NOT NULL,mysql很难对空值做查询优化
区分度高、离散度大,作为索引的字段值尽量不要有大量相同值
索引的长度不要太长(比较耗费时间)
索引失效
模糊匹配、类型隐转、最左匹配
以%开头的LIKE语句,模糊搜索
出现隐式转换(在Python这种动态语言查询中需要注意)
没有满足最左前缀原则(最左匹配)
聚集索引和非聚集索引
聚集还是非聚集指的是B+Tree叶节点存的是指针还是数据记录
MyISAM索引和数据分离,使用的是非聚集索引
InnoDb数据文件就是索引文件,主键索引就是聚集索引
聚集索引和辅助索引
辅助索引先找到主键以后在根据主键找到数据
慢查询
开启满查询日志
slow_query_log_file 开启并且查询满查询日志
explain排查索引问题
调整数据修改索引,业务代码层闲置不合理访问
mysql分库分表
分布式锁
Redis
# 使用场景
缓存系统
计数器
消息队列系统
排行榜
社交网络
实时系统
布隆过滤器
# 数据类型(Redis设计与实现)
字符串类型:用来实现简单的KV简直对存储,比如计数器
String:整数或者sds(simple dynamic string)
哈希类型:
ziplist或者hashtable
列表类型:实现双向链表,用户的关注、粉丝列表
list:ziplist或者double linked list
集合类型:存储不重复元素,不如用户的关注着
intset或者hashtable
有序集合类型:实时信息排行榜
skiplist
# 数据一致性问题、缓存穿透、击穿、雪崩问题
Redis特性
缓解关系型数据库(Mysql)并发访问的压力:热点数据
减少相应时间:内存IO速度远比磁盘快
提升吞吐量:Redis单机支持很高的并发
10w OPS # 每秒10w次读写
数据存放在内存
使用C语言编写
单线程
Redis持久化RDB
持久化方式:
快照
mysql dump
redis rdb
写日志
mysql binlog
hbase hlog
redis aof
# RDB(他也是一个复制媒介)
从内存创建一个rdb文件(二进制)到硬盘
启动redis载入硬盘上rdb文件(二进制)
触发机制
save(同步)
文件策略:如存在老的RDB文件,新的RDB文件替换老的RDB文件
复杂度:O(n)
bgsave(异步)
执行bgsave时,redis会fork一个子进程来创建RDB文件,不会阻塞其他的命令执行
文件策略:如存在老的RDB文件,新的RDB文件替换老的RDB文件
复杂度:O(n)
自动
在配置文件中配置执行条件
save 900 1 # 900秒 1次操作触发
save 300 10 # 300秒 10次操作出发
save 60 10000 # 60秒 1万次操作出发
dbfilename dump.rdb
dir ./
stop-writes-on-bgsave-error yes # 当bgsave出现错误,是否停止写入
rdbcompression yes # rdb文件是否采用压缩格式
rdbchecksum yes # rdb校验和
# 触发机制
1.全量复制
2.debug reload
3.shutdown
rdb总结
rdb是redis内存到硬盘的快照,用于持久化
save通常会阻塞redis
bgsave不会阻塞redis,但是会fork新进程
save自动配置满足任一个条件就会被执行
有些触发机制不容忽视
Redis持久化AOF
rdb有什么问题
耗时、耗性能
O(N)数据耗时
fork():消耗内存,copy-on-write
disk I/O:IO性能
不可控、丢失数据
# AOF(append only file)只追加文件,记录服务器执行的所有写操作命令,并在服务器启动时,通过重新执行这些命令来还原数据集。 AOF 文件中的命令全部以 Redis 协议的格式来保存,新命令会被追加到文件的末尾
三种策略
always
写命令刷新到缓冲区,每条命令fsync到硬盘
不丢失数据
IO开销较大,一般的sata盘只有几百TPS
everysec
写命令刷新到缓冲区,每秒把缓冲区fsync到硬盘
每秒一次fsync
丢一秒数据
no
写命令刷新到缓冲区,os决定fsync
AOF重写作用
减少硬盘占用量
加速恢复速度
AOF重写实现两种方式
bgrewriteaof
向redis服务端发一条命令,执行bgrewriteaof,redis会fork一个子进程
AOF重写配置
auto-aof-rewrite-min-size # AOF文件重写需要的尺寸
auto-aof-rewrite-percentage # AOF文件增长率
# 统计
aof_current_size # AOF当前尺寸(单位:字节)
aof_base_size # AOF上次启动和重写的尺寸
自动触发时机(同时满足)
aof_current_size > auto-aof-rewrite-min-size
(aof_current_size - aof_base_size)/aof_base_size > auto-aof-rewrite-percentage
配置
appendonly yes
appendfilename appendonly.aof
appendfsync everysec
dir ./
no-appendfsync-on-rewrite yes
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
Redis事务
和mysql的事务有什么不同?
将多个请求打包,一次性,按顺序执行多个命令的机制
redis事务提交的命令中间不可插入
# python redis-py 实现事务
import redis
conn = redis.Redis()
with conn.pipeline(True) as pipe:
pipe.multi() # 开始事务
pipe.set("Hello", "World")
pipe.lpush("x", 1)
pipe.execute() # 执行
Redis实现分布式锁
# 基于redis单线程的特性和setnx
# setnx当key存在什么都不操作并返回0,key不存在是设置value并返回1
# 通过expire添加超时时间防止死锁问题
# 锁的value值可以使用一个随机的uuid或者特定的命名
# 释放锁的时候,通过uuid判断是否是该锁,实则执行delete
缓存模式
Cache Aside:同时更新缓存和数据库
Read/Write Through:先更新缓存,缓存负责同步更新数据库
Write Behind Caching:先更新缓存,缓存定期异步更新数据库
# 先更新数据库,在更新缓存
# 先删除缓存,在更新数据库
# 先更新数据库,在删除缓存
缓存问题
缓存穿透
大量查询不到的数据的请求落到后段数据库,数据库压力增大
# 由于大量缓存查不到就去数据库取,数据库也没有要查的数据
# 解决:对于没有查到的返回为None的数据也缓存
# 插入数据的时候删除相应缓存,或者设置较短的超时时间
缓存击穿
某些非常热点的数据key过期,大量请求打到后段数据库
# 热点数据key失效导致大量请求达到数据库增加数据库压力
# 分布式锁:获取锁的线程从数据库拉数据更新混存,其他线程等待
# 异步后台更新:后台任务针对过期的key自动刷新
# 缓存雪崩
胡纳村不可用或者大量缓存key同时失效,大量请求直接打到数据库
多级缓存:不同级别的key设置不同的超时时间
随机超时:key的超时时间随机设置,方式同时超时
架构层:提升系统可用性,监控、报警完善