前言
本文为本人长期的脚本编写总结,涵盖的知识点为python的基础,但又是大家容易疏忽的,希望能对大家起到抛砖引玉的作用。
参数
痛点
实际工作中,经常会遇到写完代码后,测试发现结果和自己预期的不一致,于是开始一层层的debug,花费很多时间,最后发现仅仅是传参过程中数据结构的改变导致的问题。
解析
赋值
这种方式是我们最常用的一种方式,即b=a。这个很好理解,只要a变了,b也随之改变。
浅拷贝
浅拷贝会创建新对象,其内容非原对象本身,而是原对象内第一层对象的引用。实现浅拷贝有三种方式:切片(b=a[:])、工厂函数(b=set(a))、copy模块中的copy函数(b=copy.copy(a))。
由于浅拷贝是拷贝对象的引用,目标对象会记录下:容器类型、容器大小以及容器内每一个对象的引用。所以当改变源对象的容器类型、容器大小、容器内对象的引用地址时,都不会引起目标对象的变化。但有时候我们操作源对象的时候,会直接将容器内的对象直接改变,这个时候目标对象也会随之发生变化。
# 源对象a
a = [1, (1, 2), [1, 2]]
# 浅拷贝b,另外两种方式:b=list(a),b=copy.copy(a)
b = a[:]
# tuple不可变,其实是指向一个全新的tuple对象,改变a内部的引用,所以b不会发生变化
a[1] += (3, 4)
# 改变容器大小,不会引起b的变化
a.append(90)
# list可变,改变的是对象本身,所以b会随之发生变化
a[2].append(10)
# 改变容器类型,不会引起b的变化
a = tuple(a)
print(a)
print(b)
----------------------------------
(1, (1, 2, 3, 4), [1, 2, 10], 90)
[1, (1, 2), [1, 2, 10]]
深拷贝
深拷贝是将源对象,重新创建一遍,要注意的是它的时间和空间开销很大。实现深拷贝的方式:copy模块中的deepcopy函数(b=copy.deepcopy(a))。
深拷贝的一个重要特点,源对象与目标对象完全隔离,操作源对象并不会对目标对象产生任何影响。
总结
- 函数接收参数,如果是可变类型的参数,最好在函数开始时,使用深拷贝对参数进行拷贝,以免对源对象产生影响;
- 函数接收参数,尽量使用不可变的数据类型,避免使用复杂的数据类型,例如list最好不要作为函数入参。
- 测试脚本编写常见的问题,接口A返回response,直接作为入参传给接口B解析作为入参使用,正确的做法应该是先解析,再传参。如果一定图方便,请使用深拷贝。
扩展
python函数之间的参数传递,并非像其他语言那样有形参和实参的概念,他的传递方式为赋值传递,可以简单理解为浅拷贝的传递方式(当然并不准确)。
装饰器
痛点
实际工作中,很多函数中很可能包含一部分通用的代码块,实际函数的逻辑代码和这一部分代码块又没有太多关系,直接在函数内部写这部分代码,会让代码显得冗余并且不可读。举一个经典的测试场景:接口调用时可能因为环境原因出错,我们应该引入延时重试机制,避免环境原因导致测试脚本的执行失败。通用的例子是日志打印,不过日志打印有封装好的装饰器,无需自行封装。
解析
函数
学习装饰器,必须得知道,在python中函数也是对象,我们可以将函数一些重要的知识点:
- 函数可以作为参数传递
- 函数可以进行嵌套使用,层数可以为2+
- 嵌套使用的时候,会涉及闭包的概念
下面举几个代码的例子做一下解析
def get_message(message):
return message
def call(func, message):
print(func(message))
call(get_message, "hello world")
----------------------------------
hello world
这个知识点的经典应用是threading的调用,target参数给的就是目标函数。
def outer_func():
# 嵌套使用的时候,内部函数外面的变量会记录下来,下次再次调用的时候,会在此基础上进行变更;且多次调用时是隔离的
loc_list = []
def inner_func(func):
loc_list.append(len(loc_list) + 1)
print(f'{func} loc_list = {loc_list}')
return inner_func
clo_func_0 = outer_func()
clo_func_0('clo_func_0')
clo_func_0('clo_func_0')
clo_func_0('clo_func_0')
clo_func_1 = outer_func()
clo_func_1('clo_func_1')
clo_func_0('clo_func_0')
clo_func_1('clo_func_1')
----------------------------------
clo_func_0 loc_list = [1]
clo_func_0 loc_list = [1, 2]
clo_func_0 loc_list = [1, 2, 3]
clo_func_1 loc_list = [1]
clo_func_0 loc_list = [1, 2, 3, 4]
clo_func_1 loc_list = [1, 2]
-
闭包中的引用的自由变量只和具体的闭包有关联,闭包的每个实例引用的自由变量互不干扰。
-
一个闭包实例对其自由变量的修改会被传递到下一次该闭包实例的调用。
装饰器
def retry(retry_cnt=retry_cnt):
def _decorator(func):
# 自己封装装饰器时,这个不能缺失,这个是将添加装饰器的函数还保留原属性,如果不加上,原函数就会被更改,无法进行调用
@functools.wraps(func)
def wrapper(*args, **kwargs):
for cnt in range(retry_cnt + 1):
result = func(*args, **kwargs)
# 根据实际情况判断是否重试
if result['status']:
return result
elif cnt == retry_cnt:
return result
else:
time.sleep(sleep)
return wrapper
return _decorator
@retry() # 带有参数的装饰器,必须加()
def run_func():
... # ...和pass功能一致
总结
装饰器作为一个必知必会的知识点,对于精简脚本和增加可读性,有至关重要的作用。
线程
痛点
实际工作中,有一些接口是异步接口,并非实时完成操作的,在做校验的时候,我们不可能让主进程一直等待,这样对于时间和空间的浪费是不被允许的。这个时候,可以开启一个监听线程,轮询监听,当接收到成功或者失败的结果后,再同步给主线程进行结果的验证。
解析
使用线程,一般我们使用的threading库,下面介绍一下该包的一些常用函数。
# 子线程跑的函数为func,args为函数的参数
thread = threading.Thread(target=func,args=(arg1,arg2))
# 监听主线程,因为是脚本,所以主动停止的概率很大
thread.setDaemon(True)
# 子线程开始运行
thread.start()
总结
threading包是不支持直接获取调用函数的结果的,当然我们在threading开启子线程时,一般情况下,也是不关注调用函数的返回值的。如果需要拿到返回值:1.可以考虑使用全局变量的方式,毕竟主子线程互锁一般也是通过全局变量来实现的,我一般是这样实现的;2.自己将threading扩展一下,但这样主线程还是需要等待子线程的执行,一般不推荐使用;3.使用进程池,但使用concurrent.futures的场景和使用threading的场景其实并不一样,使用进程池的场景是下面跑的代码CPU heavy,通过多核来减轻压力加快执行速度;而threading使用场景多为子线程为监控线程,等待某一执行结果,在I/O比较多时推荐使用。
内置函数
痛点
实际工作中,会发现,大量的代码其实玩的都是list,当然写接口用例时,json也非常多,但json只是需要根据对应的key获取相应的value, 并不会涉及大量的操作;而list我们需要大量的加工操作。其实python内置很多list相关的函数,并不需要自己去实现,所以了解这些内置函数的功能,对于提高代码编写效率至关重要。
解析
这是一张关于JS中map/filter/reduce函数的解析图,不过用在python中也非常合适
lambda
匿名函数,只用一次或者逻辑较为简单等情况,可以使用该函数。
map
对sequence中的item依次执行function(item),执行结果输出为list。
filter
对sequence中的item依次执行function(item),将执行结果为True(!=0)的item组成一个List/String/Tuple(取决于sequence的类型)返回,False则退出(0),进行过滤。
reduce
对sequence中的item顺序迭代调用function,函数必须要有2个参数。要是有第3个参数,则表示初始值,可以继续调用初始值,返回一个值。
from functools import reduce
# 每个元素立方
print(list(map(lambda x: x ** 3, [1, 2, 3, 4, 5])))
# 过滤奇数
print(list(filter(lambda x: x % 2 == 1, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10])))
# 所有元素求乘积,reduce在functools库中
print(reduce(lambda x, y: x * y, [1, 2, 3, 4, 5]))
enumerate
函数用于将一个可遍历的数据对象(如列表、元组或字符串)组合为一个索引序列,同时列出数据和数据下标,提供start作为起始索引(一般索引以0开始,但可以通过start来自定义)。
list1 = ["a", "b", "c", "d"]
for index, value in enumerate(list1,1):
print({index: value})
----------------------------------
{1: 'a'}
{2: 'b'}
{3: 'c'}
{4: 'd'}
sorted
对List、Dict进行排序的内置函数,能够实现多个字段一起排序。
# 先通过a对应的value排序,然后再通过b对应的value倒序
a = [{"a": 1, "b": 1, "c": "abc", "d": "a"}, {"a": 2, "b": 1, "c": "gbt", "d": "b"}, {"a": 1, "b": 2, "c": "abt", "d": "a"}, {"a": 2, "b": 2, "c": "erg", "d": "b"}]
print(sorted(a, key=lambda x: (int(x['a']), -int(x['b']))))
----------------------------------
[{'a': 1, 'b': 2, 'c': 'abt', 'd': 'a'}, {'a': 1, 'b': 1, 'c': 'abc', 'd': 'a'}, {'a': 2, 'b': 2, 'c': 'erg', 'd': 'b'}, {'a': 2, 'b': 1, 'c': 'gbt', 'd': 'b'}]
总结
python内置函数当然还有很多很多,如上几个是比较常用,但又很容易忽略的,了解这些可以大大提高写代码的效率。其中map/filter/erduce这三个函数需要注意,他们最后返回的是一个iteration,并非list,所以需要转一下,或者直接使用该iteration也可以。
写在最后
限于能力与篇幅,就总结如上几个知识点。不过鉴于本人的工作经验,上面这些知识点是比较重要的,且又是大家容易忽视的,灵活掌握并使用这些,对于提高工作效率还是比较重要的,希望对大家能有所帮助,谢谢~~~