学习:Python 学习资源推荐 & 学习笔记

本文最后更新于:2024-08-07

终于开始学习 Python 了,记录一下推荐的学习资源,顺带把笔记也放在这里

学习资源推荐

  • 卡瓦邦噶! - 如何学Python?
    一份非常详尽的 Python 学习资源推荐,零基础、进阶、深入理解、面试,各方面资料应有尽有

    博主还有另一个页面 卡瓦邦噶! - 珍藏资料,也有很多好东西

  • Piglei - Python 工匠
    Python 非常自由,但相应的代价就是上下限差距会很大,这本书讨论如何把 能跑的代码 改成 优秀的代码、如何利用 Python 语言特性写出更加 Pythonic(Python风格) 的代码
    还介绍了一些不止 Python,任何语言都通用的知识,例如变量命名、大型项目里如何统一代码风格
    内容通俗易懂,GitHub 版本可以免费阅读,如果感觉讲的不错可以购买书籍版,比 web 版更加翔实

  • Python Documents
    有时候看不知道了几手的资料怎么都看不明白,说不定翻一下官方文档就懂了,有能力可以多看文档
    卡瓦邦噶!Python 工匠 里都提到推荐看一下 Python Documents - itertools 官方文档,了解标准库里的轮子,省的自己造

  • CS61A
    大名鼎鼎的 CS61A,久闻其名,还没看

  • 捕蛇者说
    一档播客节目,名字中的 当然就是 Python 啦,偶然听听,能发现许多新资源

    上面提到的 卡瓦邦噶! 博主 laixintao 就是主播之一


笔记

下面是学习 Python 过程中做的笔记,摘抄或复述看过的内容,加上一些感觉有价值的知识点,加深印象,还可以方便以后查找

== None or is None

Reading Python 工匠: 与 None 值的比较
On 2024.07.14

判断变量是否为 None 时,应该用 is None

在 Python 中,有两种比较变量的方法:==is
作者的解释:

  • ==:表示二者所指向的的是否一致
  • is:表示二者是否指向内存中的同一份内容,也就是 id(x) 是否等于 id(y)

None 在 Python 语言中是一个单例对象,如果你要判断某个变量是否为 None 时,记得使用 is 而不是 ==,因为只有 is 才能在严格意义上表示某个变量是否是 None。

== 实际上是调用 __eq__ 魔法方法,可以通过自定义 __eq__ 方法操作真伪值
判断变量是否为 None 时,应该用 is 而非 ==

需要重写 __eq__ 魔法方法的案例

图书版 Python 工匠 12.1.2 比较运算符重载 章节,作者介绍了一个需要重写 __eq__ 魔法方法的案例
例如,一个表示正方形的类 Square,接受一个边长作为参数,计算正方形面积

1
2
3
4
5
6
7
8
9
10
11
class Square:
"""正方形

:param length: 边长
"""

def __init__(self, length):
self.length = length

def area(self):
return self.length ** 2

显然,边长相等的两个正方形,可以看作一样的正方形
但默认情况下,Python 认为这两个正方形是不同对象,返回 False

1
2
3
4
>>> x = Square(4)
>>> y = Square(4)
>>> x == y
False

这时就需要重写 __eq__ 方法,以及其他 5 个比较运算符 __ne____lt____le____gt____ge__

使用 functools 内置模块下的 @total_ordering 装饰器,可以减少工作量
只要重写 __eq__,外加 __lt____le____gt____ge__ 中的一个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from functools import total_ordering


@total_ordering
class Square:
"""正方形

:param length: 边长
"""

def __init__(self, length):
self.length = length

def area(self):
return self.length ** 2

def __eq__(self, other):
if isinstance(other, self.__class__):
return self.length == other.length
return False

def __lt__(self, other):
if isinstance(other, self.__class__):
return self.length < other.length
return NotImplemented

这个正方形类就可以做到正常比较大小了

可迭代对象(iterable)、迭代器(iterator)

Reading Python 工匠: 区分迭代器与可迭代对象
On 2024.07.14

什么是可迭代对象(iterable)

可迭代对象(iterable) 在 官方文档中的定义

An object capable of returning its members one at a time. Examples of iterables include all sequence types (such as list, str, and tuple) and some non-sequence types like dict, file objects, and objects of any classes you define with an __iter__() method or with a __getitem__() method that implements sequence semantics.

所以可迭代对象指能够 每次返回一个成员 的对象类型
每迭代一次,可迭代对象就拿出来一个成员给你,直到可迭代对象从头到尾遍历完一遍位置

显然 liststrtuple 都能满足上述两个条件
至于文档里说 dictfile objects 也可以看作可迭代对象,似乎没什么问题(具体细节我不清楚
再有就是可以通过自定义 __iter__() 方法,把任意 class 变成可迭代对象

本来以为 set(集合) 对象是无序的,应该不能迭代才对
但偶然看这篇 Python Documents: Howto - Functional,发现 set 原来也可以迭代
查了查资料,set 不保证每次遍历时保持相同顺序,因为 set 元素顺序是由 hash 值决定的,当 set 发生变化时就有可能会改变原来的排序

区别于迭代器(iterator)

知道了什么是可迭代对象,那就试一试吧,用 next() 方法迭代看看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> # 定义一个简单 list
>>> l = [4, 3, 2, 1]

>>> # 看看这个列表
>>> l
[4, 3, 2, 1]
>>> # 嗯,没问题

>>> # 迭代一下
>>> next(l)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'list' object is not an iterator
>>> # 报错: list 不是迭代器

怎么迭代不起来呢?

回到文档,可迭代对象(iterable) 在官方文档中的定义 还有第二段话

Iterables can be used in a for loop and in many other places where a sequence is needed (zip(), map(), …). When an iterable object is passed as an argument to the built-in function iter(), it returns an iterator for the object. This iterator is good for one pass over the set of values. When using iterables, it is usually not necessary to call iter() or deal with iterator objects yourself. The for statement does that automatically for you, creating a temporary unnamed variable to hold the iterator for the duration of the loop.

可迭代对象可以用在循环或其他各种需要序列的地方
把可迭代对象当参数传给 iter() 方法,会返回一个可迭代对象的 迭代器(iterator)
迭代器适合用在一次性用途
使用可迭代对象(iterable)时,通常无须调用 iter() 或自己手搓 迭代器(iterator)
例如,在使用 for 循环时,for 循环会帮你自动控制 迭代器(iterator),方便省心

因此,常见的 for i in range(4): 循环并不是在循环 range(4),而是在循环 iter(range(4)) 这个返回的迭代器
只不过 Python 的 for 循环可以自动控制这一过程而已

用终端验证一下

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> # 接着刚才的 list
>>> l
[4, 3, 2, 1]

>>> # 对 list 对象调用 iter()
>>> iter(l)
<list_iterator object at 0x7faffaac46d0>
>>> # 创建了一个 list_iterator 对象

>>> # 对 iterator 使用 next()
>>> next(iter(l))
4
>>> # 成功拿到 list 第一个值

对迭代器一直 next() 下去,就可以顺序拿到 list 里的值

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> l
[4, 3, 2, 1]
>>> iter(l)
<list_iterator object at 0x7faaf1ac46d0>
>>> next(iter(l))
4
>>> next(iter(l))
4
>>> next(iter(l))
4
>>> next(iter(l))
4
>>>

预想应该是 4, 3, 2, 1 才对,但每次 next(iter(l)) 都拿到 list 的第一个值
出现这种问题,原因是 next(iter(l)) 里的 iter(l)
每使用一次 iter(l),都会创建一个新 迭代器(iterator)
所以 next(iter(l)) 每次都是对一个崭新的迭代器取值,当然每次都只能拿到第一个值

可以在终端里看一下

1
2
3
4
5
6
>>> iter(l)
<list_iterator object at 0x7faaf1ae4430>
>>> iter(l)
<list_iterator object at 0x7faaf1ac46d0>
>>> iter(l)
<list_iterator object at 0x7faaf1c2d240>

调用三次 iter(l),返回了三个 list_iterator 对象(at 后面的地址不同)

既然这样,要想 next 拿到迭代器的所有值,就找个中介吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
>>> # 可迭代对象
>>> l
[4, 3, 2, 1]

>>> # 调用 next 会报错
>>> next(l)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'list' object is not an iterator


>>> # 把迭代器对象赋值给 l_iterator
>>> l_iterator = iter(l)

>>> # next 它
>>> next(l_iterator)
4
>>> next(l_iterator)
3
>>> next(l_iterator)
2
>>> next(l_iterator)
1
>>> next(l_iterator)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration

这样就能拿到迭代器里的所有值了

生成器(generator) 和 生成器表达式(generator expression)

迭代器似乎很好用,但一次性的消耗品,如果每次使用前都要一堆准备工作,就失去便捷的意义了
因此 Python 里有两种简单使用迭代器的方法

  • 生成器(generator)
    在 function 里用 yield 代替 return,这样函数就能准备好一串 序列,等 next() 来拿

  • 生成器表达式(generator expression)
    常见的 for i in range(4): 就是一例

    Python Documents: generator expression 里给了另一个例子

    1
    2
    >>> sum(i*i for i in range(10))         # sum of squares 0, 1, 4, ... 81
    285

怎么计算迭代器(iterator)里有多少元素

普通的列表可以用 len(list) 得到列表的元素个数
但迭代器是懒惰对象,不迭代一遍就没办法知道有多少个元素
一旦从头到尾迭代完一遍,这个迭代器也就失效了
数它有几个似乎意义不大(

迭代器(iterator) 是一次性消耗品,用完即弃
想要多次循环时,还是需要使用 list 之类的普通对象

Ref:
Python Documents: iterable
Python Documents: iterator
Python Documents: generator
Python Documents: generator expression

函数式编程

Reading Python Documents: Functional Programming HOWTO
On 2024.07.15

省流,最后一段建议不要使用 lambda 表达式

Fredrik Lundh once suggested the following set of rules for refactoring uses of lambda:

  1. Write a lambda function.
  2. Write a comment explaining what the heck that lambda does.
  3. Study the comment for a while, and think of a name that captures the essence of the comment.
  4. Convert the lambda to a def statement, using that name.
  5. Remove the comment.

I really like these rules, but you’re free to disagree about whether this lambda-free style is better.
使用 lambda 表达式虽然可以少写两行代码,但理解成本直线上升
从可读性、理解成本角度考虑,不推荐使用函数式编程,朴实地用 def 和 for 循环就好

这篇文章前面的介绍部分感觉不错,说编程语言解决问题的思路分四个流派:
Procedual:面向过程,例如 C、Pascal、Unix Shell
Declarative:SQL
Object-oriented:面向对象
Functional:函数式编程
Python 非常灵活,面向过程、面向对象、或者函数式编程,都能做到,可以根据场景选择不同的方式

这篇文章里讲到许多 可迭代对象(iterable)、迭代器(iterator)、生成器(generator) 的例子,顺便复习了上一节的知识点
看到几个可能比较有用的函数

  • filter(predicate, iter)
    (x for x in range(10) if is_even(x)) 生成器表达式效果一样

  • enumerate(iter, start=0)
    经常用来同时取 index 和 value

  • any(iter) and all(iter)
    判断迭代器里布尔值

  • zip(iterA, iterB, …, strict=False)
    类似 Excel 里面行列转换,默认取最短的那个迭代器长度作为最后的长度
    如果希望严格限制所有迭代器长度一样,可以设置 strict=True

  • itertools.count(start, step)
    能得到一个无限长度的迭代器

  • itertools.repeat(elem, [n])
    重复 elem 元素 n 次

  • itertools.islice(iter, [start], stop, [step])
    切片,一个参数是 stop,两个参数是 start 和 stop,三个参数是 start、stop、step

  • itertools.tee(iter, [n])
    把一个迭代器复制成 n 份,查了查,似乎是用在数据分析、日志分析之类的地方
    对同一组数据进行多次分析,可以用 tee 分出多个迭代器,各自分析

  • itertools.filterfalse(predicate, iter)
    和 filter 相反

  • itertools.dropwhile(predicate, iter)
    字面意思

  • itertools.combinations(iterable, r)
    排列组合

    itertools.combinations([1, 2, 3, 4, 5], 2) =>
    (1, 2), (1, 3), (1, 4), (1, 5),
    (2, 3), (2, 4), (2, 5),
    (3, 4), (3, 5),
    (4, 5)

这篇读完了,写的还挺有意思的

补充一个 itertools.product(*iterables, repeat=1),相当于循环嵌套

开发大型项目常用工具

Reading 图书版 Python 工匠 13.1 开发大型项目常用工具介绍 章节
On 2024.07.16

Linter:

  • flake8,检查代码风格,可自行定制
  • black,更加严格的代码风格检查工具,几乎不可定制
  • isort,分类 import 代码
  • pre-commit,强制在 commit 前进行代码检查
  • mypy,配合类型注解实现静态类型检查

四类常见内建容器类型

Reading Python 工匠:4. 容器的门道
On 2024.07.11

Python 中,有四类最常见的内建容器类型:列表(list)、元组(tuple)、字典(dict)、集合(set)

  • 列表(List):有序、可变、允许重复。适用于需要按顺序存储和访问数据的场景。
  • 元组(Tuple):有序、不可变、允许重复。适用于需要存储不可变数据的场景。
  • 字典(Dictionary):有序(Python 3.7+)、可变、键唯一。适用于需要通过键快速查找值的场景。
  • 集合(Set):无序、可变、不允许重复。适用于需要存储唯一元素并进行集合操作(如交集、并集)的场景。

Python 工匠:写更快的代码

  1. 避免频繁扩充列表/创建新列表

    • 多用迭代器,yield生成器表达式
    • 尽量使用模块提供的懒惰对象:
      • 使用 re.finditer 替代 re.findall
      • 直接使用可迭代的文件对象: for line in fp,而不是 for line in fp.readlines()
  2. 在列表头部操作多的场景使用 deque 模块

    列表是基于数组结构(Array)实现的,当你在列表的头部插入新成员(list.insert(0, item))时,它后面的所有其他成员都需要被移动,操作的时间复杂度是 O(n)。这导致在列表的头部插入成员远比在尾部追加(list.append(item) 时间复杂度为 O(1))要慢。

    如果你的代码需要执行很多次这类操作,请考虑使用 collections.deque 类型来替代列表。因为 deque 是基于双端队列实现的,无论是在头部还是尾部追加元素,时间复杂度都是 O(1)

  3. 使用集合/字典来判断成员是否存在

    当你需要判断成员是否存在于某个容器时,用集合比列表更合适。因为 item in [...] 操作的时间复杂度是 O(n),而 item in {...} 的时间复杂度是 O(1)。这是因为字典与集合都是基于哈希表(Hash Table)数据结构实现的。

    Hint: 强烈建议阅读 TimeComplexity - Python Wiki,了解更多关于常见容器类型的时间复杂度相关内容。

    如果你对字典的实现细节感兴趣,也强烈建议观看 Raymond Hettinger 的演讲 Modern Dictionaries(YouTube)

边界情况、海象操作符

Reading Python 工匠:15. 在边界处思考
Reading Python 工匠:16. 语句、表达式和海象操作符
On 2024.07.16

图书版 Python 工匠 没有收录这两章

在边界处思考

作者认为最好可以利用语言特性更加优雅的处理边界情况,具体到 Python 中

  • 使用 try 捕获异常 而非 if else 来处理边界情况
    Python 社区比起 LBYL (Look Before You Leap),更倾向于 EAFP (sEasier to Ask for Forgiveness than Permission),因为 Python 中抛出异常是很轻量的操作
    作者推荐阅读 Write Cleaner Python: Use Exceptions
  • 容器内容不存在时的处理
    collections.defaultdict 可以处理字典类型里 键不存在 的错误
    使用 setdefault 取值并修改字典值
    使用 dict.pop 删除不存在的键
    列表切片不会抛出 Index 越界错误,无须做边界处理
  • 使用 or 短路求值特性设定默认值时,除了 None,空容器也会被判定为 False,用 if 来进行边界处理更准确
  • 不要手动做用户输入数据校验,使用 pydantic 或框架自带的校验模块
  • 善用 %abs()math.floor()

语句、表达式和海象操作符

海象操作符 (walrus operator) 结合了赋值和表达式两种功能
可以用于条件分支、循环、列表推导式中,有利于减少代码重复
但是海象操作符也提高了代码的理解成本,不宜盲目追求精炼

感觉海象操作符有点类似函数式编程,精炼但增加理解成本



本博客所有文章除特别声明外,均采用 CC BY-NC-ND 4.0协议 。转载请注明出处~