学习内容来自极客时间专栏
由于广告原因,CSDN官方不让放截图。大家可以自行搜索就好啦。
安装Jupyter Notebook
- 安装教程:https://jupyter.org/install.html安装教程
- windows下安装:
pip install jupyterlab
jupyter notebook
列表和元组用哪一个
- 列表和元组都是一个可以放置任意数据类型的有序集合。
- 列表是动态的:长度大小不固定,可以随意地增加、删减或者改变元素(mutable)。
- 元组是静态的:长度大小固定,无法增加删减或者改变(immutable)。
l = [1, 2, 3, 4]
l[3] = 40 # 和很多语言类似,python中索引同样从0开始,l[3]表示访问列表的第四个元素
l
[1, 2, 3, 40]
tup = (1, 2, 3, 4)
tup[3] = 40
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment
元组是不可变类型,如果想改变元组某个位置的值,系统会再分配一块新的内存空间给元组,而列表可以直接追加或修改,不会创建新的列表
- Python中的列表和元组都支持负数索引,-1表示最后一个元素,-2表示倒数第二个元素,以此类推。
l = [1, 2, 3, 4]
l[-1]
4
tup = (1, 2, 3, 4)
tup[-1]
4
- 列表和元组都支持切片操作
list = [1, 2, 3, 4]
l[1:3] # 返回列表中索引从1到2的子列表
[2, 3]
tup = (1, 2, 3, 4)
tup[1:3] # 返回元组中索引从1到2的子元组
(2, 3)
- 列表和元组都可以随意嵌套
l = [[1, 2, 3], [4, 5]] # 列表的每一个元素也是一个列表
tup = ((1, 2, 3), (4, 5, 6)) # 元组的每一个元素也是一元组
- 两者也可以通过list()和tuple()函数相互转换
list((1, 2, 3))
[1, 2, 3]
tuple([1, 2, 3])
(1, 2, 3)
-
count(item)表示统计列表/元组中item出现的次数。
-
index(item)表示返回列表/元组中item第一次出现的索引。
-
list.reverse()和list.sort()分别表示原地倒转列表和排序(注意,元组没有内置的这两个函数)。
-
reversed()和sorted()同样表示对列表/元组进行倒转和排序,但是会返回一个倒转后或者排好序的新的列表/元组。
l = [1, 2, 3]
l.__sizeof__()
64
tup = (1, 2, 3)
tup.__sizeof__()
48
- 列表和元组,我们放置了相同的元素,但是元组的存储空间,却比列表要少16字节。这是为什么呢?
- 因为列表是动态的,所以需要存储指针来指向对应的元素
- 因为列表可变,所以需要额外存储已经分配的长度大小,这样可以实时追踪列表空间的使用情况,当空间不足时,及时分配额外空间。
- 元组中只包含一个元素时,需要在元素后面添加逗号来消除歧义
tup1 = (50,);
- 元组的讲解:https://www.cnblogs.com/still-smile/p/11586452.html
列表和元组的性能
- 为了减小每次增加/删减操作时空间分配的开销,Python每次分配空间时都会额外多分配一些,这样的机制(over-allocating)保证了其操作的高效性:增加/删除的时间复杂度均为O(1)。
- 但是对于元组,情况就不同了。元组长度大小固定,元素不可变,所以存储空间固定。
python -m timeit 'x=[1,2,3,4,5,6]'
python -m timeit 'x=(1,2,3,4,5,6)'
初始化相同的列表和元组所需要的时间,元组快5倍
python3 -m timeit -s 'x=[1,2,3,4,5,6]' 'y=x[3]'
python3 -m timeit -s 'x=(1,2,3,4,5,6)' 'y=x[3]'
索引操作速度几乎一样
- 结论:元组要比列表更加轻量级一些,所以总体上来说,元组的性能速度要略优于列表。
Python会在后台,对静态数据做一些资源缓存(resource caching)。通常来说,因为垃圾回收机制的存在,如果一些变量不被使用了,Python就会回收它们所占用的内存,返还给操作系统,以便其他变量或其他应用使用。
但是对于一些静态变量,比如元组,如果它不被使用并且占用空间不大时,Python会暂时缓存这部分内存。这样,下次我们再创建同样大小的元组时,Python就可以不用再向操作系统发出请求,去寻找内存,而是可以直接分配之前缓存的内存空间,这样就能大大加快程序的运行速度。
列表和元组的使用场景
- 1.如果存储的数据是不变的,如有一个函数需要返回的是这个地点的经纬度,然后直接传给前端渲染,选用元组更合适
def get_location():
.....
return (longitude, latitude)
- 2.如果存储的数据是可变的,例如某社交平台的日志功能,统计一个用户在一周内看的帖子,用列表更合适
viewer_owner_id_list = [] # 里面的每个元素记录了这个viewer一周内看过的所有owner的id
records = queryDB(viewer_id) # 索引数据库,拿到某个viewer一周内的日志
for record in records:
viewer_owner_id_list.append(record.id)
总结
- 列表是动态的, 可以随意增加删减,改变元素。列表的存储空间略大于元组,性能略逊于元组。
- 元组是静态的,大小长度固定,不可以对元素进行增加,删减,改变操作。元组更加轻量级,性能稍优。
思考题
-
- 想创建一个空的列表,我们可以用下面的A、B两种方式,请问它们在效率上有什么区别吗?我们应该优先考虑使用哪种呢?可以说说你的理由。
# 创建空列表
# option A
empty_list = list()
# option B
empty_list = []
list()是一个function call,会创建stack,并且进行一些列参数检查的操作,[]是内置的C函数可以直接被调用,效率更高。
- list和tuple的内部实现都是array的形式。源码:
- list:https://github.com/python/cpython/blob/master/Objects/listobject.c.
- tuple:https://github.com/python/cpython/blob/master/Objects/tupleobject.c
1.元素不需要改变时:两三个元素,使用 tuple,元素多一点使用namedtuple。
2.元素需要改变时:需要高效随机读取,使用list。
3.需要关键字高效查找,采用 dict。
4.去重,使用 set。大型数据节省空间,使用标准库 array。
5.大型数据高效操作,使用 numpy.array。
- 如果一个列表在元组中的话,其实这个元组是”可变”的,只是这个可变只是能改变该列表里的内容。
import timeit
print(timeit.timeit('list(x for x in range(1,1000))',number=10000))
print(timeit.timeit('[x for x in range(1,1000)]',number=10000))
- 元祖是直接存储的元素,但是列表存储的是指向元素的指针
>>> l=list()
>>> l.__sizeof__()
20
>>> l.append(1)
>>> l.__sizeof__()
36
import timeit
timeit.timeit('a=list()',number=10000) 返回 0.0006914390251040459
timeit.timeit('a=[]',number=10000) 返回 0.00018375739455223083
timeit.timeit('a=()',number=10000) 返回 0.00010870955884456635
- list的内部实现是over-allocate array的形式
- 那在需要扩容的时候,是不是也是需要重新开辟一块连续的内存空间呢?
- 每次扩容都会预留一些空间,这里面有没有公式,公式是什么呢
- 作者回复
- 是的
- 你可以自己overallocate的pattern一般是0, 4, 8, 16, 25, 35, 46, 58, 72, 88, …
字典和集合
- 相比于列表和元组,字典的性能更优,特别是对于查找、添加和删除操作,字典都能在常数时间复杂度内完成。
- 而集合和字典基本相同,唯一的区别,就是集合没有键和值的配对,是一系列无序的、唯一的元素组合。
- 列表和元组是有序的,集合和字典是无序的
- 字典和集合是可变的
- 再来看元素访问的问题。字典访问可以直接索引键,如果不存在,就会抛出异常:
d = {'name': 'jason', 'age': 20}
d['name']
'jason'
d['location']
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 'location'
- 也可以使用get(key, default)函数来进行索引。如果键不存在,调用get()函数可以返回一个默认值。比如下面这个示例,返回了’null’。
d = {'name': 'jason', 'age': 20}
d.get('name')
'jason'
d.get('location', 'null')
'null'
一些思考:在访问字典的业务中可以不用try语句来查询不存咋的key 可以通过get就可以做到
d = {"foo":"bar", "ping": "pong"}
d.get("cui", 0)
- 集合并不支持索引操作,因为集合本质上是一个哈希表,和列表不一样。所以不能通过索引的方式来访问元素
- 想要判断一个元素在不在字典或集合内,我们可以用value in dict/set 来判断。
demo = {1,2,3}
"1" in demo
False
d = {"foo":"bar", "ping": "pong"}
"foo" in d
True
- 字典和集合是可变的,不仅可以访问,也可以增加,修改,删除
s = {1, 2, 3}
s.add(4) # 增加元素4到集合
s
{1, 2, 3, 4}
s.remove(4) # 从集合中删除元素4
s
{1, 2, 3}
注意:集合的pop()是删除集合的最后一个元素,但是集合本身是无序的,所以要谨慎使用
- 对于字典,我们通常会根据键或值,进行升序或降序排序:
d = {'b': 1, 'a': 2, 'c': 10}
d_sorted_by_key = sorted(d.items(), key=lambda x: x[0]) # 根据字典键的升序排序
d_sorted_by_value = sorted(d.items(), key=lambda x: x[1]) # 根据字典值的升序排序
d_sorted_by_key
[('a', 2), ('b', 1), ('c', 10)]
d_sorted_by_value
[('b', 1), ('a', 2), ('c', 10)]
-
当然,因为字典本身是无序的,所以这里返回了一个列表。列表中的每个元素,是由原字典的键和值组成的元组。
-
而对于集合,其排序和前面讲过的列表、元组很类似,直接调用sorted(set)即可,结果会返回一个排好序的列表。
s = {3, 4, 2, 1}
sorted(s) # 对集合的元素进行升序排序
[1, 2, 3, 4]
思考:返回值都是一个列表,因为集合和字典都是无序的。
字典和集合的性能
-
字典和集合是性能高度优化的数据结构,特别是对于查找删除操作。
-
字典的内部是一张哈希表
-
如果用列表就需要遍历,时间复杂度为O(n)。如果先排序再二分查找时间复杂度也很高。快排堆排序n logn 二分查找logn
-
集合是高度优化的哈希表,里面元素不能重复,并且其添加和查找操作只需O(1)的复杂度,那么,总的时间复杂度就只有O(1)。
-
- 例如某业务场景,电商企业的后台,存储了每件产品的ID、名称和价格,需求给定某件商品的ID找出价格。
import time
id = [x for x in range(0, 100000)]
price = [x for x in range(200000, 300000)]
products = list(zip(id, price))
# 计算列表版本的时间
start_using_list = time.perf_counter()
find_unique_price_using_list(products)
end_using_list = time.perf_counter()
print("time elapse using list: {}".format(end_using_list - start_using_list))
## 输出
time elapse using list: 41.61519479751587
# 计算集合版本的时间
start_using_set = time.perf_counter()
find_unique_price_using_set(products)
end_using_set = time.perf_counter()
print("time elapse using set: {}".format(end_using_set - start_using_set))
# 输出
time elapse using set: 0.008238077163696289
可能windows平台对一些东西做了限制和优化,实测结果不一样
字典和集合的工作原理
-
对于字典而言,这张表存储了哈希值(hash)、键和值这3个元素。
-
而对集合来说,区别就是哈希表内没有键和值的配对,只有单一的元素了。
-
在老版本的python中,哈希表的结构如下
--+-------------------------------+
| 哈希值(hash) 键(key) 值(value)
--+-------------------------------+
0 | hash0 key0 value0
--+-------------------------------+
1 | hash1 key1 value1
--+-------------------------------+
2 | hash2 key2 value2
--+-------------------------------+
. | ...
__+_______________________________+
- 不难想象,随着哈希表的扩张,它会变得越来越稀疏。举个例子,比如我有这样一个字典:
{'name': 'mike', 'dob': '1999-01-01', 'gender': 'male'}
- 那么它会存储为类似下面的形式:
entries = [
['--', '--', '--']
[-230273521, 'dob', '1999-01-01'],
['--', '--', '--'],
['--', '--', '--'],
[1231236123, 'name', 'mike'],
['--', '--', '--'],
[9371539127, 'gender', 'male']
]
- 新的设计:这样的设计结构显然非常浪费存储空间。为了提高存储空间的利用率,现在的哈希表除了字典本身的结构,会把索引和哈希值、键、值单独分开,也就是下面这样新的结构
Indices
----------------------------------------------------
None | index | None | None | index | None | index ...
----------------------------------------------------
Entries
--------------------
hash0 key0 value0
---------------------
hash1 key1 value1
---------------------
hash2 key2 value2
---------------------
...
---------------------
- 那么,刚刚的这个例子,在新的哈希表结构下的存储形式,就会变成下面这样:
indices = [None, 1, None, None, 0, None, 2]
entries = [
[1231236123, 'name', 'mike'],
[-230273521, 'dob', '1999-01-01'],
[9371539127, 'gender', 'male']
]
- 我们可以很清晰地看到,空间利用率得到很大的提高。
插入操作
- 每向字典或集合插入一个元素的时候,python先计算键的哈希值,再计算应该插入的位置。如果哈希表中此位置为空,元素就会被插入其中。
- 如果此位置被占用,python会比较两个元素的哈希值和键是否相等
- 如果两者都相等,则表明这个元素已经存在,如果值不同,则更新值。
- 若其中有一个不相等,这种情况称作哈希冲突,代表两个元素的键不相等,但是哈希值相等。这种情况,python会继续寻找表中空余的位置,直到找到为止。
- 值得一提的是,通常来说,遇到这种情况,最简单的方式是线性寻找,即从这个位置开始,挨个往后寻找空位。当然,Python内部对此进行了优化(这一点无需深入了解,你有兴趣可以查看源码,我就不再赘述),让这个步骤更加高效。
查找操作
- python会根据哈希值找到应该处于的位置,然后比较这个位置中元素的哈希值和键,相等就返回,不相等就抛出异常
删除操作
- 对于删除操作,Python会暂时对这个位置的元素,赋于一个特殊的值,等到重新调整哈希表的大小时,再将其删除。
- 不难理解,哈希冲突的发生,往往会降低字典和集合操作的速度。因此,为了保证其高效性,字典和集合内的哈希表,通常会保证其至少留有1/3的剩余空间。随着元素的不停插入,当剩余空间小于1/3时,Python会重新获取更大的内存空间,扩充哈希表。不过,这种情况下,表内所有的元素位置都会被重新排放。
- 虽然哈希冲突和哈希表大小的调整,都会导致速度减缓,但是这种情况发生的次数极少。所以,平均情况下,这仍能保证插入、查找和删除的时间复杂度为O(1)。
总结
- 字典和集合都是无序的数据结构,其内部的哈希表存储结构,保证了其查找、插入、删除操作的高效性。所以,字典和集合通常运用在对元素的高效查找、去重等场景。
思考题
- 字典的键可以是一个列表吗?下面这段代码中,字典的初始化是否正确呢?如果不正确,可以说出你的原因吗?
d = {'name': 'jason', ['education']: ['Tsinghua University', 'Stanford University']}
答:字典的键值,需要不可变,而列表是动态的,可变的。可以改为元组
深入浅出字符串
- 在python中单引号,双引号,三引号都是一样的
s1 = 'hello'
s2 = "hello"
s3 = """hello"""
s1 == s2 == s3
True
-
Python同时支持这三种表达方式,很重要的一个原因就是,这样方便你在字符串中,内嵌带引号的字符串。
-
python中的转义字符
s = 'a\nb\tc'
print(s)
a
b c
- 注意:虽然最后打印的输出横跨了两行,但是整个字符串s仍然只有5个元素。
在转义字符的应用中,最常见的就是换行符’\n’的使用。比如文件读取,如果我们一行行地读取,那么每一行字符串的末尾,都会包含换行符’\n’。而最后做数据处理时,我们往往会丢掉每一行的换行符。
字符串的常用操作
- 可以把字符串想象成一个由单个字符组成的数组,所以,Python的字符串同样支持索引,切片和遍历等等操作。
- [index:index+2]则表示第index个元素到index+1个元素组成的子字符串。
- 注意:Python的字符串是不可变的(immutable)。
- Python中字符串的改变,通常只能通过创建新的字符串来完成。比如上述例子中,想把’hello’的第一个字符’h’,改为大写的’H’,我们可以采用下面的做法:
s = 'H' + s[1:]
s = s.replace('h', 'H')
1.第一种方法,是直接用大写的’H’,通过加号’+'操作符,与原字符串切片操作的子字符串拼接而成新的字符串。
2.第二种方法,是直接扫描原字符串,把小写的’h’替换成大写的’H’,得到新的字符串。
- 在java中有可变的字符串类型,StringBuilder,每次的增删改查,无需创建新的字符串,时间复杂的为O(1)
- 但可惜的是,Python中并没有相关的数据类型,我们还是得老老实实创建新的字符串。因此,每次想要改变字符串,往往需要O(n)的时间复杂度,其中,n为新字符串的长度。
- 但是,随着版本的更新,Python也越来越聪明,性能优化得越来越好了。
- 使用加法操作符’+='的字符串拼接方法。因为它是一个例外,打破了字符串不可变的特性。
str1 += str2 # 表示str1 = str1 + str2
- 看一个例子
s = ''
for n in range(0, 100000):
s += str(n)
- 自从Python2.5开始,每次处理字符串的拼接操作时(str1 += str2),Python首先会检测str1还有没有其他的引用。如果没有的话,就会尝试原地扩充字符串buffer的大小,而不是重新分配一块内存来创建新的字符串并拷贝。这样的话,上述例子中的时间复杂度就仅为O(n)了。
- 另外,对于字符串拼接问题,除了使用加法操作符,我们还可以使用字符串内置的join函数。string.join(iterable),表示把每个元素都按照指定的格式连接起来。
l = []
for n in range(0, 100000):
l.append(str(n))
l = ' '.join(l)
- 由于列表的append操作是O(1)复杂度,字符串同理。因此,这个含有for循环例子的时间复杂度为n*O(1)=O(n)。
- 接下来,我们看一下字符串的分割函数split()。string.split(separator),表示把字符串按照separator分割成子字符串,并返回一个分割后子字符串组合的列表。它常常应用于对数据的解析处理,比如我们读取了某个文件的路径,想要调用数据库的API,去读取对应的数据,我们通常会写成下面这样:
def query_data(namespace, table):
"""
given namespace and table, query database to get corresponding
data
"""
path = 'hive://ads/training_table'
namespace = path.split('//')[1].split('/')[0] # 返回'ads'
table = path.split('//')[1].split('/')[1] # 返回 'training_table'
data = query_data(namespace, table)
- 此外还有其他的函数
1.string.strip(str),表示去掉首尾的str字符串;
2.string.lstrip(str),表示只去掉开头的str字符串;
3.string.rstrip(str),表示只去掉尾部的str字符串。
- Python中字符串还有很多常用操作,比如,string.find(sub, start, end),表示从start到end查找字符串中子字符串sub的位置等等。
总结
-
Python中字符串使用单引号、双引号或三引号表示,三者意义相同,并没有什么区别。其中,三引号的字符串通常用在多行字符串的场景。
-
Python中字符串是不可变的(前面所讲的新版本Python中拼接操作’+='是个例外)。因此,随意改变字符串中字符的值,是不被允许的。
-
Python新版本(2.5+)中,字符串的拼接变得比以前高效了许多,你可以放心使用。
-
Python中字符串的格式化(string.format)常常用在输出、日志的记录等场景。
思考题
- 字符串的拼接用 += 还是join更优 ?
- 答:数据量大时用join更优
python"黑箱":输入与输出
- input() 函数暂停程序运行,同时等待键盘输入;直到回车被按下,函数的参数即为提示语,输入的类型永远是字符串型(str)。
- 把 str 强制转换为 int 请用 int(),转为浮点数请用 float()。而在生产环境中使用强制转换时,请记得加上 try except
- python对int类型没有最大限制,但是对float类型依然有精度限制
- 除了在一些算法竞赛中要注意,在生产环境中也要时刻提防,避免因为对边界条件判断不清而造成 bug 甚至 0day(危重安全漏洞)。
我们回望一下币圈。2018年4月23日中午11点30分左右,BEC 代币智能合约被黑客攻击。黑客利用数据溢出的漏洞,攻击与美图合作的公司美链 BEC 的智能合约,成功地向两个地址转出了天量级别的 BEC 代币,导致市场上的海量 BEC 被抛售,该数字货币的价值也几近归零,给 BEC 市场交易带来了毁灭性的打击。
由此可见,虽然输入输出和类型处理事情简单,但我们一定要慎之又慎。毕竟相当比例的安全漏洞,都来自随意的 I/O 处理。
文件的输入输出
-
命令行的输入输出,只是 Python 交互的最基本方式,适用一些简单小程序的交互。而生产级别的 Python 代码,大部分 I/O 则来自于文件、网络、其他进程的消息等等。
-
NLP的例子
import re
# 你不用太关心这个函数
def parse(text):
# 使用正则表达式去除标点符号和换行符
text = re.sub(r'[^\w ]', '', text)
# 转为小写
text = text.lower()
# 生成所有单词的列表
word_list = text.split(' ')
# 去除空白单词
word_list = filter(None, word_list)
# 生成单词和词频的字典
word_cnt = {}
for word in word_list:
if word not in word_cnt:
word_cnt[word] = 0
word_cnt[word] += 1
# 按照词频排序
sorted_word_cnt = sorted(word_cnt.items(), key=lambda kv: kv[1], reverse=True)
return sorted_word_cnt
with open('in.txt', 'r') as fin:
text = fin.read()
word_and_freq = parse(text)
with open('out.txt', 'w') as fout:
for word, freq in word_and_freq:
fout.write('{} {}\n'.format(word, freq))
- 所有 I/O 都应该进行错误处理。因为 I/O 操作可能会有各种各样的情况出现,而一个健壮(robust)的程序,需要能应对各种情况的发生,而不应该崩溃(故意设计的情况除外)。
- 请记得加上错误处理。不然,哪怕只是给 json.loads() 发送了一个非法字符串,而你没有 catch 到,程序就会崩溃了。
- 当开发一个第三方应用程序时,你可以通过 JSON 将用户的个人配置输出到文件,方便下次程序启动时自动读取。这也是现在普遍运用的成熟做法。
- 那么 JSON 是唯一的选择吗?显然不是,它只是轻量级应用中最方便的选择之一。据我所知,在 Google,有类似的工具叫做Protocol Buffer,当然,Google 已经完全开源了这个工具,你可以自己了解一下使用方法。
- 相比于 JSON,它的优点是生成优化后的二进制文件,因此性能更好。但与此同时,生成的二进制序列,是不能直接阅读的。它在 TensorFlow 等很多对性能有要求的系统中都有广泛的应用。
def get_y(a,b):
return lambda x:ax+b
y1 = get_y(1,1)
y1(1) # 结果为2
- https://www.cnblogs.com/hf8051/p/8085424.html:lambda表达式
- https://www.runoob.com/python/python-func-filter.html:filter()函数的用法
- https://www.runoob.com/python/att-dictionary-items.html:字典的内置函数,将字典的key
- sorted(students, key=lambda s: s[2], reverse=True)value都返回一下
总结
- I/O操作需要谨慎,一定要进行充分的错误处理
- 编码时,对内存占用和磁盘占用要有充分估计
思考题
- 实现上面的NLP例子,但是in.txt 非常大(不能一次读取到内存中),但输出不是很大
import re
# 你不用太关心这个函数
def parse(text):
# 使用正则表达式去除标点符号和换行符
text = re.sub(r'[^\w ]', '', text)
# 转为小写
text = text.lower()
# 生成所有单词的列表
word_list = text.split(' ')
# 去除空白单词
word_list = filter(None, word_list)
# 生成单词和词频的字典
word_cnt = {}
for word in word_list:
if word not in word_cnt:
word_cnt[word] = 0
word_cnt[word] += 1
# 按照词频排序
sorted_word_cnt = sorted(word_cnt.items(), key=lambda kv: kv[1], reverse=True)
return sorted_word_cnt
with open(r'in.txt', 'r') as fb:
text = fb.readlines()
for i in text:
res = parse(i)
with open('out.txt', 'w') as fout:
for word, freq in res:
fout.write('{} {}\n'.format(word, freq))
条件与循环
- 条件与循环是编程的基本功
- for item in
- 这里需要单独强调一下字典。字典本身只有键是可迭代的,如果我们要遍历它的值或者是键值对,就需要通过其内置的函数values()或者items()实现。其中,values()返回字典的值的集合,items()返回键值对的集合。
- 当我们同时需要索引和元素时,还有一种更简洁的方式,那就是通过Python内置的函数enumerate()。用它来遍历集合,不仅返回每个元素,并且还返回其对应的索引
- 所谓continue,就是让程序跳过当前这层循环,继续执行下面的循环;而break则是指完全跳出所在的整个循环体。
- while是给一个不知道的数
- range()函数是直接由C语言写的,调用它速度非常快。
class dict(**kwarg)
class dict(mapping, **kwarg)
class dict(iterable, **kwarg)
**kwargs – 关键字
mapping – 元素的容器。
iterable – 可迭代对象。
a = dict(mappint=3)
print(a)
a = [1,2,3]
b = ["a", "b", "c"]
x = zip(a, b)
print(dict(x))
res = [dict(zip(attributes,v)) for v in values]
异常处理
- https://docs.python.org/3/library/exceptions.html#bltin-exceptions:ERROR
- 在最后写Exception
- 还有一个很常见的用法是finally,经常和try、except放在一起来用。无论发生什么情况,finally block中的语句都会被执行,哪怕前面的try和excep block中使用了return语句。
- python可以自定义异常类型
class MyInputError(Exception):
"""Exception raised when there're errors in input"""
def __init__(self, value): # 自定义异常类型的初始化
self.value = value
def __str__(self): # 自定义异常类型的string表达形式
return ("{} is invalid input".format(repr(self.value)))
try:
raise MyInputError(1) # 抛出MyInputError这个异常
except MyInputError as err:
print('error: {}'.format(err))
- 在json.loads()函数中,输入的字符串如果不符合其规范,那么便无法解码,就会抛出异常,因此加上异常处理十分必要。
- 因此,对于flow-control(流程控制)的代码逻辑,我们一般不用异常处理。
try:
db = DB.connect('<db path>') # 可能会抛出异常
raw_data = DB.queryData('<viewer_id>') # 可能会抛出异常
except (DBConnectionError, DBQueryDataError) err:
print('Error: {}'.format(err))
#自定义函数
-
和其他需要编译的语言(比如C语言)不一样的是,def是可执行语句,这意味着函数直到被调用前,都是不存在的。当程序调用函数时,def语句才会创建一个新的函数对象,并赋予其名字。
-
输入一串数字,找出最大的数
def find_largest_el(l):
if not isinstance(l, list):
return "input is not a list"
if len(l) == 0:
return "empty input"
max = l[0]
for i in l:
if i > max:
max = i
return max
res = find_largest_el([3,5,1,5,6,7,43,12,64,23])
print(res)
- 在调用函数func()时,如果参数param没有传入,则参数默认为0;而如果传入了参数param,其就会覆盖默认值。
- 函数的嵌套:
- 第一,函数的嵌套能够保证内部函数的隐私。内部函数只能被外部函数所调用和访问,不会暴露在全局作用域,因此,如果你的函数内部有一些隐私数据(比如数据库的用户、密码等),不想暴露在外,那你就可以使用函数的的嵌套,将其封装在内部函数中,只通过外部函数来访问。
- 第二,合理的使用函数嵌套,能够提高程序的运行效率。只做一次输入检查,可以用面向对象也可以用闭包
- 不能在函数内部随意改变全局变量的值。
- 如果我们一定要在函数内部改变全局变量的值,就必须加上global这个声明:
MIN_VALUE = 1
MAX_VALUE = 10
def validation_check(value):
global MIN_VALUE
...
MIN_VALUE += 1
...
validation_check(5)
- 这里的global关键字,并不表示重新创建了一个全局变量MIN_VALUE,而是告诉Python解释器,函数内部的变量MIN_VALUE,就是之前定义的全局变量,并不是新的全局变量,也不是局部变量。这样,程序就可以在函数内部访问全局变量,并修改它的值了。
- 如果遇到函数内部局部变量和全局变量同名的情况,那么在函数内部,局部变量会覆盖全局变量,比如下面这种:
MIN_VALUE = 1
MAX_VALUE = 10
def validation_check(value):
MIN_VALUE = 3
...
- 对于嵌套函数来说,内部函数可以访问外部函数定义的变量,但是无法修改,若要修改,必须加上nonlocal这个关键字:
def outer():
x = "local"
def inner():
nonlocal x # nonlocal关键字表示这里的x就是外部函数outer定义的变量x
x = 'nonlocal'
print("inner:", x)
inner()
print("outer:", x)
outer()
# 输出
inner: nonlocal
outer: nonlocal
- 如果不加上nonlocal这个关键字,而内部函数的变量又和外部函数变量同名,那么同样的,内部函数变量会覆盖外部函数的变量。
def outer():
x = "local"
def inner():
x = 'nonlocal' # 这里的x是inner这个函数的局部变量
print("inner:", x)
inner()
print("outer:", x)
outer()
# 输出
inner: nonlocal
outer: local
加上nonlocal会修改,不加不会修改,根本就是外部函数的值变不变
总结
-
Python中函数的参数可以接受任意的数据类型,使用起来需要注意,必要时请在函数开头加入数据类型的检查;
-
和其他语言不同,Python中函数的参数可以设定默认值;
-
嵌套函数的使用,能保证数据的隐私性,提高程序运行效率;
-
合理地使用闭包,则可以简化程序的复杂度,提高可读性。
-
在Python里是不推荐使用递归的,是因为Python没有对递归做优化,那使用 yield from 来代替递归会不会好些呢?
- 快排:
def quickSort(l, start, end):
if start > end:
return None
left = start
right = end
mid = l[left]
while left < right:
while left < right and l[right] >= mid:
right -= 1
l[left] = l[right]
while left < right and l[left] < mid:
left += 1
l[right] = l[left]
l[right] = mid
quickSort(l, start, left-1)
quickSort(l, left+1, end)
return l
l = [4,2,6,34,32,65,14,453,1,4,6,12,46,7,3,1,6,9,567,23]
res = quickSort(l, 0, len(l)- 1)
print(res)
- 堆排序:
pass
- 冒泡排序
def bubbleSort(arr):
for i in range(1,len(arr)):
for j in range(0,len(arr) - i):
if arr[j] > arr[j+1]:
arr[j], arr[j+1] = arr[j+1], arr[j]
return arr
res = bubbleSort([3,4,6,2,6,34,2,42,36,2,6,8,4,345,7,5756767,5,243,2])
print(res)
lambda表达式
- 简洁的语法
command=lambda: print('being pressed')) # 点击时调用lambda函数
- map()是最快的。因为map()函数直接由C语言写的,运行时不需要通过Python解释器间接调用,并且内部做了诸多优化,所以运行速度最快。
l = [1, 2, 3, 4, 5]
new_list = map(lambda x: x * 2, l) # [2, 4, 6, 8, 10]
python3 -mtimeit -s'xs=range(1000000)' 'map(lambda x: x*2, xs)'
2000000 loops, best of 5: 171 nsec per loop
python3 -mtimeit -s'xs=range(1000000)' '[x * 2 for x in xs]'
5 loops, best of 5: 62.9 msec per loop
python3 -mtimeit -s'xs=range(1000000)' 'l = []' 'for i in xs: l.append(i * 2)'
5 loops, best of 5: 92.7 msec per loop
- 如果想对列表的元素进行简单的操作可以考虑用map,reduce,filter等配合lambda表达式进行使用
- 不过,如果你要对集合中的元素,做一些比较复杂的操作,那么,考虑到代码的可读性,我们通常会使用for循环,这样更加清晰明了。
思考
- 一行对字典进行排序
d = {'mike': 10, 'lucy': 2, 'ben': 30}
x = sorted(d.items(), key=lambda res:res[1],reverse=False)
print(x)
OOP
- init 表示构造函数,意即一个对象生成时会被自动调用的函数。
- 如果一个属性以 __ (注意,此处有两个_) 开头,我们就默认这个属性是私有属性。私有属性,是指不希望在类的函数之外的地方被访问和修改的属性。
- 一种很常规的做法,是用全大写来表示常量,因此我们可以在类中使用 self.WELCOME_STR ,或者在类外使用 Entity.WELCOME_STR ,来表达这个字符串。
- 类函数、成员函数和静态函数三个概念
- 前两者产生的影响是动态的,能够访问或者修改对象的属性;而静态函数则与类没有什么关联,最明显的特征便是,静态函数的第一个参数没有任何特殊性。
class Document():
WELCOME_STR = 'Welcome! The context for this book is {}.'
def __init__(self, title, author, context):
print('init function called')
self.title = title
self.author = author
self.__context = context
# 类函数
@classmethod
def create_empty_book(cls, title, author):
return cls(title=title, author=author, context='nothing')
# 成员函数
def get_context_length(self):
return len(self.__context)
# 静态函数
@staticmethod
def get_welcome(context):
return Document.WELCOME_STR.format(context)
- 一般而言,静态函数可以用来做一些简单独立的任务,既方便测试,也能优化代码结构。静态函数还可以通过在函数前一行加上 @staticmethod 来表示
- 类函数的第一个参数一般为 cls,表示必须传一个类进来。类函数最常用的功能是实现不同的 init 构造函数
- 成员函数则是我们最正常的类的函数,它不需要任何装饰器声明,第一个参数 self 代表当前对象的引用,可以通过此函数,来实现想要的查询/修改类的属性等功能。
from abc import ABCMeta, abstractmethod
class Entity(metaclass=ABCMeta):
@abstractmethod
def get_title(self):
pass
@abstractmethod
def set_title(self, title):
pass
class Document(Entity):
def get_title(self):
return self.title
def set_title(self, title):
self.title = title
document = Document()
document.set_title('Harry Potter')
print(document.get_title())
entity = Entity()
- 你应该发现了,Entity 本身是没有什么用的,只需拿来定义 Document 和 Video 的一些基本元素就够了。不过,万一你不小心生成 Entity 的对象该怎么办呢?为了防止这样的手误,必须要介绍一下抽象类。
- 抽象类是一种特殊的类,它生下来就是作为父类存在的,一旦对象化就会报错。同样,抽象函数定义在抽象类之中,子类必须重载该函数才能使用。相应的抽象函数,则是使用装饰器 @abstractmethod 来表示。
- 我们可以看到,代码中entity = Entity()直接报错,只有通过 Document 继承 Entity 才能正常使用。
Facebook 中,在 idea 提出之后,开发组和产品组首先会召开产品设计会,PM(Product Manager,产品经理) 写出产品需求文档,然后迭代;TL(Team Leader,项目经理)编写开发文档,开发文档中会定义不同模块的大致功能和接口、每个模块之间如何协作、单元测试和集成测试、线上灰度测试、监测和日志等等一系列开发流程。
面向对象编程是软件工程中重要的思想。正如动态规划是算法中的重要思想一样,它不是某一种非常具体的技术,而是一种综合能力的体现,是将大型工程解耦化、模块化的重要方法。在实践中要多想,尤其是抽象地想,才能更快掌握这个技巧。
- 菱形继承问题
- 面向对象编程的四要素:抽象,封装,继承,多态
Python实现搜索引擎
- 一个搜索引擎由搜索器、索引器、检索器和用户接口四个部分组成。
- 询问(query)
模块化
- 在大型工程中尽可能使用绝对位置是第一要义。对于一个独立的项目,所有的模块的追寻方式,最好从项目的根目录开始追溯,这叫做相对的绝对路径。
- 后来,随着工作的深入,我才发现了这种代码仓库独有的几个优点。
- 第一个优点,简化依赖管理。整个公司的代码模块,都可以被你写的任何程序所调用,而你写的库和模块也会被其他人调用。调用的方式,都是从代码的根目录开始索引,也就是前面提到过的相对的绝对路径。这样极大地提高了代码的分享共用能力,你不需要重复造轮子,只需要在写之前,去搜一下有没有已经实现好的包或者框架就可以了。
- 第二个优点,版本统一。不存在使用了一个新模块,却导致一系列函数崩溃的情况;并且所有的升级都需要通过单元测试才可以继续。
- 第三个优点,代码追溯。你可以很容易追溯,一个 API 是从哪里被调用的,它的历史版本是怎样迭代开发,产生变化的。
深拷贝
- 等于()和is是Python中对象比较常用的两种方式。简单来说,’'操作符比较对象之间的值是否相等,比如下面的例子,表示比较变量a和b所指向的值是否相等。
- 而’is’操作符比较的是对象的身份标识是否相等,即它们是否是同一个对象,是否指向同一个内存地址。
- 对于整型数字来说,以上a is b为True的结论,只适用于-5到256范围内的数字。比如下面这个例子:
a = 257
b = 257
a == b
True
id(a)
4473417552
id(b)
4473417584
a is b
False
- 事实上,出于对性能优化的考虑,Python内部会对-5到256的整型维持一个数组,起到一个缓存的作用。这样,每次你试图创建一个-5到256范围内的整型数字时,Python都会从这个数组中返回相对应的引用,而不是重新开辟一块新的内存空间。
- 但是,如果整型数字超过了这个范围,比如上述例子中的257,Python则会为两个257开辟两块内存区域,因此a和b的ID不一样,a is b就会返回False了。
- 在实际工作中,当我们比较变量时,使用’=='的次数会比’is’多得多,因为我们一般更关心两个变量的值,而不是它们内部的存储地址。
- 当我们比较一个变量与一个单例(singleton)时,通常会使用’is’。一个典型的例子,就是检查一个变量是否为None:
- 比较操作符’is’的速度效率,通常要优于’==’。因为’is’操作符不能被重载,这样,Python就不需要去寻找,程序中是否有其他地方重载了比较操作符,并去调用。执行比较操作符’is’,就仅仅是比较两个变量的ID而已。
- 但是’=='操作符却不同,执行a == b相当于是去执行a.eq(b),而Python大部分的数据类型都会去重载__eq__这个函数,其内部的处理通常会复杂一些。比如,对于列表,__eq__函数会去遍历列表中的元素,比较它们的顺序和值是否相等。
t1 = (1, 2, [3, 4])
t2 = (1, 2, [3, 4])
t1 == t2
True
t1[-1].append(5)
t1 == t2
False
- 我们知道元组是不可变的,但元组可以嵌套,它里面的元素可以是列表类型,列表是可变的,所以如果我们修改了元组中的某个可变元素,那么元组本身也就改变了,之前用’is’或者’=='操作符取得的结果,可能就不适用了。
- 常见的浅拷贝的方法,是使用数据类型本身的构造器,比如下面两个例子:
l1 = [1, 2, 3]
l2 = list(l1)
l2
[1, 2, 3]
l1 == l2
True
l1 is l2
False
s1 = set([1, 2, 3])
s2 = set(s1)
s2
{1, 2, 3}
s1 == s2
True
s1 is s2
False
- 当然,对于可变的序列,我们还可以通过切片操作符’:'完成浅拷贝,比如下面这个列表的例子:
l1 = [1, 2, 3]
l2 = l1[:]
l1 == l2
True
l1 is l2
False
- Python中也提供了相对应的函数copy.copy(),适用于任何数据类型:
import copy
l1 = [1, 2, 3]
l2 = copy.copy(l1)
- 对于元组,使用tuple()或者切片操作符’:'不会创建一份浅拷贝,相反,它会返回一个指向相同元组的引用:
t1 = (1, 2, 3)
t2 = tuple(t1)
t1 == t2
True
t1 is t2
True
- 这里,元组(1, 2, 3)只被创建一次,t1和t2同时指向这个元组。
- 浅拷贝,是指重新分配一块内存,创建一个新的对象,里面的元素是原对象中子对象的引用。
- 因此,如果原对象中的元素不可变,那倒无所谓;但如果元素可变,浅拷贝通常会带来一些副作用,尤其需要注意。我们来看下面的例子:
l1 = [[1, 2], (30, 40)]
l2 = list(l1)
l1.append(100)
l1[0].append(3)
l1
[[1, 2, 3], (30, 40), 100]
l2
[[1, 2, 3], (30, 40)]
l1[1] += (50, 60)
l1
[[1, 2, 3], (30, 40, 50, 60), 100]
l2
[[1, 2, 3], (30, 40)]
- l1.append(100),表示对l1的列表新增元素100。这个操作不会对l2产生任何影响,因为l2和l1作为整体是两个不同的对象,并不共享内存地址。操作过后l2不变,l1会发生改变:
- 再来看,l1[0].append(3),这里表示对l1中的第一个列表新增元素3。因为l2是l1的浅拷贝,l2中的第一个元素和l1中的第一个元素,共同指向同一个列表,因此l2中的第一个列表也会相对应的新增元素3。操作后l1和l2都会改变:
- 最后是l1[1] += (50, 60),因为元组是不可变的,这里表示对l1中的第二个元组拼接,然后重新创建了一个新元组作为l1中的第二个元素,而l2中没有引用新元组,因此l2并不受影响。操作后l2不变,l1发生改变:
- 通过这个例子,你可以很清楚地看到使用浅拷贝可能带来的副作用。因此,如果我们想避免这种副作用,完整地拷贝一个对象,你就得使用深度拷贝。
import copy
l1 = [[1, 2], (30, 40)]
l2 = copy.deepcopy(l1)
l1.append(100)
l1[0].append(3)
l1
[[1, 2, 3], (30, 40), 100]
l2
[[1, 2], (30, 40)]
- 所谓深度拷贝,是指重新分配一块内存,创建一个新的对象,并且将原对象中的元素,以递归的方式,通过创建新的子对象拷贝到新对象中。因此,新对象和原对象没有任何关联。
- 不过,深度拷贝也不是完美的,往往也会带来一系列问题。如果被拷贝对象中存在指向自身的引用,那么程序很容易陷入无限循环:
import copy
x = [1]
x.append(x)
x
[1, [...]]
y = copy.deepcopy(x)
y
[1, [...]]
- 上面这个例子,列表x中有指向自身的引用,因此x是一个无限嵌套的列表。但是我们发现深度拷贝x到y后,程序并没有出现stack overflow的现象。这是为什么呢?
其实,这是因为深度拷贝函数deepcopy中会维护一个字典,记录已经拷贝的对象与其ID。拷贝过程中,如果字典里已经存储了将要拷贝的对象,则会从字典直接返回,我们来看相对应的源码就能明白:
def deepcopy(x, memo=None, _nil=[]):
"""Deep copy operation on arbitrary Python objects.
See the module's __doc__ string for more info.
"""
if memo is None:
memo = {}
d = id(x) # 查询被拷贝对象x的id
y = memo.get(d, _nil) # 查询字典里是否已经存储了该对象
if y is not _nil:
return y # 如果字典里已经存储了将要拷贝的对象,则直接返回
...
总结
- 比较操作符’=='表示比较对象间的值是否相等,而’is’表示比较对象的标识是否相等,即它们是否指向同一个内存地址。
- 比较操作符’is’效率优于’’,因为’is’操作符无法被重载,执行’is’操作只是简单的获取对象的ID,并进行比较;而’'操作符则会递归地遍历对象的所有值,并逐一比较。
- 浅拷贝中的元素,是原对象中子对象的引用,因此,如果原对象中的元素是可变的,改变其也会影响拷贝后的对象,存在一定的副作用。
- 深度拷贝则会递归地拷贝原对象中的每一个子对象,因此拷贝后的对象和原对象互不相关。另外,深度拷贝中会维护一个字典,记录已经拷贝的对象及其ID,来提高效率并防止无限递归的发生。
- 浅拷贝,不可变的不可变,可变的依旧可变。深拷贝,都不可变
值传递引用传递
- 所谓值传递,通常就是拷贝参数的值,然后传递给函数里的新变量。这样,原变量和新变量之间互相独立,互不影响。(值传递就是复制一份,两者相互不影响)
- 引用传递就是传递指针,一个改变会影响另一个
- Python的数据类型,例如整型(int)、字符串(string)等等,是不可变的。
a = 1
b = a
a = a + 1
- Python里的变量可以被删除,但是对象无法被删除。
l = [1, 2, 3]
del l
- del l 删除了l这个变量,从此以后你无法访问l,但是对象[1, 2, 3]仍然存在。Python程序运行时,其自带的垃圾回收系统会跟踪每个对象的引用。如果[1, 2, 3]除了l外,还在其他地方被引用,那就不会被回收,反之则会被回收。
- 变量的赋值,只是表示让变量指向了某个对象,并不表示拷贝对象给变量;而一个对象,可以被多个变量所指向。
- 可变对象(列表,字典,集合等等)的改变,会影响所有指向该对象的变量。
- 对于不可变对象(字符串,整型,元祖等等),所有指向该对象的变量的值总是一样的,也不会改变。但是通过某些操作(+=等等)更新不可变对象的值时,会返回一个新的对象。
- 变量可以被删除,但是对象无法被删除。
- 准确地说,Python的参数传递是赋值传递 (pass by assignment),或者叫作对象的引用传递(pass by object reference)。Python里所有的数据类型都是对象,所以参数传递时,只是让新变量与原变量指向相同的对象而已,并不存在值传递或是引用传递一说。
def my_func3(l2):
l2.append(4)
l1 = [1, 2, 3]
my_func3(l1)
l1
[1, 2, 3, 4]
def my_func4(l2):
l2 = l2 + [4]
l1 = [1, 2, 3]
my_func4(l1)
l1
[1, 2, 3]
和其他语言不同的是,Python中参数的传递既不是值传递,也不是引用传递,而是赋值传递,或者是叫对象的引用传递。(不是指向内存地址,而是指向对象)
如果对象是可变的,当其改变时,所有指向这个对象的变量都会改变。
如果对象不可变,简单的赋值只能改变其中一个变量的值,其余变量则不受影响。
- 如果你想通过一个函数来改变某个变量的值,通常有两种方法。一种是直接将可变数据类型(比如列表,字典,集合)当作参数传入,直接在其上修改;第二种则是创建一个新变量,来保存修改后的值,然后将其返回给原变量。在实际工作中,我们更倾向于使用后者,因为其表达清晰明了,不易出错。
装饰器
- 在Python中,函数是一等公民(first-class citizen),函数也是对象。我们可以把函数赋予变量,比如下面这段代码:
def func(message):
print('Got a message: {}'.format(message))
send_message = func
send_message('hello world')
# 输出
Got a message: hello world
- 我们可以把函数当作参数,传入另一个函数中,比如下面这段代码:
def get_message(message):
return 'Got a message: ' + message
def root_call(func, message):
print(func(message))
root_call(get_message, 'hello world')
# 输出
Got a message: hello world
- 可以在函数里定义函数,也就是函数的嵌套。这里我同样举了一个例子:
def func(message):
def get_message(message):
print('Got a message: {}'.format(message))
return get_message(message)
func('hello world')
# 输出
Got a message: hello world
- 函数的返回值也可以是函数对象(闭包),比如下面这个例子:
def func_closure():
def get_message(message):
print('Got a message: {}'.format(message))
return get_message
send_message = func_closure()
send_message('hello world')
# 输出
Got a message: hello world
-
@my_decorator就相当于前面的greet=my_decorator(greet)语句
-
如果我另外还有一个函数,也需要使用my_decorator()装饰器,但是这个新的函数有两个参数,又该怎么办呢?比如:
@my_decorator
def celebrate(name, message):
...
- 事实上,通常情况下,我们会把*args和kwargs,作为装饰器内部函数wrapper()的参数。*args和kwargs,表示接受任意数量和类型的参数,因此装饰器就可以写成下面的形式:
def my_decorator(func):
def wrapper(*args, **kwargs):
print('wrapper of decorator')
func(*args, **kwargs)
return wrapper
类装饰器
class Count:
def __init__(self, func):
self.func = func
self.num_calls = 0
def __call__(self, *args, **kwargs):
self.num_calls += 1
print('num of calls is: {}'.format(self.num_calls))
return self.func(*args, **kwargs)
@Count
def example():
print("hello world")
example()
# 输出
num of calls is: 1
hello world
example()
# 输出
num of calls is: 2
hello world
...
- 装饰器的嵌套
@decorator1
@decorator2
@decorator3
def func():
...
- 它的执行顺序从里到外,所以上面的语句也等效于下面这行代码:
decorator1(decorator2(decorator3(func)))
- 身份认证
import functools
def authenticate(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
request = args[0]
if check_user_logged_in(request): # 如果用户处于登录状态
return func(*args, **kwargs) # 执行函数post_comment()
else:
raise Exception('Authentication failed')
return wrapper
@authenticate
def post_comment(request, ...)
...
- 日志记录
import time
import functools
def log_execution_time(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
res = func(*args, **kwargs)
end = time.perf_counter()
print('{} took {} ms'.format(func.__name__, (end - start) * 1000))
return res
return wrapper
@log_execution_time
def calculate_similarity(items):
...
- 在大型公司的机器学习框架中,我们调用机器集群进行模型训练前,往往会用装饰器对其输入(往往是很长的json文件)进行合理性检查。这样就可以大大避免,输入不正确对机器造成的巨大开销。
import functools
def validation_check(input):
@functools.wraps(func)
def wrapper(*args, **kwargs):
... # 检查输入是否合法
@validation_check
def neural_network_training(param1, param2, ...):
...
- Python内置的LRU cache为例来说明,在Python中的表示形式是@lru_cache。@lru_cache会缓存进程中的函数参数和结果,当缓存满了以后,会删除least recenly used 的数据。
- 正确使用缓存装饰器,往往能极大地提高程序运行效率。
- 大型公司服务器端的代码中往往存在很多关于设备的检查,比如你使用的设备是安卓还是iPhone,版本号是多少。这其中的一个原因,就是一些新的feature,往往只在某些特定的手机系统或版本上才有(比如Android v200+)。
- 这样一来,我们通常使用缓存装饰器,来包裹这些检查函数,避免其被反复调用,进而提高程序运行效率,比如写成下面这样:
@lru_cache
def check(param1, param2, ...) # 检查用户设备类型,版本号等等
...
- 所谓的装饰器,其实就是通过装饰器函数,来修改原函数的一些功能,使得原函数不需要修改。
- 而实际工作中,装饰器通常运用在身份认证、日志记录、输入合理性检查以及缓存等多个领域中。合理使用装饰器,往往能极大地提高程序的可读性以及运行效率。
名师分享-metaclass,是潘多拉魔盒还是阿拉丁神灯?
- YAML是一个家喻户晓的Python工具,可以方便地序列化/逆序列化结构数据。YAMLObject的一个超越变形能力,就是它的任意子类支持序列化和反序列化(serialization & deserialization)。
class Monster(yaml.YAMLObject):
yaml_tag = u'!Monster'
def __init__(self, name, hp, ac, attacks):
self.name = name
self.hp = hp
self.ac = ac
self.attacks = attacks
def __repr__(self):
return "%s(name=%r, hp=%r, ac=%r, attacks=%r)" % (
self.__class__.__name__, self.name, self.hp, self.ac,
self.attacks)
yaml.load("""
--- !Monster
name: Cave spider
hp: [2,6] # 2d6
ac: 16
attacks: [BITE, HURT]
""")
Monster(name='Cave spider', hp=[2, 6], ac=16, attacks=['BITE', 'HURT'])
print yaml.dump(Monster(
name='Cave lizard', hp=[3,6], ac=16, attacks=['BITE','HURT']))
# 输出
!Monster
ac: 16
attacks: [BITE, HURT]
hp: [3, 6]
name: Cave lizard
- 比方说,在一个智能语音助手的大型项目中,我们有1万个语音对话场景,每一个场景都是不同团队开发的。作为智能语音助手的核心团队成员,我不可能去了解每个子场景的实现细节。
- 在动态配置实验不同场景时,经常是今天我要实验场景A和B的配置,明天实验B和C的配置,光配置文件就有几万行量级,工作量不可谓不小。而应用这样的动态配置理念,我就可以让引擎根据我的文本配置文件,动态加载所需要的Python类。
metaclass
- 在Python的类型世界里,type这个类就是造物的上帝。这可以在代码中验证:
# Python 3和Python 2类似
class MyClass:
pass
instance = MyClass()
type(instance)
# 输出
<class '__main__.C'>
type(MyClass)
# 输出
<class 'type'>
- 用户自定义类,只不过是type类的__call__运算符重载。
- metaclass仅仅是给小部分Python开发者,在开发框架层面的Python库时使用的。而在应用层,metaclass往往不是很好的选择。
- 也正因为这样,据我所知,在很多硅谷一线大厂,使用Python metaclass需要特例特批。
- metaclass是Python黑魔法级别的语言特性。天堂和地狱只有一步之遥,你使用好metaclass,可以实现像YAML那样神奇的特性;而使用不好,可能就会打开潘多拉魔盒了。
深入理解迭代器和生成器
-
在python中一切皆对象,对象的抽象就是类,对象的结合就是容器
-
对于容器,你可以很直观地想象成多个元素在一起的单元;而不同容器的区别,正是在于内部数据结构的实现方法。然后,你就可以针对不同场景,选择不同时间和空间复杂度的容器。
-
所有的容器都是可迭代的(iterable)。这里的迭代,和枚举不完全一样。迭代可以想象成是你去买苹果,卖家并不告诉你他有多少库存。这样,每次你都需要告诉卖家,你要一个苹果,然后卖家采取行为:要么给你拿一个苹果;要么告诉你,苹果已经卖完了。你并不需要知道,卖家在仓库是怎么摆放苹果的。
-
严谨地说,迭代器(iterator)提供了一个 next 的方法。调用这个方法后,你要么得到这个容器的下一个对象,要么得到一个 StopIteration 的错误(苹果卖完了)。
-
你不需要像列表一样指定元素的索引,因为字典和集合这样的容器并没有索引一说。
-
比如,字典采用哈希表实现,那么你就只需要知道,next 函数可以不重复不遗漏地一个一个拿到所有元素即可。
-
而可迭代对象,通过 iter() 函数返回一个迭代器,再通过 next() 函数就可以实现遍历。for in 语句将这个过程隐式化,所以,你只需要知道它大概做了什么就行了。
-
isinstance(obj, Iterable)
-
生成器是懒人版本的迭代器。
-
在迭代器中,如果我们想要枚举它的元素,这些元素需要事先生成。
-
[i for i in range(100000000)]就可以生成一个包含一亿元素的列表。每个元素在生成后都会保存到内存中,你通过代码可以看到,它们占用了巨量的内存,内存不够的话就会出现 OOM 错误。
-
不过,我们并不需要在内存中同时保存这么多东西,比如对元素求和,我们只需要知道每个元素在相加的那一刻是多少就行了,用完就可以扔掉了。
-
于是,生成器的概念应运而生,在你调用 next() 函数的时候,才会生成下一个变量。生成器在 Python 的写法是用小括号括起来,(i for i in range(100000000)),即初始化了一个生成器。
-
这样一来,你可以清晰地看到,生成器并不会像迭代器一样占用大量内存,只有在被使用的时候才会调用。
-
而且生成器在初始化的时候,并不需要运行一次生成操作,相比于 test_iterator() ,test_generator() 函数节省了一次生成一亿个元素的过程,因此耗时明显比迭代器短。
-
接下来的yield 是魔术的关键。对于初学者来说,你可以理解为,函数运行到这一行的时候,程序会从这里暂停,然后跳出,不过跳到哪里呢?答案是 next() 函数。那么 i ** k 是干什么的呢?它其实成了 next() 函数的返回值。
-
下面这段代码你应该不陌生,也就是常规做法,枚举每个元素和它的 index,判断后加入 result,最后返回。
def index_generator(L, target):
for i, num in enumerate(L):
if num == target:
yield i
print(list(index_generator([1, 6, 2, 4, 5, 2, 8, 6, 3, 2], 2)))
########## 输出 ##########
[2, 5, 9]
- 接下来我们再来看一个问题:给定两个序列,判定第一个是不是第二个的子序列。
- 要解决这个问题,常规算法是贪心算法。我们维护两个指针指向两个列表的最开始,然后对第二个序列一路扫过去,如果某个数字和第一个指针指的一样,那么就把第一个指针前进一步。第一个指针移出第一个序列最后一个元素的时候,返回 True,否则返回 False。
def is_subsequence(a, b):
b = iter(b)
return all(i in b for i in a)
print(is_subsequence([1, 3, 5], [1, 2, 3, 4, 5]))
print(is_subsequence([1, 4, 3], [1, 2, 3, 4, 5]))
########## 输出 ##########
True
False
揭秘python协程
- 事件循环启动一个统一的调度器,让调度器来决定一个时刻去运行哪个任务,于是省却了多线程中启动线程、管理线程、同步锁等各种开销。
- 爬虫每秒钟都会爬取大量的网页,提取关键信息后存储在数据库中,以便日后分析。爬虫有非常简单的 Python 十行代码实现,也有 Google 那样的全球分布式爬虫的上百万行代码,分布在内部上万台服务器上,对全世界的信息进行嗅探。
- await 执行的效果,和 Python 正常执行是一样的,也就是说程序会阻塞在这里,进入被调用的协程函数,执行完毕返回后再继续,而这也是 await 的字面意思。代码中 await asyncio.sleep(sleep_time) 会在这里休息若干秒,await crawl_page(url) 则会执行 crawl_page() 函数。
- 我们可以通过 asyncio.create_task() 来创建任务
- 最后,我们需要 asyncio.run 来触发运行。asyncio.run 这个函数是 Python 3.7 之后才有的特性,可以让 Python 的协程接口变得非常简单。
- 结果显示,运行总时长等于运行时间最长的爬虫。
Python并发编程之Futures
- 虽然线程的数量可以自己定义,但是线程数并不是越多越好,因为线程的创建、维护和删除也会有一定的开销。所以如果你设置的很大,反而可能会导致速度变慢。我们往往需要根据实际的需求做一些测试,来寻找最优的线程数量。
- Python主程序只允许有一个线程执行,所以Python的并发,是通过多线程的切换完成的。
Python单元测试
- Python mock则主要使用mock或者MagicMock对象,这里我也举了一个代码示例。这个例子看上去比较简单,但是里面的思想很重要。
import unittest
from unittest.mock import MagicMock
class A(unittest.TestCase):
def m1(self):
val = self.m2()
self.m3(val)
def m2(self):
pass
def m3(self, val):
pass
def test_m1(self):
a = A()
a.m2 = MagicMock(return_value="custom_val")
a.m3 = MagicMock()
a.m1()
self.assertTrue(a.m2.called) #验证m2被call过
a.m3.assert_called_with("custom_val") #验证m3被指定参数call过
if __name__ == '__main__':
unittest.main(argv=['first-arg-is-ignored'], exit=False)
## 输出
..
----------------------------------------------------------------------
Ran 2 tests in 0.002s
OK
- 单元测试去除掉依赖项mock掉
pdb进行单元测试
cProfile性能分析
- ncalls,是指相应代码/函数被调用的次数;
- tottime,是指对应代码/函数总共执行所需要的时间(注意,并不包括它调用的其他代码/函数的执行时间);
- tottime percall,就是上述两者相除的结果,也就是tottime / ncalls;
- cumtime,则是指对应代码/函数总共执行所需要的时间,这里包括了它调用的其他代码/函数的执行时间;
- cumtime percall,则是cumtime和ncalls相除的平均结果。
def memoize(f):
memo = {}
def helper(x):
if x not in memo:
memo[x] = f(x)
return memo[x]
return helper
@memoize
def fib(n):
if n == 0:
return 0
elif n == 1:
return 1
else:
return fib(n-1) + fib(n-2)
def fib_seq(n):
res = []
if n > 0:
res.extend(fib_seq(n-1))
res.append(fib(n))
return res
fib_seq(30)