一. 基础
- 编译型语言
- 定义:在程序运行之前,源代码会先经过编译器将其转换为机器语言的形式,生成可执行文件。在运行时,计算机直接执行该可执行文件,无需再进行翻译或解释。C、C++ 和 Java 等语言属于编译型语言。
- 白话定义:只有第一次执行的时候需要编译,之后如果没修改源代码就不会再编译了
- 示例:C++、Go、Java
- 解释型语言
- 定义:在程序运行时,源代码逐行解释并执行。解释器将源代码转换为机器语言,并逐行执行代码。解释型语言的代码无需编译,可以直接运行,但由于需要逐行解释执行,相对于编译型语言,解释型语言的执行速度通常较慢。
- 白话定义:每次运行都会从第一行进行编译,编译一行执行一行
- 示例:Python、JavaScript、Ruby
1. 基中基
1.1. 关键字
| 关键字 | 涉及功能 |
|---|---|
| True, False, None | 真 假 空 |
| if, elif, else, and, not, assert, or, is, in | 逻辑判断 |
| try, except, finally, raise | 异常捕获 |
| for, while, continue, break, return | 循环 |
| from, import | 导包 |
| def, class, lambda | 定义函数、类、匿名函数 |
| async, await, yield | 异步 |
| global, nonlocal | 变量空间 |
| as, del, pass, with, type | 重命名、删除、PASS、上下文、类型 |
1.2. 逻辑运算符
- 与
and: 一假即假 - 或
or: 一真即真(注意短路逻辑:1 or 1 / 0不会报错,程序只会走第一个1然后就会执行下一行) - 非
not: 真假取反
1.3. 比较运算符
//取整除 9 // 4 = 2%取余 9%4 = 1^取异或 把数字转化为二进制取 0 1 为 1 ,1 0 为 1, 1 1 为1, 0 0 为 0**幂
1.4. 比较运算符
> < == != >= <=返回的结果都是bool类型, True表示条件成立, False表示不成立.
1.5. 输入输出
print()调用底层的sys.stdout.write方法,前往控制台打印输出input()无论输入什么类型的数据,都会转化为字符串
1.6. 循环
循环语句结合
else语句使用,当循环语句执行了break表示非正常结束,else语句不会执行,否则会执行else语句循环语句里有
break/return时,break/return执行了,else语句就不会执行break/continue只影响一层循环
1.7. if三目运算
a if a > b else b条件成立取a,不成立取bif 除了判断bool类型, 还可以判断
- 容器类型(字符串, 列表, 元组, 字典, 集合,
range(),bytes()) 判断是否有数据 - 非零即真(只要不是0, 条件都成立)
- None 条件不成立, not none, 表示非空, 条件成立
- 容器类型(字符串, 列表, 元组, 字典, 集合,
1.8 注释
- 单行
# 这是注释 - 多行
""" 这是多行注释 """
2. 容器
2.1. 字符串
字符串用
join比直接+高效原因:在 Python 中,字符串是不可变类型,这意味着一旦我们创建了一个字符串对象,它就不能被修改或更新。因此,每次使用+操作符拼接字符串时,都会创建一个新的字符串对象,并将之前字符串对象的内容复制到新的字符串对象中,这个复制的操作会带来额外的内存分配和内存拷贝的开销,特别是在需要拼接大量的字符串时,会消耗大量的系统资源,导致程序运行缓慢。 相对地,使用join()方法的拼接字符串操作则更加高效。join()方法本质上是将多个字符串通过指定的分隔符拼接在一起,而与此相关的方法包括split(),replace()和format()等方法,它们均采用类似的算法。在join()方法的实现中,Python 的解释器会先在内存中分配一个足够大的单个字符串缓冲区,然后扫描要拼接的字符串,将其复制到单个缓冲区中,并在不同字符串之间插入指定的分隔符。这种方法可以有效地避免频繁创建或拷贝字符串对象,从而提高拼接字符串的效率。 此外,join()方法还可以接受一个可迭代对象作为参数,如列表,元组,生成器等,它们会返回一个字符串,其中可迭代对象按照指定的分隔符进行拼接。这种方式具有更高的灵活性和实用性,因为它可以用于拼接任意数量的字符串,而且可以用于迭代较大的数据集合,而不会导致系统资源消耗过多。
- 定义:用单引号、双引号、三引号均可,仅三引号可以换行
- 切片:
[开始位置:结束位置:步长]左闭右开[::-1]字符串快速逆置
1 | my_str = "python" |
常用
.find(要查询的字符, 查询开始索引, 查询结束索引)左闭右开,查空返回 -1.replace(str1, str2, 替换次数)字符串替换,替换次数默认-1全部替换.split(str1, 切片次数)字符串以 str1 为分隔符进行切片,切片次数默认-1全部切,返回分隔后的列表.lower()英文转小写upper()转大写
2.2. 列表
定义:[] 或 list()
列表推导式: 列表中可以包含条件语句,表示筛选符合条件的元素。在这种情况下,底层逻辑还会涉及以下步骤:
- 创建一个空列表。
- 对于列表推导式中的每个元素表达式,按照迭代顺序依次执行以下步骤:
- 在当前作用域中计算元素表达式的值。
- 如果条件表达式的值为真,则将计算得到的值添加到列表中。
- 返回最终生成的列表。
增
.append加整个对象,加字典也是整个字典.extend打散加进去,加字典默认加字典的key.insert(index, obj)在指定位置前面加, 是整个加进去的
删
del list[::]可以和切片一起操作根据索引删除列表元素.pop()只能根据单独索引删除对应元素, 默认删最后一个, 会返回删除的元素.remove()指定元素删除
改
list[index] = "new" 可以用切片同时更改多个数据,注意下面示例
1 | my_list = [1, "2", 3] |
查
in判断是否存在,只可判断最外层数据,内层还有容器要索引进去查.index(obj, start, end)左闭右开,找不到报ValueError.count(obj)
排序
.sort(reverse=False)默认是从小到大,reverse=True改为从大到小.reverse()将列表逆置, 与上面的reverse=True不同
2.3. 元组
定义:tuple() 或 (1, 2) 如果只有一个元素, 逗号不能省略, 有序
2.4. 字典
定义
dict()或{"a": 1, "b": 2}zip组合key和value生成字典
1 | keys = ["a", "b"] |
增:
dict[key] = value
删:
del dict[key]必须要有key数据.pop(key, default)必须要有key数据, 会返回所删项的value, 如果字典里没有这个key就会返回default.popitem()takes no arguments, return the last item as a tuple().clear()清空
改:
dict[key] = value和增加元素相同, 所以当原本没有此key就变成增加元素了
查:
dict[key]如果不存在会报错.get(key, default)如果不存在则返回default不写default找不到会返回None不会报错
合并:
dict1.update(dict2)把字典2的每个键值对数据合并到字典1中, 如果有重复的则更新1的内容
遍历:
for key in dict.keys()for values in dict.values()for item in dict.items()item是个tuplefor key, value in dict.items()遍历 + 拆包
排序:
sorted(d.items(), key=lambda x : x[0/1])
2.5. 集合
- 集合是一个容器类型, 可以存储多个数据, 但是多个数据不能重复
- 集合只能存储不可变类型数据, 也就是: 数字, 字符串, 元组 同字典的key
- 空集合不能使用**{}**来表示, {}是字典, 创建时用 set() 来创建空集合
- 遍历集合不能用通过下标, 可以用for遍历, 可迭代对象
特点:
- 无序, so集合不能通过索引获取数据和通过索引修改数据
- 数据不能重复,数据是唯一的
- 可变类型
操作符:
^ 补 {1,2} ^ {2, 3} => {1, 3}
& 交 {1,2} & {2, 3} => {2, }
| 并 {1,2} | {2, 3} => {1, 2, 3}
- 差 {1,2} - {2, 3} => {1, }
增:
.add() 重复的数据只保留一个
删:
.remove(value) 指定数据删除
2.6. 公共方法
| 运算符 | Python 表达式 | 结果 | 描述 | 支持的数据类型 |
|---|---|---|---|---|
| + | [1, 2] + [3, 4] | [1, 2, 3, 4] | 合并 | 字符串、列表、元组 |
| * | [‘Hi!’] * 4 | [‘Hi!’, ‘Hi!’, ‘Hi!’, ‘Hi!’] | 复制 | 字符串、列表、元组 |
| in | 3 in (1, 2, 3) | True | 元素是否存在 | 字符串、列表、元组、字典 |
| not in | 4 not in (1, 2, 3) | True | 元素是否不存在 | 字符串、列表、元组、字典 |
python内置函数
len()获取容器的元素数量max()返回容器中元素最大值 | 类似的min()最小enumerate()- 在使用for循环的时候可以遍历数据又可以遍历索引, 列表/元组/字典都可以使用
- 当用于字典遍历所有items时返回的是一个索引数据和一个元组
2.7. 可变不可变类型
不可变类型
- 定义:不允许在原本内存空间基础上修改数据, 修改数据后内存地址会发生变化
- 示例:列表, 字典, 集合
可变类型
- 定义:允许在原本内存空间修改数据,修改后一是在原有内存空间基础上修改数据内存不变,二是重新赋值内存地址可能发生变化
- 示例:数字, 字符串, 元组
3. 函数
程序中定义的变量都是保存在内存中的, 局部变量也是, 当函数执行结束后局部变量都会销毁,内存释放
3.1. 文档说明
1 | def show(): |
3.2. 返回值
- 函数不写
return取函数返回值时会取到None - 在多层循环中
return可把多层全部终止,break只能终止一层
3.3. 局部变量&全局变量
局部变量:作用域仅在函数体内部, 随着函数执行结束会销毁
全局变量:在函数体内外都生效,不会随着函数执行结束会销毁
global本质是表示: 要修改全局变量的内存地址, 所以只有不可变类型需要global在函数内部使用全局变量时, 要先声明
global全局变量, 如果是可变类型就不需要了对于操作全局变量的数据, 如果是通过重新赋值来完成的, 那么必须加上
global关键字nonlocal使用场景是函数嵌套时,内层函数要使用外层函数的变量或参数
1 | a = 1 |
3.4. 函数参数
分类
位置参数:按照位置顺序依次给函数的参数传值
关键字参数:按照关键字名给函数的参数传值
注:前面按位置, 后面按关键字, 如果前面用了关键字参数, 后面不能再使用位置参数, 只能使用关键字参数传参
不定长参数: 函数的参数个数不确定, 可能0个, 可能多个
- 不定长位置参数,
*args, 调用函数时所有位置参数都封装成元组, 赋值给args - 不定长关键字参数,
**kwargs, 调用函数时所有关键字参数都封装成字典, 赋值给kwargs
注:
*args和**kwargs这两个参数名可以修改, 但一般不改, 大家习惯了- 不定长位置参数,
1 | def show(name, *args, age=18, **kwargs): |
拆包:使用不同变量保存容器类型中的每个数据,对应的变量和数据数量要保持一致致
容器类型如:字符串, 列表, 元组, 字典, range, 集合(set) 都可以利用拆包, 容器类型可以使用变量保存不同的数据
*my_tuple: 对元组/列表进行拆包, 也就是把元组/列表里每个数据按位置参数进行传参**my_dict: 对字典进行拆包, 也就是把字典里面的每一个键值对按关键字的方式进行传参
3.5. 匿名函数
定义:没有名字的函数, 就是匿名函数, 匿名函数返回值不需要 return,用 lambda 定义
格式: lambda [形参1], [形参2], ... : [单行表达式] 或 [函数调用]
1 | my_func = lambda a: print(a) |
3.6. 常见函数定义
- 递归函数: 在一个函数内调用的是函数本身, 这样的函数称为递归函数
- 函数嵌套:python中, 可以在函数内部再定义一个函数, 称为函数的嵌套(例:装饰器)
- 高阶函数:函数的参数或者返回值是一个函数类型, 那么这样的函数就叫高阶函数(例:装饰器)
4. 文件
4.1. 常识
在windows的python解释器里面, 打开文件默认的编码格式是
gbk的在mac和linux的解释器里面, 打开文件默认的编码格式是
utf-8的utf-8一个汉字占用三个字节, 一个字母占1个字节编码:
.encode("utf-8")解码:
.decode("utf-8")
5. 面向对象
面向对象就是对面向过程的封装
面向对象的三大特性
封装: 把属性和方法放到类里面的操作就是封装, 封装可以控制属性和方法的访问权限
继承: 子类可以使用父类的方法或者属性, 提高了代码的复用性, 注意点: 父类的功能满足不了子类的需要, 重写父类的方法
多态:
- 对象调用同一个方法会出现不同的表现形式(表现结果)
- 多态的好处, 代码的可扩展性强, 代码兼容性强, 不关系类型, 只关系对象是否具有指定功能方法
5.0. 类的实例化过程
内存分配:Python为对象分配所需的内存空间。
初始化实例:调用类的__init__方法来初始化实例。__init__方法是类中一个特殊的方法,它在实例化对象时被自动调用。
创建对象引用:创建一个指向该实例的引用,允许你通过变量来访问该实例。
执行__new__方法(可选):如果定义了__new__方法,它将在__init__之前被调用。__new__方法负责创建实例。
返回实例:返回一个指向新实例的引用,使你可以使用该引用来操作该实例。
总结起来,实例化一个类时,Python会为对象分配内存空间,然后调用__init__方法初始化实例,最后返回新实例的引用。这样,就可以通过该引用来操作和访问该实例的属性和方法。
5.1. 魔法方法
定义:方法名前后都有两个下划线, 这样的方法称为魔法方法, 魔法方法具有一定的特殊功能
常见魔法方法:
__new__分配内存的方法, 在__init__之前调用__init__, 在创建一个对象时默认被调用,不需要手动调用, 可以在此方法内添加对象属性__del__, 创建对象后, python解释器默认调用__init__方法, 当删除对象时, python解释器也会默认调用__del__方法__str__, 当使用print输出对象的时候, 默认打印对象的内存地址, 如果类定义了此方法,那么就会打印从在这个方法中return的数据, 此方法返回必须是字符串类型, 作为这个对象的描述信息.__slots__方法, 限定自定义类型的的对象只能绑定某些属性, 只对当前类对象生效, 对子类并不起任何作用__enter__表示上文方法,需要返回一个操作文件对象__exit__表示下文方法,with语句执行完成会自动执行,即使出现异常也会执行该方法
对象销毁的方式:
程序运行结束, 程序中所使用对象都要在内存中销毁
当对象没有变量使用的时候, 该对象就会被销毁, 引用计数为0时会销毁
5.2. 继承
语法:class 子类名(父类名): / class 子类名(父类1, 父类2):
说明:子类复用父类里面的属性或方法, 提高代码的复用性, 能够使用父类里面的方法或者属性, 包括__init__方法
常识:
父类也称为基类, 子类也称为派生类
单继承:子类只继承一个父类
多继承:子类继承多个父类, 可以使用多个父类里的方法
.mro()方法可查看类的继承顺序
多层继承:只要有类继承关系, 子类对父类及所有上层父类的方法都可以使用
继承后方法的调用:先从本类查找, 依次往后查找, 找到就停, 如果没找到对应方法, 程序崩溃
重写:子类继承父类, 对父类的功能方法进行重新改造(子类方法名要和父类方法名相同)
子类调用父类方法:
self.方法(): 当子类没有这个方法时候才可以用, 子类有相同方法时用父类的类名.方法(self)父类的类名.方法(self): 类名调用对象方法, 需要自己手动传入self参数, 对象调用对象方法, 不需要传self参数super().方法():super是一个类,super()表示创建了一个父类对象, 通过__init__方法给对象添加属性完整写法
super(子类名称, self).父类方法(): 指定类名, 根据子类获取对应父类super本质: 根据指定类 在类的继承顺序**类名.mro()**中获取下一个类, 然后调用下一个类的方法, 如果是单继承, super的调用可以认为是调用的是父类的方法
5.3. 私有权限
- 在属性名和方法名前加两个下划线
- 私有属性和私有方法只能在本类中使用, 不能在类外部使用
- 其实私有属性及方法只是对属性名和方法名进行了包装, 把名字进行了修改
- 总结: 私有属性和方法 的包装格式: 在属性名和方法名前面加
_本类类名__ - 子类无法使用父类的私有属性和私有方法, 也是把名字做了包装, 同上
- 给对象添加私有属性只能在
__init__方法里面完成
5.4. 类属性和实例属性
类属性: 在类的内部init方法外部定义的属性, 类属性属于类
- 私有类属性: 类名前加两个下划线, 也是把名字做了包装, 实际同对象的私有
实例属性: 在init方法内部定义的属性称为实例属性, 实例属性属于实例 (实例 == 对象)
类不能访问对象属性, 但是对象可以访问类属性(对象不能修改类属性, 只能类去修改)
总结: 对象属性的操作是由对象完成, 类属性操作由类来完成, 只不过对象可以访问类属性(也可以用 self.class.类属性 修改类属性, 用class找到类然后是类去修改类属性), 类不能访问对象属性
5.5. 类中方法的种类
实例方法: 方法的第一个参数是self, 那么这样的方法就是对象方法, self表示当前对象, 实例方法, 类不能调用
类方法(修改和获取类的私有属性时使用): 方法第一参数cls并且还需要使用
@classmethod的关键字进行修饰, cls表示当前类, 类方法可以获取和修改类的私有属性, 类方法类和对象都可以调用静态方法: 方法里没有self和cls参数并且还需要使用
@staticmethod的关键字进行修饰
6. 异常&模块
6.1. 异常
异常捕获 try...except...
try表示尝试执行可能出问题的代码,except表示如果代码出现异常, 进行捕获as e:- 捕获异常类型的通用写法就是用
Exception, 因为大多数异常类型都是最终继承Exception的 BaseException可以捕获任何异常
try...except...else...finally
except与else互斥,finally不管有没有异常都执行
异常的传递
- 当执行代码的时候遇到错误, 首先判断当前代码块对异常进行捕获, 如果没有, 那么再把异常一层一层往外传递, 如果外界的都没对异常的捕获, 程序就会崩溃, 如果有异常捕获, 就不会崩溃了
自定义异常
- class定义自定义异常类, 必须继承
Exception或者BaseException才可以 - 抛出自定义异常使用关键字
raise - 注意:raise只能抛出异常类的对象
6.2. 模块
通俗理解模块就是一个 .py 文件, 模块里面可以定义具体的功能代码(类, 函数, 全局变量, 匿名函数等等)
1 | # 查看导入模块的搜索路径 |
- 模块好比一个工具箱, 模块里的每一个具体代码好比一个工具
- 模块的命名规则和变量名的命名规则一样 使用下划线命名
- 模块名的组成和变量名组成一样, 字母, 数字, 下划线开头, 如果以数字开头, 这个模块就不能使用了
导入模块的两种方式
- import 模块名 as 别名
- from 模块名 import 功能代码(函数, 类, 全局变量) as 别名
- from 模块名 import * 导入模块里所有功能代码 一般不这样使用
导入模块注意点
- 自制的模块名不要和系统的模块重名
- 使用from 模块名 import 功能, 在当前模块不要再定义导入功能的代码, 否则会覆盖之前导入功能代码
自制模块
- all 指定导入对应的功能代码 all = [类名, 类名…..] all定义针对外界使用from 模块名 import * 导入, 只能导入all里面指定的功能代码
主模块名字: main
导入的模块名字: 就是模块原本的名字
包: 通俗理解只要文件夹里包含一个__init__.py文件, 那么这个文件夹就是包
包的作用: 包是用来管理不同模块的
模块的作用: 模块是用来管理不同功能代码的
包名的命名规则和变量名一样, 使用下划线命名法
包名的组成和变量名的组成一样, 数字, 字母, 下划线 不能数字开头
包的特点:
- 包里面有一个__init__.py文件,这是包的初始化文件, 当且仅当第一次导入包的时候会执行这个文件
- init.py 其实就是包的象征文件
- init.py 可以控制模块的导入行为
- init.py 可以定义类, 函数, 全局变量等代码
包的导入目的使用包里面的模块
格式:
- import 包名 指定导入包, 用包调用模块, 使用模块中的功能代码 第一次导入包的时候会默认调用__init__.py
- import 包名.模块名
- from 包名 import 模块名
- from 包名 import 模块名 as 模块别名
- from 包名 import * 默认不是导入包里所有模块, 需要在__init__.py中使用__all__去指定
二. 高级
1. 多任务
1.1. 常识
多任务的目的:充分利用CPU资源,提高执行效率
时间片:内核分配给程序执行的一小段时间,这个时间内进程拥有cpu资源
同步:协同步调,按预定的先后次序进行运行。如:你说完,我再说
进程、线程同步:可理解为进程或线程A和B一块配合,A执行到一定程度时要依靠B的某个结果,于是停下来,示意B运行; B执行,再将结果给A; A再继续操作
进程状态: 等待状态不占用时间片, 即使时间片有剩余也会退出不占用CPU资源, 只有运行状态才占用CPU资源

1.2. 执行形式
- 并发:在一个时间段内,交替的执行多个任务,任务数 > CPU核心数,时间片轮转
- 并行:在一个时间点,多核CPU同时执行多个任务,任务数 < CPU核心数
- 一般情况下,并发和并行同时存在
1.3. 实现方式:进程
- 进程是操作系统进行资源(CPU、内存)分配的基本单位
- 程序中至少有一个进程,这个进程称为主进程
- 主进程会等待所有子进程执行结束再结束
- 如果子进程没执行完,主进程会一直等待,此时如果子进程进入死循环会导致主进程无法退出解决办法:
- 设置子进程为守护主进程,主进程退出时子进程直接销毁:
sub_process.daemon = True - 主进程退出前先销毁子进程:
sub_process.terminate()
- 设置子进程为守护主进程,主进程退出时子进程直接销毁:
- 如果子进程没执行完,主进程会一直等待,此时如果子进程进入死循环会导致主进程无法退出解决办法:
- 每个进程中至少有一个线程,这个线程称为主线程
- 进程间不共享全局变量
- 进程之间执行也是无序的,由操作系统调度决定
1 | # 进程创建子进程时程序会复制一份代码去跑(也就是说操作系统会再次进行资源分配,所以创建出来的子进程所拥有的内存是和创建它的进程的内存是不同的,所以不可能共享全局变量) |
1.4. 实现方式:线程
- 线程是进程中执行代码的一个分支,每个线程想到执行代码需要CPU进行调度
- 线程是CPU调度的基本单位,每个进程至少有一个线程,称为主线程
- 主线程会等待所有子线程结束再结束
sub_thread.setDaemon(True)threading.Thread(target=task, daemon=True)
- 线程执行时无序的, 谁抢到CPU, 谁就执行
- 线程之间共享全局变量,因为在同一进程里面,所以使用的内存资源是相同的,这会导致数据错乱问题,解决方案
- 线程等待
sub_thread.join() - 互斥锁:对共享数据进行锁定,保证同一时刻只有一个线程操作共享数据
- 以上两种方法都是把多任务改成单任务去执行,保证了数据的准确性,但执行效率会下降
- 线程等待
1 | import time |
1.5. 实现方式:协程
迭代器 Iterator
- 可迭代对象(Iterable)定义:包含
__iter__方法- 可迭代对象不一定是迭代器,但迭代器一定是可迭代对象
1 | # 判断一个对象是否可迭代 |
- 迭代器定义:包含
__iter__和__next__方法- 迭代是访问集合元素的一种方式
- 迭代器是一个可以记住遍历位置的对象
- 迭代器对象从集合第一个元素开始访问,直到所有元素被访问结束
- 迭代器只能往前不能后退
- 迭代器可以节省内存空间,实现循环
- 迭代器优点:存放生成数据的实现方式而不是具体数据,占用很少的内存空间
1 | from collections.abc import Iterable, Iterator |
生成器 Generator
- 生成器是一种特殊的迭代器
- 如果一个函数中有
yield语句,那么这个函数就不再是函数,而是一个生成器模板 - 定义:生成器推导式
- 列表推导式:
[i for i in range(3)]- 把列表推导式的
[]改为()返回的就是一个生成器
- 把列表推导式的
- 列表推导式:
- 生成器的启动:让生成器从断点处继续执行,即唤醒生成器
next()第几次启动都可以,但不能传参obj.send(param)需要传参时使用,不能第一次启动时使用
- 获取生成器数据用
next(generator)方法 - 生成器数据全部取出后再次使用
next()方法会报StopIteration错误 yield关键字有两个作用- 保存当前运行状态,暂停执行,将生成器挂起
- 将
yield关键字后面表达式的值作为返回值返回,此时类似return
1 | def create_num(cnt): |
协程 Coroutine
1 | # 使用greenlet |
1.6. 不同实现方式对比
- 进程是资源分配的基本单位,切换需要资源最大,效率很低
- 线程是操作系统调度的基本单位,切换需要的资源一般,效率一般(不考虑GIL的情况下)
- 协程切换任务需要的资源很小,效率高
- 多进程、多线程根据CPU核数不同可能是并行的,协程在一个线程中所以一定是并发的
1.7. GIL锁
GIL(全局解释器锁)是一个在CPython解释器中的锁,用于确保同一时刻只有一个线程执行Python字节码。这是由于CPython的内存管理机制并不是线程安全的,因此GIL可以防止多个线程同时访问、修改同一块内存,从而避免了可能出现的数据竞争和内存错误。但同时,GIL也限制了Python多线程并行性能,在一些密集计算和多线程CPU密集型任务场景中表现不及其他语言和并发框架。
CPython解释器的内存管理机制是基于引用计数的垃圾回收,即对象被引用一次计数器加一,对象引用被释放计数器减一,当计数器变为0时,对象被回收。这种内存管理机制并不是线程安全的,因为多个线程可能同时访问和修改同一块内存,从而导致计数器不一致,或者对象被销毁多次,或者内存泄漏等问题。因此,为了避免这些问题,CPython引入了GIL锁来确保同一时刻只有一个线程执行Python字节码,从而保证内存管理的线程安全性。
- 全局解释器锁
- 保证同一时间, 只有一个线程使用CPU, 不管主子线程
- GIL的存在导致, python中只有进程是可以并行的, 多线程实际也是并发的
- 一个进程有一个GIL锁
- GIL不是python的特性, 只是CPython解释器的概念, 历史遗留问题
- 所以cpu超线程对python是没用的!比如2核4线程,一个python死循环会让cpu两个线程跑满(这里说的线程是硬件层面的技术,模拟多个逻辑CPU提高处理并发性能)
GIL锁什么时候释放
- 当前线程执行超时后会释放
- 当前线程阻塞操作时会自动释放(input, io/输入输出)
- 当前执行完成时
GIL的弊端
- GIL对计算密集型的程序会产生影响。因为计算密集型的程序,需要占用系统资源。
- GIL的存在,相当于始终在进行单线程运算,这样自然就慢了。
- IO密集型影响不大的原因在于,IO,input/output,这两个词就表明程序的瓶颈在于输入所耗费的时间,线程大部分时间在等待,所以它们是多个一起等(多线程)还是单个等(单线程)无所谓的。
解决方案:
要提升多线程执行效率,解决方案:
- 更换解释器
- 改为进程替换多线程
- 子线程使用C语言实现(绕过GIL锁)
必须要知道的是:
- CPU 密集(计算密集)型不太适合多线程
- I/O 密集型适合多线程/协程(Gil锁会释放)
2. 高级语法
2.1. 闭包&装饰器
1 | """闭包 |
2.2. property
3. property属性
- property属性就是负责把一个方法当做属性进行使用,这样做可以简化代码使用
- 定义方式
- 装饰器方式
- 类属性方式
1 | # 类属性方式 |
2.3. with语句&上下文管理器
- with 语句执行完成以后自动调用关闭文件操作, 即使出现异常
- 一个类只要实现了
__enter__()和__exit__()这个两个方法,通过该类创建的对象我们就称之为上下文管理器
1 | # 要实现上下文管理器, 要实现__enter__ 和 __exit__ |
上下文管理器可以使用 with 语句,with语句之所以这么强大,背后是由上下文管理器做支撑的,也就是说刚才使用 open 函数创建的文件对象就是就是一个上下文管理器对象
__enter__表示上文方法,需要返回一个操作文件对象__exit__表示下文方法,with语句执行完成会自动执行,即使出现异常也会执行该方法
2.4. 深拷贝和浅拷贝
import copy拷贝的目的: 保证原数据和拷贝的数据之间不影响copy.copy()浅拷贝,只对可变类型的第一层对象进行拷贝,对拷贝的对象开辟新的内存空间进行存储,不会拷贝对象内部的子对象- 不可变类型进行浅拷贝不会给拷贝的对象开辟新的内存空间,而只是拷贝了这个对象的引用
- 可变类型进行浅拷贝只对可变类型的第一层对象进行拷贝,对拷贝的对象会开辟新的内存空间进行存储,子对象不进行拷贝
copy.deepcopy()深拷贝, 只要发现对象有可变类型就会对该对象到最后一个可变类型的每一层对象就行拷贝, 对每一层拷贝的对象都会开辟新的内存空间进行存储- 不可变类型进行深拷贝如果子对象没有可变类型则不会进行拷贝,而只是拷贝了这个对象的引用,否则会对该对象到最后一个可变类型的每一层对象就行拷贝, 对每一层拷贝的对象都会开辟新的内存空间进行存储
- 可变类型进行深拷贝会对该对象到最后一个可变类型的每一层对象就行拷贝, 对每一层拷贝的对象都会开辟新的内存空间进行存储
- 浅拷贝最多拷贝对象的一层 (即使可变类型, 也只拷贝第一层) 其它情况都是拷贝引用
- 深拷贝可能拷贝对象的多层 (只要是有可变类型, 就全部拷贝) 其它情况都是拷贝引用
2.5. 单例
1 | # 只有一份内存空间 |
面试造飞机
生成器和迭代器的区别
- 定义方式:
- 生成器是一种特殊的函数,它使用
yield关键字来定义,并在函数体内使用yield来产生值。生成器函数可以暂停执行,并在需要时恢复执行,每次恢复执行时会从上次暂停的位置继续执行。 - 迭代器是一个实现了迭代协议的对象,它包含
__iter__()方法和__next__()方法。__iter__()方法返回迭代器自身,而__next__()方法用于返回迭代器的下一个值,如果没有下一个值则抛出StopIteration异常。
- 生成器是一种特殊的函数,它使用
- 使用方式:
- 生成器通常用于生成一个序列的值,它们可以通过
for循环来迭代产生的值,也可以通过调用next()函数手动迭代。 - 迭代器则是一种更通用的迭代工具,它可以迭代任何实现了迭代协议的对象,包括列表、元组、集合等。迭代器可以通过
iter()函数获取,也可以直接用于for循环中。
- 生成器通常用于生成一个序列的值,它们可以通过
- 内存占用:
- 生成器是一种惰性求值(Lazy Evaluation)的机制,它只在需要时生成值,并且不会一次性将所有值存储在内存中,因此生成器在处理大量数据时具有较低的内存消耗。
- 迭代器通常需要将所有的值存储在内存中,因此在处理大量数据时可能会消耗较多的内存。
- 可变性:
- 生成器是不可变的,一旦定义完成后,生成器的值无法修改。
- 迭代器通常是可变的,可以在迭代过程中修改迭代器的状态,例如添加、删除元素等。
综上所述,生成器和迭代器在 Python 中都是用于处理可迭代对象的工具,但它们的定义方式、使用方式、内存占用和可变性等方面有一些区别。生成器通常用于惰性生成值,而迭代器则是一种通用的迭代工具,用于迭代任意可迭代对象。
- 定义方式:
python内存管理(同GIL锁是cpython解释器的特性)
Python的内存管理机制主要依赖于引用计数来迅速释放不再使用的对象,同时通过垃圾回收来处理循环引用等特殊情况,保证内存的高效利用。这种自动内存管理的方式让开发者从手动管理内存的负担中解放出来,提高了开发效率。
引用计数:Python使用引用计数来跟踪每个对象被引用的次数。当一个对象被创建或者另一个对象引用它时,引用计数就会增加。当一个对象的引用计数降为0时,表示没有任何变量指向该对象,那么该对象将成为垃圾,Python会回收这部分内存以供其他对象使用。
- 引用计数的优点是在对象不再被引用时立即释放内存,因为没有等待垃圾回收器的运行。但是引用计数无法解决循环引用的问题(两个或更多的对象相互引用,导致它们的引用计数都不会降为0),为此,Python引入了垃圾回收机制。
垃圾回收(Garbage Collection): Python中的垃圾回收机制是为了解决循环引用以及其他无法通过引用计数检测的垃圾对象。Python采用了分代回收算法,将对象分为不同的代(generation)。新创建的对象位于第0代,每经过一次垃圾回收,存活的对象会晋升到下一代。随着对象存活时间的增加,回收的频率会降低,因为大部分对象很快就会变成垃圾,只有少数长时间存活的对象才需要耗费更多的垃圾回收时间。
标记-清除(Mark and Sweep):垃圾回收器首先标记所有可以访问的对象,然后清除所有未标记的对象,释放其内存。
分代回收(Generational Garbage Collection):将对象按照存活时间分为不同的代,一般将新创建的对象放入第0代,存活时间更长的对象依次放入第1代、第2代,通过不同代之间的垃圾回收来提高效率。
引用计数+标记-清除:综合利用引用计数和标记-清除算法,处理循环引用以及其他无法通过引用计数回收的对象。
分代回收在处理循环引用的情况时,通过为每个对象分配一个分代标记来解决这个问题。分代标记可以被划分为两种类型:线程局部分代标记(thread local allocation context)和全局局部分代标记(global allocation context)。
当一个对象被创建时,Python内核会为其分配一个分代标记,并将该标记与对象一起存储。对于循环引用的对象,分代标记会被保持不变。当一个对象被销毁时,Python通过检查对象的分代标记来确定是否已经被回收。如果对象的分代标记未被其他地方引用,那么该对象已经不再被使用,并需要进行回收。
为了避免不必要的循环回收,Python内核会记录每个对象是否已经被回收。一旦对象被回收,就不会再被释放,即使再次被检测到。这样可以确保只有一次地回收每个对象。
tcp与udp区别
- 连接:tcp是一种面向连接的协议,在数据传输前要先建立一个连接,UDP是无连接协议,不需建立连接
- 可靠性:TCP提供可靠的数据传输,确保数据包在从发送端到接收端的传输过程中不会丢失、被改变或出现重复,UDP不保证数据传输可靠性,可能导致丢包、被改变或重复
- 速度:由于TCP的可靠性和流量控制机制,传输速度相对UDP会慢很多
- 设计目的:TCP适用于对数据传输可靠性和稳定性要求较高,如文件传输、电子邮件和网页浏览等,UDP更擅长对实时性要求较高的应用,如实时语音、视频通信,在线游戏等
- 头部信息:TCP和UDP的头部信息分别为20字节和8字节。TCP的头部信息包括数据包的序号和个别流标记,用于确保数据包在传输过程的顺序和完整性。UDP的头部信息相对较少主要包括数据包的源端和目标端的IP地址和端口
https原理
- SSL/TCS 密钥包括一个私钥和一个公钥
- 当用户通过浏览器访问一个HTTPS页面,服务器首先向浏览器发送公钥,浏览器接收到公钥后,会使用它来加密数据(请求头和请求体),然后将加密数据发送回服务器
- 服务器使用接收到的加密数据和自己的私钥来解密数据。
TCP3次握手4次挥手
- 客户端向服务端发送一个SYN(初始化)包,表明希望建立一个连接
- 服务端同意建立连接会回复SYN+ACK包,表示可以建立连接
- 客户端回复ACK包,连接建立
- 客户端向服务端发送一个FIN包,表明希望断开连接,进入终止等待1状态
- 服务端向客户端回复一个ACK包,表示进入终止等待状态,此时可传数据
- 服务端向客户端发送一个FIN包,客户端进入终止等待2状态
- 客户端向服务端回复一个ACK包,客户等待超时时间后断开连接,服务端立刻断开连接
Flask路由原理
Flask 的路由原理主要基于装饰器和 Python 的函数式编程概念。具体来说,Flask 使用 @app.route() 装饰器将 URL 路径和视图函数绑定在一起。当接收到一个 HTTP 请求时,Flask 会根据请求的 URL 找到对应的视图函数,并执行该函数来处理请求。
django处理csrf原理
Django处理CSRF的原理基本上是通过在每个页面加载时生成一个唯一的CSRF令牌,并将其存储在用户的会话中。当用户在表单中提交请求时,Django会验证请求中包含的CSRF令牌是否与用户会话中存储的令牌匹配,以确保请求来自于站点的合法来源。
django cookie session原理
在Django中,会话(Session)是一种存储在服务器端的数据,用于在用户请求之间保持状态。Django通常使用基于cookie的会话来管理会话数据,其原理如下:
- 会话数据存储: 当用户首次访问Django应用时,Django会为该用户创建一个唯一的会话标识符(session ID),通常是一个随机生成的字符串。会话数据存储在服务器端的缓存中,默认情况下使用的是数据库缓存。会话数据可以包含任意类型的数据,例如用户的身份验证信息、用户偏好设置等。
- 会话ID的传递: 一旦会话数据存储在服务器端,Django会将会话ID发送给客户端浏览器,通常是通过一个名为
sessionid的cookie。这个cookie包含了用户的会话ID,用于在后续的请求中标识用户的会话数据。 - 请求中的会话ID提取: 当用户在浏览器上发送新的请求时,浏览器会自动将之前设置的
sessionidcookie包含在请求头中。 - 会话数据的检索: Django在处理请求时会检查请求头中是否包含
sessionidcookie,如果包含,Django会使用该会话ID来检索对应的会话数据。 - 会话数据的使用: 一旦会话数据被检索到,Django会将其提供给视图函数,以便在视图中使用。开发人员可以在视图中读取和修改会话数据,从而实现跨请求的状态保持。
- 响应中的会话ID更新: 如果在处理请求时会话数据发生了变化,Django会在响应中将更新后的会话ID发送给客户端浏览器。这是为了确保会话ID的安全性,以及避免可能的会话固定攻击(Session Fixation Attack)。
通过这种方式,Django使用基于cookie的会话机制来管理用户会话数据,从而实现了用户状态的保持和跨请求的数据传递。这种机制是一种常见的Web应用程序开发中用于处理用户状态的方式。
TIPS
字典无序性: 在Python 3.7及之前的版本中,字典元素的顺序是不确定的,即它们的存储顺序与插入顺序不一定一致。但是从Python 3.7开始,字典保留了元素插入的顺序。这意味着当你迭代字典时,它们的顺序将与你添加键-值对的顺序相同。