极客时间专栏:python核心技术与实战

学习内容来自极客时间专栏

由于广告原因,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)

总结

  • 列表是动态的, 可以随意增加删减,改变元素。列表的存储空间略大于元组,性能略逊于元组。
  • 元组是静态的,大小长度固定,不可以对元素进行增加,删减,改变操作。元组更加轻量级,性能稍优。

思考题

    1. 想创建一个空的列表,我们可以用下面的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的形式
  1. 那在需要扩容的时候,是不是也是需要重新开辟一块连续的内存空间呢?
  2. 每次扩容都会预留一些空间,这里面有没有公式,公式是什么呢
  • 作者回复
  1. 是的
  2. 你可以自己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会修改,不加不会修改,根本就是外部函数的值变不变


总结

  1. Python中函数的参数可以接受任意的数据类型,使用起来需要注意,必要时请在函数开头加入数据类型的检查;

  2. 和其他语言不同,Python中函数的参数可以设定默认值;

  3. 嵌套函数的使用,能保证数据的隐私性,提高程序运行效率;

  4. 合理地使用闭包,则可以简化程序的复杂度,提高可读性。

  5. 在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)

在这里插入图片描述

发布了140 篇原创文章 · 获赞 53 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/qq_44291044/article/details/102678297