Python开发学习(1)
最后更新时间:
文章总字数:
预计阅读时间:
页面浏览: 加载中...
清一下存货
哒哒哒哒哒,好想玩原神,云原神?
py2和py3的区别
一个很明显的区别是print
在python2的语法是这样的:
1 | |
print是一个关键字
而python3的print就变成了一个函数:
1 | |
除此之外,字节字符串和文本字符串的类型也不同:
| 版本 | python2 | python3 |
|---|---|---|
| 字节字符串 | str | byte |
| 文本字符串 | Unicode | str |
还有python2的默认字符串类型是ASCII、python3的是Unicode
python2中的除法结果是整形、而python3是浮点型
python2默认是旧式类,需要显式继承新式类来创建新式类。python3中完全移除旧式类,所有类都是新式类,仍可继承object类
闭包
闭包指的是延伸了作用域的函数,访问定义体之外定义的非全局变量。
创建一个闭包必须满足以下几点:
- 必须有一个内嵌函数
- 内嵌函数必须引用了外部函数的变量
- 外部函数的返回值是必须是内嵌函数
1 | |
作用:
- 能够保存状态
如下所示:
1 | |
此时add5已经记住了x为5,后续调用add5会记住这个x为5
- 还能够用于实现装饰器
- 能用于函数回调或者延迟执行,函数稍后执行时仍能带上原来的数据。
如果内部函数需要修改外部变量时,需要对该变量添加nonlocal关键字:
1 | |
装饰器
装饰器在导入模块时立即执行,被装饰的函数只能够在明确调用时执行
例如下面的装饰器例子,用于限制请求次数:
1 | |
这里是一个带参数装饰器,如果请求次数在time_window内超过max_requests请求则会直接return,反之正常执行
python的GC
python中的GC:自动找出程序里不再使用的对象,把内存收回来
python的三种回收方式:
- 引用计数(主要使用)
每个对象都记着被引用了多少次。此时被引用计数器会+1,后面引用减少了就会-1,当引用计数达到0时就会被回收
- 标记-清除法
标记-清除法用于处理循环引用的回收。比如A引用B,B里也引用A。即使外部已经不用它们,引用计数也不会变成0,此时GC会从根对象出发,能找到的标记为“活着”,找不到的直接清理。
- 分代回收
将对象按年龄分为0、1、2代,新对象更容易变成垃圾,因此优先扫第0代;活得越久的对象(代数越高),扫描次数越少,越节省性能
GIL
cpython使用简单的锁机制避免多个线程同时执行字节码,cPython中的GIL机制为:同一时刻,只允许一个线程执行python字节码。
缺陷:会限制程序的多核运行,同一时间里只有一个线程在执行字节码,如果是CPU密集型多线程(例如计算)难以真正利用多核并行,多个线程会互相抢GIL。
但是,对于IO密集型程序(大量程序花费在网络传输、资源调度上),影响小,等待网络、文件、sleep时会释放GIL
因此,为了避免GIL影响到多核,常常对于密集CPU运算采用多进程+进程池的方式,而IO密集型采用多线程的策略,还可以使用cython扩展(将python转换为c代码)
线程安全:
一个操作如果只用1个字节码指令就可以完成时,该操作就是原子的。原子操作可以保证线程安全,有了GIL之后仍需要关注线程安全,必要时要人为加锁。
浅拷贝和深拷贝
简单来说,浅拷贝只复制最外层,里面的嵌套是共享的,而深拷贝会让副本和原对象完全独立。
1 | |
鸭子类型
有点何意味了,意思就是对象的特征是通过对象的方法决定的:
1 | |
其实就是有点类似java里接口的概念
猴子补丁
猴子补丁就是在运行时对已有的代码进行修改,达到热补丁的目的:
1 | |
python作用域
L:局部作用域
E:闭包函数外的函数中
G:全局作用域
B:Builtin内建作用域
按L-E-G-B的规则去查找。
即局部找不到,会去局部外的局部找,再去全局找,再去内建中找
lambda函数
使用lambda定义一个匿名函数,lambda并不会带来程序运行效率的提升,只是会使代码更加简洁,使用lambda时函数内不要有循环,如果有,定义函数来完成。
常用lambda的函数:
1 | |
- map()函数接收两个参数,一个是函数,一个是Iterable,map将传入的函数依次作用到序列的每个元素,并把结果作为新的Iterator返回。
- reduce函数,reduce函数将一个函数作用在一个序列[x1, x2, x3, …]上,这个函数必须接收两个参数,reduce把结果继续和序列的下一个元素做累积计算,其效果就是:
reduce(f, [x1, x2, x3, x4]) = f(f(f(x1, x2), x3), x4) - filter()函数用于过滤序列。filter()接收一个函数和一个序列,把传入的函数依次作用于每个元素,然后根据返回值是True还是False决定保留还是丢弃该元素。
迭代器和生成器
iterables和iterators
iterable是可迭代对象,例如列表、元组、字典等(可迭代就是可以用for循环遍历),能够使用iter()方法转换为迭代器
iterator是迭代器,它需要实现__next__和__iter__魔术方法
__iter__() 方法返回迭代器对象自身,而 __next__() 方法返回下一个迭代值。
因此,iterable是一类对象,它们可以被迭代,而iterator是一类对象,它们是可迭代对象的具体实现,可以用于遍历可迭代对象中的元素。
只有实现__iter__方法的是iterable,实现了__next__和__iter__的是iterator
迭代器
迭代器是一个可以记住遍历位置的对象
- 迭代器从集合的第一个元素开始访问,直到所有元素访问结束
- 结束时会报错
- 只能前进不能够后退
1 | |
使用next()进行迭代:
1 | |
同时可以使用for来循环遍历:
1 | |
可以通过重写__iter__函数和__next__函数实现一个自己的迭代器:
1 | |
生成器
通过列表生成式可以直接创建一个列表,但是会使得列表的空间复杂度较高
因此如果列表是有规律可循的,就可以通过循环的过程不断推算出后续的元素
该方法称为生成器。
1 | |
当遇到较为复杂的算法时,可以使用yield关键字实现
1 | |
- 生成器的return返回值必须从StopIteration异常中的value获取
1 | |
协程
协程比线程更加有优势:
- 协程比线程更加轻量级,占用的内存和资源更少
- 在多线程环境下,需要考虑线程安全和加锁的问题,而协程在单线程中执行,不需要额外的线程同步机制
- 更易管理,协程在单线程中执行,避免了多线程中的竞争条件和死锁问题,使得并发编程更容易管理和调试
- 高效I/O操作:协程会在IO操作的等待时间中自动切换其他的协程,充分利用等待时间,实现高效的非阻塞IO操作
在python中,可以使用async和await来实现协程的定义和管理
例如网络爬虫:
1 | |
在上面的示例中,fetch_url函数使用aiohttp库发送异步HTTP请求,而main函数创建了多个协程来并发执行。每个协程在等待HTTP响应时会切换到其他协程,从而实现了高并发的网络爬虫。
老三样:封装、继承、多态
封装其实就是把内容封装成一个类,然后从类里调用被封装的内容
1 | |
继承:
哎呀就是子类继承父类的方法啊变量这些,继承的好处在于,复用已有代码,同时支持扩展和多态,让类之间形成清晰的层次关系。
但是有些特别:
-
Python的类如果继承了多个类,那么其寻找方法的方式有两种,深度优先和广度优先。
- 当类是经典类时,多继承情况下,会按照深度优先方式查找,Python 2.x中默认都是经典类,只有显式继承了object才是新式类。
- 当类是新式类时,多继承情况下,会按照广度优先方式查找Python 3.x中默认都是新式类,不必显式的继承object。
-
如果子类继承父类做了初始化,且不调用super初始化父类构造函数,那么子类不会自动继承父类的属性。
-
如果子类继承父类做了初始化,且调用了super初始化了父类的构造函数,那么子类也会继承父类的属性。
1 | |
多重继承的问题,调用顺序为如果多个父类有同名方法,Python 按 MRO(方法解析顺序) 决定用哪个。
规则简单理解:从左到右找,先找到先用。
- 多态
java里的多态类似
1 | |
但是python里没有多态,也用不到
classmethod和staticmethod
1 | |
- 普通实例方法:由实例对象调用;至少一个self参数;执行普通方法时,自动将调用该方法的实例对象赋值给self;
- 类方法:由类调用;至少一个cls参数;执行类方法时,自动将调用该方法的类复制给cls;
- 静态方法:由类调用;无默认参数;
一句话总结:classmethod 绑定类,第一个参数是 cls;staticmethod 不绑定类也不绑定实例,就是类里的普通函数。
@property
@property 是 Python 内置装饰器,用来把类里的方法“变成属性”来用。
1 | |
@property装饰器可以实现其他语言所拥有的getter,setter和deleter的功能(例如实现获取,设置,删除隐藏的属性);通过@property装饰器可以对属性的取值和赋值加以控制,提高代码的稳定性。
简单来说就是对private成员不用直接Person.getAge(),直接使用Person.age即可
类成员的修饰符
_xxx表示这是一个保护成员(属性或者方法),它不能用from module import * 导入,其他方面和公有一样访问;__xxx这表示这是一个私有成员,它无法直接像公有成员一样随便访问,但可以通过实例对象名._类名__xxx这样的方式可以访问;__xxx__表示这是一个特殊成员,用来区别其他用户自定义的命名以防冲突,就是例如__init__()、 __del__()这些特殊方法
元编程
写代码操作代码
1 | |
在运行时创建或修改类、函数、属性,常用于框架和减少重复代码。
单例模式
一个类只能够创建一个实例,全局共用这一个对象:
1 | |
装饰器实现:
1 | |
工厂模式
工厂模式更加常见:
1 | |
把创建对象的代码集中起来,调用者只说要什么,不关心具体怎么创建。
工厂模式一般包含工厂方法和抽象工厂两种模式。
Python中的可变与不可变数据类型的区别
可变类型的修改操作是原地修改,而不可变类型的修改操作则是创建一个新的对象并且返回
列表和元组
python列表的基本操作
-
创建列表:使用方括号 [] 将一组元素括起来即可创建一个列表,例如:
a = [1, 2, 3, 4, 5] -
索引和切片:可以使用索引和切片返回列表中的元素,例如
a[0]返回列表中的第一个元素,a[1:3]返回列表中第二个到第四个元素 -
修改列表元素:可以直接通过索引来修改列表中的元素,例如
a[0]=0就是将列表中的第一个元素修改为0 -
添加列表元素:append()向列表末尾添加一个元素,insert()在指定位置插入一个元素,例如:
a.append(6)、a.insert(4,0) -
删除列表元素:
del a[0]删除列表中的第一个元素,a.remove(3)删除列表中值为3的元素,a.pop()删除列表中的第x个元素,并返回这个元素 -
合并列表:可以直接使用
+对两个列表进行合并,例如a + [6,7,8],似乎也可以用extend() -
复制列表:可以使用切片或
copy()方法来复制一个列表,例如:b = a[:]或b = a.copy() -
获取列表长度:可以使用
len()函数来获取列表的长度,例如:len(a)返回列表 a 中元素的个数
python元组的基本操作
- 创建元组:使用圆括号
()将元素括起来,例如:a = (1, 2, 3, 4, 5)
注意:单元素元组必须加逗号,例如:t = (1,);也可以用tuple()转换。 - 索引和切片:和列表类似,例如:
a[0]返回第一个元素,a[1:3]返回索引 1 到 2 的元素,a[-1]返回最后一个元素。 - 修改元素:元组不可变,不能通过索引修改,例如:
a[0] = 0会报TypeError。想“修改”只能创建一个新元组。 - 添加元素:没有
append()、insert(),但可以用+拼接生成新元组,例如:a + (6,)。 - 删除元素:不能删除元组中的单个元素;可以
del a删除整个元组变量,或用切片构造一个不含某元素的新元组。 - 合并元组:使用
+运算符,例如:a + (6, 7, 8)会得到一个新元组。 - 复制元组:可以用切片
a[:]或tuple(a),但元组不可变,通常直接b = a就够,因为不会改。 - 获取长度:使用
len()函数,例如:len(a)返回元组中元素个数。 - 其他常用操作:
a.count(x)统计元素出现次数,a.index(x)查找元素索引;也支持解包,例如:x, y = (1, 2)。
列表和元组的区别
可变性:列表是可变的,可以在原地添加、修改或者删除元素,而元组是不可变的,无法在原地进行修改操作
语法:列表采用[]进行表示,元素之间用逗号分隔;元组使用()进行表示,元素之间也用逗号进行分割,但是如果元组只包含一个元素,则要在该元素后面加一个逗号表示他是一个元组而不是一个普通值,例如:t = (1,)。
性能:元组对于列表来说,在创建、遍历和访问元素时具有更高的性能,因为元组的结构是不可变的,因此在创建后不需要进行额外的修改,不会出现额外的开销
用途:列表常用于要求添加、删除或者修改元素的场景,例如缓存数据、维护计数器、记录用户输入等;而元组通常用于存储一些固定不可变的数据,例如坐标点、日期、时间等。
为列表去重的方法
使用set函数去重:
1 | |
这种方法会改变列表元素的顺序
列表推导式去重
1 | |
这种方法需要额外的空间
for循环去重:
1 | |
这种方法比较简单,但是速度较慢
字典的fromkeys去重,利用的是字典的键不能重复的特性,再将字典的键转换为列表即可:
1 | |
*args和**kwargs
Python 中,*args 和 **kwargs 用于在函数定义时接受任意数量的参数,只能放在参数的最后位置
*args:
1 | |
*args表示接受任意数量的位置参数,这些参数将作为元组传递给函数,具体来说,当函数定义使用*args时,它可以接受任意数量的位置参数,这些参数将会被打包成一个元组
**kwargs,**kwargs表示接受任意数量的关键字参数,这些参数将被作为字典传递给函数:
1 | |
除了在函数定义时使用 *args 和 **kwargs 外,它们还可以在函数调用时使用。当在函数调用时使用 *args 和 **kwargs 时,它们的作用是将一个序列或字典拆包成多个参数
1 | |
Python传参的方式
既然讲到了*args和**kwargs,那还是来了解下python如何传递参数吧
最常见的就是位置参数,就是按位置确定参数是什么
1 | |
其二是关键字参数,给定参数名称=值传入:
1 | |
其三,默认参数,在函数定义时给出的默认值:
1 | |
不建议函数的默认参数传入可变对象,例如:
1 | |
由于列表是可变对象导致了它
注意关键字参数必须在位置参数之后
其四,可变参数*args和**kwargs
Python中的__new__和__init__
两个魔术方法,区别在下面:
- 执行时机。
__new__是在实例创建之前被调用的,因为它的任务就是创建实例然后返回该实例对象,是个静态方法,而__init__是实例对象创建完后被调用的,通常用在初始化一个类实例的时候,是一个实例方法。 - 参数不同。
__new__必须至少要有一个参数cls,代表当前类。而__init__有一个参数self,代表的是当前类的实例(就是__new__返回的实例) - 返回值。
__new__必须要有返回值,返回一个实例化出来的实例,可以返回父类__new__出来的实例,也可以直接返回object的__new__出来的实例,而__init__不需要返回值
1 | |
如果__new__创建的是当前类的实例,会自动调用__init__函数(通过 return 语句里面调用的__new__函数的第一个参数是cls来保证是当前类实例),如果是其他类的类名,则实际创建的是其他类的实例,因此就不会调用当前类的__init__函数,也不会调用其他类的__init__函数
1 | |
1 | |
由于__new__在__init__之前被启动,因此__init__()方法里可以使用self对象已经存在的属性,但是不能修改这些属性
1 | |
==和is
==是比较操作符,用于比较两个变量之间的值是否一致
而 is 则判断的是对象之间的身份(内存地址)是否一致。对象的身份,可以通过 id() 方法来查看。
lambda小问题
1 | |
res的值预期来看应该是[0,2,4,6],因为lst里的函数应该是:0*x, 1*x, 2*x, 3*x
实际上应该是[6,6,6,6]
原理其实是因为这里的i
i在外层作用域lambda x: x*i 为内层(嵌)函数,他的命名空间中只有 {'x': 1} 没有i,所以运行时会向外层函数(这里是列表解析式函数[])的命名空间中请求i,而当列表解析式运行时,列表解析式命名空间中的i经过循环依次变化为0-->1-->2-->3最后固定为3,所以当 lambda x: x*i内层函数运行时,去外层函数取i每次都只能取到 3。
lambda x: x * i 中的 i 不是在定义 lambda 时立即捕获的值,而是一个自由变量。
所有 lambda 都引用同一个变量 i,而这个 i 是列表推导式内部作用域中的循环变量。
列表推导式执行过程:
- 第 1 次循环:
i = 0,生成一个 lambda,但它没有保存0,只是记住了“我要用变量i”。 - 第 2 次循环:
i = 1,生成第二个 lambda,同样引用同一个i。 - 第 3 次循环:
i = 2 - 第 4 次循环:
i = 3
循环结束后,这个共享的 i 最终值是 3。
之后执行 [m(2) for m in lst] 时,每个 lambda 被调用,此时它们去查找 i,找到的都是 3,所以每个结果都是:
python
1 | |
因此输出 [6, 6, 6, 6]。
with
打开文件在进行读写的时候可能会出现一些异常状况,如果按照常规的f.open()写法,我们需要 try,except,finally,做异常判断,并且文件最终不管遇到什么情况,都要执行f.close() 关闭文件,with方法帮我们实现了finally 中 f.close()。
read、readline、readlines
read()直接读全部
readline()读一行
readlines() 读全部行并返回列表。
isinstance和type()
1 | |
区别在于:
- type()不会认为子类是一种父类类型,不考虑继承关系
- isinstance()考虑继承关系
1 | |
sort和sorted
不管是sort还是sorted都有两个关键字可选:
- reverse,如果reverse被设定为True,则会降序输出
- key,一个只有一个参数的函数,这个函数会被用于序列里的每一个元素上,所产生的结果将是排序算法依赖的对比关键字,比如说对一些字符串排序时可以用
key=str.lower来实现忽略大小写的排序。
而sort是可变对象列表的方法,sort会改变可变对象,不可变对象是没有该方法的:
1 | |
sorted是产生一个新的对象,sorted(L)返回一个排序后的L,而不改变原始的L,sorted()适用于任何可迭代容器
比较两个json是否相等
转成字典,再比较:
1 | |
异步非阻塞
同步:调用后一直等到结果返回再接着往下做
异步:调用后不等结果,先做别的,好了再通知
阻塞:当前线程被卡住,不能干别的,即当前线程调用结果返回之前会被挂起,调用线程也只有在得到结果之后才会返回。函数只有在得到结果后才会将被阻塞的线程激活
非阻塞:当前线程没被卡住,可以继续干别的。不能够立刻得到结果之前也会立刻返回,同时该函数不会阻塞当前线程
类比烧水:
- 同步阻塞:站在水壶前一直等水开。
- 异步非阻塞:按下开关,去扫地;水开了再回来倒水。
1 | |
总耗时约 2 秒,而不是 3 秒。因为 A 等待时,程序没有干等,而是去执行 B 了。
幂等的概念
f(f(x)) = f(x)
也就是说无论做多少次,访问多少次这个得到的结果都是一样的
幂等键就是一个标识符,他会用来认出这几个请求其实都是同一件事情
HTTP里,GET、PUT、DELETE等天然幂等(直接重发是没有问题的),而POST不幂等——发送几次就创建几条记录,而扣钱的流程恰好是用POST发的,因此必须要人为补上幂等性。
在项目里的幂等键是放在请求头里的idempotency_key,由App.tsx(前端)生成,是随机的16字节转成16进制的结果。用这个请求头来让后端认为操作是同一个。
后端FastAPI通过
1 | |
将http请求头映射成参数
最后校验该key,并进入数据库查询环节
1 | |
如果没有查询,就新增一条数据
项目
介绍一下你这个项目
我做的这个项目是一个街机游戏厅的自助投币方案。简单来说用户可以在自己的手机上访问这个网页端,然后选择实际的门店,扫描机台上的二维码,然后用自己账户里的余额,只需要点击一下按钮,就能够直接通过后端去投币开一局。这个方案的优势在于现在市面上很多机厅还在用实体币,而我这套方案不需要带实体币,可以省去前台人工接待的时间,减少人工成本。项目设计上使用了python的fastapi作为后端设计,redis和postgresql存储数据。其中redis用于存储一些丢失了不影响正常业务的数据,例如验证码答案、登录session等。postgresql用于存储用户信息、支付信息等关键数据。
项目的技术栈
- fastapi,由于我这个项目需要大量与外部网络进行请求和响应,因此更好的想法是做一个异步处理,这样在等待的时候也能够正常继续业务,不占用线程。而fastapi有原生异步功能,同时能够结合pydantic库等做强数据校验。
- postgresql,其实用mysql也可以做到同样的功能,但是fastapi中有asyncpg来支持PostgreSQL的异步访问,所以干脆就使用postgresql了
- redis,用于存储非重要临时数据,例如登录时的滑块验证码将滑块的正确坐标值存在redis中,在验证时直接从redis中取出,为什么要使用redis存这些数据呢?说白了这些数据其实只是临时数据,存redis里到期就能够自动删除,如果这些东西放数据库还需要自己写一个方法去定时清除。
项目的亮点
我认为我这个项目的亮点在于投币这一块的逻辑上。它的逻辑是这样的,用户点击投1局之后,并不会立刻扣款,而是先更新数据库里的表,标记并冻结金额,并且请求上游api进行投币操作。投币完成后返回实际扣款金额后,将该冻结金额减去,然后余额实际上变为原有的余额减去实际的扣额,并且将该完成订单更新进数据库。这样可以避免预期价格和实际价格不一致导致的差价问题如多退少补等问题。做了一个冻结的设计,如果不做这个冻结的设计的话退款要单独做一个出来。
项目的难点
项目的其中一个难点在于二维码识别上。原先的设计是浏览器唤起手机摄像头然后用户进行拍照,拍照后的图片进入后端进行解析。
后来呢,打算修改成类似微信扫码一样的实时扫描功能,于是采用了react的zxing模组实现实时的条码解析功能,但是问题在于扫码这个功能在本地测试时一切正常,使用时能够扫描并且解析出正确内容,但是拿着手机扫描真实二维码时,识别率较低。
问题的猜测可能是屏幕在手机摄像头里会出现一些摩尔纹等畸变,而且手机在靠近时没有自动聚焦,直接变模糊,二维码清晰时离取景框较远,反之会变得模糊。
解决方式:
- 加了点击聚焦功能
- 采用了双层保障,第一层用OpenCV库对二维码进行识别,如果OpenCV无法解析的,会使用更为强大的微信CNN开源模型进行识别。
改进之后识别成功率大概有六成以上。