Python-函数记录-技巧记录-特性记录

本文最后更新于:2021年1月18日 下午

函数

sorted

函数说明

sorted(iterable, key=None, reverse=False)

参数 参数说明
iterable 可迭代对象
key 主要是用来进行比较的元素,只有一个参数,具体的函数的参数就是取自于可迭代对象中,指定可迭代对象中的一个元素来进行排序。
reverse 排序规则
reverse = True 降序
reverse = False 升序(默认)

用例

1
2
>>>sorted([5, 2, 3, 1, 4])
[1, 2, 3, 4, 5] # 默认为升序
1
2
3
4
>>>array = [{"age":20,"name":"a"},{"age":25,"name":"b"},{"age":10,"name":"c"}]
>>>array = sorted(array,key=lambda x:x["age"])
>>>print(array)
[{'age': 10, 'name': 'c'}, {'age': 20, 'name': 'a'}, {'age': 25, 'name': 'b'}]

zip

函数说明

1
zip([iterable, ...])
参数 参数说明
iterable 可迭代对象

zip() 函数用于将可迭代的对象作为参数,将对象中对应的元素打包成一个个元组,然后返回由这些元组组成的列表

如果各个迭代器的元素个数不一致,则返回列表长度与最短的对象相同,利用 * 号操作符,可以将元组解压为列表

用例

1
2
3
4
5
6
7
8
9
>>>a = [1,2,3]
>>>b = [4,5,6]
>>>c = [4,5,6,7,8]
>>> zipped = zip(a,b) # 打包为元组的列表
[(1, 4), (2, 5), (3, 6)]
>>> zip(a,c) # 元素个数与最短的列表一致
[(1, 4), (2, 5), (3, 6)]
>>> zip(*zipped) # 与 zip 相反,*zipped 可理解为解压,返回二维矩阵式
[(1, 2, 3), (4, 5, 6)]

技巧

生成式

基础

案例:遍历生成新数据

1
2
3
>>>a = [1,2,3,4,5]
>>>[i for i in a]
[1, 2, 3, 4, 5, 6]

案例:遍历计算生成新数据

1
2
3
>>>a = [1,2,3,4,5]
>>>[i+2 for i in a]
[3, 4, 5, 6, 7, 8]

带判断

注意:判断在执行计算之前,如果判断为False,则不进入计算流程,直接忽略

案例:遍历判断计算生成新数据

1
2
>>>[i+2 for i in a if i!=2]
[3, 5, 6, 7, 8]

双生

案例:遍历两个list,计算生成新数据

1
2
3
>>> a, b = ['a', 'b'], ['c', 'd']
>>> [x+y for x in a for y in b]
['ac', 'ad', 'bc', 'bd']

案例:遍历两个list,进行判断计算生成新数据

1
2
3
>>> a, b = ['a', 'b', 'c'], ['d', 'e', 'f']
>>> [x+y for x in a if x!='b' for y in b if y!='e']
['ad', 'af', 'cd', 'cf']

更美观的print - pprint

print函数能在终端输出一些东西
如果输出的内容是一个数组或元组,且其元素类型各不相同,数量又多,看着很难受。此时可以使用 pprint函数 来取代 print函数

pprint()模块打印出来的数据结构更加完整,每行为一个数据结构,更加方便阅读打印输出结果

缺点在于使用需要 from pprint import pprint

更美观的字符串拼接 - format

使用format函数能让print函数看上去更美观,同时不会出现需要转换类型的问题

1
2
3
name = 'Rem'
country = 'China'
print("Hi, I'm {} from {}. {1} {0}",format(name,country))

输出结果

1
Hi, I'm Rem from China. China Rem

更好的遍历

使用enumerate

如果你希望遍历元素,又不希望使用变量计算索引,可以试试enumerate

1
2
3
4
5
6
a = ['a', 'b', 'c']
for i, x in enumerate(a):
print(i, x)
>>>0 a
>>>1 b
>>>2 c

使用{}表达式

python3.6以上才有的特性

1
2
3
4
5
name = 'Rem'
country = 'China'
age = 18
print("Hi, I'm {name} from {country}. {age-2} years old")
>>>Hi, I'm Rem from China. 16 years old

eval() 与 json.loads()

eval()能计算表达式,也能将str转为 python对象
json.loads()能将str转为 python对象
如果只是将写着JSON内容的字符串转为python对象,那么json.loads()的转换速度比eval()大概快十倍

要点

Python的GIL

GIL是什么

GIL全称Global Interpreter Lock
GIL并不是 Python 的特性,它是在实现Python解析器(CPython)时所引入的一个概念。

官方解释:
In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple native threads from executing Python bytecodes at once. This lock is necessary mainly because CPython’s memory management is not thread-safe. (However, since the GIL exists, other features have grown to depend on the guarantees that it enforces.)

个人翻译:
在CPython中,GIL是一个用来防止多个线程同时请求同一机器码的互斥锁。CPython的内存管理方式是线程不安全的,所以这个锁是很有必要设置的。

线程安全与线程不安全

  • 线程安全
    指多个线程在执行同一段代码的时候采用加锁机制,使每次的执行结果和单线程执行的结果都是一样的,不存在执行程序时出现意外结果。
  • 线程不安全
    是指不提供加锁机制保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。

GIL为何存在

由于物理上得限制,各CPU厂商在核心频率上的比赛已经被多核所取代。
为了更有效的利用多核处理器的性能,就出现了多线程的编程方式,而随之带来的就是线程间数据一致性和状态同步的困难。为了有效解决多份缓存之间的数据同步时各厂商花费了不少心思,也不可避免的带来了一定的性能损失。

Python当然也逃不开,为了利用多核,Python开始支持多线程。而解决多线程之间数据完整性和状态同步的最简单方法自然就是加锁。 于是有了GIL这把超级大锁,而当越来越多的代码库开发者接受了这种设定后,他们开始大量依赖这种特性(即默认python内部对象是thread-safe的,无需在实现时考虑额外的内存锁和同步操作)。

慢慢的这种实现方式被发现是蛋疼且低效的。但当大家试图去拆分和去除GIL的时候,发现大量库代码开发者已经重度依赖GIL而非常难以去除了。有多难?做个类比,像MySQL这样的“小项目”为了把Buffer Pool Mutex这把大锁拆分成各个小锁也花了从5.5到5.6再到5.7多个大版为期近5年的时间,并且仍在继续。MySQL这个背后有公司支持且有固定开发团队的产品走的如此艰难,那又更何况Python这样核心开发和代码贡献者高度社区化的团队呢?

所以简单的说GIL的存在更多的是历史原因。如果推到重来,多线程的问题依然还是要面对,但是至少会比目前GIL这种方式会更优雅。

GIL的影响

从上文的介绍和官方的定义来看,GIL无疑就是一把全局排他锁。毫无疑问全局锁的存在会对多线程的效率有不小影响。甚至就几乎等于Python是个单线程的程序。 那么读者就会说了,全局锁只要释放的勤快效率也不会差啊。只要在进行耗时的IO操作的时候,能释放GIL,这样也还是可以提升运行效率的嘛。或者说再差也不会比单线程的效率差吧。理论上是这样,而实际上呢?Python比你想的更糟。

Python的多线程在多核CPU上,只对于IO密集型计算产生正面效果;而当有至少有一个CPU密集型线程存在,那么多线程效率会由于GIL而大幅下降

用 multiprocessing 替代 Thread

multiprocessing库的出现很大程度上是为了弥补thread库因为GIL而低效的缺陷。
它完整的复制了一套thread所提供的接口方便迁移。唯一的不同就是它使用了多进程而不是多线程。每个进程有自己的独立的GIL,因此也不会出现进程之间的GIL争抢。

当然multiprocessing也不是万能良药。
它的引入会增加程序实现时线程间数据通讯和同步的困难。就拿计数器来举例子,如果我们要多个线程累加同一个变量,对于thread来说,申明一个global变量,用thread.Lock的context包裹住三行就搞定了。而multiprocessing由于进程之间无法看到对方的数据,只能通过在主线程申明一个Queue,put再get或者用share memory的方法。这个额外的实现成本使得本来就非常痛苦的多线程程序编码,变得更加痛苦了。

所以没救了么?

当然Python社区也在非常努力的不断改进GIL,甚至是尝试去除GIL。并在各个小版本中有了不少的进步。有兴趣的读者可以扩展阅读这个Slide 另一个改进Reworking the GIL

将切换颗粒度从基于opcode计数改成基于时间片计数
避免最近一次释放GIL锁的线程再次被立即调度
新增线程优先级功能(高优先级线程可以迫使其他线程释放所持有的GIL锁)

总结

Python GIL其实是功能和性能之间权衡后的产物,它尤其存在的合理性,也有较难改变的客观因素。

  • 因为GIL的存在,只有IO Bound场景下得多线程会得到较好的性能
  • 如果对并行计算性能较高的程序可以考虑把核心部分也成C模块,或者索性用其他语言实现
  • GIL在较长一段时间内将会继续存在,但是会不断对其进行改进

参考:

不同目录import

import 说到底是根据路径导入
这个导入路径有多个,可以使用sys.path来查看

1
2
import sys
sys.path

添加sys以处理不同级导入

1
2
3
4
5
6
7
8
9
import sys
import os
CURRENT_DIR = os.path.split(os.path.abspath(__file__))[0] # 当前目录
config_path = CURRENT_DIR.rsplit('/', 3)[0] # 上三级目录
sys.path.append(config_path)
from config import param
print('config_path=', config_path)
print(sys.path)
print(param)

小整数池

为避免整数频繁申请和销毁内存空间,Python 定义了一个小整数池 [-5, 256] 这些整数对象是提前建立好的,不会被垃圾回收

以下代码请在 终端Python环境下测试,如果你是在IDE中测试,由于 IDE 的影响,效果会有所不同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
>>> a = -10
>>> b = -10
>>> a is b
False

>>> a = 256
>>> b = 256
>>> a is b
True

>>> a = 257
>>> b = 257
>>> a is b
False

>>> a = 257; b = 257
>>> a is b
True

字符串驻留 intern

Python解释器中使用了 intern(字符串驻留)的技术来提高字符串效率

什么是intern机制
同样的字符串对象仅仅会保存一份,放在一个字符串储蓄池中,共用。这也决定了字符串必须是不可变对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
>>> s1="hello"
>>> s2="hello"
>>> s1 is s2
True

# 如果有空格,默认不启用intern机制
>>> s1="hell o"
>>> s2="hell o"
>>> s1 is s2
False

# 如果一个字符串长度超过20个字符,不启动intern机制
>>> s1 = "a" * 20
>>> s2 = "a" * 20
>>> s1 is s2
True

>>> s1 = "a" * 21
>>> s2 = "a" * 21
>>> s1 is s2
False

>>> s1 = "ab" * 10
>>> s2 = "ab" * 10
>>> s1 is s2
True

>>> s1 = "ab" * 11
>>> s2 = "ab" * 11
>>> s1 is s2
False

其它

Python 之禅

当你尝试引入this

1
import this

你会发现它会输出这样一段话

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
The Zen of Python, by Tim Peters

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!

翻译

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Python之禅 by Tim Peters
优美胜于丑陋(Python 以编写优美的代码为目标)
明了胜于晦涩(优美的代码应当是明了的,命名规范,风格相似)
简洁胜于复杂(优美的代码应当是简洁的,不要有复杂的内部实现)
复杂胜于凌乱(如果复杂不可避免,那代码间也不能有难懂的关系,要保持接口简洁)
扁平胜于嵌套(优美的代码应当是扁平的,不能有太多的嵌套)
间隔胜于紧凑(优美的代码有适当的间隔,不要奢望一行代码解决问题)
可读性很重要(优美的代码是可读的)
即便假借特例的实用性之名,也不可违背这些规则(这些规则至高无上)

不要包容所有错误,除非你确定需要这样做(精准地捕获异常,不写 except:pass 风格的代码)

当存在多种可能,不要尝试去猜测
而是尽量找一种,最好是唯一一种明显的解决方案(如果不确定,就用穷举法)
虽然这并不容易,因为你不是 Python 之父(这里的 Dutch 是指 Guido )

做也许好过不做,但不假思索就动手还不如不做(动手之前要细思量)

如果你无法向人描述你的方案,那肯定不是一个好方案;反之亦然(方案测评标准)

命名空间是一种绝妙的理念,我们应当多加利用(倡导与号召)

很有意义的文字,不是吗

它的源码字符串使用了凯撒加密,有兴趣可以去看
定义路径:环境/lib/this.py

try-finaly-return 执行问题

1
2
3
4
5
6
7
8
9
10
11
12
13
def b():
try:
return 'A'
finally:
print('B')
try:
return 'C'
finally:
print('D')
print(b())
>>>B
>>>D
>>>C

无论是否发生异常,finally子句始终在离开try语句之前执行
如果try子句中发生了异常且未由except子句处理(或在exceptelse子句中发生),则在执行finally子句后重新引发该异常
try语句的任何其他子句通过breakcontinuereturn语句离开时,finally子句也将“在离开的try时候”执行

如果一个函数没有 return,会隐式的返回 None
函数的返回只有一个,如果显式声明了 return,那么会覆盖旧的 return

参考


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!