你确定学会了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 命令获取类的内置属性和方法。

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 类型的转换。

对于 setdict 类型则比较特殊,他们的参数不能是 str,具体可用 help(dict) 查看说明。那么如何将转换为 strdict 恢复回来呢?

这里用到 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编程中,我们需要的堆/优先队列的数据结构由两个库提供,最基础的 heapqqueue 中的 PriorityQueue 类。Python的堆默认是小顶堆,即队首元素是最小值。Python未提供重载比较函数的方式来定义大顶堆,如有需要,可以以元组(-val, val)作为新的item添加,或者直接插入负值。

以往我通常使用PriorityQueue来定义堆结构,因为其有更方便使用的接口,所有队列类都提供了统一的 getputemptyqsize 等方法。不同于 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

getput 方法中,队列需要考虑进程间的通信,判断是否阻塞,等待超时等。这些操作无疑引入了额外的资源消耗和计算量。

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)

(长期更新,未完待续...)

posted @ 2023-04-14 15:47  izcat  阅读(88)  评论(0编辑  收藏  举报