你确定学会了Python?Python技巧总结与语法糖分析
0. 内置函数 enumerate
偶然看到别人题解中 for i, item in enumerate(arr)
的写法,非常方便。
在需要获取元素index而又不关心数组长度情况下,推荐这种写法,节省了两行代码!
否则:
n = len(arr)
for i in range(n):
item = arr[i]
pass
# 或者
i = 0
for item in arr:
pass
i += 1
参考Python enumerate() 函数 | 菜鸟教程 (runoob.com),该函数输入为可迭代对象,可选第二个参数为开始下标位置,默认为0。
1. zip函数妙用
类似于map函数,内置函数zip将一组可迭代对象按每个元素顺序打包,返回元组的可迭代类型/列表。
一般配合符号 *
使用,将列表解压,可以理解为去掉了外层括号,将多个元素作为不定长参数。
二维矩阵转置:
a = [[1,2,3],[4,5,6]]
a_T = list(zip(*a)) # [(1, 4), (2, 5), (3, 6)]
2. Python传参
2023/04/13
写这道题1268. 搜索推荐系统碰到一个问题,需要部分搜索,返回前缀相同的字典序最小的三个字符串。
要求字典序最小,构建了字典树后,使用DFS即可。因为只要最多3个,常规做法就是维护全局变量,记录已经找到的答案。
查看原始DFS写法
# 全局变量
cnt = 3 # 还需要找的数量
now = [] # 已找到的字符串
def find(p, pre=''):
# 从p位置开始找3个
nonlocal now, cnt
if 'isEnd' in p:
now.append(pre)
cnt -= 1
for x in sorted(p.keys()):
if cnt==0:
break
if x!='isEnd':
find(p[x], pre+x)
研究发现,Python传递的可变参数,如list,dict类型其实都是引用,因此可以将全局变量传参进去,这样会使代码更加简洁。
def change(arr, i=1):
arr.append(i)
if len(arr)<10:
change(arr, i+1)
arr = []
change(arr)
print(arr) # [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
查看修改后DFS写法
def find(p, pre, now):
# 从p位置开始找3个
if 'isEnd' in p:
now.append(pre)
if len(now)==3:
return
for x in sorted(p.keys()):
if len(now)==3:
break
find(p[x], pre+x, now)
3. Python装饰器与__name__属性
2023/4/18
项目中有一个计时小程序是用装饰器实现的,代码如下:
查看装饰器计时代码
def how_much_time(func):
def inner(*args, **kwargs):
# 开始时间
t_start = time.time()
func(*args, **kwargs)
t_end = time.time()
print("一共花费了{:3f}秒时间".format(t_end - t_start, ))
return inner
需要对一个函数的运行时间计时,我们只要在函数前一行加上 @how_much_time
语句,那么程序运行过程中就会输出对应函数的运行时间。
这段代码有两个地方需要改进:
- 没有显示运行的函数名
- 无法获取函数返回值
修改后的代码如下:
# 利用内置属性__name__,加入调用函数名
# 计时的闭包函数需要返回值,否则实际调用一直返回None
def how_much_time(func):
def inner(*args, **kwargs):
# 开始时间
t_start = time.time()
result = func(*args, **kwargs)
t_end = time.time()
print("{}函数运行了{:3f}秒时间".format(func.__name__, t_end - t_start, ))
return result
return inner
Python通过闭包实现的装饰器。我们可以看到,函数 how_much_time
内部嵌套了一层函数 inner
,代码实际执行时,首先将调用函数名传递给参数func
,而函数参数通过可变长参数 args
和字典参数 kwargs
传递给 inner
函数。在代码第7行,保存了原始函数的输出结果,并在第11行返回。返回前是新增的运行时间计时输出。
__name__
是Python内置变量,通常我们可能通过 if __name__=='__main__'
来使用它,这里可以详见 __name__ 是什么来了解在模块中的作用。
简单总结不同情况下的该变量:
-
在模块中
- 在被直接调用
main.py
时,__name__
值设置为'__main__'
; - 被
import
导入时,被执行的main.py
时,__name__
值设置为模块名,如main
。如果在test.py
,则为test
。 - 因此上述代码可以避免被导入时运行
- 在被直接调用
-
在类或函数中
- Python的类和函数都包含了该内置方法,其值为类名和函数名
>>> print(int.__name__) int >>> print(map.__name__) map
- 进一步,我们可以通过
dir
命令获取类的内置属性和方法。
- Python的类和函数都包含了该内置方法,其值为类名和函数名
4. Python推导式与乘号重复
2023/4/22
使用动态规划算法时,我们通常需要提前分配内存,创建一个特定长度的数组,利用 Python 推导式 可以很快实现。
Python的以下四类数据结构都支持推导式语法,将普通的for
循环放入定义的三种括号中即可。
- 列表
[item for ...]
- 集合
{item for ...}
- 字典
{k:v for ...}
- 元组
(item for ...)
,产生生成器对象
为了方便,也可以用乘号来表示重复的内容:
a = [0 for _ in range(10)]
# 等价于
b = [0] * 10
特别的,列表和元组支持加号 +
来连接两个同类型列表/元组。
然而,数组的重复存在一个潜在问题。重复的每个元素其实是同一个对象,当它是一个数值类型(不可变类型,详见下一条)时可能不会有什么影响,但如果它是一个可变类型对象,或者嵌套多层,则会引发意想不到的后果。
# 定义二维数组
# 错误写法
mat = [[0]*n]*m
# 如果赋值 a[0][0] = x, 则每一行第一个元素都变成了 x
# 正确写法
mat = [[0]*n for _ in range(m)]
# 或者
mat = [[0 for _ in range(n)] for _ in range(m)]
# 错误写法 本质上只有一个dict对象,对dp[i]的更改会导致所有dp全部更改
dp = [dict()]*n
# 正确写法
dp = [dict() for _ in range(n)]
进一步验证每个元素的id值,可以发现由乘号创建的列表内的元素都是同一个(相同的id),如果该元素本身是数值,赋值时会被覆盖,将重新分配内存,不会产生影响。
因此务必记住,由乘号重复创建出来的元素指向同一个空间,如果内层包含了非数值型元素,请使用for循环的推导式。
5. 可哈希(hashable)与不可变类型
2023/4/25
Python不可变的数据结构包含:
-
数字类型(int,float,bool)
-
字符串str
-
元组tuple
-
自定义类的对象
对于不可变类型的对象和变量,每个对象都是唯一存在的,即相同的值的变量存储在相同的内存,而不同的值意味着不同的内存。在Python中,只有不可变数据类型是可哈希hashable的,可以作为哈希表的key(如set、dict类型)。
Python可变的数据结构包含
- 字典dict
- 列表list
- 集合set
这些类型是不可哈希的。对于同一个列表,我们在改变其内容(值)时,对应的列表内存(id)不变。哈希函数需要保证key到value是单射的,即相同的key必须对应相同的value,而可变数据类型不满足这一对应关系。对于可变类型,即使两个列表内容完全一样,他们的内存也会不同。
官方说明
An object is hashable if it has a hash value which never changes during its lifetime (it needs a
__hash__()
method), and can be compared to other objects (it needs an__eq__()
or__cmp__()
method). Hashable objects which compare equal must have the same hash value.
6. eval妙用:dict序列化/str类型转换
2023/4/26
Python的数值类型本身支持类型转换,例如在传递 float
类型数值时,可以把 int
看作取整函数,在传递 str
类型时,指定 base 可作进制转换。
int(3.5) # 3
float('1') # 1.0
float('inf') # 无穷大
str(123) # '123'
int('123', base=0) # 转化为显示值 123
int('0x10', base=16) # 16
对于 str
,可以将其他类型转换为 print
函数打印出来的字符串形式;对于自定义对象,我们可以实现 __str__
方法以实现对 str
类型的转换。
对于 set
,dict
类型则比较特殊,他们的参数不能是 str
,具体可用 help(dict)
查看说明。那么如何将转换为 str
的 dict
恢复回来呢?
这里用到 eval
函数,它会执行表达式求值,当字符串本身为字典类型,则结果产生一个字典。
另一种方式是利用 exec
函数,执行 Python 命令。
d = {'a':1, 'b':2}
# 转换为 str
s = str(d) # "{'a': 1, 'b': 2}"
# 1) eval函数
d1 = eval(s)
# 2) exec函数
exec("d2="+s) # 执行了 "d2={'a': 1, 'b': 2}" 命令
# 恢复了原始字典
assert d1==d
assert d2==d
Python文档说明,可见eval
第一个参数可以是Python表达式或者一段代码,后面参数可以传入全局和局部上下文。
eval(source, globals=None, locals=None, /)
Evaluate the given source in the context of globals and locals.The source may be a string representing a Python expression or a code object as returned by compile().
The globals must be a dictionary and locals can be any mapping, defaulting to the current globals and locals.
If only globals is given, locals defaults to it.
7. 再谈Python传参与默认参数
2023/4/28
在第二节中,我们知道了Python的函数参数对于可变类型都是引用传递,例如list,dict类型。那么如果默认参数是可变类型,会发生什么呢?
def func(n, arr=[]):
arr.append(n)
return arr
res1 = func(111)
res2 = func(222)
print(res2) # 输出 [111, 222]
print(id(res1)==id(res2)) # 输出 True
由以上测试代码可见,第二次调用 func
函数,默认参数并没有重新赋值为空,而仍然是第一次创建的变量 arr
。只有通过 func(222, [])
方式调用,重新创建新的空列表变量,才能得到预期结果。不同于C/C++,Python语言对参数的传递都没有产生新的拷贝,对于不可变类型(字符串或元组),本身只存在一个对象,只不过被新的标识(变量名)所指向;对于可变类型(列表,字典和集合),新传入的参数或者默认参数的生命周期可以看作是静态变量。
总的来说,如Python:值传递,引用传递?总结,Python语言中参数传递都可以看作是对象本身的传递,既不是值的复制,也不是引用的拷贝,就是原始对象的引用传递。对于传递后的行为,则根据赋值语句 =
或自身方法,可变类型和不可变类型会产生不同的行为。直接使用 =
赋值,两者都会指向新的对象。对于可变类型,要使其内容改变,列表赋值可采用 arr[:] = xxx
,或者调用自身方法,如 列表的 append
,集合的 add
等。
8. heapq与PriorityQueue
2023/5/1
在Python编程中,我们需要的堆/优先队列的数据结构由两个库提供,最基础的 heapq
和 queue
中的 PriorityQueue
类。Python的堆默认是小顶堆,即队首元素是最小值。Python未提供重载比较函数的方式来定义大顶堆,如有需要,可以以元组(-val, val)
作为新的item添加,或者直接插入负值。
以往我通常使用PriorityQueue来定义堆结构,因为其有更方便使用的接口,所有队列类都提供了统一的 get
, put
,empty
,qsize
等方法。不同于 get
方法直接出队列,如果只想拿到队首元素而不出队列(类似C++ 的 front
方法),Python可以直接操作底层列表,即
q = Queue()
q.put(1)
q.put(2)
q.queue # 队列所有元素
q.queue[0] # 队首元素
最近写题时遇到了提交评测超时,深入阅读两者源码后,尝试将经常使用又非常方便的PriorityQueue替换成heapq,问题得到了解决。从效率上考虑,在仅需要堆的特性时,务必优先使用heapq库。
首先来看两者使用的方式
使用库 | heapq | queue |
---|---|---|
定义堆 | import heapq q = [] |
from queue import PriorityQueue q = PriorityQueue() |
初始化堆 | q = [3,1,4,1,5,9] heapq.heapify(q) # O(n)复杂度! |
不支持,除非直接操作 q.queue |
插入元素 | heapq.heappush(q, item) | q.put(item) |
弹出队首 | heapq.heappop(q) | q.get() |
判断为空 | q!=[] | not q.empty(),q.qsize()>0 |
其他 | heappushpop:先push再pop最小值 heapreplace:先pop最小值再push当前item |
多线程支持 join:阻塞直到队列元素均被处理 task_done:向已完成的队列发送信号 |
merge:合并多个有序列表 | ||
nlargest:前n大的元素 nsmallest:前n小的元素 |
两者的差异
heapq库提供函数式编程方法,封装了小顶堆相关的操作,操作对象为原生列表。
在Python文档中,详细介绍了堆(heap)的特点和Python语言下的特性。
- 下标从0开始,而不是通常教科书的定义,节点 i 的两个儿子分别为 \(2*i+1\) 和 \(2*i+2\)
- heapq构建的是小顶堆, q[0]是最小元素
- 如需使用大顶堆,可以使用以下方法
众所周知,堆基于两个siftup 和 siftdown基础方法实现 O(logn) 的插入和删除堆顶元素:
-
插入时先将元素放到末尾,不断向上判断当前节点是否满足堆的特性,直到找到合适位置
-
删除时先将末尾元素放到树根,不断向下判断当前节点是否满足堆的特性,直到找到合适位置
-
由于调整只影响二叉树上的一条链,操作次数最多为树的高度 logn
queue是基于heapq和treading实现的生产者-消费者队列库,提供了三种不同的队列。其中 Queue(先进先出队列) 是 PriorityQueue(优先队列)和 LifoQueue(后进先出队列)的基类。
在Queue的构造函数中可以看到,队列引入了进程间通信的互斥量 mutex
锁和三个 Condition:队列不空,队列未满和所有任务已完成,以及一个全局常量 未完成任务数 unfinished_tasks
。
在 get
和 put
方法中,队列需要考虑进程间的通信,判断是否阻塞,等待超时等。这些操作无疑引入了额外的资源消耗和计算量。
9. 二分查找与bisect模块
2023/6/10
二分查找是一个重要的思想,通常用于在有序列表中的高效(logN时间)查询。Python提供了bisect模块,掌握了该模块,妈妈再也不用担心不会边界判断和死循环啦。
bisect模块非常简单,查阅源码,可见其中只有4个函数:['bisect_left', 'bisect_right', 'insort_left', 'insort_right'],而'bisect'就是'bisect_right','insort'就是'insort_right'。其中bisect
用于查找插入index,insort
调用插入位置后,再执行元素的插入,保持列表的有序。
这里的左右的含义是,查找元素与数组中元素相等时,我们的下标需要停下的位置:
- bisect_left:返回第一个>=val的位置 i,即 a[:i] < val,而 a[i:] >= val
- bisect_right:返回第一个>val的位置 i,即 a[:i] <= val,而 a[i:] > val
用于计数:
- 数组arr中小于val的个数:
bisect_left(arr, val)
- 数组arr中大于val的个数:
n - bisect_right(arr, val)
- 数组arr中小于等于val的个数:
bisect_right(arr, val)
- 数组arr中大于等于val的个数:
n - bisect_left(arr, val)
(长期更新,未完待续...)