Python核心技术与实战:学习笔记(一)

03|列表和元组,到底用哪一个?

列表是动态的,长度大小不固定,可以随意地增加、删减或者改变元素(mutable)。 而元组是静态的,长度大小固定,无法增加删减或者改变(immutable)。

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

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 个元素的空间
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

04 | 字典、集合,你真的了解吗?

集合并不支持索引操作,因为集合本质上是一个哈希表,和列表不一 样。

#字典和集合的创建方式
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')
print(d1 == d2 == d3 ==d4)
# True
s1 = {1, 2, 3}
s2 = set([1, 2, 3])
print(s2)
# {1, 2, 3}
print(s1 == s2)
# True

字典访问可以直接索引键,如果不存在,就会抛出异常;也可以使用 get(key, default) 函数来进行索引。如果键不存在,调用 get() 函数可以返回 一个默认值。

集合并不支持索引操作,因为集合本质上是一个哈希表,和列表不一 样。

想要判断一个元素在不在字典或集合内,我们可以用 value in dict/set 来判断。

插入操作

每次向字典或集合插入一个元素时,Python 会首先计算键的哈希值(hash(key)),再和 mask = PyDicMinSize - 1 做与操作,计算这个元素应该插入哈希表的位置 index = hash(key) & mask。如果哈希表中此位置是空的,那么这个元素就会被插入其中。 而如果此位置已被占用,Python 便会比较两个元素的哈希值和键是否相等。若两者都相等,则表明这个元素已经存在,如果值不同,则更新值。 若两者中有一个不相等,这种情况我们通常称为哈希冲突(hash collision),意思是两 个元素的键不相等,但是哈希值相等。这种情况下,Python 便会继续寻找表中空余的位置,直到找到位置为止。

值得一提的是,通常来说,遇到这种情况,最简单的方式是线性寻找,即从这个位置开始, 挨个往后寻找空位。当然,Python 内部对此进行了优化(这一点无需深入了解,你有兴趣 可以查看源码,我就不再赘述),让这个步骤更加高效。 查找操作 和前面的插入操作类似,Python 会根据哈希值,找到其应该处于的位置;然后,比较哈希 表这个位置中元素的哈希值和键,与需要查找的元素是否相等。如果相等,则直接返回;如 果不等,则继续查找,直到找到空位或者抛出异常为止。

删除操作

对于删除操作,Python 会暂时对这个位置的元素,赋于一个特殊的值,等到重新调整哈希 表的大小时,再将其删除。 不难理解,哈希冲突的发生,往往会降低字典和集合操作的速度。因此,为了保证其高效 性,字典和集合内的哈希表,通常会保证其至少留有 1/3 的剩余空间。随着元素的不停插 入,当剩余空间小于 1/3 时,Python 会重新获取更大的内存空间,扩充哈希表。不过,这 种情况下,表内所有的元素位置都会被重新排放。 虽然哈希冲突和哈希表大小的调整,都会导致速度减缓,但是这种情况发生的次数极少。所 以,平均情况下,这仍能保证插入、查找和删除的时间复杂度为 O(1)。

05 | 深入浅出字符串

常见的的转义字符

Python 的字符串是不可变的(immutable),Python 中字符串的改变,通常只能通过创建新的字符串来完成。

s='HKSIHh'
s = 'H' + s[1:]
s = s.replace('h', 'H'

你可能了解到,在其他语言中,如 Java,有可变的字符串类型,比如 StringBuilder,每次 添加、改变或删除字符(串),无需创建新的字符串,时间复杂度仅为 O(1)。这样就大大 提高了程序的运行效率。 但可惜的是,Python 中并没有相关的数据类型,我们还是得老老实实创建新的字符串。因 此,每次想要改变字符串,往往需要 O(n) 的时间复杂度,其中,n 为新字符串的长度。

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

string.strip(str),表示去掉首尾的 str 字符串; string.lstrip(str),表示只去掉开头的 str 字符串; string.rstrip(str),表示只去掉尾部的 str 字符串。

06 | Python “黑箱”:输入与输出

让我们来做一个简单的 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))

07 | 修炼基本功:条件与循环

不过,切记,在实际写代码时,我们鼓励,除了 boolean 类型的数据,条件判断最好是显 性的。比如,在判断一个整型数是否为 0 时,我们最好写出判断的条件:

if i != 0:
 ...
 
#不推荐的写法:
if i:
...

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

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

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

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

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

异常,通常是指程序运行的过程中遇到了错误,终止并退出。我们通常使用 try except 语句去处理异常,这样程序就不会被终止,仍能继续执行。 处理异常时,如果有必须执行的语句,比如文件打开后必须关闭等等,则可以放在 finally block 中。 异常处理,通常用在你不确定某段代码能否成功执行,也无法轻易判断的情况下,比如数 据库的连接、读取等等。正常的 flow-control 逻辑,不要使用异常处理,直接用条件语 句解决就可以了。

09 | 不可或缺的自定义函数

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

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

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

  1. Python 中函数的参数可以接受任意的数据类型,使用起来需要注意,必要时请在函数开 头加入数据类型的检查;
  2. .和其他语言不同,Python 中函数的参数可以设定默认值;
  3. 嵌套函数的使用,能保证数据的隐私性,提高程序运行效率;
  4. 合理地使用闭包,则可以简化程序的复杂度,提高可读性。

10 | 简约不简单的匿名函数

函数 map(function, iterable) 的第一个参数是函数对象,第二个参 数是一个可以遍历的集合,它表示对 iterable 的每一个元素,都运用 function 这个函数。

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

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 进行计算,所以最后返回的是一个单独的数值。

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

你可能感兴趣的:(Python核心技术与实战:学习笔记(一))