测试基础-Python篇 基础①
变量名命名规则 - 遵循PEP8原则
-
普通变量:max_value
-
全局变量:MAX_VALUE
-
内部变量:_local_var
-
和关键字重名:class_
-
函数名:bar_function
-
类名:FooClass
-
布尔类型的变量名用 is,has 这类词语前缀
is_superuser
has_errors
allow_empty -
释义为数字的单词
port
age
radius -
以_id 为结尾的单词
user_id
port_id -
以 length/count 开头或结尾的词
length_of_username
max_length
users_count
注:不要用名词的复数形式来作为 int 类型的变量名,因为名词的负数形式更像是一个容器。建议使用 number_of_apples 或 trips_count; -
超短命名
数组索引三剑客 i、j、k
某个整数 n
某个字符串 s
某个异常 e
文件对象 fp
变量注解
在Python3.5之后,可以使用类型注解功能来注明变量类型,在变量后添加类型,并用冒号隔开;
def repeat_message(message: str, count: int) -> str:
return message * count
算术运算符
-
// 取整除
-
% 取余
-
** 幂
不同类型变量之间的计算
-
数字型变量之间可以直接计算;
-
如果变量是 bool 型,在计算时,true 对应的是1,false 对应的是0;
-
字符串变量之间使用 + 拼接字符串。
获取输入的信息-input
-
字符串变量 = input("提示信息")
input 输入的数据类型都是字符串类型
格式化输出
-
%s --字符串
-
%d --有符号十进制整数,%06d 表示输出的整数显示位数,不足的地方使用 0 补全
-
%f --浮点数,%.2f 表示小数点后只显示两位,会四舍五入
-
%% --输出%
vb1 = 'Tom'print("hello %s" % vb1)vb2 = 5print('有符号十进制整数:%d' % vb2)print('输出显示位数的整数:%06d' % vb2)vb3 = 3.1415926print('保留两位小数:%.2f' % vb3)print('保留三位小数:%.3f' % vb3)vb4 = 80print('正确率为:%d%%' % vb4)--------------------------------------------------------------------hello Tom有符号十进制整数:5输出显示位数的整数:000005保留两位小数:3.14保留三位小数:3.142正确率为:80%
逻辑运算
-
and:
条件1 and 条件2 -
or:
条件1 or 条件2 -
not:(取反)
not 条件a = 10b = 20c = 10if c == a and c == b:print('right')else:print('error')-------------------------------error
a = 10b = 20c = 10if c == a or c == b:print('right')else:print('error')-------------------------------right
循环-while
初始条件设置 -- 通常是重复执行的 计数器
while 条件 1:
条件满足时,做的事情 1
条件满足时,做的事情 2
条件满足时,做的事情 3
……
while 条件 2:
条件满足时,做的事情 1
条件满足时,做的事情 2
条件满足时,做的事情 3
……
处理条件 2
处理条件 1
print 函数增强
在默认情况下,print 函数输出内容之后,会自动在内容末尾增加换行;
如果不希望末尾增加换行,可以在 peint 函数输出内容的后面增加,end=""
其中""中间可以指定 print 函数输出内容之后,继续希望现实的内容;
语法格式如下:
print("*",end="")
转义字符
-
\t--在控制台输出一个制表符,协助在输出文本时,垂直方向,保持对齐
-
\n--在控制台输出一个换行符
-
\r--回车
-
\--反斜杠符号
-
\'--单引号
-
\"--双引号
列表
-
列表通过索引取值,列表索引从0开始,且不能超过范围;
-
len(列表)--获取列表的长度
-
列表.count(数据)--数据在列表中出现的次数
-
列表.index(数据)--获取数据第一次出现的索引
-
del 列表 [索引]--删除指定索引的数据
-
列表.remove[数据]--删除第一个出现的指定数据
-
列表.pop--删除末尾数据
-
列表.pop(索引)--删除指定索引的数据
-
列表.insert(索引,数据)--在指定位置插入数据
-
列表.append(数据)--在末尾追加数据
-
列表.extend(列表 2)--将列表2的数据追加到列表1
-
列表.sort()--升序排序
-
列表.sort(reverse=True)--降序排序
-
列表.reverse() 反转/逆序
元祖
-
Tuple(元组)与列表类似,不同之处在于元组的 元素不能修改;
-
创建空元组:info_tuple = ()
-
元组中只包含一个元素时,需要在元素后面添加逗号:info_tuple = (50, )
-
len(元组)--获取元组的长度 n+1;
-
元组.count(数据)--数据在元组中出现的次数;
-
元组 [索引]--从元祖中取值;
-
元组.index(数据)--获取数据第一次出现的索引。
元组和列表之间的转换
-
使用 list 函数可以把元组转换成列表
list(元组) -
使用 tuple 函数可以把列表转换成元组
tuple(列表)
字典
-
Python 里的字典在底层使用了哈希表 (hash table) 数据结构
-
和列表的区别:列表 是 有序 的对象集合 字典 是 无序 的对象集合
Python3.6 之后的字典是有序的,如果解释器版本没有那么新,也可以使用 collections 模块里的 OrderedDict 方法保证字典的有序性。
OrderedDict 与新版字典在比较上面的区别:在对比两个内容相同但顺序不同的字典时,新版字典会返回 True,OrderedDict 则会返回 False。
from collections import OrderedDict
d = OrderedDict()
d['one'] = 1
d['two'] = 2
print(d)————————————————————
OrderedDict([('one', 1), ('two', 2)])
-
键必须是唯一的;
-
值可以取任何数据类型,但键只能使用字符串、数字或元组;
-
字典.keys()--所有 key 列表;
-
字典.values()--所有 value 列表;
-
字典.items()--所有(key,value)元组列表;
-
字典 [key]--可以从字典中取值,key 不存在会报错;
1.返回的数据类型类似列表,但不是真正意义的列表,没有 append() 方法;
2.但是可以用于 for 循环;
3.可以用 list() 方转换成真正的列表; -
字典.get(key)--可以从字典中取值,key 不存在不会报错;
-
del 字典 [key]--删除指定键值对,key 不存在会报错;
-
字典.pop(key)--删除指定键值对,并且返回删除键对应的值,key 不存在会报错;
-
字典.pop(key, default=msg)--删除指定键值对,并且返回删除键对应的值,key 不存在不会报错,会返回 msg;
-
字典 popitem() 方法返回并删除字典中的最后一对键和值。
-
字典.clear()--清空字典;
-
字典 [key] = value
如果 key 存在,修改数据
如果 key 不存在,新建键值对 -
字典.setdefault(key,value)
如果 key 存在,不会修改数据
如果 key 不存在,新建键值对 -
字典.update(字典2)--将字典2的数据合并到字典1,如果字典2中有和字典 1 重复的键值对,则替换字典 1 中的键值对;
-
生成字典的方法:d = dict.fromkeys(["name","age","code"],0) #0 为默认值
字符串
-
拼接多个字符串,使用 str.join 和 +=同样好用;
-
len(字符串)--获取字符串的长度;
-
字符串.count(字符串)--小字符串在大字符串中出现的次数;
-
字符串 [索引]--从字符串中取出单个字符;
-
字符串.index(字符串)--获得小字符串第一次出现的索引;
-
string.istitle() | 如果 string 是标题化的 (每个单词的首字母大写) 则返回 True;
-
string.startswith(str) | 检查字符串是否是以 str 开头,是则返回 True;
-
string.endswith(str) | 检查字符串是否是以 str 结束,是则返回 True;
-
string.find(str, start=0, end=len(string)) | 检测 str 是否包含在 string 中,如果 start 和 end 指定范围,则检查是否包含在指定范围内,如果是返回开始的索引值,否则返回
-1
; -
string.index(str, start=0, end=len(string)) | 跟 find() 方法类似,不过如果 str 不在 string 会报错;
-
string.replace(old_str, new_str, num=string.count(old)) | 把 string 中的 old_str 替换成 new_str,如果 num 指定,则替换不超过 num 次;
-
string.capitalize() | 把字符串的第一个字符大写;
-
string.title() | 把字符串的每个单词首字母大写;
-
string.lower() | 转换 string 中所有大写字符为小写;
-
string.upper() | 转换 string 中的小写字母为大写;
-
string.swapcase() | 翻转 string 中的大小写;
字符串 - 切片
-
切片方法适用于字符串、列表、元组;
-
字符串 [开始索引:结束索引:步长];
-
切片:正反向索引(正:从 0 开始,反:从-1 开始)
-
切片索引:[startENDstep]
-
start:开始截取的位置,包含在截取内容内
-
end:结束截取的位置,结束截取的位置并不包含
-
step:截取的步长,默认值为 1
-
step:为正,表示从左到右进行截取,start 必须在 end 之前(从左开始算前,下标必须从左到右)
-
step:为负,表示从右到左进行截取,start 必须在 end 之前(从右开始算前,下标必须从右到左)
-
s = "hello,world"
print(s[:]) # 取全部
print(s[1:]) # 从第 2 位取到最后
print(s[:-1]) # 从开始取到倒数第二位
print(s[::2]) # 步长为 2
print(s[::-1]) # 反序
指定的区间属于左闭右开型 [开始索引, 结束索引) => 开始索引 >= 范围 < 结束索引
从 起始位开始,到 结束位的前一位 结束(不包含结束位本身)
从头开始,开始索引 数字可以省略,冒号不能省略
到末尾结束,结束索引 数字可以省略,冒号不能省略
步长默认为 1,如果连续切片,数字和冒号都可以省略
索引的顺序和倒序:
在 Python 中不仅支持 顺序索引,同时还支持 倒序索引
所谓倒序索引就是 从右向左 计算索引
最右边的索引值是 -1,依次递减
字符串格式化
-
优先使用 f-string 方式
# 将username靠右对齐,左侧补空格一共到20位username = 'Lili'print(f'{username:>20}')-------------------------------------
Lili
-
对参数复用时可以使用 str.format 方式
username = 'Lily'sore = 10print('{0}:{0}的成绩是{1}'.format(username, sore))
集合
-
集合是一个无序的可变容器类型,他最大的特点就是成员不能重复
-
要初始化一个空集合只能调用 set() 方法,因为{}表示的是一个空字典,而不是一个空集合
-
集合也有自己的推导式-nums = {n for n in range(10) if n % 2 == 0}
-
集合是可变类型,可以通过.add() 追加元素
-
可以使用 update 方法可以将一个可迭代元素更新到集合中
s1 = set([1,2,3])s2 = set([2,3,4])s1.update(s2)s1.update('hello')print(s1)------------------------{1, 2, 3, 4, 'o', 'h', 'e', 'l'}
-
使用.remove() 可以删除集合中的元素,但元素不存在会报错-KeyError:
-
使用.discard() 可以删除集合中的元素,元素不存在也不会报错
-
集合的元素不可以修改,只能先删再加
-
我们可以使用 in 判断某个元素是否在某个集合中,不能在集合中取值,只能使用 for 循环遍历集合中的元素
-
集合只能存放可哈希对象
s1 = set([1,2,3])s1.add([1])-----------------------TypeError: unhashable type: 'list'
集合的运算
集合支持集合运算,比如交集、并集、差集。所有的操作都可以用两种方式:方法和运算符;
-
交集
fruits_1 = {'apple','orange','pineapple'}fruits_2 = {'tomato','orange','grapes','mango'}print(fruits_1 & fruits_2)-----------------------------{'orange'}
-
并集
fruits_1 = {'apple','orange','pineapple'}fruits_2 = {'tomato','orange','grapes','mango'}print(fruits_1 | fruits_2)----------------------------------------------------------{'tomato', 'pineapple', 'orange', 'apple', 'grapes', 'mango'}
-
差集(前有后没有)
fruits_1 = {'apple','orange','pineapple'}fruits_2 = {'tomato','orange','grapes','mango'}print(fruits_1 - fruits_2)------------------------------------{'apple', 'pineapple'}
-
symmetric_difference:返回两个集合中不重复的元素
fruits_1 = {'apple','orange','pineapple'}fruits_2 = {'tomato','orange','grapes','mango'}print(fruits_1.symmetric_difference(fruits_2)){'apple', 'mango', 'tomato', 'pineapple', 'grapes'}
-
issubset:用于判断集合的所有元素是否都包含在指定集合中
fruits_1 = {'apple','orange','pineapple'}fruits_2 = {'apple','orange','pineapple','water'}print(fruits_1.issubset(fruits_2))print(fruits_2.issubset(fruits_1))--------------------------------------------------TrueFalse
Python 循环结构
-
什么时候用 for:当循环次数是一定的,或者是循环对象是一定的,比如说在一个固定的字符串或列表中进行循环,那么最好使用 for
-
什么时候用 while:当循环次数不是一定的,只是满足某个条件时才进行循环,那么最好使用 while
-
没有 do…while…循环
-
循环里面加 else:当循环执行完毕时,else 才会执行;如果循环在中间退出,则 else 不会运行
-
break&continue:不管是 break 还是 continue 都只作用于当前循环
匿名函数-lambda
-
lambda 关键字能够帮我们创建小型的匿名函数:
-
lambda x:express
-
lambda 返回的是该匿名函数的指针
-
func = lambda x,y:x*y
print(func(2,3))
类
类定义
-
类名首字母大写,多个字母连接一起
-
默认继承 object 类,若继承其他类则在类名后加(继承父类)
类方法
-
实例方法
1、只能通过对象 (实例) 调用的方法
2、实例方法在定义时总是以 self 作为第一个参数
3、实例方法在调用时不需要传入 self,这个实例本身会自动传到方法中作为 self -
初始化方法 (init())
1.不需要显式调用,在初始化对象时会有 python 自动调用
2.初始化方法一般只在定义对象属性的时候才会定义 -
类方法
1、可以直接通过类名调用的方法,也可以通过实例调用
2、类方法必须通过@classmethod装饰器进行装饰
3、所有的类方法第一个参数必须是 cls
4、类方法不能访问实例属性,只能访问类属性 -
属性方法
使用场景:属性方法对应的属性的值无法直接确定,要通过一系列的操作才能得到这个值,而且用户不关心这个操作过程,只想得到这个值。
定义:当成属性使用的方法,调用属性方法时不需要加 () -
静态方法
1、通过@staticmethod装饰器来进行装饰的方法
2、静态方法既不能访问实例属性,也不能访问类属性
3、可以通过类名直接调用,也可以通过对象调用
类的三大特征
-
封装
暴露接口,隐藏细节 -
继承
1.子类通过继承直接获得父类的全部属性和方法,实现代码复用
2.初始化的几种情况:
2.1 当子类中没有定义init() 方法,则初始化子类时将默认使用父类的初始化方法,并传入对应的参数
2.2 当子类定义了自己的初始化方法,但没有调用父类的初始化方法,则父类中的相关属性不会被初始化
2.3 若在子类中重新定义了 init 方法,若仍要继承父类的属性,则需要显示调用父类的 init 方法:super().init() -
多态
类的反射
-
反射原理
通过字符串的形式在运行时动态修改程序的变量、方法及属性,所有的修改都在内存中进行,所以他并不会实际修改代码,主要目的就是提高代码在运行时的灵活性; -
反射相关的方法
hasattr 输入一个字符串,判断对象有没有这个方法或属性;
getattr 获取对象属性值或方法的引用,如果是方法,则返回方法的引用,如果是属性,则返回属性的值,如果该方法或属性不存在,则抛出异常;
setattr 动态添加一个方法或属性;
delattr 动态删除一个方法或属性。
异常处理
Python 异常处理依赖的关键字:
try
except
else
finally
-
try
try 块里面放置所有可能引起异常的代码,一个异常处理块里面只能有一个 try; -
except
放置要处理的异常类型和相应语句块,用于表明该 except 要处理的异常类型;
一个异常处理块里面可以跟 1 到 n 个 except 块;
每个 except 块后面可以跟 1 到 n 个异常类型,也可以不跟任何异常类型; -
else
如果 try 块里面的语句没有引起异常,则会运行 else 里面的语句; -
finally
主要用于回收再 try 块里面打开的物理资源,异常处理机制会保证 finally 块一定会被执行; -
异常处理语法结构
1.只有 try 是必须的
2.如果没有 try,就不能有 except 和 finally
3.except 块和 finally 块都是可选的,但 except 和 finally 必须出现其中之一,也可以同时出现
4.可以有多个 except 块,但捕获父类异常的 except 块要写在捕获子类异常的 except 块的后面
5.多个 except 块必须位于 try 块之后,finally 块必须位于所有块的最后
IO 读写 - 文本文件
-
open (path,mode)
默认是 r:只读模式,文件必须事先存在,不主动生成文件,从文件开头开始读;
r+:读写模式,文件也必须事先存在,不主动生成文件,从文件开头开始读或写;
w:只写模式,如果用 w 模式打开,一律会清空之前文件的所有内容,如果文件不存在,则自动创建文件,从头开始写;
w+:读写模式,也会清空之前文件的所有内容,如果文件不存在,则自动创建文件,从头开始写;
a:追加模式,只写,不会清空以前文件的内容,主动生成文件,从文件尾开始写入;
a+:追加模式,读和写,不会清空以前文件的内容,主动生成文件,从文件尾开始写入或读取;
二进制读写,一般用于图片或音视频:rb+,wb+,ab+; -
查看和设置文件指针位置:
with open('user.txt', 'a+') as f: # 将文件指针重置至开始位置(这样就不会导致f.readlines()读不到数据了) f.seek(0) # 返回文件指针位置 print(f.tell())
-
with 是 python 中的上下文管理器,它会自动帮你管理文件的句柄
with open(r'D:\testlog.txt') as f:for line in f.readlines(): print(line,end='')
-
文件与文件夹
windows 文件路径用反斜线,Linux 文件路径用正斜线,要想将程序在不同系统上运行,则可用 os.path.join() 方法;
myFiles = ['accounts.txt','details.csv','invite.docx']
for filename in myFiles:
print(os.path.join('c:\\User\\asweigart',filename))-----------------------------------------------------------------------------------------c:\User\asweigart\accounts.txtc:\User\asweigart\details.csvc:\User\asweigart\invite.docx
其余相关知识点附张图吧:
多线程和多进程编程
-
概念
程序:指的是一段静态的代码指令;
进程:正在执行的程序,将静态的执行代码运行起来,进程内拥有该程序执行所需的全部资源;
线程:是指正在执行程序的最小单元。一个进程中至少必须有一个线程(主线程),在程序中线程是独立的可运行的流;
多线程:在单个程序中同时运行多个不同的线程,完成不同的工作; -
进程特征
独立性:进程是系统中独立存在的实体,拥有独立的资源空间;
动态性:进程拥有自己的生命周期;
并发性:多个进程可以在单个处理器上并发执行,互不影响; -
线程特征
每个线程都有自己的堆栈,自己的程序计数器,自己的局部变量,这里体现了程序的独立性;
在相同父进程下的所有线程共享进程内所有资源,可以实现线程间的消息互通;
多个线程之间也可以并发执行,互不影响; -
创建多线程-threading
1.使用 threading 模块的 Thread 类的构造器创建线程对象。在创建线程对象时使用 target 参数指定函数线程的执行体;
2.调用线程对象的 start() 方法启动线程; -
通过 join 方法去阻塞主线程
d = Demo()t1 = threading.Thread(target=d.music,args=('摇篮曲',))t2 = threading.Thread(target=d.movie, args=('灰太狼',))t1.start()t2.start()t1.join()t2.join()
-
设置守护线程
主线程结束后立即结束所有设置为守护线程的子线程; -
多线程锁
import threadingbalance = 0lock = threading.RLock()def change_it(n):lock.acquire()try: global balance balance += n balance -= nfinally: lock.release()def run_threading(n):for i in range(100000000): change_it(n)t1 = threading.Thread(target=run_threading, args=(5,))t2 = threading.Thread(target=run_threading, args=(5,))t1.start()t2.start()t1.join()t2.join()
GIL 全局解释器锁
什么是 GIL 全局解释器锁:
GIL(Global Interpreter Lock)是 Python 的一个重要特性,它是一种机制,用于保护多线程环境下共享内存数据的完整性。它锁定了整个解释器,只允许一个线程同时执行 Python 字节码,从而避免多线程下出现数据竞争问题。这意味着即使使用多核 CPU,Python 程序也不能充分利用多核优势。GIL 在性能上可能带来一定的影响,因此不适合处理需要大量的 CPU 运算的任务。
什么条件下会释放 GIL:
当前活跃线程遇到 IO 等待,比如要访问网络或建立数据库链接等情况;
活跃线程执行了 100 个字节码的程序后,GIL 也会释放该线程的锁,然后与其他线程参与竞争;
python 的多线程适合场景:
python 的多线程只适合于 IO 密集型应用,对于计算密集型的应用最好使用多进程或协程的方式解决;
可迭代对象
-
通俗说,可迭代对象就是可以放在 for 循环内进行迭代的对象
比如列表、字典、元祖、字符串; -
判断一个可迭代对象的依据是什么:
必须至少实现getitem或iter这两个方法中的其中一个
迭代器
-
任何实现了iter和next方法的对象都是迭代器(这两个方法必须同时实现);
-
其中iter会返回迭代器本身;
-
next会返回迭代器中的下一个元素,如果没有元素了将抛出 stopIteration 异常;
-
迭代器当然也可以用到 for 循环中;
-
迭代器实际上就是一种工厂模式。
迭代器和可迭代对象的区别
-
迭代器是迭代对象的一种,迭代器一定是可迭代对象,可迭代对象不一定是迭代器
-
一个合法的迭代器,必须同时实现iter和next两个魔法方法
-
可迭代对象只需要实现iter方法即可
-
判断对象 obj 可迭代的唯一方法就是调用 iter(obj),看返回结果是不是一个迭代器
-
每个迭代器的被迭代过程是一次性的,可迭代对象则不一定
生成器
-
特殊的迭代器,只需要使用 yield 关键字,那么就会立即变为一个生成器,也就是说,只要一个函数中包含了一个 yield 关键字(不管几个),那么这个函数就会自动变成一个生成器函数;
-
生成器一定是一个迭代器,但反之不一定成立;
-
特点:
生成器中每次遇到 yield 关键字之后,会返回相应的结果;
保留函数当前的运行状态,等待下一次调用,下次调用时将从上一次返回 yield 语句处开始执行后面的语句; -
生成器的 send 方法可以向函数体内去传递值;
对于 next 和 send 方法的异同:
next 和 send 都可以去调用一次生成器,从调用生成器的角度来说,他们的作用完全一样;
next 无法像生成器内部的变量赋值,但 send 可以;
next(gen) 等同于 send(None),可以互换; -
在生成器中使用 for 循环
每一次 for 循环相当于调用一次 next;
for 循环会自动帮助我们处理 stopIteration 异常。
装饰器
-
定义
装饰器本质是函数,只是它的作用是为其他函数添加特定的附加功能; -
编写装饰器的原则
装饰器一定不能修改被装饰器的函数的源码;
装饰器一定不能修改被装饰的函数的调用方式; -
实现装饰器的前置知识条件
1.函数即变量
函数和普通变量的存储原理是一样的,函数名可以像变量名那样去使用,比如可以进行赋值;
2.掌握高阶函数相关知识
符合下面任意条件之一即为高阶函数
条件一:接收函数名作为参数
条件二:返回值中包含函数名
3.掌握函数嵌套相关知识
通过 def 关键字在一个函数 A 中去定义另外一个函数 B,则函数 B 称为嵌套函数;
4.装饰器=高阶函数 + 嵌套函数 -
了解装饰器的本质优势
1.运行时校验:在执行阶段进行特定校验,当校验不通过时终止执行:
Django 框架中的用户登录态校验装饰器@login_required;
2.注入额外参数:在函数被调用时自动注入额外的调用参数:
unittest.mock 模块的装饰器@patch;
3.缓存执行结果:通过调用参数等输入信息,直接缓存函数执行结果:
functools 模块的缓存装饰器@lru_cache;
4.注册函数:将被装饰函数注册为某个外部流程的一部分:
Flask 框架的路由注册装饰器@app.route;
5.替换为复杂对象:将原函数 (方法) 替换为更复杂的对象,比如类实例或特殊的描述符对象:
静态方法的装饰器@staticmethod。
正则表达式
正则表达式匹配步骤:
1.import re
2.用 re.compile() 函数创建一个 Regex 对象(记得使用原始字符串)
3.向 Regex 对象的 search() 方法传入想查找的字符串。它返回一个 Match 对象(一般用 mo 接收)
4.调用 Match 对象的 group() 方法,返回实际匹配的字符串
demo:
import re>>> phoneNumRegex = re.compile(r'(\d\d\d)-(\d\d\d-\d\d\d\d)')>>> mo = phoneNumRegex.search('my number is 415-555-4242.')>>> mo.group(1)'415'>>> mo.group(2)'555-4242'>>> mo.group()'415-555-4242'>>> mo.group(0)'415-555-4242'
PLus:
我在学习中,发现正则表达式在任何语言中都占有很大部分的占比,但正则表达式相关的知识点又过于零碎,对于榆木脑袋的我真是学一遍忘一遍。在实际工作中,我自己真正用到正则的地方并不多,再看同事,目前就发现前端同学有可能会用到正则去做一些事情,并且用到的时候都是度娘,一是自己真记不住,二是度娘 copy 过来的多数情况是比自己写要严谨的多的。
基于此,我把正则视为投入产出比太低的事情,仅需要记住个大概印象,真到用时能分清度娘上哪个轮子能用哪个轮子用不了就可以了。
测试基础-Python篇 基础②
常用模块-math
import math
-
print(math.ceil(3.14)) # 取大于等于 x 的最小整数
-
print(math.fabs(-3)) # 取绝对值
-
print(math.floor(3.14)) # 取小于等于 x 的最大整数
-
print(math.fsum([1,2,3])) # 求和
-
print(math.pow(3,4)) #3 的 4 次方 等价于 3**4
-
print(math.sqrt(3)) # 开平方,3 的平方根
常用模块-random
import random
-
print(random.random()) # 返回 [0.0,1.0) 之间的浮点数
-
print(random.randint(10,20)) # 生成 10 到 20 之间的一个随机整数,也就是 [10,20]
-
print(random.randrange(10,20)) # 生成 10 到 20 之间的一个随机整数,也就是 [10,20)
-
print(random.uniform(10,20)) # 生成 10 到 20 之间的一个随机浮点数,也就是 [10,20]
-
print(random.choice([10,20,30])) # 随机从列表选择一个数
-
print(random.choices([10,20,30],k=2)) # 随机从列表选择 k 个数,返回列表形式,取出放回方式,意思是取出的数可以重复
-
print(random.sample(a1,3)) # 随机从列表选 k 个数,返回列表形式,取出不放回方式,意思是取出的数不会重复
-
random.shuffle(a1) # 洗牌,随机变换列表顺序
常用模块-json
-
用 loads() 函数读取 JSON
要将包含 JSON 数据的字符串转换为 Python 的值,就将它传递给 json.loads() 函数; -
用 dumps() 函数写出 JSON
将一个 python 值转换成 JSON 格式的数据字符串,就用 json.dumps() 函数;
import jsond = {'name':'Tom','age':26}j = json.dumps(d)d2 = json.loads(j)print(d)print(type(d))print()print(j)print(type(j))print()print(d2)print(type(d2))-------------------------------------{'name': 'Tom', 'age': 26}<class 'dict'>
{"name": "Tom", "age": 26}
<class 'str'>
{'name': 'Tom', 'age': 26}
<class 'dict'>
常用模块-time
-
round() 第一个参数是要处理的数字,第二个可选参数为四舍五入保留的位数,若不传如第二个参数,则默认四舍五入到最近的整数
import timen = time.time()print(n)n1 = round(n,3)print(n1)------------------------1675392067.29749661675392067.297
-
time.time() ---获取当前时间戳:1675392067.2974966
-
time.sleep(sec) ---睡眠 sec 秒
常用模块-datetime()
-
datetime 模块有自己的 datetime 数据类型:
-
datetime 的一些方法:
import datetimedt = datetime.datetime.now()print(dt)print('dt.year:' + str(dt.year) + '---type:' + str(type(dt.year)))print('dt.month:' + str(dt.month) + '---type:' + str(type(dt.month)))print('dt.day:' + str(dt.day) + '---type:' + str(type(dt.day)))print('dt.hour:' + str(dt.hour) + '---type:' + str(type(dt.hour)))print('dt.minute:' + str(dt.minute) + '---type:' + str(type(dt.minute)))print('dt.second:' + str(dt.second) + '---type:' + str(type(dt.second)))----------------------------------------------------------------------------------------------------------2023-02-03 11:00:08.205691dt.year:2023---type:<class 'int'>
dt.month:2---type:<class 'int'>
dt.day:3---type:<class 'int'>
dt.hour:11---type:<class 'int'>
dt.minute:0---type:<class 'int'>
dt.second:8---type:<class 'int'>
-
Unix 时间戳可以通过 datetime.datetime.fromtimestamp() 转换为 datetime 对象
import datetimeimport timenow = time.time()date = datetime.datetime.fromtimestamp(now)print(now)print(date)----------------------------------------------------------------------------1675393953.08603122023-02-03 11:12:33.086031
-
datetime对象可以用比较操作符进行比较,后面的datetime对象时"更大"的值
import datetimeimport timed1 = datetime.datetime.now()time.sleep(1)d2 = datetime.datetime.now()print(d2>d1)-----------------------------------------------True
-
datetime 对象的差的数据类型是'timedelta'
import datetimeimport timed1 = datetime.datetime.now()time.sleep(1)d2 = datetime.datetime.now()d3 = d2 -d1print(d3)print(type(d3))------------------------------------------------0:00:01.002022<class 'datetime.timedelta'>
-
要创建 timedelta 对象,就用 datetime.timedelta() 函数
import datetimedt = datetime.timedelta(days=11,hours=10,minutes=9,seconds=8)print(dt)print(type(dt))print(dt.days)print(dt.seconds) # 天不参与计算,10*60*60 + 9*60 + 8 = 36548print(dt.total_seconds()) # 11*24*60*60 + 10*60*60 + 9*60 + 8 = 986948print(str(dt))---------------------------------------------------------------------------------------------------------11 days, 10:09:08<class 'datetime.timedelta'>
11
36548
986948.0
11 days, 10:09:08
-
算数运算符可以用于对 datetime 值进行日期运算,例如,要计算今天之后 1000天的日期;
import datetimedt = datetime.datetime.now()print(dt)thounsandDays = datetime.timedelta(days=1000)print(thounsandDays)print(dt + thounsandDays)------------------------------------------------------------------------------2023-02-06 14:20:46.2650841000 days, 0:00:002025-11-02 14:20:46.265084
常用模块-logging
Logging 库是非常常用的记录日志库,通过 logging 模块存储各种格式的日志,主要用于输出运行日志,可以设置输出日志的等级、日志保存路径、日志文件回滚等;
-
日志级别
DEBUG --logging.debug()
INFO --logging.info()
WARNING --logging.warning()
ERROR --logging.error()
CRITICAL --logging.critical() -
使用日志模块
import logginglogging.basicConfig(format='%(levelname)s %(asctime)s: %(message)s: %(module)s',level=logging.DEBUG)logging.debug('This message should appear on the console')logging.info('So should this')logging.warning('And this, too')-----------------------------------------------------------------------------------------------------DEBUG 2023-02-07 17:04:37,473: This message should appear on the console: logging_practiceINFO 2023-02-07 17:04:37,473: So should this: logging_practiceWARNING 2023-02-07 17:04:37,473: And this, too: logging_practice
-
将日志记录到文件-logging.basicConfig() 函数接受 filename 关键字。
import logginglogging.basicConfig(filename='myProgramLog',format='%(levelname)s %(asctime)s: %(message)s: %(module)s',level=logging.DEBUG)logging.debug('This message should appear on the console')logging.info('So should this')logging.warning('And this, too')
-
禁用日志 只要向 logging.disable() 传入一个日志级别,他就会禁止该级别和更低级别的所有日志消息; logging.disable(logging.CRITICAL) 加在代码前面即可隐藏日志信息(接近 import.logging 的位置更容易找到和管理);
import logginglogging.disable(logging.CRITICAL)logging.basicConfig(filename='myProgramLog',format='%(levelname)s %(asctime)s: %(message)s: %(module)s',level=logging.DEBUG)logging.debug('This message should appear on the console')logging.info('So should this')logging.warning('And this, too')
常用模块-threading
-
要得到单独的线程,首先要调用 threading.Thread() 函数,生成一个 Thread 对象。
import timeimport threadingprint('Start of program.')def takeANap():
time.sleep(5)
print('Wake up!')threadObj = threading.Thread(target=takeANap)threadObj.start()print('End of program')----------------------------------------------------------------------------Start of program.End of programWake up!
注意:target 参数名后传的是方法名,不加 (),因为此处并不是调用。
-
向线程的目标函数传递参数 常规参数可以作为一个列表,传递给 threading.Thread() 中的 args 关键字参数。关键字参数可以作为一个字典,传递给 threading.Thread() 中的 kwargs 关键字参数:
import threadingl = [1,2,3,4]def a(*args):
for _ in args:
print(_)thread_a = threading.Thread(target=a,args=l)thread_a.start()------------------------------------------------------------------------1234
import threadingd = {'name': 'Tom', 'age': 18}def b(**kwargs):
for k, v in kwargs.items():
print(str(k) + ':' + str(v))thread_b = threading.Thread(target=b, kwargs=d)thread_b.start()----------------------------------------------------------------------------name:Tomage:18
-
并发问题需要注意的是:为了避免并发问题,绝不让多个线程读取或写入相同变量。当创建一个新的 Thread 对象时,要确保其目标函数只使用该函数中的局部变量。
-
线程阻塞-thread.join()
thread.join() 方法的作用是阻塞当前线程,直到调用 join() 方法的线程结束。也就是说,如果你有多个线程并希望在其中一个线程结束之后再继续执行,则可以使用 join() 方法。
# 不使用join,两个线程并行运行import timeimport threadingdef a():
time.sleep(1)
print('我是a:1/3')
time.sleep(1)
print('我是a:2/3')
time.sleep(1)
print('我是a:3/3')def b():
print('我是b:1/2')
print('我是b:2/2')thread_a = threading.Thread(target=a)thread_b = threading.Thread(target=b)thread_a.start()thread_b.start()-------------------------------------------------------------我是b:1/2我是b:2/2我是a:1/3我是a:2/3我是a:3/3
# 使用join,线程b需要等线程a运行完后再运行import timeimport threadingdef a():
time.sleep(1)
print('我是a:1/3')
time.sleep(1)
print('我是a:2/3')
time.sleep(1)
print('我是a:3/3')def b():
print('我是b:1/2')
print('我是b:2/2')thread_a = threading.Thread(target=a)thread_b = threading.Thread(target=b)thread_a.start()thread_a.join()thread_b.start()--------------------------------------------------------------我是a:1/3我是a:2/3我是a:3/3我是b:1/2我是b:2/2
# join()方法可以自定义timeout参数,意为最长暂用CPU时间,如果不设置的话就永远等待;import timeimport threadingdef a():
time.sleep(1)
print('我是a:1/3')
time.sleep(1)
print('我是a:2/3')
time.sleep(1)
print('我是a:3/3')def b():
print('我是b:1/2')
print('我是b:2/2')thread_a = threading.Thread(target=a)thread_b = threading.Thread(target=b)thread_a.start()thread_a.join(timeout=2)thread_b.start()-------------------------------------------------------------我是a:1/3我是a:2/3我是b:1/2我是b:2/2我是a:3/3
深拷贝浅拷贝
不可变数据类型(如整型,字符串等)在 Python 中只是拷贝了值,因此在执行浅拷贝时实际上是创建了一个新的副本,而不是拷贝引用。因此,对原数据的更改不会影响到拷贝后的数据。
import copya = 1000b = ac = copy.copy(a)d = copy.deepcopy(a)print(a, b, c, d)print(id(a), id(b), id(c), id(d))a += 1print(a, b, c, d)print(id(a), id(b), id(c), id(d))----------------------------------------------1000 1000 1000 10002518799374640 2518799374640 2518799374640 25187993746401001 1000 1000 10002518805613936 2518799374640 2518799374640 2518799374640
对于可变数据类型,浅拷贝只拷贝第一层中的引用,深拷贝在拷贝时,会逐层进行拷贝,直到所有的引用都是不可变对象为止。
import copya = [1, 2, [3, 4]]b = ac = copy.copy(a)d = copy.deepcopy(a)e = a.copy()f = a[:]print(a, b, c, d, e,f)print(id(a), id(b), id(c), id(d), id(e),id(f))print()a.append(5)print(a, b, c, d, e,f)print(id(a), id(b), id(c), id(d), id(e),id(f))print()a[2].append(6)print(a, b, c, d, e,f)print(id(a), id(b), id(c), id(d), id(e),id(f))----------------------------------------------------[1, 2, [3, 4]] [1, 2, [3, 4]] [1, 2, [3, 4]] [1, 2, [3, 4]] [1, 2, [3, 4]] [1, 2, [3, 4]]2213595331584 2213595331584 2213595362304 2213595356992 2213595443008 2213595442368[1, 2, [3, 4], 5] [1, 2, [3, 4], 5] [1, 2, [3, 4]] [1, 2, [3, 4]] [1, 2, [3, 4]] [1, 2, [3, 4]]2213595331584 2213595331584 2213595362304 2213595356992 2213595443008 2213595442368[1, 2, [3, 4, 6], 5] [1, 2, [3, 4, 6], 5] [1, 2, [3, 4, 6]] [1, 2, [3, 4]] [1, 2, [3, 4, 6]] [1, 2, [3, 4, 6]]2213595331584 2213595331584 2213595362304 2213595356992 2213595443008 2213595442368
-
Python 有多种方式实现浅拷贝,copy 模块的 copy 函数 ,对象的 copy 函数 ,工厂方法,切片等。
-
赋值符号"=":如果是可变类型,就是引用传递;如果是不可变类型,就是值传递。
-
浅拷贝的优点:拷贝速度快,占用空间少,拷贝效率高。
-
因为浅拷贝不能解决嵌套问题,所以引出了深拷贝,深拷贝会遍历并拷贝 items 里所有的内容 - 包括他嵌套的子列表;
对象的可哈希性
-
不可变的内置类型都是可哈希的,比如 str、int、float、frozenset
-
可变的内置类型都是不可以哈希的,比如 list、dict
-
对于不可变容器类型(tuple、frozenset),仅当他的所有成员都不可变时,他自身才是可哈希的
-
用户定义的类型默认都是可哈希的
注意:只有可哈希对象才能被放进集合或者作为字典的键
sorted() 函数
sorted 函数是 Python 内置函数,用于对可迭代对象进行排序,并返回一个新的列表。
注意:sorted 函数不会改变原来的可迭代对象,而是返回一个新的列表。如果需要改变原来的可迭代对象,可以使用 sort 方法,但它只能用于列表。
参数:
-
iterable:可以是列表、元组、字典等任意可迭代对象。
-
key:一个函数,用于提取每个元素的排序关键字。默认为 None,表示按元素本身的顺序进行排序。
-
reverse:是否按降序排列,默认为 False,表示按升序排列。
enumerate() 函数
enumerate() 适用于任何"可迭代对象",可以用于列表、元祖、字典、字符串等。
def enumerate_func():
names = ['lily','wenwen','tom']
for index, s in enumerate(names,start=1):
print(index,s)---------------------------------------------1 lily2 wenwen3 tom
如果不指定 start 参数,则 index 从 0 开始
测试基础-Python篇 基础③
浮点数精度问题
print(0.1+0.2)----------------------0.30000000000000004
可以使用 decimal 模块解决浮点数精度问题:
from decimal import Decimalprint(Decimal('0.1') + Decimal('0.2'))print(type(Decimal('0.1') + Decimal('0.2')))print(type(Decimal('0.1')))---------------------------------------------------------0.3<class 'decimal.Decimal'>
<class 'decimal.Decimal'>
注意:在使用 Decimal 时要用字符串来表示数字
布尔值其实也是数字
-
布尔类型其实是整型的子类型
-
True 和 False 这两个布尔值可以直接当做 1 和 0 来使用
-
通过这个特点,最常用于统计总数
def sum_even(numbers: list[int]):
"""
返回numbers中偶数的个数
:param numbers: 整数列表
"""
return sum(i % 2 == 0 for i in numbers)
不常用但特别好用的字符串方法
-
str.partition(sep) 按照切分符 sep 切分字符串,返回一个包含三个成员的元祖 (part_before, sep, part_after);若 s 不包括分隔符,则最后一个成员默认是空字符串'';
s = 'TomAndMarry's2 = s.partition('And')print(s2)--------------------------------('Tom ', 'And', ' Marry')
-
2.str.translate(table) 他可以按规则一次性替换多个字符,使用它比调用多次 replace 方法更快也更简单;
s = '明明是中文,却使用了英文标点.'table = s.maketrans(',.', ',。')s2 = s.translate(table)print(s2)-----------------------------------------------明明是中文,却使用了英文标点。
使用枚举类型来替代字面量
-
更易读:所有人都不需要记忆某个数字代表什么;
-
更健壮:降低输错数字或字母产生 bug 的可能性;
# 用户每日奖励积分数量DAILY_POINTS_REWARDS = 100# VIP用户额外奖励20VIP_EXTRA_POINTS = 20from enum import Enumclass UserType(int, Enum):
# VIP用户 VIP = 3
# 小黑屋用户 BANNED = 13def add_daily_points(user):
"""用户每天第一次登录增加积分"""
if user.type == UserType.BANNED:
return
if user.type == UserType.VIP:
user.points += DAILY_POINTS_REWARDS + VIP_EXTRA_POINTS
return
user.points += DAILY_POINTS_REWARDS
return
生成器
-
定义一个生成器需要生成器函数和 yield 关键字
-
yield 和 return 最大的区别在于,return 会一次性返回结果,使用它会直接中断函数执行,而 yield 可以逐步给调用方生成结果
-
使用生成器的优点是它们占用的内存比列表要少,因为它们只生成一个元素并在需要时生成下一个元素。这使得生成器特别适合于处理大量数据
-
每次调用生成器函数都会生成一个新的生成器对象
fruits = {'apple','orange','pineapple'}def batch(item):
for _ in item:
yield _print(next(batch(fruits)))print(next(batch(fruits)))print(next(batch(fruits)))---------------------------------------appleappleapple
因为每次调用生成器函数都会生成一个新的生成器对象,所以以上程序运行结果会输出三个 apple
fruits = {'apple','orange','pineapple'}def batch(item):
for _ in item:
yield _g = batch(fruits)print(next(g))print(next(g))print(next(g))-------------------------------------------------pineappleappleorange
以上代码再次验证了'每次调用生成器函数都会生成一个新的生成器对象'结论
-
使用生成器的 next() 函数可以在需要时单独生成生成器中的元素,而不是一次性生成整个列表。如果生成器中没有元素,则会引发 StopIteration 异常
-
用生成器替代列表
def batch_process(item):
result = []
for i in item:
# process_item = ..处理item result.append(process_item)
return result# 以上方法会有一个问题,当item过大时,会导致函数执行很耗时,并且若调用方想在某个process_item达到条件时中断,以上方法也是做不到的。所以可以使用生成器函数替代。def batch_process_2(item):
for i in item:
# process_item = ..处理item yield process_item# 调用方for processed_item in batch_process_2(items):
# 如果某个处理对象过期了,就中断当前的所有处理 if processed_item.has_expired():
break
面向对象编程
内置类方法装饰器
-
类方法
1.用 def 在类里定义一个函数时,这个函数通常被称作方法。调用这个方法需要先创建一个类实例;
2.可以使用@classmethod装饰器定义类方法,它属于类,无需实例化也能调用;
3.作为一种特殊方法,类方法最常见的使用场景,就是定义工厂方法来生成新实例,类方法的主角是类型本身,当发现某个行为不属于实例,而是属于整个类型是,可以考虑使用类方法; -
静态方法
1.如果发现某个方法不需要使用当前实例里的任何内容,那可以使用@staticmethod来定义一个静态方法;
2.和普通方法相比,静态方法不需要访问实例的任何状态,是一种与状态无关的方法,因此静态方法其实可以写成脱离于类的外部普通函数;
2.1.如果静态方法特别通用,与类关系不大,那么把他改成普通函数会更好;
2.2.如果静态方法与类关系密切,那么用静态方法更好;
2.3.相比函数,静态方法有一些先天优势,比如能被子类继承和重写; -
属性装饰器
1.@property 装饰器模糊了属性和方法间的界限,使用它,可以把方法通过属性的方式暴露出来;
2.@property 除了可以定义属性的读取逻辑外,还支持自定义写入和删除逻辑;
class FilePath:
@property
def basename(self):
....
@property.setter
def basename(self, name):
....
@property.deleter
def basename(self):
....
经过@property的装饰以后,basename 已经从一个普通的方法变成了 property 对象,所以可以使用 basename.setter 和 basename.deleter 方法;
定义 setter 方法,该方法会在对属性赋值是被调用;
定义 deleter 方法,该方法会在删除属性时被调用;
鸭子类型及其局限性
-
在鸭子类型编程风格下,如果想操作某个对象,你不会去判断他是否属于某种类型,而会直接判断他是不是有你需要的方法 (或属性)。或者更激进一些。你甚至会直接尝试调用需要的方法,假如失败了,那就让她报错好了;
-
鸭子类型的优点就是编写简单,使用灵活;
-
鸭子类型的缺点就是缺乏标准、过于隐式;
-
可以使用类型注解、静态检查 (mypy)、抽象类来补充鸭子类型;
抽象类
-
isinstance() 函数
利用 isinstance() 函数,我们可以判断对象是否属于特定类型;
isinstance() 函数能理解类之间的继承关系,因此子类的实例同样可以通过基类的校验; -
校验对象是否是 Iterable 类型
在 collections.abc 模块中,有许多和容器相关的抽象类,比如代表集合的 Set、代表序列的 Sequence 等,其中有一个最简单的抽象类:Iterable,他表示的是可迭代类型; -
鸭子类型和抽象类总结
鸭子类型是一种编码风格,在这种风格下,代码只关心对象的行为,不关心对象的类型;
鸭子类型降低了类型校验的成本,让代码变得更灵活;
传统的鸭子类型里,各种对象接口和协议都是隐式的,没有统一的显示标准;
传统的 isinstance() 类型检查和鸭子类型的理念是相违背的;
抽象类是一种特殊的类,他可以通过钩子方法来定制动态的子类检查行为;
因为抽象类的定制子类化特征,isinstance() 也变得更灵活、更契合鸭子类型了;
使用@abstractmethod装饰器,抽象类可以强制要求子类在继承时重写特定方法;
除了抽象方法外,抽象类也可以实现普通的基础方法,供子类继承使用;
在 collections.abc 模块中,有许多与容器相关的抽象类;
多重继承于MRO
-
Python 里的一个类可以同时继承多个父类;
-
调用类的 mro() 方法,可以看到按 MRO 算法排好序的基类列表;
-
在许多人印象中。super() 是一个用来调用父类方法的工具函数。但这么说并不准确,super() 使用的其实不是当前类的父类,而是当前类在 MRO 链条上的上一个类;
-
应该避免多重继承;
学习建议
对于Python入门及进阶,我推荐两本我认为值得多次阅读的书籍:
-
《Python编程从入门到实践(第二版)》- 第一部分为基础语法部分,建议刚接触 Python 的同学多次阅读并实践,夯实基础利器!
-
《Python工匠》- 整本无尿点,强烈建议多次阅读并实践,是Python进阶的不二之选!
其他琐碎但十分实用的知识点
-
for......else......的执行顺序:
当迭代对象完成所有迭代后且此时的迭代对象为空时,如果存在 else 子句则执行 else 子句,没有则继续执行后续代码;如果迭代对象因为某种原因(如带有 break 关键字)提前退出迭代,则 else 子句不会被执行,程序将会直接跳过 else 子句继续执行后续代码; -
数值型比较不能使用 not in,因为 not in 本质是循环遍历右侧数据,用左侧数据进行比对,len() in range(1,5) 可以使用,因为 range 是一个集合,可以遍历, "li" in "lili"不建议使用,因为返回结果为 True(包含),达不到预期结果,所以准确比较建议使用'==';
-
字典取值:有两种方式,a={'name':'lili'}
第一种:a['name']
第二种:a.get('name')
区别:a['wenwen'] 报错-KeyError a.get('wenwen') 不报错-None -
如果在方法中给全局变量赋值,则要在方法中提前声明全局变量-global;
-
列表转成字符串''.join(list)
用''中的字符串作为连接,拼接 list 列表中的每个元素 --- 只能用在每个元素都是字符串的时候可以用,要不然就会报错; -
集合数据类型 set 取值
因为集合(set)是一种无序的不重复的元素的集合。因为集合中的元素没有特定的顺序,所以无法通过下标索引来访问。
可以使用循环遍历取值,也可使用 in 判断目标元素是否在集合中。 -
当遇到复杂计算的数字字面量时,保留整个数学公式,提示可读性也不会降低性能;
-
要判断某个容器是否包含特定成员,用集合比用列表更合适,因为集合底层使用了哈希表数据结构
# 列表数据越多效果越明显VALID_NAMES = ['pip', 'lili', 'name']VALID_NAMES_SET = set(VALID_NAMES)def validate_name(name):
if name not in VALID_NAMES_SET:
raise ValueError(f'{name} is not a valid name')
-
使用集合去重会导致去重后的集合丢失原有的成员顺序,若要去重,又要保持原有的成员顺序可使用有序字典 OrderedDict
nums = [10, 2, 3, 21, 10, 3]def ordered_dict(member: list[int]):
from collections import OrderedDict
result = list(OrderedDict.fromkeys(member).keys())
return resultprint(ordered_dict(nums))----------------------------------------------------[10, 2, 3, 21]
-
在遍历时不要直接修改原列表,可以启用一个新列表来保存修改后的成员;
-
对于未来可能会变动的多返回值函数来说,可以使用 NamedTuple 类型对返回结果进行建模,可以减少后期代码变动对已有程序的影响
from typing import NamedTupleclass Address(NamedTuple):
"""地址信息结果"""
country: str
province: str
city: strdef latlon_to_address(lat, lon):
return Address(
country = country,
province= province,
city = city,
)addr = latlon_to_address(lat, lon)# 通过属性名来使用addr
# addr.city / addr.country / addr.province
-
使用 product() 扁平化多层嵌套循环 product() 接收多个可迭代对象作为参数,然后根据他们的笛卡尔积不断生成结果;
from itertools import productprint(list(product([1,2],[3,4])))----------------------------------------[(1, 3), (1, 4), (2, 3), (2, 4)]
# 常用多层嵌套循环def find_twelve(nem_list1, num_list2, num_list3):
"从这三个数字列表中,寻找是否存在和为12的3个数"
for num1 in num_list1:
for num2 in num_list2:
for num3 in num_list3:
if num1 + num2 + num3 == 12:
return num1, num2, num3# 使用product()扁平化多层嵌套循环from itertools import productdef find_tewlve_v2(num_list1, num_list2, num_list3):
for num1, num2, num3 in product(num_list1, num_list2, num_list3):
if num1 + num2 + num3 == 12:
return num1, num2, num3
-
使用 islice 实现循环内隔行处理 需求:有一个文件,隔行读取文件中的内容。
# 方法1:使用enumeratedef parse_titles(filename):
"""从各行数据文件中取数据"""
with open(filename, 'r') as fp:
for i, line in enumerate(fp):
if i % 2 == 0:
yield lineif __name__ == '__main__':
message_generator = parse_titles('logs.txt')
while True:
try:
print(next(message_generator))
except StopIteration as e:
break
但如果需求变更为每隔3行读取或者每隔4行读取,那我们按照以上的写法应该怎么筛选呢?
# 方法2:使用islicefrom itertools import islicedef parse_titles_v2(filename):
"""使用slice实现隔行取值"""
with open(filename, 'r') as fp:
for line in islice(fp, 0, None, 2): # islice(seq, start, end, step) yield lineif __name__ == '__main__':
message_generator = parse_titles_v2('logs.txt')
while True:
try:
print(next(message_generator))
except StopIteration as e:
break
如果需求变更为每隔3行读取或者每隔4行读取,我们只需要将 islice(seq, start, end, step) 中的 step 改成3或者4就行了
Python基础四十问
面对洋洋洒洒的知识点,我往往 “一看就会,一写就废”,为了更有针对性的加深知识点的印象,接下来,我将以做题的形式继续总结Python系列,如果有不正确的地方,希望各位佬儿哥指正纠偏:
1.列表的增删查改?
-
增
l.insert(idx, value) ---指定索引位置增加
l.append(value) ---在末尾追加
l.extend(iterable) ---取出可迭代对象的值,追加到列表末尾l1 = [1,2,3,3,4,5]l1.extend([1,[2]])print(l1)--------------------------[1, 2, 3, 3, 4, 5, 1, [2]]
-
删
del l[idx] ---删除索引处元素,索引超出会报错:IndexError: list assignment index out of range
del l ---彻底删除列表
l.pop(idx) ---删除索引处元素,索引超出会报错:IndexError: pop index out of range
l.pop() ---删除列表最后一个元素,列表为空会报错:IndexError: pop from empty list
l.remove(value) ---删除列表中第一个出现 value 的元素,值不在会报错:ValueError: list.remove(x): x not in list
l.clear() --清除列表中的所有元素 -
查
l[idx]
l.count(value) ---统计列表中指定元素出现次数,指定元素不在列表中会返回:0
l.index(value) ---返回列表中第一个指定元素的索引,指定元素不在列表中会报错:ValueError: 11 is not in list -
改
l[idx] = value
2.字典的增删查改?
-
增
d[new_key] = value ---如果 key 已存在则是更新操作
d.update({key: value}) ---如果 key 存在则是更新操作
d = dict.fromkeys(keys, value) ---dict.fromkeys(keys, value) 方法用于创建一个新字典,其中包含指定的键,并将所有键的值设置为相同的值
d.setdefault(key, default_value) ---如果 key 已存在,则返回对应的 value,若 key 不存在,则返回 default_value,并在原字典中新增键值对 key: default_value# 可用setdefault()统计每个单词的出现次数text = "Hello world! Hello python! Hello chatbot!"word_list = text.split()word_count = {}for word in word_list:word_count[word] = word_count.setdefault(word, 0) + 1print(word_count)--------------------------------------------------------------------------------------------{'Hello': 3, 'world!': 1, 'python!': 1, 'chatbot!': 1}
-
删
d.pop(key) ---key 不存在会报错:KeyError: 'addr'
d.popitem() ---随机删除一个键值对,因为字典无序,所以是随机删除,字典为空则报错:KeyError: 'popitem(): dictionary is empty'
del d[key] ---删除指定 key,若 key 不存在则报错:KeyError: 'addr'
del d ---彻底删除字典
d.clear ---清空字典 -
查
d[key] ---若 key 不存在则报错:KeyError: 'addr'
d.get(key) ---若 key 不存在则返回 None
d.keys() ---以列表形式返回字典的所有 key
d.values() ---以列表的形式返回字典的所有 value
d.items() --以列表的形式返回字典的所有键值对,每一组键值对是一组元祖
d1 = {'name': 'Tom', 'age': 25}print(d1.keys())print(type(d1.keys()))print()print(d1.values())print(type(d1.values()))print()print(d1.items())print(type(d1.items()))------------------------------------------------dict_keys(['name', 'age'])<class 'dict_keys'>
dict_values(['Tom', 25])
<class 'dict_values'>
dict_items([('name', 'Tom'), ('age', 25)])
<class 'dict_items'>
-
改
d[key] = value
d1.update(d2) ---将 d2 更新到 d1 中,相同的 key 则更改 d1 中的 value
3.元祖的增删查改
-
增
因为元祖是不可变数据类型,所以不能不能在原元祖新增
t = (1,2,'hello')t2 = (2,3,'world')print(id(t))t += t2print(t)print(id(t))-------------------------2049302192960(1, 2, 'hello', 2, 3, 'world')2049302145920
-
删
del t ---不能 del t[idx],否则报错:TypeError: 'tuple' object doesn't support item deletion
-
查
t[idx] ---idx 不能越界,否则报错:IndexError: tuple index out of range
可遍历元祖
for _ in t:
print(_)
t.count(value) ---返回元祖中 value 的个数,value 不存在返回 0
t.index(value) ---返回元祖中首个 value 的索引,value 不存在则报错:ValueError: tuple.index(x): x not in tuple
-
改
元组是不可变的,因此不能修改元组中的任何元素。
4.例举Python6种基本的数据类型,并标出哪些是不可变数据类型?
整形-int-不可变、浮点型-float-不可变、字符型-str-不可变、列表-list-可变、元祖-tuple-不可变、字典-dict-可变
5.请说明循环结构和异常处理中的 else 在什么情况下会执行?
在 for.else 循环结构中,当循环提全部循环完毕后,没有 break 或者 return 的情况下,会执行 else 代码段;
在异常处理中,try 下的代码段没有出现异常的情况下,会执行 else 代码
条件分支语句用 else 来表示"否则执行某件事"
6.列举python函数定义中有哪些不同的参数类型?
1.位置参数:根据参数在函数定义中的位置来确定。
2.默认参数:在函数定义中指定的参数值。
3.关键字参数:在调用函数时指定参数名称和对应的值。
4.星号参数:在函数定义中使用来捕获剩余的参数。
5.双星号参数:在函数定义中使用* 来捕获剩余的关键字参数。
7.python中==和is的区别?
==比较的是两个对象的值,is 比较的是两个对象的内存地址。
不能用 is 替代==,仅当你需要判断某个对象是否是 None、False、True 时,使用 is,其他情况下请使用==。
a = [1,2,3]b = [1,2,3]print(id(a))print(id(b))print(a == b)print(a is b)--------------------22782856545922278285654272TrueFalse
a = [1,2,3]b = aprint(id(a))print(id(b))print(a == b)print(a is b)---------------------20215765550722021576555072TrueTrue
额外:关于赋值或者复制后 id() 是否异同的知识点,结论:除了'='赋值,其余的 id() 都不一样,直接举例:
import copya = [1,2,3]b = ac = a.copy()d = copy.copy(a)e = copy.deepcopy(a)f = a[:]print(id(a))print(id(b))print(id(c))print(id(d))print(id(e))print(id(f))------------------192714345516819271434551681927140937408192714093715219271435578241927143558528
8.请描述global关键字在什么条件下必须被使用?
在函数局部要重新赋值全局变量之前,需要使用 global 关键字声明全局变量。
9.请说明if name == 'main':语句的作用?
在 Python 中,当一个模块被执行时,if name == 'main':语句块会被执行。此语句块可以用来测试模块的代码,而不会影响模块的其他功能。
如果模块被其他模块导入,则name的值为该模块的名称,而不是main。如果该模块是主程序,则name的值为main,此时该语句块会被执行。
这样的语句使得可以在模块被其他模块导入时忽略测试代码,并且只在模块被独立运行时执行测试代码。
10.面向对象编程有哪三大特性?请说明各个特性的意义。
封装:隐藏内部状态和实现细节,仅提供必要的接口给外部使用。
继承:允许创建新的对象类型,并基于现有的对象类型派生,从而继承其行为和状态。
多态:允许不同类型的对象对相同消息做出不同的响应。
11.类反射中常用的方法及含义?
常用的类反射方法有:
type(object):获取对象的类型,返回一个 type 对象。
isinstance(object, classinfo):判断对象是否是某种类型的实例,返回布尔值。
issubclass(class, classinfo):判断一个类是否是另一个类的子类,返回布尔值。
getattr(object, name[, default]):获取对象的属性或方法,如果不存在,可以返回 default 参数指定的默认值。
hasattr(object, name):判断对象是否具有某个属性或方法,返回布尔值。
setattr(object, name, value):设置对象的属性值。
delattr(object, name):删除对象的属性。
使用类反射可以动态地获取和操作类的信息,是动态语言的重要特性。
12.python中创建一个新线程有哪几种方式?
1.使用 threading 模块:通过定义一个继承自 Thread 的类并重写其 run 方法,然后通过该类的实例调用 start 方法启动线程。
2.使用 multiprocessing 模块:通过使用 Process 类创建新的进程。
3.使用协程:通过使用生成器实现协程,在协程内部通过 yield 实现非阻塞的多任务执行。
多线程编程是高并发编程的一种常见形式,可以提高程序的执行效率。
13.python中GIL全局解释器锁在什么情况下会被释放?
Python 中的 GIL (Global Interpreter Lock) 是一种机制,它限制任意时刻仅有一个线程可以运行在 Python 解释器中。GIL 在以下情况下会被释放:
解释器在执行 CPU 密集型任务时:例如运算,在这种情况下,GIL 会每隔一定时间被释放,以便其他线程有机会被调度执行。
解释器在执行 I/O 密集型任务时:例如读取文件,在这种情况下,由于 I/O 操作需要等待外部设备,所以 GIL 会在 I/O 操作期间被释放。
GIL 可能会影响多线程程序的性能,因此通常不建议在 Python 中开发 CPU 密集型的多线程程序。可以使用多进程来代替多线程以提高性能。
14.描述编写装饰器的原则是什么?
1.不改变原函数的内部逻辑
2.不改变原函数的调用方法
15.现在有一个Animal类有初始化方法定义如下:
class Animal:
def init(self, skin, legs):
self.skin = skin
self.legs = legs
如果现在想定义一个Dog类,并继承于这个Animal类,并想给这个Dog类增加一个nickname对象属性,Dog类的初始化方法应该怎么定义才能保证 Dog 类和其父类均能初始化成功?
# 两种写法,第二种以Cat举例class Animal:
def __init__(self, skin, legs):
self.skin = skin
self.legs = legsclass Dog(Animal):
def __init__(self, skin, legs, nickname):
super().__init__(skin, legs)
self.nickname = nicknameclass Cat(Animal):
def __init__(self, skin, legs, hobby):
Animal.__init__(self, skin, legs)
self.hobby = hobbya = Animal(skin='sss', legs='lll')d = Dog(skin='sss', legs='lll', nickname='nnn')c = Cat(skin='sss', legs='lll', hobby='hhh')print(a.skin)print(d.nickname)print(c.hobby)print(c.skin)----------------------------------------------------------------------sssnnnhhhsss
super 方法是 Python 中的一种特殊方法,用于引用父类。它常用于多重继承,当子类需要调用父类的方法时,就可以使用 super 方法。这个方法可以直接调用父类的方法,而无需显式命名父类名称。
16.文件zen.txt中保存着python之禅,请使用python代码统计该文件中每个单词出现的次数。
file_path = r'C:\Users\EDZ\Desktop\zen.txt'with open(file_path, 'r') as f:
text = f.read()words = text.split()word_counts = {}for word in words:
if word in word_counts:
word_counts[word] += 1
else:
word_counts[word] = 1print(word_counts)
17.编写一个装饰器,使用该装饰器能够显示任意函数运行所需时间。
def running_time(fun):
def wrapper(*args, **kwargs):
start_time = time.time()
result = fun(*args, **kwargs)
end_time = time.time()
print(f'{fun.__name__}运行花费了:{end_time-start_time:.2f} 秒。')
return result
return wrapper@running_timedef take_time():
time.sleep(2)take_time()----------------------------------------------------take_time运行花费了:2.01 秒。
18.用两种方法合并下面列表
x = ['a', 'b']y = ['c', 'd', 'e']x.extend(y)# 或x += y
19.计算得到列表当中长度最长的字符串words = ['Python', 'is', 'awesome']
words = ['Python', 'is', 'awesome']# 我的笨方法s = ''for _ in words:
if len(_) > len(s):
s = _print(s)# 标答给的方法
# 使用内置函数 max() 和 len() 计算,在 max() 函数中使用 key 参数指定了用于比较大小的函数为 len(),这样比较的就是字符串的长度,而不是字典序。longest_word = max(words, key=len)print(longest_word)
20.将列表中的顺序倒转 (2 种方法) words = ['Python', 'is', 'awesome']
words = ['Python', 'is', 'awesome']# 方法1,列表的reverse()方法(改变原列表)words.reverse()print(words)# 方法2,列表切片(生成新列表,原列表不变)w = words[::-1]print(w)
21.将字典当中的键值对位置调换
staff = {'Data Scientist': 'Mike', 'Django Developer': 'Dylan'}
答:
staff = {'Data Scientist': 'Mike', 'Django Developer': 'Dylan'}# 我的笨方法reverse = {}for k, v in staff.items():
reverse[v] = kprint(reverse)# 标答给的方法(字典推导式)inverted_staff = {v: k for k, v in staff.items()}print(inverted_staff)
22.将嵌套列表合并为一个列表
l = [[1, 2, 3], [4, 5], [6], [7, 8], [9]]
答:
# 针对此题目,我想到的是循环遍历result = []for i in l:
for n in i:
result.append(n)print(result)# 标答给的方法1:
# 在 sum() 函数中使用的第二个参数是空列表,表示从空列表开始计算和。merged_list = sum(l, [])print(merged_list) # 标答给的方法2:列表推导式merged_list = [item for sublist in l for item in sublist]print(merged_list)# 可如上3个方法只能针对此题目,一旦题目列表没这么规范,比如下面的列表,那么如上3个方法就无法实现l = [0, [1, 2, [3]], [4, 5], [6], [7, 8], [9]]# 能够同时解决如上问题的方法是使用递归函数def flatten_list(l):
flat_list = []
for item in l:
if type(item) == list:
flat_list.extend(flatten_list(item))
else:
flat_list.append(item)
return flat_listprint(flatten_list(l))
23.列表当中数据类型的转换
-
我们要将其转换成整数类型====>['1', '2', '3']
-
我们要将其转换成浮点数类型====>['1', 2, '3.0', 4.0, '5', 6]
答:
l = ['1', '2', '3']l1 = [int(i) for i in l]print(l1)l2 = ['1', 2, '3.0', 4.0, '5', 6]l3 = [float(i) for i in l2]print(l3)
24.将列表转化成字典
cars = ['Audi', 'BMW', 'Ford', 'Tesla', 'Volvo']
答:
cars = ['Audi', 'BMW', 'Ford', 'Tesla', 'Volvo']d = dict.fromkeys(cars, 0)print(d)
25.将列表当中的重复元素去除
['a', 'a', 'b', 'a', 'c']
答:
l = ['a', 'a', 'b', 'a', 'c']l1 = list(set(l))print(l1)
26.从列表中筛选出特定元素 (2种方法)
cars = ['Audi', 'BMW', 'Ford', 'Tesla', 'Volvo']
答:
cars = ['Audi', 'BMW', 'Ford', 'Tesla', 'Volvo']# 我想到的是列表推导式l = [i for i in cars if i == 'Volvo']print(l)# 标答使用filterl2 = filter(lambda car: car == 'Volvo', cars)print(list(l2))# filter 函数在 Python 中返回一个过滤器对象,它是一个迭代器,所以想打印值需要使用list()
27.列表中的元素排序
numbers = [55, -30, 28, -36, 48, 20]
cars = ['Ford', 'Tesla', 'BMW', 'Volvo', 'Audi']
答:
numbers.sort()print(numbers)cars.sort()print(cars)
28.合并集合
set1 = {"1", "2", "5"}
set2 = {"4", "6", "7"}
答:
set1.update(set2)print(set1)
29.根据键来对字典进行排序
d = {'one': 1, 'three': 4, 'five': 8, 'six': 10, 'two': 2}
答:
sorted_dict = sorted(d.items(), key=lambda x: x[0])print(sorted_dict)
30.根据键值来对字典进行排序
d = {'one': 1, 'three': 4, 'five': 8, 'six': 10, 'two': 2}
答:
sorted_dict = sorted(d.items(), key=lambda x: x[1])print(sorted_dict)
31.替换字符串
s = "Python is a programming language. Python is awesome"# 字符类型是不可变数据类型,str.replace()方法不会改变原字符串,会生成新值ss = s.replace('P','p')print(ss)
32.计算指定字符串出现的次数
a = 'python is a programming language. python is python.'
答:
# 我得笨方法:d = {}for _ in a:
if _ not in d:
d[_] = 1
else:
d[_] += 1print(d['p'])# 标答print(a.count('p'))
33.将矩阵转置
a = [[1, 2, 3],
[4, 5, 6],
[7, 8, 9]]
答:
b = [list(s) for s in zip(*a)]print(b)
34.生成斐波那契数列
斐波那契数列是一个数列,其中的每个数都是前两个数的和。
答:
def fibonacci(n):
if n <= 0:
return []
elif n == 1:
return [0, ]
elif n == 2:
return [0, 1]
else:
fib = [0, 1]
for i in range(2, n):
fib.append(fib[-1] + fib[-2])
return fibprint(fibonacci(10))-----------------------------------------------------[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
35.冒泡排序
def bubble(l):
for i in range(len(l) - 1):
for j in range(len(l) - 1 - i):
if l[j] > l[j + 1]:
l[j], l[j + 1] = l[j + 1], l[j]
return l
36.Python的解包是什么?
解包是 Python 里的一种特殊赋值语句,可以把可迭代对象 (列表,元祖,字典,字符串) 一次性赋给多个值。
a, b, c = (1, 2, 3)
a, b = b, a
numbers = [1, 2, 3, 4, 5]for a, b in zip(numbers, numbers[1:]):
print(a, b)---------------------------------------------------------1 22 33 44 5
注意:
-
普通变量解包,需要注意变量数量和列表中元素数量应保持一致,否则报'SyntaxError: cannot assign to literal';
-
如果可迭代对象是字典,则变量个数要等于字典中的键值对个数,若不指定字典的取值,则默认取字典的 key 值,若指定字典的值为 items(),则变量为字典键值对的元组形式;
37.有一个key为姓名,value为年龄的字典,根据年龄正序排列姓名,并输出姓名列表,若没有年龄,则放在最后;
users = {'tom': 19, 'jerry': 13, 'jack': None, 'andrew': 43}
答:
知识点:在排序前将年龄为 None 的值变更为正无穷大;
-
正无穷:float("inf")
-
负无穷:float("-inf")
# 我想到的方法:
# 先将字典中年龄为None的值变更成正无穷大def key_func(users):
for key in users.keys():
if users[key] is None:
users[key] = float('inf')# 根据年龄进行正序排序,最后以列表形式输入排序好的姓名def sort_user(user: dict):
sort_item_for_v = sorted(user.items(), key=lambda x: x[1])
return [user[0] for user in sort_item_for_v]key_func(users)print(sort_user(users))-------------------------------------------------------------------------------['jerry', 'tom', 'andrew', 'jack']
# 参考答案:def sort_user_inf(users: dict):
"""
接收一个key为姓名,value为年龄的字典,根据年龄正序排列姓名,若没有年龄,则放在最后
:param users: 用户名:年龄字典
:return: 返回姓名列表
"""
def key_func(username):
age = users[username]
# 当年龄为空时,返回正无穷大作为key,因此就会被排到最后 return age if age is not None else float('inf')
return sorted(users.keys(), key=key_func)print(sort_user_inf(users))-----------------------------------------------------------------------------['jerry', 'tom', 'andrew', 'jack']
38.合并字典的多种方式?
-
方式 1:update
d1 = {'name': 'Lili', 'score': 90}d2 = {'name': 'Tom', 'hobby': 'run'}d1.update(d2)print(d1)-----------------------------{'name': 'Tom', 'score': 90, 'hobby': 'run'}# 这种方法会改变d1的原始值
-
方式 2:编写一个函数 (不改变 d1 的原始值)
d1 = {'name': 'Lili', 'score': 90}d2 = {'name': 'Tom', 'hobby': 'run'}def update_dict(d1: dict, d2: dict):
d = d1.copy()
d.update(d2)
return dprint(update_dict(d1,d2))print(d1)print(d2)---------------------------------------{'name': 'Tom', 'score': 90, 'hobby': 'run'}{'name': 'Lili', 'score': 90}{'name': 'Tom', 'hobby': 'run'}
-
方式 3:解包
d1 = {'name': 'Lili', 'score': 90}d2 = {'name': 'Tom', 'hobby': 'run'}print({**d1, **d2})print({**d2, **d1})print(d1)print(d2)--------------------------------------{'name': 'Tom', 'score': 90, 'hobby': 'run'}{'name': 'Lili', 'hobby': 'run', 'score': 90}{'name': 'Lili', 'score': 90}{'name': 'Tom', 'hobby': 'run'}
-
方式 4:Python3.9 版本后可用"|"合并字典
d1 = {'name': 'Lili', 'score': 90}d2 = {'name': 'Tom', 'hobby': 'run'}print(d1|d2)print(d2|d1)print(d1)print(d2)-----------------------------{'name': 'Tom', 'score': 90, 'hobby': 'run'}{'name': 'Lili', 'hobby': 'run', 'score': 90}{'name': 'Lili', 'score': 90}{'name': 'Tom', 'hobby': 'run'}
39.json和Python中的dict有什么区别?
-
1.JSON 的类型是字符串,字典的类型是 dict;
-
2.JSON 的 key 只能是字符串,字典的 key 可以是任何可 hash 对象,如:字符串,数字,元祖;
-
3.JSON 的 key 是有序的,可以重复的;字典的 key 不能重复;
-
4.JSON 的 key 有默认值 undefined,字典的 key 默认没有默认值;
-
5.JSON 的 value 可以是字符串、浮点数、布尔值、null 或者他们组成的数组或对象,字典的 value 可以是任意类型的对象;
-
6.JSON 的 value 访问方式可以是 [] 或者.,字典的 value 访问方式只能是 key 值;
-
7.JSON 的字符串强制使用双引号,字典的字符串可以是单引号也可以是双引号;
-
8.字典可以嵌套元祖,JSON 只有数组;
-
9.真假空的表示:JSON(true,false,null),字典(True,False,None);
-
10.JSON 的中文必须是 Unicode 编码,如"\u6211"。
40.Python列表和字符串的相互转换?
第一种类型:
s = '1,2,3,4,5'l = s.split(',')print(l)l2 = [int(i) for i in l]print(l2)--------------------['1', '2', '3', '4', '5'][1, 2, 3, 4, 5]
第二种类型:
l1 = ['1','2','3']s1 = ','.join(l1)print(s1)---------------1,2,3
第三种类型:
l2 = [1,2,3]s2 = ','.join([str(i) for i in l2])print(s2)-----------------------------1,2,3
最后:下方这份完整的软件测试视频学习教程已经整理上传完成,朋友们如果需要可以自行免费领取 【保证100%免费】
这些资料,对于【软件测试】的朋友来说应该是最全面最完整的备战仓库,这个仓库也陪伴上万个测试工程师们走过最艰难的路程,希望也能帮助到你!