Python学习笔记
目录
文章目录
- Python学习笔记
- 目录
- (一)、环境搭建
- (二)、变量和数据类型
- (三)、运算符
- (四)、数据容器
- (五)、python语法基础
- (六)、输入和输出介绍(input和print)
- (七)、Python3条件控制语句
- (八)、Python循环控制语句
- (九)、函数
- (十)、python模块和包的应用
- (十一)、程序异常捕获处理
- (十二)、面向对象编程
- (十三)、闭包和装饰器
- (十四)、多任务编程
- (十五)、语法糖
- (十六)、日期和时间
- (十七)内置函数
- (十八)、文件操作
- (十九)、Json格式解析
- (二十)、python数据库的操作
python是一个解释性语言也就是说脚本语言和我们在windows系统中使用dos编写批处理文件一样,编写完直接执行。脚本语言的特点如下:
- 代码无需编译成机器码,而是由解释器逐行执行。
- 便于快捷开发和调试,因为无需等待编译过程。
常用的脚本语言有:PHP、JavaScript、Perl、Ruby、R语言等。
(一)、环境搭建
1、开发环境选择
1.1 常用开发环境(IDE)
记事本、终端、pytharm、vscode等。
1.2 本篇笔记使用的IDE说明
本笔记主要使用终端和vscode模式环境写第一个python语句:
1、终端(每输入一条回车)
dave.ruan@DavesMBP ~ % python3 # 进入python开发环境
Python 3.12.0 (v3.12.0:0fb18b02c8, Oct 2 2023, 09:45:56) [Clang 13.0.0 (clang-1300.0.29.30)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>>print("Hello World!") # 每输入一条回车,解释器就直接执行了
Hello World! # 这就是我们第一条输出结果hello world!
>>> print("hello\
... world!") # 解释器判断上面一条未输入完成,让你继续输入所以使用……。
hello world!
>>>name='Dave'
>>>name # 在终端编辑器中可以这样写代表print(name)
dave
2、IDE模式
print("Hello World!") # 每输入一条回车,解释器就直接执行了
输出结果:
Hello World!
这种方式是写完所有代码解析器逐行进行解析,ide编辑器是需要使用print输出的,如果直接写个变量的话会报错。
现在对这些代码不明没关系,先了解下,今后的例子将采用这两种方式进行。
(二)、变量和数据类型
1、变量
变量是变成语言中用来存和表示数据的一种抽象的概念,它们用于给数据或数值起一个具有意义的名字,一边在程序中引用和操作。
变量,顾名思义就是可以发生变化的量。它相当于是一个容器,可以存放不同的内容;比如”张三笔盒“可以理解为是变量,张三笔盒是变量名,笔盒里面有什么东西、数量是多少就是变量值。
数组(容器)、类、方法(函数)等的名字都可以称为变量。
1.1 python变量的命名规则
在Python中,变量的命名有一些基本的规则需要遵守,以下是一些常见的命名规范:
- 变量名通常由字母、数字和下划线组成,但必须以字母或下划线开头。同时,变量名通常区分大小写。
- 变量名通常小写,如果由多个单词组成,使用下划线(_)连接,例如:my_variable。
- 类名使用驼峰体(CamelCase),每个单词的首字母大写,例如:MyClass。
- 常量命名时所有字母大写,由下划线连接多个单词,例如:MY_CONSTANT。
- 避免使用python关键字作为变量。
注意:
- 2、3、4点只规范要求系统不强制约束,1、2点系统强制要求,否则系统会抛出异常。
- 常量是一种特殊的变量,其他语言中常量有特殊的定义方式且定义完后是不可以修改了,而python对常量没有没有特殊的定义方式,看做一个特殊的变量。
1.2 变量的初始赋值(声明)
由于python的变量内存存储机制的不同,所以python有别其他语言在使用变量时需要声明指定变量的数据类型,python只定义个变量给个初始值即可或者在需要使用到变量存储数据时直接赋值即可。
以下是python变量的赋值语法:
a=100 # 单一变量赋值
b=c=100 # 多个变量赋值同一个值
d=1;e=2 # 多个变量赋值不同的值
str="hello world" # 字符串变量赋值
str1='hello world'# 单引号和双引号都可以定义字符串。
以上这几种变量的定义都可以。
1.3 删除变量
变量通常系统会有垃圾回收机制回收,如果需要手动删除的话可以使用del关键字进行删除即可,被删除的变量将被从内存堆栈清除,将在后续的代码中无法被引用,否则系统将抛出异常。
del a # 这就删除了变量,后面就不能被引用了。
1.4 变量的内存存储方式(延伸)
1.4.1 变量内存存储
为什么要了解变量的内存存储方式,变量在传递中值的变化和存储机制有着至关重要的作用。
要了解变量在内存的存储方式,需要了解内存的两个主要区域堆和栈,堆与栈表示两种内存管理方式,简单的了解两种方式。
栈由操作系统自动分配释放 ,用于存放函数的参数值、局部变量等,逻辑或函数执行完系统自动释放;栈是先进后出的机制;栈相对与堆来说空间是固定的,较小,但读取快。
堆由开发人员分配和释放, 若开发人员不释放,程序结束时由 OS 回收,用于存放被引用的对象;空间较大,读取相对较慢。
查看一下代码(ID函数是获取内存地址的内置函数):
>>> s1=100 # 变量初始赋值
>>> s2=100
>>> id(s1) # 获取变量s1对应地址
4312542016
>>> id(s2) # 获取变量s2对应地址
4312542016
>>> s2=200 # 修改变量s2的值
>>> id(s2)
4312545216
从上面的例子看,两个变量都给于赋值100时对应的地址都是一样的,s2修改后地址也随之改变,这个和其他语言有的本质的区别,我们来看看下图python的变量存储方式(该图例是局部变量存储方式,全局变量机制一样只是变量名和映射地址不是存在栈中而是全局命名空间中,具体1.4.2会介绍):
从上图可以更好的理解python变量的值100和200都是int对象被放入堆内存中,而变量名和对应的堆中值的地址被存于栈中==(变量名和应用关系存于栈中,值存于堆中)==,所以s1和s2同时赋值100变量是同时指向100的内存地址,而s2被修改成200后,从新指向了200的地址。
而其他语言普通变量是值引用,也就是说直接在栈中存储值所以指向的是自己本身的地址也就是说变量如何改变地址都是声明时的地址;只有对象类(数组、字符串、实例等)才会向python一样在堆中存储对象然后引用。
说到数组,python有类似数据容器叫序列(后面学习)python的存储方式又如何,请看代码:
list1=[11,12,32,45]
print("打印列表中的元素所指向地址:")
[print(id(x)) for x in list1]
print("打印列表list1所指向的地址:",id(list1))
print("修改第三个元素值:")
list1[2]=734
print("修改后打印列表中的元素所指向地址:")
[print(id(x)) for x in list1]
print("修改后打印列表list1所指向的地址:",id(list1))
输出结果:
打印列表中的元素所指向地址:
4465913888
4465913920
4465914560
4465914976
打印列表list1所指向的地址: 4469082368
修改第三个元素值:
修改后打印列表中的元素所指向地址:
4465913888
4465913920
4468485104 # 值被修改引用地址发生改变
4465914976
修改后打印列表list1所指向的地址: 4469082368
从上面例子结果看序列变量指向一个地址,序列中的各个元素也同时指向对应的地址,元素值被改变的同时地址指向的值会发生改变而序列变量的地址指向不会发生改变,看下面图示就知道内存的存储方式为何:
从上图例可以理解如下:
- 在初始化
list1=[11,12,32,45]
list1列表时,首先在栈中存储list1列表变量,在堆中存入一个列表对象该和各个元素对象的值,列表对象的地址是4469082368,该地址和列表名”list1“存于栈中,而堆中的列表对象中存储了元素棕个数(计数器)和各个元素的地址指针。 - 要查找list1[1]的值是多少?先在栈中找到名为list1的空间中的地址4469082368,通过这个地址找到列表对象,通过列表对象中的元素地址指针找到第二个指针是4465913920地址里面存放着int对象的值是12,所以就找到了list1[1]是12。
- 第三个元素的值被修改成734(
list1[2]=734
),734是int类型,值734作为int对象存于堆中地址为:4468485104,这不操作代码执行时会通过list1变量–>list对象地址4469082368–>在list对象中用4468485104地址指针替换了4465914560地址,所以32就变为了734,所以列表就是[11,12,734,45]。
1.4.2 变量存储的扩展
1.4.1章节所说的存储方式其实是以局部变量的存储方式来解释说明,python对变量的管理涉及到堆、栈、全局命名空间、类命名空间、局部命名空间等等相互配合来实现管理的。
总结来说:变量名和地址映射存储于命名空间中(根据全局、类、局部存于不同的地方),而变量的值作为一个数据对象存储于堆中,每次变量名和地址映射的改变就意味着可能值的变化。
全局命名空间:
- 功能:用于存储全局变量、类名、函数名及他们在堆中的值对象地址(对象存储地址)。
- 存储位置:堆中的模块对象中(全局变量存在于模块中,而模块对象存于堆中)。
- 生命周期:贯穿程序始终。
类命名空间:
- 功能:用于存储类变量名、方法名及它们在堆中的值对象地址(对象存储地址)。
- 存储位置:堆中的类对象中
- 生命周期:随这类引用结束而销毁。
局部命名空间(是栈帧的组成部分):
- 功能:用于存储函数或类函数中变量(局部变量)名、函数参数等
- 存储位置:函数被调用就会在栈中建立一个栈帧,而栈帧中有个空间就是局部命名空间,用于存储变量名和值对象在堆中的地址。
- 生命周期:函数调用结束而销毁
堆和栈:
- 功能:栈是函数运行时存储一系列局部变量等信息,堆中存储的是各个变量(包含类、模块、函数、类函数)的值作为一个对象存储,比如x=10,10存储于堆中的整数对象中,列表就是存储于列表对象中(列表对象中包含元素各个值的对象地址)
程序如何寻找全局变量的值:
- 全局变量:从堆中模块对象中找到变量名对应的地址,通过地址指向堆中对象地址中的值就是变量值(复合变量则是元素的地址指向,如果要元素的值根据地址指向就可以找到)。
- 局部变量:函数被调用后栈中建立了一个栈帧,存有函数内的变量名、参数地址、返回地址等,从栈帧中的局部命名空间中找到变量名指向的地址,找到堆中地址就是其变量的对象,值就找到了。
2、数据类型
Python 3 中有六个标准的数据类型:
- Int(整型):表示整数,例如:x = 2
- String(字符串):可以使用单引号或双引号表示,例如:text = “Welcome, Python!”
- float(浮点数):示带有小数点的数值,例如:y = 1.23
- bool(布尔型):表示真(True)或假(False)的值,例如:is_true = True,注意首字符大写
- List(列表):复合型数据类型,有序可变容的序列
- Tuple(元组):复合型数据类型,有序不可变的序列
- Sets(集合):复合型数据类型,无序可变,不可重复的序列
- Dictionaries(字典):复合型数据类型,无序的键值对集合
- 等等……
常用的数据类型都在上面,后面四个属于容器,它是python经常用到的,在后面容器的章节记录各个容器的使用方法。
2.1 数据类型的转换
在日常使用中经常会遇到不同数据类型键的转换,比如‘1’转成1等。
- int(x):表示把x转成int类型
- float(x):把x转成float类型
- str(x):把x转成字符串类型
- Bool(int):布尔类型Ture=1,False=0,所以布尔类型的转换只能是整型。
- List()、Tuple、Sets、Dictionaries都是采取这种方式进行转换。
注意点:整型和浮点型计算的时候,计算过程中会把整型数据转换成浮点型进行计算,但不改变变量中数据的类型
2.2 关键函数的使用
type(arg):返回数据或变量内数据的数据类型,type(123),type(a) a为变量。
isinstance(arg,datatype):校验数据是否是否个类型,是返回True,否则False。用法:isinstance(123,int)校验123是不是int类型。
(三)、运算符
Python中提供了各种各样的运算符帮助我们解决各种实际问题。Python中的运算符主要包括算术运算符、比较运算符、位运算符、逻辑运算符和赋值运算符、成员运算符。下面将一一介绍这些运算符的具体种类和使用方法。
1、算术运算符
以下假设变量 a 为 21,变量 b 为 10:
算符 | 描述 | 实例 |
---|---|---|
+ | 加 - 两个对象相加 | a + b 输出结果 31 |
- | 减 - 得到负数或是一个数减去另一个数 | a - b 输出结果 11 |
* | 乘 - 两个数相乘或是返回一个被重复若干次的字符串 | a * b 输出结果 210 |
/ | 除 - x 除以 y | a / b 输出结果 2.1 |
% | 取模 - 返回除法的余数 | a % b 输出结果 1 |
** | 幂 - 返回 x 的 y 次幂 | a ** b 为 21 的 10 次方 |
// | 取整除 - 返回商的整数部分 | 9//2 输出结果 4 , 9.0//2.0 输出结果 4.0 |
2、比较运算符
以下假设变量 a 为 10,变量 b 为 20:
运算符 | 描述 | 实例 |
---|---|---|
== | 等于 – 比较对象是否相等 | (a == b) 返回 False。 |
!= | 不等于 – 比较两个对象是否不相等 | (a != b) 返回 True. |
> | 大于 – 返回 x 是否大于 y | (a > b) 返回 False。 |
< | 小于 – 返回 x 是否小于 y。 | (a < b) 返回 True。 |
>= | 大于等于 – 返回 x 是否大于等于 y。 | (a >= b) 返回 False。 |
<= | 小于等于 – 返回 x 是否小于等于 y。 | (a <= b) 返回 True。 |
所有比较运算符返回 1 表示真,返回 0 表示假。这分别与特殊的变量 True 和 False 等价。
3、赋值运算符
以下假设变量 a 为 10,变量 b 为 20:
运算符 | 描述 | 实例 |
---|---|---|
= | 简单的赋值运算符 | c = a + b 将 a + b 的运算结果赋值给 c |
+= | 加法赋值运算符 | c += a 等效于 c = c + a |
-= | 减法赋值运算符 | c -= a 等效于 c = c - a |
*= | 乘法赋值运算符 | c *= a 等效于 c = c * a |
/= | 除法赋值运算符 | c /= a 等效于 c = c / a |
%= | 取模赋值运算符 | c %= a 等效于 c = c % a |
**= | 幂赋值运算符 | c **= a 等效于 c = c ** a |
//= | 取整除赋值运算符 | c //= a 等效于 c = c // a |
4、位运算符
按位运算符是把数字看作二进制来进行计算的。Python 中的按位运算法则如下:
例子如下表中变量 a 为 60,b 为 13。
按位与运算(a&b) | 按位或运算(a|b) | 按位异或(a^b) | |
---|---|---|---|
a(60)的二进制表示 | 0011 1100 | 0011 1100 | 0011 1100 |
b(13)的二进制表示 | 0000 1101 | 0000 1101 | 0000 1101 |
运算结果 | 0000 1100 | 0011 1101 | 0011 0001 |
结果的十进制表示 | 12 | 61 | 49 |
按位取反(~a) | 左移(a<<2) | 右移(a>>2) | |
---|---|---|---|
a(60)的二进制表示 | 0011 1100 | 0011 1100 | 0011 1100 |
运算结果 | 1100 0011 | 1111 0000 | 0000 1111 |
运算结果的十进制表示 | -61 | 240 | 15 |
注:关于原码,补码和反码:
原码:假设机器字长为n,原码就是用一个n位的二进制数,其中最高位为符号位:正数是0,负数是1。剩下的表示概数的绝对值,位数如果不够就用0补全。
反码:在原码的基础上,符号位不变其他位取反,也就是就是0变1,1变0。
补码:在反码的基础上加1。
PS:正数的原、反、补码都一样,0的原码跟反码都有两个,因为这里0被分为+0和-0。
按位取反和反码有一定的相似之处但又不尽相同(反码符号位不取反)。
在计算机中,是以补码的形式存放数据的。1100 0011刚好对应-61。
-61的原码-> 1011 1101->反码->1100 0010->补码->1100 0011
运算符 | 描述 | 实例 |
---|---|---|
& | 按位与运算符:参与运算的两个值,如果两个相应位都为 1,则该位的结果为 1,否则为 0 | (a & b) 输出结果 12 ,二进制解释: 0000 1100 |
| | 按位或运算符:只要对应的二个二进位有一个为 1 时,结果位就为 1。 | (a | b) 输出结果 61 ,二进制解释: 0011 1101 |
^ | 按位异或运算符:当两对应的二进位相异(不同)时,结果为 1 | (a ^ b) 输出结果 49 ,二进制解释: 0011 0001 |
~ | 按位取反运算符:对数据的每个二进制位取反,即把 1 变为 0,把 0 变为 1 | (~a ) 输出结果 -61 ,二进制解释: 1100 0011 |
<< | 左移动运算符:运算数的各二进位全部左移若干位,由"<<"右边的数指定移动的位数,高位丢弃,低位补 0。 | a << 2 输出结果 240 ,二进制解释: 1111 0000 |
>> | 右移动运算符:把">>“左边的运算数的各二进位全部右移若干位,”>>"右边的数指定移动的位数 | a >> 2 输出结果 15 ,二进制解释: 0000 1111 |
5、逻辑运算符
Python 语言支持逻辑运算符,以下假设变量 a 为 10, b 为 20:
运算符 | 逻辑表达式 | 描述 | 实例 |
---|---|---|---|
and | x and y | 布尔"与" - 如果 x 为 False,x and y 返回 False,否则它返回 y 的计算值。 | (a and b) 返回 20。 |
or | x or y | 布尔"或" - 如果 x 是 True,它返回 x的值,否则它返回 y 的计算值。 | (a or b) 返回 10。 |
not | not x | 布尔"非" - 如果 x 为 True,返回 False 。如果 x 为 False,它返回 True。 | not(a and b) 返回 False |
6、成员运算符
除了以上的一些运算符之外,Python 还支持成员运算符,测试实例中包含了一系列的成员,包括字符串,列表或元组。
运算符 | 描述 | 实例 |
---|---|---|
in | 如果在指定的序列中找到值返回 True,否则返回 False。 | x 在 y 序列中 , 如果 x 在 y 序列中返回 True。 |
not in | 如果在指定的序列中没有找到值返回 True,否则返回 False。 | x 不在 y 序列中 , 如果 x 不在 y 序列中返回 True。 |
(四)、数据容器
Python中常用的容器有序列和集合、字典,它们都是数据容器中的一种基本数据结构,而序列指的是内容连续、有序,并且可以使用下标索引的一类数据容器如:列表、元祖、字符串。
在Python开发中序列、集合、字典是经常反反复复被使用到,这些数据容器应用灵活多变。在其他语言中数组、字典、哈希表等无不有着相同的定义。
1、序列1-列表(List)
1.1 列表定义
列表是Python中内置有序、可变序列,它以一个方括号“[]”内包含多个其他数据项(字符串,数字等甚至是另一个列表),数据项间以逗号作为分隔的数据类型。
1.2 列表初始化语法
# 正常列表定义初始化
list1 = ['Google', 'Baidu', 1997, 2000]
list1 = [ # 这种多行的每个元素一行的也是可以的,如果嵌套的话会比较看。
'Google',
'Baidu',
1997,
2000
]
# 嵌套列表
list2=[1,2,['a','b','c']]
# 以下定义空列表的两种方法
mylist=[]
mylist=list()
1.3 列表推导式
Python中的列表推导式是一种方便快捷的语法,可以快速地生成列表。
列表推导式的基本语法结构为:
[ expression for item in iterable if condition ]
例子:
urls=[a for a in range(1,11) if a%2==0]
print(urls)
urls=[a for a in range(1,11)]
print(urls)
输入结果:
[2, 4, 6, 8, 10]
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
1.4 列表下标
1.4.1 下标的理解
下标在python的序列中可以看做为索引,可以高效定位序列中的元素,随时随地找到元素值或通过元素值找到下标。下标在序列中的使用无时无刻不在,下标在序列中起到至关重要的作用,要学习序列就必须要了解下标。
下标分为正反向下标其值也按正反向有序递增或递减(序列切片中会详细介绍):
- 正向可以理解从左到右(从第一个元素开始,首个下标为0)逐个元素加1。
- 反向可以理解从右到左(从最后一个元素开始,首个下标为-1)逐个元素减1。
1.4.2 下标代码定义
下标使用列表名[下标值]来定义,具体看例子:
list1=['a','b','c','d','e']
# 获取第一和最后一个的元素值,(列表名[下标值])
print(list1[0],list1[-1]) #输出的结果是’a‘和’e‘
嵌套列表的下标则是由外到里进行定义,定方式:
列表名[外层下标][里面一层下标][……][最里层下标]
list1=['a','b',[1,2,3],'d','e']
print(list1[2][0]) # 输入的结果是“1”。
注意:如果里层不是序列(如上例子:list1[1][0])list1[1]是’b’不是列表如果进行嵌套下去,系统会报错,告诉你不是列表不能进行下标操作。
1.5 列表常用的操作方法
python提供了若干个对列表进行操作的方法,通过这些方法我们可以有效的对列表进行高效的管理,比如增删改查操作,可以让我们在动态时使用列表。
函数/方法 | 使用说明 |
---|---|
int=len(list) | 返回列表的元素个数,返回值是int类型 |
var=max(list) | 返回列表元素的最大值,元素必须一个数据类型,否则报错。 |
var=min(list) | 返回列表元素的最小值,元素必须一个数据类型,否则报错。 |
list1=list(seq) | seq是元组,集合,将序列(元组,集合等)转换为列表序列list1 |
list.index(“元素值”) | 返回指定元素值的正向下标值,如果值不存程序报错抛出异常。 |
list[下标]=“值” | 通过指定下标的列表进行赋值(修改元素值),下标值超出将报错,所以这个方法不能进行追加元素。 |
list.insert(下标,元素值) | 在指定下标处插入一个元素,原来下标以下的下标值都加1;如果指定下标超过元素个数将在末尾插入元素。 |
list.append(元素) | 在列表中追加一个元素,即末尾添加一个元素。 |
list1.extend(list2) | 在list1列表中追加一个列表list2的元素,注意:这个是把列表list2的元素追加进入,而不是把列表追加进去。 |
del list[0] | 删除list列表的第一个元素,列表元素个数减少一个,所有下标-1 |
a=list.pop(0) | 和del list[0]一样的功能,唯一的区别是它在删除前会把元素的值先复制出来给a,让你知道删除的元素是什么。 |
list.remove(元素值) | 删除从正向方向的指定元素值的第一个元素,如果列表中多个元素的值是一样了,它只删除第一个。元素值不存报错。 |
list.clear() | 清空列表。 |
list.count(元素值) | 找出列表中指定元素值的个数,返回整型,比如:[1,2,2,3,2],指定2就会返回3。 |
list.reverse() | 正反向调个个,返回列表对象,比如list=[1,2,3],函数执行完变成list=[3,2,1] |
list.sort() | 对列表按元素大小进行排序,返回列表对象。重要参数:reverse=False(默认)升序,reverse=True 降序 |
list.copy() | 1、对列表进行复制,返回列表对象,例如:list1=list2.copy()使用list2列表来初始化list1, list1=list2虽效果一样,但前者list1和list2的地址是不一样了,后者是两个列表变量指向的是同一个地址,某个元素改变list1和list2的对应的元素都改变。注:copy是在堆中复制了一个列表对象(增加一个列表对象) |
列表也可以使用某些运算符号进行操作,具体如下:
Python 表达式 | 结果 | 描述 |
---|---|---|
[1, 2, 3] + [4, 5, 6] | [1, 2, 3, 4, 5, 6] | 组合(两个列表元素组合) |
[‘Hi!’] * 4 | [‘Hi!’, ‘Hi!’, ‘Hi!’, ‘Hi!’] | 列表中所有元素重复4次 |
3 in [1, 2, 3] | True | 元素是否存在于列表中 |
for x in [1, 2, 3]: print(x, end=" ") | 1 2 3 | 遍历 |
注意点:关于列表复制(深浅复制)
- list2=list1.copy()是浅copy,如果列表list1是个两层嵌套乃至更多层,浅复制只是对最外层列表新建个对象,里层的列表还是直接引用的,list1和list2无论谁里层的列表元素的改变都影响到另外一个。
- 受python变量存储机制的影响,两层列表嵌套在堆中存储就有两个列表对象,每个列表对象中存有元素地址,浅copy只是外层做了新建对象,里层的列表对象可以理解类似还是共享。
2、序列2-元组(tuple)
2.1 元组定义和语法
Python 的元组(tuple,简写为tup)是个有序、不可变序列;其与列表极为相似,不同之处在于元组的元素不能修改(列表作为元素嵌套除外);元组使用小括号()
,列表使用方括号[]
,可以看做是只读列表。
# 元组定义
my_tuple=(1,"sd",2)
my_tuple=(1,) # 注意:元组初始化时,元素个数为一个时,必须加个’,’。
#定义空元组(以下两行写法)
my_tuple=()
my_tuple=tuple()
# 推导式创建
t= (item for item in ['aaa', 'bbb', 'ccc', 'ddd'])
如果元组的元素为列表,由于列表是可以修改了,所以列表作为元组的元素时列表是允许修改的。
元组初始化时,元素个数为一个时,必须加个’,’。
2.2 元组下标
序列都可以使用下标,用法相同。
2.3 元组常用操作方法
元组的常用方法除了不能新增、插入、修改、删除、清空元素外,其他和列表的常用函数一样,用法一样。
2.4 元组和列表的差异
代码定义标识符不同,元组使用(),列表使用[]。
元组不可变性而列表是可变的。
元组元素个数为一个时,定义的时候必须加个逗号,比如:tup(1,)
。
3、序列-字符串(string,简写str)
3.1 定义和语法
字符串可以看做是一个特殊元组,字符串的每个字符可以看做元组的元素,每个字符串中的字符都不允许增删改操作。
字符串定义可以使用""
和''
:
# 定义一个字符串
str="hello"
str1='hello'
# 定义个空字符
str3=""
str4=''
3.2 转义字符
有一些字符因为在python中已经被定义为一些操作(比如单引号和双引号被用来引用字符串),而这些符号我们可能在字符串中需要使用到。为了能够使用这些特殊字符,可以用反斜杠 \ 转义字符(同样地,反斜杠也可以用来转义反斜杠)。如下表:
转义字符 | 描述 | 实例 |
---|---|---|
\(在行尾时) | 续行符,代码过长为了可阅读性,人为换行,解析器认为是一行。 | print("hello\ world") 输出:hello world |
\\ | 反斜杠符号 | >>> print("\\") 输出:\ |
\’ | 单引号 | >>> print('\'') 输出:' |
\" | 双引号 | >>> print("\"") 输出:" |
\a | 响铃 | >>> print("\a") 执行后电脑有响声。 |
\b | 退格(Backspace) | >>> print("Hello \b World!") 输出:Hello World! |
\000 | 空 | >>> print("\000") >>> |
\n | 换行符 | >>> print("hello\nworld") >>> hello world |
\v | 纵向制表符 | >>> print("Hello \v World!") Hello World! >>> |
\t | 横向制表符 | >>> print("Hello \t World!") Hello World! >>> |
\r | 回车,将 \r 后面的内容移到字符串开头,并逐一替换开头部分的字符,直至将 \r 后面的内容完全替换完成。 | >>> print("Hello\rWorld!") World!>>> print('google baidu taobao\r123456') 123456 baidu taobao |
\f | 换页 | >>> print("Hello \f World!") Hello World! >>> |
\yyy | 八进制数,y 代表 0~7 的字符,例如:\012 代表换行。 | >>> print("\110\145\154\154\157\40\127\157\162\154\144\41") Hello World! |
\xyy | 十六进制数,以 \x 开头,y 代表的字符,例如:\x0a 代表换行 | >>> print("\x48\x65\x6c\x6c\x6f\x20\x57\x6f\x72\x6c\x64\x21") Hello World! |
\other | 其它的字符以普通格式输出 |
3.3 字符串格式化
Python 支持格式化字符串的输出,在日常开发中会遇到复杂的字符串表达式以及字符串动态变化等,都需要字符串格式化来解决。
字符串拼接除了常规采⽤”+“来拼接,还有其他好几种方式,具体如下:。
3.3.1 使用%占位符拼接
语法格式:'字符串%[格式化控制符] 格式化字符’ % 对应变量对象(元组)
格式化控制符(可选填也可以多选)有:
- -:可选参数,指定左对⻬
- +:可选参数,指定右对⻬
- 0:可选参数,表示右对⻬,⽤0填充空⽩处(⼀般和m参数⼀起使⽤)
- m:可选参数,指定占⽤宽度
- n:可选参数,指定⼩数点后保留位数
格式化字符(必填)(格式化字符是对应变量的数据类型):
- s:字符串
- d:整型
- f:浮点数
- c:ASCII码
>>>name = "小明"
>>>age = 10
>>> print("我叫%s" % name)
我叫小明
>>> print("我叫%s" % (name,))
我叫小明
>>>print ("我叫 %s 今年 %d 岁!" % (name, age)) #这就是格式化输出1
我叫 小明 今年 10 岁
>>> data=555
>>> print('%5d' % data)
555 # 占位5个不够用空格补,左边两位是空格
>>> print('%05d' % data)
00555 # 占位5个不够使用0补并左对齐
>>> print('%.2f' % data)
555.00 # 保留两位小数点
3.3.2 format格式化
使用关键字format:
>>> name='lucy'
>>> age=10
>>> print('I am {},I am {} years old'.format(name,age)) # 方式一
I am lucy,I am 10 years old
>>> print('I am {0},I am {1} years old'.format(name,age)) # 方式二
I am lucy,I am 10 years old
>>> print('I am {1},I am {0} years old'.format(name,age)) # 方式二的验证,说明大括号里面的序号是和format里面的一一对应。
I am 10,I am lucy years old
>>> del name # 删除变量
>>> del age # 删除变量。
>>> print('I am {name},I am {age} years old'.format(name='lucy',age=12)) # 方式三
I am lucy,I am 12 years old
>>> name='dd'
>>> print('I am {name},I am {age} years old'.format(name='lucy',age=12))
I am lucy,I am 12 years old # format变量只是一样的名字而已,不互相干扰
>>> print(name)
dd
format三种格式说明:
- 方式一、变量必须按照要填入的顺序即可。
- 方式二、大括号里面的序号和format里面的顺序一致。
- 方式三、format根据前面的标签名逐一进行赋值,这个方式相对灵活好用。
3.3.3 f-string
f-string 是 python3.6 之后版本添加的,称之为字⾯量格式化字符串,是新的格式化字符串的语法。
f-string 格式化字符串以 f 开头,后⾯跟着字符串,字符串中的表达式⽤⼤括号 {} 包起来,它会将变量或表达式计
算后的值替换进去,实例如下:
>>>name = '张三'
>>>age = 18
>>>print(f'我的名字是{name},我的年龄是{age}')
我的名字是张三,我的年龄是18
3.4 字符串常用方法
函数/方法 | 描述 |
---|---|
str=str.replace(old, new[, max]) | 返回字符串中的 old(旧字符串) 替换成 new(新字符串)后生成的新字符串,如果指定第三个参数max,则替换不超过 max 次 |
list=str.split(s[, num=string.count(str)]) | 字符串str以s进行分割,如果参数num 有指定值,则仅分隔 num 个子字符串,然后返回列表。例如:str=“a,b,c" str.split(“,”)得到的结果就是[a,b,c]。 |
string=str.strip([chars]) | 移除字符串头尾指定的字符,chars选填,如果不填表示去除空格,有填写去除填写的字符。 |
string=str.rstrip([chars]) | 移除字符串末尾指定的字符,chars选填,如果不填表示去除空格,有填写去除填写的字符。 |
string=str.lstrip([chars]) | 移除字符串开头指定的字符,chars选填,如果不填表示去除空格,有填写去除填写的字符。 |
string=str.capitalize() | 将字符串的第一个字母变成大写,其他字母变小写 |
int=str.count(sub, start= 0,end=len(string)) | 字符串sub在str字符串中出现的次数,start和end选填可以限制范围,不填表示整个字符串。 |
bool=str.islower() | 如果字符串中包含至少一个区分大小写的字符,并且所有这些(区分大小写的)字符都是小写,则返回 True,否则返回 False |
string=str.lower() | 返回将字符串中所有大写字符转换为小写后生成的字符串 |
string=str.upper() | 返回小写字母转为大写字母的字符串 |
string=str.title() | 返回"标题化"的字符串,就是说所有单词都是以大写开始 |
string=str.swapcase() | 返回大小写字母转换后生成的新字符串 |
string=str.join(sequence) | sequence可是是字符串、也可以是列表等容器(元素必须是字符),返回以sequence各个元素使用str分割相连的字符串,例子:list2=[‘h’,‘l’,‘s’] ‘-’.join(list2) 结果是:‘h-l-s’ |
int=str.find(str1, beg=0, end=len(string)) | 检测字符串str中是否包含子字符串 str1 ,如果指定 beg(开始) 和 end(结束) 范围,则检查是否包含在指定范围内,如果包含子字符串返回开始的索引值,否则返回-1,例子:str=‘string’,str1=’tr’,返回:1即t的位置 |
str.index(str1, beg=0, end=len(string)) | 和find一样用法和作用,区别在str1在str中找不到的时候会抛出异常,而find只是返回-1。 |
4、序列切片(下标的应用)
4.1 定义和语法
切片是Python中一种用于操作序列类型(列表、字符串和元组)的方法。它通过指定下标的起始索引和结束索引来截取出序列的一部分,形成一个新的序列。
如图复习:字符串”python“的各个字母作为列表(元组字符串)的元素,p是最早存入,n是最晚存入,所以这些下标索引可以分为正向索引(正数)和反向索引(负数),所以根据书写习惯我们把最早输入的写在最左端,也可以做如下认为:
正向索引:从左到右,从0开始递增。
反向索引:从右到左,从-1开始递减。
特点:
- 正向和反向索引的共同特点,从左到右的索引值都是递增的,也就是由小变大的过程。
- 正反向索引可以联合使用
序列切片语法:
sequence[start:end:step]
sequence:表示待切片的序列(列表、字符串、元组)
start:表示起始索引==(包含)
==,不填时表示正向表示0,反向表示最小值。
end:表示结束索引==(不包含)
==,不填时正向表示最大值,反向表示-1。
setp:表示步长(不填默认为1),步长为-1时表示切片(即反向切片)。
4.2 正反切片
根据对序列的切片方向不同分为正反向切片,正反向切片由步长的正负值来判定,具体正反向切片的定义如下:
- 正向切片:切片的方向是从左到右进行切片,也就是说索引是从小到大的过程(无论正反向索引都一样,正反索引联合使用除外)。
- 方向切片:切片的方法是从右边到左进行切片,得到一个倒序的列表。
重点
- 区别正反切片和正反索引的定义,正反索引其实就是正负下标,正反切片就是正负步长。
- 反向切片有个特点:由于是从右到左的进行切片,所以得到的列表结果和原列表对比元素值是倒过来的,比如正向是[1,2,3],反切片得到是[3,2,1]。
4.3 切片例子
4.3.1 正向切片(步长为正)
使用正向索引进行正向切片(例子取列表为例,元组和字符串用法一样;例子采取密令行方式):
>>> list1=[1,2,3,4,5,6,7,8,9] # 设个列表(元组和字符串一样)
>>> list1[2:4] # 步长不填默认为1
[3, 4] # 上一行的输出结果
>>> list1[1:6:2] # 步长为2的例子
[2, 4, 6]
>>> list1[:2] # 起始索引不填,表示从0开始
[1, 2]
>>> list1[1::2] # 结束索引不填,表示到结束
[2, 4, 6, 8]
>>> list[::2] # 起止索引不填,只填步长
[1, 3, 5, 7, 9]
使用反向索引进行正向切片:
>>> list1=[1,2,3,4,5,6,7,8,9] # 设个列表(元组和字符串一样)
>>> list1[:-1] # -1是结束索引,所以不包括9
[1, 2, 3, 4, 5, 6, 7, 8]
>>> list1[-5:-1] # 切片的顺序是以索引值从小切到大。
[5, 6, 7, 8]
>>> list1[-5:-1:2] # 步长为2
[5, 7]
以上正向切片都是从左到右进行切片的,start都比end小,切片的结果也是按原列表顺序进行的。
正反索引联合正向切片例子
>>> list1=[1,2,3,4,5,6,7,8,9] # 设个列表(元组和字符串一样)
>>> list1[2:-1] # 切片第三个和倒数第二中间的值
[3, 4, 5, 6, 7, 8]
>>> list[2:-7] # 正向索引2和反向索引-7都是指向3,所以切出来空列表。
[]
正反索引联合使用打破常规,但它是个正向切片是从左到右的切片过程,联合使用不能使用正向切片的索引是从小到大的过程,但正反索引联合正向切片,把正反索引按规则转换成正向索引还是反向索引,还是遵从正向切片索引是从小到大的规律。
4.3.2 反向切片(步长为负)
步长为负数的反向切片例子:
>>> list1=[1,2,3,4,5,6,7,8,9] # 设个列表(元组和字符串一样)
>>> list1[::-1] # 步长为1,负数表示方向
[9, 8, 7, 6, 5, 4, 3, 2, 1]
>>> list1[::-2] # 步长为2,负数表示方向
[9, 7, 5, 3, 1]
>>> list1[5:2:-1] # 开始索引是5,由于是负数步长反向切片,结束索引要小于开始索引所以是2(结束索引不包含,所以3就不能取)。
[6, 5, 4] # 处理逻辑:找到开始索引5(包含)取值6,回退取索引4,3的值5和4,结束下标2由于不包含就不取了。
>>> list1[-4:-7:-1]
[6, 5, 4] # 结果和上面一样,和上面的写法区别就是正反索引的写法
以上是反向切片,从右到左进行切片,start>end,切片的结果和原列表是倒过来的。
正反向切片的切片示意图:
5、集合(set)
5.1 定义和语法
集合(set)特点是元素无序、不重复,所以元素值是不能出现重复,每次输出的值的顺序是不固定了每次都有可能不一样。集合还支持一些比较操作,如交集、并集、差集等。
集合的定义使用大括号{}
:
# 定义个集合
s={1,2,"hello",3,[1,2,3]}
myset=set("hello") # 这个输出得到是{'e', 'l', 'o', 'h'},这个起就是把字符串转成集合
# 定义个空集合
s=set()# 只有这一种方法,不能使用空大括号定义空集合
# 推导式创建
str1= "abcdefg"
s1= {ch for ch in str1 if ch not in "abc"}
print(s1) # 输出的结果是{'e','g','d','f'},从这个结果也能看出集合的无序特征
集合的无序不重复的特点:
集合是不支持下标索引的。
集合会自动去重,所以在定义的时候,元素如果重复集合会自动去重,重复的元素自动给与过滤掉。
集合是无序的,每次输出的结果顺序都有可能是不一样的,执行效果如下代码框。
空集合切记不能使用s={}这样来定义,因为这个是定义空字典的初始化定义。
无序的输出结果:
>>> thisset = set(("Google", "baidu", "Taobao"))
>>> print(thisset)
{'baidu', 'Google', 'Taobao'}
>>> print(thisset)
{'Google', 'Taobao', 'baidu'}
5.2 常用函数
方法 | 描述 |
---|---|
set.add(元素) | 为集合追加元素,如果元素存在则不做增加。 |
set.clear() | 清空集合元素 |
set1=set2.copy() | 拷贝一个集合给另一个集合,set1=set2也可以实现,但后者引用的地址是一个地址,某个集合元素一变另一个集合也变。 |
set3=set1.difference(set2) | 取出集合1不在集合2的元素返回给集合3,不改变集合1和2的元素。 |
set1.difference_update(set2) | 抹去集合1中在集合2出现的元素,集合1元素被改变。 |
set.discard(value) | 删除集合中指定的元素(元素不存在时不抛异常) |
z = x.intersection(y) | 返回xy集合的交集给z集合 |
x.intersection_update(y) | 移除x集合中不在y集合中的元素,x集合元素被改变。 |
Bool = x.isdisjoint(y) | 判断集合 y 中是否有包含 集合 x 的元素,如果没有返回 True,否则返回 False。 |
Bool = x.issubset(y) | x是否是y的子集合,判断集合 x 的所有元素是否都包含在集合 y 中,如果都包含返回 True,否则返回 False。 |
Bool = x.issuperset(y) | x是否是y的父集合,判断集合 y 的所有元素是否都包含在集合 x 中,如果都包含返回 True,否则返回 False |
元素值=set.pop() | 随机移除一个元素,返回被移除的元素值 |
set.remove(item) | 移除指定元素,元素不存在会抛出异常。 |
z = x.symmetric_difference(y) | 返回两个集合组成的新集合,但会移除两个集合的重复元素 |
x.symmetric_difference_update(y) | 在原始集合 x 中移除与 y 集合中的重复元素,并将不重复的元素插入到集合 x 中 |
z = x.union(y) | 合并两个集合xy给z,重复元素只会出现一次 |
x.update(y) | 合并两个集合xy给x,重复元素只会出现一次: |
a=len(set) | 统计集合的元素个数 |
5.3 集合运算符计算
集合a | b | d | r | a | c | |||
---|---|---|---|---|---|---|---|---|
集合b | a | c | l | z | m | |||
a|b(并集) | b | d | r | a | c | l | z | m |
a-b(a集合中b没有的元素) | b | d | r | |||||
a&b(交集) | a | c | ||||||
a^b(不同时包含于a和b的元素) | b | d | r | l | z | m |
a-b:(a集合中b没有的元素)相当于a.defference(b)
a|b:(并集)合并两个集合 a.union(b)
a&b:(交集)取两个集合相同的部分
a^b:(不同时包含于a和b的元素),取两个集合的元素,但是去掉两个集合都出现的元素。
6、字典(dictionary ,简写为dict)
6.1 定义和语法
概念:字典(dictionary ,简写为dict)是另一种可变容器模型,且可存储任意类型对象,和c#的字典和哈希表,以及其他有的语言的map一样,就是key和value对应的键值对关系存储的数据容器,字典的键(key)必须是唯一的且不可变(如字符串、数字或元组),而值(value)则可以是任意数据类型。
用法:字典的每个键值 (key=>value
) 对用冒号 (:) 分割,每个对之间用逗号 (,) 分割,整个字典包括在花括号 ({}
) 中 ,格式如下所示:
# 定义字典
dict = {key1 : value1, key2 : value2 }
#定义空字典
dict={}
dict=dict()
# 推导式创建
listDemo= ["Google", "BaiDu", "TaoBao", "JingDong"]
Dict= {name:len(name) for name in listDemo}
关注点:
- 字典是以一个键值对存在作为字典的元素,key不可重复和修改,所以key可以是字典外的任何数据类型;键值对可以被删除,value也可以被修改。
- 字典非序列所以不存在下标一说;字典可以被嵌套。
6.2 常用函数
函数/方法 | 描述 |
---|---|
value=dict[key] | 返回key对应的value值,如果是嵌套的字典就是采用dict[key][key]来获取几层就几个[],key不在dict中会抛出异常。 |
dict[key]=value | 如果key存在的话,相当于对key的值进行修改,如果key不存在就是新增key和value键值对。 |
dict.pop(key) | 删除指定key的键值对,并返回key对应的value值 |
tuple=dict.popitem() | 随机删除一个键值对,返回一个键值对元组。 |
dict.clear() | 清空字典 |
dict_keys=dict.keys() | 返回字典的所有key值(keys是dict_keys类型对象),所以这时可以使用for语句遍历key的值(for key in keys来遍历key)。 |
dict_values=dict.values() | 返回字典的所有value值(values是dict_values类型对象),可以使用for语句遍历。 |
bool=key in dict | 判断键是否在字典里,如果键在字典dict里返回True,否则返回False;可以使用for语句进行遍历。 |
dict2=dict1.copy() | 字典1复制给字典2(copy都是在堆中重建字典对象,所以地址都是不一样,修改dict1的值不会改变dict2。 |
value=dict.get(key,default=None) | 取键对应的值,defalult是设置个默认值,如果键不存在就返回设置的默认值。 |
value=dict.setdefault(key,default=None) | 取key对应的值,如果key不存在,会把key和default写入到字典并返回defalult。 |
dict1.update(dict2) | 把字典2的元素追加到字典1中,追加后的字典1包含了1和2的内容,如果出现重复key了话讲不会被写入。 |
dict_items=dict.items() | 返回所有的键值对集合,每个键值对以元组形式存在。例如:字典dict={‘name’:‘lucy’,‘age’:12},dict.items()=[(‘name’,‘lucy’),(‘age’,12)] for循环中 key,value in dict.items(),这样可以直接遍历出key和value。 |
dict1.update(dict2) | 合并两个字典给dict1,如果两个key冲突,使用dict2的值覆盖。 |
dict1= dict.fromkeys(keys[,default_value]) | 创建一个新字典,keys可是各种序列、集合,default_value不填默认是None,新字典以keys中的元素为key,以default为值。 |
总结:
- 字典相对于其他容器如列表,查找和插入的速度极快,不会随了key的增加而增加,而列表随了元素的增加速度上会有所影响。
- 字典相对于列表在内存损耗上会占用更多的空间,这就是字典空间换时间的说法。
7、Python组包和拆包
在Python中,拆包和组包是非常常见的操作,主要是指将一个可迭代对象拆分为独立的变量叫拆包,或者将多个变量组合成一个元组叫组包。
什么事可迭代对象:
可迭代对象(Iterable)是指一个能够返回其元素的对象,可以逐一遍历其中的每个元素。换句话说,可迭代对象可以被用在 for 循环中,或者可以通过函数 iter() 来获取它的迭代器。
7.1 组包(Packing)
将多个变量组合成一个元组,组包只是组成一个元组。
# 直接组包
a, b, c = 1, 2, 3 # 变量赋值
data = a, b, c # 组包
print(data) # 输出一个元组:(1, 2, 3)
7.2 拆包(Unpacking)
将字典的键和值分别拆包成独立变量,变量的个数要和元素的个数一致,否则将抛出异常。
# 拆包
a,b,c=(1,2,3)
# 字典解包
info = {'name': 'Alice', 'age': 25}
key, value = info.popitem()
print(key, value) # 输出:age 25
8、数字序列(Range)
8.1 概念和定义
range() 是Python的一个内置函数,返回的是一个可迭代对象。用于创建数字序列。所谓的数字序列元素一定是数字。
语法:
range(start, stop, step)
start:开始值(不填表示从0开始)
stop:结束值(不含)(必须填写)
step:步长,默认为1(不填为默认1)
三种常用语法的示例:
for var in range(1,10,2):
print(var,end="")
print()
for var in range(1,10): # 未写步长默认为1
print(var,end="")
print()
for var in range(10): # 单个变量表示为0开始,10结算,步长为1
print(var,end="")
输出结果:
13579
123456789
0123456789
9、各种数据容器小结对比
9.1 数据容器相关参数对比
列表 | 元组 | 字符串 | 集合 | 字典 | |
---|---|---|---|---|---|
实现方式 | 基于动态数组 | 特殊的数组结构 | 特殊的数组结构 | 基于哈希表 | 基于哈希表 |
元素数量 | 支持多个 | 支持多个 | 支持多个 | 支持多个 | 支持多个 |
元素类型 | 任意 | 任意 | 任意 | 任意 | key和valu都任意 |
下标索引 | 支持 | 支持 | 支持 | 不支持 | 不支持 |
重复元素 | 支持 | 支持 | 支持 | 不支持 | 不支持 |
可修改性 | 支持 | 不支持 | 不支持 | 支持 | 支持 |
数据有序 | 有序 | 有序 | 有序 | 无序 | 无序 |
使用场景 | 可修改、可重复的一 批数据记录场景 | 不可修改、可重复的 一批数据记录场景 | 一串字符的记录场景 | 不可重复的数据记录场景; 还有对两个容器中的元素 进行交叉比对的场景; | 以key检索value的数据 记录场景 |
重点:
- 数据容器通用的函数:len,max,min(元素个数,最大元素,最小元素)字典只是体现key的最大最小值。
- 各个容器都支持拆包,拆包的变量个数必须和元素的个数一样即可直接对变量赋值。
9.2 各种容器的优缺点
列表list:
- 优点:灵活性强,支持多种操作。
- 缺点:拓展性能不佳。
元组tuple:
- 优点:因不变性,安全高、占用内存少、性能叫列表高。
- 缺点:因不变性导致灵活性低。
集合set:
- 优点:高效集合运算,可以进行并集、交集、差集计算。
- 缺点:无序,不可索引检索。
字典dictionary:
- 优点:键值存储灵活,性能非常高效。
- 缺点:无序,占用内存。
(五)、python语法基础
1、Python命名规范
标识符是用来标识代码的变量名、函数名、类名、等的标识,为了和系统的关键字不起冲突及代码的可读性,所以进行了一系列的规范。
- 标识符只允许出现:英文、数字、下划线(_)
- 数字不能作为标识符的开始
- Python对英文标识符是大小写敏感的。
- 系统的关键字不能作为标识符使用
- 变量的命名规范做到见名知意思
- 对于多个英文单词建议使用下划线隔开
2、Python注释
注释就是对代码的解释和说明,其目的是让人们能够更加轻松地了解代码。
2.1、单行注释
# 这个就是单行注释,“#”后必须根个空格,只是规范不是强制要求。
2.2、多行注释
# 多行注释
'''
-这就是多行注释
'''
"""
-这也是多行注释
"""
# 也可以作为定义字符串
my_string = """
这是一个多行字符串的示例,
它可以包含多行文本和换行。
"""
print(my_string) # 输出:这是一个多行字符串的示例,它可以包含多行文本和换行。
注意点:
1、# 作为单行注释
2、‘’'和”“” 作为多行注释的同时,也可以作为定义字符串,这个字符串也是多行的,包含换行符和空格什么。
3、TODO注解
注解就是注释的特殊存在,使用单行注释+TODO(大小写都无所谓)来实现,todo并不是python语言中的关键字,只是一种标准而已。
在实际开发中, TODO 注释可以与任务跟踪工具(如JIRA、Trello等)结合使用,以帮助团队更好地管理和跟踪代码中的待办事项。此外,一些集成开发环境(IDE)和代码编辑器提供了对 TODO 注释的搜索和标记功能,使得查找和处理这些待办事项变得更加方便。
# TODO 主要是标明待做任务,注明任务人和任务时间等,一般也作为功能步骤说明。
注解:它是一种特殊的注释,主要作为开发步骤、待办事项、功能说明的解释作用。
4、代码段和代码结束标识
4.1、缩进
Python 最具特色的就是使用缩进来表示代码块,不像其他语言比如c#那样通过大括号==“{}”==来标识代码块,通过大括号的内嵌匹配来分层级;而python是通过缩进方式,相同的缩进表示相同层级,缩进越深表示越里层。
4.2、逻辑换行
行分为物理行和逻辑行。
- 物理行:程序员编写代码的行。
- 逻辑行:python解释器需要执行的指令。
逻辑换行是指一行代码过长不易解读需要换行,但又能让解释器认为他是一行代码,这就是需要引入==“\”==符号来换行,具体看如下代码(如下两行代码执行出来是一样的):
conf = (SparkConf().setMaster("local[*]")\
.setAppName("test_spark_app"))
conf = (SparkConf().setMaster("local[*]").setAppName("test_spark_app")
(六)、输入和输出介绍(input和print)
这里学习的输入输出主要是学习input和print函数的用法,这两部分是python比较经常使用的函数。
1、input(输入)
input主要是通过键盘输入,让系统得到你所输入的内容。
语法:
str=input(‘提示内容’)
返回:字符串
提示内容:一个字符串
例子:
str=input('请输入一个整数:') # 会输出一串字符串”请输入一个整数“后系统阻塞等待输入,等待你输入回车后str会收到你的输入结果。
返回值都是字符串,如果希望是整型就需要进行转换。
2、print(输出)
2.1 语法和定义
print是程序直接在程序中输出结果。
语法:
print(values,…[,sep=’ ‘,end=’\n’,file=sys.stdout])
values:要输出的对象,可以是数字,字符串,序列等;(省略号表示一次可以输出多个对象分别使用逗号隔开,输出结果是空格隔开的内容)。
sep:分隔符。当输出两个或者两个以上的对象时,对象与对象之间默认使用空格’ ‘分隔开,可以带入一个符号,输出对象将使用该字符分隔。
end:输出所有对象后,默认换行。’\n’代表换行,为空代表不换行。
file:设置输出设备,默认输出到显示器。
例子:
>>> print('hello','world','dave','ruan','good') # 默认的使用办法
hello world dave ruan good
>>> print('hello','world','dave','ruan','good',sep='/') # 使用了sep参数,输入结果就起了变化
hello/world/dave/ruan/good
>>> print('hello')+print('world') # 模拟连续输入两个print,默认情况下是会换行的。
hello
world
>>> print('hello',end='')+print('world') # 对第一个print的end参数给与”“(空值)代表不换行,输出结果就不换行了。
helloworld
print可以配合字符串的格式化一起使用。
(七)、Python3条件控制语句
1、IF-条件语句
1.1 定义和语法
Python条件语句if和其他语言的一样都是通过一条或多条语句的执行结果(True或者False)来决定执行的代码块。
语法1:
if condition_1: 条件为True时才能进入执行
statement_block_1
elif condition_2:
statement_block_2
else:
statement_block_3
语法2(嵌套):
if condition_1:
statement_block_1
if condition_2:
statement_block_2
else:
statement_block_3
else:
statement_block_4
if语句例子:
cars = ['BMW', 'Audi', 'Toyota', 'Honda']
for car in cars: # 遍历列表
if car == 'BMW': # 判断
print('I like BMW!')
else:
print(car.title(), ', nice car!')
注意:
- 每个条件后面要使用冒号(:),表示接下来是满足条件后要执行的语句块。
- 使用缩进来划分语句块,相同缩进数的语句在一起组成一个语句块。
- if语句可以进行嵌套。
7、match-case语句
7.1 定义和语法
match-case是Python3.0开始才有的,在python2.0时代是没有的;它和c系列的Switch……case类似,但用法还是有点差别了,它比其他语言的更加强大和灵活。
语法:
match expression:
case pattern1:
# 当expression与pattern1匹配时执行的代码块
case pattern2:
# 当expression与pattern2匹配时执行的代码块
case pattern3:
# 当expression与pattern3匹配时执行的代码块
……
case _:
# 如果没有其他case匹配 ,则执行这里的代码块
基础语法例子:
i=2
match i:
case 1:
print(1)
case 2:
print(2)
case 3:
print(3)
case 4:
print(4)
case 5:
print(5)
输出:
2
注意:
match-case和C系列的Switch-case的作用一样,用法还是有差异了,Switch每个case都需要break,不然以上的例子就会出现2、3、4、5的结果,而match不会。
7.2 多种匹配例子
7.2.1 并列整数匹配
def greet_by_time_of_day(hour):
match hour:
case 6 | 7 | 8 | 9 | 10 | 11:
print("Good morning!")
case 12 | 13 | 14 | 15 | 16 | 17 | 18:
print("Good afternoon!")
case 19 | 20 | 21 | 22 | 23 | 0 | 1 | 2 | 3 | 4 | 5:
print("Good evening!")
case _:
print("Invalid time")
greet_by_time_of_day(14) 输出:Good afternoon!
7.2.2 数据类型匹配
def process_number(number):
match number:
case int():
print("This is an integer.")
case float():
print("This is a floating point number.")
case _:
print("Not a number.")
process_number(42) # 输出:"This is an integer."
process_number(3.14) # 输出:"This is a floating point number."
7.2.3 列表(元组)匹配
def analyze_scores(scores):
match scores:
case [int(score1), int(score2)] if score1 > score2:
print(f"Score 1 ({score1}) is higher than Score 2 ({score2}).")
case [int(score1), int(score2)]:
print(f"Score 1 ({score1}) and Score 2 ({score2}) have been recorded.")
case _:
print("Invalid scores format.")
analyze_scores([85, 70]) # 输出:Score 1 (85) is higher than Score 2 (70).
7.2.4 字典匹配
def process_user_info(user_info):
match user_info:
case {"name": str(name), "age": int(age)}:
print(f"Name: {name}, Age: {age}")
case {"name": str(name)}:
print(f"Name: {name}, Age not provided")
case _:
print("Invalid user info format")
# 测试
process_user_info({"name": "Alice", "age": 30}) # 输出:Name: Alice, Age: 30
process_user_info({"name": "Bob"}) # 输出:Name: Bob, Age not provided
7.2.5 守卫模式
def process_data(data):
match data:
case [int(x), int(y)] if x > y:
print(f"x({x}) is greater than y({y})")
case [int(x), int(y)]:
print(f"x({x}) and y({y}) are processed")
case _:
print("Data does not match expected pattern")
# 测试
process_data([5, 3]) # 输出:x(5) is greater than y(3)
process_data([2, 8]) # 输出:x(2) and y(8) are processed
(八)、Python循环控制语句
所有的语言都离不开条件判断和循环控制语句,循环控制语句顾名思义就是重复循环执行条件内的代码块,具体流程图如下:
1、while循环语句
1.1 定义和语法
while 判断条件: 条件为True时才能进入循环体
statements
例子:
n = 100
sum = 0
counter = 1
while counter <= n: # 条件后的冒号不能忘记
sum = sum + counter # 循环体内的缩进要注意,要比while语句更缩进代表体内代码块。
counter += 1
print('Sum of 1 until %d: %d' % (n,sum))
循环需要有条件限制且限制到位,在合适的时候跳出循环,避免写个死循环导致程序崩溃
2、for循环语句
2.1 定义和语法
python的for语句和while语句是有本质的区别,也不同于其他语言的for语句,更像其他语言的foreach语句,它只能适用于数据容器的元素遍历使用。
语法:
for in : 条件为True时才能进入循环体
else:
例子:
>>> languages = ["C", "C++", "Perl", "Python"]
>>> for x in languages:
... print (x)
...
C
C++
Perl
Python
>>>
3、循环配套关键字和函数使用(break、continue、pass、range())
3.1 break 和 continue、pass
- break语句:终止循环,立刻跳出整个循环。
- continue语句:被用来告诉 Python 跳过当前本轮循环,然后继续进行下一轮循环。
- pass语句:pass是空语句什么都不做。它只在语法上保持语法的完整性。
(九)、函数
一直以来函数和方法这连个概念都很是令人混淆,其实不难区分方法其实就是绑定在对象内的函数,简单理解就是在类中的函数叫方法。
1、定义和语法
函数的定义就是组织一个代码块实现某些功能,而这些功能有可能随时在不同的地方被多次引用,为了实现代码的可读性和可扩展性引入函数代码块概念,简单的理解是如果没有函数,将会在不同地方使用相同功能的话将重复写这个功能的代码,浪费时间也不利后续的功能修改。
1.1 语法
1.1.1 函数定义语法
def 函数名(形参1、形参2、……形参n=default):# 设置默认值的形参只能放在最后一个。
函数体
return 返回值
重点关注:
- 形参概念:在函数定义的时声明的参数称为形参,形参的本质就是一个变量名。
- 函数无需返回时,可以不需要return语句。
- 带默认值的形参不能出现在未带默认值的形参前面。
1.1.2 函数被调用语法两种
1、[变量]=函数名([形参名]=实参) 该种形参=实参的方式,调用时无需按照形参顺序带入
2、[变量]=函数名(实参) 这种方式实参要和形参一一对应就必须和形参一样的顺序
重点关注:
- 实参概念:实参是在调用函数时传递给函数的值,可以是常量、变量、表达式或它们的组合。
- 带有默认值的形参可以带入实参也可以不带入实参,不带入时形参取默认值进入函数体运算。
1.1.3 实现具体例子
def test(a,b,c=0):# 形参c=0代表设置的默认值为0,注意:有默认值的参数不能出现在没有默认值参数的前面。
'''
功能说明:返回几个数字的乘积
-param a:参数a的说明
-param b:参数b的说明
-param c:参数c的说
-return:返回三个数的乘积
'''
return a*b*c
# 函数调用方法:
# 带入形参名的调用,顺序可以打乱
test(b=2,a=3,c=8)
输出:48 # 3*2*8=48
# 由于c有默认值可以不带入
test(b=2,a=3)
输出:0 # 3*2*0=0
# 不带入形参名的
test(3,2,8)
输出:48 # 3*2*8=48
# 不带入形参名切有默认值的
test(3,2)
输出:0 # 3*2*0=0
以上代码是标准的规范化写法,2到8行是函数的说明文档,让人家知道你定义函数的目的和用法,说白的就是一份说明书
2、函数参数传递
所谓的参数传递就是函数在调用的时候,使用实参的值传递给形参的过程,称为参数传递。
根据前面[变量的内存存储特性](# 1.1 变量的内存存储方式),参数在传递的过程中如果形参在函数执行过程中被重新赋值,对于普通变量和数据容器,将产生出不同的结果。
2.1 普通变量作为实参传递
def test(a):
print(f"形参未被重新赋值前的地址:{id(a)} ,值a={a}")
a=100
print(f"形参被重新赋值前的地址:{id(a)} ,值a={a}")
x=0
print(f"实参的初始赋值地址:{id(x)} , 值x={x}")
test(x)
print(f"函数执行后的实参地址:{id(x)} , 值x={x}")
输出结果:
实参的初始赋值地址:4434841280 , 值x=0
形参未被重新赋值前的地址:4434841280 ,值a=0
形参被重新赋值前的地址:4434844480 ,值a=100
函数执行后的实参地址:4434841280 , 值x=0
实参是普通变量传递时参照例子说明:
- 实参传递给形参是实参指向值对象的地址(值在堆内存中的地址)传给形参。
- 形参的值改变只是重新指向新值100的地址,未影响实参的指向对象地址,所以未改变实参的值。
2.2 数据容器(序列、集合、字典)作为实参传递
2.2.1 常规序列(可变序列)作为实参
由2.1例子实参是普通变量的传递,可以看出实参传递给形参的是实参变量指向其值对象的地址,也就是变量的值在堆内存中的地址。
def test(p_lsit:list):
print(f"形参未被改变的列表内存地址:{id(p_lsit)}")
# 遍历改变前各个元素的地址
for var in p_lsit:
print(f"\t形参改变前下标为:{p_lsit.index(var)} 号元素值:{var} 内存地址:{id(var)}")
p_lsit[0]=100
print(f"形参元素被改变的列表内存地址:{id(p_lsit)}")
# 遍历改变后各个元素的地址
for var in p_lsit:
print(f"\t形参改变后下标为:{p_lsit.index(var)} 号元素值:{var} 内存地址:{id(var)}")
my_list=[1,2]
print(f"函数执行前实参的列表内存地址:{id(my_list)}")
for var in my_list:
print(f"\t函数执行前实参列表下标为:{my_list.index(var)} 号元素值:{var} 内存地址:{id(var)}")
test(my_list)
print(f"函数执行后实参的列表内存地址:{id(my_list)}")
for var in my_list:
print(f"\t函数执行后实参列表下标为:{my_list.index(var)} 号元素值:{var} 内存地址:{id(var)}")
输出结果:
函数执行前实参的列表内存地址:4308093184
函数执行前实参列表下标为:0 号元素值:1 内存地址:4304908000
函数执行前实参列表下标为:1 号元素值:2 内存地址:4304908032
形参未被改变的列表内存地址:4308093184
形参改变前下标为:0 号元素值:1 内存地址:4304908000
形参改变前下标为:1 号元素值:2 内存地址:4304908032
形参元素被改变的列表内存地址:4308093184
形参改变后下标为:0 号元素值:100 内存地址:`4304911168` # 对0号元素的值修改为100,0号的地址改变了,以为了列表空间存的地址也改变了
形参改变后下标为:1 号元素值:2 内存地址:4304908032
函数执行后实参的列表内存地址:4308093184
函数执行后实参列表下标为:0 号元素值:100 内存地址:`4304911168` # 实参列表地址中的元素地址被修改了。
函数执行后实参列表下标为:1 号元素值:2 内存地址:4304908032
例子分析说明:
- 如例子开头的说明,实参传递给形参的是值的对象地址,而其值是列表,列表对象在堆中存储着有元素地址指针((一)篇的变量存储方式)。
- 形参列表元素值的改变,会更新形参指向的列表对象地址中存储的元素地址,而形参和实参指向的列表对象地址是一样了,所以形参元素值的改变会影响到实参的元素值。
- 如果形参重新指向一个列表对象地址,怎么修改元素值都不会影响到实参,比如重新赋值个新列表给形参,或者形参=形参.copy(),因为使用copy会重新创建个列表对象空间,这样实参和形参指向的是不同列表对象的地址就互不干扰了。
2.2.2 元组序列(字符串)作为实参
元组和字符串作为不可变序列存在作为实参有它们不同的特点:
- 元组:由于不可修改性,所以元组作为实参传入,在函数中形参也是不能被修改的。
- 字符串:字符串是特殊元组,在传入函数形参时,形参是不能对字符串采用下标的方式对字符串中的某些字符进行修改,通常也不会这样操作;最经常的操作是给形参重新赋值个新字符串,这样形参指向的字符串对象地址就发生了改变(例如:先str='hello’在str=‘hao’,这就是两个字符串都给同一个变赋值过,对应字符串对象的存放地址是不一样的),也就是形参和实参指向的是两个不同的字符串对象地址。
3、函数的多个返回值
有时函数需要返回多个值,例如:x,y
例如:
# 定义个函数有两个返回值
def test():
return 1,2
# 调用函数,赋值变量的顺序和函数定义返回的顺序以一一对应,1和2分别赋值于x和y。
x,y=test()
关键点:
- 如上例子return 1,2和retrun(1,2)是一样了,虽然一个是两个数字,一个是一个数组,你可以理解当返回多个值时,会进行组包;
- 调用函数时返回的是一个元组,可以返回一个元组变量也可以拆包的方式返回给对等数量及一一对应数据类型的变量。
4、函数的多种参数形式
可变参数也叫魔法参数,分为两种位置参数和关键字参数。
4.1 可变形参-位置参数(*args)
定义语法:
def test(*args)
args:形参是个元组 # args这个单词不是固定要求,你可以是var关键是前面要星号
调用语法:
test(1,2,3,4,……,n) # 允许n个常量或者变量作为实参传入给形参
test((1,2,3),……) # 元组作为一个实参传给形参
test(*列表|元组|字符串|集合]) #<–单个星号加各种数据容器拆分容器中的元素传递给形参
test(*字典) # 单个星号加字典,拆分字典把全部key传递给形参
实参1:是多个逗号分隔的常量或变量
实参2:*数据容器,拆分容器中的元素作为实参传入,字典插入的是所有的key。
4.2 可变形参-关键词参数(**kwargs)
定义语法:
def test(**kwargs) # kwargs这个词不是固定要求,可以是任意个单词只要前面有两个星号
kwargs:形参是个字典
调用语法:
test(key1=value,……,keyn=valuen) # 实参采用键=值的方式传递给形参变成字典
test(name=‘lucy’,age=12) # 采用这种方式,形参得到{‘name’:‘lucy’,‘age’:12}
test(**字典) # 字典被拆出键值对传给形参
4.3 可变形参的组合(*args,**kwargs)
定义语法:
def test(*args,**kwargs) # 一个星不能写在两个星后面否则报错,比如:(**kwargs,*args)就报错
args:一个元组
kwargs:一个集合
调用语法:
调用方法就是上面两种方法的组合,单个变量或常量就给元组,key=value的就给字典。
4.4 函数作为参数
函数名就是一个变量,所以函数名是可以作为一个变量是可以作为实参和形参的,具体看示例:
def test_func(compute): # compute 就是一个函数参数
result=compute(3,5)
return result
def add(x,y):
return x+y
result=test_func(add) #调用是,直接把函数add作为实参传入
print(result)
输出:
8
5、函数变量作用域
5.1 变量作用域
提到变量的作用域就要提全局变量(Global variables)和局部变量(Local variables)两个概念。
- 全局变量:就是在模块中(模块就是一个py为后缀的文件)所有函数都可以调用的变量,一般在函数体和类的外围被定义。这个全局只是限定在模块中,模块在后面章节介绍。
- 局部变量:就是在函数体内的变量。
**看例子理解:**局部变量和全局变量重名时局部变量优先
mycount=10
def test():
print('看全局:',globals()) # 获取全局变量
mycount=100 # 这里相当于声明了一个同名的局部变量,而_mycount+=1,则相当是对全局变量进行赋值就会报错
print("创建同名变量后看全局:",globals()) # 获取全局变量
print("创建同名变量后看局部:",locals()) # 获取局部变量
print("创建同名变量后变量的值:",mycount) # 获取变量值
test() # 函数体内声明了一个同名的局部变量,函数体默认使用局部变量优先
输出:
看全局: {'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x1045b4140>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__file__': '/Users/dave.ruan/Documents/Dave/pyexfile/exvscode/helloworld/helloworld.py', '__cached__': None, 'mycount': 10, 'test': <function test at 0x1045863e0>} # 全局变量找到mycount=10
创建同名变量后看全局: {'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x1045b4140>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__file__': '/Users/dave.ruan/Documents/Dave/pyexfile/exvscode/helloworld/helloworld.py', '__cached__': None, 'mycount': 10, 'test': <function test at 0x1045863e0>} # 全局变量找到mycount=10
创建同名变量后看局部: {'mycount': 100} # 局部变量存在mycount=100
创建同名变量后变量的值: 100 # 根据变量名输出:100
注意点
- 函数中允许创建和全局变量同名的变量。
- 函数中创建同名变量后,同名变量名在函数中被引用时,系统会认为是局部变量。
看例子思考:
name = 'Charlie'
def test_1():
print(name) # 这里打印出来的是全局变量
def test_2():
print(name) # 这里会报错,原因是后续代码有创建同名变量,这里系统认为name是局部变量,引用早于创建系统会报错。
name='lucy'
test_1()
test_2()
输出:
Charlie
Traceback (most recent call last):
File "/Users/dave.ruan/Documents/Dave/python学习例子/myproject12/match_test.py", line 11, in <module>
test_2()
File "/Users/dave.ruan/Documents/Dave/python学习例子/myproject12/match_test.py", line 7, in test_2
print(name)
^^^^
UnboundLocalError: cannot access local variable 'name' where it is not associated with a value
关注点
- 函数中未对全局变量名进行赋值(表示创建或修改)时,系统会认为该变量是全局变量。
- 系统中存在对和全局变量进行赋值时,系统会把函数中同名的变量都认为是局部变量,无论改变是否早于创建时如上例子。
5.2 global关键字提升函数体内宣告全局变量
综上所述,只要把全局变量在函数中进行赋值时都会被认为创建的局部变量,导致在函数中无法修改全局变量的值,但在实际开发中就要在函数中去修改全局变量的需求,这时就要引入关键字global来宣告全局变量的存在,对此无论如何系统都会认为该变量就是全局变量了,如下例子:
name = 'Charlie'
def test_1():
print(name)
def test_2():
global name # 这里声明本函数内的name是全局变量。
print(name) # 这里再也不会认为是局部变量了就不会报错了
name='lucy'
test_1()
test_2()
# 输出的结果:
# Charlie
# Charlie
5.3 nonlocal关键字函数体内宣告外层变量
如果函数中存在嵌套,里层怎么修改外层的变量呢?使用nolocal关键字,如果嵌套层级超过两次,就使用global关键字把它宣告成全局。
def outer():
x = 10 # 外层函数的局部变量
def inner():
nonlocal x # 声明使用外层函数的 x
x = 20 # 修改外层函数的 x
print("内层函数修改后的 x:", x)
inner() # 调用内层函数
print("外层函数中的 x:", x)
outer()
5.4 规范化杜绝全局变量和局部变量同名
虽然系统并没有限制局部变量和全局变量的同名,为了避免出现不必要的理解混淆导致出错,有必要规范变量的命名及相关操作。
- 应该杜绝使用存在的全局变量名或者类变量名直接在函数中给与赋值,若要在函数中修改全局变量的值使用global关键字宣告。
- 应该在命名过程中遵从Python社区规范进行命名,全局使用全部大写字母、局部使用小写字母和下划线联合使用(下划线不要开头)、下滑线开头的变量名作为私有变量使用等等。
6、函数的嵌套
6.1 函数嵌套定义例子
函数的嵌套言外之意就是在函数中定义一个函数并调用它,具体看下面例子:
def outer_function():
print("In outer_function")
def inner_function(): # 内部定义个函数
print("In inner_function")
inner_function() # 调用内定义的函数
outer_function()
6.2 函数嵌套的意义和使用场景
- 作用域来看:内部定义的函数只能在定义它的函数中使用,不在以外地方被使用。
- 生命周期:内嵌函数的生命周期仅限于外层函数调用时的执行期间,而外部函数需要贯穿整个模块的生命周期,会永久占用一个固定的内存空间。
- 访问性:内嵌函数仅在其所在的外层函数中可见,无法被外层函数外部的代码调用。
- 性能:内嵌函数的定义仅在外层函数执行时创建和销毁,每次调用外层函数时都会重新创建内嵌函数。
- 使用场景:当你需要将复杂的功能拆分为多个小部分,但只希望在特定的函数内使用这些小部分时,可以使用内嵌函数。这样可以提高封装性并限制函数的作用域。
7、匿名函数lambda
7.1 定义和语法
匿名函数也称为lambda函数,是一种没有函数名的函数。它是一种一次性的、在需要的时候定义,用完即丢弃的函数。
语法:
lambda arguments:expression
- arguments:参数,多个参数使用逗号隔开
- expression:表达式是
lambda
函数计算并返回的结果,只能一行不能多行。
示例:
lambda x,y:x+y # 表示这个函数有形参x和y,表达是return x+y。
# 前面章节有函数作为函数的参数进行使用的,我们也可以使用lambda更便捷.
def test_func(compute):
result=compute(3,5)
return result
result=test_func(lambda x,y:x+y)
print(result)
# 输出结果:8
8、python不存在函数重载
python不存在函数重载,python可以通过*args、**kwargs参数实现多种参数的调用,也就实现了其他语言意义上的重载。
(十)、python模块和包的应用
1、模块(moudle)
1.1 概念定义
模块就是一个后缀名是py的文件,里面包含着类、变量、方法、函数等,我们可以引入模块而使用模块中的类、方法等成员。我们写的任何一个py文件都能称为模块,模块的名称就是文件名。
1.2 模块导入
[from 模块] import [模块|类|变量|函数|*] [as 别名]
- 使用from 模块时import 可以是*(表示全部导入)也可以指定某个函数或者某个变量导入。
- 可以不使用from直接使用import 模块,表示整个模块导入。
1.2.1各种导入方式的用法差异
- 使用 from 模块 import * 或者是指定函数导入的,在使用的过程中可以直接使用函数调用就可以了。
- 使用import 模块 导入的,使用模块里的函数时,需要使用模块名.函数名()。
如果使用第一种方法导入时,如果本模块中存在同名的函数时,调用的时候会默认调用本模块的函数,如果要调用导入模块的函数名就需要带入模块名。
1.3 模块的内置变量
__name__ # 当前模块被作为主程序执行时:__name__=__main__, 如果是作为被导入引用的,其值就是模块名称。
__all__ # 卸载模块头,一般低于模块导入行,这时个列表,
__all__ = ["public_function", "PublicClass"] # 表示模块被导入时,只可以使用列表中的函数或类
注意:
- all内置变量在模块被导入时,限制使用all列表列出的函数或类,只针对from 模块 import 这样的导入密令,其他密令导入无效。
- 正常会在模块中判断、__name__是否等于“__main__"来测试模块的代码避免在导入的时候执行如下代码。
2、包(package)
2.1 包概念
从物理上看就是个文件夹,是模块的归类,包里面放着相同类的模块,包里还必须包含一个__init__.py文件(该文件内容可以为空)。切记__init__文件的存在才能称为包。
2.2 包的导入
导入语法如下四种:
from 包 import 模块
from 包 import *
from 包.模块 import 函数(变量)
from 包.模块 import *
2.3 __init__.py文件作用
__init__.py文件对包的重要性非常重要,该文件存在才表示包,否则就不被认为是包,该文件的具体作用如下:
-
标识包:将包含它的文件夹标识为 Python 包,哪怕文件是空文件。
-
包初始化:每次导入包,__init__.py模块中的代码都会被指向。
# mypackage/__init__.py print("Initializing mypackage") # 导入包中的模块 from .module1 import greet from .module2 import add
-
导入包中成员:可以控制包中的模块或成员在包被导入时的行为,具体如下例子
# mypackage/__init__.py from .module1 import greet from .module2 import add # 可以直接通过 mypackage.greet() 访问 # 在使用包的代码 # main.py import mypackage # 以下直接使用包和函数,不用再指向模块就可以了。 mypackage.greet("Python") # Hello, Python! mypackage.add(5, 3) # 8
-
定义公共接口:定义__all__ 的列表内容来决定外部可以访问的成员,具体例子如下:
__all__ = ['greet'] # 只暴露 greet 函数
-
动态导入:你可以在
__init__.py
中使用动态导入技术,根据条件动态导入不同的模块。例如,可能根据 Python 版本或某些配置条件导入不同的模块:# mypackage/__init__.py import sys if sys.version_info[0] == 3: from .module_py3 import * else: from .module_py2 import *
-
维护包级别的变量和状态:
# mypackage/__init__.py config = { "version": "1.0", "author": "John Doe" } def get_config(): return config # 导入包 # main.py import mypackage print(mypackage.get_config()) # 输出: {'version': '1.0', 'author': 'John Doe'}
2.4 包的意义
引入包概念对于大型项目的管理有着非同寻常的意义存在,具体意义如下:
- 对模块文件进行按功能分类保存,可以高效的管理代码,包还有子包使项目结构非常清晰,层次感非常好;
- 各个包有着独立的命名空间,所以各个包之间允许模块和函数同名存在,互不干扰;
- 包结构提高了代码的可维护性和可读性;
- 方便多人协作,并行开发不同的包;
- 便于项目的扩展和功能的增加
2.5 第三方包和虚拟环境
2.5.1 第三方包
使用pip(pip3)pip3是3.0版本以后的使用。pip install 包名,就可以了有的包可以自动从网络上下载,这样是连国外的网站下载比较慢,所以我们可以这样:pip3 install -i https://pypi.tuna.tsinghua.edu.cn/simple 包名,这个使用了清华的网站进行下,下载速度会比较快点。
2.5.2 python虚拟环境
为了项目好管理性建议每个项目都建立个虚拟环境,这样第三方包都可以安装在虚拟环境中,就建立起了隔离性,每个项目之间的包就互不干扰。建立虚拟环境的如下:
1、建立目录(打开终端引导到项目目录下)windows、linux、macos系统都支持
python -m venv myenv # myenv是我们建立的虚拟空间名字,名字任取。
2、激活虚拟环境(也可以称进入虚拟环境):
windows:
myenv\Scripts\activate
linux、macOS:
source venv/bin/activate
在密令提示符的最前端有(myenv)这个标志代表进入了虚拟环境。
3、退出虚拟环境:
deactivate
(十一)、程序异常捕获处理
1、什么是异常
当我们写程序难免遇到报错,专业的称呼叫做异常,行业俗语叫做bug,由于异常的存在程序会被报错停止异常,而对于有些异常并不需要程序停止运行,用户只要能捕获异常进行处理即可。
2、异常捕获语法
2.1 语法
try:
# 待监测的代码(可能会出错的代码)
except Exception as e: # e别名就是系统提示的错误信息
# Exception针对各种常见的错误类型全部统一处理
# 可以指定其他针对性的进行处理(具体错误类型如下表)
else:
# try的子代码正常运行结束没有任何的报错后 再执行else子代码,此段代码可以不需要存在
finally:
# 如果写finally,无论try的子代码是否报错 最后都要执行finally子代码
Exception:是顶级的错误类型,如果需要针对不同的异常做不同的处理,看下面常见的一些错误类型。
异常类型名 | 描述 |
---|---|
NameError | NameError是当某个局部或全局名称未找到时将被引发,也就是指变量名称发生错误,比如用户试图调用一个还未被赋值或初始化的变量时会被触发。 |
SyntaxError | SyntaxError主要是因为当解析器遇到语法错误,比如少个冒号、多个引号之类的,编程时稍微疏忽大意一下就会出错,应该是最常见的一种异常错误了。 |
IndexError | 当序列抽取超出范围时将被引发,也就是索引超出范围,比如最常见下标索引超出了序列边界,比如当某个序列m只有三个元素,却试图访问m[4] |
IOError | 输入/输出异常,主要是无法打开文件 |
OverflowError | 数值运算超出最大限制 |
ZeroDivisionError | 除法运算中除数0 或者 取模运算中模数为0 |
AttributeError | 访问的对象属性不存在 |
ImportError | 无法导入模块或者对象,主要是路径有误或名称错误 |
AttributeError | AttributeError是属性错误,当属性引用或赋值失败时就会出现。比如列表有index方法,而字典却没有,所以对一个字典对象调用该方法就会引发该异常。 |
SystemError | 当解释器发现内部错误,但情况看起来尚未严重到要放弃所有希望时将被引发。 关联的值是一个指明发生了什么问题的字符串(表示为低层级的符号)。 |
ValueError | 当操作或函数接收到具有正确类型但值不适合的参数,也就是值错误,比如想获取一个列表中某个不存在值的索引。 |
如果不知道具体异常类型时可以使用顶级异常(Exception)替代,它可以捕获所有异常。
3、异常的传递性
异常是可以传递的怎么理解呢?嵌套调用或者函数嵌套时,无论多少层从里到外的错误都可以被最外层捕获到异常信息。
4、主动抛出异常
在编写函数的过程可能会对一些值进行判断,不符合项目意思的判断需要告知用户,让用户做出调整,就需要我们抛出异常。例如:在做用户登录校验时,我们对密码进行判断,如果不正确我们就要抛出异常让用知道错误是什么。
语法:
raise 异常类型名称(‘异常描述’)
例子:
try:
if len(self._node_list) == 0:
raise ValueError('Wrong parameter format') # 抛出异常,并把错误信息告知用户。
except ValueError as e:
print(e)
注意:raise抛出了异常如果没有使用try……except进行捕获处理,系统将抛出异常信息并终止程序的运行。
(十二)、面向对象编程
什么是对象?对象是现在世界中具体实物的抽象概念,在python中的所有东西几乎都是对象,比如:所有的数据类型(int、str、bool、list、tuple……)、模块、包、函数、变量等都是一个对象,生活中,这个世界的每样东西都是对象,比如你也是一个对象。
对象是类的实例,对象可以具有属性(特性)和方法(行为),并且对象是通过类创建。
举例:
- 小汽车可以看做是一个类,它具有属性(品牌、颜色、动力、序列号)和行为(转向、刹车、加油)。
- 一台红色序列号为xxx的奥迪A4车就是一个对象,它具有小汽车类的所有成员,这台车可以认为它就是按小汽车这类的标准属性和行为而建出来了一个具体的实物。
1、类的介绍
1.1 类定义及类实例化
从上文大家已对对象也有所了解,上文在谈及对象时涉及到类,这里就来了解下类的概念以及类的实例化。
类的概念:类是创建对象的模板或蓝图,它定义了对象的属性和方法,它描述了这些对象共同的特性和行为。类的每个实例(对象)都继承了类的属性和方法,并可以独立操作。
类实例化:就是以类为模版创建一个具体实例的对象叫实例对象(即对象),对象拥有类的所有特性和行为即属性和方法,如上文的例子:小汽车是类,序列号xxxx的奥迪A4是真实存在一辆车,它就是小汽车的实例对象也可以交对象。
1.1.1 类和对象的关系
- 类是对象的模板,而对象是类的具体实例。类定义了对象的属性和方法,但对象是具体的、可操作的。
- 一个类可以创建多个对象,每个对象都是类的不同实例。
- 类的定义包含了所有对象的共同特性和行为,而每个对象有其独立的状态(即属性的值可以不同)。
1.1.2 类定义和实例化语法例子
# 类的定义
class Car: # 有的写法car后跟(object),两者写法一样,后者是继承基类的写法,不写也是继承基类
# 构造方法,用于初始化属性
def __init__(self, make, model):
self.make = make # 实例属性
self.model = model
# 类的方法
def start(self):
print(f"The {self.make} {self.model} is starting.")
# 创建类的实例(对象),如果构造方法形参除self外有其他时,实例化一个类时务必带入实参,否则报错。
mycar1 = Car("Toyota", "Camry") # car是类,mycar1就是实例(对象)
mycar1.start()
mycar2 = Car("AuDi", "A4") # car是类,mycar2就是实例(对象)
mycar2.start()
从上面例子来了解类和对象的关系
- 类定义的属性和方法都能被其对象所用(属性make和model,方法:start)
- 上例子中类car创建了两个实例mycar1,mycar2,说明一个类可以创建多个对象,每个对象都可以对属性进行定义和执行方法,两个对象互不干扰。
- 在创建两个对象实例的时候给与属性的定义是不一样了,比如mycar1的make是Toyota,而mycar2则是AuDi,它们拥有了类的属性make,但值可以是不相同的。
- 实例化一个类时,如果构造方法存在self以外的形参时,务必带入实参,否则报错。
1.2 类成员和对象成员
类成员:是与类本身绑定的成员,通常包括类变量(常叫类属性)和类方法,这些成员属于类本身,而不是属于类的具体实例(对象)。类成员在类的所有实例之间是共享的。
对象成员:是与具体实例(对象)绑定的成员,通常包括实例变量(实例属性或对象属性)和实例方法。每个对象都有自己独立的对象成员,不同对象的成员之间是互相独立的。
1.2.1 类变量、对象变量及局部变量
class Car:
wheels = 4 # 类变量
def __init__(self, make, model):
Car.wheels=5 # 对类变量进行赋值,访问类成员必须使用类名.成员
self.make = make # 实例变量(也可以叫对象变量和对象属性、实例属性)
self.model = model # 实例变量
engine = "V8" # 不使用self,局部变量
# 类成员可以通过类名访问
print(Car.wheels) # 输出: 4
# 类变量对所有实例是共享的
car1 = Car("Toyota", "Camry")
car2 = Car("Honda", "Civic")
print(Car.wheels) # 输出: 5,在两次实例化中都对类变量进行修改5
Car.wheels = 6 # 对类变量改6,下面两个对象输出类变量也都是6,说明类变量对对象的共享性
print(car1.wheels) # 输出: 6
print(car2.wheels) # 输出: 6
# 访问实例变量,实例变量在不同对象中的值可以是不同的。
print(car1.make) # 输出: Toyota
print(car2.make) # 输出: Honda
# print(car1.engine) 不可以使用,局部变量不能在定义的方法外使用。
请查看例子中的代码、运行结果以及备注,我们来了解类变量和对象变量:
- 类变量:常称为类属性是直接在类中定义的变量,通常定义在方法以外及类以内的变量,如上例子;它们是类层级的变量,不属于某个对象,而是属于整个类。所有实例(对象)共享同一个类变量。
- 实例变量:常称为对象属性或实例属性,是在构造方法(
__init__
)或其他方法中使用self
来定义的变量,它们属于具体的对象,每个对象的实例变量是独立的。 - 类的局部变量:在类中的方法中不使用
self
来定义的变量,局部变量的作用域只限于定义它的方法内,在其他地方都不能被使用。 - self的作用:
self
是一个对当前实例(对象)的引用,它是类的方法的第一个参数(无论是构造方法__init__
,还是其他方法),用于访问该实例的属性和方法;self
是代表当前实例(对象),由self
定义的变量是实例变量在实例内的任何方法中是共享使用的。
1.2.2 类方法和对象方法
class Car:
wheels = 4 # 类变量
# 构造方法
def __init__(self, make, model):
self.make = make # 实例变量
self.model = model # 实例变量
# 实例方法
def display_info(self):
print(f"Make: {self.make}, Model: {self.model}")
# 定义类方法
@classmethod
def set_wheels(cls, number):
cls.wheels = number # 修改类变量
# 通过类方法修改类变量
Car.set_wheels(6)
print(Car.wheels) # 输出: 6
car1 = Car("Toyota", "Camry")
car1.display_info() # 输出: Make: Toyota, Model: Camry
请看上面例子的代码和注释来了解类方法和对象方法:
- 类方法:是绑定到类本身的方法,而不是绑定到对象。它可以通过类名调用(类名.方法()),使用
@classmethod
装饰,并且第一个参数必须是cls
,用于表示类。 - 对象方法:即实例方法,是与对象绑定的方法,必须通过对象调用,并且第一个参数是
self
,表示对象本身。实例方法可以访问和修改对象的实例变量。
1.2.3 静态方法
静态方法使用@staticmethod
装饰,它不需要cls
或self
参数,也就是说它既不与类实例绑定,也不与类本身绑定。静态方法可以用于执行与类或对象无关的操作。
class MyClass:
@staticmethod
def my_static_method(arg1, arg2):
# 静态方法的逻辑
return arg1 + arg2
静态方法特点:不需要访问类或实例的状态:它只处理传入的参数,不访问类属性或实例属性,通常用于逻辑功能的实现,和类或实例的状态无关。
访问方式:可以通过类名或实例来调用静态方法,但它不访问类或实例的任何信息,具体例子如下:
# 调用方式 1: 通过类名调用
result = MyClass.my_static_method(3, 5)
# 调用方式 2: 通过实例对象调用
my_instance = MyClass()
result = my_instance.my_static_method(3, 5)
1.2.4 方法的链式调用
链式调用在Python中是一种编程风格,允许你在一条语句中连续调用多个方法,要实现链式调用,方法需要在结束时返回self
,这样可以在同一对象上连续调用多个方法。
class MyClass:
def method_one(self):
print("Method one called")
return self # 返回当前对象以便进行链式调用
def method_two(self):
print("Method two called")
return self # 返回当前对象以便进行链式调用
# 链式调用
obj = MyClass()
obj.method_one().method_two() # 输出: Method one called
# Method two called
1.2.5 类和对象的成员总结
类成员和对象成员对比:
类型 | 类成员 | 对象成员 |
---|---|---|
归属 | 属于类本身,所有实例共享 | 属于类的实例,每个实例有自己独立的成员 |
包括 | 类变量、类方法、静态方法 | 实例变量、实例方法 |
定义位置 | 类体内部,类方法和静态方法使用装饰器定义 | 通常定义在__init__ 或其他实例方法中,通过self |
访问方式 | 通过类名访问,类方法通过cls 引用 | 通过实例访问,实例方法通过self 引用 |
共享性 | 所有对象共享类成员 | 每个对象拥有自己独立的对象成员 |
总结:
- 类成员:类变量、类方法、静态方法,属于类本身,所有实例共享。
- 对象成员:实例变量、实例方法,属于类的实例,每个实例拥有自己独立的成员。
1.3 构造方法和魔术方法
在Python中,构造方法和魔术方法(也称为特殊方法)是实现类的一些特殊行为的函数,它们由双下划线包围(如__init__
、__str__
等),并且具有特定的功能。这些方法通常是由Python内部机制自动调用的,而不是直接调用。
1.3.1 构造方法概念和定义
构造方法是Python中的一种特殊方法,它在实例化对象时自动调用,用于初始化对象的属性。它通常用来为对象设定初始状态(如属性的值)。在Python中,构造方法的名称是__init__
。
构造函数在创建实例的时候构造函数就会被系统自动调用,一般构造函数用来初始化实例对象,具体就看下面例子。
python需要使用构造函数__init__
来初始化实例变量就需要对它进行重写,具体如下:
def __init__(self,*args, **kwargs):
重写说明:
self
:必不可少,它代表一个实例对象。
*args, **kwargs
:各类形式形参,这些形参可以认为就是初始化实例的参数。
具体实例变量的初始化如下例子:
class MyClass:
def __init__(self, name, age): # 这就是定义了一个构造方法
self.name = name # 实例变量
self.age = age # 实例变量
print(f"Object created for {self.name}, age {self.age}")
# 创建对象时自动调用构造方法
obj = MyClass("Alice", 30) # 输出: Object created for Alice, age 30
1.3.2 构造函数的深入理解
其实python的构造函数不止一个__init__
函数,它还有个函数是__new__
,其实创建实例对象时系统自动调用的是__new__
,由它来创建实例后才调用了__init__
并返回一个实例对象,所以__init__
函数通常被用作实例变量的初始化。
__new__
函数的解读:
该函数是一个特殊的静态方法,语法定义如下:
def __new__(cls,*args, **kwargs)
语法说明:
cls
:它是特殊的静态方法所以它可以带入cls参数代表类。
*args, **kwargs
:什么参数都可以带入。
重写注意事项:
- 由于它是特殊静态方法,所以在重写它时无需使用装饰器@classmethod。
- 重写过程务必调用
self=super().__new__(cls)
来创建实例,self就是一个对象;无须写调用__init__
系统会自动调。 - 创建实例对象self后需要被返回。
- 如果同时重写
__new__
和__init__
除了第一个形参cls和self不一样,其他形参都要一样。
重写例子:
class MyClass:
def __new__(cls,name):
print(f"this is new funciton:{name}")
self=super().__new__(cls) # 创建实例对象
return self
def __init__(self,name):
print(f"this is init function {name}")
def myprint(self):
return "this is myprint"
__str__=myprint # 相当于定义了一个__str__魔法函数
if __name__ == "__main__":
myclass=MyClass("dave")
print(myclass)
输出:
this is new funciton:dave
this is init function dave
this is myprint
例子说明:从例子可以看到new是在init前执行。
1.3.2 魔术方法
魔术方法:是Python提供的一组内置的、具有特定用途的方法,它们通常用于定义对象的某些行为(如字符串表示、数学运算、比较运算等)。这些方法通过特殊的名称以双下划线包围,Python会在特定的情况下自动调用这些方法。
常用的的魔术方法有:
__str__(self)
:定义对象的字符串表示。当使用print()
或str()
函数输出对象时,会调用该方法。__repr__(self)
:定义对象的“官方”字符串表示,通常用于调试,调用repr()
或在交互式解释器中直接输入对象时,会使用这个方法。__len__(self)
:定义对象的长度,配合len()
函数使用。__getitem__(self, key)
:允许对象像字典或列表一样通过索引访问元素。__setitem__(self, key, value)
:允许对象像字典一样设置键值对。__delitem__(self, key)
:允许对象删除元素。__call__(self, \*args, \**kwargs)
:允许对象像函数一样被调用,使用它重写函数,实例对象名作为方法名来执行它。__eq__(self, other)
:定义对象的相等比较操作,配合==
操作符使用。__add__(self, other)
:定义加法运算,配合+
操作符使用。
魔术方法的使用例子:
class MyClass:
def __init__(self, name):
self.name = name
def __str__(self):
return f"MyClass object (name: {self.name})"
def __repr__(self):
return f"MyClass('{self.name}')"
def __call__(self,str): #让类名作为函数名替代__call__方法。
print(f"我输出的是:{str}")
# 实例化对象
obj = MyClass("Python")
# 使用对象名来调用__call__方法
obj("hello world!") # 输出:我输出的是:hello world!
# 输出对象的字符串表示
print(str(obj)) # 输出: MyClass object (name: Python)
# 输出对象的"官方"表示(例如在交互式解释器中)
print(repr(obj)) # 输出: MyClass('Python')
重载__add__
魔术方法
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
return Point(self.x + other.x, self.y + other.y)
def __repr__(self):
return f"Point({self.x}, {self.y})"
# 创建两个 Point 对象
p1 = Point(1, 2)
p2 = Point(3, 4)
# 使用 + 运算符,它会调用 __add__ 方法
p3 = p1 + p2
print(p3) # 输出: Point(4, 6)
2、类封装
2.1 封装概念
封装:是面向对象编程中的一个重要概念。它通过将对象的属性和方法封装在类内部,限制对这些数据的直接访问,从而实现对数据的保护和隐藏。封装的目的是为了提高程序的安全性和可维护性,同时隐藏类的实现细节,提供简洁的接口。
在Python中,封装通过访问控制来实现,可以将类的属性和方法分为公有(public)、私有(private)和受保护(protected)。虽然Python没有像其他语言(如Java、C#)那样严格的访问权限控制,但可以通过一些命名约定实现类似效果。
2.1.1 公有(public)
在Python中,所有默认定义的类属性和方法都是公有的,可以在类的外部直接访问和修改。
2.1.2 受保护(protected)
受保护属性和方法通过单个下划线 _
作为前缀来表示。这是一种约定,表明这些属性和方法应该仅在类及其子类中使用,但外部仍然可以访问(Python 并不强制限制访问,只是约定俗成的保护机制)。
class Person:
def __init__(self, name, age):
self._name = name # 受保护属性
self._age = age # 受保护属性
def _greet(self): # 受保护方法
print(f"Hello, my name is {self._name} and I am {self._age} years old.")
person = Person("Alice", 30)
person._greet() # 外部仍然可以访问受保护方法(但不推荐)
2.1.3 私有(private)
私有属性和方法通过双下划线 __
作为前缀来表示。这会触发Python的**名称改写(name mangling)**机制,防止这些属性和方法在类外部被直接访问,从而实现更强的封装。
class Person:
def __init__(self, name, age):
self.__name = name # 私有属性,执行的时候会被系统改名为_Person__name,让你无法访问。
self.__age = age # 私有属性
def __greet(self): # 私有方法
print(f"Hello, my name is {self.__name} and I am {self.__age} years old.")
def public_greet(self): # 公有方法,调用私有方法
self.__greet()
person = Person("Alice", 30)
# person.__greet() # 这将会报错,无法直接访问私有方法
# print(person.__name) # 这将会报错,无法直接访问私有属性
person.public_greet() # 调用公有方法,通过其间接访问私有方法
2.1.4 python封装访问控制权限
访问控制 | 类内是否可以访问 | 是否可以被继承 | 类外是否可以访问 | 类对象是否可以访问 | 实例对象是否可以访问 |
---|---|---|---|---|---|
公有(public | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
保护(protected) | ✔️ | ✔️ | ❌ | ✔️ | ✔️ |
私有(private | ✔️ | ❌ | ❌ | ❌ | ❌ |
注意:python的访问控制很多时候是个约定,系统不做强制控制,除私有变相改名来控制外。
2.2 封装的好处
- 数据隐藏:封装可以防止对象的属性被外部代码意外修改,从而保护数据的完整性。
- 安全性:通过限制直接访问对象的内部数据,可以提高程序的安全性。
- 灵活性:通过公开控制的接口(如getter和setter方法),可以灵活控制数据的读取和修改过程。
- 可维护性:封装隐藏了类的实现细节,只暴露必要的接口,这使得代码更容易维护和扩展。
2.3 访问私有属性和方法的常用方式(getter和setter)
通常,私有属性通过getter和setter方法来访问和修改。这种方式可以有效控制对私有属性的读取和写入。这个和c#的属性使用get和set的方法一致。
通过getter
和setter
方法,你可以在访问或修改属性时加入逻辑,比如对值进行验证或转换。如下例子:
class Person:
def __init__(self, name, age):
self.__name = name # 私有属性
self.__age = age # 私有属性
# Getter 方法
def get_name(self):
return self.__name
def get_age(self):
return self.__age
# Setter 方法
def set_name(self, name):
self.__name = name
def set_age(self, age):
if age > 0:
self.__age = age
else:
raise ValueError("Age must be positive")
person = Person("Alice", 30)
# 使用 getter 访问私有属性
print(person.get_name()) # 输出: Alice
print(person.get_age()) # 输出: 30
# 使用 setter 修改私有属性
person.set_age(35)
print(person.get_age()) # 输出: 35
2.4 使用@property装饰器实现getter和setter
Python 提供了 @property
装饰器,使得可以将访问方法像访问属性一样调用,从而实现更简洁的代码。
class Person:
def __init__(self, name, age):
self.__name = name # 私有属性
self.__age = age # 私有属性
@property
def name(self):
return self.__name
@name.setter # 设置私有变量时的装饰器不一样
def name(self, value):
self.__name = value
@property
def age(self):
return self.__age
@age.setter
def age(self, value):
if value > 0:
self.__age = value
else:
raise ValueError("Age must be positive")
person = Person("Alice", 30)
# 使用属性访问
print(person.name) # 输出: Alice
print(person.age) # 输出: 30
# 修改属性值
person.age = 35
print(person.age) # 输出: 35
从上例可以看到这个就有点像c# public string name{get,set}的功能。
2.3和2.4的区别和二者在开发中的作用:
- 在调用的时候2.3是调用方式的方式调用,2.4就像是调用属性的方式调用。
- 经常开发中都会把对象变量(属性)设置成私有,只能类中被访问,对象可以对对象变量进行赋值,对象变量会进行数据逻辑处理
3、类继承
3.1 继承概念和定义
继承(Inheritance) 是面向对象编程(OOP)中的一个重要特性。它允许一个类(子类/派生类)从另一个类(父类/基类)获取属性和方法。通过继承,子类可以重用父类中的代码,减少重复,并且可以根据需要扩展或复写父类的行为。
继承可以分为单继承和多继承,单继承即是父类只有一个,多继承是父类有两个或以上的。
如下单继承的定义示例:
class Parent: # 父类
def __init__(self, name):
self.name = name
def greet(self):
print(f"Hello, I am {self.name}")
# 重写父类的方法时会覆盖原来的方法中的逻辑,如果要调用父类方法可以使用super().方法
class Child(Parent): # 子类继承自父类
def __init__(self, name, age): # 重写构造方法
# 调用父类的构造函数
super().__init__(name)
self.age = age
def greet(self): # 重写父类方法
print(f"Hello, I am {self.name}, and I am {self.age} years old")
Python 支持多重继承,即一个类可以继承多个父类。这种情况下,子类将拥有所有父类的属性和方法。具体定义如下示例:
class Walkable:
def walk(self):
print("Walking...")
class Talkable:
def talk(self):
print("Talking...")
class Robot(Walkable, Talkable): # 多重继承
pass
robot = Robot()
robot.walk() # 调用 Walkable 的方法
robot.talk() # 调用 Talkable 的方法
3.2 继承的关键概念
3.2.1 super()函数
super()
是用来调用父类的方法或构造函数的。- 在子类中可以使用
super()
调用父类的方法,确保父类的初始化逻辑得到执行。 - 它在多重继承时尤其重要,因为它会按照类的继承顺序依次调用父类的方法。
3.2.2 方法解析顺序
- 当一个类从多个父类继承时,Python 会按照一种特定的顺序查找方法。这种顺序由
MRO
决定,可以通过类名.mro()
查看。 - Python 使用C3线性化算法来确定调用的顺序。
class A:
def method(self):
print("A's method")
class B(A):
def method(self):
print("B's method")
class C(A):
def method(self):
print("C's method")
class D(B, C):
pass
d = D()
d.method()
print(D.mro()) # 查看MRO顺序
输出结果:
B's method
[<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]
从 mro()
输出可以看出,方法查找顺序是 D -> B -> C -> A -> object
,因此首先调用了 B
类的 method()
方法。
从上面例子可以看出多继承时,继承的顺序是从左到右的过程。
3.2.3 继承的优缺点
有点:
- 提高代码的复用性,避免重复代码。
- 通过继承可以方便地扩展类的功能。
- 提供了代码的层次化结构,增强了代码的可维护性。
缺点:
- 过度使用继承可能导致代码过于复杂,尤其是在使用多重继承时。
- 继承会带来强耦合,子类和父类之间的联系变得紧密,修改父类的代码可能会影响子类的行为。
4、类的抽象
4.1 抽象的概念和定义
4.1.1 抽象概念
抽象是面向对象编程(OOP)中的一个核心概念。抽象的主要目的是隐藏复杂的实现细节,只向用户暴露必要的接口或功能,使得对象的使用更加简单、清晰。通过抽象,程序员能够专注于“做什么”,而不是“怎么做”。在 Python 中,抽象通过定义抽象类和抽象方法来实现。
4.1.2 抽象的定义
在 Python 中,抽象是通过 abc
模块(Abstract Base Class,抽象基类)来实现的。通过这个模块,你可以定义抽象类,并使用 抽象方法 强制子类实现某些方法。
- 抽象类是不能被实例化的类。它通常用于定义接口,而具体的实现由子类提供。
- 抽象类中的抽象方法没有具体实现,子类必须实现这些方法,。
- Python 中的抽象类使用
abc.ABC
作为基类,抽象方法则通过@abstractmethod
装饰器来声明。
4.1.3 抽象类的语法示例
from abc import ABC, abstractmethod
# 定义抽象类
class PaymentMethod(ABC):
# 定义个抽象方法
@abstractmethod
def pay(self, amount): # 由于这个抽象对象必须被复写,所以可以看作接口
pass
# 具体的支付方式:信用卡
class CreditCardPayment(PaymentMethod):
def pay(self, amount): # 抽象方法必须复写
print(f"Paying {amount} using credit card.")
# 具体的支付方式:PayPal
class PayPalPayment(PaymentMethod):
def pay(self, amount): # 抽象方法必须复写
print(f"Paying {amount} using PayPal.")
# 使用具体的支付方式
def process_payment(payment_method: PaymentMethod, amount: float):
payment_method.pay(amount) # 信用卡和PayPal都要有支付功能,所以继承抽象类,实现统一的支付接口,来满足简化代码
# 实例化具体支付方式并支付
credit_card_payment = CreditCardPayment()
paypal_payment = PayPalPayment()
process_payment(credit_card_payment, 100.0) # 输出: Paying 100.0 using credit card.
process_payment(paypal_payment, 200.0) # 输出: Paying 200.0 using PayPal.
从上例,信用卡和paypal都是一种支付方,它们都有自己的支付逻辑,但是他们都要有相同的动作支付功能,所以定义各个抽象类PaymentMethod作为模板类实现统一接口(抽象方法)pay()的动作。
4.1.4 抽象类的好处
- 提供统一的接口:抽象类提供了一种确保所有子类都实现相同方法的机制,保证了代码的一致性。
- 提高代码的可扩展性:当需要添加新的功能或新的子类时,只需要继承抽象类并实现必要的方法,而不需要修改现有的代码。
- 减少代码重复:抽象类可以定义一些通用的行为,子类可以继承这些行为,避免重复编写相同的代码。
- 增强代码的可维护性:抽象类分离了接口和具体实现,当需求变化时,只需要修改具体的实现,而不需要影响其他部分。
总结:
- 抽象类 主要用于定义通用接口或模板,子类必须实现抽象类中的抽象方法。
- 抽象类在多个类有相似行为但具体实现不同的场景下非常有用,可以强制子类实现特定的方法,保证接口一致性。
- 使用抽象类可以让代码更加模块化、易于扩展和维护。
5、类的多态
5.1 多态的概念和定义
是面向对象编程(OOP)的核心概念之一。多态允许不同类的对象通过相同的接口调用不同的实现方法,这提高了代码的灵活性和可扩展性。在 Python 中,多态性通过类的继承和方法重写来实现,体现为:同一接口,表现不同。
举个例子:鸭子、鸟、猫它们都会叫,只不过叫的方式不一样,发出的声音不一样,但它们都有个行为会叫,用面向对象编程的逻辑来看,这些动物都有一个名字为“叫”的方法,方法内具体怎么叫每个动物都不一样。名字“叫”的这个方法就是相同的接口,方法内不同的动物不同的叫法就是方法的不同实现。
5.1.1 方法重写实现多态
不同的子类可以继承父类的同一个方法,并根据需要重写该方法。当子类的实例被调用时,会执行子类的实现,而不是父类的。
class Animal:
def speak(self):
pass
class Dog(Animal):
def speak(self):
return "Woof!"
class Cat(Animal):
def speak(self):
return "Meow!"
def animal_sound(animal): # 使用多态的函数
print(animal.speak())
# 实例化不同的动物
dog = Dog()
cat = Cat()
# 调用同一个函数,但根据对象的类型输出不同的结果
animal_sound(dog) # 输出: Woof!
animal_sound(cat) # 输出: Meow!
在这个例子中,Dog
和 Cat
类都继承了 Animal
类,并且都重写了 speak()
方法。当我们调用 animal_sound()
函数时,它能根据传入对象的类型执行不同的 speak()
方法。这就是多态的典型应用。
5.1.2 接口抽象类实现多态
通过定义一个抽象类,可以强制子类实现该抽象类的特定方法,从而实现接口标准化。Python 的 abc
模块允许我们定义抽象类和抽象方法。
from abc import ABC, abstractmethod
class Animal(ABC):
@abstractmethod
def speak(self):
pass
class Dog(Animal):
def speak(self):
return "Woof!"
class Cat(Animal):
def speak(self):
return "Meow!"
def animal_sound(animal):
print(animal.speak())
dog = Dog()
cat = Cat()
animal_sound(dog) # 输出: Woof!
animal_sound(cat) # 输出: Meow!
在这个例子中,Animal
类是一个抽象类,定义了抽象方法 speak()
,所有继承 Animal
的类必须实现 speak()
方法。我们可以创建不同的子类如 Dog
和 Cat
,每个子类都有自己不同的 speak()
实现。
5.2 多态关键概念
- 统一接口:多态的核心思想是通过一个统一的接口来操作不同的类。这种接口在父类或抽象类中定义,而子类负责实现自己的逻辑。
- 方法重写(Overriding):子类可以重写父类的方法,并在实际操作时根据对象的类型调用相应的实现。
- 灵活性:多态允许在不修改现有代码的情况下扩展功能。如果我们需要添加新类型的形状(比如三角形),只需添加一个新的子类,而不需要修改
print_area()
函数。 - 多态和鸭子类型:Python 中的多态性不一定依赖于继承,任何对象只要实现了需要的方法,就可以被认为是符合这个接口的对象。这种现象称为鸭子类型。举例:多个类不一定要去继承,只要他们都实现符合接口规范的方法,它们就实现了多态。
class Shape:
def area(self):
pass
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14 * self.radius ** 2
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
def print_area(shape): # 多态函数
print(f"Area: {shape.area()}")
circle = Circle(5)
rectangle = Rectangle(4, 6)
print_area(circle) # 输出: Area: 78.5
print_area(rectangle) # 输出: Area: 24
6、组合和聚合
在面向对象编程中,组合(Composition)和聚合(Aggregation)都是类之间的一种关联关系,它们用于表示一个对象包含另一个对象。它们帮助我们建立复杂的对象结构,从而促进代码的重用、模块化和解耦。虽然组合和聚合的概念相似,但它们在对象的生命周期管理上有所不同。
6.1 组合和聚合的定义和功
定义:组合和聚合是对象之间的关系。通过组合,一个类可以包含另一个类的实例,表示"有一个"的关系。组合表示强关系,聚合表示弱关系。
功能:通过组合和聚合,类可以重用其他类的功能,而不需要通过继承。
6.1.1 组合
组合 是一种强关系,表示一个类完全控制了另一个类的生命周期。如果容器对象(包含对象的类)被销毁,那么其中包含的对象也会随之销毁。也就是说,包含的对象是容器对象不可分割的一部分,没有独立的存在。
class Engine:
def __init__(self, horsepower):
self.horsepower = horsepower
def start(self):
print("Engine started with horsepower:", self.horsepower)
class Car:
def __init__(self, make, model, horsepower):
self.make = make
self.model = model
# Car 拥有 Engine 的对象
self.engine = Engine(horsepower) # 组合关系,Car 负责创建和管理 Engine 对象
def drive(self):
print(f"Driving {self.make} {self.model}")
self.engine.start()
# 创建 Car 对象时会自动创建 Engine 对象
my_car = Car("Toyota", "Corolla", 120)
my_car.drive()
在这个例子中,Car
包含了 Engine
对象。组合的特点是 Car
对象完全控制了 Engine
对象的生命周期,当 Car
对象被销毁时,Engine
也随之销毁。Engine
是 Car
不可分割的一部分。
6.1.2 聚合
聚合 是一种较为松散的关联关系,它表示一个对象包含另一个对象,但两个对象可以独立存在。也就是说,包含的对象(部分)不依赖于容器对象的生命周期,容器对象和被包含的对象可以独立地被销毁。
class Coach: # 教练
def __init__(self, name):
self.name = name
def guide_team(self):
print(f"Coach {self.name} is guiding the team.")
class Team:
def __init__(self, name, coach):
self.name = name
self.coach = coach # 聚合关系,Team 只是使用 Coach 对象,不管理其生命周期
def start_training(self):
print(f"Team {self.name} starts training.")
self.coach.guide_team()
# 创建 Coach 对象
coach = Coach("John")
# 创建 Team 对象,传入 Coach 对象
team = Team("Warriors", coach)
team.start_training()
# Coach 对象和 Team 对象是独立的
del team # 删除 Team 对象时,Coach 对象仍然存在
coach.guide_team() # 仍然可以使用 Coach 对象
在这个例子中,Team
包含了一个 Coach
对象,但 Coach
对象的生命周期不依赖于 Team
。即使 Team
被销毁,Coach
仍然可以独立存在和使用。这是聚合的特点。
6.2 聚合和组合的对比
特点 | 组合 (Composition) | 聚合 (Aggregation) |
---|---|---|
关系强度 | 强,表示 “拥有” 关系。 | 弱,表示 “包含” 关系。 |
生命周期 | 容器对象销毁时,包含对象也会被销毁。 | 容器对象销毁时,包含对象不会被销毁。 |
依赖性 | 组成部分依赖于容器对象,无法独立存在。 | 组成部分独立于容器对象,可以独立存在。 |
示例 | 汽车和引擎,房子和房间。 | 大学和教授,球队和教练。 |
6.3 聚合和组合的使用场景
- 使用组合:当两个对象是不可分割的整体时,比如一个对象的组成部分在逻辑上依赖于容器对象的存在。例如,
Car
和Engine
,如果Car
不存在,Engine
也没有存在的意义。 - 使用聚合:当两个对象可以独立存在,且关系较为松散时,比如一个对象可以包含另一个对象,但另一个对象可以独立存在。例如,
University
和Professor
,大学和教授之间的关系是松散的,即使大学不存在,教授依然可以在别处工作。
7、类型注解
在 Python 中,类型注解(Type Hinting)是一种用来为代码中的变量、函数参数、返回值等提供类型提示的机制。虽然 Python 是动态类型语言,不会在运行时强制类型检查,但类型注解可以提高代码的可读性、帮助 IDE 和类型检查工具(如 mypy
)进行静态分析,从而提早发现类型错误。
虽然python作为动态类型语言并没有要求声明类型,但在开发的实际过程中变量和参数的处理对于程序员来说还是会划定了数据类型,所以使用注解让能提高的程序代码可读性。
类型注解可以用于:
- 变量
- 函数参数和返回值
- 类属性
- 集合类型(如列表、字典等)
- 复杂类型(如联合类型、可选类型、泛型等)
7.1 基本类型注解
# 整数类型
x: int = 10
# 字符串类型
name: str = "John"
# 浮点数类型
pi: float = 3.14
# 布尔类型
is_active: bool = True
7.2 函数参数和返回值注解
def greet(name: str) -> str:
return f"Hello, {name}!"
print(greet("Alice")) # 输出: Hello, Alice!
7.3 数据容器类型注解
from typing import List
from typing import Tuple
from typing import Dict
# 声明一个包含整数的列表
numbers: List[int] = [1, 2, 3, 4]
# 一个包含整数和字符串的元组,表示元组的第一个元素是字符串类型,第二个元素是整数类型
person: Tuple[str, int] = ("Alice", 30)
# 声明一个键为字符串,值为整数的字典
student_ages: Dict[str, int] = {"Alice": 20, "Bob": 21}
7.4 可选类型注解
from typing import Optional
def get_user_id(user: Optional[str]) -> int:
if user is None:
return -1
return len(user)
print(get_user_id(None)) # 输出: -1
print(get_user_id("Alice")) # 输出: 5
Optional[str]
表示参数 user
要么是字符串,要么是 None
。这是一个常见的情况,尤其是在处理可能为空的参数时。
7.5 联合注解
如果一个变量可以是多种不同的类型,我们可以使用 Union
来进行类型注解。例如,一个函数可能返回整数或字符串。
from typing import Union
# 在这个例子中,Union[int, str] 表示 data 可以是整数或字符串。
def process_data(data: Union[int, str]) -> str:
if isinstance(data, int):
return f"Processed number: {data}"
return f"Processed string: {data}"
print(process_data(10)) # 输出: Processed number: 10
print(process_data("text")) # 输出: Processed string: text
7.6 泛型注解
在某些情况下,我们可能希望定义一个通用的数据结构,它可以适用于任何类型。typing
模块提供了 Generic
来支持这种用法。
from typing import TypeVar, Generic
T = TypeVar('T') # # 定义一个类型变量 T
class Box(Generic[T]):# 这里采用了继承泛型的基类
def __init__(self, content: T):
self.content = content
def get_content(self) -> T:
return self.content
# 创建一个装整数的 Box
box_int = Box
print(box_int.get_content()) # 输出: 123
# 创建一个装字符串的 Box
box_str = Box[str]("Hello")
print(box_str.get_content()) # 输出: Hello
在这个例子中,T
是一个类型变量,表示 Box
可以装载任何类型的数据。我们不再局限于某种特定的数据类型,而是通过 T
实现泛化处理。通过使用 Generic[T]
,我们创建了一个灵活的 Box
类,它可以容纳任何类型的数据。
7.6.1 泛型类的继承
# 继续使用 T 作为类型变量
class GenericBox(Box[T]):
def double_content(self) -> str:
return f"{self.content} {self.content}"
在这里,GenericBox
继承了 Box
,并保持了泛型 T
的特性。
如果你希望子类不再是泛型,而是为其指定一个具体的类型,你可以在继承时直接指定类型。
# 子类指定类型为 int
class IntBox(Box[int]):
def square_content(self) -> int:
return self.content ** 2
7.6.2 泛型函数
不仅类可以使用泛型,函数也可以通过类型变量处理多种类型。假设我们编写一个函数,交换两个变量的值。
from typing import TypeVar
T = TypeVar('T') # 定义一个类型变量 T
def swap(a: T, b: T) -> Tuple[T, T]:
return b, a
7.7 Any类型注解
from typing import Any
def process(value: Any) -> None:
print(f"Processing value: {value}")
(十三)、闭包和装饰器
1、闭包
1.1 闭包概念
基本闭包示例:
def outer_function(message):
def inner_function():
print(f"Message is: {message}")
return inner_function
closure_func = outer_function("Hello, World!") # 外部函数执行完毕
closure_func() # 输出:Message is: Hello, World! # 外部函数已指向完毕,扔能使用到它的变量
如上例子,inner_function
是闭包,它可以访问 outer_function
中的 message
变量,即使 outer_function
已经执行完毕并离开作用域。
闭包保持状态例子:
def counter():
count = 0
def increment():
nonlocal count # 访问外部函数的变量
count += 1
return count
return increment
counter1 = counter()
print(counter1()) # 输出:1
print(counter1()) # 输出:2
counter2 = counter()
print(counter2()) # 输出:1
在这个例子中,counter1
和 counter2
是两个独立的闭包,每个闭包都有自己独立的 count
变量。当你调用 counter1()
或 counter2()
时,闭包保持了各自的状态。
工程函数,闭包常用于生成带有不同参数的函数:
def outer(logo):
def inner(msg): # 闭包特点:内嵌定义一个函数(函数内部定义了另外一个函数)
print(f"<{logo}>{msg}<{logo}>") # 使用到了外部函数的参数log,(依赖外层函数的变量和参数)
return inner # 返回的是内部函数的引用
func_outer=outer('福建')
func_outer('海边人') # 以下两行执行的结果都会依赖在外层函数下,所以程序未停止,外层函数一直在内存中以满足内存函数的使用。
func_outer('山里洞人') # 程序未结束,闭包的外函数将一直被内部函数使用。
输出结果:
<福建>海边人<福建>
<福建>山里洞人<福建>
outer是一个工厂函数,返回了一个闭包。成的闭包分别将 logo 固定为 ‘福建’,所以闭包函数被执行时就带上了logo。
综上三例子来看闭包概念:闭包(Closure)是一种特殊的函数,它可以访问它定义时所在作用域中的变量,即使这些变量的作用域已经结束。在 Python 中,闭包是一种函数,它不仅包含函数本身的逻辑,还包括函数定义时所捕获的自由变量(即未在函数内部定义的变量)。闭包使得函数能够记住其上下文环境。
闭包的成立需要以下满足条件:
- 有一个嵌套函数(内部函数)。
- 嵌套函数引用了外部函数中的变量。
- 外部函数返回了内部函数,而这个内部函数可以在外部函数调用后仍然使用外部函数中的变量。
1.2 闭包的用途
闭包的主要意义在于它允许状态的保持,同时避免使用全局变量或类来维护状态。相比类和全局变量,闭包是一种更加轻量级的方式来创建功能丰富的函数。
闭包的用途:
- 数据封装:闭包可以封装数据,使得某些数据仅在特定函数范围内可访问,类似于对象的私有属性;从例子2看封装count。
- 延迟执行:闭包允许我们创建函数,并在稍后调用时访问当时的环境变量。它适用于工厂函数、回调函数等延迟执行的场景,从两个例子看闭包被创建后,在任何地方都可以执行,可以看作是延迟执行。
- 提高代码的灵活性和复用性:闭包使得我们可以在不同的地方以不同的上下文来调用同一逻辑,避免使用类来保存状态,看例子2就是保存状态。
2、装饰器
2.1 装饰器的概念
Python 装饰器(Decorator)是一种高级函数,用于在不改变原函数代码的前提下,动态地为函数或方法添加额外的功能。装饰器通常用于在函数执行的前后,或者某些特定条件下,插入额外的行为。它使代码更加简洁、模块化、可复用。
装饰器本质上是一个高阶函数,它接收一个函数作为输入,并返回一个增强后的函数。
2.1.1 简单装饰器
装饰器的基本实现方式是定义一个高阶函数,接受另一个函数作为参数,并在这个函数调用前后添加功能。
def simple_decorator(func):
def wrapper():
print("Before the function is called")
func() # 调用原函数
print("After the function is called")
return wrapper
@simple_decorator
def say_hello():
print("Hello, World!")
say_hello()
输出结果:
Before the function is called
Hello, World!
After the function is called
看例子分析:
- 这里的
simple_decorator
接收say_hello
函数作为参数,并返回一个wrapper
函数。wrapper
在say_hello
函数执行前后添加了额外的行为。 @simple_decorator
是装饰器的语法糖,等效于say_hello = simple_decorator(say_hello)
。
2.1.2 带参数的装饰器
有些函数需要参数,装饰器可以通过 *args
和 **kwargs
来处理任意数量的参数。
def decorator_with_args(func):
def wrapper(*args, **kwargs):
print(f"Arguments were: {args}, {kwargs}")
return func(*args, **kwargs)
return wrapper
@decorator_with_args
def greet(name, age):
print(f"Hello, my name is {name} and I am {age} years old.")
greet("Alice", 30)
输出结果:
Arguments were: ('Alice', 30), {}
Hello, my name is Alice and I am 30 years old.
通过使用 *args
和 **kwargs
,装饰器可以处理任意数量的参数,并将它们传递给原函数。
2.1.3 带参数的装饰器工厂
如果你需要为装饰器传递参数,可以使用装饰器工厂,它返回一个装饰器函数。
def decorator_factory(message):
def decorator(func):
def wrapper(*args, **kwargs):
print(f"{message} - Before the function call")
result = func(*args, **kwargs)
print(f"{message} - After the function call")
return result
return wrapper
return decorator
@decorator_factory("LOG")
def say_hello():
print("Hello!")
say_hello()
输出结果:
LOG - Before the function call
Hello!
LOG - After the function call
decorator_factory
接受参数 message
,返回了一个具体的装饰器函数 decorator
,后者可以为不同的函数生成带有定制功能的装饰器。
2.1.4 类方法添加装饰器
装饰器不仅能应用于函数,还能应用于类的方法。
def log_method_call(func):
def wrapper(self, *args, **kwargs):
print(f"Calling method {func.__name__}")
return func(self, *args, **kwargs)
return wrapper
class MyClass:
@log_method_call
def method1(self):
print("Method1 called")
@log_method_call
def method2(self):
print("Method2 called")
obj = MyClass()
obj.method1()
obj.method2()
输出结果:
Calling method method1
Method1 called
Calling method method2
Method2 called
2.2 装饰器的优缺点
优点:
- 代码复用:可以轻松复用相同功能,如日志、权限验证等。
- 代码清晰:装饰器将功能扩展和核心逻辑分开,使代码更易读、更易维护。
- 增强功能:在不修改原函数代码的情况下动态增强其功能。
缺点:
- 调试困难:装饰器会改变函数的行为,调试时可能难以跟踪实际执行的函数。
- 装饰器嵌套时可读性下降:多个装饰器嵌套时,代码的可读性和理解性可能降低。
总结:
- 装饰器的作用是为函数或方法添加额外的功能,而无需修改其代码。
- 应用场景包括日志记录、性能监控、权限验证、输入校验、缓存等。
- 实现方式包括简单装饰器、带参数的装饰器、类方法的装饰器等,能够为代码的增强和复用提供灵活的解决方案。
(十四)、多任务编程
Python 多任务编程是指让程序可以同时执行多个任务,通常用于提高程序的效率和性能。
多任务编程可以通过并行或并发执行多个任务来提高程序的运行效率,特别是在需要处理大量 I/O 或 CPU 密集型任务时。多任务编程的优势体现在以下几个方面:
- 并行处理:可以同时利用多核 CPU 来加快处理速度。
- 并发处理:在 I/O 密集型任务(如网络请求)中,充分利用 CPU 空闲时间。
- 资源利用:提高系统资源(如 CPU 和内存)的利用率。
并发和并行的概念:
并发:是任务数大于CPU核数,通过操作系统任务调度算法,实现多任务一起执行,任务数大于CPU核数时,系统分配最大核数的进程进行执行一小段时间然后暂停给其他任务执行,以此类推直到任务都执行完成;所以一个任务并发时的执行是断断续续的执行,只不过这个切换的时间比较快感观不到而已。
并行:是任务数小于等于CPU核数,每个任务真正意义上的都能被分配上CPU同时一起执行。
1、多进程
进程(Process):是系统资源分配的最小单位,它是操作系统进行资源和调度运行的基本单位
,通俗理解:一个正在运行的程序就是一个进程。
多进程是一种通过在系统中启动多个进程来实现并行处理的方式。每个进程拥有独立的内存空间、数据和资源,这意味着进程之间不会共享内存(除非使用专门的机制,如 multiprocessing
模块中的 Queue
或 Pipe
)。多进程非常适合处理CPU 密集型任务,因为每个进程可以充分利用多核 CPU。
1.1 多进程类的实现
Python 提供了 multiprocessing
模块来实现多进程编程。它的编程方式类似于线程编程。
import multiprocessing # 引入多进程包
import os
def worker(num):
print(f"Worker {num} started, PID: {os.getpid()}")
if __name__ == '__main__':
processes = [] # 创建进程列表,用于保存子进程对象
for i in range(5):
p = multiprocessing.Process(target=worker, args=(i,)) # 实例化一个子进程(创建子进程的这个进程就是主进程)
processes.append(p)
p.start() # 启动子进程
for p in processes: # 主进程会等待每个子进程结束,
p.join() # 执行的必要性,否则主进程结束,子进程未结束,会导致内存泄漏。
输出结果
Worker 0 started, PID: 12345
Worker 1 started, PID: 12346
...
1.1.1 创建子进程(Process实例化)
多进程类multiprocessing.Process实例化即创建一个子进程,下面是类的实例化说明:
process=multiprocessing.Process([group=None, ]target=None[, name=None, args=(), kwargs={}, daemon=None])
- process:返回参数,子进程对象
- target:必填,worker为子进程需要执行的函数。
- Group:选填,通常保持为
None
。该参数用于未来的扩展,目前不被使用。 - Name:选填,指定进程的名称。默认是
Process-N
的格式(如Process-1
,Process-2
)。可以手动为进程命名,便于调试和跟踪。 - Args、kwargs:选填,提供参数给target的子进程函数的实参。
- daemon:选填,是否为守护进程。默认值是False,设置成True后,主进程不会等待子进程,也就是时候主进程结束也会把未结束的子进程结束掉。
1.1.2 Process类常方法和属性
multiprocessing.Process类的常用对象方法如下:
- start():启动进程。当调用
start()
时,进程开始运行,并调用run()
方法(默认调用target
函数)。你不能多次调用start()
,只能启动一次。 - run():在改方法内主要是执行target赋值的函数,正常情况下不怎么使用,除非继承process类进行重写方法。
- join([timeout]):阻塞主进程,直到子进程结束。可选参数
timeout
允许设定超时时间。若子进程在指定的timeout
内未结束,则主进程继续执行。如果timeout
为None
,它将无限期等待。 - terminate():强制终止进程。这是立即杀死进程的方法,而不管进程的状态如何。因此,进程中的任何数据可能会丢失或损坏。
- bool=is_alive():检查进程是否仍然存活。返回
True
表示进程正在运行,False
表示进程已结束。
multiprocessing.Process类的常用对象属性如下:
属性 | 描述 |
---|---|
daemon | 进程守护,默认值是False,设置成True后,主进程不会等待子进程,也就是时候主进程结束也会把未结束的子进程结束掉。 |
pid | 得到进程pid(编号) |
name | 得到进程名 |
exitcode | exitcode:进程在运行时为None、如果为–N,表示被信号N结束(了解即可) |
1.1.3 使用多进程类的业务场景
multiprocessing.Process
类适合以下场景:
- 任务数较少且简单:当你需要创建几个独立的进程并手动管理它们时,
Process
类非常适合。 - 需要对每个进程进行细粒度控制:如果你想对进程的生命周期(启动、等待、终止)进行精细控制,
Process
类提供了灵活的接口。
1.2 进程池
使用进程池的目的就是实现多进程多任务进行并发或并行运行的一种方式,进程池相对多进程类的业务场景而言如下:
- 任务数量很多且类似:当你需要处理大量相似的任务(如批量数据处理、I/O 密集型任务)时,进程池能够高效管理这些任务。
- 无需精细控制每个进程:进程池自动管理进程的创建、执行和销毁,不需要手动控制进程的生命周期。
- 资源管理:进程池会限制同时运行的进程数量,避免消耗过多的系统资源。
进程池内部维护一个进程序列,当使用时,则去进程池中获取一个进程,如果进程池序列中没有可供使用的进进程,那么程序就会等待,直到进程池中有可用进程为止。进程池设置最好等于CPU核心数量。
1.2.1 pool进程池
Pool进程池是multiprocessing包带了一个进程池;以下是创建pool对象的实例:
pool=multiprocessing.Pool([processes[, initializer[, initargs[, maxtasksperchild[, context]]]]])
(1)参数解释:(以下参数也就processes常用)
- pool:返回值,返回一个进程池对象。
- processes :可选、int型,使用的工作进程的数量,如果processes是None那么使用系统默认的最大进程数量(os.cpu_count()可以知道系统支持多少进程数)。
- initializer:可选、实参要是一个函数名,如果有设置实参,则工作进程启动时将调用它。
- initargs:可选,*args或**kwgds,作为initializer初始函数的实参。
- maxtasksperchild:可选,如果设置了这个参数,那么在完成这个数量的任务之后,工作进程会被自动关闭和重新启动。这可以帮助管理内存泄漏。
- context: 可选,用在制定工作进程启动时的上下文,一般使用 multiprocessing.Pool()或者一个context对象的Pool()方法来创建一个池,两种方法都适当的设置了context
具体实现例子:
from multiprocessing import Pool
def init_worker(init_arg):# 创建子进程的初始函数
print(f'Worker initializing with {init_arg}')
def long_running_task(task_id):
print(f'Task {task_id} is running')
if __name__ == '__main__':
with Pool(processes=4, initializer=init_worker, initargs=('initialized',)) as pool:
pool.map(long_running_task, range(4))
(2)常用方法:
-
res=apply(func[, args[, kwds]])
:同步进程池,单任务阻塞型,即是每次提交一个任务,主进程阻塞等待子进程完成,然后继续下个子进程。func:必选,任务需要执行的函数。
args和kwds:可选,为func函数的实参。
-
res=apply_async(func[, args[, kwds[, callback[, error_callback]]]])
:异步进程池,单任务非阻塞,每次提交一个任务,无须等待上个任务是否完成继续提交一个任务,由于是非阻塞的主进程不会等待子进程所以必须使用join来等待子进程完成,注意join必须是在close或terminate之后。func:带入子进程执行的方法或函数;
args(kwds):提供给func的实参;
callback:回调函数(有返回值时执行该方法,该函数必须有个参数);
error_callback:和callback相反,任务失败时回调。
res:返回值,AsyncResult对象,通过res.get()获取返回值(注意:get会变成同步变成任务逐个执行)。
-
res=map(func, iterable, chunksize=None)
:阻塞多任务型(使用一个函数一次发起了多个进程任务叫多任务)。func一样是带入执行的函数。
iterable:作为func的实参的可迭代对象(可循环遍历的对象),每个元素就是一个任务参数。
chunksize:chunksize是可选的参数,用于指定任务分片的大小。
res:返回值,列表类型。
-
res=map_async(func, iterable[, chunksize[, callback[, error_callback]]])
:非阻塞多任务型.其他参数参考map()
callback:代表进程执行结束后的回调函数;
error_callback:代表进程执行出现异常后的回调函数。
Res:返回值,AsyncResult对象,通过res.get()获取返回值(注意:get会变成同步变成任务逐个执行)。
-
close()
: 关闭进程池,阻止更多的任务提交到pool,待任务完成后,工作进程会退出。 -
terminate()
: 结束工作进程,不在处理未完成的任务 -
join()
: wait工作线程的退出,在调用join()前,必须调用close() or terminate()。这样是因为被终止的进程需要被父进程调用wait(join等价与wait),否则进程会成为僵尸进程。
(3)执行子进程池的四种方式
(3.1)apply(func[, args[, kwds]]):单任务同步阻塞型(建议不使用)
import multiprocessing as mp
import time
def mycall(msg):
print(f"[{msg}]进程开始工作……")
time.sleep(1)
print(f"[{msg}]进程执行结束……")
return f"msg:{msg}"
if __name__ == '__main__':
pool=mp.Pool(4)
for i in range(5):
res=pool.apply(mycall,(i,))
print(res) # 直接返回结果。
print("主进程完毕")
pool.close()
print("close over")
pool.join() # 阻塞型这部可有可无。
print("所有都结束")
输出结果:
[0]进程开始工作……
[0]进程执行结束……
msg:0
[1]进程开始工作……
[1]进程执行结束……
msg:1
[2]进程开始工作……
[2]进程执行结束……
msg:2
[3]进程开始工作……
[3]进程执行结束……
msg:3
[4]进程开始工作……
[4]进程执行结束……
msg:4
主进程完毕 # 说明子进程在运行的时候进行的阻塞
close over
所有都结束
进程已结束,退出代码为 0
结果分析:
- 同步型:从结果可以确认,每次只执行一个任务,一个任务完成才继续下一个任务这就是同步执行。
- 阻塞型:子进程开始执行时,主进程处于阻塞中,直到子进程全部执行完毕才继续执行主进程的后续内容,所以这种情况可以不用join函数也可以。
- 单任务:如例子使用for循环进行调用apply来添加任务,循环10次就添加了10个任务,而每个apply只执行一个任务的添加。
- Python3.0后就淘汰了,现在apply其实就是调用apply_async,然后每个返回都get()形成阻塞,和本来的apply同步一样的。
(3.2) apply_async(func[, args[, kwds[, callback[, error_callback]]]]):单任务异步非阻塞型
import multiprocessing as mp
import time
def mycall(msg):
print(f"[{msg}]进程开始工作……")
time.sleep(1)
print(f"[{msg}]进程执行结束……")
return f"msg:{msg}"
def callback(arg): # 注意一个参数,是进程有返回值时调用的,这个arg也是进程的返回值即mycall的返回值
print(f"i am callbak:{arg}")
if __name__ == '__main__':
res_list = []
pool=mp.Pool(4)
for i in range(5):
res=pool.apply_async(mycall,args=(i,),callback=callback)
res_list.append(res) # 返回的是AsyncResult对象,可以使用get()提取,但get会阻塞会使异步变成同步,所以这里先添加到列表。
print("主进程完毕")
# pool.close()
print("close over")
res = pool.apply_async(mycall, (10,))
res_list.append(res)
pool.close()
pool.join()
print("所有都结束")
结果输出:
主进程完毕 # 非阻塞,提交任务后不会等子进程完成,继续完成主进程自己的任务。
close over
[0]进程开始工作…… # 异步类型,提交任务后不等任务结果。
[1]进程开始工作……
[2]进程开始工作……
[3]进程开始工作…… # 由于设置了进程池最大运行数是4,已满等待
[0]进程执行结束……
[4]进程开始工作…… # 0号进程结束,4号马上补上
i am callbak:msg:0 # 第一个进程已顺利完成并返回了值,所以调用了callback函数。
[2]进程执行结束……
[1]进程执行结束……
i am callbak:msg:2
[10]进程开始工作……
i am callbak:msg:1
[3]进程执行结束……
i am callbak:msg:3
[4]进程执行结束……
i am callbak:msg:4
[10]进程执行结束……
所有都结束 # 主进程由于join的存在必须等待所有子进程完成。
进程已结束,退出代码为 0
结果分析
- 异步型:提交一个任务后,不等任务结果,继续下个任务这就叫异步型。
- 非阻塞:主进程完成任务的提交到进程池后,不会等子进程结束先结束,会照成内存泄漏,所以必须使用join等待子进程。
- 单任务:和apply一样,每次只能提交一个任务到任务池中。
- 返回值是AsyncResult对象,需要用get来方法来获取,由于get方法会阻塞改变异步的特性,所以一般是使用列表来保存,子进程全部结束后在进行遍历取值。
(3.3) map(func, iterable, chunksize=None):多任务同步阻塞型
import multiprocessing as mp
import time
def mycall(msg):
print(f"[{msg}]进程开始工作……")
time.sleep(1)
print(f"[{msg}]进程执行结束……")
return f"msg:{msg}"
if __name__ == '__main__':
res_list = []
pool=mp.Pool(4)
res=pool.map(mycall,range(5)) # 一个函数同时并行五个进程,这就是多任务,这里只做两个参数其他参数的用法参考apply_async
print(f"返回值对象:{type(res)},返回值:{res}")
print("主进程完毕")
pool.close()
print("close over")
pool.join() # 阻塞型这部可有可无。
print("所有都结束")
输出结果:
[0]进程开始工作……
[1]进程开始工作……
[2]进程开始工作……
[3]进程开始工作……
[1]进程执行结束……
[0]进程执行结束……
[2]进程执行结束……
[4]进程开始工作……
[3]进程执行结束……
[4]进程执行结束……
返回值对象:<class 'list'>,返回值:['msg:0', 'msg:1', 'msg:2', 'msg:3', 'msg:4']
主进程完毕
close over
所有都结束
进程已结束,退出代码为 0
结果分析:
- 阻塞型:和上面两种方法一样,就是子进程在执行的时候,主进程进行阻塞等待全部子进程完成任务后再继续执行主进程的任务。
- 同步性:任务必须完成后给与返回。
- 多任务:使用一个函数一次执行多个任务创立多个子进程,所以在形参中给执行函数的形参是可迭代对象(可以遍历循环的对象)。
- 返回值:返回一个列表,元素是有序排列的。
- 这种阻塞型,可以不用join也可以。
- 适合场景:适用于批量并发,小规模、短时间任务。
(3.4) map_async(func, iterable[, chunksize[, callback[, error_callback]]]):多任务异步非阻塞型
import multiprocessing as mp
import time
def mycall(msg):
print(f"[{msg}]进程开始工作……")
time.sleep(1)
print(f"[{msg}]进程执行结束……")
return f"msg:{msg}"
if __name__ == '__main__':
pool=mp.Pool(4)
res=pool.map_async(mycall,range(10))
print(f"返回值对象:{type(res)},返回值:{res}") # 如果这里res使用get(),会把非阻塞变成阻塞型。
print("主进程完毕")
pool.close()
print("close over")
pool.join()
print(res.get())
print("所有都结束")
输出结果:
返回值对象:<class 'multiprocessing.pool.MapResult'>,返回值:<multiprocessing.pool.MapResult object at 0x10603da30>
主进程完毕
close over
[0]进程开始工作……
[1]进程开始工作……
[2]进程开始工作……
[3]进程开始工作……
[0]进程执行结束……
[1]进程执行结束……
[4]进程开始工作……
[2]进程执行结束……
[3]进程执行结束……
[4]进程执行结束……
['msg:0', 'msg:1', 'msg:2', 'msg:3', 'msg:4']
所有都结束
进程已结束,退出代码为 0
结果分析:
- 非阻塞型:和上面三种方法一样,主进程根据进程池的最大量提交任务后,不管子进程执行结果就继续执行自己的后续任务。
- 多任务:使用一个函数一次执行多个任务创立多个子进程,所以在形参中给执行函数的形参是可迭代对象(可以遍历循环的对象)。
- 返回值:返回一个MapResult对象,需要使用get()得到一个列表,并且列表的元素是有序排列的;注意:get()会使主进程进行阻塞,所以为了实现非阻塞可以放在join后面进行执行。
- 由于非阻塞,为了避免出现僵死进程,务必使用join等待子进程完成,而join必须房子close后面。
- 适合场景:适用于需要并发处理的大量任务
(3.5) 三种执行进程池的方法对比
特性 | apply_async() | map() | map_async() |
---|---|---|---|
执行模式 | 异步执行单个任务 | 同步执行批量任务 | 异步执行批量任务 |
是否阻塞主进程 | 不阻塞 | 阻塞主进程,等待所有任务完成 | 不阻塞 |
适用场景 | 适用于提交单个任务的异步处理 | 适用于需要同步处理的批量任务 | 适用于需要异步处理的批量任务 |
结果返回方式 | 返回 AsyncResult 对象 | 任务完成后返回结果列表 | 返回 AsyncResult 对象 |
回调函数支持 | 支持 callback 和 error_callback | 不支持回调函数 | 支持 callback 和 error_callback |
结果顺序 | 任务完成顺序与提交顺序无关 | 结果顺序与输入顺序一致 | 结果顺序与输入顺序一致 |
主进程的任务执行 | 主进程可以继续执行其他操作 | 主进程等待所有任务完成 | 主进程可以继续执行其他操作 |
任务分块(chunksize ) | 不适用 | 支持任务分块(提高性能) | 支持任务分块(提高性能) |
结果获取方式 | AsyncResult.get() | 直接返回完整结果 | AsyncResult.get() 或 callback |
1.2.2 ProcessPoolExecutor进程池
ProcessPoolExecutor进程池是concurrent.futures
包中的类;以下是创建对象实例:
ProcessPoolExecutor(max_workers=None, mp_context=None, initializer=None, initargs=(), max_tasks_per_child=None)
(1) 参数解释
- max_workers:最大的工作进程数,一般是设置本机的最大cpu核数,可以不用设置即None或不带入参数系统默认最大核数。
- mp_context:进程的启动方式[spawn](https://www.baidu.com/s?sa=re_dqa_generate&wd=spawn&rsv_pq=f252924504a093e2&oq=python python ProcessPoolExecutor context参数默认是什么&rsv_t=41b3pf00F8Ft3mDlwwtwa5Xl04ZivdY3XbImTACQn+upI68XzkF2Mh4ka6qUBRFmjkht&tn=baiduhome_pg&ie=utf-8)和[fork](https://www.baidu.com/s?sa=re_dqa_generate&wd=fork&rsv_pq=f252924504a093e2&oq=python python ProcessPoolExecutor context参数默认是什么&rsv_t=41b3pf00F8Ft3mDlwwtwa5Xl04ZivdY3XbImTACQn+upI68XzkF2Mh4ka6qUBRFmjkht&tn=baiduhome_pg&ie=utf-8),默认是spawn,具体选择什么因部署的系统平台而定。
- initializer:初始化环境用的回调/钩子,会在传进去的任务执行之前调用
- initargs:对应initializer的函数的参数,是一个元组。
- max_tasks_per_child:任务数大于进程数时,每个子进程会重复的完成不同的任务,这里是限定每个子进程最大的完成任务数。
(2) 常用方法
-
res=submit(fn, *args, **kwargs)
:提交特定任务(单任务,每次提交一个任务)。返回一个future实例;fn:要执行的函数。
*args, **kwargs
:fn参数res:返回值,future对象,future.result()来取值,futrue对象的常用方法如下:
result(timeout=None): 获取任务的结果,如果任务未完成,则阻塞直到任务完成或超时。
done(): 检查任务是否完成。
exception(timeout=None): 获取任务执行时抛出的异常,如果没有异常,则返回
None
。 add_done_callback(fn): 为任务添加一个回调函数,当任务完成时调用该函数。
-
list=map(func, *iterables, timeout=None)
:批量submit(多任务);func:需要异步执行的函数;
iterables:可迭代对象,如列表等,每一次func执行,都会从iterables中取参数;
timeout:设置每次异步操作的超时时间,timeout的值可以是int或float,如果操作超时,会返回raisesTimeoutError;如果不指定timeout参数,则不设置超时间。
lsit:返回值,列表类型。
-
shutdown(wait=True, cancel_futures=False)
:方法用于关闭进程池,等待所有已提交的任务完成后关闭进程池。通常在使用with
语句时,不需要手动调用此方法,因为with
语句会自动调用它。wait: 默认是True,如果为 True,则阻塞主进程直到所有任务完成。
cancel_futures:默认:False, 如果为
True
,则尝试取消所有尚未开始执行的任务。
(3)执行进程池的两个方式
以下创建进程池对象以及执行时最好在if __name__ == '__main__':
下否则容易报错。
*(3.1)submit(fn, args, **kwargs)单任务提交的例子:
import concurrent.futures
import time
def mycall(msg):
print(f"[{msg}]进程开始工作……")
time.sleep(msg/10)
print(f"[{msg}]进程执行结束……")
return f"msg:{msg}"
if __name__ == '__main__':
resultlist=[]
with concurrent.futures.ProcessPoolExecutor(4) as pool:
for i in range(5):
future=pool.submit(mycall,i)
resultlist.append(future) # 这里是把返回值逐个添加到列表,这里不能直接futrue.result获取值,原因和get一样会阻塞。
# for res in concurrent.futures.as_completed(resultlist): # 这个和下面的一样获取返回值,结果1
for res in resultlist: # 结果2
print(res.result())
print("over")# 说明以上都是阻塞
输出结果1:使用concurrent.futures.as_completed达到谁先完成谁先输出。
[5]进程开始工作……
[4]进程开始工作……
[3]进程开始工作……
[2]进程开始工作……
[2]进程执行结束……
[1]进程开始工作……
msg:2 # 先完成先输出
[3]进程执行结束……
msg:3
[1]进程执行结束……
msg:1
[4]进程执行结束……
msg:4
[5]进程执行结束……
msg:5
over # 子进程执行时主进程处于阻塞
进程已结束,退出代码为 0
输出结果2:常规做法,按提交顺序逐个等待完成输出,哪怕后面提交的先完成的,也得等。
[5]进程开始工作……
[4]进程开始工作……
[3]进程开始工作……
[2]进程开始工作……
[2]进程执行结束……
[1]进程开始工作……
[3]进程执行结束……
[1]进程执行结束……
[4]进程执行结束……
[5]进程执行结束……
msg:5 # 耗时最长时间完成了,由于是第一个提交的所以先输出。
msg:4
msg:3
msg:2
msg:1
over #子进程执行时主进程处于阻塞
进程已结束,退出代码为 0
结果分析:
- 使用with会自动释放内存,所以可以不用shutdown()销毁资源。
- submit可以支持执行函数带多个参数。
- 返回值是future对象,需要result方法来获取值,和get一样不要在提交进程时输出,这样的话会阻塞变成同步无法达到多进程任务。
- concurrent.futures.as_completed修饰future结果,会达到先完成先输出的结果。
- 子进程执行时主进程处于阻塞
- 以上两个输出结果是windows平台的,linux和macos两种方法输出的都是第一种结果
*(3.2)map(func, iterables, timeout=None)实例:
import concurrent.futures
import time
def mycall(msg):
print(f"[{msg}]进程开始工作……")
time.sleep(msg/10)
print(f"[{msg}]进程执行结束……")
return f"msg:{msg}"
if __name__ == '__main__':
mylist=[5,4,3,2,1]
with concurrent.futures.ProcessPoolExecutor(4) as pool:
reslist=pool.map(mycall,mylist)
for res in reslist:
print(res)
print("over")
输出结果:
[5]进程开始工作……
[4]进程开始工作……
[3]进程开始工作……
[2]进程开始工作……
[2]进程执行结束……
[1]进程开始工作……
[3]进程执行结束……
[1]进程执行结束……
[4]进程执行结束……
[5]进程执行结束……
msg:5
msg:4
msg:3
msg:2
msg:1
over # 子进程执行时主进程处于阻塞
进程已结束,退出代码为 0
结果分析:
- 使用with会自动释放内存,所以可以不用shutdown()销毁资源。
- 使用一个函数带入可迭代对象满足一次建立多个任务。
- 子进程执行时主进程处于阻塞
- 返回值就是一个列表。
1.2.3 两个进程池的使用区别
虽然 ProcessPoolExecutor
和 multiprocessing.Pool
都用于管理进程池,但它们在接口设计和使用体验上有一些区别。
特性 | ProcessPoolExecutor | multiprocessing.Pool |
---|---|---|
模块 | concurrent.futures | multiprocessing |
接口设计 | 高层次、面向对象,支持 Future 对象 | 较低层次,基于函数调用 |
方法名称 | submit() , map() | apply_async() , map() , map_async() |
回调支持 | 通过 Future 的 add_done_callback() | 通过 apply_async和map_async 的 callback |
与 ThreadPoolExecutor 的一致性 | 设计上与 ThreadPoolExecutor 兼容 | 不直接兼容 |
错误处理 | 通过 Future 的异常捕获机制 | 通过回调函数处理异常 |
使用习惯 | 更适合现代 Python 编程风格,支持上下文管理 | 更适合传统的多进程编程风格 |
总结:
- ProcessPoolExecutor 提供了更现代化、易用的接口,适合与
ThreadPoolExecutor
共享相同的设计理念,使得多线程和多进程编程更加一致。 - multiprocessing.Pool 适用于需要更底层控制的场景,但接口较为复杂。
1.3 多进程是不会共享全局变量
进程是系统资源分配的最小单位,所以每个进程都有独立的内存空间,进程之间是相互不干扰;子进程被创建其实是已主进程为模板进行复制创立了,所以子进程拥有主进程的变量等,而独立的内存空间导致全局变量在不同子进程之间形成了两个空间互不干扰也就形成不了交互了。
import multiprocessing
list1 = [11, 22, 33, 44]
def add_value():
list1.append(55)
print(list1)
def pop_value():
list1.pop()
print(list1)
def print_list():
print(list1)
if __name__ == '__main__':
p1 = multiprocessing.Process(target=add_value) # 对列表进行增加元素55,最后print_list进程还是默认值
p2 = multiprocessing.Process(target=pop_value) # 对列表进行删除元素,最后print_list进程还是默认值
p3 = multiprocessing.Process(target=print_list) # 通过以上两个进程的修改,并未对list产生影响,说明变量是不共享的。
p1.start()
p2.start()
p3.start()
输出结果:
[11, 22, 33, 44, 55]
[11, 22, 33]
[11, 22, 33, 44]
1.4 进程间的通信
如本章的1.3章节,告诉我们由于进程的机制导致每个子进程之间,子进程与主进程之间都有着独立的内存空间,所以他们之间是无法共享数据,但在现实的任务实现中必须实现进程间的通信,所以我们下面引入内存共享、队列共享、管道共享等逻辑来帮助我们实现进程间的通信。
1.4.1 内存共享
共享内存是一种通过在不同进程之间共享相同的物理内存来实现通信的机制。在python中,可以使用multiprocessing模块中的两个函数来创建共享内存对象,它们分别是multiprocessing.Value()和multiprocessing.Array(),它们允许多个进程共享单个变量或数组的值。它们提供了对共享内存中数据的原子性访问;这里解决的常规变量的共享问题,而Manager则允许多个进程共享数据容器,解决了数据容器共享。
(1)Value:
multiprocessing.Value(typecode_or_type, args, lock=True)
-
typecode_or_type:定义 ctypes() 对象的类型,可以传 Type code 或 C Type,具体对照表见下文。
-
args:传递给 typecode_or_type 构造函数的参数,其实就是传入的一个初始值。
-
lock:默认为True,创建一个互斥锁来限制对Value对象的访问,如果传入一个锁,如Lock或RLock的实例,将用于同步。如果传入False,Value的实例就不会被锁保护,它将不是进程安全的。
(2)Array:
Array(typecode_or_type, size_or_initializer, **kwds[, lock])
-
typecode_or_type:定义 ctypes() 对象的类型,可以传 Type code 或 C Type,具体对照表见下文
-
size_or_initializer 如果它是一个整数,那么它确定数组的长度,并且数组将被初始化为0。否则,size_or_initializer 是用于初始化数组的序列,其长度决定数组的长度。
-
kwds:是传递给 typecode_or_type 构造函数的参数。
-
lock:和value一样的锁保护。
type code表格(ctype和ptype的数字类型比对):
Type code | C Type | Python Type | Minimum size in bytes |
---|---|---|---|
'b' | signed char | int | 1 |
'B' | unsigned char | int | 1 |
'u' | Py_UNICODE | Unicode character | 2 |
'h' | signed short | int | 2 |
'H' | unsigned short | int | 2 |
'i' | signed int | int | 2 |
'I' | unsigned int | int | 2 |
'l' | signed long | int | 4 |
'L' | unsigned long | int | 4 |
'q' | signed long long | int | 8 |
'Q' | unsigned long long | int | 8 |
'f' | float | float | 4 |
'd' | double | float | 8 |
Value和Array共享内存示例:
from multiprocessing import Process, Value, Array
def worker(val, arr):
# 修改共享的 Value 和 Array
val.value += 1
for i in range(len(arr)):
arr[i] += 1
if __name__ == "__main__":
# 创建一个共享的整数和数组
shared_val = Value('i', 0) # 'i' 表示整数类型
shared_arr = Array('i', [0, 1, 2, 3, 4]) # 共享数组
# 创建多个进程
processes = [Process(target=worker, args=(shared_val, shared_arr)) for _ in range(3)]
# 启动所有进程
for p in processes:
p.start()
# 等待所有进程结束
for p in processes:
p.join()
# 打印共享的 Value 和 Array 的最终结果
print(f"Shared Value: {shared_val.value}") # 输出:Shared Value: 3
print(f"Shared Array: {list(shared_arr)}") # 输出:Shared Array: [3, 4, 5, 6, 7]
注意:
- 从例子可以看出共享内存的变量在三个不同的进程中被计算了。
- 这里的lock是默认的True,某进程对内存变量进行赋值时是独占。
(3) Manager共享:其非内容共享,简单理解就是通过网络协议访问实现跨进程访问
Manager
允许多个进程共享复杂的 Python 数据结构,比如 list
和 dict
。它会在后台启动一个服务器进程,所有的进程都可以通过网络协议与该进程进行通信,从而共享这些数据。
Manager也有value和array方法和上面的用法一样,但二者的机制不一样,具体区别如下:
from multiprocessing import Process, Manager
def worker(shared_dict, key, value):
# 修改共享字典
shared_dict[key] = value
if __name__ == "__main__":
# 创建一个 Manager 对象
with Manager() as manager:
shared_dict = manager.dict()
# 创建多个进程并传递共享字典
processes = [Process(target=worker, args=(shared_dict, i, i*2)) for i in range(5)]
# 启动所有进程
for p in processes:
p.start()
# 等待所有进程结束
for p in processes:
p.join()
# 打印共享字典的内容
print(shared_dict) # 输出:{1: 2, 0: 0, 3: 6, 2: 4, 4: 8}
解释:在这个例子中,Manager()
创建了一个 dict
类型的共享数据容器,多个进程可以同时访问并修改它。进程通过 shared_dict[key] = value
来更新共享数据,最终父进程可以读取所有子进程写入的数据。
Manager也有value和array方法和上面的用法一样,但二者的机制不一样,具体区别如下:
特性 | Value 和 Array | Manager().Value() 和 Manager().Array() |
---|---|---|
实现方式 | 通过共享内存实现,数据直接存储在共享内存中 | 通过服务器进程管理,使用代理对象进行通信 |
支持的数据类型 | 基本数据类型(如整数、浮点数)和固定大小的数组 | 基本数据类型、数组,以及复杂对象(如列表、字典等) |
性能 | 高效,因为直接使用共享内存 | 相对较低,因为需要通过服务器进程和代理对象进行通信 |
进程间共享 | 仅限于同一台机器上的进程 | 支持跨机器的进程间共享,适合分布式场景 |
使用场景 | 共享简单的基础数据类型或数组,适用于高性能场景 | 共享复杂数据结构,如列表、字典等,适用于灵活的场景 |
线程安全性 | 不提供内置的锁机制,需手动添加锁 | 内置线程安全机制,管理共享对象的并发访问 |
1.4.2 队列共享(Queue跨进程队列)
进程间彼此通信可创建一个队列,一个生产另一个消费的机制存在,Queue类就是实现了队列的功能,而Queue类分别来自两个包multiprocessing和Queue,实现的机制有所不同,multiprocessing.Queue是跨进程通信队列,queue.Queue是进程内非阻塞队列(不能用于多进程),二者区别如下:
- From queque import Queue:这是一个普通的队列,是一个进程内的非阻塞队列,通常用在单个进程内的多个线程间通信,它不能在多个进程间进行通信,在使用get消费时会阻塞,直到get到数据为止(详细请看多线程队列的例子)。
- From multiprocessing import Queue:多进程中共享的队列(
先进先出原则
),用于多进程间的彼此通信,普通Queue是无法实现了,多进程队列共享着重介绍这个用法。
创建队列对象:
# 创建对象
queue=multiprocessing.Queue([maxsize])
# 例子:
queue=multiprocessing.Queue()
queue=multiprocessing.Queue(10)
queue=multiprocessing.Queue(maxsize=10)
形参解析:
- maxsize:默认值是0,队列中允许最大项数,省略则视为0,无大小限制。
主要方法:
-
Put(obj[,blocked][,timeout]):用以插入数据到队列中,put方法还有三个可选参数:
obj:必填、需要插入队列的数据;
blocked:可选、为True(默认值)是否阻塞和timerout联合使用。
timeout:可选、并且timeout为正值,该方法会阻塞timeout指定的时间,直到该队列有剩余的空间。如果超时,会抛出Queue.Full异常。如果blocked为False,但该Queue已满,会立即抛出Queue.Full异常。【生产者提供】
-
get([blocked][,timeout]):可以从队列读取并且删除一个元素。同样,get方法有两个可选参数:blocked和timeout。如果blocked为True(默认值),并且timeout为正值,那么在等待时间内没有取到任何元素,会抛出Queue.Empty异常。如果blocked为False,有两种情况存在,如果Queue有一个值可用,则立即返回该值,否则,如果队列为空,则立即抛出Queue.Empty异常.【消费者消费】
-
get_nowait():同q.get(False).
-
put_nowait():同q.put(False).
-
empty():调用此方法时queue为空则返回True,该结果不可靠,比如在返回True的过程中,如果队列中又加入了项目。
-
full():调用此方法时q已满则返回True,该结果不可靠,比如在返回True的过程中,如果队列中的项目被取走。
-
qsize():返回队列中目前项目的正确数量,结果也不可靠,理由同q.empty()和q.full()一样。
-
cancel_join_thread():不会在进程退出时自动连接后台线程。可以防止join_thread()方法阻塞。
-
close():关闭队列,防止队列中加入更多数据。调用此方法,后台线程将继续写入那些已经入队列但尚未写入的数据,但将在此方法完成时马上关闭。如果queue被垃圾收集,将调用此方法。关闭队列不会在队列使用者中产生任何类型的数据结束信号或异常。例如,如果某个使用者正在被阻塞在get()操作上,关闭生产者中的队列不会导致get()方法返回错误。
-
join_thread():连接队列的后台线程。此方法用于在调用q.close()方法之后,等待所有队列项被消耗。默认情况下,此方法由不是queue的原始创建者的所有进程调用。调用q.cancel_join_thread方法可以禁止这种行为。
示例:
from multiprocessing import Process, Queue
def worker(q):
q.put('Hello, world!')
def consumer(q):
msg=q.get()
print(f"消费者得到信息:{msg}")
if __name__ == '__main__':
q = Queue(5) # 限制项目数为5个,可以不要限制。
p = Process(target=worker, args=(q,))
p2= Process(target=consumer, args=(q,))
p.start()
p2.start()
p.join()
p2.join()
Queue处理并发时的队列同步:Queue
是线程安全的,但在高并发的情况下,仍然可能存在竞争条件(比如 qsize()
的结果可能过时)。因此在某些场景中,为了确保数据一致性,可以考虑使用 Lock
来手动同步对队列的访问。
from multiprocessing import Process, Queue, Lock
import time
def producer(q, lock):
with lock: # 加锁保护对队列的访问(这种写法会自动释放)
for i in range(5):
q.put(i)
print(f"Produced {i}")
time.sleep(1)
def consumer(q, lock):
while not q.empty():
with lock: # 加锁保护对队列的访问
if not q.empty():
item = q.get()
print(f"Consumed {item}")
time.sleep(1)
if __name__ == "__main__":
q = Queue()
lock = Lock() # 创建锁对象
# 创建并启动生产者进程
producer_process = Process(target=producer, args=(q, lock))
producer_process.start()
# 等待生产者完成生产
producer_process.join()
# 创建并启动消费者进程
consumer_process = Process(target=consumer, args=(q, lock))
consumer_process.start()
# 等待消费者完成消费
consumer_process.join()
解释:
- 在这个例子中,我们使用
Lock
来同步对队列的访问。虽然Queue
是线程安全的,但是为了避免并发时出现未同步的操作,我们在访问队列时显式地使用了锁。 - 生产者进程负责将数据放入队列,而消费者进程从队列中取出数据并处理。
- 通过加锁机制,确保生产者和消费者不会同时操作队列,避免数据不一致或竞争条件。
1.4.3 管道共享(Pipe)
multiprocessing.Pipe
是 Python 中 multiprocessing
模块提供的另一种进程间通信机制。Pipe
提供了一个双向的通信通道,通过这个通道,两个进程可以发送和接收数据。Pipe
通常用于父进程和子进程之间交换信息。
Pipe是Python中的一个双向管道,可以用于在两个进程之间传递数据。使用Pipe时,我们可以通过一端将数据发送给另一端,也可以从另一端接收数据。Python中的Pipe方法返回的是一个元组,其中包含了两个端点,每个端点都是一个Connection对象。
# 以下三种都是全双工的管道模式
parent_conn, child_conn = multiprocessing.Pipe(duplex=True)
parent_conn, child_conn = multiprocessing.Pipe(True)
parent_conn, child_conn = multiprocessing.Pipe()
参数解释:
duplex:是否全双工(两端都可以发送和接收)否则只有一端可以发送,另一端只可以接收,默认值是True表示是全双工。
返回值:一个数组两个Connection对象(示例中的,parent_conn,child_conn)每个进程分配一个对象进行互相通信。
重要信息:管道只支持两个进程之间通信,因为它只创建了两个Connection对象,也就只能分配给两个进程,如果是三个进程通信就不可以使用管道了。
常用函数:
parent_conn.send(msg):向对方发送信息。
msg=parent_conn.recv():接收信息。
parent_conn.close():关闭销毁connection对象。
poll([timeout]):检查是否有数据可供接收。如果设置了 timeout
参数,指定等待的秒数,默认值为 None
,表示会无限期等待。
双向通信的例子:
from multiprocessing import Process, Pipe
def worker(conn):
conn.send("Hello from child") # 子进程通过 Pipe 发送数据
print("Child received:", conn.recv()) # 子进程从 Pipe 接收数据
conn.close()
if __name__ == "__main__":
parent_conn, child_conn = Pipe() # 创建 Pipe 对象,返回两个连接对象
# 创建子进程,并传递子进程的连接端
p = Process(target=worker, args=(child_conn,))
p.start()
print("Parent received:", parent_conn.recv()) # 父进程从 Pipe 接收数据
parent_conn.send("Hello from parent") # 父进程通过 Pipe 发送数据
p.join() # 等待子进程结束
单向通信例子:
from multiprocessing import Process, Pipe
def worker(conn):
print("Child received:", conn.recv()) # 子进程只能接收数据
conn.close()
if __name__ == "__main__":
child_conn, parent_conn = Pipe(duplex=False) # 设置单向通信,第一个是接收,第二个事发送,颠倒报错
# 创建子进程,并传递子进程的连接端
p = Process(target=worker, args=(child_conn,))
p.start()
parent_conn.send("Hello from parent") # 父进程只能发送数据
p.join() # 等待子进程结束
Pipe 阻塞和非阻塞模式
管道是阻塞的,recv()方法使用时没有接收到数据或管道满时就会进行阻塞,直到有数据可读时;同样如果管道已满或者没有空间可写时,send()方法也会造成阻塞,直到有空间可写。
可以结合 poll()
方法,避免 recv()
阻塞。poll()
方法用于检查是否有可用的数据,可以用它实现非阻塞的消息接收。
示例:
from multiprocessing import Process, Pipe
def worker(conn):
if conn.poll(1): # 阻塞等待是否有数据,有数据进来返回true,没有数据一直等待1秒时间到返回false
print("Child received:", conn.recv()) # 子进程只能接收数据
else:
print("no data^")
conn.close()
if __name__ == "__main__":
child_conn, parent_conn = Pipe(duplex=False) # 设置单向通信
# 创建子进程,并传递子进程的连接端
p = Process(target=worker, args=(child_conn,))
p.start()
# parent_conn.send("Hello from parent") # 父进程只能发送数据
p.join() # 等待子进程结束
多进程间的pipe通信
Pipe
适用于两个进程之间的通信,通常用于父进程和子进程。如果需要多个进程之间通信,Queue
会更适合这种场景。然而,Pipe
仍然可以在复杂的通信拓扑中使用,如多个 Pipe
连接不同的进程对。
from multiprocessing import Process, Pipe
def worker1(conn):
conn.send("Message from worker1")
print("Worker1 received:", conn.recv())
conn.close()
def worker2(conn):
print("Worker2 received:", conn.recv())
conn.send("Message from worker2")
conn.close()
if __name__ == "__main__":
parent_conn1, child_conn1 = Pipe() # 创建第一个 Pipe
parent_conn2, child_conn2 = Pipe() # 创建第二个 Pipe
# 创建两个子进程,分别使用不同的 Pipe
p1 = Process(target=worker1, args=(child_conn1,))
p2 = Process(target=worker2, args=(child_conn2,))
p1.start()
p2.start()
# 父进程与子进程进行通信
print("Parent received from worker1:", parent_conn1.recv())
parent_conn1.send("Reply to worker1")
parent_conn2.send("Message to worker2")
print("Parent received from worker2:", parent_conn2.recv())
p1.join()
p2.join()
2、多线程
在 Python 中,threading.Thread
类用于创建并管理线程。每个线程是一个独立的执行路径,可以并行执行多个任务。多线程适用于 I/O 密集型任务(如文件读取、网络请求等),因为这些任务通常会等待外部资源响应,而 CPU 资源在等待期间可以被其他线程利用。
2.1 实例化语法
2.1.1 定义语法说出
thread=threading.Thread(target[,args,kwargs,name])
group
: 此参数未使用,默认应为None
。target
: 线程将执行的函数。args
: 目标函数的位置参数元组。kwargs
: 目标函数的关键字参数字典。name
: 线程的名字。默认是Thread-N
形式的字符串,其中 N 是一个递增的数字。Thread
:返回一个实例对象。
例子:
import threading
import time
# 定义线程要执行的任务函数
def task(name):
print(f"Thread {name} starting")
time.sleep(2)
print(f"Thread {name} finished")
# 创建并启动两个线程
thread1 = threading.Thread(target=task, args=("A",))
thread2 = threading.Thread(target=task, args=("B",))
thread1.start()
thread2.start()
# 等待两个线程执行完毕
thread1.join()
thread2.join()
print("All threads are done")
2.1.2 常用方法
start()
:启动线程活动。在每个线程对象中最多被调用一次。它安排对象的run() 被调用在一单独的控制线程中。Run()
:用以表示线程活动的方法。你可能在Python Thread类的子类重写这方法。标准的 run()方法调用作为target传递给对象构造函数的回调对象。join([timeout])
:等待至线程中止。阻塞调用线程直至线程的join() 方法被调用中止-正常退出或者抛出未处理的异常-或者是可选的超时发生。timeout参数不是None,它应当是浮点数指明以秒计的操作超时值。因为join()总是返回None,你必须调用isAlive()来判别超时是否发生。当timeout 参数没有被指定或者是None时,操作将被阻塞直至线程中止。getName()
:返回线程的名字。setName(name)
:设置线程的名字。isAlive()
:返回线程是否活动的。isDaemon()
:返回线程的守护线程标志。setDaemon(daemonic)
:设置守护线程标志为布尔值daemonic。它必须在start()调用之前被调用。
2.2 使用类方法实现线程
除了将函数作为目标传递外,还可以通过继承 threading.Thread
类来自定义线程。
import threading
import time
# 继承 threading.Thread 类
class MyThread(threading.Thread):
def __init__(self, name):
threading.Thread.__init__(self)
self.name = name
# 重写 run 方法
def run(self): # 重写run是线程启动后要执行的函数
print(f"Thread {self.name} starting")
time.sleep(2)
print(f"Thread {self.name} finished")
# 创建并启动线程
thread1 = MyThread("A")
thread2 = MyThread("B")
thread1.start()
thread2.start()
thread1.join()
thread2.join()
print("All threads are done")
解释:
- 我们通过继承
threading.Thread
创建了一个自定义线程类MyThread
。 run()
方法定义了线程启动后要执行的操作。start()
会自动调用run()
,启动线程
2.3 线程间共享数据
import threading
# 初始化共享变量
counter = 0
# 创建锁
lock = threading.Lock()
# 线程要执行的任务
def increment_counter():
global counter
for _ in range(100000):
with lock: # 获取锁
counter += 1
# 创建多个线程
threads = []
for i in range(5):
thread = threading.Thread(target=increment_counter)
threads.append(thread)
thread.start()
# 等待所有线程执行完毕
for thread in threads:
thread.join()
print(f"Final counter value: {counter}")
解释:
counter
是所有线程共享的全局变量。lock
保证同一时间只有一个线程可以修改counter
,防止数据竞争。- 每个线程都会对
counter
执行 100,000 次加法操作,最终结果将是 500,000。
2.4 队列共享线程数据
队列 (queue.Queue
):线程安全的队列,允许线程之间通过队列交换数据。(和进程队列multiprocessing.Queue使用大同小异)
import threading
import queue
import time
def producer(q):
for i in range(5):
time.sleep(1)
item = f"Item {i}"
q.put(item)
print(f"Produced: {item}")
def consumer(q):
while True:
item = q.get()
if item is None:
break
print(f"Consumed: {item}")
q.task_done()
q = queue.Queue()
# 创建生产者和消费者线程
producer_thread = threading.Thread(target=producer, args=(q,))
consumer_thread = threading.Thread(target=consumer, args=(q,))
producer_thread.start()
consumer_thread.start()
# 等待生产者完成
producer_thread.join()
# 结束消费者线程
q.put(None)
consumer_thread.join()
解释:
queue.Queue
是线程安全的,多个线程可以安全地进行put()
和get()
操作。- 生产者线程通过
put()
向队列中放入数据,消费者线程通过get()
从队列中取数据。 - 通过
q.put(None)
向消费者发送结束信号。
2.5 线程池
线程池是一种并发编程设计模式,允许一次创建多个线程并复用它们执行多个任务,而不是为每个任务创建一个新的线程。Python 提供了 ThreadPoolExecutor
来管理线程池,常用线程池来自concurrent.futures模块。
线程池示例:
from concurrent.futures import ThreadPoolExecutor
import time
def task(n):
time.sleep(1)
print(f"Task {n} done")
return n * 2
# 创建线程池
with ThreadPoolExecutor(max_workers=3) as executor:
futures = [executor.submit(task, i) for i in range(5)]
# 获取任务结果
for future in futures:
print(f"Result: {future.result()}")
2.5.1 初始化语法
executor=ThreadPoolExecutor(max_workers=None, thread_name_prefix=‘’, initializer=None, initargs=())
- max_workers: 指定线程池中同时运行的最大线程数。如果设置为
None
,则默认值为系统处理器数量乘以 5。 - thread_name_prefix: 可选参数,用于为每个线程分配一个前缀名,便于调试或日志输出。
- initializer: 可选参数,指定每个线程启动时运行的初始化函数。
- initargs: 可选参数,与
initializer
配合使用,为初始化函数传递参数。
2.5.2 常用方法
-
res=submit(fn, *args, kwargs): 将一个任务(函数及其参数)提交给线程池并异步执行,返回
Future
对象。fn:要执行的函数。
*args, **kwargs
:fn参数res:返回值,future对象,future.result()来取值,futrue对象的常用方法如下:
result(timeout=None): 获取任务的结果,如果任务未完成,则阻塞直到任务完成或超时。
done(): 检查任务是否完成。
exception(timeout=None): 获取任务执行时抛出的异常,如果没有异常,则返回
None
。 add_done_callback(fn): 为任务添加一个回调函数,当任务完成时调用该函数。
-
list=map(func, *iterables, timeout=None, chunksize=1): 并发执行
func
函数,对每个可迭代对象中的元素应用func
。返回与输入迭代器顺序相同的结果。func:需要异步执行的函数;
iterables:可迭代对象,如列表等,每一次func执行,都会从iterables中取参数;
timeout:设置每次异步操作的超时时间,timeout的值可以是int或float,如果操作超时,会返回raisesTimeoutError;如果不指定timeout参数,则不设置超时间。
list:返回值,列表类型。
-
shutdown(wait=True, cancel_futures=False): 关闭线程池,停止接受新任务。
wait=True
表示等待所有任务完成后再关闭。wait: 默认是True,如果为 True,则阻塞主进程直到所有任务完成。
cancel_futures:默认:False, 如果为
True
,则尝试取消所有尚未开始执行的任务。 -
Future.result(timeout=None): 获取任务的结果,如果任务还未完成则阻塞,直到有结果或超时。
2.5.3 示例:submit()方法提交单个任务
from concurrent.futures import ThreadPoolExecutor
import time
def task(name):
print(f"Task {name} is starting.")
time.sleep(2)
print(f"Task {name} is completed.")
return f"Result of {name}"
# 创建一个线程池,最多同时运行 3 个线程
with ThreadPoolExecutor(max_workers=3) as executor:
future = executor.submit(task, 'A') # 提交一个任务
result = future.result() # 获取任务结果
print(result)
解释:
用法和ProcessPoolExecutor基本一致。
2.5.4 示例:使用submit()提交多个任务
from concurrent.futures import ThreadPoolExecutor
def square(n):
return n * n
numbers = [1, 2, 3, 4, 5]
# 创建线程池
with ThreadPoolExecutor(max_workers=3) as executor:
# 提交多个任务
futures = [executor.submit(square, num) for num in numbers]
# 获取每个任务的结果
results = [future.result() for future in futures]
print(results)
2.5.5 示例:使用map()并行执行任务
from concurrent.futures import ThreadPoolExecutor
def square(n):
return n * n
numbers = [1, 2, 3, 4, 5]
# 创建线程池
with ThreadPoolExecutor(max_workers=3) as executor:
# 使用 map 并行执行任务
results = list(executor.map(square, numbers))
print(results)
3、多进程和多线程的选择
3.1 选择多进程和多线程的环境
3.1.1 选择多进程的环境
优点:
- 不受 GIL 的限制,能够利用多核 CPU,真正实现并行计算。
- 进程之间是隔离的,进程之间的干扰较小,出现死锁或数据竞争的可能性更小。
缺点:
- 开销较大:进程的创建、销毁和切换的开销比线程大,尤其是进程之间的数据共享需要使用队列、管道或其他进程间通信(IPC)机制。
- 进程间的数据共享较麻烦,可能会引入额外的复杂性。
适合场景:
- CPU 密集型:任务需要大量计算资源,GIL 限制了 Python 中多线程对 CPU 的利用率时,使用多进程可以绕过 GIL,充分利用多核 CPU 并行执行任务。
- 任务独立且对共享数据依赖少:进程间的数据隔离有助于降低数据竞争的风险,适用于独立运行的任务。
- 安全性和稳定性:进程之间隔离更严格,崩溃或卡死的进程不会影响其他进程,适合需要高稳定性的任务。
例子:
- 图像处理:每个进程独立处理一部分图像,充分利用 CPU 并行处理。
- 数据分析:处理大规模数据集时,使用多个进程来并行化计算任务。
3.1.2 选择多线程环境
优点:
- 轻量级,创建、销毁开销小,切换线程速度快。
- 线程之间可以共享内存空间,适合需要共享数据的任务。
缺点:
- 全局解释器锁 (GIL) 限制:在 CPython 中,GIL 使得同一时刻只有一个线程执行 Python 字节码,因此多线程不能充分利用多核 CPU 的计算能力。这对于 CPU 密集型任务(如数学计算、图像处理等)来说是个瓶颈。
- 数据共享虽然方便,但容易导致竞争条件、死锁等问题,处理并发时需要使用锁等同步机制,增加复杂性。
适合场景:
- I/O 密集型:任务的大部分时间都在等待外部资源(例如网络、文件等)时使用多线程。多线程可以在等待期间执行其他任务,从而提高整体吞吐量。
- 任务需要频繁访问和修改共享数据:在同一个内存空间中多个线程可以直接访问同一块数据,而不需要 IPC 机制。
- 资源开销小:线程切换比进程切换更轻量,创建线程比创建进程快,适用于轻量任务。
例子:
- 网络爬虫:多个线程可以同时发送请求、处理数据,而无需等待每次 I/O 操作完成。
- Web 服务器:同时处理多个用户的请求时,用户请求常常涉及 I/O 操作(如数据库查询),这时多线程可以提高响应速度。
3.1.3 总结
I/O 密集型任务: 优先考虑 多线程。
CPU 密集型任务: 优先考虑 多进程,绕过 GIL 限制,提升计算效率。
任务轻量且需要频繁共享数据: 多线程 适合,因为线程间共享内存相对简单。
任务独立、开销大且需要最大化利用 CPU: 多进程 是理想选择,充分利用多核处理能力。
4、异步编程(asyncio)
4.1 异步编程的理解
- 异步编程是什么?
异步编程的关键思想是:当我们在等待某些操作时,程序不会停下来傻等,而是去处理其他工作。就像一个人一边做饭,一边等着锅里的水烧开。传统的同步编程就像一个人一直盯着水壶,直到水开了才继续做其他事。而异步编程让我们在等水烧开的同时,可以去切菜、准备调料等。
- 它实现了什么呢?
单线程程序传统意义上的同步编程避不可避的问题是等待,等待IO、网络请求、文件读取等,这就是阻塞,而异步它就解决了**阻塞(blocking)*问题。因为异步编程通过*非阻塞机制,让程序在等待的同时,继续执行其他任务,从而提升效率。
- 它在python中是如何实现的?就是我们接下来要学习的内容。
Python 中的异步编程基于 协程 和 事件循环,通过非阻塞操作来实现并发执行。在单线程内,程序可以通过暂停当前任务来执行其他任务,从而提升并发能力。下面详细说明 Python 异步编程的原理、实现语法以及示例。
4.2 python异步核心概念
Python实现异步离不开协程 (Coroutine)和事件循环 (Event Loop),python 提供了 asyncio
库,帮助实现异步编程。async
和 await
是异步编程的两个关键字。
- 协程 (Coroutine):协程是异步函数的执行单位,和普通函数类似,但可以在运行中暂停,并在合适的时候恢复执行。协程是通过
async def
定义的,使用await
来暂停当前协程并等待另一个协程完成。 - 事件循环 (Event Loop):事件循环是负责调度和管理异步任务的核心,它不断运行,检查是否有协程完成、任务需要调度。协程挂起时,事件循环可以调度其他协程执行。
- await关键字:
await
用于暂停协程的执行,等待另一个awaitable
对象(如另一个协程或异步函数)完成,并在它完成后恢复执行。 - async和await:
async def
用于声明一个协程,await
用于等待协程执行结果。 - Task (任务):任务是协程的一个包装,用于提交给事件循环执行。
asyncio.create_task()
可以用来创建一个任务,将其添加到事件循环中。
异步编程的简单例子,看例子理解核心概念:
import asyncio
async def task1():
print("Task 1: Started")
await asyncio.sleep(2) # 模拟 I/O 操作,挂起协程 2 秒钟
print("Task 1: Resumed after 2 seconds")
async def task2():
print("Task 2: Started")
await asyncio.sleep(1) # 挂起协程 1 秒钟
print("Task 2: Resumed after 1 second")
async def main():
# 同时执行两个任务
await asyncio.gather(task1(), task2())
# 运行主协程
asyncio.run(main())
输出:
Task 1: Started
Task 2: Started
Task 2: Resumed after 1 second
Task 1: Resumed after 2 seconds
在这个例子中,task1
和 task2
都在不同的时间点挂起等待 asyncio.sleep()
完成。当定时器触发时,它们各自被事件循环唤醒,继续执行后续代码。
总结:
- 协程在遇到
await
时被挂起,等待某个异步操作(如 I/O、定时器等)完成。 - 事件循环会在操作完成时唤醒协程,继续执行。
- 这让程序在等待期间不会浪费时间,而是可以执行其他任务,大大提高了并发处理的效率。
4.2 异步编程的工作原理
- 当程序执行一个异步函数并遇到
await
时,执行会暂停并让出控制权,这使得事件循环可以去执行其他任务。 - 在异步任务等待(如 I/O 操作)完成时,事件循环不阻塞,而是继续处理其他异步任务。
- 当异步任务完成时,事件循环将继续调度并恢复暂停的任务。
通过这种方式,异步编程避免了传统同步编程中阻塞的缺点,大幅提升了并发效率。
4.3 异步编程语法及顺序说明
4.3.1 定义协程函数
一个异步编程的开始就需要定义协程函数。使用 async def
定义一个异步函数(即协程),它表示一个可以异步执行的函数。协程是 Python 中的基本异步单元。具体定义如下:
async def my_coroutine():
# 这是一个协程
pass
4.3.2 在协程中使用关键字await
定义异步(协程)函数只是一个开始,一个异步函数中若没有await
关键字的语句,异步函数也将和普通函数一样无法被挂起;而await
关键字的语句也只能在异步函数中使用,否则将抛出异常。
async def my_coroutine():
await asyncio.sleep(1) # 暂停协程 1 秒钟
4.3.3 运行协程
如果是单协程异步编程,在定义玩一个携程后就可以运行携程了。
asyncio.run(my_coroutine()) # 启动事件循环并运行协程,my_coroutine()就是协程函数。
4.3.4 并发执行多个协程
如果我们是并发执行多个协程定义玩协程不能直接执行的,什么是并发协程?就是同时执行多个协程,当某个协程等待就下个协程开始,以此类推。并发协还需要建立个主协程,主协程中使用通过 asyncio.gather()
或 asyncio.create_task()
这个方法提交需要并发的所有协程,然后执行主协程即完成了整个异步编程。
以下分别使用asyncio.gather()
或 asyncio.create_task()
使用这两种并发协程方式示例如下:
import asyncio
# 定义两个异步任务
async def task1():
print("Task 1 started")
await asyncio.sleep(2)
print("Task 1 finished")
async def task2():
print("Task 2 started")
await asyncio.sleep(1)
print("Task 2 finished")
# 主协程并发执行多个任务
async def main():
await asyncio.gather(task1(), task2()) # 并发执行 task1 和 task2
# 运行主协程
asyncio.run(main())
输出:
Task 1 started
Task 2 started
Task 2 finished
Task 1 finished
实例说明:
- 并发执行:通过
asyncio.gather()
可以同时启动多个协程(task1
和task2
)。虽然task1
比task2
运行时间长,但它们是并发执行的,而不是一个任务完成后才执行下一个。 - 非阻塞:在
await asyncio.sleep()
期间,事件循环没有闲置,而是继续执行其他任务。
使用 asyncio.create_task()
这种方式:
import asyncio
# 定义一个异步函数
async def task(name, duration):
print(f"Task {name} started")
await asyncio.sleep(duration)
print(f"Task {name} finished after {duration} seconds")
# 主协程
async def main():
# 创建独立的任务
task1 = asyncio.create_task(task("A", 2))
task2 = asyncio.create_task(task("B", 1))
print("Tasks created, now waiting...")
# 等待任务完成
await task1
await task2
# 运行主协程
asyncio.run(main())
输出:
Task A started
Task B started
Tasks created, now waiting...
Task B finished after 1 seconds
Task A finished after 2 seconds
示例说明:
asyncio.create_task()
:用于创建独立的异步任务,这些任务会立即开始执行,并且可以在稍后await
它们的完成。- 并发性:任务 A 和任务 B 是并发执行的,
Task B
更快完成,但Task A
并不会阻塞它。
4.4 异步编程步骤总结
- 定义异步函数:
async def
用于定义协程。 - 启动事件循环:通过
asyncio.run()
启动事件循环,运行主协程。 - 协程内的
await
:遇到await
时,协程会挂起等待异步操作完成,同时事件循环会调度其他协程。 - 并发执行多个任务:通过
asyncio.gather()
或asyncio.create_task()
,可以并发执行多个协程,事件循环负责调度它们的执行。
步骤图示:
时间轴 ---->
事件循环:
┌───Task 1 开始──┬───[等待中,执行其他任务]───┬───Task 1 结束───┐
│ │ │ │
└───Task 2 开始──┴───Task 2 结束───────────────┘
4.4.1 总结
- 定义协程:使用
async def
定义异步函数,并使用await
来执行异步操作。 - 运行协程:通过
asyncio.run()
启动事件循环并运行协程。 - 并发执行:通过
asyncio.gather()
或asyncio.create_task()
并发执行多个协程,利用事件循环高效调度任务,最大限度利用资源。 - 适用场景:异步编程特别适用于 I/O 密集型操作,如网络请求、文件读写等,通过减少等待时间提升程序性能。
注意:
异步的并发和多进程和多线程的并发是有着本质的区别,异步是在单线程中运作,本质上讲同一个时间点只有一个任务在执行,只是科学统筹的方法在一个任务等待的时候执行其他任务,有效的利用空闲时间提高效率,不像多进程和多线程在同一个时间上可不止一个任务在执行。
(十五)、语法糖
1、语法糖的概念
在 Python 中,语法糖(syntactic sugar) 指的是一种简化或更具可读性的方法来实现代码功能的语法结构。它让代码更直观、更简洁,背后仍然执行与更冗长的标准代码相同的功能。语法糖本质上是语言设计者为了提高开发体验,而提供的一些方便的语法形式,使得程序员可以更方便地表达常见的编程模式。
2、Python的常用语法糖
2.1 数字分隔符
如果遇到比较大的数字时,在阅读的过程中我们需要进行数位数比较麻烦,语法糖使用下划线帮我们解决这个问题,具体例子如下:
a=10_0000_0000 # 这个就是可以代表10个亿,比起1000000000阅读起来方便多了。
2.2 连续比较式
在众多语言中如果我们需要判断某个变量的值是否在某个区间内的时候,一般需要使用到and或者&&与逻辑的判断,当然python也并不可免,不过python提供的一种语法糖也可以绕开这种写法,具体如下例子:
# 常规的写法
a=97
if a>90 and a< =100: # <=写法其实是< =
# 语法糖的写法
if 90<a< = 100:
2.3 列表拼接
两个列表进行拼接,这个在列表中有介绍过,就是两个列表使用+相加就会输出两个列表的元素。
2.4 变量值交换
两个变量的值想交换了话,常规做法是声明第三个变量,以第三个变量作为中介进行转换,python语法糖提供了一个简便的语法,具体如下:
a,b=b,a # 这个就进行了a和b的值的互换。
2.5 字符串的运算
print('-'*30) # 就可以打印出30个”-“。
(十六)、日期和时间
在 Python 中,日期和时间处理是编程中的常见需求,特别是在处理事件、记录时间戳、处理日志文件等场景中。Python 提供了多种工具和库来处理日期和时间,最常用的模块有 datetime
、time
、calendar
以及 dateutil
(外部库)。
1、time模块
time
模块主要用于处理与**时间戳(timestamp)**相关的操作,它提供了一些用于获取和处理 Unix 时间戳的函数。Unix 时间戳是从 1970 年 1 月 1 日 00:00:00 UTC 开始计算的秒数。
1.1 特点
- 基于时间戳:
time
模块中的许多函数都依赖于时间戳(秒为单位)。 - 适用于低级时间操作:通常用于与系统时间相关的低级时间操作,比如暂停程序运行、测量时间差、获取当前时间的时间戳等。
- 支持的精度有限:
time
模块主要支持秒和毫秒级别的时间操作。
1.2 struct_time类
time
模块中唯一类似类的结构是 struct_time
,它是一个时间对象,用来表示时间元组。它包含了年、月、日、时、分、秒等信息,并且可以通过 time.localtime()
、time.gmtime()
等函数来获取。
struct_time
的属性,它是一个包含了 9 个元素的元组,分别表示时间的各个部分:
- tm_year:年,例如 2024。
- tm_mon:月,范围 [1, 12]。
- tm_mday:日,范围 [1, 31]。
- tm_hour:小时,范围 [0, 23]。
- tm_min:分钟,范围 [0, 59]。
- tm_sec:秒,范围 [0, 61],包括闰秒。
- tm_wday:一周中的第几天,范围 [0, 6],0 表示星期一。
- tm_yday:一年中的第几天,范围 [1, 366]。
- tm_isdst:夏令时标记,0 表示非夏令时,1 表示夏令时,-1 表示未知
1.3 常用函数
-
**
time.time()
:**返回当前时间的时间戳,返回值是浮点型。 -
**
time.sleep(seconds)
:**暂停程序执行指定秒数,seconds需要暂停的时间,单位是秒。 -
**
time.localtime([secs])
:**将时间戳转换为当前时区的struct_time
对象,不带入时间戳实参则返回当前时间的struct_time对象。 -
**
time.strftime(format[, t])
:**把struct_time对象转换为指定格式的字符串,t是struct_time对象,t不存在时以当前时间作为格式输出。format常用格式化符号:
%Y
:年份(四位数)%m
:月份(01-12)%d
:日期(01-31)%H
:小时(24小时制)%M
:分钟(00-59)%S
:秒(00-61)
import time
# 格式化当前时间
formatted_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
print("格式化当前时间:", formatted_time)
# 格式化指定时间戳的时间
timestamp = 1695124878
formatted_time_from_timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(timestamp))
print("格式化时间戳的时间:", formatted_time_from_timestamp)
# 不带入参数也可以格式化输出当前的时间
print(time.strftime("%Y-%m-%d %H:%M:%S"))
输出:
格式化当前时间: 2024-09-22 15:11:11
格式化时间戳的时间: 2023-09-19 20:01:18
2024-09-22 15:11:11
t**ime.strptime(string[, format])
:**将一个字符串解析为struct_time
对象。format
参数用于指定输入的字符串格式,解析后的结果是一个struct_time
对象。
例子:
import time
# 将时间字符串解析为 struct_time 对象
time_string = "2024-09-19 15:34:38"
parsed_time = time.strptime(time_string, "%Y-%m-%d %H:%M:%S")
print("解析后的时间:", parsed_time)
# 输出:
# 解析后的时间: time.struct_time(tm_year=2024, tm_mon=9, tm_mday=19, tm_hour=15, tm_min=34, tm_sec=38, tm_wday=4, tm_yday=263, tm_isdst=-1)
- **
time.mktime(t)
:**将一个struct_time
对象转换为时间戳。该函数是time.localtime()
的逆操作。 - **
time.gmtime([secs])
:与time.localtime()
类似,但time.gmtime()
返回的是UTC(协调世界时)**的时间,而不是本地时间。 - **
time.perf_counter()
😗*返回一个高精度的计时器值(包括睡眠时间),用于精确测量程序执行时间。它不与系统时间相关,因此特别适合用于计算耗时操作。
用法如下:
import time
start = time.perf_counter()
# 模拟耗时操作
time.sleep(1)
end = time.perf_counter()
print(f"耗时: {end - start:.4f} 秒") # 耗时: 1.0004 秒
- **
time.process_time()
😗*返回当前进程使用的 CPU 时间,不包括睡眠时间或其他进程的 CPU 时间。适用于衡量 CPU 密集型任务的执行时间。
例子:
import time
start = time.process_time()
# 模拟 CPU 密集型任务
for _ in range(1000000):
pass
end = time.process_time()
print(f"CPU 耗时: {end - start:.4f} 秒") # 输出: CPU 耗时: 0.0100 秒
2、datetime模块
datetime
模块是 Python 中更高级的日期和时间处理模块,提供了对日期和时间的全面支持。它支持对日期、时间、时区、时间差等的操作,适合需要精确控制日期和时间的场景。
2.1 特点
- 更丰富的数据类型:支持
datetime
、date
、time
、timedelta
等多个类型,分别用于表示不同的时间相关概念。 - 适合高级日期和时间处理:支持日期和时间的加减、比较、时区处理等操作。
- 更易读的日期时间对象:相比
time
模块的时间戳和struct_time
对象,datetime
提供了更加人性化的日期时间表示形式。
2.2 常用类及常用方法
2.2.1 datetime.datetime类
定义:表示日期和时间(年、月、日、小时、分钟、秒、微秒)。
功能:支持日期时间的操作、比较、格式化和转换。
常用方法:
now(tz=None)
: 获取当前本地日期时间,如果传入时区,则返回相应时区的时间。today()
: 获取当前本地日期和时间。utcnow()
: 获取当前的 UTC 时间(不带时区信息)。fromtimestamp(timestamp, tz=None)
: 根据给定的时间戳返回日期时间对象,可选地带有时区。combine(date, time, tzinfo=None)
: 将一个日期对象和时间对象组合为一个日期时间对象。strftime(format)
: 将日期时间对象格式化为指定的字符串格式。strptime(date_string, format)
: 将字符串解析为日期时间对象,format
参数指定字符串的格式。
示例如下:
from datetime import datetime
now = datetime.now() # 当前时间
print(now) # 输出 2024-09-22 16:14:52.332620
timestamp = 1695124878
dt_from_timestamp = datetime.fromtimestamp(timestamp) # 从时间戳创建时间对象
print(dt_from_timestamp) # 输出: 2023-09-19 20:01:18
formatted_time = now.strftime("%Y-%m-%d %H:%M:%S") # 格式化时间
print(formatted_time) # 输出:2024-09-22 16:14:52
str="2024-09-22 16:14:52"
dt=datetime.strptime(str,"%Y-%m-%d %H:%M:%S")# 转换成datetime类型
print(dt)
2.2.2 datetime.date类
表示:日期(年、月、日),不包含时间信息。
常用方法:
today()
: 返回当前的日期。fromtimestamp(timestamp)
: 根据时间戳返回日期对象。date1.strftime(format)
: 将日期对象格式化为字符串,date1是个date对象。replace(year, month, day)
: 替换日期对象的年、月、日。isoformat()
: 以YYYY-MM-DD
格式返回日期的 ISO 格式化字符串。weekday()
: 返回当前日期是星期几(0 表示周一,6 表示周日)date1.weekday()返回指定日期的星期。fromisoformat(date_string)
: 从 ISO 格式字符串创建日期对象。- **
fromtimestamp(timestamp)
:**根据给定的 Unix 时间戳(秒数)返回日期对象。
例子:
from datetime import date
today = date.today() # 当前日期
print(today) # 输出:2024-09-22
new_date = today.replace(year=2025) # 修改日期年份
print(new_date) # 输出:2025-09-22
iso_str = today.isoformat() # 获取 ISO 格式字符串
print(iso_str) # 输出:2024-09-22
2.2.3 datetime.time类
表示:时间(时、分、秒、微秒),不包含日期信息。
常用方法:
replace(hour, minute, second, microsecond)
: 替换时间对象的时、分、秒、微秒。
isoformat()
: 返回 HH:MM:SS
格式的 ISO 时间字符串。
strftime(format)
: 将时间对象格式化为字符串。
isoformat()
: 以 HH:MM:SS
格式返回时间的 ISO 格式化字符串。
例子:
from datetime import time
t = time(12, 30, 45) # 创建时间对象
print(t)
formatted_time = t.strftime("%H:%M:%S") # 格式化时间
print(formatted_time)
new_time = t.replace(hour=14) # 替换时间中的小时
print(new_time)
2.3 时间间隔计算
在 Python 中,可以使用 datetime
模块来进行日期和时间的加减操作。通过 timedelta
对象,您可以轻松加减天、小时、分钟等时间间隔。要进行月份和年份的加减操作,可以借助 relativedelta
,该类来自 dateutil
模块。
2.3.1 天以内的计算
可以使用 datetime.timedelta
对象,它是datetime模块中一个类,主要用来计算天以内的间隔计算。
timedelta对象的初始化属性如下:
days:天 hours:时 minutes:分 seconds:秒 microseconds:微秒
实现例子:
from datetime import datetime, timedelta
# 获取当前日期和时间
now = datetime.now()
# 加 5 天
new_date = now + timedelta(days=5)
# 加 3 小时
new_time = now + timedelta(hours=3)
delta = timedelta(days=5, hours=3) # 表示5天3小时
diff = timedelta(hours=5) - timedelta(minutes=30)
print(diff) # 输出: 4:30:00
2.3.2 月以上的计算
对于月份和年份,datetime.timedelta
不支持,需要使用 dateutil.relativedelta
,它对timedelta支持的都支持,也就是天以下。
from datetime import datetime
from dateutil.relativedelta import relativedelta
# 获取当前日期和时间
now = datetime.now()
# 加 2 个月
new_date = now + relativedelta(months=2)
# 加 1 年
new_date_year = now + relativedelta(years=1)
# 输出结果
print(f"当前时间: {now}")
print(f"加 2 个月后的日期: {new_date}")
print(f"加 1 年后的日期: {new_date_year}")
3 time和datetim模块的区别
实用场景:
time
模块 适合以下情况:
- 需要处理系统时间或与 Unix 时间戳相关的操作。
- 需要实现简单的延迟(比如
time.sleep()
)。 - 需要执行时间格式化或解析操作,但不涉及复杂的日期操作(比如时区、日期加减等)。
datetime
模块 适合以下情况:
- 需要处理日期、时间、时区的高级操作。
- 需要进行日期时间的加减运算(如
timedelta
)。 - 需要处理与特定日期或时间格式相关的复杂计算(比如工作日、节假日计算)。
- 需要生成、解析和格式化日期与时间。
二者对比如下:
功能/特性 | time 模块 | datetime 模块 |
---|---|---|
主要用途 | 系统级时间操作、时间戳、格式化操作 | 日期和时间的高级操作、时区、时间差计算等 |
时间表示 | Unix 时间戳、struct_time | datetime 、date 、time 、timedelta 对象 |
时间戳支持 | 主要依赖 Unix 时间戳 | 可通过 timestamp() 和 fromtimestamp() 互换 |
时区支持 | 无 | 支持时区(通过 timezone 和 tzinfo ) |
时间加减运算 | 无 | 支持(通过 timedelta ) |
延迟操作 | time.sleep() | 无 |
总结:
time
模块:更偏向于与系统级时间操作的交互,适合轻量级的时间处理,如时间戳和程序暂停。datetime
模块:提供了丰富的日期和时间处理功能,适用于需要复杂时间计算、时区处理和格式化的场景。
(十七)内置函数
1、数据类型转换
int(x)
:将 x
转换为整数。
float(x)
:将 x
转换为浮点数。
str(x)
:将 x
转换为字符串。
list(iterable)
:将可迭代对象转换为列表。
tuple(iterable)
:将可迭代对象转换为元组。
set(iterable)
:将可迭代对象转换为集合。
2、数学和统计
abs(x)
:返回 x
的绝对值。
max(iterable, \*[, key])
:返回可迭代对象中的最大值。
min(iterable, \*[, key])
:返回可迭代对象中的最小值。
sum(iterable, /, start=0)
:返回可迭代对象的总和。
3、序列操作
len(s)
:返回对象(如字符串、列表、元组等)的长度。
sorted(iterable, /, \*, key=None, reverse=False)
:返回一个排序后的列表。
reversed(seq)
:返回一个反向迭代器。
4、逻辑和判断
all(iterable)
:如果可迭代对象中的所有元素都为真,则返回 True
,例如判断一个列表中的只要有个元素为空就返回False。
any(iterable)
:如果可迭代对象中至少有一个元素为真,则返回 True
。
sum()
:返回可迭代对象中所有元素的和。
5、迭代和生成器
enumerate(iterable, start=0)
:返回可迭代对象的索引和元素的迭代器。
zip(\*iterables)
:将多个可迭代对象的元素打包成元组。
6、输入和输出
print(\*objects, sep=' ', end='\n', file=sys.stdout)
:将对象打印到标准输出。
input(prompt)
:从标准输入读取一行。
7、其他
type(object)
:返回对象的类型。
id(object)
:返回对象的唯一标识符。
dir(object)
:返回对象的属性和方法列表。
help(object)
:调用内置的帮助系统。
**globals()
:**返回全部全局变量。
**locals()
:**返回全部的局部变量。
(十八)、文件操作
1、文件读写操作步骤
在 Python 中,文件操作是通过内置的 open()
函数和文件对象来完成的。open()
函数用于打开文件并返回一个文件对象,之后可以使用该文件对象进行读写操作。文件操作的常见任务包括读取文件内容、写入数据、追加内容、文件指针操作等。
1.1 open打开文件
open()
函数用于打开文件,返回一个文件对象,基本语法如下:
file = open(filename, mode, encoding=None)
filename
:文件路径,可以是绝对路径或相对路径。mode
:指定打开文件的模式,如只读、写入、追加等。
常见的文件打开模式如下:
模式 | 描述 |
---|---|
'r' | 只读模式(默认),文件必须存在。 |
'w' | 写入模式,若文件存在则清空内容,不存在则创建新文件。 |
'a' | 追加模式,写入的数据会附加到文件末尾。 |
'b' | 二进制模式,与其他模式结合使用,如 'rb' , 'wb' 。 |
'x' | 创建模式,文件必须不存在,否则抛出异常。 |
't' | 文本模式(默认),与其他模式结合使用。 |
'+' | 读写模式,与 'r+' , 'w+' , 'a+' 结合使用。 |
encoding
:指定编码方式,通常在处理文本文件时指定(如utf-8
)。
1.2 文件读取操作
文件读取有多种方法,常见的有 read()
、readline()
和 readlines()
。
read()
:read(size)
方法一次读取指定的字符数(字节数)。如果不指定size
,则读取整个文件。
with open('example.txt', 'r', encoding='utf-8') as file:
content = file.read() # 读取整个文件内容
print(content)
readline()
: 方法每次读取一行,通常用于按行处理大文件。
with open('example.txt', 'r', encoding='utf-8') as file:
line = file.readline() # 读取文件的第一行
print(line)
readlines()
:方法一次读取文件的所有行,并返回一个列表。
with open('example.txt', 'r', encoding='utf-8') as file:
lines = file.readlines() # 返回所有行的列表
for line in lines:
print(line)
1.3 文件写入操作
写入操作可以通过 write()
和 writelines()
来完成。
write()
:方法用于将字符串写入文件。如果文件不存在,Python 会根据模式自动创建文件。
with open('example.txt', 'w', encoding='utf-8') as file:
file.write('Hello, World!') # 写入一行文本
writelines()
:方法用于写入一个字符串列表,不会自动添加换行符。
lines = ['Hello\n', 'World\n']
with open('example.txt', 'w', encoding='utf-8') as file:
file.writelines(lines) # 写入多行文本
1.4 文件关闭
通过 close()
方法显式关闭文件,但更推荐使用 with
语句,因为它会在块结束时自动关闭文件,避免文件未关闭的问题。
2、文件指针操作
在 Python 的文件操作中,文件指针(或称为“文件流位置指针”)用于指示当前读写操作的位置。文件指针的操作对于灵活读取和写入文件非常重要,尤其是在处理大文件时。
2.1 文件指针的应用
文件对象的指针可以通过 tell()
和 seek()
来控制,用于更灵活地读取和写入。
tell()
:方法返回当前文件指针的位置。
with open('example.txt', 'r', encoding='utf-8') as file:
print(file.tell()) # 输出当前指针位置
seek()
:seek(offset, whence)
用于移动文件指针,whence
指定相对位置:
0
:相对于文件开头(默认)。1
:相对于当前文件指针位置。2
:相对于文件末尾。
with open('example.txt', 'r', encoding='utf-8') as file:
file.seek(5) # 移动指针到文件的第 5 个字符位置
print(file.read()) # 从指针位置开始读取,指针也会相应移动直到读完一行记录下指针值
2.2 文件指针的意义
它的意义主要体现在以下几个方面:
- **控制读写位置:**文件指针的核心作用是控制读取或写入数据的起始位置。当我们打开文件时,文件指针默认位于文件的开头,但通过文件指针操作,我们可以在文件的任意位置进行读写操作。
with open('example.txt', 'r') as file:
file.seek(10) # 将文件指针移动到第 10 个字节位置
print(file.read()) # 从当前位置开始读取数据
- **随机读写:**文件指针允许在文件中的任意位置进行读写操作,支持随机存取文件内容。在某些情况下,我们只需要访问文件的某些部分,而不是从头开始读取整个文件。通过文件指针,可以跳转到特定位置来进行读取或写入,避免不必要的文件遍历操作。
with open('example.txt', 'rb') as file:
file.seek(-10, 2) # 移动到文件末尾前 10 个字节处
print(file.read()) # 从该位置读取剩余的 10 个字节
- **高效处理大文件:**在处理非常大的文件时,一次性读取整个文件可能会导致内存不足或性能问题。这时,可以通过文件指针来分段读取文件的部分内容,而不是加载整个文件到内存中。
chunk_size = 1024 # 每次读取 1024 字节
with open('large_file.txt', 'r') as file:
while True:
chunk = file.read(chunk_size) # 读取一块数据
if not chunk:
break # 文件读取完毕
print(chunk)
# 通过文件指针的自动移动,read() 每次只读取指定大小的块,处理大文件时更加高效。
- **文件重写:**文件指针允许我们灵活控制文件的写入位置,从而可以在文件中间插入或修改内容。通过
seek()
和tell()
,可以定位到文件的任意位置进行覆盖写入。
with open('example.txt', 'r+') as file:
file.seek(5) # 将指针移动到第 5 个字符位置
file.write("INSERTED") # 从该位置开始写入内容,覆盖现有内容
- **灵活处理文本和二进制文件:**文件指针操作不仅适用于文本文件,也适用于二进制文件。例如,在处理图像、音频、视频等二进制文件时,文件指针可以定位到特定的字节偏移处,进行精确的数据读写。
with open('image.png', 'rb') as file:
file.seek(128) # 移动到文件中的第 128 个字节
header = file.read(32) # 读取 32 个字节
print(header)
(十九)、Json格式解析
在 Python 中,json
模块用于解析和生成 JSON 数据。JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,易于人类阅读和编写,也易于机器解析和生成。Python 提供了内置的 json
模块,支持将 JSON 格式的字符串解析为 Python 数据类型,以及将 Python 数据类型序列化为 JSON 格式的字符串。
1、模块应用
1.1 模块的常用函数
json.loads(s, *, cls=None, object_hook=None, parse_float=None, parse_int=None, parse_constant=None, object_pairs_hook=None)
:将 JSON 格式的字符串解析为 Python 对象。
常用形参说明:
s
:必选,要解析的 JSON 字符串。该字符串必须是有效的 JSON 格式,否则会抛出json.JSONDecodeError
。。cls
:可选,用于指定一个自定义的 JSON 解码器类(继承自json.JSONDecoder
)。默认使用json.JSONDecoder
。自定义解码器可以用来扩展或修改 JSON 的解析行为。object_hook
:一个函数,将转换后的字典作为输入,并返回自定义对象。通常用于自定义字典到对象的转换。parse_float
:可选,一个函数,允许自定义解析 JSON 中的浮点数。默认情况下,JSON 的浮点数会被解析为 Python 的float
类型。parse_int
:可选,一个函数,允许自定义解析 JSON 中的整数。默认情况下,整数会被解析为 Python 的int
类型。parse_constant
:可选,用于解析特殊的 JSON 值如NaN
、Infinity
和-Infinity
。object_pairs_hook
:可选,接受一个函数,解析时被调用。与object_hook
类似,但它处理的是键值对的列表,而不是单独的字典。
json.load(fp, *, cls=None, object_hook=None, parse_float=None, parse_int=None, parse_constant=None, object_pairs_hook=None)
:从文件中读取 JSON 数据并解析为 Python 对象。
常用形参说明:
fp
:表示读取的文件对象。- 其他参数与
json.loads()
几乎一致。
json.dumps(obj, *, skipkeys=False, ensure_ascii=True, check_circular=True, allow_nan=True, cls=None, indent=None, separators=None, default=None, sort_keys=False)
:将 Python 对象序列化为 JSON 格式的字符串。
常用形参说明:
obj
:要序列化的 Python 对象。skipkeys
(默认False
):如果为True
,当字典中的键不是基本类型(如字符串、数字、元组)时,跳过这些键而不会抛出错误。ensure_ascii
(默认True
):如果为True
,所有非 ASCII 字符会被转义为\uXXXX
序列。如果为False
,允许输出原始 Unicode 字符。check_circular
(默认True
):如果为True
,检查对象中的循环引用并避免无限递归。allow_nan
(默认True
):如果为True
,允许序列化特殊的NaN
、Infinity
和-Infinity
值为 JSON。如果为False
,这些值会引发ValueError
。cls
:指定自定义的JSONEncoder
子类以自定义序列化过程。indent
:如果指定了整数或字符串值,则生成的 JSON 字符串将以缩进的形式美化输出。None
表示紧凑输出。separators
:通过指定元组(如(',', ': ')
),可以自定义 JSON 项之间的分隔符。default
:一个函数,提供无法直接序列化的对象的默认序列化方法。sort_keys
(默认False
):如果为True
,字典中的键按字母顺序排序。
json.dump(obj, fp, *, skipkeys=False, ensure_ascii=True, check_circular=True, allow_nan=True, cls=None, indent=None, separators=None, default=None, sort_keys=False)
:将 Python 对象序列化为 JSON 格式并写入文件。
常用形参说明:
obj
:要序列化的 Python 对象。fp
:文件对象,表示将输出写入的文件。- 其他参数与
json.dumps()
几乎一致。
1.2 json解析的python对象
使用 json.loads()
可以将 JSON 格式的字符串解析为 Python 数据类型。常见的对应关系如下:
JSON 数据类型 | Python 数据类型 |
---|---|
对象(Object) | 字典(dict ) |
数组(Array) | 列表(list ) |
字符串(String) | 字符串(str ) |
数字(Number) | 整数(int )、浮点数(float ) |
布尔值(Boolean) | 布尔值(bool ) |
空(Null) | None |
1.3 python解析json串
1.3.1 json字符串的解析
json.loads()普通示例:
import json
# JSON 字符串
json_string = '{"name": "Alice", "age": 30, "city": "New York"}'
# 将 JSON 字符串解析为 Python 对象(字典)
data = json.loads(json_string)
print(data) # {'name': 'Alice', 'age': 30, 'city': 'New York'}
print(type(data)) # <class 'dict'>
# 访问解析后的数据
print(data['name']) # Alice
示例说明:在这个示例中,json_string
是一个 JSON 格式的字符串,json.loads()
将其解析为一个 Python 字典。
json.loads()带参数object_pairs_hook和object_hook自定义函数解析的例子及二者的使用区别。
import json
# 自定义的解码器
def dict_to_person(d):
print(d)
json_string = '[{"name": "Alice", "age": 25},{"name": "lucy", "age": 29}]'
json_string1 = '{"name": "Alice", "age": 25}'
print("json_string的输出结果:")
json.loads(json_string, object_pairs_hook=dict_to_person) # 从立到外的找出字典,然后把键值对改为元组,把字典转成列表
json.loads(json_string, object_hook=dict_to_person) # 从里到外的去处字典然后调用函数
print("json_string1的输出结果:")
json.loads(json_string, object_pairs_hook=dict_to_person)
json.loads(json_string, object_hook=dict_to_person)
输出:
json_string的输出结果:
[('name', 'Alice'), ('age', 25)]
[('name', 'lucy'), ('age', 29)]
{'name': 'Alice', 'age': 25}
{'name': 'lucy', 'age': 29}
json_string1的输出结果:
[('name', 'Alice'), ('age', 25)]
[('name', 'lucy'), ('age', 29)]
{'name': 'Alice', 'age': 25}
{'name': 'lucy', 'age': 29}
注意:
- 二者都是从里到外的把字典给找出来,如果是字典套字典父级字典的value值改为none,比如{“a”:{“b”:1}},二者取出的字典就是{“b”:1}和{“a”:none}。
- 每取出一个字典就作为参数调用自定义函数一次,顺序是从里到外。
- object_pairs_hook参数带自定义函数时,会把字典的键值转为元组,字典转为列表。
json.loads其他形参的例子:
import json
json_str = '{"age": 25, "price": 19.99}'
data = json.loads(json_str, parse_int=lambda x: float(x))
print(data)# 输出:{'age': 25.0, 'price': 19.99}
1.3.2 json文件中解析
使用 json.load()
可以从文件中直接读取 JSON 数据并解析为 Python 对象。这在处理存储在文件中的 JSON 数据时非常有用。
假设我们有一个名为 data.json
的文件,内容如下:
{
"name": "Bob",
"age": 25,
"city": "San Francisco"
}
可以使用 json.load()
读取该文件并解析内容:
import json
# 从文件中读取 JSON 数据
with open('data.json', 'r') as file:
data = json.load(file)
print(data) # {'name': 'Bob', 'age': 25, 'city': 'San Francisco'}
这里,json.load()
从文件中读取 JSON 数据并将其转换为 Python 字典。
1.3.3 其他json解析例子
自定义对象解析,可以通过自定义的解码器将 JSON 数据还原为自定义对象。
import json
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
# 自定义的解码器
def dict_to_person(d):
return Person(d['name'], d['age'])
json_string = '{"name": "Alice", "age": 25}'
person = json.loads(json_string, object_hook=dict_to_person)
print(person.name) # Alice
print(person.age) # 25
1.4 python对象序列化成json字符串
1.4.1 python对象转成json字符串
使用 json.dumps()
,可以将 Python 对象(如字典、列表等)序列化为 JSON 格式的字符串。这通常用于将 Python 数据结构转换为 JSON 格式以便于传输或存储。
import json
# Python 对象(字典)
data = {
"name": "Charlie",
"age": 35,
"city": "Los Angeles"
}
# 将 Python 对象序列化为 JSON 字符串
json_string = json.dumps(data)
print(json_string) # {"name": "Charlie", "age": 35, "city": "Los Angeles"}
print(type(json_string)) # <class 'str'>
此外,json.dumps()
还可以通过设置参数来更好地控制生成的 JSON 字符串格式,例如设置缩进和排序键。
1.4.2 python对象序列化转文件
json.dump()
可以将 Python 对象序列化为 JSON 格式,并将其写入文件。这在需要将数据存储为 JSON 文件时非常有用。
import json
# Python 对象
data = {
"name": "David",
"age": 28,
"city": "Chicago"
}
# 将 Python 对象序列化为 JSON 并写入文件
with open('output.json', 'w') as file:
json.dump(data, file, indent=4)
print("JSON 数据已写入文件。")
这里,json.dump()
将 Python 对象序列化为 JSON 并写入 output.json
文件,同时使用缩进格式化输出。
1.4.3 其他序列化json示例
自定义对象序列化,假设有一个自定义类 Person
,要将其转换为 JSON,可以通过定义自定义的序列化方法。
import json
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
# 自定义的序列化函数
def person_to_dict(person):
return {
'name': person.name,
'age': person.age
}
person = Person("John", 30)
json_string = json.dumps(person, default=person_to_dict)
print(json_string) # {"name": "John", "age": 30}
(二十)、python数据库的操作
1、主要数据库的连接
在日常工作中存在着诸多的数据操作,以至于需要python操作很多数据库,这里主要介绍主要使用的的数据库SQLserver、MYsql、Oracle三种数据作为例子了解下python是怎么连接数据库的。
1.1 sqlserver数据库连接
SQL Server 通常使用 pyodbc
进行连接。
1.1.1 安装驱动器
pip install pyodbc
1.1.2 连接数据示例
import pyodbc
# 建立数据库连接
conn = pyodbc.connect(
"DRIVER={SQL Server};"
"SERVER=your_server_name;"
"DATABASE=your_database_name;"
"UID=your_username;"
"PWD=your_password;"
)
conn.close() #关闭连接
1.2 mysql数据库连接
1.2.1 安装驱动器
MySQL 通常使用 pymysql
或 mysql-connector-python
进行连接。可以通过以下命令安装 pymysql
:
pip install pymysql
pip install mysql-connector-python
1.2.2 连接数据示例
import pymysql
# 建立数据库连接
conn = pymysql.connect(
host="your_host",
port=3306, # 端口号,如果非默认端口则需要指定
user="your_username",
password="your_password",
database="your_database"
)
conn.close() #关闭连接
# mysql-connector-python
import mysql.connector
host="your_host",
port=3306, # 端口号,如果非默认端口则需要指定
user="your_username",
password="your_password",
database="your_database"
)
1.2.3 二者的主要区别如下:
库类型:
- pymysql:是一个纯 Python 实现的库,易于安装和使用,支持 MySQL 的大多数特性。
- mysql-connector-python:是 MySQL 官方提供的库,包含更多的功能和支持,如 SSL 连接等。
性能:
- pymysql:由于是纯 Python 实现,性能可能稍逊色,尤其在处理大量数据时。
- mysql-connector-python:通常性能更好,因为它与 MySQL 的底层实现更紧密结合。
功能:
- pymysql:支持基本的 CRUD 操作、连接池等功能。
- mysql-connector-python:支持更多高级特性,如异步编程、连接配置选项和事务管理。
文档和社区支持:
- pymysql:文档简单明了,社区支持活跃。
- mysql-connector-python:作为官方库,文档和支持更为详尽。
1.3 Oracle数据库连接
1.3.1 安装驱动器
Oracle 通常使用 cx_Oracle
进行连接。你可以通过以下命令安装该库:
pip install cx_Oracle
注意:在平台上还需要安装 Oracle 的 Instant Client 来支持 Oracle 数据库的连接。
1.3.2 连接数据示例
import cx_Oracle
# 建立数据库连接
conn = cx_Oracle.connect(
user="your_username",
password="your_password",
dsn="your_host/your_service_name"
)
# conn = cx_Oracle.connect('your_username/your_password@your_host/your_service_name')
conn.close() #关闭连接
1.4 连接异常捕获和注意点
为了避免数据库连接错误,请使用try……except来捕获异常。
1.4.1 异常捕获的类型
三种数据库按照连接模块pyodbc、cx_Oracle、pymysql、mysql.connector分分别是:pyodbc.Error、cx_Oracle.DatabaseError、pymysql.MySQLError、mysql.connector.Error。
1.4.2 注意点
- 如果数据库连接异常是不会给创建connection对象的,所以在关闭的时候需要判断连接对象是否存在在进行关闭,构造语句会报错。
if 'connection' in locals():
connection.close()
- 创建连接对象的初始参数远不止这些,具体有哪些看具体模块的connection类,针对这几个模块这些参数都是必填项,比如mysql的两个模块参数还有port端口参数默认是3306。
- 这几个模块的事务参数autocommit默认值是False,表示自动提交时关闭的,每次操作数据库都需要手动提交
- 还有其他如果查询结果展示可以选择,具体查看下面介。
- 这些参数不但在初始化的时候可以设置,初始化后还是可以修改的。
2、数据库操作游标(cursor)
在 Python 中,游标是用于执行数据库操作的重要工具。游标允许你与数据库进行交互,包括执行 SQL 查询、获取结果以及管理事务。以下是游标的详细说明,包括其创建、操作和管理。
注:以下内容对于上章节连接数据库操作的模块都通用,以下以mysql数据库作为例子,有特别数据库不同操作会单独描述。
2.1 游标的创建
游标的创建需要上章节提到了数据库连接返回个connect对象来创建,具体如下例子:
# connection 是数据库连接后返回的connect对象
cursor = connection.cursor() # 创建一个游标对象
2.2 数据库sql执行
游标可以用来执行各种 SQL 语句,如查询、插入、更新和删除。
查询:
cursor.execute("SELECT * FROM users")
results = cursor.fetchall() # 获取所有结果
for row in results:
print(row)
插入、修改、删除:
cursor.execute("INSERT INTO users (name, age) VALUES (%s, %s)", ("Alice", 30)) # 插入
cursor.execute("UPDATE users SET age = %s WHERE name = %s", (31, "Alice")) # 更新
cursor.execute("DELETE FROM users WHERE name = %s", ("Alice",)) # 删除
2.3 游标的常用方法
2.3.1 数据库执行的方法
cursor.execute(query, params=None)
:执行一句sql语句一次,执行结果使用cursor对象其他方法获取。
query:要执行的 SQL 语句(字符串),不能使用?占位符。
params:可选,参数为元组,作为sql语句的参数。
cursor.execute("SELECT * FROM users")
cursor.execute("INSERT INTO users (name, age) VALUES (%s, %s)", ("Alice", 30))
# cursor.execute("INSERT INTO users (name, age) VALUES (?, ?)", ("Alice", 30)) 这种写法会报错哦!
- **
cursor.executemany(sql, seq_of_params)
:**执行一句sql语句多次,主要使用insert into语句居多。
sql:要执行的 SQL 语句(字符串),通常包含参数占位符(如 %s
和?
)。
seq_of_params:一个序列(如列表或元组),每个元素是一个参数元组,表示一组要替换的值。
cursor = connection.cursor()
# 要插入的用户数据
users = [
("Alice", 30),
("Bob", 25),
("Charlie", 35)
]
# 批量插入数据
sql = "INSERT INTO users (name, age) VALUES (%s, %s)"
# sql = "INSERT INTO users (name, age) VALUES (?, ?)" # 这样写也可以
cursor.executemany(sql, users)
- **
cursor.callproc(procname, params=None)
:**执行存储过程。
procname:存储过程的名称(字符串)。
params:可选,参数元组。
返回值:返回一个包含存储过程返回值的列表(如果有)。
list=cursor.callproc('my_stored_procedure', (param1, param2)) # 如果存储过程有返回查询数据就在list体现。
- **
cursor.close()
😗*关闭游标。
注意:
- 执行数据库操作execute和executemany都不可以执行存储过程,callproc也不能执行增删改查的动作。
- execute和executemany都不允许执行复合语句(带分号隔开的两句语句以上的sql语句),它们只能一句句的执行,所以复合语句可以分开执行,具体例子如下:
cursor.execute("set @a:=1;") cursor.execute("select * from user where id=@a;")
2.3.2 查询执行结果方法
查询结果的形式由connection对象的cursorclass属性决定,默认是:connection.cursorclass=pymysql.cursors.Cursor
:
展示的结果是:嵌套的元组,里面元组代表一行数据,元组的元素代表数据的一列值,缺点只展示数据,不展示数据名,比如age=12,它只展示12。
connection.cursorclass=pymysql.cursors.DictCursor
时:
展示结果是:每行是字典的列表,整个数据是列表,每行的字典是其元素;字典中的键值对是字段名和数据。
游标提供多种方法来获取查询结果:
fetchone()
:获取下一行结果,返回一个元组,如果没有返回数据,则返回None
代表结束。
cursor=connection.cursor()
cursor.execute("select * from user")
while True:
rows=cursor.fetchone() # 每执行一次获取一行
if rows==None:break
print(rows)
输出结果:
1, None, 'lucy')
(2, datetime.datetime(2024, 9, 5, 0, 0), 'dave、')
(3, datetime.datetime(2024, 9, 13, 0, 0), '张三')
(7, datetime.datetime(2024, 9, 24, 13, 33, 45), 'kk')
fetchall()
:获取所有结果,返回一个包含所有行的嵌套元组,每行数据一个元组。
cursor=connection.cursor()
cursor.execute("select * from user")
print(cursor.fetchall())
# 输出结果:总共4行数据4个元组
# ((1, None, 'lucy'), (2, datetime.datetime(2024, 9, 5, 0, 0), 'dave、'), (3, datetime.datetime(2024, 9, 13, 0, 0), '张三'), (7, datetime.datetime(2024, 9, 24, 13, 33, 45), 'kk'))
fetchmany(size)
:返回一个包含指定数量行的列表,每行是一个元组。
**size:**指定要获取的行数。
cursor.execute("SELECT * FROM users")
rows = cursor.fetchmany(2) # 获取前两行
for row in rows:
print(row)
2.4 游标常用属性
rowcount
:返回最近执行的操作影响的行数。例如,插入、更新或删除的行数。description
:返回列的信息,包括列名和类型,通常用于描述查询结果。
3、数据库事务处理
数据库事务( transaction)是访问并可能操作各种数据项的一个数据库操作序列,这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位。事务由事务开始与事务结束之间执行的全部数据库操作组成。
事务对于数据非常重要,它可以控制数据库的数据完整性等等作用。
3.1 事务自动提交设置
在默认情况下众多模块连接时自动提交事务是关闭的。
- 可以在创建连接对象时初始化参数中修改,如下:
conn=pymysql.connect(……,autocommit=True)
- 也是可以在创建连接对象后进行修改:
conn.autocommit(True)
3.2 事务操作
3.2.1 自动提交关闭
在默认情况下,在执行 DML 操作(如 INSERT
、UPDATE
、DELETE
)时需要手动提交事务。你可以使用 commit()
提交事务,或者使用 rollback()
回滚事务。
try:
cursor.execute("INSERT INTO users (name, age) VALUES (%s, %s)", ("Bob", 25))
cursor.execute("INSERT INTO users (name, age) VALUES (%s, %s)", ("lucy", 25))
connection.commit() # 提交事务
print("事务提交成功!")
except pymysql.MySQLError as e:
print(f"事务失败: {e}")
connection.rollback() # 回滚事务
注意:
- 一旦执行游标的execute时就已对数据库进行写的动作,在未提交或未回滚时在这个会话内是可以使用查询语句查询出来的。
- 进行dml操作的数据只有commit后才算保存到数据库中,否则rollback后或者会话断开后数据就没了。
- 一个连接对象就是一个会话。
3.2.2 自动提交打开
自动提交打开后游标每次执行就自动提交了,也就是说一个execute就是一个提交。
3.3 事务的另一种操作
我们除了可以使用模块提供的方法提交和回滚事务,我们也可以直接执行sql语句执行提交和回滚,具体如下:
cursor.execute("INSERT INTO your_table (name, age) VALUES ('Alice', 25)")
cursor.execute("COMMIT") # 手动提交事务
这个操作也会提交事务,无须我们再次提交事务了。
4、数据库其他重要概念
4.1 事务级别和会话级别概念
在 Python 的数据库操作中,事务级别和会话级别是两个不同的概念,它们影响数据库的操作和行为,这个概念很重要比如oracle临时表就分这两个级别。
- 事务级别:指的是一组操作(如
INSERT
、UPDATE
、DELETE
)的整体性,这些操作要么全成功,要么全失败(通过COMMIT
或ROLLBACK
控制)。 - 会话级别:指的是当前连接数据库的一个持续会话,通常从数据库连接 (
connect()
) 开始,到关闭连接 (close()
) 为止。
完结于2024.9.24 福州