乐趣区

关于后端:Python基础篇

大家好,我是易安!

Python 语言比起 C ++、Java 等支流语言,语法更简洁,也更靠近英语,对编程世界的新人还是很敌对的,这也是其显著长处。最近总有人问我 Python 相干的问题,这些问题也偏根底,自古有句话,授人以鱼不如授人以渔,刚好趁五一工夫总结了几篇 Python 的知识点,帮忙小伙伴胜利入坑 Python,将这门工具语言顺利把握起来。

Python 罕用数据结构

对于每一门编程语言来说,数据结构都是其根基。理解把握 Python 的根本数据结构,对于学好这门语言至关重要。

列表和元组

首先,咱们须要弄清楚最根本的概念,什么是列表和元组呢?

实际上,列表和元组,都是 一个能够搁置任意数据类型的有序汇合

在绝大多数编程语言中,汇合的数据类型必须统一。不过,对于 Python 的列表和元组来说,并无此要求:

l = [1, 2, 'hello', 'world'] # 列表中同时含有 int 和 string 类型的元素
l
[1, 2, 'hello', 'world']

tup = ('jason', 22) # 元组中同时含有 int 和 string 类型的元素
tup
('jason', 22)

其次,咱们必须把握它们的区别。

  • 列表是动静的,长度大小不固定,能够随便地减少、删减或者扭转元素(mutable)。
  • 而元组是动态的,长度大小固定,无奈减少删减或者扭转(immutable)。

上面的例子中,咱们别离创立了一个列表与元组。你能够看到,对于列表,咱们能够很轻松地让其最初一个元素,由 4 变为 40;然而,如果你对元组采取雷同的操作,Python 就会报错,起因就是元组是不可变的。

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

可是,如果你想对已有的元组做任何 ” 扭转 ”,该怎么办呢?那就只能从新开拓一块内存,创立新的元组了。

比方上面的例子,咱们想减少一个元素 5 给元组,实际上就是创立了一个新的元组,而后把原来两个元组的值顺次填充进去。

而对于列表来说,因为其是动静的,咱们只需简略地在列表开端,退出对应元素就能够了。如下操作后,会批改原来列表中的元素,而不会创立新的列表。

tup = (1, 2, 3, 4)
new_tup = tup + (5,) # 创立新的元组 new_tup,并顺次填充原元组的值
new _tup
(1, 2, 3, 4, 5)

l = [1, 2, 3, 4]
l.append(5) # 增加元素 5 到原列表的开端
l
[1, 2, 3, 4, 5]

通过下面的例子,置信你必定把握了列表和元组的基本概念。接下来咱们来看一些列表和元组的基本操作和注意事项。

首先,和其余语言不同,Python 中的列表和元组都反对正数索引,- 1 示意最初一个元素,- 2 示意倒数第二个元素,以此类推。

l = [1, 2, 3, 4]
l[-1]
4

tup = (1, 2, 3, 4)
tup[-1]
4

除了根本的初始化,索引外,列表和元组都反对切片操作

l = [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)

最初,咱们来看一些列表和元组罕用的内置函数:

l = [3, 2, 3, 7, 8, 1]
l.count(3)
2
l.index(7)
3
l.reverse()
l
[1, 8, 7, 3, 2, 3]
l.sort()
l
[1, 2, 3, 3, 7, 8]

tup = (3, 2, 3, 7, 8, 1)
tup.count(3)
2
tup.index(7)
3
list(reversed(tup))
[1, 8, 7, 3, 2, 3]
sorted(tup)
[1, 2, 3, 3, 7, 8]

这里我简略解释一下这几个函数的含意。

  • count(item)示意统计列表 / 元组中 item 呈现的次数。
  • index(item)示意返回列表 / 元组中 item 第一次呈现的索引。
  • list.reverse()和 list.sort()别离示意原地倒转列表和排序(留神,元组没有内置的这两个函数)。
  • reversed()和 sorted()同样示意对列表 / 元组进行倒转和排序,reversed()返回一个倒转后的迭代器(上文例子应用 list()函数再将其转换为列表);sorted()返回排好序的新列表。

列表和元组存储形式的差别

后面说了,列表和元组最重要的区别就是,列表是动静的、可变的,而元组是动态的、不可变的。这样的差别,势必会影响两者存储形式。咱们能够来看上面的例子:

l = [1, 2, 3]
l.__sizeof__()
64
tup = (1, 2, 3)
tup.__sizeof__()
48

你能够看到,对列表和元组,咱们搁置了雷同的元素,然而元组的存储空间,却比列表要少 16 字节。这是为什么呢?

事实上,因为列表是动静的,所以它须要存储指针,来指向对应的元素(上述例子中,对于 int 型,8 字节)。另外,因为列表可变,所以须要额定存储曾经调配的长度大小(8 字节),这样才能够实时追踪列表空间的应用状况,当空间有余时,及时调配额定空间。

l = []
l.__sizeof__() // 空列表的存储空间为 40 字节
40
l.append(1)
l.__sizeof__()
72 // 退出了元素 1 之后,列表为其调配了能够存储 4 个元素的空间 (72 - 40)/8 = 4
l.append(2)
l.__sizeof__()
72 // 因为之前调配了空间,所以退出元素 2,列表空间不变
l.append(3)
l.__sizeof__()
72 // 同上
l.append(4)
l.__sizeof__()
72 // 同上
l.append(5)
l.__sizeof__()
104 // 退出元素 5 之后,列表的空间有余,所以又额定调配了能够存储 4 个元素的空间

下面的例子,大略形容了列表空间调配的过程。咱们能够看到,为了减小每次减少 / 删减操作时空间调配的开销,Python 每次调配空间时都会额定多调配一些,这样的机制(over-allocating)保障了其操作的高效性:减少 / 删除的工夫复杂度均为 O(1)。

然而对于元组,状况就不同了。元组长度大小固定,元素不可变,所以存储空间固定。

看了后面的剖析,你兴许会感觉,这样的差别能够忽略不计。然而设想一下,如果列表和元组存储元素的个数是一亿,十亿甚至更大数量级时,你还能疏忽这样的差别吗?

列表和元组的性能

通过学习列表和元组存储形式的差别,咱们能够得出结论:元组要比列表更加轻量级一些,所以总体上来说,元组的性能速度要略优于列表。

另外,Python 会在后盾,对静态数据做一些 资源缓存(resource caching)。通常来说,因为垃圾回收机制的存在,如果一些变量不被应用了,Python 就会回收它们所占用的内存,返还给操作系统,以便其余变量或其余利用应用。

然而对于一些动态变量,比方元组,如果它不被应用并且占用空间不大时,Python 会临时缓存这部分内存。这样,下次咱们再创立同样大小的元组时,Python 就能够不必再向操作系统发出请求,去寻找内存,而是能够间接调配之前缓存的内存空间,这样就能大大放慢程序的运行速度。

上面的例子,是计算 初始化 一个雷同元素的列表和元组别离所需的工夫。咱们能够看到,元组的初始化速度,要比列表快 5 倍。

python3 -m timeit 'x=(1,2,3,4,5,6)'
20000000 loops, best of 5: 9.97 nsec per loop
python3 -m timeit 'x=[1,2,3,4,5,6]'
5000000 loops, best of 5: 50.1 nsec per loop

但如果是 索引操作 的话,两者的速度差异十分小,简直能够忽略不计。

python3 -m timeit -s 'x=[1,2,3,4,5,6]' 'y=x[3]'
10000000 loops, best of 5: 22.2 nsec per loop
python3 -m timeit -s 'x=(1,2,3,4,5,6)' 'y=x[3]'
10000000 loops, best of 5: 21.9 nsec per loop

当然,如果你想要减少、删减或者扭转元素,那么列表显然更优。起因你当初必定晓得了,那就是对于元组,你必须得通过新建一个元组来实现。

列表和元组的应用场景

那么列表和元组到底用哪一个呢?依据下面所说的个性,咱们具体情况具体分析。

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)

字典和汇合

那到底什么是字典,什么是汇合呢?字典是一系列由键(key)和值(value)配对组成的元素的汇合,在 Python3.7+,字典被确定为有序(留神:在 3.6 中,字典有序是一个 implementation detail,在 3.7 才正式成为语言个性,因而 3.6 中无奈 100% 确保其有序性),而 3.6 之前是无序的,其长度大小可变,元素能够任意地删减和扭转。

相比于列表和元组,字典的性能更优,特地是对于查找、增加和删除操作,字典都能在常数工夫复杂度内实现。

而汇合和字典基本相同,惟一的区别,就是汇合没有键和值的配对,是一系列无序的、惟一的元素组合。

首先咱们来看字典和汇合的创立,通常有上面这几种形式:

d1 = {'name': 'jason', 'age': 20, 'gender': 'male'}
d2 = dict({'name': 'jason', 'age': 20, 'gender': 'male'})
d3 = dict([('name', 'jason'), ('age', 20), ('gender', 'male')])
d4 = dict(name='jason', age=20, gender='male')
d1 == d2 == d3 ==d4
True

s1 = {1, 2, 3}
s2 = set([1, 2, 3])
s1 == s2
True

这里留神,Python 中字典和汇合,无论是键还是值,都能够是混合类型。比方上面这个例子,我创立了一个元素为 1'hello'5.0 的汇合:

s = {1, 'hello', 5.0}

再来看元素拜访的问题。字典拜访能够间接索引键,如果不存在,就会抛出异样:

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'

说完了字典的拜访,咱们再来看汇合。

首先我要强调的是,汇合并不反对索引操作,因为汇合实质上是一个哈希表,和列表不一样。所以,上面这样的操作是谬误的,Python 会抛出异样:

s = {1, 2, 3}
s[0]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'set' object does not support indexing

想要判断一个元素在不在字典或汇合内,咱们能够用 value in dict/set 来判断。

s = {1, 2, 3}
1 in s
True
10 in s
False

d = {'name': 'jason', 'age': 20}
'name' in d
True
'location' in d
False

当然,除了创立和拜访,字典和汇合也同样反对减少、删除、更新等操作。

d = {'name': 'jason', 'age': 20}
d['gender'] = 'male' # 减少元素对 'gender': 'male'
d['dob'] = '1999-02-01' # 减少元素对 'dob': '1999-02-01'
d
{'name': 'jason', 'age': 20, 'gender': 'male', 'dob': '1999-02-01'}
d['dob'] = '1998-01-01' # 更新键 'dob' 对应的值
d.pop('dob') # 删除键为 'dob' 的元素对
'1998-01-01'
d
{'name': 'jason', 'age': 20, 'gender': 'male'}

s = {1, 2, 3}
s.add(4) # 减少元素 4 到汇合
s
{1, 2, 3, 4}
s.remove(4) # 从汇合中删除元素 4
s
{1, 2, 3}

不过要留神,汇合的 pop()操作是删除汇合中最初一个元素,可是汇合自身是无序的,你无奈晓得会删除哪个元素,因而这个操作得审慎应用。

理论利用中,很多状况下,咱们须要对字典或汇合进行排序,比方,取出值最大的 50 对。

对于字典,咱们通常会依据键或值,进行升序或降序排序:

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]

字典和汇合性能

字典和汇合是进行过性能高度优化的数据结构,特地是对于查找、增加和删除操作。那接下来,咱们就来看看,它们在具体场景下的性能体现,以及与列表等其余数据结构的比照。

比方电商企业的后盾,存储了每件产品的 ID、名称和价格。当初的需要是,给定某件商品的 ID,咱们要找出其价格。

如果咱们用列表来存储这些数据结构,并进行查找,相应的代码如下:

def find_product_price(products, product_id):
    for id, price in products:
        if id == product_id:
            return price
    return None

products = [(143121312, 100),
    (432314553, 30),
    (32421912367, 150)
]

print('The price of product 432314553 is {}'.format(find_product_price(products, 432314553)))

# 输入
The price of product 432314553 is 30

假如列表有 n 个元素,而查找的过程要遍历列表,那么工夫复杂度就为 O(n)。即便咱们先对列表进行排序,而后应用二分查找,也会须要 O(logn)的工夫复杂度,更何况,列表的排序还须要 O(nlogn)的工夫。

但如果咱们用字典来存储这些数据,那么查找就会十分便捷高效,只需 O(1)的工夫复杂度就能够实现。起因也很简略,刚刚提到过的,字典的外部组成是一张哈希表,你能够间接通过键的哈希值,找到其对应的值。

products = {
  143121312: 100,
  432314553: 30,
  32421912367: 150
}
print('The price of product 432314553 is {}'.format(products[432314553]))

# 输入
The price of product 432314553 is 30

相似的,当初需要变成,要找出这些商品有多少种不同的价格。咱们还用同样的办法来比拟一下。

如果还是抉择应用列表,对应的代码如下,其中,A 和 B 是两层循环。同样假如原始列表有 n 个元素,那么,在最差状况下,须要 O(n^2)的工夫复杂度。

# list version
def find_unique_price_using_list(products):
    unique_price_list = []
    for _, price in products: # A
        if price not in unique_price_list: #B
            unique_price_list.append(price)
    return len(unique_price_list)

products = [(143121312, 100),
    (432314553, 30),
    (32421912367, 150),
    (937153201, 30)
]
print('number of unique price is: {}'.format(find_unique_price_using_list(products)))

# 输入
number of unique price is: 3

但如果咱们抉择应用汇合这个数据结构,因为汇合是高度优化的哈希表,外面元素不能反复,并且其增加和查找操作只需 O(1)的复杂度,那么,总的工夫复杂度就只有 O(n)。

# set version
def find_unique_price_using_set(products):
    unique_price_set = set()
    for _, price in products:
        unique_price_set.add(price)
    return len(unique_price_set)

products = [(143121312, 100),
    (432314553, 30),
    (32421912367, 150),
    (937153201, 30)
]
print('number of unique price is: {}'.format(find_unique_price_using_set(products)))

# 输入
number of unique price is: 3

可能你对这些工夫复杂度没有直观的意识,我能够举一个理论工作场景中的例子,让你来感受一下。

上面的代码,初始化了含有 100,000 个元素的产品,并别离计算了应用列表和汇合来统计产品价格数量的运行工夫:

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

你能够看到,仅仅十万的数据量,两者的速度差别就如此之大。事实上,大型企业的后盾数据往往有上亿乃至十亿数量级,如果应用了不适合的数据结构,就很容易造成服务器的解体,岂但影响用户体验,并且会给公司带来微小的财产损失。

字典和汇合的工作原理

咱们通过举例以及与列表的比照,看到了字典和汇合操作的高效性。不过,字典和汇合为什么可能如此高效,特地是查找、插入和删除操作?

这当然和字典、汇合外部的数据结构密不可分。不同于其余数据结构,字典和汇合的内部结构都是一张哈希表。

  • 对于字典而言,这张表存储了哈希值(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 会首先计算键的哈希值(hash(key)),再和 mask = PyDicMinSize – 1 做与操作,计算这个元素应该插入哈希表的地位 index = hash(key) & mask。如果哈希表中此地位是空的,那么这个元素就会被插入其中。

而如果此地位已被占用,Python 便会比拟两个元素的哈希值和键是否相等。

  • 若两者都相等,则表明这个元素曾经存在,如果值不同,则更新值。
  • 若两者中有一个不相等,这种状况咱们通常称为哈希抵触(hash collision),意思是两个元素的键不相等,然而哈希值相等。这种状况下,Python 便会持续寻找表中空余的地位,直到找到地位为止。

值得一提的是,通常来说,遇到这种状况,最简略的形式是线性寻找,即从这个地位开始,挨个往后寻找空位。当然,Python 外部对此进行了优化(这一点无需深刻理解,你有趣味能够查看源码,我就不再赘述),让这个步骤更加高效。

查找操作

和后面的插入操作相似,Python 会依据哈希值,找到其应该处于的地位;而后,比拟哈希表这个地位中元素的哈希值和键,与须要查找的元素是否相等。如果相等,则间接返回;如果不等,则持续查找,直到找到空位或者抛出异样为止。

删除操作

对于删除操作,Python 会临时对这个地位的元素,赋于一个非凡的值,等到从新调整哈希表的大小时,再将其删除。

不难理解,哈希抵触的产生,往往会升高字典和汇合操作的速度。因而,为了保障其高效性,字典和汇合内的哈希表,通常会保障其至多留有 1 / 3 的残余空间。随着元素的不停插入,当残余空间小于 1 / 3 时,Python 会从新获取更大的内存空间,裁减哈希表。不过,这种状况下,表内所有的元素地位都会被从新排放。

尽管哈希抵触和哈希表大小的调整,都会导致速度减缓,然而这种状况产生的次数极少。所以,均匀状况下,这仍能保障插入、查找和删除的工夫复杂度为 O(1)。

字符串

什么是字符串呢?字符串是由独立字符组成的一个序列,通常蕴含在单引号('')双引号("")或者三引号之中(''''''"""""",两者一样),比方上面几种写法。

name = 'jason'
city = 'beijing'
text = "welcome to python world"

这里定义了 name、city 和 text 三个变量,都是字符串类型。咱们晓得,Python 中单引号、双引号和三引号的字符串是截然不同的,没有区别,比方上面这个例子中的 s1、s2、s3 齐全一样。

s1 = 'hello'
s2 = "hello"
s3 = """hello"""
s1 == s2 == s3
True

Python 同时反对这三种表达方式,很重要的一个起因就是,这样不便你在字符串中,内嵌带引号的字符串。比方:

"I'm a student"

Python 的三引号字符串,则次要利用于多行字符串的情境,比方函数的正文等等。

def calculate_similarity(item1, item2):
    """
    Calculate similarity between two items
    Args:
        item1: 1st item
        item2: 2nd item
    Returns:
      similarity score between item1 and item2
    """

同时,Python 也反对转义字符。所谓的转义字符,就是用反斜杠结尾的字符串,来示意一些特定意义的字符。我把常见的的转义字符,总结成了上面这张表格。

为了不便你了解,我举一个例子来阐明。

s = 'a\nb\tc'
print(s)
a
b    c

这段代码中的 '\n',示意一个字符——换行符;'\t' 也示意一个字符——横向制表符。所以,最初打印进去的输入,就是字符 a,换行,字符 b,而后制表符,最初打印字符 c。不过要留神,尽管最初打印的输入横跨了两行,然而整个字符串 s 依然只有 5 个元素。

len(s)
5

在转义字符的利用中,最常见的就是换行符 '\n' 的应用。比方文件读取,如果咱们一行行地读取,那么每一行字符串的开端,都会蕴含换行符 '\n'。而最初做数据处理时,咱们往往会丢掉每一行的换行符。

字符串的罕用操作

讲完了字符串的基本原理,上面咱们一起来看看字符串的罕用操作。你能够把字符串设想成一个由单个字符组成的数组,所以,Python 的字符串同样反对索引,切片和遍历等等操作。

name = 'jason'
name[0]
'j'
name[1:3]
'as'

和其余数据结构,如列表、元组一样,字符串的索引同样从 0 开始,index= 0 示意第一个元素(字符),[index:index+2]则示意第 index 个元素到 index+ 1 个元素组成的子字符串。

遍历字符串同样很简略,相当于遍历字符串中的每个字符。

for char in name:
    print(char)
j
a
s
o
n

特地要留神,Python 的字符串是不可变的(immutable)。因而,用上面的操作,来扭转一个字符串外部的字符是谬误的,不容许的。

s = 'hello'
s[0] = 'H'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'str' object does not support item assignment

Python 中字符串的扭转,通常只能通过创立新的字符串来实现。比方上述例子中,想把 'hello' 的第一个字符 'h',改为大写的 'H',咱们能够采纳上面的做法:

s = 'H' + s[1:]
s = s.replace('h', 'H')
  • 第一种办法,是间接用大写的 'H',通过加号 '+' 操作符,与原字符串切片操作的子字符串拼接而成新的字符串。
  • 第二种办法,是间接扫描原字符串,把小写的 '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)

你感觉这个例子的工夫复杂度是多少呢?

每次循环,仿佛都得创立一个新的字符串;而每次创立一个新的字符串,都须要 O(n)的工夫复杂度。因而,总的工夫复杂度就为 O(1) + O(2) + … + O(n) = O(n^2)。这样到底对不对呢?

乍一看,这样剖析的确很有情理,然而必须阐明,这个论断只实用于老版本的 Python 了。自从 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)

此外,常见的函数还有:

  • string.strip(str),示意去掉首尾的 str 字符串;
  • string.lstrip(str),示意只去掉结尾的 str 字符串;
  • string.rstrip(str),示意只去掉尾部的 str 字符串。

这些在数据的解析解决中同样很常见。比方很多时候,从文件读进来的字符串中,结尾和结尾都含有空字符,咱们须要去掉它们,就能够用 strip()函数:

s = 'my name is jason'
s.strip()
'my name is jason'

当然,Python 中字符串还有很多罕用操作,比方,string.find(sub, start, end),示意从 start 到 end 查找字符串中子字符串 sub 的地位等等。这里,我只强调了最罕用并且容易出错的几个函数,其余内容你能够自行查找相应的文档、范例加以理解,我就不一一赘述了。

字符串的格式化

最初,咱们一起来看看字符串的格式化。什么是字符串的格式化呢?

通常,咱们应用一个字符串作为模板,模板中会有格局符。这些格局符为后续实在值预留地位,以呈现出实在值应该出现的格局。字符串的格式化,通常会用在程序的输入、logging 等场景。

举一个常见的例子。比方咱们有一个工作,给定一个用户的 userid,要去数据库中查问该用户的一些信息,并返回。而如果数据库中没有此人的信息,咱们通常会记录下来,这样有利于往后的日志剖析,或者是线上 bug 的调试等等。

咱们通常会用上面的办法来示意:

print('no data available for person with id: {}, name: {}'.format(id, name))

其中的 string.format(),就是所谓的格式化函数;而大括号 {} 就是所谓的格局符,用来为前面的实在值——变量 name 预留地位。如果 id = '123'name='jason',那么输入便是:

'no data available for person with id: 123, name: jason'

这样看来,是不是非常简单呢?

不过要留神,string.format()是最新的字符串格局函数与标准。天然,咱们还有其余的示意办法,比方在 Python 之前版本中,字符串格式化通常用 % 来示意,那么上述的例子,就能够写成上面这样:

print('no data available for person with id: %s, name: %s' % (id, name))

其中 %s 示意字符串型,%d 示意整型等等,这些属于常识,你应该都理解。

当然,当初你写程序时,我还是举荐应用 format 函数,毕竟这是最新标准,也是官网文档举荐的标准。

兴许有人会问,为什么非要应用格式化函数,上述例子用字符串的拼接不也能实现吗?没错,在很多状况下,字符串拼接的确能满足格式化函数的需要。然而应用格式化函数,更加清晰、易读,并且更加标准,不易出错。

语句

下面咱们理解了列表、元组、字典、汇合和字符串等一系列 Python 的根本数据类型,紧接着咱们来看下编程中另外一个重要的概念,条件循环语句。

“条件与循环”,堪称编程中的基本功。为什么称它为基本功呢?因为它管制着代码的逻辑,能够说是程序的中枢系统。如果把写程序比作盖楼房,那么条件与循环就是楼房的根基,其余所有货色都是在此基础上构建而成。

毫不夸大地说,写一手简洁易读的条件与循环代码,对进步程序整体的品质至关重要。

条件语句

首先,咱们一起来看一下 Python 的条件语句,用法很简略。比方,我想要示意 y =|x| 这个函数,那么相应的代码便是:

# y = |x|
if x < 0:
    y = -x
else:
    y = x

和其余语言不一样,咱们不能在条件语句中加括号,写成上面这样的格局。

if (x < 0)

但须要留神的是,在条件语句的开端必须加上冒号(:),这是 Python 特定的语法标准。

因为 Python 不反对 switch 语句,因而,当存在多个条件判断时,咱们须要用 else if 来实现,这在 Python 中的表白是 elif。语法如下:

if condition_1:
    statement_1
elif condition_2:
    statement_2
...
elif condition_i:
    statement_i
else:
    statement_n

整个条件语句是程序执行的,如果遇到一个条件满足,比方 condition\_i 满足时,在执行完 statement\_i 后,便会退出整个 if、elif、else 条件语句,而不会持续向下执行。这个语句在工作中很罕用,比方上面的这个例子。

理论工作中,咱们常常用 ID 示意一个事物的属性,而后进行条件判断并且输入。比方,在 integrity 的工作中,通常用 0、1、2 别离示意一部电影的色情暴力水平。其中,0 的水平最高,是 red 级别;1 其次,是 yellow 级别;2 代表没有品质问题,属于 green。

如果给定一个 ID,要求输入某部电影的品质评级,则代码如下:

if id == 0:
    print('red')
elif id == 1:
    print('yellow')
else:
    print('green')

不过要留神,if 语句是能够独自应用的,但 elif、else 都必须和 if 成对应用。

另外,在咱们进行条件判断时,不少人喜爱省略判断的条件,比方写成上面这样:

if s: # s is a string
    ...
if l: # l is a list
    ...
if i: # i is an int
    ...
...

对于省略判断条件的常见用法,我大略总结了一下:

不过,切记,在理论写代码时,咱们激励,除了 boolean 类型的数据,条件判断最好是显性的。比方,在判断一个整型数是否为 0 时,咱们最好写出判断的条件:

if i != 0:
    ...

而不是只写出变量名:

if i:
    ...

循环语句

讲完了条件语句,咱们接着来看循环语句。所谓循环,顾名思义,实质上就是遍历汇合中的元素。和其余语言一样,Python 中的循环个别通过 for 循环和 while 循环实现。

比方,咱们有一个列表,须要遍历列表中的所有元素并打印输出,代码如下:

l = [1, 2, 3, 4]
for item in l:
    print(item)
1
2
3
4

你看,是不是很简略呢?

其实,Python 中的数据结构只有是可迭代的(iterable),比方列表、汇合等等,那么都能够通过上面这种形式遍历:

for item in <iterable>:
    ...

这里须要独自强调一下字典。字典自身只有键是可迭代的,如果咱们要遍历它的值或者是键值对,就须要通过其内置的函数 values()或者 items()实现。其中,values()返回字典的值的汇合,items()返回键值对的汇合。

d = {'name': 'jason', 'dob': '2000-01-01', 'gender': 'male'}
for k in d: # 遍历字典的键
    print(k)
name
dob
gender

for v in d.values(): # 遍历字典的值
    print(v)
jason
2000-01-01
male

for k, v in d.items(): # 遍历字典的键值对
    print('key: {}, value: {}'.format(k, v))
key: name, value: jason
key: dob, value: 2000-01-01
key: gender, value: male

看到这里你兴许会问,有没有方法通过汇合中的索引来遍历元素呢?当然能够,其实这种状况在理论工作中还是很常见的,甚至很多时候,咱们还得依据索引来做一些条件判断。

咱们通常通过 range()这个函数,拿到索引,再去遍历拜访汇合中的元素。比方上面的代码,遍历一个列表中的元素,当索引小于 5 时,打印输出:

l = [1, 2, 3, 4, 5, 6, 7]
for index in range(0, len(l)):
    if index < 5:
        print(l[index])

1
2
3
4
5

当咱们同时须要索引和元素时,还有一种更简洁的形式,那就是通过 Python 内置的函数 enumerate()。用它来遍历汇合,不仅返回每个元素,并且还返回其对应的索引,这样一来,下面的例子就能够写成:

l = [1, 2, 3, 4, 5, 6, 7]
for index, item in enumerate(l):
    if index < 5:
        print(item)

1
2
3
4
5

在循环语句中,咱们还经常搭配 continue 和 break 一起应用。所谓 continue,就是让程序跳过以后这层循环,继续执行上面的循环;而 break 则是指齐全跳出所在的整个循环体。在循环中适当退出 continue 和 break,往往能使程序更加简洁、易读。

比方,给定两个字典,别离是产品名称到价格的映射,和产品名称到色彩列表的映射。咱们要找出价格小于 1000,并且色彩不是红色的所有产品名称和色彩的组合。如果不必 continue,代码应该是上面这样的:

# name_price: 产品名称 (str) 到价格 (int) 的映射字典
# name_color: 产品名字 (str) 到色彩 (list of str) 的映射字典
for name, price in name_price.items():
    if price < 1000:
        if name in name_color:
            for color in name_color[name]:
                if color != 'red':
                    print('name: {}, color: {}'.format(name, color))
        else:
            print('name: {}, color: {}'.format(name, 'None'))

而退出 continue 后,代码显然清晰了很多:

# name_price: 产品名称 (str) 到价格 (int) 的映射字典
# name_color: 产品名字 (str) 到色彩 (list of str) 的映射字典
for name, price in name_price.items():
    if price >= 1000:
        continue
    if name not in name_color:
        print('name: {}, color: {}'.format(name, 'None'))
        continue
    for color in name_color[name]:
        if color == 'red':
            continue
        print('name: {}, color: {}'.format(name, color))

咱们能够看到,依照第一个版本的写法,从开始始终到打印输出符合条件的产品名称和色彩,共有 5 层 for 或者 if 的嵌套;但第二个版本退出了 continue 后,只有 3 层嵌套。

显然,如果代码中呈现嵌套里还有嵌套的状况,代码便会变得十分冗余、难读,也不利于后续的调试、批改。因而,咱们要尽量避免这种多层嵌套的状况。

后面讲了 for 循环,对于 while 循环,原理也是一样的。它示意当 condition 满足时,始终反复循环外部的操作,直到 condition 不再满足,就跳出循环体。

while condition:
    ....

很多时候,for 循环和 while 循环能够相互转换,比方要遍历一个列表,咱们用 while 循环同样能够实现:

l = [1, 2, 3, 4]
index = 0
while index < len(l):
    print(l[index])
    index += 1

那么,两者的应用场景又有什么区别呢?

通常来说,如果你只是遍历一个已知的汇合,找出满足条件的元素,并进行相应的操作,那么应用 for 循环更加简洁。但如果你须要在满足某个条件前,不停地反复某些操作,并且没有特定的汇合须要去遍历,那么个别则会应用 while 循环。

比方,某个交互式问答零碎,用户输出文字,零碎会依据内容做出相应的答复。为了实现这个性能,咱们个别会应用 while 循环,大抵代码如下:

while True:
    try:
        text = input('Please enter your questions, enter"q"to exit')
        if text == 'q':
            print('Exit system')
            break
        ...
        ...
        print(response)
    except Exception as err:
        print('Encountered error: {}'.format(err))
        break

同时须要留神的是,for 循环和 while 循环的效率问题。比方上面的 while 循环:

i = 0
while i < 1000000:
    i += 1

和等价的 for 循环:

for i in range(0, 1000000):
    pass

到底哪个效率高呢?

要晓得,range()函数是间接由 C 语言写的,调用它速度十分快。而 while 循环中的“i += 1”这个操作,得通过 Python 的解释器间接调用底层的 C 语言;并且这个简略的操作,又波及到了对象的创立和删除(因为 i 是整型,是 immutable,i += 1 相当于 i = new int(i + 1))。所以,显然,for 循环的效率更胜一筹。

条件与循环的复用

后面两局部讲了条件与循环的一些基本操作,接下来,咱们重点来看它们的进阶操作,让程序变得更简洁高效。

在浏览代码的时候,你应该经常会发现,有很多将条件与循环并做一行的操作,例如:

expression1 if condition else expression2 for item in iterable

将这个表达式合成开来,其实就等同于上面这样的嵌套构造:

for item in iterable:
    if condition:
        expression1
    else:
        expression2

而如果没有 else 语句,则须要写成:

expression for item in iterable if condition

举个例子,比方咱们要绘制 y = 2*|x| + 5 的函数图像,给定汇合 x 的数据点,须要计算出 y 的数据汇合,那么只用一行代码,就能够很轻松地解决问题了:

y = [value * 2 + 5 if value > 0 else -value * 2 + 5 for value in x]

再比方咱们在解决文件中的字符串时,经常遇到的一个场景:将文件中逐行读取的一个残缺语句,按逗号宰割单词,去掉首位的空字符,并过滤掉长度小于等于 3 的单词,最初返回由单词组成的列表。这同样能够简洁地表达成一行:

text = 'Today,  is, Sunday'
text_list = [s.strip() for s in text.split(',') if len(s.strip()) > 3]
print(text_list)
['Today', 'Sunday']

当然,这样的复用并不仅仅局限于一个循环。比方,给定两个列表 x、y,要求返回 x、y 中所有元素对组成的元组,相等状况除外。那么,你也能够很容易示意进去:

[(xx, yy) for xx in x for yy in y if xx != yy]

这样的写法就等价于:

l = []
for xx in x:
    for yy in y:
        if xx != yy:
            l.append((xx, yy))

纯熟之后,你会发现这种写法十分不便。当然,如果遇到逻辑很简单的复用,你可能会感觉写成一行难以了解、容易出错。那种状况下,用失常的模式表白,也不失为一种好的标准和抉择。

输入输出

最简略间接的输出来自键盘操作,比方上面这个例子。

name = input('your name:')
gender = input('you are a boy?(y/n)')

###### 输出 ######
your name:Jack
you are a boy?

welcome_str = 'Welcome to the matrix {prefix} {name}.'
welcome_dic = {
    'prefix': 'Mr.' if gender == 'y' else 'Mrs',
    'name': name
}

print('authorizing...')
print(welcome_str.format(**welcome_dic))

########## 输入 ##########
authorizing...
Welcome to the matrix Mr. Jack.

input() 函数暂停程序运行,同时期待键盘输入;直到回车被按下,函数的参数即为提醒语,输出的类型永远是字符串型(str)。留神,初学者在这里很容易犯错,上面的例子我会讲到。print() 函数则承受字符串、数字、字典、列表甚至一些自定义类的输入。

咱们再来看上面这个例子。

a = input()
1
b = input()
2

print('a + b = {}'.format(a + b))
########## 输入 ##############
a + b = 12
print('type of a is {}, type of b is {}'.format(type(a), type(b)))
########## 输入 ##############
type of a is <class 'str'>, type of b is <class 'str'>
print('a + b = {}'.format(int(a) + int(b)))
########## 输入 ##############
a + b = 3

这里留神,把 str 强制转换为 int 请用 int(),转为浮点数请用 float()。而在生产环境中应用强制转换时,请记得加上 try except(即谬误和异样解决)。

Python 对 int 类型没有最大限度(相比之下,C++ 的 int 最大为 2147483647,超过这个数字会产生溢出),然而对 float 类型仍然有精度限度。这些特点,除了在一些算法比赛中要留神,在生产环境中也要时刻提防,防止因为对边界条件判断不清而造成 bug 甚至 0day(危重安全漏洞)。

咱们回望一下币圈。2018 年 4 月 23 日中午 11 点 30 分左右,BEC 代币智能合约被黑客攻击。黑客利用数据溢出的破绽,攻打与美图单干的公司美链 BEC 的智能合约,胜利地向两个地址转出了天量级别的 BEC 代币,导致市场上的海量 BEC 被抛售,该数字货币的价值也几近归零,给 BEC 市场交易带来了毁灭性的打击。

由此可见,尽管输入输出和类型解决事件简略,但咱们肯定要慎之又慎。毕竟相当比例的安全漏洞,都来自随便的 I/O 解决。

文件输入输出

命令行的输入输出,只是 Python 交互的最根本形式,实用一些简略小程序的交互。而生产级别的 Python 代码,大部分 I/O 则来自于文件、网络、其余过程的音讯等等。

接下来,咱们来详细分析一个文本文件读写。假如咱们有一个文本文件 in.txt,内容如下:

I have a dream that my four little children will one day live in a nation where they will not be judged by the color of their skin but by the content of their character. I have a dream today.

I have a dream that one day down in Alabama, with its vicious racists, . . . one day right there in Alabama little black boys and black girls will be able to join hands with little white boys and white girls as sisters and brothers. I have a dream today.

I have a dream that one day every valley shall be exalted, every hill and mountain shall be made low, the rough places will be made plain, and the crooked places will be made straight, and the glory of the Lord shall be revealed, and all flesh shall see it together.

This is our hope. . . With this faith we will be able to hew out of the mountain of despair a stone of hope. With this faith we will be able to transform the jangling discords of our nation into a beautiful symphony of brotherhood. With this faith we will be able to work together, to pray together, to struggle together, to go to jail together, to stand up for freedom together, knowing that we will be free one day. . . .

And when this happens, and when we allow freedom ring, when we let it ring from every village and every hamlet, from every state and every city, we will be able to speed up that day when all of God's children, black men and white men, Jews and Gentiles, Protestants and Catholics, will be able to join hands and sing in the words of the old Negro spiritual:"Free at last! Free at last! Thank God Almighty, we are free at last!"

好,让咱们来做一个简略的 NLP(自然语言解决)工作。如果你对此不太理解也没有影响,我会带你一步步实现这个工作。

首先,咱们要分明 NLP 工作的根本步骤,也就是上面的四步:

  1. 读取文件;
  2. 去除所有标点符号和换行符,并把所有大写变成小写;
  3. 合并雷同的词,统计每个词呈现的频率,并依照词频从大到小排序;
  4. 将后果按行输入到文件 out.txt。

你能够本人先思考一下,用 Python 如何解决这个问题。这里,我也给出了我的代码,并附有具体的正文。咱们一起来看下这段代码。

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))

########## 输入(省略较长的两头后果) ##########

and 15
be 13
will 11
to 11
the 10
of 10
a 8
we 8
day 6

...

old 1
negro 1
spiritual 1
thank 1
god 1
almighty 1
are 1

你不必太关怀 parse() 函数的具体实现,你只须要晓得,它做的事件是把输出的 text 字符串,转化为咱们须要的排序后的词频统计。而 sorted\_word\_cnt 则是一个二元组的列表(list of tuples)。

首先咱们须要先理解一下,计算机中文件拜访的基础知识。事实上,计算机内核(kernel)对文件的解决绝对比较复杂,波及到内核模式、虚构文件系统、锁和指针等一系列概念,这些内容我不会深刻解说,我只说一些根底但足够应用的常识。

咱们先要用 open() 函数拿到文件的指针。其中,第一个参数指定文件地位(绝对地位或者相对地位);第二个参数,如果是 'r' 示意读取,如果是 'w' 则示意写入,当然也能够用 'rw',示意读写都要。a 则是一个不太罕用(但也很有用)的参数,示意追加(append),这样关上的文件,如果须要写入,会从原始文件的最开端开始写入。

这里我插一句,在 Facebook 的工作中,代码权限治理十分重要。如果你只须要读取文件,就不要申请写入权限。这样在某种程度上能够升高 bug 对整个零碎带来的危险。

好,回到咱们的话题。在拿到指针后,咱们能够通过 read() 函数,来读取文件的全部内容。代码 text = fin.read(),即示意把文件所有内容读取到内存中,并赋值给变量 text。这么做天然也是有利有弊:

  • 长处是不便,接下来咱们能够很不便地调用 parse 函数进行剖析;
  • 毛病是如果文件过大,一次性读取可能造成内存解体。

这时,咱们能够给 read 指定参数 size,用来示意读取的最大长度。还能够通过 readline() 函数,每次读取一行,这种做法罕用于数据挖掘(Data Mining)中的数据荡涤,在写一些小的程序时十分轻便。如果每行之间没有关联,这种做法也能够升高内存的压力。而 write() 函数,能够把参数中的字符串输入到文件中,也很容易了解。

这里我须要简略提一下 with 语句(后文会具体讲到)。open() 函数对应于 close() 函数,也就是说,如果你关上了文件,在实现读取工作后,就应该立即关掉它。而如果你应用了 with 语句,就不须要显式调用 close()。在 with 的语境下工作执行结束后,close() 函数会被主动调用,代码也简洁很多。

最初须要留神的是,所有 I/O 都应该进行错误处理。因为 I/O 操作可能会有各种各样的状况呈现,而一个强壮(robust)的程序,须要能应答各种状况的产生,而不应该解体(成心设计的状况除外)。

JSON 序列化实战

最初,我来讲一个和理论利用很贴近的知识点。

JSON(JavaScript Object Notation)是一种轻量级的数据交换格局,它的设计用意是把所有事件都用设计的字符串来示意,这样既不便在互联网上传递信息,也不便人进行浏览(相比一些 binary 的协定)。JSON 在当今互联网中利用十分宽泛,也是每一个用 Python 程序员该当熟练掌握的技能点。

构想一个情景,你要向交易所购买肯定数额的股票。那么,你须要提交股票代码、方向(买入 / 卖出)、订单类型(市价 / 限价)、价格(如果是限价单)、数量等一系列参数,而这些数据里,有字符串,有整数,有浮点数,甚至还有布尔型变量,全副混在一起并不不便交易所解包。

那该怎么办呢?

其实,咱们要讲的 JSON,正能解决这个场景。你能够把它简略地了解为两种黑箱:

  • 第一种,输出这些杂七杂八的信息,比方 Python 字典,输入一个字符串;
  • 第二种,输出这个字符串,能够输入蕴含原始信息的 Python 字典。

具体代码如下:

import json

params = {
    'symbol': '123456',
    'type': 'limit',
    'price': 123.4,
    'amount': 23
}

params_str = json.dumps(params)

print('after json serialization')
print('type of params_str = {}, params_str = {}'.format(type(params_str), params))

original_params = json.loads(params_str)

print('after json deserialization')
print('type of original_params = {}, original_params = {}'.format(type(original_params), original_params))

########## 输入 ##########

after json serialization
type of params_str = <class 'str'>, params_str = {'symbol': '123456', 'type': 'limit', 'price': 123.4, 'amount': 23}
after json deserialization
type of original_params = <class 'dict'>, original_params = {'symbol': '123456', 'type': 'limit', 'price': 123.4, 'amount': 23}

其中,

  • json.dumps() 这个函数,承受 Python 的根本数据类型,而后将其序列化为 string;
  • 而 json.loads() 这个函数,承受一个非法字符串,而后将其反序列化为 Python 的根本数据类型。

是不是很简略呢?

不过还是那句话,请记得加上错误处理。不然,哪怕只是给 json.loads() 发送了一个非法字符串,而你没有 catch 到,程序就会解体了。

到这一步,你可能会想,如果我要输入字符串到文件,或者从文件中读取 JSON 字符串,又该怎么办呢?

是的,你依然能够应用下面提到的 open() 和 read()/write(),先将字符串读取 / 输入到内存,再进行 JSON 编码 / 解码,当然这有点麻烦。

import json

params = {
    'symbol': '123456',
    'type': 'limit',
    'price': 123.4,
    'amount': 23
}

with open('params.json', 'w') as fout:
    params_str = json.dump(params, fout)

with open('params.json', 'r') as fin:
    original_params = json.load(fin)

print('after json deserialization')
print('type of original_params = {}, original_params = {}'.format(type(original_params), original_params))

########## 输入 ##########

after json deserialization
type of original_params = <class 'dict'>, original_params = {'symbol': '123456', 'type': 'limit', 'price': 123.4, 'amount': 23}

这样,咱们就简略清晰地实现了读写 JSON 字符串的过程。当开发一个第三方应用程序时,你能够通过 JSON 将用户的集体配置输入到文件,不便下次程序启动时主动读取。这也是当初广泛使用的成熟做法。

那么 JSON 是惟一的抉择吗?显然不是,它只是轻量级利用中最不便的抉择之一。据我所知,在 Google,有相似的工具叫做 Protocol Buffer,当然,Google 曾经齐全开源了这个工具,你能够本人理解一下应用办法。

相比于 JSON,它的长处是生成优化后的二进制文件,因而性能更好。但与此同时,生成的二进制序列,是不能间接浏览的。它在 TensorFlow 等很多对性能有要求的零碎中都有宽泛的利用。

谬误与异样

首先要理解,Python 中的谬误和异样是什么?两者之间又有什么分割和区别呢?

通常来说,程序中的谬误至多包含两种,一种是语法错误,另一种则是异样。

所谓语法错误,你应该很分明,也就是你写的代码不合乎编程标准,无奈被辨认与执行,比方上面这个例子:

if name is not None
    print(name)

If 语句漏掉了冒号,不合乎 Python 的语法标准,所以程序就会报错 invalid syntax

而异样则是指程序的语法正确,也能够被执行,但在执行过程中遇到了谬误,抛出了异样,比方上面的 3 个例子:

10 / 0
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ZeroDivisionError: integer division or modulo by zero

order * 2
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'order' is not defined

1 + [1, 2]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'int' and 'list'

它们语法完全正确,但显然,咱们不能做除法时让分母为 0;也不能应用未定义的变量做运算;而让一个整型和一个列表相加也是不可取的。

于是,当程序运行到这些中央时,就抛出了异样,并且终止运行。例子中的 ZeroDivisionError NameErrorTypeError,就是三种常见的异样类型。

当然,Python 中还有很多其余异样类型,比方 KeyError 是指字典中的键找不到;FileNotFoundError 是指发送了读取文件的申请,但相应的文件不存在等等,我在此不一一赘述,你能够自行参考 相应文档。

如何解决异样

刚刚讲到,如果执行到程序中某处抛出了异样,程序就会被终止并退出。你可能会问,那有没有什么方法能够不终止程序,让其照样运行上来呢?答案当然是必定的,这也就是咱们所说的异样解决,通常应用 try 和 except 来解决,比方:

try:
    s = input('please enter two numbers separated by comma:')
    num1 = int(s.split(',')[0].strip())
    num2 = int(s.split(',')[1].strip())
    ...
except ValueError as err:
    print('Value Error: {}'.format(err))

print('continue')
...

这里默认用户输出以逗号相隔的两个整形数字,将其提取后,做后续的操作(留神 input 函数会将输出转换为字符串类型)。如果咱们输出 a,b,程序便会抛出异样 invalid literal for int() with base 10: 'a',而后跳出 try 这个 block。

因为程序抛出的异样类型是 ValueError,和 except block 所 catch 的异样类型相匹配,所以 except block 便会被执行,最终输入 Value Error: invalid literal for int() with base 10: 'a',并打印出 continue

please enter two numbers separated by comma: a,b
Value Error: invalid literal for int() with base 10: 'a'
continue

咱们晓得,except block 只承受与它相匹配的异样类型并执行,如果程序抛出的异样并不匹配,那么程序照样会终止并退出。

所以,还是刚刚这个例子,如果咱们只输出 1,程序抛出的异样就是 IndexError: list index out of range,与 ValueError 不匹配,那么 except block 就不会被执行,程序便会终止并退出(continue 不会被打印)。

please enter two numbers separated by comma: 1
IndexError Traceback (most recent call last)
IndexError: list index out of range

不过,很显然,这样强调一种类型的写法有很大的局限性。那么,该怎么解决这个问题呢?

其中一种解决方案,是在 except block 中退出多种异样的类型,比方上面这样的写法:

try:
    s = input('please enter two numbers separated by comma:')
    num1 = int(s.split(',')[0].strip())
    num2 = int(s.split(',')[1].strip())
    ...
except (ValueError, IndexError) as err:
    print('Error: {}'.format(err))

print('continue')
...

或者第二种写法:

try:
    s = input('please enter two numbers separated by comma:')
    num1 = int(s.split(',')[0].strip())
    num2 = int(s.split(',')[1].strip())
    ...
except ValueError as err:
    print('Value Error: {}'.format(err))
except IndexError as err:
    print('Index Error: {}'.format(err))

print('continue')
...

这样,每次程序执行时,except block 中只有有一个 exception 类型与理论匹配即可。

不过,很多时候,咱们很难保障程序笼罩所有的异样类型,所以,更通常的做法,是在最初一个 except block,申明其解决的异样类型是 Exception。Exception 是其余所有非零碎异样的基类,可能匹配任意非零碎异样。那么这段代码就能够写成上面这样:

try:
    s = input('please enter two numbers separated by comma:')
    num1 = int(s.split(',')[0].strip())
    num2 = int(s.split(',')[1].strip())
    ...
except ValueError as err:
    print('Value Error: {}'.format(err))
except IndexError as err:
    print('Index Error: {}'.format(err))
except Exception as err:
    print('Other error: {}'.format(err))

print('continue')
...

或者,你也能够在 except 前面省略异样类型,这示意与任意异样相匹配(包含零碎异样等):

try:
    s = input('please enter two numbers separated by comma:')
    num1 = int(s.split(',')[0].strip())
    num2 = int(s.split(',')[1].strip())
    ...
except ValueError as err:
    print('Value Error: {}'.format(err))
except IndexError as err:
    print('Index Error: {}'.format(err))
except:
    print('Other error')

print('continue')
...

须要留神,当程序中存在多个 except block 时,最多只有一个 except block 会被执行。换句话说,如果多个 except 申明的异样类型都与理论相匹配,那么只有最后面的 except block 会被执行,其余则被疏忽。

异样解决中,还有一个很常见的用法是 finally,常常和 try、except 放在一起来用。无论产生什么状况,finally block 中的语句都会被执行,哪怕后面的 try 和 excep block 中应用了 return 语句。

一个常见的利用场景,便是文件的读取:

import sys
try:
    f = open('file.txt', 'r')
    .... # some data processing
except OSError as err:
    print('OS error: {}'.format(err))
except:
    print('Unexpected error:', sys.exc_info()[0])
finally:
    f.close()

这段代码中,try block 尝试读取 file.txt 这个文件,并对其中的数据进行一系列的解决,到最初,无论是读取胜利还是读取失败,程序都会执行 finally 中的语句——敞开这个文件流,确保文件的完整性。因而,在 finally 中,咱们通常会放一些 无论如何都要执行 的语句。

值得一提的是,对于文件的读取,咱们也经常应用 with open,你兴许在后面的例子中曾经看到过,with open 会在最初主动敞开文件,让语句更加简洁。

用户自定义异样

后面的例子里充斥了很多 Python 内置的异样类型,你可能会问,我能够创立本人的异样类型吗?

答案是必定是,Python 当然容许咱们这么做。上面这个例子,咱们创立了自定义的异样类型 MyInputError,定义并实现了初始化函数和 str 函数(间接 print 时调用):

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))

如果你执行上述代码块并输入,便会失去上面的后果:

error: 1 is invalid input

理论工作中,如果内置的异样类型无奈满足咱们的需要,或者为了让异样更加具体、可读,想减少一些异样类型的其余性能,咱们能够自定义所需异样类型。不过,大多数状况下,Python 内置的异样类型就足够好了。

异样的应用场景与留神点

学完了后面的基础知识,接下来咱们着重谈一下,异样的应用场景与留神点。

通常来说,在程序中,如果咱们不确定某段代码是否胜利执行,往往这个中央就须要应用异样解决。除了上述文件读取的例子,我能够再举一个例子来阐明。

大型社交网站的后盾,须要针对用户发送的申请返回相应记录。用户记录往往贮存在 key-value 构造的数据库中,每次有申请过去后,咱们拿到用户的 ID,并用 ID 查询数据库中此人的记录,就能返回相应的后果。

而数据库返回的原始数据,往往是 json string 的模式,这就须要咱们首先对 json string 进行 decode(解码),你可能很容易想到上面的办法:

import json
raw_data = queryDB(uid) # 依据用户的 id,返回相应的信息
data = json.loads(raw_data)

这样的代码是不是就足够了呢?

要晓得,在 json.loads()函数中,输出的字符串如果不合乎其标准,那么便无奈解码,就会抛出异样,因而加上异样解决十分必要。

try:
    data = json.loads(raw_data)
    ....
except JSONDecodeError as err:
    print('JSONDecodeError: {}'.format(err))

不过,有一点切记,咱们不能走向另一个极其——滥用异样解决。

比方,当你想要查找字典中某个键对应的值时,绝不能写成上面这种模式:

d = {'name': 'jason', 'age': 20}
try:
    value = d['dob']
    ...
except KeyError as err:
    print('KeyError: {}'.format(err))

诚然,这样的代码并没有 bug,然而让人看了摸不着头脑,也显得很冗余。如果你的代码中充斥着这种写法,无疑对浏览、合作来说都是阻碍。因而,对于 flow-control(流程管制)的代码逻辑,咱们个别不必异样解决。

字典这个例子,写成上面这样就很好。

if 'dob' in d:
    value = d['dob']
    ...

函数

那么,到底什么是函数,如何在 Python 程序中定义函数呢?

说白了,函数就是为了实现某一性能的代码段,只有写好当前,就能够反复利用。咱们先来看上面一个简略的例子:

def my_func(message):
    print('Got a message: {}'.format(message))

# 调用函数 my_func()
my_func('Hello World')
# 输入
Got a message: Hello World

其中:

  • def 是函数的申明;
  • my\_func 是函数的名称;
  • 括号外面的 message 则是函数的参数;
  • 而 print 那行则是函数的主体局部,能够执行相应的语句;
  • 在函数最初,你能够返回调用后果(return 或 yield),也能够不返回。

总结一下,大略是上面的这种模式:

def name(param1, param2, ..., paramN):
    statements
    return/yield value # optional

和其余须要编译的语言(比方 C 语言)不一样的是,def 是可执行语句,这意味着函数直到被调用前,都是不存在的。当程序调用函数时,def 语句才会创立一个新的函数对象,并赋予其名字。

咱们一起来看几个例子,加深你对函数的印象:

def my_sum(a, b):
    return a + b

result = my_sum(3, 5)
print(result)

# 输入
8

这里,咱们定义了 my\_sum()这个函数,它有两个参数 a 和 b,作用是相加;随后,调用 my\_sum()函数,别离把 3 和 5 赋于 a 和 b;最初,返回其相加的值,赋于变量 result,并输入失去 8。

再来看一个例子:

def find_largest_element(l):
    if not isinstance(l, list):
        print('input is not type of list')
        return
    if len(l) == 0:
        print('empty input')
        return
    largest_element = l[0]
    for item in l:
        if item > largest_element:
            largest_element = item
    print('largest element is: {}'.format(largest_element))

find_largest_element([8, 1,-3, 2, 0])

# 输入
largest element is: 8

这个例子中,咱们定义了函数 find\_largest\_element,作用是遍历输出的列表,找出最大的值并打印。因而,当咱们调用它,并传递列表 [8, 1, -3, 2, 0] 作为参数时,程序就会输入 largest element is: 8

须要留神,主程序调用函数时,必须保障这个函数此前曾经定义过,不然就会报错,比方:

my_func('hello world')
def my_func(message):
    print('Got a message: {}'.format(message))

# 输入
NameError: name 'my_func' is not defined

然而,如果咱们在函数外部调用其余函数,函数间哪个申明在前、哪个在后就无所谓,因为 def 是可执行语句,函数在调用之前都不存在,咱们只需保障调用时,所需的函数都曾经申明定义:

def my_func(message):
    my_sub_func(message) # 调用 my_sub_func()在其申明之前不影响程序执行

def my_sub_func(message):
    print('Got a message: {}'.format(message))

my_func('hello world')

# 输入
Got a message: hello world

另外,Python 函数的参数能够设定默认值,比方上面这样的写法:

def func(param = 0):
    ...

这样,在调用函数 func()时,如果参数 param 没有传入,则参数默认为 0;而如果传入了参数 param,其就会笼罩默认值。

后面说过,Python 和其余语言相比的一大特点是,Python 是 dynamically typed 的,能够承受任何数据类型(整型,浮点,字符串等等)。对函数参数来说,这一点同样实用。比方还是刚刚的 my\_sum 函数,咱们也能够把列表作为参数来传递,示意将两个列表相连接:

print(my_sum([1, 2], [3, 4]))

# 输入
[1, 2, 3, 4]

同样,也能够把字符串作为参数传递,示意字符串的合并拼接:

print(my_sum('hello', 'world'))

# 输入
hello world

当然,如果两个参数的数据类型不同,比方一个是列表、一个是字符串,两者无奈相加,那就会报错:

print(my_sum([1, 2], 'hello'))
TypeError: can only concatenate list (not "str") to list

咱们能够看到,Python 不必思考输出的数据类型,而是将其交给具体的代码去判断执行,同样的一个函数(比方这边的相加函数 my\_sum()),能够同时利用在整型、列表、字符串等等的操作中。

在编程语言中,咱们把这种行为称为 多态。这也是 Python 和其余语言,比方 Java、C 等很大的一个不同点。当然,Python 这种不便的个性,在理论应用中也会带来诸多问题。因而,必要时请你在结尾加上数据的类型查看。

Python 函数的另一大个性,是 Python 反对函数的嵌套。所谓的函数嵌套,就是指函数外面又有函数,比方:

def f1():
    print('hello')
    def f2():
        print('world')
    f2()
f1()

# 输入
hello
world

这里函数 f1()的外部,又定义了函数 f2()。在调用函数 f1()时,会先打印字符串 'hello',而后 f1()外部再调用 f2(),打印字符串 'world'。你兴许会问,为什么须要函数嵌套?这样做有什么益处呢?

其实,函数的嵌套,次要有上面两个方面的作用。

第一,函数的嵌套可能保障外部函数的隐衷。外部函数只能被内部函数所调用和拜访,不会裸露在全局作用域,因而,如果你的函数外部有一些隐衷数据(比方数据库的用户、明码等),不想裸露在外,那你就能够应用函数的的嵌套,将其封装在外部函数中,只通过内部函数来拜访。比方:

def connect_DB():
    def get_DB_configuration():
        ...
        return host, username, password
    conn = connector.connect(get_DB_configuration())
    return conn

这里的函数 get\_DB\_configuration,便是外部函数,它无奈在 connect\_DB()函数以外被独自调用。也就是说,上面这样的内部间接调用是谬误的:

get_DB_configuration()

# 输入
NameError: name 'get_DB_configuration' is not defined

咱们只能通过调用内部函数 connect\_DB()来拜访它,这样一来,程序的安全性便有了很大的进步。

第二,正当的应用函数嵌套,可能进步程序的运行效率。咱们来看上面这个例子:

def factorial(input):
    # validation check
    if not isinstance(input, int):
        raise Exception('input must be an integer.')
    if input < 0:
        raise Exception('input must be greater or equal to 0')
    ...

    def inner_factorial(input):
        if input <= 1:
            return 1
        return input * inner_factorial(input-1)
    return inner_factorial(input)

print(factorial(5))

这里,咱们应用递归的形式计算一个数的阶乘。因为在计算之前,须要查看输出是否非法,所以我写成了函数嵌套的模式,这样一来,输出是否非法就只用查看一次。而如果咱们不应用函数嵌套,那么每调用一次递归便会查看一次,这是没有必要的,也会升高程序的运行效率。

理论工作中,如果你遇到类似的状况,输出查看不是很快,还会消耗肯定的资源,那么使用函数的嵌套就十分必要了。

函数变量作用域

Python 函数中变量的作用域和其余语言相似。如果变量是在函数外部定义的,就称为局部变量,只在函数外部无效。一旦函数执行结束,局部变量就会被回收,无法访问,比方上面的例子:

def read_text_from_file(file_path):
    with open(file_path) as file:
        ...

咱们在函数外部定义了 file 这个变量,这个变量只在 read\_text\_from\_file 这个函数里无效,在函数内部则无法访问。

绝对应的,全局变量则是定义在整个文件档次上的,比方上面这段代码:

MIN_VALUE = 1
MAX_VALUE = 10
def validation_check(value):
    if value < MIN_VALUE or value > MAX_VALUE:
        raise Exception('validation check fails')

这里的 MIN\_VALUE 和 MAX\_VALUE 就是全局变量,能够在文件内的任何中央被拜访,当然在函数外部也是能够的。不过,咱们 不能在函数外部随便扭转全局变量的值。比方,上面的写法就是谬误的:

MIN_VALUE = 1
MAX_VALUE = 10
def validation_check(value):
    ...
    MIN_VALUE += 1
    ...
validation_check(5)

如果运行这段代码,程序便会报错:

UnboundLocalError: local variable 'MIN_VALUE' referenced before assignment

这是因为,Python 的解释器会默认函数外部的变量为局部变量,然而又发现局部变量 MIN\_VALUE 并没有申明,因而就无奈执行相干操作。所以,如果咱们肯定要在函数外部扭转全局变量的值,就必须加上 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
    ...

在函数 validation\_check()外部,咱们定义了和全局变量同名的局部变量 MIN\_VALUE,那么,MIN\_VALUE 在函数外部的值,就应该是 3 而不是 1 了。

相似的,对于嵌套函数来说,外部函数能够拜访内部函数定义的变量,然而无奈批改,若要批改,必须加上 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

闭包

闭包其实和刚刚讲的嵌套函数相似,不同的是,这里内部函数返回的是一个函数,而不是一个具体的值。返回的函数通常赋于一个变量,这个变量能够在前面被继续执行调用。

举个例子你就更容易了解了。比方,咱们想计算一个数的 n 次幂,用闭包能够写成上面的代码:

def nth_power(exponent):
    def exponent_of(base):
        return base ** exponent
    return exponent_of # 返回值是 exponent_of 函数

square = nth_power(2) # 计算一个数的平方
cube = nth_power(3) # 计算一个数的立方
square
# 输入
<function __main__.nth_power.<locals>.exponent(base)>

cube
# 输入
<function __main__.nth_power.<locals>.exponent(base)>

print(square(2))  # 计算 2 的平方
print(cube(2)) # 计算 2 的立方
# 输入
4 # 2^2
8 # 2^3

这里内部函数 nth\_power()返回值,是函数 exponent\_of(),而不是一个具体的数值。须要留神的是,在执行完 square = nth_power(2)cube = nth_power(3) 后,内部函数 nth\_power()的参数 exponent,依然会被外部函数 exponent\_of()记住。这样,之后咱们调用 square(2)或者 cube(2)时,程序就能顺利地输入后果,而不会报错说参数 exponent 没有定义了。

看到这里,你兴许会思考,为什么要闭包呢?下面的程序,我也能够写成上面的模式啊!

def nth_power_rewrite(base, exponent):
    return base ** exponent

的确能够,不过,要晓得,应用闭包的一个起因,是让程序变得更简洁易读。构想一下,比方你须要计算很多个数的平方,那么你感觉写成上面哪一种模式更好呢?

# 不实用闭包
res1 = nth_power_rewrite(base1, 2)
res2 = nth_power_rewrite(base2, 2)
res3 = nth_power_rewrite(base3, 2)
...

# 应用闭包
square = nth_power(2)
res1 = square(base1)
res2 = square(base2)
res3 = square(base3)
...

显然是第二种,是不是?首先直观来看,第二种模式,让你每次调用函数都能够少输出一个参数,表白更为简洁。

其次,和下面讲到的嵌套函数长处相似,函数结尾须要做一些额定工作,而你又须要屡次调用这个函数时,将那些额定工作的代码放在内部函数,就能够缩小屡次调用导致的不必要的开销,进步程序的运行效率。

另外还有一点,咱们前面会讲到,闭包经常和装璜器(decorator)一起应用。

匿名函数

首先,什么是匿名函数呢?以下是匿名函数的格局:

lambda argument1, argument2,... argumentN : expression

咱们能够看到,匿名函数的关键字是 lambda,之后是一系列的参数,而后用冒号隔开,最初则是由这些参数组成的表达式。咱们通过几个例子看一下它的用法:

square = lambda x: x**2
square(3)

9

这里的匿名函数只输出一个参数 x,输入则是输出 x 的平方。因而当输出是 3 时,输入便是 9。如果把这个匿名函数写成惯例函数的模式,则是上面这样:

def square(x):
    return x**2
square(3)

9

能够看到,匿名函数 lambda 和惯例函数一样,返回的都是一个函数对象(function object),它们的用法也极其类似,不过还是有上面几点区别。

第一,lambda 是一个表达式(expression),并不是一个语句(statement)

  • 所谓的表达式,就是用一系列“公式”去表白一个货色,比方 x + 2x**2 等等;
  • 而所谓的语句,则肯定是实现了某些性能,比方赋值语句 x = 1 实现了赋值,print 语句 print(x) 实现了打印,条件语句 if x < 0: 实现了抉择性能等等。

因而,lambda 能够用在一些惯例函数 def 不能用的中央,比方,lambda 能够用在列表外部,而惯例函数却不能:

[(lambda x: x*x)(x) for x in range(10)]
# 输入
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

再比方,lambda 能够被用作某些函数的参数,而惯例函数 def 也不能:

l = [(1, 20), (3, 0), (9, 10), (2, -1)]
l.sort(key=lambda x: x[1]) # 按列表中元组的第二个元素排序
print(l)
# 输入
[(2, -1), (3, 0), (9, 10), (1, 20)]

惯例函数 def 必须通过其函数名被调用,因而必须首先被定义。然而作为一个表达式的 lambda,返回的函数对象就不须要名字了。

第二,lambda 的主体是只有一行的简略表达式,并不能扩大成一个多行的代码块。

这其实是出于设计的思考。Python 之所以创造 lambda,就是为了让它和惯例函数各司其职:lambda 专一于简略的工作,而惯例函数则负责更简单的多行逻辑。对于这点,Python 之父 Guido van Rossum 曾发了一篇 文章 解释,你有趣味的话能够本人浏览。

为什么要应用匿名函数?

实践上来说,Python 中有匿名函数的中央,都能够被替换成等价的其余表达形式。一个 Python 程序是能够不必任何匿名函数的。不过,在一些状况下,应用匿名函数 lambda,能够帮忙咱们大大简化代码的复杂度,进步代码的可读性。

通常,咱们用函数的目标无非是这么几点:

  1. 缩小代码的重复性;
  2. 模块化代码。

对于第一点,如果你的程序在不同中央蕴含了雷同的代码,那么咱们就会把这部分雷同的代码写成一个函数,并为它取一个名字,不便在绝对应的不同中央调用。

对于第二点,如果你的一块儿代码是为了实现一个性能,但内容十分多,写在一起升高了代码的可读性,那么通常咱们也会把这部分代码独自写成一个函数,而后加以调用。

不过,再试想一下这样的状况。你须要一个函数,但它十分简短,只须要一行就能实现;同时它在程序中只被调用一次而已。那么请问,你还须要像惯例函数一样,给它一个定义和名字吗?

答案当然是否定的。这种状况下,函数就能够是匿名的,你只须要在适当的中央定义并应用,就能让匿名函数发挥作用了。

举个例子,如果你想对一个列表中的所有元素做平方操作,而这个操作在你的程序中只须要进行一次,用 lambda 函数能够示意成上面这样:

squared = map(lambda x: x**2, [1, 2, 3, 4, 5])

如果用惯例函数,则示意为这几行代码:

def square(x):
    return x**2

squared = map(square, [1, 2, 3, 4, 5])

这里我简略解释一下。函数 map(function, iterable)的第一个参数是函数对象,第二个参数是一个能够遍历的汇合,它示意对 iterable 的每一个元素,都使用 function 这个函数。两者一比照,咱们很显著地发现,lambda 函数让代码更加简洁明了。

再举一个例子,在 Python 的 Tkinter GUI 利用中,咱们想实现这样一个简略的性能:创立显示一个按钮,每当用户点击时,就打印出一段文字。如果应用 lambda 函数能够示意成上面这样:

from tkinter import Button, mainloop
button = Button(
    text='This is a button',
    command=lambda: print('being pressed')) # 点击时调用 lambda 函数
button.pack()
mainloop()

而如果咱们用惯例函数 def,那么须要写更多的代码:

from tkinter import Button, mainloop

def print_message():
    print('being pressed')

button = Button(
    text='This is a button',
    command=print_message) # 点击时调用 lambda 函数
button.pack()
mainloop()

显然,使用匿名函数的代码简洁很多,也更加合乎 Python 的编程习惯。

Python 函数式编程

最初,咱们一起来看一下,Python 的函数式编程个性,这与咱们明天所讲的匿名函数 lambda,有着亲密的分割。

所谓函数式编程,是指代码中每一块都是不可变的(immutable),都由纯函数(pure function)的模式组成。这里的纯函数,是指函数自身互相独立、互不影响,对于雷同的输出,总会有雷同的输入,没有任何副作用。

举个很简略的例子,比方对于一个列表,我想让列表中的元素值都变为原来的两倍,咱们能够写成上面的模式:

def multiply_2(l):
    for index in range(0, len(l)):
        l[index] *= 2
    return l

这段代码就不是一个纯函数的模式,因为列表中元素的值被扭转了,如果我屡次调用 multiply\_2()这个函数,那么每次失去的后果都不一样。要想让它成为一个纯函数的模式,就得写成上面这种模式,从新创立一个新的列表并返回。

def multiply_2_pure(l):
    new_list = []
    for item in l:
        new_list.append(item * 2)
    return new_list

函数式编程的长处,次要在于其纯函数和不可变的个性使程序更加强壮,易于调试(debug)和测试;毛病次要在于限度多,难写。当然,Python 不同于一些语言(比方 Scala),它并不是一门函数式编程语言,不过,Python 也提供了一些函数式编程的个性,值得咱们理解和学习。

Python 次要提供了这么几个函数:map()、filter()和 reduce(),通常联合匿名函数 lambda 一起应用。这些都是你须要把握的货色,接下来我逐个介绍。

首先是 map(function, iterable)函数,后面的例子提到过,它示意,对 iterable 中的每个元素,都使用 function 这个函数,最初返回一个新的可遍历的汇合。比方方才列表的例子,要对列表中的每个元素乘以 2,那么用 map 就能够示意为上面这样:

l = [1, 2, 3, 4, 5]
new_list = map(lambda x: x * 2, l) # [2,4,6,8,10]

咱们能够以 map()函数为例,看一下 Python 提供的函数式编程接口的性能。还是同样的列表例子,它还能够用 for 循环和 list comprehension(目前没有对立中文叫法,你也能够直译为列表了解等)实现,咱们来比拟一下它们的速度:

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()是最快的。因为 map()函数间接由 C 语言写的,运行时不须要通过 Python 解释器间接调用,并且外部做了诸多优化,所以运行速度最快。

接下来来看 filter(function, iterable)函数,它和 map 函数相似,function 同样示意一个函数对象。filter()函数示意对 iterable 中的每个元素,都应用 function 判断,并返回 True 或者 False,最初将返回 True 的元素组成一个新的可遍历的汇合。

举个例子,比方我要返回一个列表中的所有偶数,能够写成上面这样:

l = [1, 2, 3, 4, 5]
new_list = filter(lambda x: x % 2 == 0, l) # [2, 4]

最初咱们来看 reduce(function, iterable)函数,它通常用来对一个汇合做一些累积操作。

function 同样是一个函数对象,规定它有两个参数,示意对 iterable 中的每个元素以及上一次调用后的后果,使用 function 进行计算,所以最初返回的是一个独自的数值。

举个例子,我想要计算某个列表元素的乘积,就能够用 reduce()函数来示意:

l = [1, 2, 3, 4, 5]
product = reduce(lambda x, y: x * y, l) # 1*2*3*4*5 = 120

当然,相似的,filter()和 reduce()的性能,也能够用 for 循环或者 list comprehension 来实现。

通常来说,在咱们想对汇合中的元素进行一些操作时,如果操作非常简单,比方相加、累积这种,那么咱们优先思考 map()、filter()、reduce()这类或者 list comprehension 的模式。至于这两种形式的抉择:

  • 在数据量十分多的状况下,比方机器学习的利用,那咱们个别更偏向于函数式编程的示意,因为效率更高;
  • 在数据量不多的状况下,并且你想要程序更加 Pythonic 的话,那么 list comprehension 也不失为一个好抉择。

不过,如果你要对汇合中的元素,做一些比较复杂的操作,那么,思考到代码的可读性,咱们通常会应用 for 循环,这样更加清晰明了。

模块化

说到最简略的模块化形式,你能够把函数、类、常量拆分到不同的文件,把它们放在同一个文件夹,而后应用 from your_file import function_name, class_name 的形式调用。之后,这些函数和类就能够在文件内间接应用了。

# utils.py

def get_sum(a, b):
    return a + b
# class_utils.py

class Encoder(object):
    def encode(self, s):
        return s[::-1]

class Decoder(object):
    def decode(self, s):
        return ''.join(reversed(list(s)))
# main.py

from utils import get_sum
from class_utils import *

print(get_sum(1, 2))

encoder = Encoder()
decoder = Decoder()

print(encoder.encode('abcde'))
print(decoder.decode('edcba'))

########## 输入 ##########

3
edcba
abcde

咱们来看这种形式的代码:get\_sum() 函数定义在 utils.py,Encoder 和 Decoder 类则在 class\_utils.py,咱们在 main 函数间接调用 from import,就能够将咱们须要的货色 import 过去。

非常简单。

然而这就足够了吗?当然不,缓缓地,你会发现,所有文件都堆在一个文件夹下也并不是方法。

于是,咱们试着建一些子文件夹:

# utils/utils.py

def get_sum(a, b):
    return a + b
# utils/class_utils.py

class Encoder(object):
    def encode(self, s):
        return s[::-1]

class Decoder(object):
    def decode(self, s):
        return ''.join(reversed(list(s)))
# src/sub_main.py

import sys
sys.path.append("..")

from utils.class_utils import *

encoder = Encoder()
decoder = Decoder()

print(encoder.encode('abcde'))
print(decoder.decode('edcba'))

########## 输入 ##########

edcba
abcde

而这一次,咱们的文件构造是上面这样的:

.
├── utils
│   ├── utils.py
│   └── class_utils.py
├── src
│   └── sub_main.py
└── main.py

很容易看出,main.py 调用子目录的模块时,只须要应用 . 代替 / 来示意子目录,utils.utils 示意 utils 子文件夹下的 utils.py 模块就行。

那如果咱们想调用下层目录呢?留神,sys.path.append("..") 示意将以后程序所在位置 向上 提了一级,之后就能调用 utils 的模块了。

同时要留神一点,import 同一个模块只会被执行一次,这样就能够避免反复导入模块呈现问题。当然,良好的编程习惯应该杜绝代码屡次导入的状况。在 Facebook 的编程标准中,除了一些极其非凡的状况,import 必须位于程序的最前端

最初我想再提一下版本区别。你可能在许多教程中看到过这样的要求:咱们还须要在模块所在的文件夹新建一个 __init__.py,内容能够为空,也能够用来表述包对外裸露的模块接口。不过,事实上,这是 Python 2 的标准。在 Python 3 标准中,__init__.py 并不是必须的,很多教程里没提过这一点,或者没讲明确,我心愿你还是能留神到这个中央。

整体而言,这就是最简略的模块调用形式了。在我初用 Python 时,这种形式曾经足够我实现大学期间的我的项目了,毕竟,很多学校我的项目的文件数只有个位数,每个文件代码也只有几百行,这种组织形式能帮我顺利完成工作。

然而在我来到 Facebook 后,我发现,一个项目组的 workspace 可能有上千个文件,有几十万到几百万行代码。这种调用形式曾经齐全不够用了,学会新的组织形式火烧眉毛。

接下来,咱们就零碎学习下,模块化的迷信组织形式。

我的项目模块化

咱们先来回顾下相对路径和绝对路径的概念。

在 Linux 零碎中,每个文件都有一个绝对路径,以 / 结尾,来示意从根目录到叶子节点的门路,例如 /home/ubuntu/Desktop/my_project/test.py,这种示意办法叫作绝对路径。

另外,对于任意两个文件,咱们都有一条通路能够从一个文件走到另一个文件,例如 /home/ubuntu/Downloads/example.json。再如,咱们从 test.py 拜访到 example.json,须要写成 '../../Downloads/example.json',其中 .. 示意上一层目录。这种示意办法,叫作相对路径。

通常,一个 Python 文件在运行的时候,都会有一个运行时地位,最开始时即为这个文件所在的文件夹。当然,这个运行门路当前能够被扭转。运行 sys.path.append(".."),则能够扭转以后 Python 解释器的地位。不过,一般而言我并不举荐,固定一个确定门路对大型工程来说是十分必要的。

理分明这些概念后,咱们就很容易搞懂,我的项目中如何设置模块的门路。

首先,你会发现,绝对地位是一种很不好的抉择。因为代码可能会迁徙,绝对地位会使得重构既不雅观,也易出错。因而,在大型工程中尽可能应用相对地位是第一要义。对于一个独立的我的项目,所有的模块的追寻形式,最好从我的项目的根目录开始追溯,这叫做绝对的绝对路径。

事实上,在 Facebook 和 Google,整个公司都只有一个代码仓库,全公司的代码都放在这个库里。我刚退出 Facebook 时对此感到很困惑,也很离奇,难免会有些放心:

  • 这样做仿佛会增大项目管理的复杂度吧?
  • 是不是也会有不同组代码隐衷泄露的危险呢?

起初,随着工作的深刻,我才发现了这种代码仓库独有的几个长处。

第一个长处,简化依赖治理。整个公司的代码模块,都能够被你写的任何程序所调用,而你写的库和模块也会被其他人调用。调用的形式,都是从代码的根目录开始索引,也就是后面提到过的绝对的绝对路径。这样极大地提高了代码的分享共用能力,你不须要反复造轮子,只须要在写之前,去搜一下有没有曾经实现好的包或者框架就能够了。

第二个长处,版本对立。不存在应用了一个新模块,却导致一系列函数解体的状况;并且所有的降级都须要通过单元测试才能够持续。

第三个长处,代码追溯。你能够很容易追溯,一个 API 是从哪里被调用的,它的历史版本是怎么迭代开发,产生变动的。

如果你有趣味,能够参考这篇论文:https://cacm.acm.org/magazines/2016/7/204032-why-google-stores-billions-of-lines-of-code-in-a-single-repository/fulltext

在做我的项目的时候,尽管你不可能把全世界的代码都放到一个文件夹下,然而相似模块化的思维还是要有的——那就是以我的项目的根目录作为最根本的目录,所有的模块调用,都要通过根目录一层层向下索引的形式来 import。

明确了这一点后,这次咱们应用 PyCharm 来创立一个我的项目。这个我的项目构造如下所示:

.
├── proto
│   ├── mat.py
├── utils
│   └── mat_mul.py
└── src
    └── main.py
# proto/mat.py

class Matrix(object):
    def __init__(self, data):
        self.data = data
        self.n = len(data)
        self.m = len(data[0])
# utils/mat_mul.py

from proto.mat import Matrix

def mat_mul(matrix_1: Matrix, matrix_2: Matrix):
    assert matrix_1.m == matrix_2.n
    n, m, s = matrix_1.n, matrix_1.m, matrix_2.m
    result = [[0 for _ in range(n)] for _ in range(s)]
    for i in range(n):
        for j in range(s):
            for k in range(m):
                result[i][k] += matrix_1.data[i][j] * matrix_2.data[j][k]

    return Matrix(result)
# src/main.py

from proto.mat import Matrix
from utils.mat_mul import mat_mul

a = Matrix([[1, 2], [3, 4]])
b = Matrix([[5, 6], [7, 8]])

print(mat_mul(a, b).data)

########## 输入 ##########

[[19, 22], [43, 50]]

这个例子和后面的例子长得很像,但请留神 utils/mat_mul.py,你会发现,它 import Matrix 的形式是 from proto.mat。这种做法,间接从我的项目根目录中导入,并顺次向下导入模块 mat.py 中的 Matrix,而不是应用 .. 导入上一级文件夹。

是不是很简略呢?对于接下来的所有我的项目,你都能间接应用 Pycharm 来构建。把不同模块放在不同子文件夹里,跨模块调用则是从顶层间接索引,一步到位,十分不便。

我猜,这时你的好奇心来了。你尝试应用命令行进入 src 文件夹,间接输出 Python main.py,报错,找不到 proto。你不甘心,退回到上一级目录,输出 Python src/main.py,持续报错,找不到 proto。

Pycharm 用了什么黑魔法呢?

实际上,Python 解释器在遇到 import 的时候,它会在一个特定的列表中寻找模块。这个特定的列表,能够用上面的形式拿到:

import sys

print(sys.path)

########## 输入 ##########

['','/usr/lib/python36.zip','/usr/lib/python3.6','/usr/lib/python3.6/lib-dynload','/usr/local/lib/python3.6/dist-packages','/usr/lib/python3/dist-packages']

请留神,它的第一项为空。其实,Pycharm 做的一件事,就是将第一项设置为我的项目根目录的相对地址。这样,每次你无论怎么运行 main.py,import 函数在执行的时候,都会去我的项目根目录中找相应的包。

你说,你想批改下,使得一般的 Python 运行环境也能做到?这里有两种办法能够做到:

import sys

sys.path[0] = '/home/ubuntu/workspace/your_projects'

第一种办法,“鼎力出奇观”,咱们能够强行批改这个地位,这样,你的 import 接下来必定就畅通无阻了。但这显然不是最佳解决方案,把绝对路径写到代码里,是我十分不举荐的形式(你能够写到配置文件中,但找配置文件也须要门路寻找,于是就会进入无解的死循环)。

第二种办法,是批改 PYTHONHOME。这里我略微提一下 Python 的 Virtual Environment(虚构运行环境)。Python 能够通过 Virtualenv 工具,十分不便地创立一个全新的 Python 运行环境。

事实上,咱们提倡,对于每一个我的项目来说,最好要有一个独立的运行环境来放弃包和模块的污浊性。更深的内容超出了明天的范畴,你能够本人查资料理解。

回到第二种批改办法上。在一个 Virtual Environment 里,你能找到一个文件叫 activate,在这个文件的开端,填上上面的内容:

export PYTHONPATH="/home/ubuntu/workspace/your_projects"

这样,每次你通过 activate 激活这个运行时环境的时候,它就会主动将我的项目的根目录增加到搜寻门路中去。

神奇的 if __name__ == '__main__'

最初一部分,咱们再来讲讲 if __name__ == '__main__',这个咱们常常看到的写法。

Python 是脚本语言,和 C++、Java 最大的不同在于,不须要显式提供 main() 函数入口。如果你有 C++、Java 等语言教训,应该对 main() {} 这样的构造很相熟吧?

不过,既然 Python 能够间接写代码,if __name__ == '__main__' 这样的写法,除了能让 Python 代码更好看(更像 C++)外,还有什么益处吗?

我的项目构造如下:

.
├── utils.py
├── utils_with_main.py
├── main.py
└── main_2.py
# utils.py

def get_sum(a, b):
    return a + b

print('testing')
print('{} + {} = {}'.format(1, 2, get_sum(1, 2)))
# utils_with_main.py

def get_sum(a, b):
    return a + b

if __name__ == '__main__':
    print('testing')
    print('{} + {} = {}'.format(1, 2, get_sum(1, 2)))
# main.py

from utils import get_sum

print('get_sum:', get_sum(1, 2))

########## 输入 ##########

testing
1 + 2 = 3
get_sum: 3
# main_2.py

from utils_with_main import get_sum

print('get_sum:', get_sum(1, 2))

########## 输入 ##########

get_sum_2: 3

看到这个我的项目构造,你就很清晰了吧。

import 在导入文件的时候,会主动把所有裸露在里面的代码全都执行一遍。因而,如果你要把一个货色封装成模块,又想让它能够执行的话,你必须将要执行的代码放在 if __name__ == '__main__' 上面。

为什么呢?其实,__name__ 作为 Python 的魔术内置参数,实质上是模块对象的一个属性。咱们应用 import 语句时,__name__ 就会被赋值为该模块的名字,天然就不等于 __main__ 了。更深的原理我就不做过多介绍了,你只须要明确这个知识点即可。

总结

Python 的根底篇,咱们先是讲了对于列表和元组,列表和元组都是有序的,能够存储任意数据类型的汇合,区别次要在于上面这两点。

  • 列表是动静的,长度可变,能够随便的减少、删减或扭转元素。列表的存储空间略大于元组,性能略逊于元组。
  • 元组是动态的,长度大小固定,不能够对元素进行减少、删减或者扭转操作。元组绝对于列表更加轻量级,性能稍优。

紧接着咱们一起学习了字典和汇合的基本操作,并对它们的高性能和外部存储构造进行了解说。

字典在 Python3.7+ 是有序的数据结构,而汇合是无序的,其外部的哈希表存储构造,保障了其查找、插入、删除操作的高效性。所以,字典和汇合通常使用在对元素的高效查找、去重等场景。

而后咱们学习了 Python 字符串的一些基本知识和罕用操作,并且联合具体的例子与场景加以阐明,特地须要留神上面几点。

  • Python 中字符串应用单引号、双引号或三引号示意,三者意义雷同,并没有什么区别。其中,三引号的字符串通常用在多行字符串的场景。
  • Python 中字符串是不可变的(后面所讲的新版本 Python 中拼接操作’+=’ 是个例外)。因而,随便扭转字符串中字符的值,是不被容许的。
  • Python 新版本(2.5+)中,字符串的拼接变得比以前高效了许多,你能够放心使用。
  • Python 中字符串的格式化(string.format)经常用在输入、日志的记录等场景。

咱们还一起学习了条件与循环的基本概念、进阶用法以及相应的利用。这里,我重点强调几个易错的中央。

  • 在条件语句中,if 能够独自应用,然而 elif 和 else 必须和 if 同时搭配应用;而 If 条件语句的判断,除了 boolean 类型外,其余的最好显示进去。
  • 在 for 循环中,如果须要同时拜访索引和元素,你能够应用 enumerate()函数来简化代码。
  • 写条件与循环时,正当利用 continue 或者 break 来防止简单的嵌套,是非常重要的。
  • 要留神条件与循环的复用,简略性能往往能够用一行间接实现,极大地提高代码品质与效率。

而后介绍了 Python 的一般 I/ O 和文件 I/O,同时理解了 JSON 序列化的基本知识,并通过具体的例子进一步把握。再次强调一下须要留神的几点:

  • I/O 操作需谨慎,肯定要进行充沛的错误处理,并仔细编码,防止出现编码破绽;
  • 编码时,对内存占用和磁盘占用要有充沛的预计,这样在出错时能够更容易找到起因;
  • JSON 序列化是很不便的工具,要联合实战多多练习;
  • 代码尽量简洁、清晰

对于 Python 程序的异样解决,你须要留神以下几点:

  • 异样,通常是指程序运行的过程中遇到了谬误,终止并退出。咱们通常应用 try except 语句去解决异样,这样程序就不会被终止,仍能继续执行。
  • 解决异样时,如果有必须执行的语句,比方文件关上后必须敞开等等,则能够放在 finally block 中。
  • 异样解决,通常用在你不确定某段代码是否胜利执行,也无奈轻易判断的状况下,比方数据库的连贯、读取等等。失常的 flow-control 逻辑,不要应用异样解决,间接用条件语句解决就能够了。

对于 Python 的函数咱们理解了 Python 函数的概念及其利用,有这么几点你须要留神:

  1. Python 中函数的参数能够承受任意的数据类型,应用起来须要留神,必要时请在函数结尾退出数据类型的查看;
  2. 和其余语言不同,Python 中函数的参数能够设定默认值;
  3. 嵌套函数的应用,能保证数据的隐衷性,进步程序运行效率;
  4. 正当地应用闭包,则能够简化程序的复杂度,进步可读性。
  5. 匿名函数 lambda,它的主要用途是缩小代码的复杂度。须要留神的是 lambda 是一个表达式,并不是一个语句;它只能写成一行的表达形式,语法上并不反对多行。
  6. 匿名函数通常的应用场景是:程序中须要应用一个函数实现一个简略的性能,并且该函数只调用一次。

其次,咱们也入门了 Python 的函数式编程,次要理解了常见的 map(),fiilter()和 reduce()三个函数,并比拟了它们与其余模式(for 循环,comprehension)的性能,显然,它们的性能效率是最优的。

最初如何应用 Python 来构建模块化和大型工程。这里须要强调几点:

  1. 通过绝对路径和相对路径,咱们能够 import 模块;
  2. 在大型工程中模块化十分重要,模块的索引要通过绝对路径来做,而绝对路径从程序的根目录开始;
  3. 应用用 if __name__ == '__main__' 来避开 import 时执行。

本文由 mdnice 多平台公布

退出移动版