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、数据结构
-
列表:python中列表类型的对象相当于数组,封装处理之后它可以当做数据结构中的数组、堆栈、队列使用,只是在效率上会有所差异。(默认情况下,列表相当于堆栈,以队列的方式使用时需要import deque)
-
元组:就是一个只读不能写的列表。
-
范围:相当于一个元组,也支持索引切片的功能,但是不能更改变量的值。使用它的好处在于,可以使用固定长度的表达式表达出任意范围的值。
-
集合:是一个无序不重复元素的集。基本功能包括关系测试(交并关系)和消除重复元素。
-
字典:序列是以连续的整数为索引,与此不同的是,字典以关键字为索引,关键字可以是任意不可变类型且必须互不相同,通常用字符串或数值。基本功能是快速索引数据。
(*)优缺点:
list是线性存储,数据量大的时候,插入和删除效率很低。
deque是为了高效实现插入和删除操作的双向列表,适合用于队列和栈。
3、变量及作用域
(1)作用域主要分为全局作用域(global)、嵌套作用域(enclosing,介于全局与局部之间的作用域,主要出现在函数嵌套函数的函数中。)、局部作用域(local)
(2)变量的查找顺序:局部作用域->嵌套作用域->全局作用域,若最终全局作用域也未找到则报错。
(3)全局定义的变量可以被其下的局部作用域读取,但是不能被修改,除非使用关键字global 和 nonlocal。
(4)当内部作用域想修改全局作用域的变量时,需使用global关键字在内部作用域声明变量;当内部作用域想修改嵌套作用域的变量时,需使用nolocal关键字在内部作用域声明变量。否则报错,若通过参数传递的方式def test(a),则也正常打印值,因为参数的存在相当于已经在函数中定义过变量a,故不报错。
(5)Python对在函数定义体中 赋值的变量都认为是局部变量。从而导致局部变量b未赋值先使用的问题。
(6)全局定义的变量,如果通过参数传递给函数,那么函数可以对该变量指向的对象进行值的更改;如果不通过参数传递而是直接当做全局变量来使用,那么该变量可以被读取但是不能写入,如果需要写入则必须在函数开头处对该全局变量进行global的声明。(全局定义的变量属于一个类实例对象,函数参数传递时相当于传递了对象的物理地址给了一个变量,因此参数传递的变量可以被修改,且修改的就是全局定义的变量本身。)
4、数组-切片
(1)字符串访问子串的截取格式:变量[头下标:尾下标:步长](截取方式是左闭右开,默认步长是+1)
(2)关于索引号的规则:相当于将字符串首尾连接,然后以0为界,正向为正,反向为负。
(3)切片用法。【1】字串全部遍历:list[:]【2】字串的反向遍历:list[::-1]【3】字串后5个值:list[-5:]【4】遍历全部取出偶数位的值:list[::2]
5、列表推导式:提供了从序列创建列表的简单途径。通常应用程序将一些操作应用于某个序列的每个元素,用其获得的结果作为生成新列表的元素,或者根据确定的判定条件创建子序列。 (除了元组以外,列表、集合、字典都支持推导式)
6、杂项。
-
判断赋值:a = 1 if 1>2 else 0:如果1>2则a=1,否则a=0。
-
列表推导式-格式化字符串:['%s="%s"' %item for item in d1.items()] 输入:size=6输出size="6"。
貳-流程控制
1、条件控制(if)
- Python中没有switch – case语句
2、循环控制(while、for、foreach)
- Python 中没有 do..while、foreach 这些循环
*、杂项
-
continue、break只有在循环中,它们才可以与if等语句搭配使用,否则会报错或不生效。
-
条件表达式是否需要括号?答:不管是条件控制还是循环控制,它们所认定的条件只有两个值:true、false。所以只要条件表达式最终表现出来的结果符合bool类型就行,是否带括号无所谓,pycharm中带括号被认为是冗余。
叁-函数
1、函数参数传递。
在python中,类型属于对象,变量是没有类型的。如a=[1,2],a='12'中,[1,2]是list类型的对象,'12'是string类型的对象,而变量a是没有类型的,它只是一个对象的引用(一个指针),可以指向任何类型的对象。(在c语言中,每个变量都对应一个类型,即便是指针也是占用内存空间且拥有类型的,而python中的变量,它相当于一个void的指针,可以存储任何类型的地址,以引用的方式使用对象。这种变量也是占用内存空间的,但是python将其隐身了,使我们无法看到它的存储地址。)
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})
(*)函数参数(闭包):详见剪藏笔记(返回函数)。
3、匿名函数lambda。
-
语法格式:lambda [arg1[,arg2,args]]:expression 例,sum = lambda arg1, arg2: arg1 + arg2;sum(1,2)
-
lambda的主体是一个表达式,而不是一个代码块。仅仅能在lambda表达式中封装有限的逻辑进去。
-
lambda函数拥有自己的命名空间,且不能访问自己参数列表之外或全局命名空间里的参数。
-
默认参数的匿名函数。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、函数返回函数(闭包)。
(1)闭包。返回的函数在其定义内部引用了局部变量args,所以当一个函数返回了一个函数后,其内部的局部变量还可以被新函数引用,这就是闭包技术。
(2)闭包时机。内联函数只有被 return 返回时,python才会将内联函数(即返回函数)所使用的外部局部变量进行打包以与该返回函数关联。例,如下多个返回函数被统一打包和分开打包的代码实现区别。
(3)闭包读写。闭包读关联变量不会有问题,但是闭包写关联变量时就需要注意变量作用域中在局部函数声明全局变量进行读写的事项。
6、递归函数。如果一个函数在内部调用自身本身,这个函数就是递归函数。
(1)递归函数的实现需要确定2个条件:终止条件、循环表达式。
(2)递归函数的使用需要注意防止栈溢出。在计算机中,函数调用是通过栈(stack)这种数据结构实现的,每当进入一个函数调用,栈就会加一层栈帧,每当函数返回,栈就会减一层栈帧。由于栈的大小不是无限的,所以,递归调用的次数过多,会导致栈溢出。
例,阶乘算法实现如下
*、杂项
- return语句用于退出函数,选择性地向调用方返回一个表达式。不带参数值的return语句返回关键字None。
肆-类
1、类 - 结构。类是由类属性和实例属性组合而成,属性又包含变量和方法。
(1)属性访问特性。私有属性(__ver表示私有变量,仅供类内部调用)、公有属性(ver表示公有属性,供类内外调用)、特殊属性(__solts__/__init__等表示特殊属性,均有特殊含义在特定情况下被调用)。
(2)类/实例属性区别。类变量归整个类/类实例所有,该变量可以被类和实例直接读取,但是只能被类更改,更改之后影响所有实例再次读取到的值;实例变量属于类实例所独有,仅能通过该实例去读取或更改。
(3)类实例动态绑定属性。支持动态绑定变量/方法,绑定后的变量/方法仅供该实例所使用。(可以通过定义类变量__solts__来限制动态绑定的属性数量及名称)
(4)类属性继承规则。private{__只能被类内部使用,继承之后无法被使用}、protected{_只能被继承的类内部使用(实测也可以在类外使用,与public无异)}、public{类的内外部均可使用}
(5)面向对象三大特性:封装(将对象相关的方法和属性揉合在了一起)、继承(提高了代码的复用性)、多态(在基类中创建的方法,在子类中分别重写此方法,以父类对象作为函数参数,最后传入到参数的子类是什么类型的对象,在函数中就会对应调用这个实例的方法,这就是多态)。
2、类 - 魔法方法。这类方法不需要我们手动调用(但也可以手动调用),在满足某个条件时会自动调用,这个满足的条件我们可以称为调用时机。
-
__str__的调用时机:在使用 print 打印对象、对象的格式化输出以及调用 str 方法,调用的都是__str__方法。
-
__repr__的调用时机:在交互环境下直接输出对象以及将对象放在容器中进行输出,调用的都是__repr__ 方法。(非交互环境下直接输出对象不显示输出)
-
__add__/__eq__/__iadd__/__float__的调用时机:在使用操作符运算时,会自动调用操作符所对应的方法。如加减法、大于小于、原地赋值中的加减法(a += 1)、强制类型转换中的浮点转换等。
-
__call__的调用时机:将类实例对象当做函数来调用时,调用的都是__call__方法。
-
__iter__/__next__的调用时机:当对类实例使用迭代器函数iter()/next()时,将自动调用类的__iter__/__next__方法。同类型的还有__int__、__str__等基础变量类型的魔法方法。
-
__getitem__/__setitem__/__delitem__/__len__/__contains__的调用时机:使一个自定义类拥有 集合/序列对象中使用下标[ name]进行集合中项值的获取、设置、删除,以及集合中项的总长度【len( list( 1,2 ) )】、是否包含关键值【'name' in dict( name = 'test' )】的功能。【这些魔法方法的实现通常配合python内置的反射机制函数一起使用,getattr()、setattr()、delattr()、hasattr()】
-
__enter__/__exit__的调用时机:在with上下文管理器中,当with语句刚开始执行时会自动调用__enter__,当with语句块中的代码正常退出或不正常退出时都会自动调用__exit__。【代码实现上需要注意返回值的类型】
-
__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、类 - 特殊变量。
-
类特殊变量__solts__,可以通过定义类变量__solts__的值来限制类实例动态绑定的属性数量及名称。
-
类特殊变量__dict__,分别返回类/类实例对象的所有属性名为key,属性值为value的一个字典【实例.__dict__ 和 类.__dict__ 返回的都是各自相关属性的字典】。相对应的dir( class1 )函数则会返回这个类所有相关属性的一个列表。
-
类特殊变量__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列表的关系(注意继承中重写方法的优先级)。
-
Python会对每一个类构造一个mro列表(通过_mro_可查看)。使用一个属性或方法时,python并不知道该方法是从哪个基类继承下来,故需要根据mro列表依次从左往右的类中进行查找,直至找到,找不到则报错。mro仅是一个线性关系表,而继承是具有层级关系,故通过super()【从子类直接跳到父类,而不需依据mro顺序进行搜索】就可以使得查找过程左到右上到下的进行。
-
例如,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的普通函数将不在是一个普通的函数,而会成为一个生成器。
(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()函数调用并不断返回值的对象,它就是迭代器。】
*、杂项。
-
使用0.数据名称的格式,这是类专有的打印格式,与private、protected这些属性一起使用时问题较多,不建议使用。("name={0.name} speed={0.speed}".format(self))
-
类装饰器@property,可以将类方法转换为变量,实现在隐藏类内部属性的同时,又可以对由外部传递给内部属性的值进行检查。
-
类多态函数所传递的实例变量并非严格要求必须是一个继承类的实例,它也可以是另外一个毫不相关的类,只要该类和继承类拥有共同的被调同名函数即可。如,一个关于动物类的多态,但是允许在多态函数中传入一个植物类的实例对象。
-
同一个类不同实例对象调用同一个方法时,该方法的id值是一样的。如id(c1.getm)等于id(c2.getm),说明类方法和类实例是分开的。
伍-文件与网络
1、文件IO属性的特性表
区别: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文件中存储一个程序相关的所有信息,那么就要对这个变量进行仔细的规划了。】
*、文件杂项
-
当文件被open之后,open返回IO对象中的closed属性是False状态。当文件被close之后,这个IO对象依旧存在,IO对象中的closed属性会变为True状态,此时便不能够通过这个IO对象再对文件进行读取写入的操作。(故判断打开的文件是否被close,需要通过IO.closed来进行判断,不能通过IO是否为真来判断。)
-
文件指针位置说明。文件指针从0开始计数,如:写入一个字符无回车,那么这个字符左侧位置为0,右侧位置为1;写入一个字符并换行,那么通过readline()读取之后,tell()所返回的位置是2,因为换行符也是一个字节的字符。
-
当读写中文文件时,需要在参数中携带编码类型。或者直接以二进制的方式读写,这样就不存在编码问题,此时如果对读取的二进制进行读取时需要注意。如, open/write('file-path', 'r', encoding='utf-8'),或open/write('file-path', 'rb')。
-
与文件操作关系比较紧密的模块当属os模块,其中包含了大量的文件管理方法。如,文件路径查看、目录创建/删除、文件删除/重命名等
1、网络Socket介绍
(1)Socket模块提供了访问各种套接字协议族的接口,在支持的众多协议族中,AF_UNIX(绑定在Linux文件系统上的套接字)、AF_INET(绑定在网络协议TCP/IP上的套接字)是最常用的两种协议族。但此处只介绍AF_INET协议族下的2中协议类型TCP(SOCK_STREAM)、UDP(SOCK_DGRAM)的使用。
(2)套接字创建。创建的套接字对象需要的地址格式将根据此套接字对象被创建时指定的地址族被自动选择。
(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都会返回。
*、网络杂项
-
当CS之间建立的连接被close()关闭之后,任何再通过该连接所进行的操作均会产生连接中断异常。
-
TCP编程服务端通常先发HELLO信息,客户端接收,之后开始双方的通信;而UDP编程服务端通常都是先接收HELLO消息,由客户端主动先发,之后才开始双方的通信。【这是因为UDP服务端必须先recvfrom()然后才能得知对方的地址,而TCP不需要注意这些】
-
服务端多线程下,监听一个端口的socket可供多个终端连接会话却不发生冲突,这是因为监听端口的socket和连接请求产生的socket的处理需要分别进行。【TCP类型的socket被创建时是一个类型的socket,在accept()之后又返回一个连接socket对象,数据的发送与接收均建立在此连接socket之上。】
-
tcp多连接请求的实现离不开多线程支持,因为建立连接请求的accept()函数属于阻断函数,不通过多线程的话无法执行连接建立之后需要执行的功能代码。
-
网络编程: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)本地数据(线程)
线程本地数据类全局的实例对象是用于管理多线程各自的局部数据,它使得每个线程都只能读写自己线程的独立副本,互不干扰。而且也很好的解决了参数在一个线程中各个函数之间互相传递的问题。以下是该类的实现原理及使用方式:
(2)队列(进程、线程)
多进程中的队列类与队列模块中的队列类似乎完全一样,使用上几乎无差别。multiprocessing.Queue()<-->queue.Queue()。
(3)管道(进程、线程)
管道函数返回两个表示管道两端的连接对象(默认情况下是双向),每个连接对象都有 send() 和 recv() 方法。请注意,如果两个进程/线程同时尝试读取或写入管道的同一 端,则管道中的数据可能会损坏。
4、多进程-杂项
-
多进程实现原理(猜测):当python执行fork之后,会创建2个代码一模一样的环境,在此环境中子进程的__name__等于“__mp_main__”。所有子进程的代码唯一不同的地方就在于,python会在程序的最后会附加一条委派给此进程的任务函数进行执行。
-
编写多进程程序时,必须在main结构块中进行多进程对象的创建,否则会抛出错误即便运行可以正常显示结果。【可能是无穷递归进程创建所导致的报错】
-
进程/线程实例对象的join函数被执行之后,程序将会进入阻塞状态,直到进程/线程函数运行结束阻塞状态才会被解除。因此在循环创建进程/线程的时候,不要在循环体之中执行join(通常都是在循环体外执行join),否则多进程/线程还是再以单进程/线程的方式在运行。
-
python中实现多进程跨平台的模块是multiprocessing,虽然os.fork()也可以实现多进程,但它仅限于在Linux系列的操作系统。
-
多进程之间的变量和函数都是相同且隔离的状态,每个进程根据获取到的执行函数再自己的环境空间中执行对应的函数,互相之间的变量不共享。若要达到多进程之间的通信,则需要借助Queue队列通信技术。
-
在使用进程池Pool时,需要知道,池被创建之后就处于打开状态,也就是在它被close关闭之前,只要系统给池派送任务,那么池就一直可以接收并产生进程。而如果需要调用join则需要先把池关闭禁止在接收任务,然后才能实现等池中的所有任务全部运行完成之后再执行后面的代码。
5、多线程-杂项
- 待补充。。。
柒-异常
1、异常
(1)以往在程序运行出现错误时,是通过事先约定程序返回错误码便可以根据程序返回的错误码来判断是否出现问题,问题的原因是什么。【1】但是用错误码来表示是否出错十分不便,因为函数本身应该返回的正常结果和错误码混在一起,造成调用者必须用大量的代码来判断是否出错。【2】而且一旦出错,还要一级一级上报,直到某个函数可以处理该错误。
所以高级语言通常都内置了一套try...except...finally...的错误处理机制,以更优美的方式解决:错误判断代码混乱以及级级上报的问题。
(2)异常语法
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这个异常类,故我们也可以基于这个类自定义自己的异常类(可以在抛出异常对象时为对象赋值,在捕获到异常对象时使用对象中的值。)。
(2)内置异常的类层次结构图。
4、杂项
-
Python内置的logging模块可以非常容易地记录异常抛出的详细错误信息(其实就是IDE中调试时的那些错误信息),错误信息由上而下记录了整个错误过程所经历的各个位置的错误原因,通常最后一个错误才是错误的最终起点,因此最后几行的报错信息最有用。
-
异常机制的本质是:防止程序因局部引发的错误,而造成整个程序的错误,进而被python解释器强制终止该程序。如,下载器程序为了下载一个较大的文件任务,创建了10个线程分别去下载。突然其中一个线程遇到了错误抛出了异常,如果没有使用异常机制,那么本次任务直接失败。而如果使用了异常机制,那么当该线程抛出异常时,可以由主线程捕获然后终止该线程,并将该线程的任务再次分配给新创建的线程由其继续完成,那么本次任务就可以顺利完成了。【也可以在一个被调函数中整体使用异常机制,捕获异常,最后打印该函数执行失败的信息,这样就不会造成主程序异常退出,也可以知道问题出在了哪里】
-
处在循环体中的try...exception...配合break的执行顺序:异常捕获之后在exception中运行完毕之后,继续在exception之外的代码块开始执行,exception中的break依旧是生效的(这种效果等同于将try...exception...看做if... else...,而if块中的break也同样是生效的。这似乎说明,break免疫if、try这些语句块)。实例如下图:
捌-模块
1、命令空间。命名空间通常都与类在一块被提及,但把模块、类、函数放在一块会更容易理解。命名空间逻辑分类:
-
内置空间:Python解释器把环境刚加载好之后就可以使用的变量及函数,这些值都包含在全局空间下的__builtins__字典变量中,相当于import builtins as __builtins__,只不过这些导入的函数在调用时不需要加包名称。如,dir()、iter()、exit这些函数及变量。
-
全局空间:环境加载好之后创建的变量、函数、以及导入的模块,也包含一些python专门针对该空间初始化的一些变量,就全都处在该空间中。如导入的sys模块、定义的test()函数、__name__变量等。
-
局部空间:定义的一些函数或导入模块里边的一些函数会有一些临时的变量、函数,这些变量和函数当前所处的即是局部空间。局部空间不同于内置空间和全局空间,它们只在函数被执行的时候才会短暂存在。【关于命名空间之间关系的解释中,大圈包小圈的那张图解释的有些笼统,下图是以命名空间树的形式进行的解释。】
2、作用域及变量名称追踪-LEGB规则。作用域与命名空间关联很大,区别仅在于作用域将局部空间又细分为了最内层local、非最内层enclosing。【似乎也不可完全将两者等同,因为名称空间和作用域是属于两个概念。名称空间相当于一个定义,而作用域是指这个定义的使用范围。】
-
L(Local):最内层,包含局部变量,比如一个函数/方法内部。
-
E(Enclosing):包含了非局部(non-local)也非全局(non-global)的变量。比如两个嵌套函数,一个函数(或类) A 里面又包含了一个函数 B ,那么对于 B 中的名称来说 A 中的作用域就为 nonlocal。
-
G(Global):当前脚本的最外层,比如当前模块的全局变量。
-
B(Built-in): 包含了内建的变量/关键字等,最后被搜索。
在局部找不到,便会去局部外的局部找(例如闭包),再找不到就会去全局找,再者去内置中找。
3、导入模块中函数/变量作用域
4、模块-特殊变量
-
__name__变量,标识该py文件是被当做执行文件(__main__)还是被当做模块被导入(__mod-name__)。
-
__doc__变量。,一个模块开头处的注释默认均被赋值给其默认的__doc__变量,故开头处的注释通常说明该模块的功能。
5、包管理
(1)创建包步骤。
-
新建一个文件夹,文件夹的名称就是新建包的包名;
-
在该文件夹中,创建一个 __init__.py 文件。( 该文件中可以不编写任何代码,也可以编写一些初始化代码。如,包说明注释、__all__变量定义、)
(2)导入包。
-
import 包名[.模块名 [as 别名]]
-
from 包名 import 模块名/* [as 别名]
-
from 包名.模块名 import 成员名/* [as 别名]
(3)导入规则细节。
-
直接 import pkg 导入自定义的包名时,python并不会将包中所有模块全部导入到程序中,它的作用仅仅是导入并执行包下的 __init__.py 文件,此时使用包前缀引用模块的方式会报错(因为并未将模块导入)。而标准包的导入却并非这样,而这是因为标准包中init文件中的代码初始化的原因。标准包中的init文件初始化中肯定会使用一些import去继续导入包中的模块,这样才使得我们能够直接导入便使用所有包中模块。
-
使用from方式的导入时,Python 会进入包文件系统,找到这个包里面对应/所有的子模块,然后把它们导入进来。通过这种方式导入的进来的模块,在引用时不需要携带包名称前缀,但模块名称前缀是需要的。
-
当使用from pkg import *时,如果包init文件中无__all__变量,就不会导入包里的任何子模块。他只是运行包__init__.py里定义的初始化代码;如果包init文件中__all__变量有定义,则只会把all列表中的所有名字作为包内容导入。如,__all__ = ["echo", "surround", "reverse"]。
6、模块/方法动态加载。方法动态加载的核心就是依赖于反射机制中的getattr()函数,它通过提供的变量参数,以此在类实例对象中寻找对应属性的值,实际就是指针地址。
7、杂项
- 模块的实现并非都是用python语言实现,也有其它语言实现的包,例如C语言实现的包。
玖-杂项
1、项目代码编程感想
-
先总体框架,后逐项填充。
-
先实现基本功能,后逐渐重构填充。
-
功能性函数,最好将其分割,便于灵活调用。
-
主程序最好简洁,函数都是调用。循环频繁执行的功能,最好将其拆分,以使每次循环都不执行累赘代码,以免拖慢运行速度。
-
各个类内的功能只涉及本类需要用到,不牵扯其它类的操作。
2、字符串格式化输出
(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后面代码的执行。