Python学习笔记

Python 知识图谱

Python 知识图谱

Python 入门

编译型语言和解释型语言

编程都是用的高级语言,计算机不能直接理解高级语言,只能理解和运行机器语言,
所以必须要把高级语言转成机器语言,计算机才能运行高级语言别写的程序。

  • 编译型
    编译型
  • 解释型
    解释型

Python程序文件的扩展名通常为.py。执行Python程序时,首先Python解释器将.py文件中的源代码翻译成中间码.pyc文件,
再由Python虚拟机逐条将中间码翻译成机器指令执行。

Python简介

Python(发音:/ˈpaɪθən/ )是一种强大的编程语言,它简单易学,提供众多高级的数据结构,让我们可以面向对象进行编程。
Python 的语法优雅,由于是一个解释性语言,更贴近人类自然语言,符合人类惯常的认识逻辑。

Python 的应用领域

Python 跨平台,它能够运行在所有的常见操作系统上。它在近期热门的大数据、科学研究、机器学习、人工智能等领域大显身手,
同时 Python 几乎在所有领域都有所应用,对于学习它来说十分划算。

Python 的优缺点

  • 优点
    1. 代码量少:这是简洁、优雅、明确、众多三方库带来的效果,可以让我们在处理同一个需求时相较其他语言撰写更少的代码量,大大节省了我们的时间,提高了效率;
    2. 应用范围广:可以做数据处理、机器学习、AI、图形视频处理、游戏、软件、网站等等,一种技能解决更多的问题,不用为了解决某个需要去专门学习对应的语言。
  • 缺点
    1. 慢:只有在大规模工业化的使用时才突显,对于我们日常使用差别很小,同时也可以用提高硬件配置的方案对冲,因为硬件的成本对于我们的时间来说时间更为宝贵。同时,目前也有一些解决方案来处理这方面的问题;
    2. 开源:这个其实是一个优点,开源可以带去分享,让我们有更多的学习资源,网络上 Python 的资料往往是比其他语言更多的。至于之前卖给别人软件担心别人看到源代码的顾虑,现在已经越来越没有必要,大多软件服务都是提供 Saas 服务、云服务的形式,将软件部署在自己的服务器上,给客户开一个账号就可以了。

Python 安装和配置

普通安装

Python安装

使用 Anaconda

Anaconda创建激活退出删除虚拟环境

Python 的数据类型和变量

类型体系

  • 类型 type
  • 空类型 NoneType
  • 数字 numeric
    • 整型 int
      • 布尔 bool
    • 浮点 float
    • 复数 complex
  • 容器 collections
    • 序列 sequence
      • 可变序列 abc.MutableSequence
        • 列表 list
        • 字节数组 bytearray
      • 不可变序列 ImmutableSequence
        • 元组 tuple
        • 字符串 string
        • 等差数列 range
        • 字节串 bytes
        • 内存视图 memoryview
    • 集合 set
      • 可变集合 set
      • 不可变集合 frozenset
    • 映射 mapping
      • 字典 dict
  • 上下文管理器 context manager
  • 类型注解的类型 type annotation
  • 其他内置类型
    • 迭代器 iterator
      • 生成器 generator
    • 模块 module
    • 类与类实例 class/instances
    • 函数 function
    • 方法 method
    • 代码 code
    • 省略符 Ellipsis
    • 未实现 NotImplemented
    • 栈帧 frame
  • 扩展类型 (内置库)
    • 高效数组 array.array
    • 枚举 enum.Enum
    • 有理数 fractions.Fraction
    • 指定精度浮点数 decimal.Decimal
    • 时间 datetime.datetime
    • 命名元组 collections.namedtuple
    • 双向队列 collections.deque
    • 有序字典 collections.OrderedDict
    • 映射链 collections.ChainMap
    • 计数器 collections.Counter
    • 默认字典 collections.defaultdict

常用数据类型

  1. bool
    布尔类型

  2. int
    Python 对 int 类型没有最大限制(相比之下, C++ 的 int 最大为 2147483647,超过这个数字会产生溢出)在生产环境中也要时刻提防,
    避免因为对边界条件判断不清而造成 bug 甚至 0day(危重安全漏洞)。

  3. float
    Python 对 float 类型依然有精度限制。

  4. str
    字符串是由独立字符组成的一个序列,通常包含在单引号(’’)双引号(””)或者三引号之中(’’’ ‘’’或””” “””,两者一样)。
    自从 Python2.5 开始,每次处理字符串的拼接操作时(str1 += str2),Python 首先会检测 str1 还有没有其他的引用。如果没有的话,
    就会尝试原地扩充字符串 buffer 的大小,而不是重新分配一块内存来创建新的字符串并拷贝。

    把 str 强制转换为 int 请用 int(),转为浮点数请用 float()。而在生产环境中使用强制转换时,请记得加上 try except。

  5. list
    列表是动态的,长度可变,可以随意的增加、删减或改变元素。列表的存储空间略大于元组,性能略逊于元组。

  6. tuple
    元组是静态的,长度大小固定,不可以对元素进行增加、删减或者改变操作。元组相对于列表更加轻量级,性能稍优。

  7. dict
    字典是一系列由键(key)和值(value)配对组成的元素的集合,在 Python3.7+,字典被确定为有序(注意:在 3.6 中,字典有序是一个 implementation detail,
    在 3.7 才正式成为语言特性,因此 3.6 中无法 100% 确保其有序性),而 3.6 之前是无序的,其长度大小可变,元素可以任意地删减和改变。
    相比于列表和元组,字典内部的哈希表存储结构,保证了其查找、插入、删除操作的高效性。

  8. set
    集合和字典基本相同,唯一的区别,就是集合没有键和值的配对,是一系列无序的、唯一的元素组合。

鸭子类型

鸭子类型(duck typing)在程序设计中是动态类型的一种风格。在这种风格中,一个对象有效的语义,不是由继承自特定的类或实现特定的接口,
而是由”当前方法和属性的集合”决定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 这是一个鸭子(Duck)类
class Duck:
def eat(self):
print("A duck is eating...")

def walk(self):
print("A duck is walking...")

# 这是一个狗(Dog)类
class Dog:
def eat(self):
print("A dog is eating...")

def walk(self):
print("A dog is walking...")

def animal(obj):
obj.eat()
obj.walk()

if __name__ == '__main__':
animal(Duck())
animal(Dog())

可变与不可变

标准的数据类型中,有些是可变的,有些是不可以变的,不可变就意味差你不能对它进行操作,只能读取。
不可变数据:Number(数字)、String(字符串)、Tuple(元组);
可变数据:List(列表)、Dictionary(字典)、Set(集合)。

数据类型判断

Python 内置的 type() 函数可以查看数据的类型,如:

1
2
3
4
5
6
type(123) # 返回 int
# int

a = "Hello"
type(a) # 返回 str
# str

也可以用 isinstance 来判断它是不是一个指定的类型:

1
2
3
4
5
6
isinstance(123, int) # 123 是不是一个数字整型
# True
isinstance('123', int)
# False
isinstance(True, bool)
# True

自定义数据类型

在 Python 中,可以使用类(class)来定义自己的数据类型。类是一种用户自定义的数据类型,它可以封装数据和方法,
从而实现面向对象编程(Object Oriented Programming,OOP)的概念。

变量

Python 程序的流程

我习惯把“条件与循环”,叫做编程中的基本功。为什么称它为基本功呢?因为它控制着代码的逻辑,可以说是程序的中枢系统。
如果把写程序比作盖楼房,那么条件与循环就是楼房的根基,其他所有东西都是在此基础上构建而成。

条件语句

if 可以单独使用,但是 elif 和 else 必须和 if 同时搭配使用;而 If 条件语句的判断,除了 boolean 类型外,其他的最好显示出来。
常见判断条件结果

循环语句

  • while 循环
    对于 while 循环,它表示当 condition 满足时,一直重复循环内部的操作,直到 condition 不再满足,就跳出循环体。

    `

    1. [ While 之前的代码 ]

    2. [ While {表达式} ]
      1.1 { While 循环代码}

    3. [ While 之后的代码 ]
      `

    逻辑解释:

    1. 开始执行
    2. 先执行 0 行
    3. 执行 1 ,如果 {表达式} 为 True,执行 1.1
    4. 执行完 1.1 后再执行 1,如此往复
    5. 如果执行到 1 {表达式} 为 False, 执行 2
    6. 结束执行
  • for循环
    Python 中的数据结构只要是可迭代的(iterable),比如列表、集合等等,那么都可以通过下面这种方式遍历:

    1
    2
    for item in <iterable>:
    ...

    字典本身只有键是可迭代的,如果我们要遍历它的值或者是键值对,就需要通过其内置的函数 values() 或者 items() 实现。
    其中,values() 返回字典的值的集合,items() 返回键值对的集合。

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

同时需要注意的是,for 循环和 while 循环的效率问题。比如下面的 while 循环:

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

和等价的 for 循环:

1
2
for i in range(0, 1000000):
pass

究竟哪个效率高呢?

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

条件与循环的复用

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

1
2
expression1 if condition else expression2 for item in iterable

将这个表达式分解开来,其实就等同于下面这样的嵌套结构:

1
2
3
4
5
for item in iterable:
if condition:
expression1
else:
expression2

熟练之后,你会发现这种写法非常方便。当然,如果遇到逻辑很复杂的复用,你可能会觉得写成一行难以理解、容易出错。那种情况下,用正常的形式表达,也不失为一种好的规范和选择。

break 和 continue

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

rang() 函数

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

1
2
3
4
5
6
7
8
9
10
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()。用它来遍历集合,
不仅返回每个元素,并且还返回其对应的索引,这样一来,上面的例子就可以写成:

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

1
2
3
4
5

with as 上下文管理器

match case 结构化模式匹配

assert 断言

Python 函数

定义函数

函数是Python里面组织代码的最小单元。函数就是为了实现某一功能的代码段,只要写好以后,就可以重复利用。

1
2
3
4
5
6
7
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),也可以不返回。

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

主程序调用函数时,必须保证这个函数此前已经定义过,不然就会报错。如果我们在函数内部调用其他函数,函数间哪个声明在前、哪个在后就无所谓,因为 def 是可执行语句,函数在调用之前都不存在,我们只需保证调用时,所需的函数都已经声明定义

函数的参数

  • 必选参数
  • 默认参数
  • 可变参数
  • 关键字参数
  • 命名关键字参数

参数组合
在Python中定义函数,可以用必选参数、默认参数、可变参数、关键字参数和命名关键字参数,这5种参数都可以组合使用。但是请注意,参数定义的顺序必须是:必选参数、默认参数、可变参数、命名关键字参数和关键字参数。

函数变量作用域

Python 函数中变量的作用域和其他语言类似。如果变量是在函数内部定义的,就称为局部变量,只在函数内部有效。一旦函数执行完毕,局部变量就会被回收,无法访问,比如下面的例子:

1
2
3
def read_text_from_file(file_path):
with open(file_path) as file:
...

我们在函数内部定义了 file 这个变量,这个变量只在 read_text_from_file 这个函数里有效,在函数外部则无法访问。

相对应的,全局变量则是定义在整个文件层次上的,比如下面这段代码:

1
2
3
4
5
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 就是全局变量,可以在文件内的任何地方被访问,当然在函数内部也是可以的。不过,我们不能在函数内部随意改变全局变量的值。比如,下面的写法就是错误的:

1
2
3
4
5
6
7
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 这个声明:

1
2
3
4
5
6
7
8
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,
就是之前定义的全局变量,并不是新的全局变量,也不是局部变量。这样,程序就可以在函数内部访问全局变量,并修改它的值了。

另外,如果遇到函数内部局部变量和全局变量同名的情况,那么在函数内部,局部变量会覆盖全局变量,比如下面这种:

1
2
3
4
5
MIN_VALUE = 1
MAX_VALUE = 10
def validation_check(value):
MIN_VALUE = 3
...

在函数 validation_check() 内部,我们定义了和全局变量同名的局部变量 MIN_VALUE,那么,MIN_VALUE 在函数内部的值,就应该是 3 而不是 1 了。

类似的,对于嵌套函数来说,内部函数可以访问外部函数定义的变量,但是无法修改,若要修改,必须加上 nonlocal 这个关键字:

1
2
3
4
5
6
7
8
9
10
11
12
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 这个关键字,而内部函数的变量又和外部函数变量同名,那么同样的,内部函数变量会覆盖外部函数的变量。

1
2
3
4
5
6
7
8
9
10
11
def outer():
x = "local"
def inner():
x = 'nonlocal' # 这里的x是inner这个函数的局部变量
print("inner:", x)
inner()
print("outer:", x)
outer()
# 输出
inner: nonlocal
outer: local

函数嵌套

函数嵌套,就是指函数里面定义函数。

函数嵌套

1
2
3
4
5
6
def f1():
print('hello')
def f2():
print('world')
f2()
f1()

函数的嵌套,主要有下面两个方面的作用。

  1. 函数的嵌套能够保证内部函数的隐私。内部函数只能被外部函数所调用和访问,不会暴露在全局作用域,
    因此,如果你的函数内部有一些隐私数据(比如数据库的用户、密码等),不想暴露在外,
    那你就可以使用函数的的嵌套,将其封装在内部函数中,只通过外部函数来访问。
  2. 合理的使用函数嵌套,能够提高程序的运行效率。

闭包(closure)

什么是闭包?在一个内部函数中,对外部作用域的变量进行引用,(并且一般外部函数的返回值为内部函数),即使外部函数已经执行完毕,
内部函数仍然可以使用这些变量和参数,那么内部函数就被认为是闭包。构成条件:

  1. 函数嵌套
  2. 外部函数返回内部函数名
  3. 内部函数使用外部函数的变量

闭包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def example1(x):
# 嵌套函数inner(),则是一个闭包函数
def inner(y):
return x + y # 引用外部函数的变量

return inner

print(example1(6)(5))

def example2(a, b=1):
c = 100

def useC():
print(f'调用外部函数的变量,并打印:{c}') # 100
print(a + b) # 17

useC()

example2(6, 11) # 实参值会覆盖形参的值

闭包可以保存函数的状态信息,使函数的局部变量信息依然可以保存下来,将外层函数的变量持久地保存在内存中。
和上面讲到的嵌套函数优点类似,函数开头需要做一些额外工作,而你又需要多次调用这个函数时,将那些额外工作的代码放在外部函数,
就可以减少多次调用导致的不必要的开销,提高程序的运行效率。

以一个类似棋盘游戏的例子来说明。假设棋盘大小为50*50,左上角为坐标系原点(0,0),我需要一个函数,接收2个参数,分别为方向(direction),
步长(step),该函数控制棋子的运动。 这里需要说明的是,每次运动的起点都是上次运动结束的终点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def create(pos=origin):
def go(direction,step):
new_x = pos[0] + direction[0]*step
new_y = pos[1] + direction[1]*step
pos[0] = new_x
pos[1] = new_y
return pos

return go


player = create()
print(player([1,0],10))
print(player([0,1],20))
print(player([-1,0],10))

递归函数

在函数内部,可以调用其他函数。如果一个函数在内部调用自身本身,这个函数就是递归函数。下面就是一个递归函数。

1
2
3
4
def fact(n):
if n==1:
return 1
return n * fact(n - 1)

递归函数的优点是定义简单,逻辑清晰。理论上,所有的递归函数都可以写成循环的方式,但循环的逻辑不如递归清晰。

使用递归函数需要注意防止栈溢出。在计算机中,函数调用是通过栈(stack)这种数据结构实现的,每当进入一个函数调用,
栈就会加一层栈帧,每当函数返回,栈就会减一层栈帧。由于栈的大小不是无限的,所以,递归调用的次数过多,会导致栈溢出。

解决递归调用栈溢出的方法是通过尾递归优化,事实上尾递归和循环的效果是一样的,所以,把循环看成是一种特殊的尾递归函数也是可以的。
尾递归是指,在函数返回的时候,调用自身本身,并且,return语句不能包含表达式。这样,编译器或者解释器就可以把尾递归做优化,
使递归本身无论调用多少次,都只占用一个栈帧,不会出现栈溢出的情况。尾递归调用时,如果做了优化,栈不会增长,
因此,无论多少次调用也不会导致栈溢出。遗憾的是,大多数编程语言没有针对尾递归做优化,Python解释器也没有做优化,
所以,即使递归函数改成尾递归方式,也会导致栈溢出。

函数式编程

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

函数式编程的优点,主要在于其纯函数和不可变的特性使程序更加健壮,易于调试(debug)和测试;缺点主要在于限制多,难写。
当然,Python 不同于一些语言(比如 Scala),它并不是一门函数式编程语言,不过,Python 也提供了一些函数式编程的特性,
值得我们了解和学习。

高阶函数

  • map
    函数 map(function, iterable) 的第一个参数是函数对象,第二个参数是一个可以遍历的集合,
    它表示对 iterable 的每一个元素,都运用 function 这个函数,最后返回一个新的可遍历的集合。
  • reduce
    函数 reduce(function, iterable) 的第一个参数是函数对象,它通常用来对一个集合做一些累积操作。
    function 同样是一个函数对象,规定它有两个参数,表示对 iterable 中的每个元素以及上一次调用后的结果,
    运用 function 进行计算,所以最后返回的是一个单独的数值。
  • filter
    函数filter(function, iterable) 表示对 iterable 中的每个元素,都使用 function 判断,并返回 True 或者 False,
    最后将返回 True 的元素组成一个新的可遍历的集合。
  • sorted
    函数 sorted(iterable, key=None, reverse=False) 表示对 iterable 中的元素,进行排序,默认是升序。
    key 是一个函数对象,它规定了如何对 iterable 中的元素进行排序。用sorted()排序的关键在于实现一个映射函数。

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

  1. 在数据量非常多的情况下,比如机器学习的应用,那我们一般更倾向于函数式编程的表示,因为效率更高;
  2. 在数据量不多的情况下,并且你想要程序更加 Pythonic 的话,那么 list comprehension 也不失为一个好选择。

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

lambda 匿名函数

匿名函数:lambda 表达式,也叫lambda函数,是一种简单的函数,不需要声明,直接定义,然后使用。

匿名函数的格式:

1
lambda argument1, argument2,... argumentN : expression

匿名函数的关键字是 lambda,之后是一系列的参数,然后用冒号隔开,最后则是由这些参数组成的表达式。

匿名函数 lambda 和常规函数一样,返回的都是一个函数对象(function object),它们的用法也极其相似,不过还是有下面几点区别。

  • lambda 是一个表达式(expression),并不是一个语句(statement)。
    所谓的表达式,就是用一系列“公式”去表达一个东西,比如x + 2、 x**2等等;而所谓的语句,则一定是完成了某些功能,比如赋值语句x = 1完成了赋值,
    print 语句print(x)完成了打印,条件语句 if x < 0:完成了选择功能等等。

    因此,lambda 可以用在一些常规函数 def 不能用的地方,比如,lambda 可以用在列表内部,而常规函数却不能:

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

    再比如,lambda 可以被用作某些函数的参数,而常规函数 def 也不能:

    1
    2
    3
    4
    5
    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. 模块化代码。

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

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

装饰器(Decorator)

装饰器是Python中最吸引人的特性,装饰器本质上还是一个函数,它可以让已有的函数不做任何改动的情况下增加功能。而实际工作中,装饰器通常运用在身份认证、日志记录、输入合理性检查以及缓存等多个领域中。合理使用装饰器,往往能极大地提高程序的可读性以及运行效率。

  • 简单装饰器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def my_decorator(func):
def wrapper():
print('wrapper of decorator')
func()
return wrapper

def greet():
print('hello world')

greet = my_decorator(greet)
greet()

# 输出
wrapper of decorator
hello world

这段代码中,变量 greet 指向了内部函数 wrapper(),而内部函数 wrapper() 中又会调用原函数 greet(),因此,最后调用 greet() 时,就会先打印’wrapper of decorator’,然后输出’hello world’。

这里的函数 my_decorator() 就是一个装饰器,它把真正需要执行的函数 greet() 包裹在其中,并且改变了它的行为,但是原函数 greet() 不变。

事实上,上述代码在 Python 中有更简单、更优雅的表示:

1
2
3
4
5
6
7
8
9
10
11
def my_decorator(func):
def wrapper():
print('wrapper of decorator')
func()
return wrapper

@my_decorator
def greet():
print('hello world')

greet()

这里的@,我们称之为语法糖,@my_decorator就相当于前面的greet=my_decorator(greet)语句,只不过更加简洁。因此,如果你的程序中有其它函数需要做类似的装饰,你只需在它们的上方加上@decorator就可以了,这样就大大提高了函数的重复利用和程序的可读性。

  • 带有参数的装饰器
1
2
3
4
5
def my_decorator(func):
def wrapper(*args, **kwargs):
print('wrapper of decorator')
func(*args, **kwargs)
return wrapper
  • 带有自定义参数的装饰器
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
def repeat(num):
def my_decorator(func):
def wrapper(*args, **kwargs):
for i in range(num):
print('wrapper of decorator')
func(*args, **kwargs)
return wrapper
return my_decorator


@repeat(4)
def greet(message):
print(message)

greet('hello world')

# 输出:
wrapper of decorator
hello world
wrapper of decorator
hello world
wrapper of decorator
hello world
wrapper of decorator
hello world
  • 保留原函数元信息

greet() 函数被装饰以后,它的元信息变了。元信息告诉我们“它不再是以前的那个 greet() 函数,而是被 wrapper() 函数取代了”。为了解决这个问题,我们通常使用内置的装饰器@functools.wrap,它会帮助保留原函数的元信息(也就是将原函数的元信息,拷贝到对应的装饰器函数里)。

  • 类装饰器

类也可以作为装饰器。类装饰器主要依赖于函数__call_(),每当你调用一个类的示例时,函数__call__()就会被执行一次。

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
class Count:
def __init__(self, func):
self.func = func
self.num_calls = 0

def __call__(self, *args, **kwargs):
self.num_calls += 1
print('num of calls is: {}'.format(self.num_calls))
return self.func(*args, **kwargs)

@Count
def example():
print("hello world")

example()

# 输出
num of calls is: 1
hello world

example()

# 输出
num of calls is: 2
hello world

...
  • 装饰器嵌套

Python 也支持多个装饰器,比如写成下面这样的形式:

1
2
3
4
5
@decorator1
@decorator2
@decorator3
def func():
...

它的执行顺序从里到外,所以上面的语句也等效于下面这行代码:

1
decorator1(decorator2(decorator3(func)))

偏函数

偏函数是Python中一个很有用的特性,它可以让一个函数的某些参数固定,从而简化函数的调用。

Python 面向对象编程

传统的命令式语言有无数重复性代码,虽然函数的诞生减缓了许多重复性,但随着计算机的发展,只有函数依然不够,
需要把更加抽象的概念引入计算机才能缓解(而不是解决)这个问题,于是 OOP 应运而生。

面型对象是一种符合人类思维习惯的编程思想。客观世界中存在多种形态的事务,这些事物之间存在各种各样的联系。
在程序中使用对象来模拟现实中的事务,使用对象之间的关系来描述事物之间的联系,这种思想就是面对对象。

面向对象编程概念

  • 类:具有相同属性和功能的对象的抽象,这里对应 Python 的 class。
  • 对象:一切事物皆为对象,将事物的属性和方法封装在一起,形成一个实体,这个实体就是对象。
  • 属性:对象的某个静态特征。

定义

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
28
29
30
31
32
33
34
35
36
class Document():

WELCOME_STR = 'Welcome! The context for this book is {}.'

def __init__(self, title, author, context):
print('init function called')
self.title = title
self.author = author
self.__context = context

# 类函数
@classmethod
def create_empty_book(cls, title, author):
return cls(title=title, author=author, context='nothing')

# 成员函数
def get_context_length(self):
return len(self.__context)

# 静态函数
@staticmethod
def get_welcome(context):
return Document.WELCOME_STR.format(context)


empty_book = Document.create_empty_book('What Every Man Thinks About Apart from Sex', 'Professor Sheridan Simove')


print(empty_book.get_context_length())
print(empty_book.get_welcome('indeed nothing'))

########## 输出 ##########

init function called
7
Welcome! The context for this book is indeed nothing.

三大特性

  • 封装

  • 继承
    类的继承,顾名思义,指的是一个类既拥有另一个类的特征,也拥有不同于另一个类的独特特征。在这里的第一个类叫做子类,
    另一个叫做父类,特征其实就是类的属性和函数。

    首先需要注意的是构造函数。每个类都有构造函数,继承类在生成对象的时候,是不会自动调用父类的构造函数的,因此你必须在 init() 函数中显式调用父类的构造函数。它们的执行顺序是 子类的构造函数 -> 父类的构造函数。

    抽象类是一种特殊的类,它生下来就是作为父类存在的,一旦对象化就会报错。同样,抽象函数定义在抽象类之中,
    子类必须重写该函数才能使用。相应的抽象函数,则是使用装饰器 @abstractmethod 来表示。

  • 多态

面向对象 VS 面向过程

Python 的错误、调试和测试

语法错误

语法错误就是解析代码时出现的错误。当代码不符合Python语法规则时,Python解释器在解析时就会报SyntaxError语法错误,还会指出最早探测到错误的语句。
语法错误是解释器无法容忍的,必须全部纠正才能运行。

异常

异常则是指程序的语法正确,也可以被执行,但在执行过程中遇到了错误,抛出了异常。

异常处理 try except

异常处理,通常使用 try 和 except 来解决。except block 只接受与它相匹配的异常类型并执行,如果程序抛出的异常并不匹配,那么程序照样会终止并退出。
很多时候,我们很难保证程序覆盖所有的异常类型,所以,更通常的做法,是在最后一个 except block,声明其处理的异常类型是 Exception。
Exception 是其他所有非系统异常的基类,能够匹配任意非系统异常。或者,也可以在 except 后面省略异常类型,这表示与任意异常相匹配(包括系统异常等)。

需要注意,当程序中存在多个 except block 时,最多只有一个 except block 会被执行。换句话说,如果多个 except 声明的异常类型都与实际相匹配,
那么只有最前面的 except block 会被执行,其他则被忽略。

异常处理中,还有一个很常见的用法是 finally,经常和 try、except 放在一起来用。无论发生什么情况,finally block 中的语句都会被执行,
哪怕前面的 try 和 excep block 中使用了 return 语句。因此,在 finally 中,我们通常会放一些无论如何都要执行的语句。

调试程序时看某些库的源代码,发现有如下代码读不懂,不理解后面这个from干什么用的。

1
2
3
4
5
6
7
8
9
try:
...
except KeyError:
raise **Error('') from None

try:
...
except Exception as exc:
raise **Error('') from exc

先看普通写法,控制台会输出什么,结果如下。控制台输出了2个异常发生的位置和原因,同时在2个提示中间输出一句话“在处理上述异常时,又发生了另一个异常”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
try:
print(1/0)
except Exception as exc:
raise RuntimeError('程序执行过程中发生错误')

Traceback (most recent call last):
File "D:/*/tests.py", line 5, in <module>
print(1/0)
ZeroDivisionError: division by zero

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
File "D:/*/tests.py", line 7, in <module>
raise RuntimeError('程序执行过程中发生错误')
RuntimeError: 程序执行过程中发生错误

再看raise **Error(‘’) from exc写法,控制台输出了什么,结果如下。控制台输出了2个异常发生的位置和原因,同时在2个提示中间输出一句话“上述异常是下列异常的直接原因”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
try:
print(1/0)
except Exception as exc:
raise RuntimeError('程序执行过程中发生错误') from exc

Traceback (most recent call last):
File "D:/WorkSpace/backend/user/tests.py", line 5, in <module>
print(1/0)
ZeroDivisionError: division by zero

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
File "D:/WorkSpace/backend/user/tests.py", line 7, in <module>
raise RuntimeError('程序执行过程中发生错误') from exc
RuntimeError: 程序执行过程中发生错误

最后看raise **Error(‘’) from None写法,控制台输出了什么,结果如下。控制台只输出了我们写的抛出异常。

1
2
3
4
5
6
7
8
9
try:
print(1/0)
except Exception as exc:
raise RuntimeError('程序执行过程中发生错误') from None

Traceback (most recent call last):
File "D:/WorkSpace/backend/user/tests.py", line 7, in <module>
raise RuntimeError('程序执行过程中发生错误') from None
RuntimeError: 程序执行过程中发生错误

总结:from 会为异常对象设置 cause 属性表明异常的是由谁直接引起的。处理异常时发生了新的异常,
在不使用 from 时更倾向于新异常与正在处理的异常没有关系。而 from 则是能指出新异常是因旧异常直接引起的。
这样的异常之间的关联有助于后续对异常的分析和排查。from 语法会有个限制,就是第二个表达式必须是另一个异常类或实例。
如果在异常处理程序或 finally 块中引发异常,默认情况下,
异常机制会隐式工作会将先前的异常附加为新异常的 __context__属性。当然,
也可以通过with_traceback()方法为异常设置上下文__context__属性,这也能在traceback更好的显示异常信息。
from 还有个特别的用法:raise … from None ,它通过设置 suppress_context 属性指定来明确禁止异常关联。
在异常处理程序或finally块中引发异常,可以通过from来指定异常因谁引起的。这些手段都是为了得到更友好的异常回溯信息,
打印清晰的异常上下文。若要忽略上下文,则可以通过 raise … from None 来禁止自动显示异常上下文。

用户自定义异常

实际工作中,如果内置的异常类型无法满足我们的需求,或者为了让异常更加详细、可读,想增加一些异常类型的其他功能,我们可以自定义所需异常类型。

1
2
3
4
5
6
7
8
9
10
11
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))

异常的使用场景与注意点

通常用在你不确定某段代码能否成功执行,也无法轻易判断的情况下,比如数据库的连接、读取等等。有一点切记,我们不能走向另一个极端——滥用异常处理。
正常的 flow-control(流程控制) 逻辑,不要使用异常处理,直接用条件语句解决就可以了。

Python 文件处理和IO编程

文件输入输出

事实上,计算机内核(kernel)对文件的处理相对比较复杂,涉及到内核模式、虚拟文件系统、锁和指针等一系列概念。

Python操作文件输入输出先要用 open() 函数拿到文件的指针。其中,第一个参数指定文件位置(相对位置或者绝对位置);
第二个参数,如果是 ‘r’ 表示读取,如果是’w’ 则表示写入,当然也可以用 ‘rw’ ,表示读写都要。
a 则是一个不太常用(但也很有用)的参数,表示追加(append),这样打开的文件,如果需要写入,会从原始文件的最末尾开始写入。

在拿到指针后,我们可以通过 read() 函数,来读取文件的全部内容。代码 text = fin.read() ,即表示把文件所有内容读取到内存中,
并赋值给变量 text。这么做自然也是有利有弊:

  • 优点是方便,接下来我们可以很方便地调用text;
  • 缺点是如果文件过大,一次性读取可能造成内存崩溃。

这时,我们可以给 read 指定参数 size ,用来表示读取的最大长度。还可以通过 readline() 函数,每次读取一行,
这种做法常用于数据挖掘(Data Mining)中的数据清洗,在写一些小的程序时非常轻便。如果每行之间没有关联,
这种做法也可以降低内存的压力。而 write() 函数,可以把参数中的字符串输出到文件中,也很容易理解。

这里我需要简单提一下 with 语句。open() 函数对应于 close() 函数,也就是说,如果你打开了文件,在完成读取任务后,就应该立刻关掉它。
而如果你使用了 with 语句,就不需要显式调用 close()。在 with 的语境下任务执行完毕后,close() 函数会被自动调用,代码也简洁很多。

最后需要注意的是,所有 I/O 都应该进行错误处理。因为 I/O 操作可能会有各种各样的情况出现,而一个健壮(robust)的程序,
需要能应对各种情况的发生,而不应该崩溃(故意设计的情况除外)。

Python 的并发编程

进程

线程

协程

Python的脚本编程与系统管理

Python 的正则表达式

模块与包

Python 常用内置模块

Python 常用第三方模块

Python 图形界面

Python 网络与 Web 编程

Python 电子邮件

Python 访问数据库

Python 的 Web 开发

使用 MicroPython

Python 高级特性

切片

迭代

列表生成式

生成器(Generator)

生成器是一个特殊的迭代器,并且它也是一个可迭代对象。有 2 种方式可以创建一个生成器:

  1. 生成器表达式
  2. 生成器函数

迭代器(Iterator)

迭代器是带状态的对象,它会记录当前迭代所在的位置,以方便下次迭代的时候获取正确的元素。一个对象要想使用 for 的方式迭代出容器内的所有数据,这就需要这个类实现「迭代器协议」。也就是说,一个类如果实现了迭代器协议,就可以称之为迭代器。

在 Python 中,实现迭代器协议就是实现以下 2 个方法:

  1. __iter__:这个方法返回对象本身,即 self
  2. __next__:这个方法每次返回迭代的值,在没有可迭代元素时,抛出 StopIteration 异常
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class A:
"""A 实现了迭代器协议 它的实例就是一个迭代器"""
def __init__(self, n):
self.idx = 0
self.n = n

def __iter__(self):
return self

def __next__(self):
if self.idx < self.n:
val = self.idx
self.idx += 1
return val
else:
raise StopIteration()

元编程

C语言扩展

开发者有如下方法在Python代码中调用C编写的函数,每种方式都有各自的利弊。要明确为什么在Python中调用C,常见原因如下:
提升代码运行速度;
C语言中有很多传统类库,这些是想用的,但不想用Python重写;
想对内存到文件接口这种底层资源进行访问;

  • ctypes
    Python中ctypes模块可能是Python调用C最简单一种方法,ctypes模块提供了和C语言兼容的数据类型和函数来加载dll文件,因此,
    在调用时不需要对源文件做任何的修改。

    1. 编写C语言代码并保存。

      1
      2
      3
      4
      5
      #include<stdio.h>

      int add_int(int num1, int num2){
      return num1+num2;
      };
    2. 将C语言代码文件编译为.so文件。gcc -shared -Wl,-soname,adder -o adder.so -fPIC add.c

    3. 在Python代码中调用.so文件。

      1
      2
      3
      4
      5
      from ctypes import *

      adder = CDLL('./adder.so')
      res_int = adder.add_int(4,5)
      print(res_int)
  • SWIG

  • Python/C API

数据结构与算法

数据编码与处理

base64

JSON

JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,它的设计意图是把所有事情都用设计的字符串来表示,
这样既方便在互联网上传递信息,也方便人进行阅读(相比一些 binary 的协议)。

  • json.dumps() 这个函数,接受 Python 的基本数据类型,然后将其序列化为 string。
  • json.loads() 这个函数,接受一个合法字符串,然后将其反序列化为 Python 的基本数据类型。

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

1
2
3
4
5
try:
data = json.loads(raw_data)
....
except JSONDecodeError as err:
print('JSONDecodeError: {}'.format(err))

新版本特性

3.10 引入了 match 语句

1
2
3
4
5
6
7
8
9
10
grade = 3
match grade:
case 1:
print('一年级')
case 2:
print('二年级')
case 3:
print('三年级')
case _:
print('未知年级')

3.9 增加了合并和更新运算符 (|=)

1
2
3
4
5
6
dictA = {'A': 'a', 'B': 'b'}
dictB = {1: 1, 2: 2}
dictC = {3: 3, 4: 4}

dictA |= dictB
merge_dict = dictB | dictC

3.8 增加了 F-strings 表达式

1
2
name = "foo"
f"Hello, my name is {name}"

3.8 新增海象运算符

1
2
3
4
5
6
7
8
lis = [1, 2, 3]
length = len(lis)
if length > 3:
print("lis列表拥有{}个元素,大于3。".format(length))

# 使用海象运算符后
if length := len(lis) > 3:
print("lis列表拥有{}个元素,大于3。".format(length))

3.5 新增类型注解

1
2
3
4
5
6
age: int = 8
name: str = "zhangsan"

from typing import List, Set, Dict, Tuple
lis: List[str] = ["zhangsan","lisi"]
dic: Dict[str, int] = {"zhangsan": 18}

函数参数和返回值的类型声明

1
2
def add(a: int, b: int) -> int:
return a + b