python进阶
数据库
概念
概念:
高效的存储和处理数据的介质(主要分为磁盘和内存两种)
按照一定的数据结构来组织、存储和管理数据的仓库
分类:
关系型数据库(SQL)
安全,其将数据保存到磁盘之中
Oracle、MySQL 和 SQL Server 等。
非关系型数据库(NoSQL,Not Only SQL)
存储数据的效率比较高,但储存方式比较灵活,不太安全
Memcached、MongoDB 和 Redis 等。
区别
关系型数据库(SQL)
数据存储在特定结构的表中
Oracle、MySQL 和 SQL Server 等。
非关系型数据库(NoSQL,Not Only SQL)
存储方式比较灵活,数据以对象的的形式存储在数据库中。
MySQL
mysql简介
mysql是最流行的关系型数据库管理系统之一,由瑞典MySQL AB公司开发,目前属于Oracle公司。 MySQL是一种关联数据库管理系统,关联数据库将数据保存在不同的表中,而不是将所有数据放在一个大仓库内,这样就增加了速度并提高了灵活性。
(开源,免费)
关系型数据库:采用关系模型来组织数据的数据库
关系:一张二维表,每个关系都有一个关系名,就是表名
模型:行和列(二维),具体指字段跟字段信息
库级和表级操作
#进入与退出 数据库
#进入 mysql –uusername -ppassword
#退出 mysql> exit
库级操作语句
显示所有库: show databases;
创建库: create database [if not exists] db_name;
删除库: drop database [if exists] db_name;
切换当前数据库: use db_name;
# 注意:
1.重复创建会报错,所以可以加上if not exists
2.如果不知道数据库是否存在,记得加if exists
表级操作语句
显示所有表: show tables;
创建表: create table tbl_name(create_definition,...);
显示创建表的信息:show create table tb_name;
删除表: drop table tb_name;
注意事项
语句结束符: 每个语句都以 ; 或 \G 结束
大小写: 不严格区分大小写, 默认大写为程序代码,小写为程序员写的代码
类型: 强制数据类型、任何数据都有自己的类型
逗号: 创建表最后一列不需要逗号
表中数据的增、删、查、改
# CRUD操作
C:CREATE/INSERT #创建/插入
R:READ/SELECT #读取/查询
U:UPDATE #更新
D:DELETE #删除
创建/插入
指定列插入: INSERT INTO tb_name(col_names) VALUES (col_values);
全列插入: INSERT INTO tb_name VALUES (all_values);
多行插入: INSERT INTO tb_name(col_names) VALUES (value_1), (value_2), …;
# 注意:区别
指定列插入:插入某列某值,单行操作
全列插入:需要给所有列赋值,单行操作
多行插入:插入多行数据
查询
指定列查询:SELECT col_names FROM tb_name;
全列查询: SELECT * FROM tb_name;
带条件的查询: SELECT col_names FROM tb_name WHERE conditions;
修改
修改表中所有数据:UPDATE tb_name SET field_1=value_1
修改表中指定的数据: UPDATE tb_name SET field_1=value_1 WHERE conditions;
注意:一定要写where条件,不然会修改表中全部数据
删除
删除表中所有数据:DELETE FROM tb_name;
删除表中满足条件的数据: DELETE FROM tb_name WHERE conditions;
# 注意:一定要写where条件,不然会删除表中全部数据
mysql常用数据类型
create table tb2(
id INT,
name VARCHAR(20), #指定长度,最多65535个字符。 变长字符串
sex CHAR(4), #指定长度,最多255个字符。 定长字符串
price DOUBLE(4,2), #双精度浮点型,m总个数,d小数位
detail text, #可变长度,最多65535个字符
dates DATETIME, #日期时间类型 YYYY-MM-DD HH:MM:SS
ping ENUM('好评','差评’) #枚举, 在给出的value中选择
);
insert into tb2 value( 1,'裤子','男',20.0,'这条裤子超级好!!!',now(),'好评');
表结构修改
修改表名: ALTER TABLE tb_name RENAME TO new_students;
修改字段名: ALTER TABLE tb_name CHANGE name new_name CHAR(10);
修改字段类型: ALTER TABLE tb_name MODIFY field_name CHAR(10);
表描述: DESC tb_name;
添加列: ALTER TABLE tb_name ADD [COLUMN] name varchar(10);
添加到第一列: ALTER TABLE tb_name ADD [COLUMN] name first;
添加到指定列后: ALTER TABLE tb_name ADD name varchar(10) after field_name;
删除列: ALTER TABLE tb_name DROP [COLUMN] name;
约束条件
思考
问题一: 如果插入一条记录的时候, 某列没有插入值,会是什么 ?
问题二: 能否确保某列的值绝对不为空 ?
问题三: 能否确保某列的值不重复 ?
问题四: 能否确保某列的值必须参照另一列 ?
约束条件
约束是一种限制,通过对表中的数据做出限制,来确保表中数据的完整性,唯一性。
默认约束 default
default :初始值设置,插入记录时,如果没有明确为字段赋值,则自动赋予默认值。
给一个字段添加默认值 例子:
create table tb(
id int,
name varchar(20),
age int default 18
);
#创建了表后添加
mysql> alter table tb modify age int default 20;
#删除default
mysql> alter table tb modify age int;
not null 非空约束
注意:
空字符不等于null;
限制一个字段的值不能为空,insert的时候必须添加改字段
例子:
CREATE TABLE tb1(
id int,
name varchar(20) NOT NOLL
);
#手动,添加非空约束
mysql> alter table tb1 modify id int not null;
# 取消非空约束
mysql> alter table tb1 modify id int ;
unique key 唯一约束
确保字段中的值的唯一
例子:
CREATE TABLE tb2(
id int UNIQUE KEY,
name varchar(20)
);
#添加唯一约束
mysql> alter table tb2
->modify name varchar(20) unique
->;
#删除唯一约束
mysql> alter table tb2
-> drop key name;
主键约束 primary key
主键的作用: 可以唯一标识 一条数据,每张表里面只能有一个主键,。
主键特性: 非空且唯一。当表里没有主键的时,第一个出现的非空且为唯一的列,被当成主键。
例子:
create table tb4(
id int primary key,
name varchar(20) not null
);
唯一标识 一条数据
主键 = 非空 + 唯一
#删除主键约束
mysql -> alter table tb4
-> drop primary key;
#添加主键约束
mysql> alter table tb3
-> add primary key(id);
自增长 auto_increment
auto_increment :自动编号,一般与主键组合使用。一个表里面只有一个自增,默认情况下,起始值为1,每次的增量为1。
例子:
create table tb3(
id int primary key auto_increment,
name varchar(20)
)auto_increment =100;
AUTO_INCREMENT 一般用
在主键上
#删除自动增长
mysql> alter table tb3 modify id int;
#增加自动增长auto_increment
mysql> alter table tb3
-> modify id int auto_increment;
外键约束 foreign key
外键约束 :保持数据一致性,完整性实现一对多关系。
外键必须关联到键上面去,一般情况是,关联到另一张表的主键
#增加外键
mysql> alter table b
-> add constraint AB_foreign foreign key(b_id) references a(a_id);
#删除外键
alter table b drop foreign key AB_foregin;
B表中的b_id 字段,只能添加 a_id中 已有的数据。
A表中a_id 被参照的数据,不能被修改和删除,
要先删除b_id中的数据,才能删除a_id中的数据
子查询
子查询 问题引入
问题一:如何找到“张三”的成绩 ?
问题二:能否将一个查询的结果留下来用于下一次查询 ?
案例:
找到张才的成绩 => 先找到张三的学号,再用这个学号去筛选成绩表就能找到 !
SELECT number FROM students WHERE name=‘张三’;
SELECT student_number,subjects,grade FROM grades
WHERE student_number=201804003;
嵌套查询
如果把查询结果当作条件呢 ?
将学生表的查询结果嵌套进成绩表中!
SELECT grade FROM grades
WHERE student_number = (SELECT number FROM students WHERE name='张三');
连接查询
交叉连接
交叉连接又名笛卡尔连接它列出了所有情况其中一定有你需要的组合
内连接
左表 [inner] join 右表 on 左表.字段 = 右表.字段;
on 后面是筛选条件
条件字段就是代表相同的业务含义(如my_student.c_id和my_class.id)
select name, subject_number as number, grade from
students join grades on
students.number = grades.student_number;
事务/编码
原子性
所做操作要么完成,要不完成,不会执行过程终止
一致性
执行操作前和操作后,数据完整性没有破坏
隔离性
同时读写修改同一个数据,保证数据的一致性
持久性
对数据的修改是持久的,即使系统破坏数据也不会消失
编码问题
创建数据库自定义编码
CREATET DATABASE tb_name CHARACTER SET gbk;
创建数据表自定义编码
CREATE TABLE tb_name (
id INT
) CHARSET utf8;
编辑配置文件:
vim /etc/mysql/mysql.conf.d/mysqld.cnf
重启数据库: sudo service mysql restart
远程连接
配置文件:
vim /etc/mysql/mysql.conf.d/mysqld.cnf
第一步:修改监听IP
bind 0.0.0.0
第二步:给用户添加远程访问权限
update user set host ='%' where user='root';
第三步:重启数据库
sudo service mysql restart
用户权限
Root账号权限太大,一般只在管理数据库的时候 ,一般项目都是创建一个用户去操作
创建用户:CREATE USER 'username'@'localhost' IDENTIFIED BY 'password';
用户授权:grant 权限 on 数据库.* to 用户名@登录主机 identified by "密码";
取消用户授权: revoke privilege ON databasename.tablename from 'username'@'%';
筛选条件
比较运算符
等于: = ( 注意!不是 == ) | 大于等于: >= | IS NULL |
---|---|---|
不等于: != 或 <> | 小于: < | IS NOT NULL |
大于: > | 小于等于: <= |
NULL 判断
INSERT INTO students VALUES ('201804011', NULL, NULL, NULL, NULL);
逻辑运算符
与(且): AND
或: OR
非(不是): NOT
范围查询
连续范围: BETWEEN a AND b (a <= value <= b)
间隔返回: IN
模糊查询
任意多个字符: % ( like ‘%’ )
任意一个字符: _
排序
SELECT columns
FROM tb_name
ORDER BY ord_col_1 [asc/desc];
正序: asc(默认)
倒序: desc
去重
SELECT DISTINCT columns
FROM tb_name;
聚合分组
问题引入
问题一: 仅从文字上看,你认为聚合是什么意思 ?(把多个值聚在一起)
问题二: 聚合通常是为了什么目的 ?(统计信息)
问题三: 分组对于聚合而言意味着什么 ?(分组统计 group by)
问题四: 能否对筛选聚合值 ?(可以,having)
常用聚合函数
统计个数: COUNT(column)
最大值: MAX(column)
最小值: MIN(column)
求和: SUM(column)
平均值: AVG(column)
列出字段全部值:GROUP_CONCAT(column)
分组查询
SELECT group_column, aggregations
FROM tb_name
GROUP BY group_column;
在分组的情况下,只应该出现分组列和聚合列
其他的列没有意义 , 会报错!
聚合筛选
SELECT group_column, aggregations
FROM tb_name
GROUP BY group_column
HAVING conditions;
加HAVING 条件表达式,可以限制输出的结果
WHERE 不能使用别名, having可以使用别名!
WHERE 不能筛选聚合函数!
总结
假如说, 一个查询中同时包含了 别名, 聚合函数,WHERE、HAVING 那么它们的执行顺序是:
先是执行: WHERE
然后执行: 聚合函数和别名
最后执行: HAVING
限制与分页
问题引入
问题一: 如果一次性不需要那么多数据,该如何做 ?(对结果做出限制)
问题二: 能否从指定位置开始取 ?(可以)
问题三: 分页是如何做的 ?(limit)
限制结果个数
SELECT columns FROM tb_name LIMIT count;
SELECT columns FROM tb_name LIMIT start, count;
分页
SELECT columns FROM tb_name LIMIT count;
SELECT columns FROM tb_name LIMIT start, count;
程序中分页
SELECT columns FROM tb_name LIMIT (n-1)*m, m
在python中操作mysql
统一的接口标准DB-API
连接接口: pymysql.connect(**dbconfig)
关闭接口: cnn.close()
游标接口: cnn.cursor()
关闭游标: cur.close()
执行接口: cur.execute(sql)
结果集接口: cur.fetchall()
#连接是不能操作数据库的, 需要用连接生成游标来从操作
程序连接数据库
db_config = {
'user': 'root',
'password': 'qwe123',
'db': 'python3',
'charset': 'utf8', # 不是utf-8
}
# conn = pymysql.connect(**db_config)
别忽略了这些点
注意点一: 和文件一样,别忘了关闭 游标与连接
注意点二: 在pymysql中执行SQL不需要加 ;
注意点三: execute 执行完后不是直接得到结果集而需要你主动去获取
Redis
什么是NoSQL?
NoSQL:一类新出现的数据库(not only sql),它的特点:
不支持SQL语法
存储结构跟传统关系型数据库中的那种关系表完全不同,nosql中存储的数据都是Key Value形式
NoSQL的世界中没有一种通用的语言,每种nosql数据库都有自己的api和语法,以及擅长的业务场景
NoSQL中的产品种类相当多:
Mongodb
Redis
Hbase hadoop
Cassandra hadoop
什么是redis
Redis简介:
Redis是一个开源免费、可基于内存亦可持久化的键值对存储数据可以 ,使用 C语言编写,并提供多种语言的API。从2010年3月15日起,Redis的开发工作由VMware主持。从2013年5月开始,Redis的开发由Pivotal赞助。
Redis是 NoSQL技术阵营中的一员,它通过多种键值数据类型来适应不同场景下的存储需求,借助一些高层级的接口使用其可以胜任,如缓存、队列系统的不同角色
Redis特性:
Redis支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用。
Redis不仅仅支持简单的key-value类型的数据,同时还提供list,set,zset,hash等数据结构的存储。
Redis支持数据的备份,即master-slave模式的数据备份。
如何使用Redis服务端和客户端命令?
服务器端:
服务器端的命令为redis-server
可以使⽤help查看帮助⽂档
redis-server –help
启动
sudo service redis start
停⽌
sudo service redis stop
重启 sudo service redis restart
客户端:
客户端的命令为redis-cli
可以使⽤help查看帮助⽂档
redis-cli --help
连接redis
redis-cli
切换数据库
数据库没有名称,默认有16个,通过0-15来标识,连接redis默认选择第一个数据库
select n
常用数据类型与命令
Reids的数据结构
数据结构:
redis是key-value的数据结构,每条数据都是⼀个键值对
键的类型是字符串
注意:键不能重复
值的类型分为五种:
字符串string
哈希hash
列表list
集合set
有序集合zset
基本数据类型
String 字符串
Hash 哈希
List 列表
Set 集合
List 有序集合
Sting类型
string是redis最基本的类型,一个key对应一个value。
SET key value 设置
GET key 获取
APPEND key value 追加
SET key value EX seconds 指定过期时间
全局Key操作
KEYS * | 查看查看所有的key |
---|---|
DEL key | 删除 |
EXISTS key | 查看key是否存在 |
RENAME key newkey | 改名 |
EXPIRE key seconds | 设置超时时间 |
TTL key | 返回生存时间 |
PERSIST key | 删除生存时间 |
List类型
List类型是按照插入顺序排序的字符串链表。在插入时,如果该键并不存在,Redis将为该键创建一个。
LPUSH key value [value…] | 左添加 / rpush |
---|---|
LINDEX key index | 查看list中某一个元素 |
lRANGE key start stop | 查看多个元素 |
LLEN key | 查看list元素个数 |
LSET key index value | 修改 |
LPOP key | 删除元素/ rpop |
LREM key count value | 删除指定个数 |
Hash类型
例:user { name:juhao, age:18 }
key(键) field(域) value(值
HSET key field value | 设置 |
---|---|
HMSET key field1 value1 field2 value2 … | 添加多个键值对 |
HGET key field | 获取value |
HMGET key field field | 获取多个value |
HVALS key | 获取全部的values |
HKEYS key | 获取所有的field |
HGETALL key | 获取所有field和value |
Set类型
Set类型没有排序的字符集合。
如果多次添加相同元素,Set中将仅保留该元素的一份拷贝
SADD key member1 [member2..] | 增加 |
---|---|
SMEMBERS key | 查看所有元素 |
SREM key member1 [member2..] | 指定删除 |
SISMEMBER key member | 判断集合是否存在某个值 |
SMEMBERS key | 查看set里面的元素 |
Sorted Set类型
每一个成员都会有一个分数(score)与之关联。
成员是唯一的,但是分数(score)却是可以重复的。
zadd key score1 member1 [score2 member2] | 设置 |
---|---|
zcount key min max | 返回score值在区间的元素个数 |
zcard key | 返回zset元素个数 |
zscore KEY MEMBER | 查看score值 |
zrange key start stop | 按照索引返回区间成员 |
Zrangebyscore key min max | 按照score值返回区间成员 |
zrem key member [member …] | 删除 |
Redis配置
Redis配置文件
在Ubuntu系统中默认配置文件地址: /etc/redis/redis.conf
port 5739 # 默认端口
logfile /var/log/redis.log # 日志文件位置
dbfilename dump.rdb # RDB持久化数据文件
dir /var/lib/redis #本地数据库存放目录
bind 127.0.0.1 # 指定IP进行监听
requirepass yourpassword #密码
Save 900 1 #持久化
Mongodb
mongodb介绍
非关系型数据库(NoSQL)
开源,免费,高性能的文档存储数据库
高效存储二进制大对象
海量数据下,性能优越
库级操作语句
显示所有库: show dbs
切换/创建数据库: use 数据库名称
查看所在库: db
删除库:db.dropDatabase()
集合(表)操作语句
查看当前数据库的集合: show collections
集合创建: db.createCollection(name)
删除集合:db.集合名称.drop()
数据级(文档) 增删查改 操作
插入文档
db.集合名称.insert(document)
每一条数据,就是一个document,就是一条json
例: db.student.insert({name:‘juhao',age:18})
插入文档时,如果不指定_id参数 MongoDB会为文档分配一个唯一的ObjectId
例: db.student.insert({_id: 1, name: 'juhao', age:18})
插入多条:
db.student.insert([
{name:'juhao', sex:'男', age:18},
{name:'nanbei', sex:'男', age:18},
{name:'budong', sex:'男', age:18},
])
查询文档
db.集合名称.find()
例: db.student.find()
例: db.student.insert().pretty()
高级查询
and条件 {$and:[{expression1}, {expression1}, ...] }
or条件 {$or:[{expression1}, {expression1}, ...] }
操作符 | 描述 |
---|---|
$ne | 不等于 |
$gt | 大于 |
$lt | 小于 |
$gte | 大于等于 |
$lte | 小于等于 |
更新文档
db.集合名称.update(<query>, <update>, {multi: <boolean>} )
全文档替换:
db.table.update({sex:'男'},{age:20})
指定属性更新: { $set: {age:20} }
db.table.update({name:'juhao'}, {$set: {age:666, sex: 'xx'}} )
更新多条: { multi: ture }
db.table.update({sex:'男'}, {$set:{sex:'女'}}, { multi:true} )
删除文档
db.集合名称. remove(<query>, {justOne: <boolean>)
删除满足条件的所有数据:
db.table.remove({sex: '男’} )
删除集合所有的文档
db.table.remove({})
只删除一条数据: { justOne: true }
db.table.remove( {sex:’男'},{$set:{sex:'女'}},{ multi:true} )
Python操纵mongodb
程序操纵mongodb
安装python包:pip install pymongo
建立连接: client= pymongo.MongoClient('127.0.0.1', 27017)
指定数据库:db=client[ 数据库名 ]
指定集合:collection=db [ 集合名]
Python工具库
datetime与logging模块
logging
日志五个级别
日志等级(level) | 描述 |
---|---|
DEBUG | 调试信息,通常在诊断问题的时候用得着 |
INFO | 普通信息,确认程序按照预期运行 |
WARNING | 警告信息,表示发生意想不到的事情,或者指示接下来可能会出现一些问题,但是程序还是继续运行 |
ERROR | 错误信息,程序运行中出现了一些问题,某些功能没有执行 |
CRITICAL | 危险信息,一个严重的错误,导致程序无法继续运行 |
Logging使用
函数 | 函数 |
---|---|
logging.log(level, message) | 创建一条严重级别为level的日志记录 |
logging.basicConfig() | 对logger进行配置 |
Formatter格式
%(asctime)s | 日志事件发生的时间 |
---|---|
%(levelname)s | 该日志记录的日志级别 |
%(message)s | 日志记录的文本内容 |
%(name)s | 所使用的日志器名称,默认是'root' |
%(pathname)s | 调用日志记录函数的文件的全路径 |
%(filename)s | 调用日志记录函数的文件 |
%(funcName)s | 调用日志记录函数的函数名 |
%(lineno)d | 调用日志记录函数的代码所在的行号 |
Logging与print差异
print()确实方便和易用,但是也有缺点,比如打印出来的信息不能保存,再次运行程序,之前打印出来的结果被清空
日志记录包含清晰可见的诊断信息,如文件名称,路径,函数名和行号等
模块化组件
组件 | 说明 |
---|---|
Loggers(日志记录器) | 提供程序直接使用的接口 |
Handlers(日志处理器) | 将记录的日志发送到指定的位置 |
Filters(日志过滤器) | 对日志记录进行更详细的输出 |
Formatters(日志格式器) | 用于控制日志信息的输出格式 |
模块化组件使用
1.初始化一个logger对象
2.定义handler,决定把日志发到哪里
常用的是:
StreamHandler:将日志在控制台输出
FileHandler: 将日志记录到文件里面
3.设置日志级别(level)和输出格式(Formatter)
4.把handler添加到logger中去
日期与时间
datetime是python处理时间和日期的标准库
类名 | 功能说明 |
---|---|
date | 日期对象,常用的属性有year, month, day |
time | 时间对象hour,minute,second,毫秒 |
datetime | 日期时间对象,常用的属性有hour, minute, second, microsecond |
timedelta | 时间间隔,即两个时间点之间的长度 |
主要使用: datetime.datetiem( ) 、 datetime.timedelta( )
实用函数
当前日期时间: datetime.now()
日期时间转换为时间戳:datetime.timestamp()
时间戳转换为日期时间: datetime.fromtimestamp(时间戳)
字符串转换为日期时间: datetime.strptime(data_str, format)
日期时间转字符串:datetime.strftime(format)
时间计算
datetime.timedelta( days=0,seconds=0, microseconds=0 milliseconds=0,minutes=0, hours=0, weeks=0 )
格式字符串 常用格式
格式 | 描述 |
---|---|
%Y/%y | 年 |
%m | 月 |
%d | 日 |
%H/%l | 时 |
%M | 分 |
%S | 秒 |
json与base64与hashlib模块
json模块
全名是JavaScript Object Notation(即:JavaScript对象表示法)它是JavaScript的子集。
轻量级的文本数据交换格式
易于阅读和编写,同时也易于机器解析和生成
互联网当中最理想的数据交换语言
前后端数据交互
JS对象 | JSON字符串 | Python字典 |
---|---|---|
var teacher_1 = { |
name: ‘juhao’,
age: 18,
feature : [‘高’, ‘富’, ‘帅’]
} | {
“name”: “juhao”,
“age”: 18,
“ feature “ : [‘高’, ‘富’, ‘帅’]
} | {
‘name’: ‘juhao’,
‘age’: 18
‘feature’ : [‘高’, ‘富’, ‘帅’]
} |
Json语法规则
数据在键/值对中 ; 数据有逗号分隔; 大括号保存对象 ; 中括号保存数组
Json语法规则
JSON注意事项:
名称必须用双引号(即:””)来包括
值可以是双引号包括的字符串、数字、true、false、null、数组,或对象。
Python | JSON |
---|---|
字典 | 对象 |
列表或元组 | 数组 |
字符串 | 字符串 |
int或float | 数字 |
True或False | true或false |
None | null |
json模块 API
接口一:json.dumps(obj) | #把python的数据转化成json字符串(indent实现缩进, sort_keys 实现排序, ensure_ascii实现是否用ascii解析) |
---|---|
接口二:json.loads(s) | #将JSON数据,转换成Python的数据类型 |
接口三:json.dump(obj, fp) | #转换为json并保存到文件中 |
接口四:json.load(fp) | #从文件中读取json,并转化为python数据类型 |
hashlib模块
Hash(哈希)
hash也叫做散列函数
可以把任意长度的数据转换为,一个长度固定的数据串
特点:不可逆,唯一
用处:1.数据加密 2.数据查找
Hashlib模块常用函数
Hashlib模块提供了许多的hash算法,包括
md5, sha1, sha224, sha256, sha384, sha512
api | 描述 |
---|---|
hashlib.new(name,data='b') | 生产一个hash对象 |
hashlib.update(arg=None) | 更新hash对象 |
hash.digest() | 返回hash算法计算得到的数值(bytes类型) |
hash.hexdigest() | 返回hash算法计算得到的数值(str类型) |
hashlib.pbkdf2_hmac(name,password,salt,rounds,dklen=None) | 用于密码加密,返回值是加密后的数值 |
base64模块(了解)
Base64是一种用64个字符来表示任意的二进制数据的方法
将二进制数据转换成可打印字符,方便传输数据
对数据进行简单的加密,肉眼安全。
适用于小段内容的编码,比如URL
base64模块常用函数
api | 描述 |
---|---|
base64.b64encode(s) | 编码 |
base64.b64decode(s) | 解码 |
base64.urlsafe_b64encode(s) | 对url进行编码 |
base64.urlsafe_b64decode(s) | 对url进行解码 |
网络通信
传输模型与套接字
传输模型
知识点一: 基本模型
知识点二: 层次的划分
知识点三: 传输层说明
知识点一: 基本模型
知识点二: 层次的划分
知识点三: 传输层说明
说明一:
作为Python开发,咱们都是在应用层的HTTP协议之上进行开发的。
说明二:
网络编程,主要是了解我们Python能编写的最低的层次, 即传输层的基本情况
说明三:
HTTP协议是基于TCP之上的 因此我们需要了解TCP连接的基本过程
传输模型总结
层次划分
传输层
TCP协议
TCP连接
TCP连接 知识点
知识点一: 建立连接(三次握手)
知识点二: 传输数据
知识点三: 断开连接(四次挥手)
TCP连接总结
建立连接
传输消息
断开连接
IP地址与端口
IP地址与端口知识点
知识点一: IP地址
知识点二: 端口号
私有(内网)IP
IPv4地址协议中预留了3个IP地址段,作为私有地址,供组织机构内部使用
私有地址可以自己组网时用,但不能在外网上用
这些地址的计算机要上网必须转换成为合法的IP地址,也就是公网地址
分类 | 范围 |
---|---|
A类 | 10.0.0.0-10.255.255.255 |
B类 | 172.16.0.0-172.31.255.255 |
C类 | 192.168.0.0-192.168.255.255 |
知识点二: 端口号
端口号:用来唯一标识应用程序
每个应用程序都占了一个端口
共有65535个端口
TCP连接总结
程序通信
先找IP
再找端口
套接字
套接字知识点
知识点一: 创建套接字
知识点二: 建立套接字连接
知识点三: 使用套接字传输数据
知识点四: 断开套接字连接
创建套接字
套接字基本概念
TCP连接两个计算机进行通信,一条线会有两个端点,我们把连接两端的端点称为套接字(socket)
创建套接字
导入socket模块:import socket
创建socket对象:socket = socket.socket()
建立套接字连接
建立套接字连接要点
服务端套接字的绑定与监听
客户端套接字主动连接
服务端对等连接套接字的生成
阻塞说明
使用套接字传输
使用套接字传输要点
客户端发送请求数据到服务端
服务端接受来自客户端的请求数据
服务端向客户端返回响应数据
客户端接收来自服务端的响应数据
断开套接字连接
客户端主动断开连接
客户端判断客户端断开连接后才断开连接
TCP连接总结
建立socket
通信
断开socket
六个方法
操作一: 服务器套接字绑定IP端口:bind
操作二: 服务器套接字监听:listen
操作三: 客户端套接字连接服务器:connect
操作四: 套接字发送消息:send
操作五: 套接字接受消息:recv
操作六: 套接字关闭连接:close
非阻塞套接字与IO多路复用
基本IO模型
套接字问题
问题一: 普通套接字实现的服务端有什么缺陷吗? (一次只能服务一个客户端 ! )
问题二: 这种缺陷是如何造成的?
accept阻塞: 当没有套接字连接请求过来的时候!会一直等待着
recv阻塞:没有接受到客户端发过来数据的时候!也会一直等待着
非阻塞套接字与非阻塞IO模型
非阻塞套接字问题
问题一: 非阻塞套接字是什么?
问题二:非阻塞套接字与普通套接字的区别在哪里?
try:
conn.setblocking(False) #对等套接字设置非阻塞套接字
except BlockingIOError:
pass
问题三:非阻塞套接字的使用有什么注意事项?
使用非阻塞套接字实现并发
非阻塞套接字问题
问题一: 什么叫并发?
问题二: 编程范式又是什么?
问题三: 使用非阻塞套接字最终实现了什么?
整体思路
宁可用 while True ,也不要阻塞 发呆!
将代码顺序重排,避开阻塞!
吃满 CPU !并发服务多个客户端 !
非阻塞套接字总结
非阻塞套接字
While True吃满CPU
重排代码顺序避开阻塞
IO多路复用
问题引入
问题一:非阻塞套接字服务器 还有什么不完美的嘛 ?
问题二: IO多路复用是什么?
问题三: epoll又是什么?
非阻塞不完美的地方
关键一: 任何操作都是需要花CPU资源的 !
关键二: 如果数据还没有到达,那么 accept、recv操作都是无效的CPU花费 !
关键三: 对应BlockingIOError的异常处理 也是无效的CPU花费 !
不完美的CPU利用率
IO多路复用 技术
把socket交给操作系统去监控
IO多路复用技术
epoll 是真正的答案 !
目前Linux上效率最高的 IO多路复用 技术 !
epoll
epoll 基于惰性的事件回调机制
惰性的事件回调是由用户自己调用的,操作系统只起到通知的作用
IO多路复用选择器
IO多路复用选择器是提供给我们调用epol的接口
使用步骤一:注册事件回调 epoll_selector.register(server,selectors.EVENT_READ,accept)
使用步骤二:事件回调 epoll_selector.unregister(connection)
IO多路复用总结
注册事件及回调
查询
回调
网络编程
并行与并发的深入理解
问题一: 计算机是如何执行程序指令的?
问题二: 计算机如何实现并发的?
问题三: 真正的并行需要依赖什么?
多进程并行问题
问题一: 什么是进程?
问题二: 如何在Python中使用进程?
问题三: 多进程实现并行的必要条件是什么?
进程
进程的概念
计算机程序是存储在磁盘上的文件。
只有把它们加载到内存中,并被操作系统调用 它们才会拥有其自己的生命周期。
进程表示一个正在执行的程序。
每个进程都有独立地址空间以及其他的辅助数据
进程(Process)
是计算机中已运行程序的实例
多进程并行的必要条件
总进程数量不多于 CPU核心数量!
如果不满足,那么运行的程序都是 轮询调度产生的假象。
在Python中使用进程
improt multiprocessing
import datetime
import time
print('mainpeocess start',datetime.datetime.now())
def func():
print('msubpeocess start',datetime.datetime.now())
time.sleep(5)
print('subpeocess end',datetime.datetime.now())
p = multiprocessing.Process(target=func)
p.start()
time.sleep(5)
print('mainpeocess end',datetime.datetime.now())
线程
多线程实现并发
多线程并发问题
问题一: 什么是线程?
问题二: 如何在Python中使用线程?
问题三: 为什么多线程不是并行?
线程的概念
线程被称作轻量级进程。
线程是进程中的一个实体,操作系统不会为进程分配内存空间,它只有一点在运行中必不可少的资源
线程被包含在进程中,是进程中的实际运作单位
同一个进程内的多个线程会共享相同的上下文,也就是共享资源(内存和数据)。
线程(thread)
是操作系统能够进行运算调度的最小单位
在Python中使用线程
improt threading
import datetime
import time
print('mainThread start',datetime.datetime.now())
def func():
print('subThread start',datetime.datetime.now())
time.sleep(5)
print('subThread end',datetime.datetime.now())
p = threading.Thread(target=func)
p.start()
time.sleep(5)
print('Thread end',datetime.datetime.now())
线程VS进程
稳定性
进程具有独立的地址空间,一个进程崩溃后,不会对其它进程产生影响。
线程共享地址空间,一个线程非法操作共享数据崩溃后,整个进程就崩溃了。
创建开销
创建进程操作系统是要分配内存空间和一些其他资源的。开销很大
创建线程操作系统不需要再单独分配资源,开销较小
切换开销
不同进程直接是独立的, 切换需要耗费较大资源
线程共享进程地址空间, 切换开销小
GIL锁(线程锁)
Python在设计的时候,还没有多核处理器的概念。
因此,为了设计方便与线程安全,直接设计了一个锁。
这个锁要求,任何进程中,一次只能有一个线程在执行。
因此,并不能为多个线程分配多个CPU。
所以Python中的线程只能实现并发,
而不能实现真正的并行。
但是Python3中的GIL锁有一个很棒的设计,
在遇到阻塞(不是耗时)的时候,会自动切换线程。
GIL锁(线程锁)
遇到阻塞就自动切换。我们可以利用这种机制来充分利用CPU
使用多进程与多线程来实现并发服务器
使用多进程与多线程实现并发服务器的关键点
关键点一: 多进程是并行执行, 相当于分别独立得处理各个请求。
关键点二: 多线程,虽然不能并行运行, 但是可以通过避开阻塞切换线程 来实现并发的效果,并且不浪费cpu
使用多线程实现并发服务器
import threading
import socket
server = socket.socket()
server.bin(('0.0.0.0',8000))
server.listen(1000)
def worker(connection):
while True:
recv_data = connection.recv(1024)
if recv_data:
print(recv_data)
else:
connection.close()
break
while True:
connection,remote_address = server.accept()
process = threading.Thread(target=worker,args=(conncetion,))
process.start()
进程与线程的标识
知识点一:进程id(process.pid) 与 线程ident(thread.ident)
知识点二:进程名(process.name) 与 线程名(thread.name)
知识点三:获取当前线程(thread.current_thread()) /进程信息(process.current_process())
进程与线程的其余相关操作
知识点一:等待结束 eg:process.start()
知识点三:进程与线程的生存状态 eg:process.is_aliver()
知识点二:终止进程 eg:process.terminate()
等待进程或线程结束
improt multiprocessing as mp
improt time
import datetime
def func():
time.sleep(5)
print(datetime.datetime.now())
process = mp.Process(target=func,name='进程名字')
process.start() # 开启进程
process.join() # 阻塞,等待子进程运行完后 再进行运行
time.sleep(5)
print()
print('main-process end')
守护模式
提示!
多线程中的守护线程与守护进程类似: process = mp.Process(target=func,daemon=True)
总结
进程/线程 一些标识
Join等待
守护模式daemon
以面向对象的形式使用进程与线程
面向对象使用线程/进程
步骤一: 继承 Process或Thread 类
步骤二: 重写 __init__方法
步骤三: 重写 run方法
面向对象化使用进程
import multiprocessing as mp
class My_Process(mp.Process):
def __init__(self,*args,**kwargs):
super().__init__()
self.args = args
self.kwargs = kwargs
def run(self):
print(mp.current_process())
print(self.args)
print(self.kwargs)
print(mp.current_process())
print(" --- "*10 )
p = My_Process(1,2,3,a=1,b=2,c=3)
p.start()
面向对象使用的思路
继承
调用父类初始化
重写run方法
独立的进程内存空间与共享的服务器进程空间
多进程之间的通信
问题一: 多个进程之间通信有什么限制吗?
问题二: 这种限制产生的原因?
问题三: Manger对象又是什么?
多进程之间通信的限制
进程之间是独立的,互不干扰的内存空间
进程间通信的解决方案
一般常用的空间类型是:
1. mgr.list()
2. mgr.dict()
3. mgr.Queue()
关于Queue()我们稍后讲解
from multiprocessing import Process,Manager
mgr = Manager()
list_proxy = mgr.list()
print(list_proxy)
def func(list):
list.append('a')
p = Process(target=func,args=(list_proxy,))
p.start()
p.join()
print(list_proxy)
多进程之间通信总结
独立空间变量不共享
需要借助第三方进程
Manager对象
线程间共享的全局变量与同步锁的基本概念
多线程之间的通信
问题一: 线程间也会出现进程间通信的问题嘛?
问题二: 如果不会,是否会产生一些问题?
问题三: 有没有解决这个问题的方法?
多线程之间通信的限制
improt threading as th
a = 1
def func():
global a
a = 2
thread = th.Thread(target =func )
thread.start()
thread.join()
print(a)
提示!
因为线程属于同一个进程,因此它们之间共享内存区域。
因此全局变量是公共的。
共享内存间存在竞争问题
提示!
如果1000000不能出现效果
可以继续在后面加0
使用锁来控制共享资源的访问
improt threading as th
lock = th.Lock()
def func():
lock.acquire() # 枷锁
...
lock.release() # 解锁
多线程之间通信总结
共享内存空间 变量共享
共享数据 需要加锁
竞争问题
线程与进程 安全的队列
队列的基本概念
一个入口,一个出口
先入先出(FIFO)
线程安全队列 操作一览
queue.Queue
入队: put(item)
出队: get()
测试空: empty() # 近似
测试满: full() # 近似
队列长度: qsize() # 近似
任务结束: task_done()
等待完成: join()
进程安全队列 操作一览
mgr.Queue
入队: put(item)
出队: get()
测试空: empty() # 近似
测试满: full() # 近似
队列长度: qsize() # 近似
生产者与消费者模型
生产-消费模型问题
问题一: 什么是生产者与消费者模型?
问题二: 为什么需要生产者与消费者模型?
问题三: 如何通过队列实现生产者与消费者模式?
生产者与消费者模型的概念
所谓,生产者与消费者模型,本质上是把进程通信的问题分开考虑
生产者,只需要往队列里面丢东西(生产者不需要关心消费者)
消费者,只需要从队列里面拿东西(消费者也不需要关心生产者)
消费者与生产者模式的应用
Web服务器与Web框架之间的关系
多线程的消费者与生产者模式
生产者:
只关心队列是否已满。
没满,则生产,满了就阻塞。
消费者:
只关心队列是否为空。
不为空,则消费,为空则阻塞。
安全队列, 生产者与消费者模型总结
不用考虑 内存和锁的问题
非常重要
通过队列实现
可重复利用的 线程
from threading import Thread
from queue import Queue
class MyThread(Thread):
def __init__():
self.queue = Queue()
self.daemon = True
self.start()
def run(self):
while True:
func,args,kwargs = self.queue.get()
func(*args,**kwargs)
def apply_async(self,func,*args,**kwargs):
self.queue.put((func,args,kwargs))
def join(self):
self.queue.join()
def func1():
time.sleep(2)
print(" ok1 )
def func2(*args,**kwargs):
time.sleep(2)
print(" ok2 )
thread = MyThread()
thread.apply_async(func1)
thread.apply_async(func2,1,2,c=3,d=4)
print("ok")
thread.join()
print("ok")
线程池的 简单实现
池的概念
主线程: 相当于生产者,只管向线程池提交任务。
并不关心线程池是如何执行任务的。 因此,并不关心是哪一个线程执行的这个任务。
线程池: 相当于消费者,负责接收任务, 并将任务分配到一个空闲的线程中去执行。
线程池的简单实现
from threading import Thread
from queue import Queue
import time
class ThreaPool:
def __init__(self,n):
self.queue = Queue()
for i in range(n):
Thread(target=self.worker,daemon=True).start()
def worker(self):
while Ture:
func,args,kwargs = self.queue.get()
func(*args,**kwargs)
self.queue.task_done()
def apply_async(self,func,*args,**kwargs)
self.queue.put((func,args,kwargs))
def join():
self.queue.join()
def func1():
time.sleep(2)
print(" ok1 )
def func2(*args,**kwargs):
time.sleep(2)
print(" ok2 )
pool = ThreadPool(2)
pool.apply_async(func1)
pool.apply_async(func2,1,2,c=3,d=4)
print("提交 ok")
pool.join()
print("完成 ok")
Python自带池
池的操作
操作一: apply_async – 向池中提交任务
操作二: close - 关闭提交通道,不允许再提交任务
操作三: terminate - 中止进程池,中止所有任务
内置线程池
from multiprocessing.pool import ThreadPool
import time
def func1():
time.sleep(2)
print(" ok1 )
def func2(*args,**kwargs):
time.sleep(2)
print(" ok2 )
pool = ThreadPool(2)
pool.apply_async(func1)
pool.apply_async(func1)
pool.apply_async(func2,1,2,c=3,d=4)
print("提交 ok")
pool.close() # 在join之前必须关闭提交 close,就不允许在提交任务了
pool.join() # 等待完成,继续下一个运行;
print("完成 ok")
内置的进程池
from multiprocessing.pool import Pool
import time
def func1():
time.sleep(2)
print(" ok1 )
def func2(*args,**kwargs):
time.sleep(2)
print(" ok2 )
if __name__ = "__main__":
pool = Pool(2)
pool.apply_async(func1)
pool.apply_async(func1)
pool.apply_async(func2,1,2,c=3,d=4)
print("提交 ok")
pool.close() # 在join之前必须关闭提交 close,就不允许在提交任务了
pool.join() # 等待完成,继续下一个运行;
print("完成 ok")
使用池来实现并发服务器
使用线程池来实现并发服务器
from multiprocessing.pool import ThreadPool
import socket
server = socket.socket()
server.bin(('127.0.0.1',8000))
server.listen(100)
def woker(connection):
while True:
recv_data = connection.recv(1024)
if recv_data:
print(recv_data)
connection.send(recv_data)
else:
connection.close()
break
if __name__ = "__main__":
pool = ThreadPool(2)
while True:
connection,address = server.accept()
pool.apply_async(worker,args=(connection,))
使用进程池+线程池来实现并发服务器
from multiprocessing.pool import Pool
from Pool import cup_count
import socket
server = socket.socket()
server.bin(('127.0.0.1',8000))
server.listen(100)
def woker_thread(connection):
while True:
recv_data = connection.recv(1024)
if recv_data:
print(recv_data)
connection.send(recv_data)
else:
connection.close()
break
def worker_process(server):
htread_pool = ThreadPool(cpu_count()*2)
while True:
connction,remote_address = server.accept()
thread_pool.async(worker_thread,args=(connection,))
if __name__ = "__main__":
n = cup_count()
process_pool = Pool(2)
for i in range(n):
process_pool.apply_async(worker_process,args=(server,))
process_pool.close()
process_pool.join()
聊天室
服务端
from multiprocessing import Pool, cpu_count,Manager
from multiprocessing.pool import ThreadPool
import socket
from datetime import datetime
#从队列中拿出数据,发给所有连接上的客户端
def send_data(dict_proxy, queue_proxy):
while True:
data = queue_proxy.get()
print(data.decode())
for conn in dict_proxy.values():
conn.send(data)
def worker_thread(connection, addr, dict_proxy, queue_proxy):
while True:
try:
recv_data = connection.recv(1024)
if recv_data:
time = datetime.now().strftime("%Y/%m/%d %H:%M:%S")
data = "{addr} {time} \n \t{data}".format(addr=addr, time = time, data=recv_data.decode())
queue_proxy.put(data.encode()) #把消息添加到到队列中
else:
raise Exception
except:
dict_proxy.pop(addr) #从字典中删掉退出的客户端
data = '{}退出'.format(addr)
queue_proxy.put(data.encode()) #把退出消息添加到队列中
connection.close()
break
def login(username,conncetion, thread_pool, dict_proxy, queue_proxy ):
dict_proxy.setdefault(username, conncetion) # 把套接字加入字典中
conncetion.send("恭喜你,登陆成功".encode())
data = '{}登录'.format(username)
queue_proxy.put(data.encode()) # 将用户登录消息添加到队列中
thread_pool.apply_async(worker_thread, args=(conncetion, username, dict_proxy, queue_proxy))
def login_try(conncetion,thread_pool, dict_proxy,queue_proxy, data):
conncetion.send(data)
username = conncetion.recv(1024).decode()
if username not in dict_proxy:
login(username, conncetion, thread_pool, dict_proxy, queue_proxy)
else:
data = "用户名已被使用,请重新输入!".encode()
login_try(conncetion, thread_pool, dict_proxy, queue_proxy, data)
def worker_process(server, dict_proxy, queue_proxy):
thread_pool = ThreadPool( cpu_count()*2 )
while True:
conncetion, remote_address = server.accept()
data = "请输入用户名!".encode()
login_try(conncetion, thread_pool, dict_proxy, queue_proxy, data)
if __name__ == '__main__':
server = socket.socket()
server.bind(('127.0.0.1', 8888))
server.listen(1000)
mgr = Manager()
dict_proxy = mgr.dict() #用来保存连接上来的客户端,
queue_proxy = mgr.Queue() #把客户端发过来的消息通过队列传递
n = cpu_count() #打印当前电脑的cpu核数
process_pool = Pool(n)
for i in range(n-1): #充分利用CPU,为每一个CPU分配一个进程
process_pool.apply_async(worker_process, args=(server, dict_proxy, queue_proxy)) #把server丢到两个进程里面
process_pool.apply_async(send_data, args=(dict_proxy, queue_proxy)) #用一个进程去收发消息
process_pool.close()
process_pool.join()
客户端
import socket
import threading
client = socket.socket()
client.connect(('127.0.0.1', 8888))
def recv_data():
while True:
data = client.recv(1024)
print(data.decode())
username = input("输入你的用户名:")
client.send(username.encode())
thread = threading.Thread(target=recv_data, daemon=True)
thread.start()
while True:
a = input('')
client.send(a.encode())
双人聊天
服务器端
#coding:utf-8
#导入相关包
import socket
import sys
import time
ISOTIMEFORMAT='%Y-%m-%d %X' #时间格式
host='' #本机ip
port=8888 #端口号
#创建流式套接字
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
#设置端口复用
s.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
#绑定(ip,port)
s.bind((host,port))
#监听套接字
s.listen(5)
print 'runing...'
#接受客户端连接
ClientSock,ClientAddr=s.accept()
print '%s connected.'%(str(ClientAddr))
while 1:
try:
#接收消息
buf=ClientSock.recv(1024)
if len(buf): #消息长度大于0则输出
print "he say: "+buf
data=raw_input("I say: ") #等待用户控制台输入
#格式化当前时间
send_time=time.strftime(ISOTIMEFORMAT,time.localtime())
#发送消息
ClientSock.sendall(data+'[%s]'%(send_time))
except:
print "Dialogue Over"
ClientSock.close() #关闭套接字
sys.exit(0) #退出程序
客户端
#coding:utf-8
#导入相关包
import socket
import sys
import time
ISOTIMEFORMAT='%Y-%m-%d %X' #时间格式
host='127.0.0.1' #本机地址
port=8888
#创建流式套接字
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
try:
#连接套接字
s.connect((host,port))
except socket.gaierror,e:
print "Address-related error connecting to server:%s"%e
# 1表示非正常退出程序,退出程序是告诉解释器(os)的
sys.exit(1)
except socket.error,e:
print "Connection error:%s"%e
sys.exit(1)
while 1:
try:
#等待用户控制台输入
data=raw_input("I say: ")
#发送消息的时间
send_time=time.strftime(ISOTIMEFORMAT,time.localtime())
#向服务器发送消息
s.send(data+'[%s]'%(send_time))
#从服务器接收消息
buf=s.recv(1024)
if len(buf): #消息长度大于0则输出
print "he say: "+buf
except: #异常处理
print "Dialogue Over"
s.close() #关闭套接字
sys.exit(0) #0表示正常退出程序
协程
协程问题
问题一:生成器是什么?
问题二:生成器与普通函数的区别?
问题三:协程是什么?
生成器语法
yield 一个对象:
返回这个对象
暂停这个函数
等待下次next重新激活
send与yield的切换
send 一个对象:
激活生成器
执行生成器里面的代码
遇到yield回到调用位置
注意事项
对一个生成器必须要先next()让他执行到yield才能在send数据进去。
携程是在一个线程内的执行的,本质来说就是不同函数之间的切换调用。
如果某一个协程被阻塞了,整个线程(进程)都被阻塞。任意时刻,只有一个协程在执行。
greenlet
greenlet问题
问题一: 什么是greenlet ?
问题二: 如何使用 greenlet ?
问题三: 为什么需要greenlet ?
什么是 greenlet ?
pip install greenlet
虽然CPython(标准Python)能够通过生成器来实现协程, 但使用起来还并不是很方便。
Python的一个衍生版 Stackless Python 实现了原生的协程,它更利于使用
于是,大家开始将 Stackless 中关于协程的代码 单独拿出来做成了CPython的扩展包。
这就是 greenlet 的由来,greenlet 是底层实现了原生协程的 C扩展库。
greenlet 的基本使用
form greenlet import greenlet
def fn1():
pass
def fn2():
pass
c = greenlet(fn1)
p = greenlet(fn2)
c.switch()
greenlet 的价值
价值一: 高性能的原生协程
价值二: 语义更加明确的显式切换
价值三: 直接将函数包装成协程,保持原有代码风格
gevent协程
gevent问题
问题一: 什么是 gevent ?
问题二:如何使用 gevent ?
问题三: gevent 的价值是什么 ?
什么是 gevent?
pip install gevent
虽然,我们有了 基于 epoll 的回调式编程模式,但是却难以使用。
即使我们可以通过配合 生成器协程 进行复杂的封装,以简化编程难度。
但是仍然有一个大的问题: 封装难度大,现有代码几乎完全要重写
gevent,通过封装了 libev(基于epoll) 和 greenlet 两个库。
帮我们做好封装,允许我们以类似于线程的方式使用协程。
以至于我们几乎不用重写原来的代码就能充分利用 epoll 和 协程 威力。
gevent 的价值
遇到阻塞就切换到 另一个协程继续执行 !
价值一: 使用基于 epoll 的 libev 来避开阻塞
价值二: 使用基于 gevent 的 高效协程 来切换执行
价值三: 只在遇到阻塞的时候切换, 没有轮需的开销,也没有线程的开销
gevent 协程通信
gevent通信 问题
问题一:协程之间不是能通过switch通信嘛?
是的,由于 gevent 基于 greenlet,所以可以。
问题二:那为什么还要考虑通信问题?
因为 gevent 不需要我们使用手动切换, 而是遇到阻塞就切换,因此我们不会去使用switch !
gevent
协程负责 主动切换
IO多路复用 负责避开阻塞
gevent
gevent.queue.Queue
import gevent
from gevent.queue import Queue
queue = Queue(3)
def fn1():
while True:
item = random.randint(0,99)
print("生产: {}".format(item))
queue.put(item)
def fn2():
while True:
item = queue.get()
print("消费: {}".format(item))
p = gevent.spawn(fn1,queue)
c = gevent.spawn(fn2,queue)
gevent.joinall([p,c])
什么是协程
协程,又称微线程,纤程,英文名Coroutine。协程的作用,是在执行函数A时,可以随时中断,去执行函数B,然后中断继续执行函数A(可以自由切换)。但这一过程并不是函数调用(没有调用语句),这一整个过程看似像多线程,然而协程只有一个线程执行。
- 携程
def func():
i=0
while True:
x = yield i
i += 1
print('func:', x)
a = func()
print(next(a))
a.send('hello')
a.send('nihao')
send() 可以恢复执行,并把传进去的东西当作yield的返回值
对一个生成器必须要先next()让他执行到yield才能在send数据进去。
函数里面加yield 就把一个函数变成了生成器,
生成器遇到yield暂停运行,并把后面的数据返回
通过yield实现了函数暂停运行的效果
然后想要再次运行这个函数,通过next去激活这个生成器,运行生成器函数。
如果把调用next这个过程封装成函数,通过通过yield和next实现了两个函数切换运行的功能
生产者和消费者模型
import random
import time
def producer(consumer): #生产者
next(consumer)
while True:
item = random.randint(0, 99) #随机生产一个数
print('生产者:生产了%s'%(item))
consumer.send(item) #把item给消费者
time.sleep(2) #每生产一个就休息一些,方便我们看效果
def consumer(): #消费者
#消费者里面的东西是从生产者那里来的, 用yield来接收
while True:
item = yield
print('消费者: 拿到%s' % item)
c = consumer() #生成一个消费者
producer(c)
2. greenlet协程
yield能实现协程,不过实现过程不易于理解,greenlet是在这方面做了改进
from greenlet import greenlet
import random
import time
def producer():
while True:
item = random.randint(0, 99)
print('生产者: 生产了%s'%item)
c.switch(item) #将item传给消费者, 并切换到消费者,
time.sleep(2)
def consumer():
while True:
item = p.switch() #切换到生产者,并等待生产者传入item
print('消费者: 拿到了%s'%item)
c = greenlet(consumer) #将一个普通的函数变成携程
p = greenlet(producer)
c.switch() #运行消费者,
3. gevent协程
import gevent
from gevent import monkey
monkey.patch_socket()
import socket
server = socket.socket()
server.bind(('127.0.0.1', 8080))
server.listen(100)
def worker(connection):
while True:
try:
recv_data = connection.recv(1024)
if recv_data:
print(recv_data)
connection.send(recv_data)
else:
raise Exception
except:
connection.close()
break
while True:
connection, remote_address = server.accept()
gevent.spawn(worker, connection) #这就变成了一个携程版本
猴子补丁,它把python自带的socket替换掉,实现了封装epoll的socket
这个socket遇到阻塞,就自动切换携程
只要你打了这个补丁, 以后你使用的socket只要遇到了阻塞就会切换携程
协程 (Coroutine)
又称微线程,纤程,协程是一种用户态的轻量级线程。
我们可以使用协程来实现异步操作,比如在网络爬虫场景下,我们发出一个请求之后,需要等待一定的时间才能得到响应,但其实在这个等待过程中,程序可以干许多其他的事情,等到响应得到之后才切换回来继续处理,这样可以充分利用 CPU 和其他资源,这就是异步协程的优势。
Python 中使用协程最常用的库莫过于 asyncio,所以本文会以 asyncio 为基础来介绍协程的使用。
首先我们需要了解下面几个概念:
- event_loop:事件循环,相当于一个无限循环,我们可以把一些函数注册到这个事件循环上,当满足条件发生的时候,就会调用对应的协程函数
- coroutine:协程对象,指一个使用async关键字定义的函数,它的调用不会立即执行函数,而是会返回一个协程对象。协程对象需要注册到事件循环,由事件循环调用。
- task(任务对象):一个协程对象就是一个原生可以挂起的函数,任务则是对协程进一步封装,其中包含任务的各种状态。
- future(未来对象):代表将来执行或没有执行的任务的结果,实际上和 task 没有本质区别。不用回调的方式,我们怎么知道异步调用的结果?先设计一个对象,异步调用执行完的时候,就把结果放在它里面。这种对象称之为未来对象。
另外我们还需要了解 async/await 关键字,它是从 Python 3.5 才出现的,专门用于定义协程。其中,async 定义一个协程,await 用来挂起阻塞方法的执行。
定义协程
import asyncio
async def do_some_work(x):
print('Waiting: ', x)
return 'x is %d'%(x)
coroutine = do_some_work(2) #返回一个coroutine协程对象
loop = asyncio.get_event_loop() #创建一个事件循环 loop
loop.run_until_complete(coroutine) #将协程注册到事件循环 loop 中,然后启动
创建多个task
import asyncio
async def func(x):
print('waiting: ', x)
await asyncio.sleep(x) #模拟出IO阻塞
return 'x is {}'.format(x)
task_1 = asyncio.ensure_future(func(1))
task_2 = asyncio.ensure_future(func(1))
task_3 = asyncio.ensure_future(func(1))
task_4 = asyncio.ensure_future(func(1))
loop = asyncio.get_event_loop() #创建一个事件循环 loop
tasks = [task_1, task_2, task_3, task_4]
#tasks = [asyncio.ensure_future(func(i) for i in rage(5)] #创建五个
loop.run_until_complete(asyncio.wait(tasks))
#将任务对象加到事件循环 loop 中,然后启动
for task in tasks:
print(task.result())