You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

37 KiB

Python

基础

1. 列表和元组

  • 列表和元组,都是一个可以放置任意数据类型有序集合
l = [1, 2, 'hello', 'world'] # 列表中同时含有 int 和 string 类型的元素

tup = ('jason', 22) # 元组中同时含有 int 和 string 类型的元素
  • 列表是动态的,长度大小不固定,可以随意地增加、删减或者改变元素(mutable)。
  • 元组是静态的,长度大小固定,无法增加删减或者改变(immutable)。
l = [1, 2, 3, 4]
l[3] = 40 # 和很多语言类似python 中索引同样从 0 开始l[3] 表示访问列表的第四个元素
[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 
  • 想增加一个元素给元组,实际上就是创建了一个新的元组,然后把原来两个元组的值依次填充进去。
# 元组
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 中的列表和元组都支持负数索引,-N 表示倒数第几个元素, 从1开始
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 的子列表, 从0开始, 取头不取尾
[2, 3]

tup = (1, 2, 3, 4)
tup[1:3] # 返回元组中索引从 1 到 2 的子元组, 从0开始, 取头不取尾
(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]

# count(item) 表示统计列表 / 元组中 item 出现的次数。
l.count(3) 
2

# index(item) 表示返回列表 / 元组中 item 第一次出现的索引从0开始。
l.index(7)
3

# list.reverse() 和 list.sort() 分别表示原地倒转列表和排序(注意,元组没有内置的这两个函数)。
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)

# count(item) 表示统计列表 / 元组中 item 出现的次数。
tup.count(3) 
2

# index(item) 表示返回列表 / 元组中 item 第一次出现的索引从0开始。
tup.index(7) 
3

# reversed() 和 sorted() 同样表示对列表 / 元组进行倒转和排序,
# 但是会返回一个倒转后 或者排好序的新的列表 / 元组。
list(reversed(tup))
[1, 8, 7, 3, 2, 3]
sorted(tup)
[1, 2, 3, 3, 7, 8]
  • 列表和元组存储方式的差异
  • 由于列表是动态的,所以它需要存储指针,来指向对应的元素(对于 int 型8 字节)
  • 由于列表可变,所以需要额外存储已经分配的长度大小(8字节)
  • 这样才可以实时追踪列表空间的使用情况,当空间不足时,及时分配额外空间
l = [1, 2, 3]
l.__sizeof__()
64

# ------------------
tup = (1, 2, 3)
tup.__sizeof__()
48
  • PS: 列表详细的分配过程

l = []
# 1. 空列表的存储空间为 40 字节
l.__sizeof__() 
40

# 2. 加入了元素 1 之后,列表为其分配了可以存储 4 个元素的空间 4 * 8
l.append(1)
l.__sizeof__()
72  

# 3. 由于之前分配了空间,所以加入元素 2列表空间不变
l.append(2)
l.__sizeof__()
72 

# 4. 同上
l.append(3)
l.__sizeof__()
72 
l.append(4)
l.__sizeof__()
72 

# 5. 加入元素 5 之后,列表的空间不足,所以又额外分配了可以存储 4 个元素的空间 4 * 8
l.append(5)
l.__sizeof__()
104 
  • 为了减小每次增加 / 删减 操作时空间分配的开销Python 每次分配空间时都会额外多分配一些, 这样的机制 (over-allocating)保证了其操作的高效性:增加 / 删除的时间复杂度均为 O(1)
  • 元组长度大小固定,元素不可变,所以存储空间固定

  • 列表和元组的性能
  • 元组要比列表更加轻量级一些, 元组的性能速度要略优于列表
  • Python 会在后台,对静态数据做一些资源缓存(resource caching)。通常来说, 因为垃圾回收机制的存在如果一些变量不被使用了Python 就会回收它们所占用的内存,返还给操作系统,以便其他变量或其他应用使用。
  • 但是对于一些静态变量比如元组如果它不被使用并且占用空间不大时Python 会暂时缓存这部分内存。 这样下次我们再创建同样大小的元组时Python 就可以不用再向操作 系统发出请求,去寻找内存,而是可以直接分配之前缓存的内存空间,这样就能大大加快程序的运行速度。
  • 计算初始化一个相同元素的列表和元组分别所需的时间
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
  • 元组的初始化速度,要比列表快 5 倍
  • 如果是索引操作的话,两者的速度差别非常小,几乎可以忽略不计。
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
  • 增加、删减或者改变元素,那么列表显然更优, 对于元组,你必须得通过新建一个元组来完成

  • 列表和元组的使用场景
  • 如果存储的数据和数量不变,比如你有一个函数,需要返回的是一个地点的经纬度,然 后直接传给前端渲染,那么肯定选用元组更合适。
def get_location():
    .....
    return (longitude, latitude)
  • 如果存储的数据或数量是可变的,比如社交平台上的一个日志功能,是统计一个用户在 一周之内看了哪些用户的帖子,那么则用列表更合适。
viewer_owner_id_list = [] # 里面的每个元素记录了这个 viewer 一周内看过的所有 owner 的 id
records = queryDB(viewer_id) # 索引数据库,拿到某个 viewer 一周内的日志
for record in records:
    viewer_owner_id_list.append(record.id)

2. 字典和集合

  • 字典是一系列由键(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 中字典和集合,无论是键还是值,都可以是混合类型
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'
  • 集合并不支持索引操作,因为集合本质上是一个哈希表,和列表不一样。
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 11 
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'} 6 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 会重新获取更大的内存空间, 扩充哈希表。不过,这种情况下,表内所有的元素位置都会被重新排放。

3. 深入浅出字符串

  • 字符串基础
  • 字符串是由独立字符组成的一个序列,通常包含在单引号('')双引号("")或者三引号之中(''' '''或""" """,两者一样),比如下面几种写法。
name = 'jason'
city = 'beijing'
text = "welcome to jike shijian"

  • Python 中单引号、双引号和三引号的字符串是一模一样的,没有区别
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
        """
  • PS 这个注释的模板很好

  • Python 也支持转义字符。所谓的转义字符,就是用反斜杠开头的字符串,来表示一些特定意义的字符。 深入字符串1

  • 在转义字符的应用中,最常见的就是换行符'\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',我们可以采用下面的做法:
# 第一种方法,是直接用大写的'H',通过加号'+'操作符,与原字符串切片操作的子字符串拼接而成新的字符串。
s = 'H' + s[1:]
# 第二种方法,是直接扫描原字符串,把小写的'h'替换成大写的'H',得到新的字符串。
s = s.replace('h', 'H')
  • 着重讲解一下,使用加法操作符'+='的字符串拼接方法。因为它是一个例外,打破了字符串不可变的特性。
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 += str2Python 首先会检测 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 函数,毕竟这是最新规范,也是官方文档推荐的规范。

  • PS: Python要想玩好, 一定要学会 更加 pythonic更加高效的办法来编写代码

  • 举例: 两个字符串拼接操作

  • 1

s = ''
for n in range(0, 100000):
    s += str(n)
  • 2
l = []
for n in range(0, 100000):
    l.append(str(n))
    
s = ' '.join(l)
  • 3 pythonic更加高效的办法
s = " ".join(map(str, range(0, 10000)))

4. Python “黑箱”:输入与输出

  • 输入输出基础
  • 最简单直接的输入来自键盘操作,比如下面这个例子。
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危重安全漏洞

  • 虽然输入输出和类型处理事情简单,但我们一定要慎之又慎。毕竟相当比例的安全漏洞,都来自随意的 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自然语言处理任务。
  • 基本步骤,也就是下面的四步:
    • 读取文件;
    • 去除所有标点符号和换行符,并把所有大写变成小写;
    • 合并相同的词,统计每个词出现的频率,并按照词频从大到小排序;
    • 将结果按行输出到文件 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这样打开的文件如果需要写入会从原始文件的最末尾开始写入。

  • 在拿到指针后,我们可以通过 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 ,正能解决这个场景。你可以把它简单地理解为两种黑箱:

    • 第一种,输入这些杂七杂八的信息,比如 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
  • 相比于 JSON它的优点是生成优化后的二进制文件因此性能更好。但与此同时生成的二进制序列是不能直接阅读的。它在 TensorFlow 等很多对性能有要求的系统中都有广泛的应用。

5. 修炼基本功:条件与循环

6. 异常处理:如何提高程序的稳定性?

高级

  • 装饰器
  • 并发编程

规范

  • Jupyter Notebook 使用

实战

  • 办公
  • 量化交易
  • 爬虫