主题
Python进阶
可变对象与不可变对象
- 不可变对象可以哈希,能作为字典的键;而可变对象则不能被哈希。
- 通常情况下类的实例对象是可以被哈希的,但如果修改类的
__hash__ = None
,那么实例对象就不能被哈希了。 - 可变对象作为函数参数传入,传递的是对象的引用,修改引用会直接修改原对象;不可变对象作为函数参数传递的是值,修改值不会影响原对象。
不可变对象 | 可变对象 | |
---|---|---|
赋值运算符 | 没有新的内存地址,都指向同一块内存空间 | 没有新的内存地址,是原对象的引用。修改会影响对方。 |
浅拷贝 | 有新的内存地址,但子元素是复制原对象子元素的引用。所以修改内部的可变对象会影响原对象 | |
深拷贝 | 有新的内存地址,不可变子元素复制引用(内存地址相同),可变子元素创建新副本(内存地址不同) |
不可变对象:
python
a = 10
b = a
b = 20 # 重新赋值
需要注意:虽然b是a的引用,但对于不可变对象,重新给引用赋值,原对象是不会修改的。
可变对象:
python
from copy import copy,deepcopy
a = [1000, 200, [1, 2]]
b = a # 赋值
b = copy(a) # 浅拷贝
b = deepcopy(a) # 深拷贝

提示
列表的操作符[:]
和list.copy
方法都属于浅拷贝。
迭代器、生成器、可迭代
装饰器
闭包
闭包是一种特殊的函数结构,它允许一个函数访问并操作其外部作用域中的变量。闭包的核心在于它能够“记住”并保留外部函数的局部变量,即使外部函数已经执行完毕,这些变量仍然可以被内部函数访问和使用。闭包通常由嵌套函数构成,即一个函数定义在另一个函数内部。
python
def line(a, b):
def inner(x):
return a*x+b
return inner
# 相当于直线y=ax+b,设置a和b后,改变x即可求出y
l1 = line(2, 4)
print(l1(3))
需要注意的是:返回的函数并没有立刻执行,而是直到调用了才执行。
python
def build():
fs = []
for i in range(1, 4):
def f():
return i*i
fs.append(f)
return fs
f1, f2, f3 = build()
print(f1(), f2(), f3()) # 猜猜输出是什么?
查看答案
并不是"1, 4, 9",而是"9, 9, 9"。调用的时候i的值已经是3了。
常规装饰器
装饰器的作用是给函数添加额外的功能而无需改动原函数。
python
def decorator(func):
print("此部分在装饰的时候就会运行")
def inner(*args, **kwargs):
print('---函数执行前---')
res = func(*args, **kwargs)
print("---函数执行后---")
return res
return inner
@decorator # 装饰器装饰之后:func1 = decorator(func1)
def func1():
return 'func1'
@decorator
def func2():
return 'func2'
print(func1())
print(func2())
输出如下:
此部分在装饰的时候就会运行
此部分在装饰的时候就会运行
---函数执行前---
---函数执行后---
func1
---函数执行前---
---函数执行后---
func2
函数func1
经过@decorator
装饰之后就变成了inner
函数,所以func1()
实际执行的是inner()
,原先的func1
函数内容作为参数func
传递给函数内部,根据python闭包的特性,此参数将会被保留下来,即使decorator函数运行完,此参数仍然可以被内部函数访问和使用。
带参装饰器
python
def wrap(param):
def decorator(func):
def inner(*args, **kwargs):
print('inner: param=%s' % param)
res = func(*args, **kwargs)
return res
return inner
return decorator
# f1 = wrap('abc')(f1)
# 先运行wrap('abc')返回decorator函数,@decorator(f1)就相当于之前的常规装饰器了
@wrap('abc')
def f1():
return 'f1'
python
from functools import partial
def decorator(func=None, *, name='aaa', age=18):
if func is None:
return partial(decorator, name=name, age=age)
def inner(*args, **kwargs):
print(f"inner: {name=}, {age=}")
res = func(*args, **kwargs)
return res
return inner
# f1 = decorator(f1) 相当于常规装饰器
@decorator
def f1():
print("f1")
# f2 = decorator(name='bbb', age=20)(f2)
# 先执行decorator(name='bbb', age=20),参数func为None,所以返回decorator函数
# 最后f2=decorator(f2) 相当于常规装饰器
@decorator(name='bbb', age=20)
def f2():
print("f2")
装饰类的方法
装饰类的方法和装饰一般函数没有太大区别,唯一要注意装饰器和类方法装饰器的顺序不能弄混。
python
def stat_time(func):
def inner(*args, **kwargs):
ins = args[0]
print(ins.name)
start = time.time()
res = func(*args, **kwargs)
end = time.time()
print("cost time: ", end - start)
return res
return inner
class Test:
name = "xxx"
@stat_time
def instance_method(self, n):
while n > 0:
n -= 1
time.sleep(0.1)
@classmethod
@stat_time
def class_method(cls, n):
while n > 0:
n -= 1
time.sleep(0.1)
@staticmethod
@stat_time
def static_method(n):
while n > 0:
n -= 1
time.sleep(0.1)
提示
第三方库wrapt在处理带参装饰器和装饰类的方法上更加简单,有兴趣可以阅读其文档了解。
关于函数签名问题
python
from functools import wraps
def decorator(func):
@wraps(func)
def inner(*args, **kwargs):
res = func(*args, **kwargs)
return res
return inner
@decorator
def func1():
"""函数文档"""
return 'func1'
# 不使用functools.wraps装饰时: inner None
# 使用functools.wraps装饰时:func1 函数文档
print(func1.__name__, func1.__doc__)
从上面的代码可以得出,装饰器会改变函数签名,需要使用functools.wraps
装饰器来修正函数签名。事实上,functools.wraps
也没完全修正函数签名,函数的参数签名仍然与原函数不一致,但对于大部分场景已经够用了。如果你想完全保持一致,可以使用第三方库👉decorator。
python
from decorator import decorator
from inspect import getfullargspec
from functools import wraps
def use_wraps(func):
@wraps(func)
def inner(*args, **kwargs):
print("calling %s with args %s, %s" % (func.__name__, args, kwargs))
return func(*args, **kwargs)
return inner
@decorator
def use_decorator(func, *args, **kwargs):
print("calling %s with args %s, %s" % (func.__name__, args, kwargs))
return func(*args, **kwargs)
def func1(a, *, b=None):
pass
@use_wraps
def func2(a, *, b=None):
pass
@use_decorator
def func3(a, *, b=None):
pass
print(getfullargspec(func1))
print(getfullargspec(func2))
print(getfullargspec(func3))
func2()
# 输出结果如下:
# FullArgSpec(args=['a'], varargs=None, varkw=None, defaults=None, kwonlyargs=['b'], kwonlydefaults={'b': None}, annotations={})
# FullArgSpec(args=[], varargs='args', varkw='kwargs', defaults=None, kwonlyargs=[], kwonlydefaults=None, annotations={})
# FullArgSpec(args=['a'], varargs=None, varkw=None, defaults=None, kwonlyargs=['b'], kwonlydefaults={'b': None}, annotations={})
# calling func2 with args (1,), {}
从结果可以看出,decorator
的参数签名与原函数一致。
装饰器结合函数签名的相关方法,可以验证函数的参数类型,给函数增加参数等,这里就不扩展了。一般开发搞不到这个层度。
类装饰器
python
class WrapClass:
def __init__(self, class_name):
self.cls = class_name
def __call__(self, *args, **kwargs):
print("你已经被装饰了")
return self.cls(*args, **kwargs)
# Person = WrapClass(Person) 装饰之后Person变为WrapClass类的实例对象wrap_ins,
# 当其被实例化时,实际是wrap_ins(name, age),会调用__call__方法,返回是Person类的实例
@WrapClass
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
# 实例化的时候进行装饰
p1 = Person("wgj", 27)
python
class WrapClass:
def __init__(self, level=1):
self.level = level
def __call__(self, cls):
@wraps(cls)
def wrap(*args, **kwargs):
if self.level > 1:
print(f"当前等级:{self.level}")
return cls(*args, **kwargs)
return wrap
# Person = WrapClass(2)(Person)
# 本质和上面的无参形式没啥区别,只是实例对象有初始化参数。
@WrapClass(2)
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
__call__
方法可以让类的实例像函数一样被调用。
下面看一个类装饰器实现的给函数扩充功能的例子:
python
import types
from functools import partial, update_wrapper
import time
class DelayFunc:
def __init__(self, func, duration=1):
self.duration = duration
self.__func = func
update_wrapper(self, func) # == wraps(func)(self)
def __call__(self, *args, **kwargs):
print(f'Wait for {self.duration} seconds... {args}')
time.sleep(self.duration)
return self.__func(*args, **kwargs)
# 让此装饰器可以装饰类的函数
def __get__(self, instance, owner):
if instance is None:
return self
# 返回一个函数,此函数的第一参数是实例instance(相当于实例方法)。
# 由于self成为可调用对象是通过__call__实现的,
# 所以当调用__call__时,其args的第一个参数就是这个实例instance
method = types.MethodType(self, instance)
return method
def eager_call(self, *args, **kwargs):
return self.__func(*args, **kwargs)
def delay(duration):
"""
装饰器:推迟某个函数的执行。同时提供 .eager_call 方法立即执行。
为了避免定义额外函数,直接使用 functools.partial 帮助构造 DelayFunc 实例
:param duration:
:return:
"""
return partial(DelayFunc, duration=duration)
# delay(duration=2) -> @DelayFunc(duration=2) -> add = DelayFunc(add, duration=2)
@delay(2)
def add(a, b):
print(a + b)
class Spam:
# add = DelayFunc(add, duration=2) add是DelayFunc的实例对象df了
# Spam的实例调用add方法时,由于add是非数据描述器DelayFunc的实例,
# 根据描述器协议:描述器的实例被另一个类访问时,__get__会被调用。
# df() 将调用__call__,其args=(self, a, b),就是下面的三个参数
@delay(2)
def add(self, a, b):
print(a + b)
# add已经是DelayFunc的实例,但它也有函数的部分属性(归功于update_wrapper)
print(add, add.__name__)
add(1, 2) # 这次调用将会延迟 2 秒
add.eager_call(1, 2) # 这次调用将会立即执行,用类装饰为函数添加了新的功能
s = Spam()
s.add(1, 2)
Python项目部署
使用embeddable版本
Python的embeddable版本是一种轻量级的Python环境,它被设计为可以嵌入到其他应用程序中。这种版本的Python环境通常用于分发Python应用程序,因为它不包含文档、测试套件、工具(如IDLE)和Tkinter等。
此部署方式适用于文件较多的项目,比如Django服务。
- 下载对应 embeddable python 版本,下面以3.10版本为例,下载进入到解压后的文件夹内。
- 安装 pip 。访问网站,将里面的内容全部复制粘贴到一个py文件中,如 get-pip.py 。使用当前环境目录下的 python 解释器运行 get-pip.py 文件:如果系统安装了 python 并设置了环境变量,注意不要丢掉前面的.\,否则可能会安装到系统的 python 环境中。
powershell
.\python.exe get-pip.py
检查下是否安装成功:
powershell
.\python.exe -m pip -V
安装成功后可看到一个 Scripts 文件夹,里面就是 pip 的可执行文件。
- 编辑
python310._pth
文件(如果下载的是3.12版本,那就是python312._pth
文件)
取消下面这行代码的注释
import site
- 使用 pip 安装自己工程的三方库。比如安装 requests:
powershell
.\python.exe -m pip install requests
# 或者进入到Scripts文件夹内
.\pip.exe install requests
会看到一个 Lib\site-packages 文件夹,里面就是三方库文件。
- 至此,这个文件夹就是项目的 python 运行环境了。你可以将此文件夹重命令为 venv 等,然后放到你的工程目录内,搞一个bat脚本,这样别人就能直接双击 bat 脚本启动你的工程了。
下面是一个运行 Django 的 bat 示例:( venv、 manage.py、 bat 脚本在同一目录下)
bat
@echo off
%~dp0venv/python.exe manage.py runserver 0.0.0.0:8000
%~dp0
表示当前执行 bat 脚本的路径
注意
- 在使用 DevOps 工具等进行流水线构建时,注意不要忽略了运行环境内的 python310.zip,比如使用7z打包忽略掉了所有的zip文件。
- 一旦你移动或重命名这个环境目录,那么直接执行 Scripts 里面的 exe 就会失败,因为通常这些 exe 调用的 python 解释器采用的是固定路径,移动或重命名就找不到固定路径下 python.exe 文件了。可以使用模块的方式来运行:
.\python.exe -m pip -V
- 如果需要保护源码,可以将工程内的所有 py 文件编译成 pyc 或 pyd 文件。
生成可执行文件
对于单个脚本文件的情况,生成可执行文件则更加简单。目前将py文件生成可执行文件的主流工具是nuitka和pyinstaller。
nuitka
原理是根据py文件生成C代码,再将C代码编译为exe文件,所以使用此工具需要安装C编译器。
shell
pip install nuitka # 安装nuitka
python -m nuitka --onefile script.py
--standalone
和--onefile
的区别:
standalone
将程序及其所有依赖项打包成一个独立的文件夹,生成的文件夹中包含可执行文件和所有必要的依赖文件。onefile
将程序及其所有依赖项打包成一个单独的可执行文件。运行时,该文件会自动解压到临时目录中,然后执行,通常执行完之后会自动删除此临时目录。 测试文件func.py
:
python
import os
print(__file__)
print(os.getcwd())
下面是使用standalone
生成的结果(func.dist文件夹下): onefile
只是把standalone
生成的文件夹打包成一个exe文件了,运行时会解压到临时目录运行。所以在使用onefile
时,需要注意__file__
是临时目录下的func.py路径。
注意
确保打包的文件路径不存在中文,虽然Nuitka会处理中文路径,但也可能会在编译时异常。
nuitak还能很方便的生成pyd文件:
python -m nuitka --module some_module.py
在windows平台下,将会生成类似some_module.cp310-win_amd64.pyd
的pyd文件,其它py程序直接导入模块名some_module
即可。注意:
- 编译生成的模块名不能修改。
- 扩展模块不能包含其它扩展模块。
- 生成的扩展模块只能用于相同版本的CPython。
pyinstaller
原理是将py文件及其依赖项打包到一个独立的可执行文件或文件夹中,并嵌入Python解释器,在运行时动态解压和加载资源,用解释器运行脚本文件。
shell
pip install pyinstaller # 安装pyinstaller
pyinstaller -F script.py
常用参数 | 描述 |
---|---|
-F | --onefile,类似nutika的onefile |
-D | --onedir,类似nutika的standalone |
-w | 使用窗口,无控制台 |
-c | 使用控制台,无窗口(默认) |
总结
- 打包项目最简单的方式是使用embeddable版本。
- 打包单个脚本文件使用nuitka或pyinstaller更方便。
- 打包后的exe执行时
__file__
会有所不同,应该使用os.path.getcwd()等方法。
编程技巧
分解序列
有多个元素的序列或元组,如何将它里面的值同时赋给多个变量?
python
point = (4, 5)
x, y = p
print(x, y) # 4 5
data = ['Python', 50, 91.1, (2012, 12, 21)]
# 用不到的变量可以使用_占位
name, _, _, date = data
print(date) # (2012, 12, 21)
name, shares, price, (year, mon, day) = data
# 还可使用 *+变量名 保存部分序列
name, *tmp, date = data
print(tmp) # [50, 91.1]
提示
使用*
形式解压出的变量永远都是列表类型,即使原对象是元组或文本等其它可迭代对象。