Python学习路径与笔记

#Python基础

#第1步:《自学是门手艺》

链接:selfteaching/the-craft-of-selfteaching

笔记:

#可编程的灵魂

计算器和计算机都是电子设备,但计算机更为强大的原因,用通俗的说法就是它 “可编程”(Programable)—— 而所谓可编程的核心就是布尔运算及其相应的流程控制(Control Flow);没有布尔运算能力就没有办法做流程控制;没有流程控制就只能 “按顺序执行”,那就显得 “很不智能”……(Part.1.E.1.entrance 1.2. 布尔运算)

从这个我想到可能可以为以下事物粗略分类:

boolean_human

计算机这种东西最擅长的是重复,生活中有很多东西都是重复的,最终都将被大量由计算机替代。整个世界处于“程序化/自动化”的趋势中,只是这个趋势受到各种约束而进展没想象中快。例如,少数精英的生活的各种自动化已经得到了很大程度完成,但要想让大部分人类的生活都完成自动化,却是非常困难的,因为生产力与成本的硬约束。

但是,程序化的过程中就同时在提高生产力与降低成本,所以,这个进程是个正反馈系统。作为个人,如何加入这一历史潮流中呢?——获得编程能力。所谓编程能力,就是利用计算机解决问题的能力,因为计算机这种工具能成倍的放大解决问题的效率。而最终你能减少多少熵,创造多少秩序,就有多大价值。

  • 函数的核心构成

从结构上来看,每个函数都是一个完整的程序,因为一个程序,核心构成部分就是输入处理输出

  • 它有输入 —— 即,它能接收外部通过参数传递的值;
  • 它有处理 —— 即,内部有能够完成某一特定任务的代码;尤其是,它可以根据 “输入” 得到 “输出”;
  • 它有输出 —— 即,它能向外部输送返回值……

被调用的函数,也可以被理解为子程序(Sub-Program)—— 主程序执行到函数调用时,就开始执行实现函数的那些代码,而后再返回主程序……(Part.1.E.1.entrance 1.5. 所谓函数)

一个程序最基本的结构就是顺序式的运算结构,除非特别指明,否则按顺序逐句执行。在此基础上,加上流程控制就成为“可编程”的程序。整个程序一整块按顺序和流程执行就行,按道理计算机并不需要所谓的函数。

函数其实是为了方便人的,我们常把能实现某一功能的一段代码单纯拎出来,某处要用这个功能就调用这个函数,输入和输出是作衔接用的。这样,就不用重复写不必要的代码了。这不就和数学上的函数类似吗?y = x + 1,这个一元一次函数实现的功能就是让实数的数值加一,可以把它写成y = f(x),需要时就传入x的值,返回的就是要的结果了。

然后,所谓的模块,不就是将可以合力实现一个整体上更复杂的功能的多个函数组织起来的东西吗?如果说语句是句子、函数是段落的话,那模块就是文章了,因为它单独放在同一个文件里了。

然后是包packages,其实是更进一步,相当于文件夹,用于存放多个模块。模块是以.py文件保存的,但我们 import 的时候,是直接用的模块名import MName。所以,为了区分这个是包还是模块,或者说是文件夹还是文件,在包里面,我们还需要一个叫做__init__.py 文件用于标识它是一个包。这样,当在当前目录下import NName时,Python 会先会以NName.py去寻找该目录下是否有个NName.py的模块文件,如果没找到,它会再次尝试去看看当前目录是否存在一个叫做NName的目录,而且里面有个__init__.py的文件,是的话就导入。

func_module_package◎ 名为niupi的包的可能结构

#编程语言基础概念

以下是这一章中所提到的重要概念。了解它们以及它们之间的关系,是进行下一步的基础。

  • 数据:整数、布尔值;操作符;变量、赋值;表达式
  • 函数、子程序、参数、返回值、调用
  • 流程控制、分支、循环
  • 算法、优化
  • 程序:语句、注释、语句块
  • 输入、处理、输出
  • 解释器

你可能已经注意到了,这一章的小节名称罗列出来的话,看起来像是一本编程书籍的目录 —— 只不过是概念讲解顺序不同而已。事实上还真的就是那么回事。

这些概念,基本上都是独立于某一种编程语言的(Language Independent),无论将来你学习哪一种编程语言,不管是 C++,还是 JavaScript,抑或是 Golang,这些概念都在那里。(Part.1.E.1.entrance 1.7. 总结)

1
2
def zongjie():
    pass

#编程语言基本数据类型

编程语言最基本的三种数据类型是:

  1. 布尔值(Boolean Value)
  2. 数字(Numbers)
  3. 字符串(Strings)

#数据类型的主要操作方式

  1. 操作符
    • 值运算
    • 逻辑运算
  2. 函数

Python的基础数据类型的操作符整理如下:

基础数据类型◎ Python的基础数据类型的操作符

#字符串的操作

与作者做了个差不多的图,作者漏填了搜索与替换那行,补上了,同时根据自己的习惯进行了相应调整。

str_operation◎ 字符串的操作

#数值的操作

对于数值,存在三种类型: 整数(int), 浮点数(float) 和 复数(complex)。除了操作符外,有几个内建函数可以处理它,如果需要更复杂的方式需要 import math

名称 操作 官方文档链接
绝对值 abs(n) abs()
转换为整数 int(n) int()
转换为浮点数 float(n) float()
转换为复数 complex(re, im) complex()
商余 divmod(n, m) divmod()
pow(n, m) pow()
近似(四舍五入) round(n) round()

此外,还有几个数值通用的属性和方法,即便不是复数:

名称 操作
返回数值实数部分 n . real
返回数值虚数部分 n . imag
共轭 n . comjugate( )

对于 int 型,有几个 Methods:

int.bit_length()

返回以二进制表示一个整数所需要的位数,不包括符号位和前面的零:

1
2
3
4
5
>>> n = -37
>>> bin(n)
'-0b100101'
>>> n.bit_length()
6

int.to_bytes(length, byteorder, *, signed=False)

返回表示一个整数的字节数组。

1
2
3
4
5
6
7
8
9
>>> (1024).to_bytes(2, byteorder='big')
b'\x04\x00'
>>> (1024).to_bytes(10, byteorder='big')
b'\x00\x00\x00\x00\x00\x00\x00\x00\x04\x00'
>>> (-1024).to_bytes(10, byteorder='big', signed=True)
b'\xff\xff\xff\xff\xff\xff\xff\xff\xfc\x00'
>>> x = 1000
>>> x.to_bytes((x.bit_length() + 7) // 8, byteorder='little')
b'\xe8\x03'

int.from_bytes(bytes, byteorder, *, signed=False)

返回由给定字节数组所表示的整数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
>>> int.from_bytes(b'\x00\x10', byteorder='big')
16
>>> int.from_bytes(b'\x00\x10', byteorder='little')
4096
>>> int.from_bytes(b'\xfc\x00', byteorder='big', signed=True)
-1024
>>> int.from_bytes(b'\xfc\x00', byteorder='big', signed=False)
64512
>>> int.from_bytes([255, 0, 0], byteorder='big')
16711680

int.as_integer_ratio()
返回一对整数,其比率正好等于原整数并且分母为正数。 整数的比率总是用这个整数本身作为分子,1 作为分母。(3.8 新版功能)

还有两个属性是所谓的最简分数形式的分子和分母,本来是在更细致的划分中的分数(Fraction)中的概念的,int 型本身也有。对于 int 型,最简分数形式的分子是它本身,最简分数形式的分母是1。

int.numerator
最简分数形式的分子。

int.denominator
最简分数形式的分母。

int的全部关于它自身的属性和方法都在这了:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
In [1]: dir(int)
Out[1]: 
[ ...
 'bit_length',
 'conjugate',
 'denominator',
 'from_bytes',
 'imag',
 'numerator',
 'real',
 'to_bytes']

对于 float 型,有几个 methods:

float.as_integer_ratio()

返回一对整数,其比率正好等于原浮点数并且分母为正数。 无穷大会引发 OverflowError 而 NaN 则会引发 ValueError。

float.is_integer()

如果 float 实例可用有限位整数表示则返回 True,否则返回 False:

1
2
3
4
>>> (-2.0).is_integer()
True
>>> (3.2).is_integer()
False

下面两个方法均支持与十六进制数字符串之间的转换。 由于 Python 浮点数在内部存储为二进制数,因此浮点数与 十进制数 字符串之间的转换往往会导致微小的舍入错误。 而十六进制数字符串却允许精确地表示和描述浮点数。这在进行调试和数值工作时非常有用。

float.hex()
以十六进制字符串的形式返回一个浮点数表示。 对于有限浮点数,这种表示法将总是包含前导的 0x 和尾随的 p 加指数。

float.fromhex(s)
返回以十六进制字符串 s 表示的浮点数的类方法。 字符串 s 可以带有前导和尾随的空格。

float自身的属性和方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
In [2]: dir(float)
Out[2]: 
[ ...
 'as_integer_ratio',
 'conjugate',
 'fromhex',
 'hex',
 'imag',
 'is_integer',
 'real']

#数据容器

在 Python 中,有个数据容器(Container)的概念。

其中包括字符串、由 range() 函数生成的等差数列列表(List)、元组(Tuple)、集合(Set)、字典(Dictionary)。

这些容器,各有各的用处。其中又分为可变容器(Mutable)和不可变容器(Immutable)。可变的有列表、集合、字典;不可变的有字符串、range() 生成的等差数列、元组。集合,又分为 SetFrozen Set;其中,Set 是可变的,Frozen Set 是不可变的

字符串、由 range() 函数生成的等差数列、列表、元组是有序类型(Sequence Type),而集合与字典是无序的。

Image

另外,集合没有重合元素。(Part.1.E.6.containers 1. 数据容器)

这个表格显示了基于基础的数据类型,所衍生的更复杂的所谓数据容器的整体情况。这里需要补充的是 Python3.7改版后,字典能保证有序了。在Python3.8后字典可逆了。

这里需要补充下 bytes 类型的基本概念:

class bytes([source[, encoding[, errors]]])

首先,表示 bytes 字面值的语法与字符串字面值的大致相同,只是添加了一个 b 前缀:

  • 单引号: b'同样允许嵌入 "双" 引号'
  • 双引号: b"同样允许嵌入 '单' 引号"
  • 三重引号: b'''三重单引号''', b"""三重双引号"""

bytes 字面值中只允许 ASCII 字符(无论源代码声明的编码为何)。 任何超出 127 的二进制值必须使用相应的转义序列形式加入 bytes 字面值。

像字符串字面值一样,bytes 字面值也可以使用 r 前缀来禁用转义序列处理。 请参阅 字符串和字节串字面值 了解有关各种 bytes 字面值形式的详情,包括所支持的转义序列。

虽然 bytes 字面值和表示法是基于 ASCII 文本的,但 bytes 对象的行为实际上更像是不可变的整数序列,序列中的每个值的大小被限制为 0 <= x < 256 (如果违反此限制将引发 ValueError)。 这种限制是有意设计用以强调以下事实,虽然许多二进制格式都包含基于 ASCII 的元素,可以通过某些面向文本的算法进行有用的操作,但情况对于任意二进制数据来说通常却并非如此(盲目地将文本处理算法应用于不兼容 ASCII 的二进制数据格式往往将导致数据损坏)。

除了字面值形式,bytes 对象还可以通过其他几种方式来创建:

  • 指定长度的以零值填充的 bytes 对象: bytes(10)
  • 通过由整数组成的可迭代对象: bytes(range(20))
  • 通过缓冲区协议复制现有的二进制数据: bytes(obj)

(Python Library - bytes)

#列表的操作

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
In [3]: dir(list)
Out[3]: 
[ ...
 'append',
 'clear',
 'copy',
 'count',
 'extend',
 'index',
 'insert',
 'pop',
 'remove',
 'reverse',
 'sort']

列表的基础操作没有字符串多。

作者的整理如下:

list-concepts

(Part.1.E.6.containers 1.2.6. 小结)

与字符串不同,列表的拼接不能用空格,其他操作符与字符串一样。

delattr(object, name)
setattr() 相关的函数。实参是一个对象和一个字符串。该字符串必须是对象的某个属性。如果对象允许,该函数将删除指定的属性。例如 delattr(x, 'foobar') 等价于 del x.foobar 。

del()可以既可以删除列表中的元素,也可以删除整个列表。

del(a)del a效果相同。

delattr与del与del()的区别是什么?

sort方法需要列表里为同一种元素,否则会报错。

对copy的列表进行操作,不会更改原件。

reverse只对当前序列操作,并不返回一个逆序列表;返回值是 None

remove也只对当前序列操作,返回值是 None;pop则返回被删除元素

a.extend(t)相当于 a += t

#元组的操作

元组相当于不可变的列表,其占内存更小。由于不可变,也就没有列表的排序、删除、加入、复制和清除等操作了。

但要注意元组的创建:

目标 创建方式
空元组 a = ()
单元素元组 a = 1,a = (1,)
多元素元组 a = 1,2a = (1,2)

当然元组也可以用生成式:

1
2
In [163]: tuple(x+1 for x in range(3))
Out[163]: (1, 2, 3)

虽然元组不可变,但可以在末尾追加元素生成一个元组。不可变指的是当前已有的部分不可变

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
In [155]: a = 1,

In [156]: id(a)
Out[156]: 140517386079440

In [157]: a += 2,3

In [158]: id(a)
Out[158]: 140517385537920
  
In [159]: a
Out[159]: (1, 2, 3)

#集合的操作

根据教程和官方文档做了个总结:

set_methods

del不行是因为set中的元素没索引(它不是有序容器)。对于普通set可以用remove,对于frozen set不能用remove

我用的是Python 3.8.8,作者的(Part.1.E.6.containers 1.4.4. 更新)中对 pop的介绍不符合,我这个版本的集合的pop无参数,它从集合中移除并返回任意一个元素,如果集合为空则会引发 KeyError。

set.upate(*others)相当于set |= other | ...

注意**对称差(symmetric_difference)**这种运算只能两个集合进行。

#字典的操作

根据教程和官方文档做了个总结:

dict_operation

字典的键是有顺序的。

两个 dict.values() 视图之间的相等性比较将总是返回 False。 这在 dict.values() 与其自身比较时也同样适用。

两个字典的比较当且仅当它们具有相同的 (键, 值) 对时才会相等(不考虑顺序)。 排序比较 ('<', '<=', '>=', '>') 会引发 TypeError。

字典会保留插入时的顺序。 请注意对键的更新不会影响顺序。 删除并再次添加的键将被插入到末尾。因此现在字典是有序的了。字典虽然可以排序,但本身无sort方法,需要用内建函数sorted。

#第2步:Python官方入门教程

链接:Python 教程 — 中文版

笔记:

#使用Python解释器

解释器的启动与退出

安装完 Python 后,启动 Python 解释器可以在命令行输出 pythonpython3.8,注意如果你安装的是 python 3.7,就需要输入python3.7。而退出解释器需要输入ctrl+D,或输入quit()。如果用的是 ipython,输入ipython就行,退出输入exit就行。

python -c 命令

用解释器运行单行语句或者脚本的方法是 python -c command [arg] ...,其中 command 要换成想执行的指令。具体使用场景可以参考这篇文章——python -c 的妙用

默认情况下,Python 源码文件以 UTF-8 编码方式处理。如果不使用默认编码,要声明文件所使用的编码,文件的 第一行要写成特殊的注释。语法如下所示:

1
# -*- coding: encoding -*-

其中 encoding 可以是 Python 支持的任意一种 codecs。

关于 第一行规则的一种例外情况是,源码以UNIX ”shebang” 行 开头。这种情况下,编码声明就要写在文件的第二行。例如:

1
2
#!/usr/bin/env python3
# -*- coding: cp1252 -*-

添加了第一行可以使该文件能够作为脚本直接在命令行运行。

#Python的非正式介绍

多重赋值

例如:

1
2
3
a = 1
b = 2
a, b = a+b, a

这里,赋值前等式右边是先求完了值的,而且遵循一般的从左到右。所以,先求a+b等于3a不用求值为1,然后等式的左边的a被赋予右边a+b的值3,b被赋予右边a的值为1。

值的真假

数字型除了00.0都为真,字符串除了空都为真,容器类型除了空都为真。

Python tips

  1. 在交互模式下,上一次打印出来的表达式被赋值给变量 _
  2. 如果你不希望前置了 \ 的字符转义成特殊字符,可以使用 原始字符串方式,在引号前添加 r 即可。
  3. 索引过大会报错,但切片不会。切片中的越界索引会被自动处理。
  4. 一个 列表可以包含不同类型的元素,但通常使用时最好各个元素类型相同,例如如果不同类型sort方法会报错。
  5. 切片是浅拷贝,注意list_a[:]list_a.copy()的区别。

#其他流程控制工具

遍历可变对象

在遍历同一个集合时修改该集合的代码可能很难获得正确的结果。通常,更直接的做法是循环遍历该集合的副本或创建新集合。

1
2
3
4
5
6
7
8
# Strategy: Iterate over a copy 
for user, status in users.copy().items():
    if status == 'inactive': del users[user]
# Strategy: Create a new collection
active_users = {}
for user, status in users.items():
    if status == 'active':
        active_users[user] = status

循环带有else子句

循环语句可能带有 else 子句;它会在循环耗尽了可迭代对象 (使用 for) 或循环条件变为假值 (使用while) 时被执行,但不会在循环被 break 语句终止时被执行。

try 语句中的 else 子句会在未发生异常时执行,而循环中的 else 子句则会在未发生 break 时执行。二者非常相似。

写函数习惯

函数体的第一个语句可以(可选的)是字符串文字;这个字符串文字是函数的文档字符串或 docstring。在你编写的代码中包含文档字符串是一种很好的做法,所以要养成习惯。

参数默认值

函数默认值只会执行一次。这条规则在默认值为可变对象(列表、字典以及大多数类实例)时很重要。后续的函数调用共享里默认值的赋值结果

1
2
3
4
5
6
def f(a, L=[]):
    L.append(a)
    return L
print(f(1))
print(f(2))
print(f(3))

这将输出以下结果:

1
2
3
[1]
[1, 2]
[1, 2, 3]

如果不想要在后续调用之间共享值,可以在函数内部进行处理:

1
2
3
4
5
6
7
8
def f(a, L=None):
    if L is None:
        L = []
    L.append(a)
    return L
print(f(1))
print(f(2))
print(f(3))

输出结果变为:

1
2
3
[1]
[2]
[3]

函数参数

对于设置函数参数的角度,分为必须参数和可选参数。对于调用参数的角度,分为位置参数、关键字参数、特殊参数。

在函数调用中,关键字参数必须跟随在位置参数的后面。传递的所有关键字参数必须与函数接受的其中一个参数匹配,它们的顺序并不重要。这也包括非可选参数,例如必须参数也可以用关键字参数的方式传入。不能对同一个参数多次赋值。

对于特殊参数,官网介绍如下:

image-20220428211418342

这样,

  1. 如果函数定义中未使用 /*,则就是默认情况,参数可以按位置或按关键字传递给函数。
  2. 如果使用了/,在/前的必须是位置参数,在其后的可以位置也可以关键字参数
  3. 如果使用了*,其前的可以位置也可以关键字参数,但其后的必须是关键字参数

这种设计可以规避很多冲突,官网给了个例子:

image-20220428212646066

*可解包列表或元组提供位置参数,**可解包字典提供关键字参数。

函数标注

image-20220428213350284

编码风格

image-20220428213558033

#数据结构

列表作为栈使用

栈是符合后入先出的数据结构,对于列表,要作为栈存入元素至末尾,只需要append元素即可;要取出元素,只需要pop就可以。非常方便。

列表作为队列使用

队列是符合先入先出的数据结构,然而列表用作这个目的相当低效。因为在列表的末尾添加和弹出元素非常快,但是在列表的开头插入或弹出元素却很慢 (因为所有的其他元素都必须移动一位)。若要实现一个队列,可使用 collections.deque,它被设计成可以快速地从两端添加或弹出元素。用其popleft函数配合其append就实现了队列的功能。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
>>> from collections import deque
>>> queue = deque(["Eric", "John", "Michael"])
>>> queue.append("Terry") # Terry arrives
>>> queue.append("Graham") # Graham arrives
>>> queue.popleft() # The first to arrive now leaves
'Eric'
>>> queue.popleft() # The second to arrive now leaves
'John'
>>> queue # Remaining queue in order of arrival
deque(['Michael', 'Terry', 'Graham'])

列表推导式

我们想创建一个平方列表,可以用for循环但麻烦且不够直观,而且需要创建辅助变量进行,循环结束后辅助变量还存在。

下面这个利用map函数的方法可以无副作用地创建:

1
squares = list(map(lambda x: x**2, range(10)))

然后进一步简写就是我们的列表推导式了:

1
squares = [x**2 for x in range(10)]

下面的列表推导式将交换其行和列:

1
2
3
4
5
6
matrix = [ 
[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12]]

[[row[i] for row in matrix] for i in range(4)]

实际应用中,你应该会更喜欢使用内置函数去组成复杂的流程语句。zip() 函数将会很好地处理这种情况。

1
2
>>> list(zip(*matrix))
[(1, 5, 9), (2, 6, 10), (3, 7, 11), (4, 8, 12)]

容器

元组是immutable ,其序列通常包含不同种类的元素,并且通过解包或者索引来访问。列表是mutable ,并且列表中的元素一般是同种类型的,并且通过迭代访问。

注意多重赋值其实也只是元组打包和序列解包的组合。

可以用 del 来删除一个键值对。如果你使用了一个已经存在的关键字来存储值,那么之前与这个关键字关联的值就会被遗忘。用一个不存在的键来取值则会报错。

del操作的对象是字典的值 del dict[‘key’],键同时被删除了。

如果直接循环字典,得到的是键;如果用enumerate循环字典,得到的是键和键的索引:

image-20220516090918367

当用作普通值而非布尔值时, 短路操作符的返回值通常是短路时的变量。

image-20220516091455616

混合数值类型是通过他们的数值进行比较的, 所以 0 等于 0.0。

#第3步:刷题

刷题过程中利用第1步和第2步的内容以解决各类题目,如果忘记,回顾第1、2步内容或查询Python官方文档

#Python练习题

链接:完成130道Python练习题之数据类型篇、基础语法篇、内置函数篇和字符串方法这四关。

笔记:

1.1.1 逻辑推理练习(类型转换)里,第1题 4.0 == 4 返回True在Python的官方教程里有以下解释:

注意对不同类型对象来说, 只要待比较对象提供了合适的比较方法, 就可以使用 < 和 > 来比较。 例如, 混合数值类型是通过他们的数值进行比较的, 所以 0 等于 0.0, 等等。 否则, 解释器将抛出一个 TypeError 异常,而不是随便给出一个结果。(by 《Python Tutorial》-数据结构-比较对象和其他类型)

2.2.7 寻找组合里,第2题参考代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
lst1 = [3, 6, 1, 8, 1, 9 , 2]
lst2 = [3, 1, 2, 6, 4, 8, 7]

s = lst1[0] * lst2[0]
for i in lst1:
    for j in lst2:
        if i * j > s:
            s = i * j
else:
    print(s)

2.3.1 奇偶数判断用isdigit可检查不是整数的输入:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
while True:
    s = input("请输入一个整数(输入quit可停止):")
    if s == "quit":
        break
    elif s.isdigit():
        if int(s) % 2 == 0:
            print(f"您输入的{s}是个偶数")
        else:
            print(f"您输入的{s}是个奇数")
    else:
        print("请输入整数哦")

3.5 int里,

1
2
3
for item in string:
    int_value = str_int_dic[item]
    res = res*10 + int_value

这种字符转数字的小技巧要记住。

#Python小例子

链接:https://github.com/jackzhenguo/python-small-examples

笔记:

#Python进阶

#程序设计

《计算机程序的构造和解释》

Brian Harvey’s Berkeley CS 61A

#数据结构

数据结构和算法动态可视化 (Chinese) - VisuAlgo

#Python应用

#数据分析

《利用Python进行数据分析》

#机器学习