跳到主要内容

Python 学习总结

万人操弓,共射其一招,招无不中。


导航


壹-变量

1、变量类型

变量基本类型:int 整型、float 浮点型、str 字符串、list 列表、tuple 元组、range 范围、dict 字典、set 集合。

(1)基本类型的类型。在 C 语言中,变量的基本类型就是原子类型,这些类型本身并不存在其它额外的功能;但是 Python 中的基本类型却是一个 class 类,这些类型的变量均属于一个类实例对象,它拥有类内部定义的功能方法。【猜测:变量类型的类内部实现使用的变量还是 C 语言的原子类型,毕竟封装的最后都是要回到原子处的。】例如,int.bit_length() 可以计算出该整型数字所占用的字节。

(2)变量长度。在 C 语言中提供了 short、int、long、long long 四种类型的整数,每种类型的长度都不同,能容纳的整数的大小也不同;但是 Python 中的整型不分类型(或者说它只有一种类型的整数),它的取值范围是无限的,不管多大或者多小的数字,Python 都能轻松处理。当然小整数占用的内存空间自然也少,大整数占用的内存空间自然也大。可通过方法 int.bit_length() 查看整数占用的内存空间。

(3)变量类型转换函数。int()、float()、str()、list()、tuple()、dict()、set()。可实现不同类型之间的转换,若在一个 class 类中重写了__int__方法,则可通过 int(class) 将类实例对象转换为 int 整型。

(4)在 python 赋值表达式中 a = 123,= 号右侧的类型(值)属于对象,= 号左侧的属于变量。对象有不同类型的区分,变量是没有类型的,它仅仅是一个对象的引用(一个指针),可以是指向 List 类型对象,也可以是指向 String 类型对象。【注:理解函数参数传递时必需了解此含义】

2、数据结构

  1. 列表:python 中列表类型的对象相当于数组,封装处理之后它可以当做数据结构中的数组、堆栈、队列使用,只是在效率上会有所差异。(默认情况下,列表相当于堆栈,以队列的方式使用时需要 import deque)例,list1 = ['Google', 'Runoob', 1997, 2000]

  2. 元组:就是一个只读不能写的列表。例,tup1 = ('Google', 'Runoob', 1997, 2000)

  3. 范围:相当于一个元组,也支持索引切片的功能,但是不能更改变量的值。使用它的好处在于,可以使用固定长度的表达式表达出任意范围的值。

  4. 集合:是一个无序不重复元素的集。基本功能包括关系测试(交并关系)和消除重复元素。例,set1 = {'apple', 'orange', 'apple', 'pear', 'orange', 'banana'}

  5. 字典:序列是以连续的整数为索引,与此不同的是,字典以关键字为索引,关键字可以是任意不可变类型且必须互不相同,通常用字符串或数值。基本功能是快速索引数据。例,dict1 = {'name': 'runoob', 'likes': 123, 'url': 'www.runoob.com'}

优缺点:(1)list 是线性存储,数据量大的时候,插入和删除效率很低。(2)deque 是为了高效实现插入和删除操作的双向列表,适合用于队列和栈。

3、变量及作用域

(1)作用域主要分为全局作用域(global)、嵌套作用域(enclosing,介于全局与局部之间的作用域,主要出现在函数嵌套函数的函数中。)、局部作用域(local)

(2)变量的查找顺序:局部作用域-> 嵌套作用域-> 全局作用域,若最终全局作用域也未找到则报错。

(3)全局定义的变量可以被其下的局部作用域读取,但是不能被修改,除非使用关键字 global 和 nonlocal。

(4)当内部作用域想修改全局作用域的变量时,需使用 global 关键字在内部作用域声明变量;当内部作用域想修改嵌套作用域的变量时,需使用 nolocal 关键字在内部作用域声明变量。否则报错,若通过参数传递的方式 def test(a),则也正常打印值,因为参数的存在相当于已经在函数中定义过变量 a,故不报错。

image

(5)Python 对在函数定义体中 赋值的变量都认为是局部变量。从而导致局部变量 b 未赋值先使用的问题。

(6)全局定义的变量,如果通过参数传递给函数,那么函数可以对该变量指向的对象进行值的更改;如果不通过参数传递而是直接当做全局变量来使用,那么该变量可以被读取但是不能写入,如果需要写入则必须在函数开头处对该全局变量进行 global 的声明。【注:全局定义的变量属于一个类实例对象,函数参数传递时相当于传递了对象的物理地址给了一个变量,因此参数传递的变量可以被修改,且修改的就是全局定义的变量本身。】

4、数组-切片

(1)字符串访问子串的截取格式:变量[头下标: 尾下标: 步长](截取方式是左闭右开,默认步长是 +1

(2)关于索引号的规则:相当于将字符串首尾连接,然后以 0 为界,正向为正,反向为负。

(3)切片用法。【1】字串全部遍历:list[:]【2】字串的反向遍历:list[::-1]【3】字串后 5 个值:list[-5:]【4】遍历全部并择出偶数位的值:list[::2]

image

5、列表推导式

列表推导式:提供了从序列创建列表的简单途径。通常应用程序将一些操作应用于某个序列的每个元素,用其获得的结果作为生成新列表的元素,或者根据确定的判定条件创建子序列。 【注:除了元组以外,列表、集合、字典都支持推导式】

image

6、杂项

  1. 判断赋值:a = 1 if 1 > 2 else 0:如果 1 大于 2 则 a 等于 1,否则 a 等于 0。

  2. 列表推导式-格式化字符串:['%s = "%s"' %item for item in d1.items()] 输入:size = 6 输出 size = "6"


貳-流程控制

1、条件控制(if)

image

  • Python 中没有 switch – case 语句,但从 python 3.10 之后,新增了 match – case 语法。

2、循环控制(while、for)

image

image

  • Python 中没有 do..while、foreach 这些循环

3、杂项

  1. continue、break 只有在循环中,它们才可以与 if 等语句搭配使用,否则会报错或不生效。

  2. 条件表达式是否需要括号?答:不管是条件控制还是循环控制,它们所认定的条件只有两个值:true、false。所以只要条件表达式最终表现出来的结果符合 bool 类型就行,是否带括号无所谓,pycharm 中带括号被认为是冗余。


叁-函数

1、参数传递

在 python 中,类型属于对象,变量是没有类型的。如在表达式 a =[1,2], a ='12'中,[1,2] 是 list 类型的对象,'12' 是 string 类型的对象,而变量 a 是没有类型的,它只是一个对象的引用(一个指针),可以指向任何类型的对象。【注:在 c 语言中,每个变量都对应一个类型,即便是指针也是占用内存空间且拥有类型的,而 python 中的变量,它相当于一个 void 的指针,可以存储任何类型的地址,以引用的方式使用对象。这种变量也是占用内存空间的,但是 python 将其隐身了,使我们无法看到它的存储地址。】

image

2、参数格式

  1. 必需参数:def fun(arg1, arg2)。例, fun(1,2)
  2. 关键字参数:def fun(arg1, arg2)。例,fun(arg2 = 1, arg1 = 2)
  3. 默认参数:def fun(arg1, arg2 = 2)。例, fun(1)
  4. 不定长参数:def fun(arg1,*arg)。例,fun(1,2,3) (多余参数以元组形式导入, arg =(2,3))
  5. 只接受关键字参数:def fun(arg1, *, arg2)。例,fun(1, arg2 = 2)【注:将强制关键字参数放到某个*参数或者单个*后面就能达到这种效果。这是因为*表示位置参数列表,在*之前的位置参数全部会进入*号,此时要给*后面的参数传递值,就只能通过关键字参数传递,关键字必须对应 arg2,否则应该会出错。】
  6. 不定长参数组合默认参数:def fun(arg1,*arg, key = None)。例, fun(1,2, key = key) 【注:当需要为默认变量传入参数时,参数列表必须指明关键参数,否则报错】
  7. 不定长参数:def fun(arg1,**arg)。例,fun(1, a = 2, b = 3) 【注:多余参数以字典形式导入, arg={'a':2,'b':3})】
  8. 函数参数(闭包):详见剪藏笔记(返回函数)。

3、匿名函数 lambda

  1. 语法格式:lambda [arg1[, arg2, args]]: expression。例,sum = lambda arg1, arg2: arg1 + arg2; sum(1,2)

  2. lambda 的主体是一个表达式,而不是一个代码块。仅仅能在 lambda 表达式中封装有限的逻辑进去。

  3. lambda 函数拥有自己的命名空间,且不能访问自己参数列表之外或全局命名空间里的参数。

  4. 默认参数的匿名函数。x = 10; a = lambda y, x = x: x+y; x = 20; b = lambda y: x+y; 虽然两匿名函数除了参数部分不同以外其余均一致,但是两者在变量取值上还是有所不同。默认参数的匿名函数 a 可以达到定义即固定参数 x 值的效果;无默认参数的匿名函数 b,它的 x 值取决于函数运行时 x 实际的值。

4、函数装饰器

(1)装饰器:用于修饰函数,以增强函数的行为(记录函数执行时间,建立和撤销环境,记录日志等),装饰器可以在不修改函数内部代码的前提下实现增强行为。

(2)装饰函数就是在其内部定义了一个新的增强函数,该增强函数会调用被装饰函数并增加一些其它的功能。

(3)语法是固定的。装饰函数的参数总是函数对象timethis(func),返回总是新增强函数对象 inner;新增强函数的参数理应与被装饰函数的参数相一致,但是实现上使用了两个不定长参数充当了万能参数 inner(*args,**kwargs),新增强函数的返回总是被装饰函数的返回值 result

def timethis(func):
def inner(*args,**kwargs):
...
result = func(*args,**kwargs)
...
return result
return inner
@timethis
def sleeps(seconds):
...

(4)使用 @timethis 修饰 sleeps,然后执行语句 sleeps(3) 和语句 sleeps = timethis(sleeps);sleeps(3)是等价的。

(*)详细示例见文章 5分钟掌握 Python 中的装饰器

5、返回函数(闭包)

(1)闭包。返回的函数在其定义内部引用了局部变量 args,所以当一个函数返回了一个函数后,其内部的局部变量还可以被新函数引用,这就是闭包技术。

image

(2)闭包时机。内联函数只有被 return 返回时,python 才会将内联函数(即返回函数)所使用的外部局部变量进行打包以与该返回函数关联。例,如下多个返回函数被统一打包和分开打包的代码实现区别。

image

(3)闭包读写。闭包读关联变量不会有问题,但是闭包写关联变量时就需要注意变量作用域中在局部函数声明全局变量进行读写的事项。

6、递归函数

如果一个函数在内部调用自身本身,这个函数就是递归函数。

(1)递归函数的实现需要确定 2 个条件:终止条件、循环表达式。

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

例,阶乘算法实现如下

image

7、杂项

  1. return 语句用于退出函数,选择性地向调用方返回一个表达式。不带参数值的 return 语句返回关键字 None。

肆-类

1、类 - 结构

类是由类属性和实例属性组合而成,属性又包含变量和方法。

  1. 属性访问特性。私有属性(__ver 表示私有变量,仅供类内部调用)、公有属性(ver 表示公有属性,供类内外调用)、特殊属性(__solts__/__init__等表示特殊属性,均有特殊含义在特定情况下被调用)。
  2. 类/实例属性区别。类变量归整个类/类实例所有,该变量可以被类和实例直接读取,但是只能被类更改,更改之后影响所有实例再次读取到的值;实例变量属于类实例所独有,仅能通过该实例去读取或更改。
  3. 类实例动态绑定属性。支持动态绑定变量/方法,绑定后的变量/方法仅供该实例所使用。(可以通过定义类变量 __solts__ 来限制动态绑定的属性数量及名称)
  4. 类属性继承规则。private{__只能被类内部使用,继承之后无法被使用}protected{_只能被继承的类内部使用(实测也可以在类外使用,与 public 无异)}public{类的内外部均可使用}
  5. 面向对象三大特性:封装(将对象相关的方法和属性揉合在了一起)、继承(提高了代码的复用性)、多态(在基类中创建的方法,在子类中分别重写此方法,以父类对象作为函数参数,最后传入到参数的子类是什么类型的对象,在函数中就会对应调用这个实例的方法,这就是多态)。

2、类 - 魔法方法

这类方法不需要我们手动调用(但也可以手动调用),在满足某个条件时会自动调用,这个满足的条件我们可以称为调用时机。

  1. __str__的调用时机:在使用 print 打印对象、对象的格式化输出以及调用 str 方法,调用的都是__str__方法。

  2. __repr__的调用时机:在交互环境下直接输出对象以及将对象放在容器中进行输出,调用的都是__repr__ 方法。(非交互环境下直接输出对象不显示输出)

  3. __add__/__eq__/__iadd__/__float__的调用时机:在使用操作符运算时,会自动调用操作符所对应的方法。如加减法、大于小于、原地赋值中的加减法(a += 1)、强制类型转换中的浮点转换等。

  4. __call__的调用时机:将类实例对象当做函数来调用时,调用的都是__call__方法。

  5. __iter__/__next__的调用时机:当对类实例使用迭代器函数 iter()/next()时,将自动调用类的__iter__/__next__方法。同类型的还有__int__、__str__等基础变量类型的魔法方法。

  6. __getitem__/__setitem__/__delitem__/__len__/__contains__的调用时机:使一个自定义类拥有 集合/序列对象中使用下标[ name]进行集合中项值的获取、设置、删除,以及集合中项的总长度【len( list( 1,2 ) )】、是否包含关键值【'name' in dict( name = 'test' )】的功能。【注:这些魔法方法的实现通常配合 python 内置的反射机制函数一起使用,getattr()、setattr()、delattr()、hasattr()】

  7. __enter__/__exit__的调用时机:在 with 上下文管理器中,当 with 语句刚开始执行时会自动调用__enter__,当 with 语句块中的代码正常退出或不正常退出时都会自动调用__exit__。【代码实现上需要注意返回值的类型】

  8. __getattr__/__setattr__的调用时机:类实例对象属性访问控制。(1)在一个类实例对象中通过__dict__失败查找不到属性时, 就会调用到类的__getattr__函数,如果没有定义这个函数,那么抛出 AttributeError 异常。也就是说__getattr__是属性查找的最后一步【class1.noname 因无此属性便会调用类的__getattr__函数进行警告说明,若有此属性则正常输出值】;(2)当使用 class1.name = 123 进行赋值时,便会自动调用类的__setattr__函数。【该函数的实现需要特别注意,常用套路如,def __setattr__(self, name, value): self.__dict__[name] = value 】。

3、类 - 特殊变量

  1. 类特殊变量__solts__,可以通过定义类变量__solts__的值来限制类实例动态绑定的属性数量及名称。

  2. 类特殊变量__dict__,分别返回类/类实例对象的所有属性名为 key,属性值为 value 的一个字典【实例.__dict__类.__dict__ 返回的都是各自相关属性的字典】。相对应的 dir( class1 )函数则会返回这个类所有相关属性的一个列表。

  3. 类特殊变量__doc__,返回一个描述类功能的字符串。只需在类开头的位置通过块注释对此类进行描述,python 就会自动将这个描述的值赋值给__doc__变量。

4、魔法方法__str____repr__的区别:

  1. 因为这两种方法都是和打印相关的,所以它们的返回值格式应该是 return ‘...’.format(...)。否则就需要通过手动调用__str__的方式进行输出的打印。
  2. 如果没有重写__str__方法,但重写了__repr__方法时,所有调用__str__的时机都会调用__repr__方法。

5、类中的 self 参数方法、cls 参数方法、静态方法的区别

  1. self 是类(Class)实例化对象,只能通过实例化对象来调用此方法。
  2. cls 是类(或子类)本身,不需要实例化对象就可以通过 类名.方法 的方式调用。
  3. 静态方法和 cls 方法本质上是一样的,但是通过静态函数使用类内部的属性/方法时,需要指定 类名.方法 来调用(即便该函数就是在类中定义的)。(这种方法与类有某种关系但不需要使用到实例或者类来参与)
  4. @staticmethod 属于静态方法装饰器,@classmethod 属于类方法装饰器。
  5. 使用 cls 参数的原因:因为静态方法调用类内部属性/方法总是要携带类名过于繁琐,为了便于统一直接用 cls().方法 来代替 类名.方法

6、类的多重继承

(1)MixIn 的目的就是给一个类增加多个功能,这样,在设计类的时候,我们优先考虑通过多重继承来组合多个 MixIn 的功能,而不是设计多层次的复杂的继承关系。(将各个特征进行 mixin 单类化,那么设计一个拥有多特征的类时,只需要通过多继承将这些特征类进行组合继承,即可得到需要的类。相比于单一继承继承,这种方式更便于阅读各类之间的关系。)

(2)关于类多继承中 super() 函数和类的方法解析顺序 MRO 列表的关系(注意继承中重写方法的优先级)。

  1. Python 会对每一个类构造一个 mro 列表(通过_mro_可查看)。使用一个属性或方法时,python 并不知道该方法是从哪个基类继承下来,故需要根据 mro 列表依次从左往右的类中进行查找,直至找到,找不到则报错。mro 仅是一个线性关系表,而继承是具有层级关系,故通过 super()【从子类直接跳到父类,而不需依据 mro 顺序进行搜索】就可以使得查找过程左到右上到下的进行。

  2. 例如,Base,A(Base),B(Base),C(A, B)构造的 mro 列表为 C.__mro__(< class '__main__.C'>, < class '__main__.A'>, < class '__main__.B'>,< class '__main__.Base'>, < class 'object'>)

(1)迭代器是一个变量,该变量的值可以是由可迭代的类通过 iter(class) 返回提供(返回值还是该实例,只不过动态绑定了变量),也可以是由调用生成器函数返回提供(返回值是一个系统临时构建的生成器类实例,而非一个值)。

(2)生成器,一个包含了 yeild 的普通函数将不在是一个普通的函数,而会成为一个生成器。

image

(3)list、元组这些类都属于可迭代类,因此它们的实例对象都可以使用迭代器。

(4)可迭代属性 Iterable 和迭代器 Iterator。【以下参考用例,it = iter( l1 = list(1,2,3) )

可迭代属性:只要对象拥有可迭代属性__iter__/__next__,那么不管该对象的类型是 list、元组、还是自定义的类,它都可以被 for in 循环迭代。(检测类型是否可迭代,isinstance('abc', Iterable))【l1 变量是 list 类型,它就是拥有可迭代属性的可迭代对象。】

迭代器:只要对象可以被 next() 函数调用并不断返回下一个值则称为迭代器:Iterator。【it 变量可以被 next() 函数调用并不断返回值的对象,它就是迭代器。】

7、杂项

  1. 使用 0.数据名称 的格式,这是类专有的打印格式,与 private、protected 这些属性一起使用时问题较多,不建议使用。("name={0.name} speed={0.speed}".format(self))

  2. 类装饰器 @property,可以将类方法转换为变量,实现在隐藏类内部属性的同时,又可以对由外部传递给内部属性的值进行检查。

  3. 类多态函数所传递的实例变量并非严格要求必须是一个继承类的实例,它也可以是另外一个毫不相关的类,只要该类和继承类拥有共同的被调同名函数即可。如,一个关于动物类的多态,但是允许在多态函数中传入一个植物类的实例对象。

  4. 同一个类不同实例对象调用同一个方法时,该方法的 id 值是一样的。如 id(c1.getm) 等于 id(c2.getm),说明类方法和类实例是分开的。


伍-文件与网络

1、文件 IO 属性的特性表

image

区别:r+w+ 虽然都是可读可写,但是它们对待被操作文件是否存在时的方式不同,r+ 显示报错,w+ 直接创建不存在文件。

2、文件常见操作函数

  • open()、read()、close()、seek() 移动文件指针所处的位置。
  • tell() 返回文件指针当前所在的字节位置。
  • readline()、readlines() 读取文件的所有行并返回一个行列表。
  • writelines() 向文件写入一个序列列表。
  • flush() 立即将内存缓冲区中的内容写入磁盘文件中。

3、读文件的三个函数的应用场景

(1)read() 会一次性读取文件的全部内容,如果文件很大有 10G,内存就爆了,所以,要保险起见,可以反复调用 read(size) 方法,每次最多读取 size 个字节的内容;(2)readline() 可以每次读取一行内容;(3)readlines() 一次读取所有内容并按行返回 list,适用于配置文件的读取。

4、内存中数据的读写

以往文件的读写都是在磁盘中进行,但也可以在内存中开辟一块位置,在该位置进行数据的读写。实现此目的需要调用 IO 模块的类 StringIO、BytesIO,使用方法同文件读取是一样的。如 f = StringIO(); f.write('hello'); f.getvalue()

5、序列化与解序化

从文件读写字符串很方便,但是要将复杂的数据类型(不包含类对象)保存到文件并读取出依旧为数据类型则比较复杂。于是 python 支持 json/pickle(python 独有的数据交换格式)这种数据交换格式,可以很方便的将复杂的数据类型进行序列化转换为字符串,也可以很方便的解序化将字符串转换为对应的数据类型。如,x1 = [1, 'simple', 'list']; json.dumps(x); x2 = json.load(f);【因此,若要在一个 json 文件中存储一个程序相关的所有信息,那么就要对这个变量进行仔细的规划了。】

6、杂项

  1. 当文件被 open 之后,open 返回 IO 对象中的 closed 属性是 False 状态。当文件被 close 之后,这个 IO 对象依旧存在,IO 对象中的 closed 属性会变为 True 状态,此时便不能够通过这个 IO 对象再对文件进行读取写入的操作。(故判断打开的文件是否被 close,需要通过 IO.closed 来进行判断,不能通过 IO 是否为真来判断。)

  2. 文件指针位置说明。文件指针从 0 开始计数,如:写入一个字符无回车,那么这个字符左侧位置为 0,右侧位置为 1;写入一个字符并换行,那么通过 readline() 读取之后,tell() 所返回的位置是 2,因为换行符也是一个字节的字符。

  3. 当读写中文文件时,需要在参数中携带编码类型。或者直接以二进制的方式读写,这样就不存在编码问题,此时如果对读取的二进制进行读取时需要注意。如, open/write('file-path', 'r', encoding ='utf-8')open/write('file-path', 'rb')

  4. 与文件操作关系比较紧密的模块当属 os 模块,其中包含了大量的文件管理方法。如,文件路径查看、目录创建/删除、文件删除/重命名等。


7、网络 Socket 介绍

1、网络 Socket 介绍

(1)Socket 模块提供了访问各种套接字协议族的接口,在支持的众多协议族中,AF_UNIX(绑定在 Linux 文件系统上的套接字)、AF_INET(绑定在网络协议 TCP/IP 上的套接字)是最常用的两种协议族。但此处只介绍 AF_INET 协议族下的 2 中协议类型 TCP(SOCK_STREAM)UDP(SOCK_DGRAM) 的使用。

(2)套接字创建。创建的套接字对象需要的地址格式将根据此套接字对象被创建时指定的地址族被自动选择。

image

(3)该模块下包含的功能:【1】socket() 类是主要用来创建套接字,但不是唯一能创建套接字的;【2】gethostbyname(hostname) 及同类型的其它函数是用来当做网络服务功能,如获取域名的 ip 地址等;【3】其它就都是一些常量,如 AF_*代表各种协议族、SOCK_* 代表各种协议族下的协议类型、其余常量与这两种类似。

2、TCP/UDP 相关

(1)编程函数使用。

  • TCP 主要使用的函数:s.bind()、s.listen()、s.accept()、s.connect()、s.send()、s.recv()。
  • UDP 主要使用的函数:s.bind()、s.sendto()、s.recvfrom()。【注:recvfrom() 函数不是 UDP 编程特有,TCP 也依旧可以使用。】

(2)在 TCP/UDP 方式下系统处理 IP 包的本质区别。

操作系统收到数据包会进入队列,队列之中的成员就是一个三层 ip 包。【1】如果是 TCP 连接,那么这些包又会根据包的源/目的 ip/port(即连接 socket,接收缓冲区)再次分配到各自的缓冲区,缓冲区就是剥去 ip 端口等包头的纯数据流,然后就可以被当做文件的方式供 s.recv(10) 读取,读操作完全等同于文件读操作;【2】如果是 UDP 连接,那么队列中的这些 ip 包(不经历分区剥离包头的操作)会依顺序依次被读取,s.recvfrom(10) 在读取过程中才完成 ip/port 包头的剥离。此时如果包的大小大于读取的最大字节,那么系统就会抛出异常,该包也会被丢失。【而不管是 TCP 还是 UDP,初次进入 ip 包队列时,包都是被组装好了的。如,1m 的 tcp 包被发送到网络上时在 ip 层会被解包分割,接收并进入 ip 层之后又会被组装送入队列。所以在队列中看到的包的大小决定了 recvfrom(len) 时是否会被接收。】

(3)关于 TCP 网络连接过程中双方通信数据的发送与接收。每一方在建立连接之后系统均会为该连接在本地准备一个发送缓冲区和接收缓冲区,双方根本无需考虑数据应该如何去发送、网络断网、延时等状况,程序就只管将这两缓冲区当做一个本地文件对待,按照对文件的操作方式进行相应的读取和写入就行。【注:UDP 无此特性】

套接字一端发送的数据都会依序进入到另一端的接收缓冲区,不管另一端每次从中取多少字节以及是否取完,下一次都是依序接着取。遇到换行符时不管要取的字节数是否足够,本次 recv 都会返回。

*、网络杂项

  1. 当 CS 之间建立的连接被 close() 关闭之后,任何再通过该连接所进行的操作均会产生连接中断异常。

  2. TCP 编程服务端通常先发 HELLO 信息,客户端接收,之后开始双方的通信;而 UDP 编程服务端通常都是先接收 HELLO 消息,由客户端主动先发,之后才开始双方的通信。【注:这是因为 UDP 服务端必须先 recvfrom() 然后才能得知对方的地址,而 TCP 不需要注意这些】

  3. 服务端多线程下,监听一个端口的 socket 可供多个终端连接会话却不发生冲突,这是因为监听端口的 socket 和连接请求产生的 socket 的处理需要分别进行。【注:TCP 类型的 socket 被创建时是一个类型的 socket,在 accept()之后又返回一个连接 socket 对象,数据的发送与接收均建立在此连接 socket 之上。】

  4. tcp 多连接请求的实现离不开多线程支持,因为建立连接请求的 accept() 函数属于阻断函数,不通过多线程的话无法执行连接建立之后需要执行的功能代码。

  5. 网络编程:TCP/UDP 传输的数据格式是字节码;IO 编程:文本文件读写的数据格式是字符串,二进制文件读写的数据格式是字节码。【注:同一个字串,不同的编码方式所产生的字节码的二进制也是不一样的。】


陆-进程与线程

1、进程与线程

  • 父子进程之间的代码完全相同、变量也是完全独立互不影响(相当于存在多个代码体)。因此,子进程崩溃通常不影响父进程,这是多进程最大的优点;父子线程之间不同于父子进程,它们共享由系统分配得到的同一块内存的变量及函数(相当于只有一个代码体)。因此,子线程崩溃通常都会造成整个进程的崩溃;而不管是进程还是线程,只要父进程/线程挂掉,其它子进程/线程也都统统挂掉。【验证方法:在 main 块之外的全局执行一个 print 函数,父子进程同时运行时该语句会被执行多次,而父子线程同时运行时该语句只会被执行一次。】

  • 进程与线程都属于并发一类,因此两者之间无需过分区别开来。使用上需要注意:任务侧重点、代码实现特性。(1)任务:线程侧重于 IO 密集型的任务,进程侧重于计算密集型的任务。(2)代码:线程主要关注锁的使用,进程主要关注进程通信、锁。

  • Python 的多线程存在 GIL 全局锁,即,任何线程执行前必须先获得 GIL 锁,然后每执行 100 条字节码解释器就自动释放 GIL 锁让别的线程有机会执行。这个 GIL 全局锁实际上把所有线程的执行代码都给上了锁,所以多线程在 Python 中只能交替执行,即使 100 个线程跑在 100 核 CPU 上也只能用到 1 个核(虽然只能利用 1 个核,但是却可以将这个核的资源利用到 100%,因此多线程也还是有一定意义的。)。因此要实现多核任务,指望多线程是没希望的,但可以依靠多进程来实现多核任务,因为多进程有各自独立的 GIL 锁互不影响。

  • 实现进程的模块及类 multiprocessing.Process()、实现线程的模块及类 threading.Thread(),两者在使用上几乎无差别,均支持类继承、方法重写、实例化参数也都是 target、args 及 kwargs。不过线程额外还支持进程池 multiprocessing.Pool()、子进程调用 subprocess.call()这些功能。

2、互斥锁

  • 锁的出现主要是为了对多个线程/进程同步,保证数据的正确性,避免多个线程/进程共同对某个数据修改,造成不可预料的结果。通常锁都是在存在共享变量的多线程编程中被频繁使用,但多进程之间也总会涉及到一些全局的共享变量(共同操作同一个文件),因此在进程模块中也有自己的锁可供使用。

  • 锁并非只能针对同一变量及同一块代码,它相当于一个资源。当多个线程在不同代码处共同争夺一个锁时,此时即便这些不同代码之间并不存在冲突部分,但它们互相还是会依照锁的流程逐个请求的方式依次进行。因此,在哪些位置放置锁也需要谨慎。

  • 锁并非只是针对变量,它可以对函数、标准输入输出起作用。在一个程序中锁对象一般只需要一个,这样不管多个进程/线程在执行到锁请求代码处时,只会有一个进程/线程会得到锁然后进行锁请求下面的代码,而那些没有得到锁的进程/线程,此时只会被阻塞在所处的锁请求函代码处不能继续往下执行。待锁释放之后,由下一个请求到锁的进程/线程继续执行它后面的代码。

3、数据传递

(1)本地数据(线程)

线程本地数据类全局的实例对象是用于管理多线程各自的局部数据,它使得每个线程都只能读写自己线程的独立副本,互不干扰。而且也很好的解决了参数在一个线程中各个函数之间互相传递的问题。以下是该类的实现原理及使用方式:

image

(2)队列(进程、线程)

多进程中的队列类与队列模块中的队列类似乎完全一样,使用上几乎无差别。multiprocessing.Queue()<-->queue.Queue()

image

(3)管道(进程、线程)

管道函数返回两个表示管道两端的连接对象(默认情况下是双向),每个连接对象都有 send() 和 recv() 方法。请注意,如果两个进程/线程同时尝试读取或写入管道的同一 端,则管道中的数据可能会损坏。

image

4、多进程-杂项

  1. 多进程实现原理(猜测):当 python 执行 fork 之后,会创建 2 个代码一模一样的环境,在此环境中子进程的__name__ 等于"__mp_main__"。所有子进程的代码唯一不同的地方就在于,python 会在程序的最后会附加一条委派给此进程的任务函数进行执行。

  2. 编写多进程程序时,必须在 main 结构块中进行多进程对象的创建,否则会抛出错误即便运行可以正常显示结果。【注:可能是无穷递归进程创建所导致的报错】

  3. 进程/线程实例对象的 join 函数被执行之后,程序将会进入阻塞状态,直到进程/线程函数运行结束阻塞状态才会被解除。因此在循环创建进程/线程的时候,不要在循环体之中执行 join(通常都是在循环体外执行 join),否则多进程/线程还是再以单进程/线程的方式在运行。

  4. python 中实现多进程跨平台的模块是 multiprocessing,虽然 os.fork() 也可以实现多进程,但它仅限于在 Linux 系列的操作系统。

  5. 多进程之间的变量和函数都是相同且隔离的状态,每个进程根据获取到的执行函数再自己的环境空间中执行对应的函数,互相之间的变量不共享。若要达到多进程之间的通信,则需要借助 Queue 队列通信技术。

  6. 在使用进程池 Pool 时,需要知道,池被创建之后就处于打开状态,也就是在它被 close 关闭之前,只要系统给池派送任务,那么池就一直可以接收并产生进程。而如果需要调用 join 则需要先把池关闭禁止在接收任务,然后才能实现等池中的所有任务全部运行完成之后再执行后面的代码。

5、杂项

  1. 待补充。。。

柒-异常

1、异常

(1)以往在程序运行出现错误时,是通过事先约定程序返回错误码便可以根据程序返回的错误码来判断是否出现问题,问题的原因是什么。【1】但是用错误码来表示是否出错十分不便,因为函数本身应该返回的正常结果和错误码混在一起,造成调用者必须用大量的代码来判断是否出错。【2】而且一旦出错,还要一级一级上报,直到某个函数可以处理该错误。

所以高级语言通常都内置了一套 try...except...finally... 的错误处理机制,以更优美的方式解决:错误判断代码混乱以及级级上报的问题。

(2)异常语法

image

2、异常捕获及抛出

  • 异常抛出的两种形式:(1)raise 对象。直接抛出指定的异常对象;(2)assert 表达式。先判断表达式,若表达式为 false 则抛出异常对象 AssertionError。(故 assert expression 等价于 if not expression: raise AssertionError

  • 一个 except 子句可以同时处理多个异常,这些异常将被放在一个括号里成为一个元组。例如, except (RuntimeError, TypeError, NameError):

  • 如果一个异常没有与任何的 except 匹配,那么这个异常将会传递给上层的 try 中。(可以使用 except : 去捕获未知的异常,然后在该 except 块中打印信息,然后再次抛出。)

  • raise 语句如果不带参数,就会把当前错误原样抛出。此外,通过 except 还可以把捕获到的一种类型的错误转化成另一种类型。如:except ZeroDivisionError: raise ValueError('input error!')【将捕获到的 ZeroDivisionError 异常类型转换为 ValueError 异常类型】

  • 由于内置异常通常都涉及类继承的问题,因为在捕获异常的先后顺序上也需要注意先子类后父类的规则,否则父类会将本属于子类的异常夺去。如,如果异常捕获的第一个异常是 Exception 类,那么居于其后的其它异常类型便无法在捕获到异常对象了。

  • 当异常被触发函数抛出后,该函数之前的代码依旧被执行并生效,但是该函数及之后的代码便不再被执行。【验证方法:在全局定义一变量,在 try 触发函数前修改该变量,在触发函数之后修改该变量,在最终的异常捕获处打印该变量。】

  • 异常类型太多,如何知道对应错误对应的异常类型?通过在 IDE 中手动触发报错,在报错的最后一行显示的便是该错误对应的异常类型,以及它的错误值。如:执行不存在文件打开代码 file1 = open('123', 'r') 时,报告 FileNotFoundError: [Errno 2] No such file or directory: '123' 错误。

  • 在主 main 代码块中,捕获异常的代码中再次 raise 异常时,这时候的异常将不会在被主 main 块中的 try 结构捕获,而是直接抛给了 python 解释器,引发程序终止。

3、自定义异常

(1)系统中抛出的所有异常都是继承于 Exception 这个异常类,故我们也可以基于这个类自定义自己的异常类(可以在抛出异常对象时为对象赋值,在捕获到异常对象时使用对象中的值。)。

image

(2)内置异常的类层次结构图。

image

4、杂项

  • Python 内置的 logging 模块可以非常容易地记录异常抛出的详细错误信息(其实就是 IDE 中调试时的那些错误信息),错误信息由上而下记录了整个错误过程所经历的各个位置的错误原因,通常最后一个错误才是错误的最终起点,因此最后几行的报错信息最有用。

  • 异常机制的本质是:防止程序因局部引发的错误,而造成整个程序的错误,进而被 python 解释器强制终止该程序。如,下载器程序为了下载一个较大的文件任务,创建了 10 个线程分别去下载。突然其中一个线程遇到了错误抛出了异常,如果没有使用异常机制,那么本次任务直接失败。而如果使用了异常机制,那么当该线程抛出异常时,可以由主线程捕获然后终止该线程,并将该线程的任务再次分配给新创建的线程由其继续完成,那么本次任务就可以顺利完成了。【也可以在一个被调函数中整体使用异常机制,捕获异常,最后打印该函数执行失败的信息,这样就不会造成主程序异常退出,也可以知道问题出在了哪里】

  • 处在循环体中的 try...exception... 配合 break 的执行顺序:异常捕获之后在 exception 中运行完毕之后,继续在 exception 之外的代码块开始执行,exception 中的 break 依旧是生效的(这种效果等同于将 try...exception... 看做 if... else...,而 if 块中的 break 也同样是生效的。这似乎说明,break 免疫 if、try 这些语句块)。实例如下图:

image


捌-模块

1、命令空间

命名空间通常都与类在一块被提及,但把模块、类、函数放在一块会更容易理解。命名空间逻辑分类:

  1. 内置空间:Python 解释器把环境刚加载好之后就可以使用的变量及函数,这些值都包含在全局空间下的__builtins__ 字典变量中,相当于 import builtins as __builtins__,只不过这些导入的函数在调用时不需要加包名称。如,dir()、iter()、exit 这些函数及变量。

  2. 全局空间:环境加载好之后创建的变量、函数、以及导入的模块,也包含一些 python 专门针对该空间初始化的一些变量,就全都处在该空间中。如导入的 sys 模块、定义的 test() 函数、__name__ 变量等。

  3. 局部空间:定义的一些函数或导入模块里边的一些函数会有一些临时的变量、函数,这些变量和函数当前所处的即是局部空间。局部空间不同于内置空间和全局空间,它们只在函数被执行的时候才会短暂存在。【注:关于命名空间之间关系的解释中,大圈包小圈的那张图解释的有些笼统,下图是以命名空间树的形式进行的解释。】

image

2、作用域及变量名称追踪-LEGB 规则

作用域与命名空间关联很大,区别仅在于作用域将局部空间又细分为了最内层 local、非最内层 enclosing。【注:似乎也不可完全将两者等同,因为名称空间和作用域是属于两个概念。名称空间相当于一个定义,而作用域是指这个定义的使用范围。】

  1. L(Local):最内层,包含局部变量,比如一个函数/方法内部。

  2. E(Enclosing):包含了非局部 (non-local) 也非全局 (non-global) 的变量。比如两个嵌套函数,一个函数(或类) A 里面又包含了一个函数 B ,那么对于 B 中的名称来说 A 中的作用域就为 nonlocal。

  3. G(Global):当前脚本的最外层,比如当前模块的全局变量。

  4. B(Built-in): 包含了内建的变量/关键字等,最后被搜索。

在局部找不到,便会去局部外的局部找(例如闭包),再找不到就会去全局找,再者去内置中找。

image

3、导入模块中函数/变量作用域

image

4、模块-特殊变量

  1. __name__变量,标识该 py 文件是被当做执行文件 __main__ 还是被当做模块被导入 __mod-name__

  2. __doc__变量,一个模块开头处的注释默认均被赋值给其默认的 __doc__ 变量,故开头处的注释通常说明该模块的功能。

5、包管理

(1)创建包步骤。

  • 新建一个文件夹,文件夹的名称就是新建包的包名;

  • 在该文件夹中,创建一个 __init__.py 文件。( 该文件中可以不编写任何代码,也可以编写一些初始化代码。如,包说明注释、__all__ 变量定义)

image

(2)导入包。

  • import 包名[.模块名 [as 别名]]

  • from 包名 import 模块名/* [as 别名]

  • from 包名.模块名 import 成员名/* [as 别名]

(3)导入规则细节。

  1. 直接 import pkg 导入自定义的包名时,python 并不会将包中所有模块全部导入到程序中,它的作用仅仅是导入并执行包下的 __init__.py 文件,此时使用包前缀引用模块的方式会报错(因为并未将模块导入)。而标准包的导入却并非这样,而这是因为标准包中 init 文件中的代码初始化的原因。标准包中的 init 文件初始化中肯定会使用一些 import 去继续导入包中的模块,这样才使得我们能够直接导入便使用所有包中模块。

  2. 使用 from 方式的导入时,Python 会进入包文件系统,找到这个包里面对应/所有的子模块,然后把它们导入进来。通过这种方式导入的进来的模块,在引用时不需要携带包名称前缀,但模块名称前缀是需要的。

  3. 当使用 from pkg import *时,如果包 init 文件中无 __all__ 变量,就不会导入包里的任何子模块。他只是运行包 __init__.py 里定义的初始化代码;如果包 init 文件中 __all__变量有定义,则只会把 all 列表中的所有名字作为包内容导入。如,__all__ = ["echo", "surround", "reverse"]

6、模块/方法动态加载

方法动态加载的核心就是依赖于反射机制中的 getattr() 函数,它通过提供的变量参数,以此在类实例对象中寻找对应属性的值,实际就是指针地址。

image

7、杂项

  1. 模块的实现并非都是用 python 语言实现,也有其它语言实现的包,例如 C 语言实现的包。

玖-杂项

1、项目代码编程感想

  1. 先总体框架,后逐项填充。

  2. 先实现基本功能,后逐渐重构填充。

  3. 功能性函数,最好将其分割,便于灵活调用。

  4. 主程序最好简洁,函数都是调用。循环频繁执行的功能,最好将其拆分,以使每次循环都不执行累赘代码,以免拖慢运行速度。

  5. 各个类内的功能只涉及本类需要用到,不牵扯其它类的操作。

2、字符串格式化输出

image

(1)特殊字段解释。

  • field_name:以一个数字或关键字 arg_name 映射 format 参数列表中的变量。如果为数字,则它指向一个位置参数,而如果为关键字,则它指向一个关键字参数。不管是数字还是关键字对应变量,它均支持以属性或索引来获取特定值。如,'{0}{1.x}'.format(1,obj)

  • conversion:在格式化替换之前调用字符串函数对变量进行类型的强制转换。目前只支持 str()、ascii()、repr()这三个函数。如,'{! s}'.format(123) 等价于 '{}'.format(str(123))

  • grouping_option:使用逗号, 或下划线_作为千位分隔符。如,'{:,}'.format(1233)输出'1,233'

  • type:在格式化替换之前对变量执行数字数据类型的转换,与 conversion 类似,但这个针对的是一些数字变量进制浮点数之间的转换。如,'{:b}'.format(12) 输出 12 的二进制字符串 '1100''{:x}'.format(12) 输出 16 的二进制字符串 'c';【%表示输出带 % 号的格式。如,'{:2.3%}'.format(0.955) 输出 '95.500%'

  • #:会为不同进制的输出值分别添加相应的 '0b', '0o', '0x''0X' 前缀。如,'{:#b}'.format(12) 输出 12 的二进制字符串为 '0b1100'

(2)用法示例。

  • 格式对齐。如:居中且 * 填充,'{:*^30}'.format('centered') 输出'***********centered***********'

  • 数字符号。如:正数带符号,'{:+f}; {:+f}'.format(3.14, -3.14) 输出 '+3.140000; -3.140000';正数带空格,'{: f}; {: f}'.format(3.14, -3.14) 输出 ' 3.140000; -3.140000'

  • 位置参数。如:乱序格式化,'{0}{1}{0}'.format('a', 'b') 输出 'aba'

  • 嵌套参数。如:填充字符 $/右对齐/宽度 16,'{0:{fill}{align}16}'.format(123, fill='$', align ='>')输出'$$$$ $$$$ $$$$$123'

  • 特定类型时间的专属格式化:d = datetime.datetime(2010, 7, 4, 12, 15, 58);'{:%Y-%m-%d %H:%M:%S}'.format(d);输出'2010-07-04 12:15:58'

3、正则表达式

(1)匹配函数

  • re.match 函数:只匹配字符串的开始,如果字符串开始不符合正则表达式,则匹配失败,函数返回 None。(只匹配一次)
  • re.search 函数:匹配整个字符串,直到找到一个匹配。(只匹配一次)
  • re.findall 函数:在字符串中找到正则表达式所匹配的所有子串,并返回一个列表,如果没有找到匹配的,则返回空列表。(匹配所有)
  • re.finditer 函数:和 findall 类似,在字符串中找到正则表达式所匹配的所有子串,并把它们作为一个迭代器返回。(迭代器)

(2)功能函数

  • re.split 函数:(字串分割)按照能够匹配的子串将字符串分割后返回列表
  • re.compile 函数:(编译表达式) 用于编译正则表达式,生成一个正则表达式对象,供 match() 和 search() 这两个函数使用。(便于重复使用表达式,免去手动多次输入)
  • re.sub 函数:(检索替换) 替换字符串中的匹配项。{re.sub(pattern, repl, string, count=0, flags=0)},其中 repl 表示替换的字符串,也可为一个函数。

4、关于字节码及 struct

  1. 以文本方式打开文件时,系统在将磁盘中的二进制读取出来时直接转换为对应的 ASCII 码;而以二进制方式打开文件时,系统会将磁盘中的二进制转换为 16 进制的显示方式(2 个 16 进制表示一个字节,如\xff)。
  2. 使用 bytes() 对字符串处理返回字符串的字节码(相当于 b'str'),当输出这些变量时,系统为了方便显示会直接将字符字节码直接对应成 ASCII 码,然后输出的内容看起来就与转换之前没什么两样;但是对数字进行转换之后,输出内容就会是 \x 这样的形式,由于部分 \x 的值正好处在 ASCII 码表中,所以就会将其用对应的 ASCII 码表示而不是 \x 这样的形式了,所以对于数字字节码的转换我们总是能看到 \x9c@ 这样并存的字节表示。(如,数字 10240099 转换之后,正常应该是 b'\x00\x9c\x40\x63',由于 \x40 正好在 ASCII 码的范围内对应字符 @\x63 对应 c),所以系统直接用字符代表 16 进制,显示的字节码也就变为 b'\x00\x9c@c' 了。)
  3. 一个整数如果用 4 个字节表示,而关于各个字节顺序的表示又分为大小端,这就是字节序。大端表示符合人类习惯,小端表示符合机器。(即一个整数转换为字节码,若用大端表示 【pack('>H',4658)】 则为 \x12\x32,若用小端表示 【pack('<H',4658)】 则为 \x32\x12。)
  4. 本机字节顺序可能为大端或是小端,取决于主机系统的不同。 例如, Intel x86 和 AMD64 (x86-64) 是小端的;Motorola 68000 和 PowerPC G5 是大端的;ARM 和 Intel Itanium 具有可切换的字节顺序(双端)。 使用 sys.byteorder 可以检查系统的字节顺序。

5、进制转换

int() 函数支持将 2、8、16 进制的字符串转换为 10 进制的数字;bin()、hex() 支持将 10 进制转换成对应的进制字串;bytes() 函数支持将字节码转换为 16 进制(字节码即字符串进行编码之后产生的字节效果,为 2 进制存储 16 进制显示)。由此可见,要实现上述这些进制之间的互相转换,int() 函数必将充当一个中转的功能。【bytearry() 函数是一个可变的字节数据类型,bytes() 是一个不可变的字节数据类型,两者在使用上基本一致,但是 bytearry() 对象支持对自己的值进行插入删减操作,通过 id() 观察到删减前后变量的内存并未改变,而 bytes() 对象则无此功能。bytearry 支持插入的值必须是 0-255 的数字,也就是一次插入一个字符,因此该可变类型的实用效果并不大。】

6、with 针对的是一个类实例对象,所以在用法上可能会这样:with open(...) as f ...f = open(...); with f ... 效果都是一样的,只不过一个使用了 as 别名,另一个是原变量。

7、yield 与 return 对比:程序执行到 return 时,返回值且函数直接结束;执行到 yield 时,返回值且函数暂停他,等待下一次调用时继续 yield 后面代码的执行。

*、常见函数及模块

  1. input 函数,功能:str1 = input("please:") 从键盘获取输入值。
  2. print 函数,功能:print("www","runoob","com",sep=".") 打印输出变量字串。
  3. format 函数,功能:(1)"{0} {1}".format("hello", "world") 格式化输出;(2)"{1:2d} {0:2d}".format(12, 13) 格式化输出数字。
  4. int/float/str/list/tuple 函数,功能:int(2.34) 强制转换数据类型为整数。
  5. range 函数,功能:list(range(1, 6)) 快速生成一个整数序列。
  6. len 函数,功能:len("string") 计算字串的长度。
  7. round 函数,功能:round(2.675, 2) 将浮点数四舍五入处理。
  8. pow 函数,功能:pow(x,y) 计算 x 的 y 次方的值。
  9. open 函数,功能:open('test.txt', mode='r') 打开一个文本文件。
  10. sys 模块,功能:提供了一些由解释器维护的变量。(1)argv 元组变量包含了被传递给 Python 脚本的命令行参数。
  11. os 模块,功能:提供与操作系统交互的接口。(1)system('whoami') 执行系统命令 whoami。(2)getenv('PATH') 获取操作系统的环境变量值。(3)listdir(getcwd()) 列出当前目录下的文件。
  12. os.path 模块,功能:专门用于处理目录路径的操作。(1)os.path.abspath('.') 返回指定目录的绝对路径。
  13. glob 模块,功能:根据通配符表达式搜索匹配的文件。(1)glob.glob('*.py') 搜索 py 文件。
  14. random 模块,功能:生成随机数。(1)randint(1,9) 范围内的随机整数;(2)random() 0~1 范围内的随机小数;(3)choice(list) 从一个列表中找一个随机值。
  15. time 模块,功能:时间的读取。(1)time() 获取当前时间戳。(2)sleep(1) 让进程睡眠1秒。
  16. subprocess 模块,功能:子进程管理,功能类似 os.system() 那样可执行系统命令,并与之交互。(1)subprocess.run('whoami',capture_output=True).stdout.decode() 执行 whoami 命令,并捕获其输出结果。
  17. queue 模块,功能:提供了一个线程安全的队列实现,用于在多线程编程中安全地传递数据。
  18. threading 模块,功能:提供多线程支持。(1)t = threading.Thread(target=crawl, args=(link,), kwargs={"delay": 2});t.start();t.join() 为 crawl 函数创建一个线程在后台中执行。
  19. math 模块,功能:包含各种数学计算函数。
  20. socket 模块,功能:网络编程。
  21. requests 模块,功能:HTTP 请求客户端。
  22. argparse 模块,功能:用于命令行选项、参数和子命令的解析器。