Python学习记录

参考学习链接: Python 从入门到深入

一、Pycharm 基础

1. venv(放弃)与 uv 搭建虚拟环境

PyCharm 会自动接管 Python Interpreter(解释器)(解释器 = Python + 对应环境)。也就是在用PyCharm创建项目时,如果勾选了New Virtualenv。PyCharm 会自动帮你执行"python -m venv venv"来设置好解释器路径并激活环境。这就是为什么如果在 linux 中启动python环境你还需要手动执行 "python -m venv venvsource venv/bin/activate"两行的原因。

注意在 PyCharm 中 :已创建的 venv 一旦生成,其 Python 版本和里面的 pip/包就"冻结"了,你在「设置里更换解释器版本」= 切换到 另一个解释器 / 另一个 venv ,PyCharm 不会"帮你升级 / 重建"原有 venv。使用如下命令确定pip包的安装路径及版本

复制代码
where python
where pip
python --version

新手容易混淆的点:Python 包不是"装到工程目录里",而是装到工程正在使用的 Python 解释器(环境)里。

  • uv :创建工程需要指定 uv.exe 路径,参考本文下面 【Ⅳ "uv" 安装使用】 一小节。

2. 远程调试工程

本想用Win11中的Pycharm编辑位于Linux中的python工程,后来发现需要安装Pycharm专业版,最终放弃了。后来想用一个目录同步到远端的服务器配合Pycharm是西安远程编辑,发现有 Syncthing 这个工具,每个设备端安装上这个工具可以做到多段同步,但我看配置挺复杂的,还可能存在防火墙设置,公司电脑没法操作,时间成本太高先放弃。

4. PyPI 包

由 Python 软件基金会(PSF)维护,非营利、免费、开源的中央仓库。pip install 就是从这里下载包。打开浏览器访问:https://pypi.org 可以看包的描述信息,看包需要的python版本,怎么安装等。

5. Package 与 Module

Package() :包含python文件(Module)的文件夹。当我们import package 时,并不会执行Package里面的module,而是执行Package里面的 __init__.py(一般在这个文件中执行Package内module的初始化),

Module(模块) : 是一个Python 文件,以. py 结尾,包含了Python 对象定义和Python语句。

6. 列表list,字典dict,元组tuple,字符串

  • 列表 - list

    • 是Python中最常用动态数据结构,本质是动态数组 ,它是有序可变的序列,可存储任意类型元素(整数,字符串,对象等),支持动态扩容,插入,删除等操作。整个元素集使用**中括号 [] **包围。他有如下内置方法:

    • 添加元素

      1. list0.append(x):在列表末尾添加一个元素`x``
      2. ``list0.extend(iterable):将可迭代对象(如另一个列表,元组,字符串)逐个添加到列表末尾,与append`的区别就是能添加多个元素了
      3. list0.insert(i, x):在指定索引 i 处插入元素 x。原位置及之后的元素向后移动。
    • 删除元素

      1. list0.remove(x):删除列表中第一个值为x的元素,如果找不到该值,会抛出ValueError
      2. list0.pop([i]):删除并返回指定索引[i]处的元素,如果不指定i,删除并返回最后一个元素
      3. list0.clear(): 移除列表中的所有元素,相当于 del list[:]
    • 查找与统计

      1. list0.index(x[, start[, end]]): 返回列表中第一个值为 x 的元素的索引。如果找不到,抛出 ValueError
      2. list0.count(x): 返回元素 x 在列表中出现的次数。
    • 排序与反转

      1. list0.sort(key=None, reverse=False): 对列表进行原地 排序(直接修改原列表)
        • reverse=True 表示降序。
        • key 可以指定一个函数用于提取比较键(例如按字符串长度排序)。
      2. list0.reverse(): 将列表中的元素原地反转
    • 复制

      1. list0.copy(): 返回列表的一个浅拷贝(浅拷贝只拷贝有多层可变对象的第一层)。

        注:copy的意义是希望通过拷贝来防止冲突,只能用于可变对象,用于不可变对象如元组没有任何意义。深拷贝则需要使用python内置方法 import copy

  • 字典 - dict 参考博客 : python创建字典(dict)的几种方法(详细版)

    • .keys(), .values(), .items()

      作用:分别获取字典所有的键、值、或键值对视图。

      场景:动态遍历或调试

    • get(key, 默认值)

      作用:获取指定key对应的value,如果没有或者为空,就将默认值作为返回。

      场景:当默认值可能不存在的时候用。因为直接访问不存在或键为空的默认值运行时会直接报错。

    • .pop(key, default):

      作用:移除指定的键并返回其值。如果键不存在,返回默认值(若不填默认值且键不存在,则报错)。

      场景:消耗性数据读取。

    • in运算符,不是方法但最常用

      作用:判断键是否存在于字典中。

      场景:条件路由。

    • len(dict),获取键值对数量

  • 元组 -tuple :

    1. tuple 也是一个class,是不可变list类型。不可用增删改。

    2. 定义tuple与定义一个list的方式相同,除了整个元素集是用**小括号 () **包围的而不是方括号,元素也用中括号索引得到如 t[0]

    3. tuple没有list中的 append、extend、remove、pop方法。不能增加删减新的元素,但可以用加号 + 让两个tuple合成一个。

    4. 可以用in来查看元素是否存在于tuple中。

    5. tuplelist的好处:比 list 快,因为元素不能改,所以安全。

    6. 内置的 listtuple 函数可以实现tuplelist之间的互相转化

    7. 在python中,用逗号分隔的多个值进行return会自动打包成一个 tuple,比如 return num, best_score等价于 return (num, best_score),外层的括号是可选的,除了空元组()避免歧义。同样,可以用元组解包的方式处理返回,比如 answer, score = result

  • 字符串

    这个类型搭建很熟悉,在这主要介绍字符串格式化的三种方式,比如已知:

    python 复制代码
    name_nxp = "张三"
    age_num = 25
    1. % 格式化:(老式,不推荐),但看见要认识result = "我叫 %s,今年 %d 岁" % (name_nxp, age_num)
    2. format()result = "我叫 {name},今年 {age} 岁".format(name=name_nxp, age=age_num)
    3. f-string(Python 3.6+):result = f"我叫 {name_nxp},今年 {age_num} 岁"

6. 集合 set

如果一个对象实现了__hash__方法和__eq__方法,那么这个对象就是可散列的(Hashable),参考。

7. try except , raise

python 复制代码
try:
    x = int(input("输入一个数值:"))
    result = 10 / x
    raise ZeroDivisionError("错误信息")  # 主动抛出异常,被指定的错误类型捕获,并打印错误信息
except ValueError as e: # except 后指定具体的异常名称,表示捕获指定类型的异常。
    print(e)
except ZeroDivisionError as e:  # 更具体的错误类型放后面, as 关键字获取异常实例,便于查看错误信息:
    print(e)
except (ValueError, TypeError):  # 一次捕获多个异常
    print("发生了值错误或类型错误")
>>>>>>
输入一个数值:lyri
invalid literal for int() with base 10: 'lyri'

try...except 语句用于捕获和处理异常。你可以在 except 后指定要捕获的异常类型 ,既可以使用 Python 内置的异常类型,也可以使用自定义的异常类型

  • 内置异常类 :Python 提供了丰富的内置异常类,它们都继承自 BaseException,大多数日常使用的异常继承自 Exception。常见内置异常包括:

    • ValueError:值错误(如 int('abc')
    • TypeError:类型错误(如 'a' + 1
    • IndexError:索引越界(如 list[10] 超出范围)
    • KeyError:字典键不存在
    • FileNotFoundError:文件未找到
    • ZeroDivisionError:除零错误
    • AttributeError:属性不存在
    • ImportError / ModuleNotFoundError:导入失败。

    ⚠️注意:不要用 except:(裸 except),它会捕获包括 KeyboardInterruptSystemExit 在内的所有异常,可能导致程序无法正常退出。

  • 自定义异常类型 :通常继承自 Exception 或其子类。自定义异常类名通常以 Error 结尾:

    python 复制代码
    class MyclassError(Exception):
        def __init__(self, message="这是一个自定义错误"): # 带默认值的参数
            self.message = message
            super().__init__(self.message) # super()返回当前类的父类(在这里就是 Exception)
    
    try:
        print('debug in try')
        raise MyclassError("用户名无效!")  # 用户主动调用 raise 抛出异常,中断程序,被except捕获
    except MyclassError as e:   # except 后指定具体的异常类,as后是异常实例(对象)
        print(e)
        print('debug in error')
    
    >>>>>>
    debug in try
    用户名无效!
    debug in error
  • raise 主动抛出异常

    raise 基本用法:raise <内置异常类或自定义异常类>("错误消息")

    raise发生后当前代码处立即停止,直到找到后续的except,当前函数没有except就继续向上传导。从except匹配错误处开始执行。如果没找到except则抛出错误,代码停止。

    python 复制代码
    # 方式1:创建异常实例并抛出异常信息
    raise Exception("错误信息")
    
    # 方式2:先创建实例,再抛出异常信息
    error = Exception("错误信息")
    raise error

    实际上,使用raise时调用了对应类的构造函数(__init__ 方法)并将实例抛出。

8. with...as...

with 是 Python 的基础语法 ,称为 上下文管理器(Context Manager)

作用:自动管理资源的获取与释放,确保即使发生异常,资源也能被正确清理。

常见用途:文件操作(自动关闭文件),网络链接(自动断开),锁(自动释放),子进程(自动终止,清理)

基本语法

复制代码
with 表达式 as 变量
	# 使用变量做事
# 退出with块自动调用清理逻辑

例子

python 复制代码
# ⚠️ 不推荐下面这样
f = open("file.txt")
data = f.read()
f.close() # 如果在 f.read() 出错,这行代码则没机会执行

# ✅推荐这样
with open("file.txt") as f:
    data = f.read()
# 代码运行到这,文件自动关闭,哪怕read出错了

何时用 with ?:

只要对象实现了 上下文管理协议 (即有 __enter____exit__ 方法),就可以用 with。(subprocess.Popen 从 Python 3.2+ 开始支持上下文管理器。所以可以用with)

9. if ... else ...

基本语法:[值1] if [条件] else [值2]

复制代码
age = 17
message = "成年人" if age >= 18 else "未成年人"
print(message)  # 未成年人

10. import 相对/绝对导入

导入的既可以是模块,也可以是类、对象。

  • 绝对导入:从顶层包名开始导入

    python 复制代码
    from app.all_tools.compiler_tool import CompilerTool # app需要为顶层包
    • 条件:在可以被导入的包目录下加上空的__init__.py文件,如下所示:

      shell 复制代码
      app/
      ├── __init__.py      # 建议加
      ├── core/
      │   ├── __init__.py  # 建议加
      │   ├── config.py
      │   └── security.py
      └── main.py  # 在这可用:from app.core.config import xxx
  • 相对导入:仅在包内模块互相导入时用

    python 复制代码
    from .schemas import ToolInput # 从当前目录的schemas中导入
    form ..ddd import eee # 从上级目录的ddd中导入

    其中一个点表示当前包层级,两个点表示向上一个包层级。

!IMPORTANT

当你使用 python -m app.main 运行工程时,app 会被识别为顶层包(app所在目录为项目根目录),Python 会将**当前工作目录(通常是项目根目录)**添加到 sys.path

模块 app.main 以包模块方式执行。在这种结构下:

  • 绝对导入 (如 from app.all_tools import x)工作正常
  • 相对导入 (如 from .schemas import ToolInput)也能正常解析
为何 Pycharm 中好好的放到Linux中找不到包?

核心概念:命名空间软件包。

python的一个特点就是,会搜索命名空间软件包地址里面的每个文件来导入。

  • 为什么在PyCharm环境执行不会出错? :因为PyCharm会自动把项目根地址添加到sys.path中。可以在PyCharm环境下打印一下sys.path的值就能看出来了(我也没看出太特别的,但是的确比Linux中要多一些)。

    复制代码
    >>> import sys
    >>> print(sys.path)
    ['', 'C:\\Users\\nxg01742\\AppData\\Roaming\\uv\\python\\cpython-3.11.14-windows-x86_64-none\\python311.zip', 'C:\\Users\\nxg01742\\AppData\\Roaming\\uv\\python\\cpython-3.11.14-windows-x86_64-none\\DLLs', 'C:\\Users\\nxg01742\\AppData\\Roaming\\uv\\python\\cpython-3.11.14-windows-x86_64-none\\Lib', 'C:\\Users\\nxg01742\\AppData\\Roaming\\uv\\python\\cpython-3.11.14-windows-x86_64-none', 'C:\\Users\\nxg01742\\OneDrive - NXP\\Documents\\Doc_Lyrix\\8. Project summary\\08_AI_agent\\FastAPI_Server\\.venv', 'C:\\Users\\nxg01742\\OneDrive - NXP\\Documents\\Doc_Lyrix\\8. Project summary\\08_AI_agent\\FastAPI_Server\\.venv\\Lib\\site-packages']
  • 为什么在Linux环境执行会出错?

    1. python app/main.py执行方式为何报错?

      正确的方式为 python -m app.main。这样解释器会把项目根目录放进sys.path,并以包解析规则加载app.main;使绝对导入和相对导入都更稳定。而python app/main.py会让解释器把app当作一个普通目录而非包名。这也是为什么在 IDE(如 PyCharm)里跑不报错:IDE 会帮你把"项目根"加到 sys.path,等效于 -m 的效果;而你把代码放到 Linux/CMD/Docker 后直接 python app/main.py 就容易炸。

二、Python 版本管理,虚拟环境

在Win11的Pycharm中新建工程时,只需要鼠标点几个按钮,他就可以给你自动开启了.venv,但是在 Ubuntu 中就需要知道怎么回事,怎么配置了。虚拟环境网上可以搜索到一堆工具,你也不知道哪个最好用, 虚拟环境的目的是为了给工程使用特定的python包,自然涉及到了python包多版本管理。

1. 虚拟环境为什么安全

假设你只有一个系统 Python 位于 /usr/bin/python3 ,你用 pip 安装 pip install django,问题来了,项目A需要的 django与项目B需要的版本不一样,但是系统里 只能有一套 site-packages

系统Python中包含如下内容

复制代码
大概包含如下内容:
python3.12
├── Lib
│   ├── set-package
│   └── python标准库
├── Scripts 
│    └── pip.exe
└── python.exe

虚拟环境就是上面环境的副本(把上面的系统python的拷贝),但是跟上面的还是有所区别,比如但标准库不能都复制一份啊也太占地方了,如下为虚拟环境的内容:

复制代码
python3.12
├── Lib
│   └── set-package
└── Scripts 
     ├── python.exe
     └── pip.exe

可见python.exe与pip.exe被放到了一个目录,这样可以减少设置一个环境变量啊,然后标准库没了,直接引用原来的标准库。注意:虚拟环境一旦创建,虚拟环境中的python解释器就不能简单更换了,想更换重新创建虚拟环境

2. 各种虚拟环境即版本管理工具

Python 虚拟环境介绍,参考:博客文章

  • venv:简单 。注意:venv 的工作前提是与之对应版本的python解释器已经存在,否则导致虚拟环境不能启动。参考博客:Ubuntu安装python虚拟环境
  • virtualenv:太老了,python2用这个,venv可以视为他的分支。
  • anaconda / conda:臃肿,5-6个G,深度学习 / 科学计算领域,个人使用可以(超过200人的公司内没买不让用)。
  • poetry : 在uv出现前最好用的管理工具,但已经是过去式。
  • uv:Python安装及版本管理 + 虚拟环境 + 依赖管理 + 工具安装 + 打包发布 全能选手
Ⅰ. venv 虚拟环境(Linux)(抛弃)

因为在Win11中如果下载了 python-xxx-amd64.exe 压缩包后在Pycharm使用,根本体会不到venv的存在。所以仅讨论Linux下.venv使用。

根据python版本安装对应的 venv,注意:venv 的工作前提是与之对应版本的python解释器已经存在,否则导致虚拟环境不能启动。参考博客:Ubuntu安装python虚拟环境

复制代码
sudo apt update
sudo apt install python3.11 python3.11-venv  # 因为我用的python是 python3.12

项目里创建虚拟环境:

复制代码
cd your_project
python3.11 -m venv .venv

激活:

复制代码
source .venv/bin/activate

升级 pip(强烈建议):

复制代码
pip install --upgrade pip

安装依赖:

复制代码
pip install requests rich pydantic

退出:

复制代码
deactivate
  1. venv 安装,创建,激活
Ⅱ python manager(Win11)(抛弃)

传统的通过 python-xxx-amd64.exe 方式的安装方法在 python3.11 后不在推荐了 Python Launcher 也是过时了,而是采用 python manager 的方式安装和管理Win11中的python版本。

python官网 --> download --> download python manager 可以下载安装,下载后为 *.msix 的文件。使用 python manager 前需要将之前用 python-xxx-amd64.exe 方式安装的 python 卸载掉(用安装时用的那个这个 python-xxx-amd64.exe 包就能卸载)。

  • 如何使用 python manager , 秩序打开 CMD 终端输入

    复制代码
    py list 			# 列出 当前安装的python版本
    py install 3.11.13 	 # 安装 特定版本 
    py install --update 3.11.13 	 # 从老版本升级到 特定版本
    py uninstall 3.11.13 # 卸载 特定版本
    py -V:3.11,13  	     # 启动 特定版本(-V可省略:py -3.11.13)
    py -V	# 查看默认使用哪个版本

因为在使用 python manager 安装python版本时遇到了可能因为 python manager 没及时跟进官网仓库导致安装到的python11版本与linux中用uv的python版本不同。所有决定放弃使用,同样改用uv。

Ⅲ pip进行包管理(被uv取代)

pip四条规则:

  • pip 永远优先保证"当前命令"成功
  • 已有的不兼容依赖除非版本满足约束,否则 pip 会替换它
  • pip 不帮你做"整体一致性维护"
  • pip 不拒绝"制造冲突",它只会在事后告诉你冲突存在

pip导出安装依赖清单:

复制代码
python -m pip freeze > requirements.txt

python -m pip install -r requirements.txt

pip 相较于uv的缺陷

  • 包含无关包 ,如果你在虚拟环境中装过 pytest 调试,它也会被写进去
  • 无法区分主依赖/开发依赖,所有包混在一起
  • 冲突风险高,子依赖版本锁死,升级主包时易冲突

🚫 pip freeze 适合"冻结生产环境",但不适合"管理开发依赖"

Ⅳ "uv" 安装使用

!WARNING

win11中一定要在cmd窗口使用uv,千万不要在Pycharm的cmdline使用uv,会导致莫名其妙的问题,我为此浪费了人生宝贵的4个小时。

uv 是个开源工具,用 Rust 写的,超高速的 Python安装及版本管理 + 虚拟环境 + 依赖管理 + 工具安装 + 打包发布 全能选手 。具有 pip, pip-tools, pipx, poetry, pyenv, twine, virtualenv 等的功能。可以从 UV官网 查阅UV下载安装的步骤。下面的整理来自:【让uv管理Python的一切】 https://www.bilibili.com/video/BV1Stwfe1E7s/?share_source=copy_web\&vd_source=ee75d9cc6f800ff5a59087a5eec51c9c

uv 安装(参考官网):
  • Linux 安装

    shell 复制代码
    curl -LsSf https://astral.sh/uv/install.sh | sh
    source $HOME/.local/bin/env 
    uv --version
  • Win11 安装

    参考链接:使用uv和pycharm搭建python开发环境,只需要下载解压到全英目录,添加环境变量即可。

uv python版本管理:
复制代码
uv python list  				# 列出远端仓库可供安装的 python 版本
uv python list --only-installed   # 仅列出已经被安装的 python

uv python install cpython-3.11.14 # 安装官方的3.12.2版本 python (根据列出的安装)
uv python install cpython-3.12    # 安装3.12最高子版本 python
uv python install        		 # 安装当前最高版本 python

uv python uninstall 3.12 # 卸载3.12版本 python
uv python upgrade   3.12 # 升级3.12版本 python
uv 运行 py 脚本
复制代码
uv run -p 3.12 python  # 进入指定版本python的交互界面 #这个版本如果没装过,会自动安装
uv run -p 3.12 run.py  # 用指定版本临时运行指定文件
uv 创建工程(uv init),虚拟环境
shell 复制代码
uv init -p 3.11 test # 创建一个名为test的python工程

这样会创建一个目录有如下文件:

shell 复制代码
$ ls -al
total 28
drwxrwxr-x 3 user user 4096 Jan 24 17:34 .
drwxrwxr-x 6 user user 4096 Jan 24 17:33 ..
drwxrwxr-x 7 user user 4096 Jan 24 17:34 .git
-rw-rw-r-- 1 user user  109 Jan 24 17:34 .gitignore 
-rw-rw-r-- 1 user user   90 Jan 24 17:34 main.py
-rw-rw-r-- 1 user user  158 Jan 24 17:34 pyproject.toml  # 记录工程依赖(为兼容旧系统,新的用uv.lock了)
-rw-rw-r-- 1 user user    5 Jan 24 17:34 .python-version # 存储使用解释器的版本,修改这个直接可以修改使用的解释器
-rw-rw-r-- 1 user user    0 Jan 24 17:34 README.md

可以显式添加虚拟环境,但是如果直接uv add添加依赖,这一步可以省略:

python 复制代码
uv venv --python cpython-3.11.14  # uv venv也行

uv sync → 能根据 uv.lockpyproject.toml 创建一个新的干净虚拟环境。

!NOTE

如果在linux环境直接开发,则手动激活虚拟环境:

shell 复制代码
# 进入(激活)虚拟环境
source .venv/bin/activate
# 退出(停用)虚拟环境
deactivate

如果Pycharm远程开发,则无需手动激活了,在Pycharm终端打开自动激活环境,但是我在Pycharm终端无法使用uv命令,尚未找到解决办法。

在Win11中也可手动激活虚拟环境:.venv\Scripts\activate

uv add,remove,tree 依赖

!TIP

uv安装的目标包信息,可以在 https://pypi.org/ 官网查看包支持的python解释器版本。

前面已经通过 uv init xxx 创建了工程目录,把你编辑好的python脚本放进来,此时如果你用 Pycharm 或者 VScode 打开你自己的工程,会发现缺少库。直接安装库:

shell 复制代码
uv add langchain-core # 安装 langchain-core 库,pyproject.toml 中会自动记录依赖
shell 复制代码
安装结束后,执行  ls -al 你会发现多出来如下两个文件
-rw-rw-r-- 1 user user 130731 Jan 24 17:39 uv.lock # uv 生成的完整依赖锁定文件,含版本+哈希,比pyproject.toml快
drwxrwxr-x 4 user user   4096 Jan 24 17:39 .venv  # 自动创建了虚拟环境,牛啊
shell 复制代码
uv tree # 打印整个依赖树 

如果我想使用ruff来检查是否符合规范,可以把想用的工具当作依赖引入:

shell 复制代码
uv add ruff --dev # 此处的 ruff虽为依赖,但 --dev 为在打包时不加ruff了就
uv run ruff check 就可以检查python格式是否符合规范了,(加载虚拟环境后,ruff check 即可)

删除依赖:

shell 复制代码
uv remove ruff --dev 
uv tool install

有人说 ruff 这种应该脱离项目独立运行,即安装到系统之中,执行如下命令:

shell 复制代码
uv tool install ruff # 通过 witch ruff 可以看到安装位置为系统可用的
shell 复制代码
uv tool list # 查看已经安装的工具
uv 打包脚本
  1. 编辑 pyproject.toml 文件新加入

    复制代码
    [project.scripts]
    ai = "ai:main"
    # 想要的脚本名 = "python脚本:函数名"
  2. 打包

    shell 复制代码
    uv build # 把整个工程打包成whl文件,然后这个whl文件就可以发布到python的软件仓库给全世界来使用了

三、mypy 和 Pyright 是什么?

mypyPyright 都是 Python 的静态类型检查工具(static type checkers),作用是在不运行代码的情况下分析Python代码是否符合我声明的类型注解,从而提前发现潜在类型错误。

为社么Python需要类型检查,因为Python是 动态类型语言 ,变量的类型在运行时才能确定,这样虽然灵活,但也容易因为类型错误(比如字符串当数值用)导致运行时崩溃。从 Python 3.5 开始,官方引入了 类型注解(Type Hints)PEP 484),允许开发者像这样写代码:

python 复制代码
def greet(name: str) -> str:
    return "Hello, " + name

age: int = 25

但是这些仅仅是 注释Python在运行时也是看不到 的,于是就需要一些 外部工具 如 mypy、Pyright 真正利用这些注释做检查。

mypy 是什么?

mypy 是最早的、最流行的 Python 静态类型检查器。

由 Python 类型系统的主要设计者之一 Jukka Lehtosalo 开发。

功能强大,支持泛型、协议(Protocol)、联合类型(Union)、类型别名等高级特性。

使用方式:

复制代码
pip install mypy
mypy your_script.py

✅ 优点:成熟、社区支持好、与标准库和主流库兼容性高。

⚠️ 缺点:速度相对较慢(尤其是大型项目)。

Pyright 是什么?

  • Pyright 是微软开发的 Python 静态类型检查器,用 TypeScript 编写,后编译为高性能的 Node.js 工具。
  • 设计目标:更快、更严格、更适合大型项目
  • 同时也是 VS Code 中 Pylance 语言服务器 的核心引擎(所以你在 VS Code 里看到的实时类型错误,很可能来自 Pyright)。
  • 支持 mypy 几乎所有特性,并额外提供:
    • 更严格的默认检查(如未初始化变量、不可达代码)
    • 更好的异步/协程类型推断
    • 配置更灵活(通过 pyrightconfig.json

使用方式:

复制代码
npm install -g pyright
# 或
pip install pyright  # 包装器,调用 npm 版本
pyright your_script.py

✅ 优点:极快 (比 mypy 快 10 倍以上)、集成体验好(尤其在 VS Code)。

⚠️ 缺点:某些边缘情况与 mypy 行为略有不同(但通常更严格)。


mypy 或 pyright 能帮你做什么?

复制代码
def add(a: int, b: int) -> int:
    return a + b

result = add("hello", "world")  # ❌ 传入了 str,但函数期望 int
  • 如果你只用 Python 运行 ,这段代码会报错(TypeError),但要等到运行时才发现。
  • 如果你用 mypy 或 Pyright 检查在写代码阶段就能立刻发现这个错误

四、Typing - 静态类型注解

先介绍Typing是什么:提供"类型注解"的词汇表(如 List[str], Optional[int]),而 mypy/Pyright 是"通过读这些词汇并检查你有没有用错"的老师。

说说Typing的由来,从Python3.5开始,引入了类型注解语法(比如 x : int = 5),但是光有基本类型(int, str, bool)的注解是不够的。现实中,代码实现过程用的类型往往复杂,这就可能造成无法使用"类型注解"来在写代码的时候检查错误。比如下面这个例子:

python 复制代码
def get_items() -> list:  # 只说"返回一个 list",list是Python的内置类型
    return ["apple", "banana"]

items = get_items()
first = items[0]
result = first.upper()  # 假设它是字符串,将字符串中的所有字母转换为大写形式

当某天 get_items() 被改为返回 [1, 2, 3],你的.upper() 就会在运行时崩溃!但如果你用 typing 明确声明:

python 复制代码
from typing import List

# 明确:返回字符串列表,注意此处的 List 不同于上面的 list, List 是位于Typing包中的类型
# 但是从 Python3.9 起,list[str] 也行了,但成了Python的内置类型,不用 from typing 了。
def get_items() -> List[str]:  
    return ["apple", "banana"]

那么:

  • 如果有人不小心改成 return [1, 2, 3]
  • mypy/Pyright 会立刻报错 :"函数声明返回 List[str],但实际返回了 List[int]!"(前提是你得安装这俩工具啊,Pycharm需要你在设置->工具中启用 Pyright)
Typing怎么用?
一、最常用类型(建议优先掌握)
  1. 容器泛型(现在可用内置写法,但旧代码常见 typing 版本)
含义 Python ≥ 3.9(推荐) Python ≤ 3.8(需 import)
字符串列表 list[str] List[str]
字典:str → int dict[str, int] Dict[str, int]
集合:整数 set[int] Set[int]
元组:(str, int) tuple[str, int] Tuple[str, int]
可变参数元组 tuple[int, ...] Tuple[int, ...]
  1. 可选类型:Optional ,可能为 None
python 复制代码
from typing import Optional

# 等价于 Union[str, None]
def find_user(user_id: int) -> Optional[str]:
    if user_id == 1:
        return "Alice"
    return None  # 允许返回 None

Python 3.10+ 也可写成:

复制代码
def find_user(user_id: int) -> str | None:
   ...
  1. 联合类型:多种类型之一:表示 "可以是 A 或 B 或 C"
python 复制代码
from typing import Union

def parse_value(value: str) -> Union[int, float, str]:
    try:
        return int(value)
    except ValueError:
        try:
            return float(value)
        except ValueError:
            return value

Python 3.10+ 更简洁:

复制代码
def parse_value(value: str) -> int | float | str:
  1. 任意类型(慎用!): 当你真的不知道类型,或故意绕过检查

    from typing import Any

    def debug_print(x: Any) -> None:
    print(f"Value: {x}, Type: {type(x)}")

二、函数与回调类型 - Callable
  1. Callable:表示一个可调用对象(函数、lambda 等)
python 复制代码
def addint(a:int, b:int) -> int:
    return a + b

# 接受一个 (int, int) -> int 的函数
def apply_operation(a: int, b: int, op: Callable[[int, int], int]) -> int:
    return op(a, b)

# 使用
result = apply_operation(5, 3, addint)

格式:Callable[[参数类型...], 返回类型]


三、自定义复合类型- Literal
  1. TypeAlias(Python 3.10+)或直接赋值(旧版)

给复杂类型起别名,提高可读性:

python 复制代码
# Python 3.10+
from typing import TypeAlias

UserID: TypeAlias = int
Name: TypeAlias = str
User: TypeAlias = tuple[UserID, Name]

# 旧版写法(也适用于 3.9-)
from typing import Tuple

User = Tuple[int, str]  # 注意:这里不是注解,是赋值

def get_user() -> User:
    return (123, "Alice")

  1. Literal:限制值必须是某些字面量
python 复制代码
from typing import Literal

Mode = Literal["read", "write", "append"]

def open_file(path: str, mode: Mode) -> None:
    ...

open_file("data.txt", "read")   # ✅ OK
open_file("data.txt", "delete") # ❌ mypy 报错!

四、高级类型(进阶)
  1. Protocol:结构化子类型(鸭子类型)

定义"只要具备某些方法/属性,就算符合类型":

python 复制代码
from typing import Protocol

class Drawable(Protocol):
    def draw(self) -> None: ...

def render(obj: Drawable) -> None:
    obj.draw()

# 无需继承!只要有 draw 方法就符合
class Circle:
    def draw(self) -> None:
        print("Drawing circle")

render(Circle())  # ✅ 合法

  1. Generic:泛型类
python 复制代码
from typing import TypeVar, Generic

T = TypeVar('T')

class Box(Generic[T]):
    def __init__(self, value: T) -> None:
        self.value = value

    def get(self) -> T:
        return self.value

box1 = Box[int](42)
box2 = Box[str]("hello")

五、其他实用类型
类型 用途
NoReturn 函数永不正常返回(如抛异常或 exit)
Final 表示变量/属性不应被重新赋值(常量)
Iterator[T] 迭代器类型
Iterable[T] 可迭代对象(比 list 更通用)
Mapping[K, V] 泛化的 dict-like 对象(如 dict, ChainMap
六、为类型提示添加元数据 - Annotated

用于为类型提示添加元数据,提供更多上下文信息:

复制代码
Annotated[type, metadata]
  • type 是原本的真实类型注解,静态类型检查用这个检查。
  • metadata 元数据,是你想要附加的额外信息,可以是任意 Python 对象,如字符串、整数、自定义类的实例等。一些框架(如langChain)在运行时可通过 typing.get_type_hints(..., include_extras=True)typing.get_origin/get_args 把元数据取出来,做"非类型层面"的自定义行为。常见用途
    • Web/数据校验框架:在类型上附加校验规则、范围、别名等(如 FastAPI/Pydantic)。
    • 编排/状态机框架:在字段上附加合并策略、持久化规则(如 LangGraph)。
    • 文档生成/依赖注入:携带描述、默认值来源等。

例如:

python 复制代码
from typing import Annotated
def process(value: Annotated[int, "positive"]):
    if value <= 0:
        raise ValueError("Value must be positive")
TypeDict 横空出世 - 常用

当你有一个字典,它有固定字段(比如 {"name": str, "age": int}),但是普通 dict[str, object] 太宽泛,无法检查字段名和字段类型。于是就有了 TypeDict ,用来精确描述字典"形状"(shape)。TypeDict使用Typing提供的静态类型注释。

静态类型检查器 (mypy/pyright 等)知道你的字典应有哪些键、每个键的类型、是否必填(在写代码阶段检查错误,运行时不校验)。TypedDict 确实进化源自 typing 模块(或 typing_extensions),TypedDict 允许你定义一个具有固定键和对应值类型的字典类型 。它在类型检查时生效 ,运行时仍然是普通 dict

**只做 静态类型检查 **(给"长得像 dict 的对象"标注键/值的类型与必选/可选键),运行时不做校验、也不做类型转换。

  • 注意与 Pydantic 区分,Pydantic 是运行时校验/转换框架

1. TypedDict基本用法

python 复制代码
from typing_extensions import TypedDict
from pprint import pprint
class Person(TypedDict):
    name: str
    age: int
    active: bool

# 使用, 使用的同时,类型检查器会检查是否包含所有必须字段,键对应的值类型是否匹配,不能有未声明的字段。
p1: Person = {
    'name': "lyrix",
    'age': 30,
    'active': True
}
pprint(p1)

2. TypedDict高级用法

默认所有字段都是必需的 。但你可以用 total=False 表示"所有字段可选":

python 复制代码
class Movie(TypedDict, total=False):
    title: str
    year: int
    rating: float

# 合法:只提供部分字段
m1: Movie = {"title": "Inception"}
m2: Movie = {"title": "The Matrix", "year": 1999}

3. 混合必填与可选字段(推荐方式)

python 复制代码
from typing import TypedDict

class BaseMovie(TypedDict):
    title: str  # 必填

class Movie(BaseMovie, total=False):  # total=False 就是标记所有的成员都可选了
    year: int   # 可选
    rating: float  # 可选

# 合法用法
m1: Movie = {"title": "Avatar"}                    # ✅ 只有必填
m2: Movie = {"title": "Titanic", "year": 1997}     # ✅ 必填+部分可选

但是上面这个是不是还有点麻烦呢?结果 Python 3.11+:更简洁的语法(使用 NotRequired / Required

python 复制代码
from typing import TypedDict, NotRequired

class Movie(TypedDict):
    title: str
    year: NotRequired[int]
    rating: NotRequired[float]

真特么会省事儿啊。但是务必注意以下情况:

!WARNING

  • 运行时仍是 dict

    python 复制代码
    type(p) # <class 'dict'>
    isinstance(p, dict)  # True
  • 字段访问必须用字符串键:

    复制代码
    p.name      # ❌ 错误!TypedDict 不支持点号访问
    p["name"]   # ✅ 正确
  • 不能动态添加未生名的字段(默认严格模式)

  • 需要 mypy ≥0.980 或 Pyright 才能完整支持新特性 (如 NotRequired)。

可变默认参数陷阱

在 Python 中,可变对象(如列表、字典)作为默认参数会导致所有函数调用共享同一个对象,这会产生意外的副作用。

复制代码
# 危险!不要这样做
def compile_board_image(board_name: str, flags: List[str] = []) -> str:

✅ 正确的解决方案

方案1:使用 None + 类型检查(推荐)

复制代码
def compile_board_image(board_name: str, flags: Optional[List[str]] = None) -> str:
    # 处理 None 值
    actual_flags = flags if flags is not None else []
    ... 使用 actual_flags 变量

五、Python 面向对象编程

参考链接:https://pythonhowto.readthedocs.io/zh-cn/latest/object.html

面向对象编程(Object Oriented Programming,OOP)是一种程序设计思想。把对象作为基本单元,对象可包括对象的属性和相关操作函数,相同属性和操作方法的对象被抽象为类。

  • (Class)就类似制造零件的模具。
  • 对象(Object)就是使用模具(Class)生产出的零件

面向对象编程基本特征:封装,继承,多态

1. 基本概念

为深入理解面向对象机制,先理清一些基本概念:

python 复制代码
print(isinstance(object, type))
print(isinstance(type, object))
print(isinstance(type, type))
print(isinstance(object, object))

>>>
True
True
True
True

我真的看不明白这是在干啥???

如果你看懂了,这节就不用看了,对象(Object)和类(Class)是python中两个最基本概念,如同世界的质子和电子一样。 这一章节主要参考 Python 数据模型Python 类

a. 对象(Object)

本届主要讲解对象的三个基本属性和不同对象具备的一些特殊属性。

Python中一切皆对象 ,如 值,变量,函数,类,实例等等的一切都是。每个对象有三个基本属性 :① ID , ② 类型 type,③。(一块内存中存储了一个对象,这块内存中一定存有这三个属性)

复制代码
a = 1
print(id(a), type(a), a)
print(id(int), type(int), int)
print(id(type), type(type), type)
>>>>>>
140725597131560 <class 'int'> 1
140725595647600 <class 'type'> <class 'int'>
140725595660256 <class 'type'> <class 'type'>
# 可见 a 是一个对象,它的数据类型是 int,它的值是 1。int 和 type 也是对象,它们的数据类型均是 type。
  • id()内建方法获取对象唯一编号,是个整数,通常为内存地址
  • type()方法获取对象类型(Type),尽管输出冠以class开头的说明,但它就是指对象的类型

当一个对象为数据类型时,他就有了 __base__属性

复制代码
# print(a.__base__)
print(int.__base__)
print(type.__base__)
>>>
<class 'object'>
<class 'object'>

一个对象可以使用名字引用它,也可以没有,比如整数 1,一个不具名对象我们无法同时测试它的三个属性(因为没有名字,第二次测试时无法保证还是原来的对象),但是依然可以测试匿名对象具有这三个属性。

复制代码
print(int.__name__)
print(id(1), type(1), 1)
print((1).__name__)
>>>
int
140725510689576 <class 'int'> 1 # 不具名的对象也有三个属性
AttributeError: 'int' object has no attribute '__name__'. Did you mean: '__ne__'? # 没名字

b. 类型(Type)

一个对象必有 Type 属性,同样 Type 是不能脱离开对象存在的。一个对象的类型定义了这个对象支持的行为以及它承载的值的类型,比如取名字,算数运算,求长度等等,一个 int 类型的对象只接受整型的数值。

如何获取对象的类型?两种方法,他们等价:

  • type()

  • .__class__

    a = 1
    print(type(a))
    print(a.class)

    <class 'int'>
    <class 'int'>

c. 类(Class)

大多数编程语言中,类是一组用来描述如何生成一个对象的代码段。由于Python是动态语言,类是动态生成的,它和传统意义上的意义不同。在Python中创建一个新类(Class)等同于创建了一个新类型(Type)的对象(Object)

通过 class 关键字我们可以定义一个新的类型(New User-defined Type)。

python 复制代码
class Student():
    pass
b = Student()
print(id(Student), type(Student), Student)
print(id(b), type(b), b)
>>>
1818212025136 <class 'type'> <class '__main__.Student'>
1818203681424 <class '__main__.Student'> <__main__.Student object at 0x000001A755627290>

上面了代码中,我们定义了自己的数据类型 Student,b 是他的实例(Instance)。

Class 和 Type 均是指的类型。前者指的是用户使用 class 自定义类型。后者指的是 Python 的解释器 CPython 内置的类型。(CPython 提供内置方法 type() 而没有定义 class(),因为它们本质是一样的,只是不同的语境产生的不同说法。)

分清楚几个继承相关名词:Python 中支持类的多重继承,被继承的类称为当前类的基类(Base Classes)或者超类(Super Classes),也叫做父类。当前类被称为被继承类的子类(Subclasse)。

issubclass(class, classinfo) 内置方法用于判断 class 是否为 classinfo 的子类。

复制代码
class A:
    pass
class B(A):
    pass

print(issubclass(B, A))
print(issubclass(B, object))
>>>
True
True

无论是自定义类型还是内置类型,他们均具有 __base__属性(回忆,前面一小节讲了:当一个对象为数据类型时,他就有了 __base__属性。),由于支持多继承,他是一个元组类型,指明了类型对象集成了哪些类。

复制代码
class A():
    pass
class B():
    pass
class C(A, B):
    pass

print(C.__bases__)
>>>
(<class '__main__.A'>, <class '__main__.B'>)

类(Class)有创建对象(Object)的能力,这就是为什么它是一个类的原因,它的本质仍然是一个对象(Object),于是可对类(Class)进行如下操作。

  • 可赋值给一个变量
  • 可复制它
  • 可为他增加属性
  • 可作为函数参数
  • 可作为函数返回值
  • 可动态创建一个类

type() 内置方法不仅可以获取对象类型 ,还可以动态创建一个类

python 复制代码
# 结论:方法1 和 方法2 在功能上完全等价 ------ 它们创建的类 A 和 B 行为一模一样。
#      区别只在于:方法1 是写给人看的常规写法;方法2 是 Python 内部真正创建类的方式。
#      对应方法1,Python 解释器在背后其实会调用 type() 来创建这个类。

# 方法1: 普通方式(语法糖)
class A(object):
    name = 'A'
    def print_name(self):
        print(self.name)
        
print(type(A))
print(type(A.print_name))


# 方法2:动态方式(用 type() 函数)
def print_name(self):
    print(self.name)

B = type('B', (object,), {'name': 'B', "print_name" : print_name})
print(type(B))
print(type(B.print_name))

>>>
<class 'type'>
<class 'function'>
<class 'type'>
<class 'function'>

方法2中type(name, bases, dict) 的含义:

参数 作用 对应到 class 语句中的哪部分?
'B' 类的名字(字符串) class B(...): 中的 B
(object,) 父类组成的元组(tuple) class B(object): 中的 (object)
{'name': 'B', 'print_name': print_name} 类的属性字典(包含变量和方法) 类体内的 name = 'B'def print_name...

虽然平时写代码几乎不用手动调用 type() 来创建类,但它非常重要,因为:

  1. 揭示了 Python 的本质 :类也是对象,由 type 创建。

  2. 支持动态编程:你可以在运行时根据条件生成类!

    if user_type == 'admin':
    UserClass = type('User', (BaseUser,), {'role': 'admin'})
    else:
    UserClass = type('User', (BaseUser,), {'role': 'guest'})

d. 实例(instance)

实例(Instance)和 对象(Object)是不同语境下的不同说法。

"1 是一个 int 类型的实例" 和 "1 是 int 类型的对象" 是等价的。(如果这句话中的 "类型" 替换为"类",就成了我们熟悉的面向对象编程中的说法:"1 是一个 int 类 的实例" 和 "1 是 int 类的对象"。)

强调对象的类型 时:某对象是某类型的实例

强调对象 自身时:我们只是说某对象

当一个对象是某个类的实例时,它也是这个类的基类的实例。内置方法 isinstance(obj, class) 用来判断一个对象是否是某个类的实例。

复制代码
class A(object):
    name = 'A'
a = A()
print(isinstance(a, A))
print(isinstance(a, object))
>>>
Ture
Ture

e. Object 与 Type 的关系

厘清了上述概念,开始分析 Python 中对象和类型的关系。

  • Python 中 对象之间 存在两种关系:

    • 两个类之间:父子关系或继承关系 (Subclass-Superclass 或 Object-Oriented),对象的 __bases__ 属性记录这种关系,使用 issubclass() 判断。

      复制代码
      class A(object):
          name = 'A'
      
      class B(A):
          age = 20
      
      class C(B):
          age = 20
      
      print(A.__base__)
      print(B.__base__)
      print(C.__base__)
      >>>>>>>>>>>>>>>>>>>>>>>>>>
      <class 'object'>
      <class '__main__.A'>
      <class '__main__.B'>
    • 类和实例之间:类型实例关系 (Type-Instance),如"米老鼠是一只老鼠"(Mickey is an instance of mouse),这里的米老鼠不再是抽象的类型,而是实实在在的一只老鼠。对象的 __class__ 属性记录这种关系,使用 isinstance() 判断

      复制代码
      class Student():
          age = 20
      lyrix = Student()
      
      print(Student.__class__)
      print(lyrix.__class__)
      >>>
      <class 'type'>
      <class '__main__.Student'>
  • Python 把对象分为两类,类型对象 (Type)和 非类型对象(Non-type)

    • int, type, list 以及用户自定以class 等均是类型对象(打印.__class__输出<class 'type'>),可以被继承,也可以被实例化。
    • 1, [1] , 'str'等均是非类型对象,它们不可再被继承和实例化,对象间可以根据所属类型进行各类操作,比如算数运算。
  • objecttype 是 CPython 解释器内建对象,它们的地位非常特殊,是 Python 语言的顶层元素

    • object是其他所有类型的基类,其自身没基类,它的数据类型为type

    • type 继承自 object,所有类型对象 都是它的实例,包括它自身。判断一个对象是否为类型对象,就看它是否是 type 的实例。

      python 复制代码
      print(isinstance(123, type))
      print(isinstance('lyrix', type))
      print(isinstance(A, type))
      print(isinstance(int, type))
      >>>
      False
      False
      True
      True

现在回到开篇的问题,isinstance() 内置方法本质是在判断对象数据类型,它会向基类回溯,直至回溯到 object,在 CPython 中最终调用如下函数:

复制代码
static int
type_is_subtype_base_chain(PyTypeObject *a, PyTypeObject *b)
{
    do {
        if (a == b)
            return 1;
        a = a->tp_base;
    } while (a != NULL);

    return (b == &PyBaseObject_Type);
}

object 和 type 在 CPython 中分别对应 PyTypePbject (对 PyObject 的封装) 类型的PyBaseObject_Type 和 PyType_Type 变量,其中用于表示类型的成员 ob_type 是一个指针,均指向 PyType_Type,所以 object 和 type 对象类型均为 type。

复制代码
print(isinstance(object, type))  # 1 True,因为 object 是一个类,而所有类都是 type 的实例。
print(isinstance(type, object))  # 2 True,因为 一切皆对象,type 作为一个对象,必然继承自 object。
print(isinstance(type, type))    # 3 True,type 是它自己的实例(这是 Python 的特殊设计,type 是自己的元类)。
print(isinstance(object, object))# 4 True,因为 object 是一个对象,而所有对象都是 object 的实例(包括它自己)。

2. 类 和 对象/实例

类和对象是面向对象编程中封装特征的体现, 将一类属性 和围绕属性操作的方法 封装在一起,相同功能代码内聚成类。不同类之间相互隔离。实例(Instance)和 对象(Object)是不同语境下的不同说法。--- 参见上一章节 d.实例(instance)

a. 类属性

  • 类可以有自己的属性,叫做 类属性 (Class Attribute) ,或类成员变量。属于类本身,所有实例共享同一个值,内存独一份,类名 或 实例 都可以访问 ,但只能通过类名修改不能通过实例修改

  • 类中也可存在 实例属性(Instance Attribute) ,属于每个具体的对象(实例),每个实例都有自己的一份,只能通过实例访问 (不能直接通过类名访问) 。这只是简单提一嘴,在本章 f 节 细聊。

python 复制代码
class Employee():
    class_version = "v1.0"    # 类属性,属于类本身,所有实例共享同一个值,类名 或 实例 都可以访问(但修改方式有讲究)
    def __init__(self, id, name):  # 类的"初始化方法"(也叫构造方法),当你创建一个对象(实例)时,它会自动被调用。self:代表当前正在创建的这个对象本身
        self.id = id		# 实例属性(Instance Attribute),属于每个具体的对象(实例),每个实例都有自己的一份,只能通过实例访问(不能直接通过类名访问)
        self.name = name	# 实例属性(Instance Attribute)

Employee.class_version = 'v1.5'	# 如果要改变类属性,直接进行赋值操作是最简单的方法。对类属性赋予新的值,它的所有实例的类属性也会更新。
print(Employee.class_version)   # 类名直接访问类属性
worker0 = Employee(0, "John")   # 位置参数(positional arguments),按顺序传:第一个是 id,第二个是 name
print(worker0.class_version)    # 实例访问类属性
worker1 = Employee(id = 1, name = "Xlon")   # 关键字参数(keyword arguments),明确指定参数名,顺序可以乱
print(worker1.class_version)    # 实例访问类属性

常见误区想要通过实例"修改"类属性,看下面例子:

python 复制代码
class Employee():
    class_version = 'v1.0'  # 类属性,

    def __init__(self, id, name): 
        self.id = id
        self.name = name

print(Employee.class_version)

worker0 = Employee(0, "John")
worker0.class_version_222 = 'v1.888'
print(worker0.class_version_222)  # 实例新增一个实例属性
worker0.class_version = 'v1.4' # 并不会修改类属性!而是给 e1 新增了一个同名的实例属性,它会遮盖(shadow) 类属性。
print(worker0.class_version)


worker1 = Employee(id=1, name="White")
print(worker1.class_version)
>>>>>>>>>>>>>>>>
v1.0
v1.888
v1.4
v1.0

根据上面代码,可见:类属性 要用类名来修改,不能用实例修改。

b. 类私有属性和类方法

私有属性 以双下划线开头,不能通过类名或实例直接访问,只能通过类方法 访问 (可以是类名调用类方法,也可类的实例调用类方法),只能通过类方法 修改(可以是类名调用类方法,也可类的实例调用类方法)。类私有属性在内存中永远只有一份,所有修改会影响到类和类的所有实例。

实例方法 :定义方式为普通 def , 必须通过实例来访问调用,类名访问会报错。(这里提一嘴,具体看本章f小节)

类方法 :的定义有讲究:第一个参数总是 cls,它指类自身;必须加上 @classmethod 装饰器说明符。参考 内置装饰器

python 复制代码
class Employee:
    __class_version = 'v1.0'  # 类私有属性

    def __init__(self, id, name):
        self.id = id        # 实例属性
        self.name = name
        
    def get_id(self):  	    # 实例方法,只能通过实例访问
        return self.id
    
    @classmethod
    def cls_get_version(cls):  # 类方法
        return cls.__class_version

    @classmethod
    def cls_set_version(cls, new_version):
        cls.__class_version = new_version

print(Employee.cls_get_version())
Employee.cls_set_version('v1.8')
print(Employee.cls_get_version())

如果要定义只读属性,不要定义赋值动作的函数即可,比如这里的 cls_set_version()。

c. 类的静态方法

使用 @staticmethod 装饰器说明符可以定义类的静态方法,参考 内置装饰器。与类方法的不同:没有隐式传递的参数cls ,通常用对于一类函数进行封装

python 复制代码
class Employee:
    __class_version = 'v1.0'  # 类属性

    @staticmethod
    def static_get():
        print("This is a class static method")

print(Employee.static_get())
>>>>>>>
This is a class static method
None

类的静态方法无法访问类属性。另外需注意,无论是类方法还是类的静态方法都只能通过类名加 '.' 的方式调用,不能间接调用它们,例如:

python 复制代码
class Employee():
    __class_version = "v1.0"
    def __init__(self, id, name):
        self.id = id
        self.name = name

    @classmethod
    def cls_ver_get(cls):
        return cls.__class_version

    func_map = {'cls_ver_get':cls_ver_get}  # 兜了个大圈子访问静态方法

    def call_func_map(self):
        self.func_map['cls_ver_get']()

worker0 = Employee(0, "John")
worker0.call_func_map()

>>>
TypeError: 'classmethod' object is not callable

d. dir() 查看类属性和方法

dir() 内建函数用于获取 任意对象的所有属性和方法

复制代码
print(dir(Employee))
>>>>>>
['_Employee__class_version', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'cls_get_version', 'cls_set_version', 'static_get']

所有类默认继承内建对象的基类object, 上面看到的很多方法和属性都来自 object 对象。

仔细观察,我们自定义的__class_version私有属性被加上了个前缀成了 _Employee__class_version。而我们定义的类方法没有变化:cls_get_version,cls_get_version'

复制代码
print(type(Employee.__init__))
print(type(Employee.static_get))    #类的静态方法
print(type(Employee.cls_get_version)) #类方法
>>>>
<class 'function'>
<class 'function'>
<class 'method'>

从上面代码看出,只有我们声明为类方法的函数 类型为 method, 其他的都为function.

e. 动态绑定类属性和方法

我们可以为已定义的类动态的绑定新的属性和方法。

  • 动态绑定属性和方法就是:冷不丁通过 类名 赋值一个新的类属性或者类方法。
  • 这些新的类属性和类方法都可以被类的实例访问。
python 复制代码
class Employee:
    public_version = 'pv1.0'  # 类属性, 能通过类名和实例访问,但只能通过类名修改
    __class_version = 'v1.0'  # 类私有属性, 不能通过类名或实例直接访问,只能通过类方法访问和修改

    def __init__(self, id, name): # 类的初始化方法或叫构造方法,self为正在创建的对象本身
        self.id = id		# 实例属性,只能通过实例访问
        self.name = name

    @classmethod
    def cls_get_version(cls): # 类方法
        return cls.__class_version

Employee.__class_version = "v1.3" # 动态绑定类属性, 不覆盖与之同名的类私有属性
Employee.gender = "male"          # 动态绑定类属性


def new_init(self, id, name, age): # 定义了一个函数
    self.id = id
    self.name = name
    self.age = age

Employee.__init__ = new_init # 用新定义函数覆盖类原有的构造方法
print(dir(Employee))

worker0 = Employee(123123, 'lyrix', 25) # 发现类原有的构造方法果然被新的覆盖了
print(worker0.id)
print(worker0.gender)

f. 对象的属性和方法(实例属性,实例方法)

对象是类的实例化,我们在创建对象时,复制了一份类的内存空间,并在内存空间中填入了实例的参数值。(类的私有属性和类方法不可被实例化。他们还指向原来的类。)

对象和类一样,可以定义私有属性与方法,也是加 __ 前缀。不能通过对象名直接引用。

同样可以为一个对象动态添加新的实例方法 ,新添加的实例方法为本对象私有 ,不影响类的其他实例对象,如果存在同名的 '实例方法类方法 ' 则被新的实例方法覆盖

python 复制代码
def new_get_version(self):  # 定义了一个函数
    return self.id
Employee.cls_get_version = new_get_version #通过类名 动态绑定类方法

print(type(new_get_version))
print(type(Employee.cls_get_version)) #类名访问类方法
print(type(Employee.return_id))	#类名访问实例方法

print(type(worker0.cls_get_version))  # 实例访问类方法
print(type(worker0.return_id))	 # 实例访问实例方法

print(Employee.cls_get_version())  # 因为类型为 function,表示为原始函数,没有绑定,无法调用
print(worker0.cls_get_version())  # 可以正常打印

>>>>>>>>>>>>>>>
<class 'function'>
<class 'function'>
<class 'function'>
<class 'method'>
<class 'method'>

上图可知:一个函数默认类型为 function,一个类中定义的类方法和实例方法类型都是 function,而实例化过程会将 function 类型转换为 method 类型

我修改上面的一行代码为:

python 复制代码
def new_get_version(self):  # 定义了一个函数
    return self.id
worker0.cls_get_version = new_get_version # 通过实例对象,动态绑定

print(type(new_get_version))
print(type(Employee.cls_get_version)) # 这个变化了,原来的类方法消失,被一个实例方法覆盖了
print(type(Employee.return_id))

print(type(worker0.cls_get_version)) # 这个变化了,
print(type(worker0.return_id))

print(Employee.cls_get_version())  # 可以正常打印
print(worker0.cls_get_version())  # 因为类型为 function,表示为原始函数,没有绑定,无法调用

>>>>>>>>
<class 'function'>
<class 'method'>	# 这个变化了
<class 'function'>
<class 'function'> # 这个变化了
<class 'method'>

可见通过类名访问的方法的类型变为了 method,原来的类方法消失,被一个实例方法覆盖了。如果还想保持原来的 function 类型,则需要借助 types.MethodType 函数。

所以知道 function 和 method 有啥用

  • function 是原始函数不绑定到任何对象;method 是绑定了对象(self 或 cls)的函数。

  • 当你通过实例 访问类中的函数时,Python 自动将其包装成"绑定方法"(bound method),自动传入 self。也就可以哦通过实例来调用使用方法了。

  • 避免动态绑定陷阱:比如误以为:

    复制代码
    obj.method = my_func
    obj.method()  # 应该自动传 self?
  • 实现高级功能:动态打补丁、Mock 测试、插件系统

e. 访问 对象属性(实例属性)

与类属性类似,也有两种方式:① 通过对象名直接访问,② 通过对象的方法访问。

  • 类中所有第一个参数为**self**的 函数 都是对象的方法
python 复制代码
class Employee:
    def __init__(self, id, name):
        self.id   : int = id
        self.name : str = name

    def get_name(self) -> str:
        return self.name
    def set_name(self, name : str) -> None:
        self.name = name

worker0 = Employee(id=0, name='lyrix')
print(worker0.get_name())       # 实例属性间接访问

worker0.name = 'Ming'
print(worker0.name)

worker0.set_name(int(123123))   # 通过对象间接修改实例属性,为何没报格式不匹配的错误?python不做类型检查,打开Pright做静态类型检查就查出来了。
print(worker0.name)

Employee.set_name(worker0, 'White') # 通过类名简间接改实例属性
print("class:" + worker0.name)
print("class:" + Employee.get_name(worker0)) # 通过类名简间接改实例属性
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
lyrix
Ming
123123
class:White
class:White

直接通过对象访问和修改对象属性固然简单。But, 这违反了面向对象的思想。任何对外暴露属性的编码都意味着混乱(可能是因为对象属性可以动态加载,没准哪次就写错了)。而使用对象方法来维护对象属性犯这类错误的几率就会降低(这句话的意识是,你写错了对象属性Python不会报错,但是你用错了对象方法Python就报错啦,的确)但这样多少有点麻烦啊。

!IMPORTANT

Pyright 为何不能检查出这行代码的错误呢worker0.set_name(int(123123))

Pyright 如何工作?

  • Pyright 是一个 静态类型检查器 ,它只在有类型信息的地方做检查。
  • 如果函数参数没有显式标注类型 ,Pyright 默认认为它是 Any 类型
  • 所以只要你加上类型提示信息,他才能检查出来

那如何兼顾直接访问对象属性的便捷又不违反面向对象的思想呢?答案是使用@property 装饰器,下面学到了在讲解。这类仅给出个小例子:

python 复制代码
class Employee:
    def __init__(self, name, age):
        self.name = name
        self._age = None   # 用下划线表示"内部使用"
        self.age = age     # 触发 setter

    @property # 装饰器
    def age(self):
        """getter:当读取 emp.age 时调用"""
        return self._age

    @age.setter # 装饰器
    def age(self, value):
        """setter:当写入 emp.age = xxx 时调用"""
        if not isinstance(value, int):
            raise TypeError("Age must be an integer")
        if value < 0:
            raise ValueError("Age cannot be negative")
        self._age = value


# 使用起来和直接访问属性一模一样!
emp = Employee("Alice", 25)
print(emp.age)      # ✅ 25 (实际调用了 getter)

emp.age = 30        # ✅ 成功 (实际调用了 setter)
print(emp.age)      # 30

emp.age = -5        # ❌ 报错!ValueError: Age cannot be negative

!IMPORTANT

mypy/Pyright + 类型注释 能否替代 @property 装饰器?

答:不行啊:

  • mypy/Pyright + 类型注释 是开发阶段的"语法警察",防止低级类型错误
  • @property是运行阶段的语法警察。确保对象状态始终合法

f. 限制属性和方法动态添加__slots__

我们可以动态的给对象添加属性和方法,这样虽灵活,但也破坏了面向对象的优点"封装"。类的特殊变量 __slots__可以对这种灵活性加以限制。

定义:__slots__是类中的一个特殊类变量 ,用于显式的声明该类的实例允许有哪些属性 ,一旦定义了__slots__实例将不能动态的添加新的属性。例子看一下用跟不用的区别:

python 复制代码
class Employee:
    def __init__(self, name):
        self.name = name

worker0 = Employee("Alice")
worker0.department = "HR"      # ✅ 允许!动态添加属性
worker0.salary = 10000         # ✅ 允许!
print(worker0.__dict__)        # {'name': 'Alice', 'department': 'HR', 'salary': 10000}
  • 每个实例都有个 __dict__属性,存储所有属性。
  • 没有 __slots__ 灵活,但是容易写错属性名**(如 worker0.nmae = "Bob" 不报错)**
python 复制代码
class Employee:
    __slots__ = ['id' , 'name' , 'department']
    def __init__(self, name):
        self.name = name

worker0 = Employee("Alice")
worker0.department = "HR"      # ✅ 允许!因为在 __slots__ 中声明了
print(worker0.__dict__)        # ❌ 报错!__dict__属性也没了
worker0.salary = 10000         # ❌ 报错!AttributeError: 'Employee' object has no attribute 'salary'

何时应该使用 __slots__

推荐使用场景 不推荐使用场景
创建大量轻量级对象(如数据点、节点) 需要高度动态性的类(如插件系统)
类属性固定不变,希望防止误写 需要频繁动态添加属性
追求内存效率或性能 使用多重继承且父类 slots 冲突
强调封装和接口稳定性 快速原型开发(灵活性优先)

3. 基类和继承

a. object 和 类特殊方法

object 是所有类的基类(Base Class,也被称为超类(Super Class)或父类),如果一个类在定义中没有明确定义继承的基类,那么默认就会继承 object。

python 复制代码
class worker():
    pass
class Employee(worker):
    pass
print(Employee.__mro__) # __mro__ 属性记录类继承的关系,它是一个元组类型
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
(<class '__main__.Employee'>, <class '__main__.worker'>, <class 'object'>)

下面了解一些object自带的属性和方法:

python 复制代码
for i in dir(object):
    attr_value = getattr(object, i)
    print(f"{i:<20}: {type(attr_value)}")
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
__class__           : <class 'type'>
__delattr__         : <class 'wrapper_descriptor'>
__dir__             : <class 'method_descriptor'>
__doc__             : <class 'str'>
__eq__              : <class 'wrapper_descriptor'>
__format__          : <class 'method_descriptor'>
__ge__              : <class 'wrapper_descriptor'>
__getattribute__    : <class 'wrapper_descriptor'>
__getstate__        : <class 'method_descriptor'>
__gt__              : <class 'wrapper_descriptor'>
__hash__            : <class 'wrapper_descriptor'>
__init__            : <class 'wrapper_descriptor'>
__init_subclass__   : <class 'builtin_function_or_method'>
__le__              : <class 'wrapper_descriptor'>
__lt__              : <class 'wrapper_descriptor'>
__ne__              : <class 'wrapper_descriptor'>
__new__             : <class 'builtin_function_or_method'>
__reduce__          : <class 'method_descriptor'>
__reduce_ex__       : <class 'method_descriptor'>
__repr__            : <class 'wrapper_descriptor'>
__setattr__         : <class 'wrapper_descriptor'>
__sizeof__          : <class 'method_descriptor'>
__str__             : <class 'wrapper_descriptor'>
__subclasshook__    : <class 'builtin_function_or_method'>
1. __call__

该方法未在基类实现,但有非常特殊的功能值得学习,__call__可将一个对象名函数化,一个实现了__call__方法的类,则其实例就可被直接调用。如下:

python 复制代码
class Employee():
    def __init__(self, id, name):
        self.id = id
        self.name = name

    def __call__(self, *args):
        print(*args)
        print("Printed from __call__")

worker0 = Employee(0, "Alice")
worker0("arg0", "arg1")
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
arg0 arg1
Printed from __call__

装饰器类 (还没学到)就是基于 __call__() 方法来实现的。注意 __call__() 只能通过位置参数来传递可变参数,不支持关键字参数,除非函数明确定义形参。可以使用 callable() 来判断一个对象是否可被调用,也即对象能否使用()括号的方法调用。

2. __iter__ __getitem__

参考 索引访问和循环可迭代对象和迭代器 。在下一章"生成器和迭代器"有讲解。

9. __hash__

如果一个对象有__hash__方法,那么它就是可散列的(Hashable,严格来说需要同时实现 __eq__(),基类默认实现了该函数)。

基类默认试下了__hash__方法,使用对象的id作为散列值。所以用户定义的类的实例都是可散列的。且彼此不平等。如果明确不可散列,需要做以下处理。

复制代码
def UnHashablecls():
  ......
  # 最直接的方式:__hash__ = None
  def __hash__(self):
      msg = "unhashable type: '{0}'".format(self.__class__.__name__)
      raise TypeError(msg)

如果...

b. attr 内置方法 获取 设置 判定对象属性

Python 提供了三个内置方法 getattr(),setattr(),hasattr(),分别用于获取,设置,判定对象的属性。

问:我们明明可以直接访问对象的属性,还要这三个函数干毛用?

答:当我们面对不熟悉的属性,可以进行尝试访问,以免出错。

python 复制代码
class Employee:
    def __init__(self, name):
        self.name = name

worker0 = Employee("Alice")
worker0.salary = 10000 

if hasattr(worker0, 'name'):
    print(worker0.name)
if 100 < getattr(worker0, 'salary'):
    worker0.salary = 100

c. 内置方法和对应操作

① 算术运算
② 比较运算
③ 赋值运算
④ 位运算
⑤ 逻辑运算

数不清啊

d. 实例所属类的判定 isinstance()

用 isinstance([], list) 来判定某个对象是否属于某个类的实例化。

e. 多重继承的顺序

继承是面向对象编程的一大特点,继承可以使得 子类具有父类的属性和方法 ,并可对属性和方法进行扩展

Python中继承的最大特点是可以进行多重继承,即一个类可以同时继承自多个父类。

我们可在新类中使用父类定义的方法,还可以定义同名方法,还可以复用父类方法,还可以在自定义的方法中使用super()调用父类的同名方法(用到再说)

4. 枚举类

暂时没遇到过。

复制代码
from enum import Enum

5. 元类 metaclass(待补充)

六. 生成器和迭代器

1. 生成器

引入:拿列表做例子,列表占用的空间大小与元素个数成正比,如果一个链表很大很大,超出了内存限制怎么办呢?如果列表元素可以用某种已知的算法推导出来,不用一次就创建出所有的元素,而是边用边生成,这就是生成器(generator)。用时间换空间。生成器的两种表达:① 小括号表示的生成器表达式,② 生成器函数(generaction function)。

a. 生成器表达式

生成列表和字典时,可以用推导表达式完成,只要将推导表达式的中括号换成小括号就成了生成器表达式。

python 复制代码
import sys
list0 = [x * 2 for x in range(10)]
print(sys.getsizeof(list0))
list1 = [x * 2 for x in range(1000)]
print(sys.getsizeof(list1))

list0_generation = (x * 2 for x in range(10))
print(sys.getsizeof(list0_generation))
list1_generation = (x * 2 for x in range(1000))
print(sys.getsizeof(list1_generation))
>>>>>>>>>>>>>>>>>>>>>
184
8856
208
208

如何获取生成器对象的每一个元素呢?答:借助Python的next()方法。每次调用next()方法,都会根据最后的值和生成器给出的方法计算下一个值,直到最后一个元素,抛出StopIteration异常。

python 复制代码
list1_generation = (x * 2 for x in range(10))
print(next(list1_generation))
print(next(list1_generation))
print(next(list1_generation))
print(list(list1_generation)) # 把"剩余的元素"全部取出并放进列表, 会耗尽迭代器
print(sys.getsizeof(list1_generation))
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
0
2
4
[6, 8, 10, 12, 14, 16, 18]
208

但是呢,一般不用next()来获取生成器元素,因为generator是可迭代对象,通常不会使用next()来逐个获取元素,而是用for...in...,会自动在遇到StopIteration异常时结束循环:

python 复制代码
from collections.abc import Iterable
list1_generation = (x * 2 for x in range(3))
print(isinstance(list1_generation, Iterable))
for i in list1_generation:
    print(i)
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
True
0
2
4

b. 生成器函数

生成表达式能力有限,复杂的处理需要生成器函数完成,比如斐波那契数列。

python 复制代码
def fibonacci(n):
    i, j = 0, 1
    while (i < n):
        print(i, end=' ')
        i, j = j, i + j

fibonacci(5)
print(type(fibonacci))

我们很容易写出斐波那契数列函数,有时候,我们不需要打印出来,而是只需要把这种算法封装起来,把fibonacci函数变成一个生成器函数,只需要把print这行替换成yielb i即可。

定义:如果一个函数定义中包含yielb表达式,那么该函数就不是普通函数而成了生成器函数yield语句类似return会返回一个值,但是会记住返回的位置,下一次调用next()就还从那个位置继续执行。

python 复制代码
def febonaqi(n):
    i, j = 0, 1
    print("开始执行...")
    while(i < n):
       yield i
       i, j = j, i + j
       print("醒来,继续下一轮...")

gen = febonaqi(5)  
print(type(gen))    # 可以看到 gen 是个生成器对象 <class 'generator'>
for i in gen:       # 相当于调用了5次next(gen)
    print(i, end=' ')
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
<class 'generator'>
开始执行...
0 醒来,继续下一轮...
1 醒来,继续下一轮...
1 醒来,继续下一轮...
2 醒来,继续下一轮...
3 醒来,继续下一轮...

c. 生成器本质

**生成器就是一个"带有记忆功能的暂停按钮"。**任何一个生成器都会定义一个名为__next__的方法,调用next()的本质就是调用他,在调用到最后一个元素后抛出StopIteration异常。如果想要手动实现生成器的话,原理类似如下手动实现生成器:

python 复制代码
from collections.abc import Iterable
class FibGenerator():
    def __init__(self, n): # 初始化实例属性
        self.__n = n

        self.__s0 = 0
        self.__s1 = 1
        self.__count = 0

    def __next__(self):  # 用于内建函数 next(),或for循环中被触发
       if self.__count < self.__n:
           ret = self.__s0
           self.__s0, self.__s1 = self.__s1, (self.__s0 + self.__s1)
           self.__count += 1

           return ret
       else:
           raise StopIteration

    def __iter__(self):  # 没该成员,则对象不能用于 for 循环语句
       return self

fg = FibGenerator(5)
print(type(fg))
print(isinstance(fg, Iterable))

for i in fg:
    print(i, end=' ')
  • 当你使用生成器yield,Python 自动把函数里所有的局部变量(比如 i)冻结保存起来。
  • 当你使用生成器yield,Python 自动把这个函数变成一个对象,这个对象天生就有 __next__ 方法。
  • 当你使用生成器yield,当代码运行到尽头(或遇到空的 yield),它自动抛出 StopIteration。生成的对象天生就是可迭代的,直接能用在 for 循环里(否则你要写 def __iter__(self): return self 才能让 for 循环工作)。

看到 yield,就把它想象成函数的**"存档点"**。

  1. 运行到 yield -> 存档 (保存当前所有变量),交出一个值暂停
  2. 下次调用 next() -> 读档 (恢复变量),从下一行代码继续跑。

2. 迭代和迭代对象

在 Python 中通过for...in...对象进行遍历 的操作叫做迭代 (iteration),可以被迭代的对象叫可迭代对象(Iterable)。列如:字符串,列表,元组,文件,管道,以及更复杂的生成器等等都是可迭代的。

如何判断一个对象是否可迭代呢?

python 复制代码
from collections.abc import Iterable 
list1 = [1, 2, 3, 4]
print(isinstance(list1, Iterable))

除了上面这种,还可以通过iter来做压力测试判断,通过测试的为鸭子类型,下面'迭代器-迭代器对象类型判断'小节会讲解到。

3. 迭代器

a. 可迭代对象,迭代器,生成器 关系

一句话总结 :生成器(generator)是一种用特殊语法(yield)定义的迭代器,所有生成器都是迭代器,但并非所有迭代器都是生成器。

已知能被直接用于for循环的对象称为可迭代对象(Iterable),而生成器不但可直接作用于for循环,还能被next函数调用返回下一个值,直到最后抛出 StopIteration 错误,所以生成器是迭代器(iterator)。

用集合的概念理解如下:

  • 可迭代对象 (Iterable): 最外层大圈 :只要能用for...in...遍历的都是。特征 :有__iter__方法或者(__getitem__(index) 从索引 0 开始递增访问,遇到 IndexError 停止)。
  • 迭代器 (Iterator):中间的圈 :不仅能被 for 循环,还能被 next() 一个个取值,且记住状态。特征 :既有 __iter__ 又有 __next__ 方法。
  • 生成器 (Generator):最内层圈 :函数中包含yield i后,自动生成__iter__方法和__next__方法,且在调用到最后一个元素后抛出StopIteration异常。它是python帮你维护的迭代器,它是迭代器的子集。

b. 迭代对象类型判断

判断对象是否为可迭代对象除了使用from collections.abc import Iterable,还可以用iter来判断。那他们有什么区别?

简单来说:

  • isinstance(obj, Iterable) 是查**"身份证"**(看它是否正式注册了可迭代协议),不执行代码。
  • iter(obj) 是搞**"压力测试"**(不管你有没有身份证,只要能跑起来就算数),他会去调用并执行__iter____getitem__方法。

在 99% 的情况下,它们结果一致。但在某些特殊边缘情况(Edge Cases)下,它们会有本质区别

特性 isinstance(obj, Iterable) try: iter(obj) ...
原理 检查对象的类是否继承自 collections.abc.Iterable 或定义了 __iter__ 尝试调用对象的 __iter__ 方法;如果没有,尝试调用 __getitem__
严格程度 严格。只认"正规军"。 宽松。认"正规军",也认"野路子"。
__getitem__ 的支持 ❌ 不支持。 如果一个类只实现了 __getitem__ 而没有 __iter__,它会返回 False ✅ 支持。 Python 会尝试通过索引 0, 1, 2... 来模拟迭代,成功则返回迭代器。
性能 快(只是类型检查)。 稍慢(可能触发方法调用,甚至真的开始取值)。
副作用 无。 可能有副作用。 如果 __iter____getitem__ 里有打印、网络请求等代码,调用 iter() 就会执行它们。
推荐场景 类型注解、静态分析、严格的接口检查。 实际编程中判断"能不能用 for 循环"。
python 复制代码
from collections.abc import Iterable

class Dangerous:
    def __iter__(self):
        print("⚠️ 警告:正在连接数据库...")
        print("⚠️ 警告:正在删除所有数据...")
        return iter([])

obj = Dangerous()

print("--- 使用 isinstance ---")
res1 = isinstance(obj, Iterable)
print(f"结果: {res1} (什么都没发生)")

print("\n--- 使用 iter() ---")
# 注意:这里可能会触发危险操作!
try:
    res2 = iter(obj)
    print(f"结果: 成功 (但上面的警告已经打印了)")
except TypeError:
    print("结果: 失败")

c. 生成迭代器iter

iter()内建方法可以把list, dict, str等可迭代对象转换成迭代器。

python 复制代码
list0 = [0, 1, 2]
iter0 = iter(list0)

print(type(iter0))

>>>>>>>>>>>>>>>
<class 'list_iterator'>

除了字典外,一个对象只要实现了__getitem__方法,就认为它是序列类型,序列类型总是可迭代的,循环作用在序列类型上的本质参考 索引访问和循环

iter(obj) 是转换器

  • iter(obj)会从可迭代对象创建一个迭代器,迭代器带有内部状态,能被next()逐步消耗。
    • for循环的第一步就是对目标对象调用iter()创建迭代器,因此只要iter(obj)可用,for obj in...就能工作。

iter(func, sentinel) 是自动化循环工具

  • func必须是零参数可调用对象(callable with no arguments)

  • 每次迭代都会调用一次func,当返回值 == sentinel 时停止

  • 常见用法:读文件块直到空串、从套接字读到特定分隔等。

  • 示例:按行读取直到空行

    python 复制代码
    import sys
    for line in iter(sys.stdin.readline, ''):
    	process(line) # 处理行内容的代码

d. 无限迭代器

无限迭代器就是可以无线迭代永远不抛出StopIteration异常的迭代器,下面是个奇数生成器,可以无限制生成奇数。

python 复制代码
class odd_gen:
    def __init__(self):
        self.n = 1
    def __next__(self): # 用于内建next
        num = self.n
        self.n += 2
        return num
    def __iter__(self): # 用于for循环
        return self

odd_obj = odd_gen()
for i in odd_obj:
    if i > 5:  
        break    # 无线迭代器必须要退出机制
    print(i, end=" ")
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
1 3 5

e. 惰性计算

惰性计算,又称为"惰性求值(Lazy Evalution)",是个计算机编程中的概念,目的是尽可能最小化计算机要做的工作,尽可能延迟表达式求值,延迟求值特别用于函数式编程语言中。即表达式不在他被绑定到变量之后立即求值,而是在该值被驱动的时候求值,从而能达成可构造无限长数据类型,节省内存,平滑时间的目的。举例说明:

在普通编程(急切求值)中,一旦你写了 x = 1 + 2,电脑会立刻、马上 算出 3,然后把 3 存进 x 里。这时候,计算已经完成了。

而在惰性计算 中,当你写: "x = 一个复杂的生成器" 时:

  • 电脑并不会立刻去跑那个复杂的计算。
  • 它只是把"计算公式"或者"获取数据的方法"打包好,贴个标签叫 x,放在一边。
  • 此时,没有任何实际的数据被生产出来,也没有消耗大量内存。

七. 函数和装饰器

1. 函数特性

a. 定义和调用顺序

如果要调用并执行某函数,必须要在这之前完成函数的定义。这点跟C语言一样。

但是如果在定义一个对象时,比如创建一个函数a,在该函数a中引用了另一个函数b,但是函数b的定义是在函数a之后完成的。但只要在调用函数a前完成了函数b的定义,就没事。

python 复制代码
def a():
    b()

def b():
    print("Hello World")

a() # 调用正常,因为在调用a时,b已经定义完了

b. 匿名函数

如果一个函数使用简单的表达式就可实现相同功能,那就无需显式定义一个函数,lamdba 表达式可以返回一个没有名字的函数,也即匿名函数。语法非常简单:

python 复制代码
lambda 参数列表: 表达式

特点:

  • 只有一个表达式(不能写成多行)
  • 返回值就是表达式的结果
  • 用于临时轻量函数,不需要定义def

常见使用场景:

  • sorted()一起用,给排序指定排序关键字:

    python 复制代码
    students = [("Alice, 25"), ("Bob, 33"), ("Lili, 18")] # 列表中每个元素为二元组
    students_sorted = sorted(students, key = lambda x : x[1])
  • map()一起用,对每个元素做操作:

    python 复制代码
    nums = [1, 2, 3]
    result = list(map(lambda x: x*2, nums)) # map返回迭代器,list把迭代器转化为列表会耗尽迭代器。
  • filter()一起用,筛选满足条件的元素:

    python 复制代码
    nums = [1, 2, 3, 4]
    evens = list(filter(lambda x: x % 2 == 0, nums))
  • 封装小的回调函数

    python 复制代码
    button.on_click(lambda e: print("Clicked"))

c. 函数参数类型(常用)

python中函数参数类型一共有5种。

  1. POSITIONAL_ONLY 位置参数,内置函数或模块使用,用*"户无法自定义"*一个只支持位置参数的函数。
  2. POSITIONAL_OR_KEYWORD 位置或关键字参数,参数同时支持位置或者关键字传递给函数。(懂)
  3. VAR_POSITIONAL 可变长参数,任意多个位置参数通过元组传递给函数。(有记录)
  4. KEYWORD_ONLY 关键字参数,也被称为命名参数,通过指定的键值对传递给函数。(懂)
  5. VAR_KEYWORD 可变关键字参数,任意多个键值对参数通过字典传递给函数。(有记录)

下面我们逐一举例:

1)仅位置参数(无法自定义):

因为我们无法自定义 只支持位置传参的函数,但一些自定义参数仅能通过位置传参,无法通过关键字传参,比如比如内置函数 oct(x),ord(c),divmod(x, y) 等等

2)位置或关键字参数

因为我们无法自定义只支持位置传参的函数,那就从定义一个支持位置或关键字参数的函数开始:

python 复制代码
def foo(n):
    print(f"This is a function: n = {n}")

foo(111) # 位置传参
foo(n = 222) # 关键字传参

3)可变参数

可变参数用一个 * 来表示,把所有接收到的未被位置参数与关键字参数处理的参数放入一个元组。

python 复制代码
def variable_args(name="default", *args): # 可变参数 args
    print("name: %s" % name)
    print(args)

variable_args("John", "Teacher", {"Level": 1})
# variable_args(name = "John", "Teacher", {"Level": 1}) ❌ 关键字之后不能再出现"裸的"位置参数。

>>>
name: john
('Teacher', {'Level': 1})

注意:注意键值对参数不能被 *args 处理。

4)关键字参数:(关键字参数后不能有位置参数)

python 复制代码
def keyword_only_args(name="default", *args, age): # age 关键字参数只能放在可变参数后
    print("name: %s, age: %d" % (name, age))
    print(args)

keyword_only_args("John", "Teacher", {"Level": 1}, age=30) 

age形参位于可变参数args后,位置是不确定的,此时只能指定关键字以键值对的方式传递。此时args元组不会处理他。

其实*args一般放在最后,上面的代码等效于:

python 复制代码
def keyword_only_args(age, name="default", *args):
    print("name: %s, age: %d" % (name, age))
    print(args)

keyword_only_args(30,"John",  "Teacher", {"Level": 1})

5)可变关键字参数

可变关键字参数通过 ** 来声明,这种参数类型可接收0个或多个键值对参数并存入一个字典中。

python 复制代码
def keyword_variable_args(name="default", *args, age, **kwargs): # 一定要按照这个顺序排列,不能颠倒
    print("name: %s, age: %d" % (name, age))
    print(args)
    print(kwargs)

# 如果传参复杂,按这个顺序写。
keyword_variable_args("John", "Teacher", {"Level": 1}, id="332211", city="New York", age=30) 

>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

name: John, age: 30
('Teacher', {'Level': 1})
{'id': '332211', 'city': 'New York'}

通过上述例子发现:参数处理是有优先级的,首先通过**'位置匹配',然后进行 '关键字匹配',最后剩下的所有参数按照是否提供参数名来对应到 '可变参数''可变关键字参数'**

d. 可变参数函数:

了解了不同参数类型后,我们看一个常在开源包中看到的传递任意参数函数(不是绝对的任意,键值对放后面)。

python 复制代码
def test_args(*args, **kwargs): # *args 与 **kwargs 顺序不能颠倒
    print(args)
    print(kwargs)

test_args(1, 2, {"key0": "val0"}, name="name", age=18)

>>>
(1, 2, {'key0': 'val0'})
{'name': 'name', 'age': 18}

e. 函数参数传递形式:

介绍了上面的参数类型后,也见到了一般的参数传递方式(通过位置和关键字传递)。我们介绍另一种参数传递形式:用 * 与 ** 拆包,看例子:

python 复制代码
def func0(*args, **kwargs):
    print(args, kwargs)

def func1(*args, **kwargs):
    print(args, kwargs)

def test_call_func(func, *args, **kwargs):  
    func(*args, **kwargs) # 在函数中调用函数对实现装饰器很重要

test_call_func(func0, 1)
test_call_func(func1, 1, 2)

!NOTE

函数定义中的*args, **kwargs为打包(未解析位置参数打包成元组,未解析关键字参数打包成dict)。

函数调用中的*args, **kwargs为拆包(列表元组拆成位置参数,dict拆成关键字参数)。

2. 高阶函数

高阶函数特点为调用其他函数完成功能或者把一个函数作为返回值,map(),sorted(),filter(),partial(),reduce()这些为高阶函数。由于历史原因,很多高阶函数被封装进 functools 模块。下面为 Python3.x 中高阶函数。

a. map

  • 在线文档

  • 帮助文档

    复制代码
    <usr> python -c "help(map)"
    Help on class map in module builtins:                                                                                                              
    
    class map(object)
     |  map(func, *iterables) --> map object
     |  
     |  Make an iterator that computes the function using arguments from
     |  each of the iterables.  Stops when the shortest iterable is exhausted.

    可见,map是个 class 类。

    参数:

    • func: 可以是普通函数或lambda匿名函数。
    • *iterables: 注意那个星号 *。这意味着你可以传入一个或多个可迭代对象(如列表、元组、字符串等)。

    返回值:map object关键点来了 :它返回的不是一个列表(List),而是一个迭代器对象

    map的三个核心逻辑:① 惰性计算(仅返回一个迭代器对象,遍历时执行) ② 短板效应(当你传一个长度为3的列表和一个长度为5的列表,map只会处理3次然后停止)

  • 代码举例

    python 复制代码
    # 对应日志:--> map object
    result = map(lambda x: x * 2, [1, 2, 3])
    print(result) # 输出:<map object at 0x000001E9DCDBA5F0> ,你只能只看到一个对象地址。 
    print(list(result)) # 必须转换才能看内容, 输出:[2, 4, 6]
    python 复制代码
    # 对应日志:Stops when the shortest iterable is exhausted.
    list_a = [1, 2, 3, 4, 5]       # 长度 5
    list_b = [10, 20, 30]          # 长度 3 (最短)
    
    # 定义一个接收两个参数的函数
    def add(x, y):
        return x + y
    
    result = map(add, list_a, list_b)
    print(list(result))
    # 输出:[11, 22, 33]
    # 解释:4+? 和 5+? 没有发生,因为 list_b 用完了,map 直接停止。
    python 复制代码
    # 对应日志:Make an iterator...
    def my_func(x):
        print(f"正在计算 {x}...") # 打印语句用来观察执行时机
        return x * 2
    
    # 创建 map 对象(可迭代对象)
    m = map(my_func, [1, 2, 3])
    print("Map 对象已创建,但还没开始计算!")
    
    # 只有在这一步,my_func 才会被真正调用
    for item in m:
        print(f"得到结果:{item}")

b. sorted

sorted相对于列表自带的排序函数 L.sort()相比具有以下特点:

  1. 将功能拓展到所有可迭代对象了
  2. sorted返回新的排序列表,但是L.sort()无任何返回
  3. sorted稳定排序,经过优化速度更快
  • 帮助文档

    python 复制代码
    python -c "help(sorted)"
    Help on built-in function sorted in module builtins:                                                                                               
    
    sorted(iterable, /, *, key=None, reverse=False)
        Return a new list containing all items from the iterable in ascending order.
        
        A custom key function can be supplied to customize the sort order, and the
        reverse flag can be set to request the result in descending order.
  • 代码举例

    我们可以把元素中的部分成员作为排序关键字,其中reverse决定是升序还是降序:

    python 复制代码
    scores = {'Json':15, 'Naxi':22, 'Yang':11, 'Lyrix':18}
    new_scores = sorted(scores.items(), key=lambda x:x[1], reverse = True)
    print(new_scores)
    >>>>>>>>>>>>>>>>>>>>>>>>>
    [('Naxi', 22), ('Lyrix', 18), ('Json', 15), ('Yang', 11)]

    还可以对自定义类对象排序。

c. filter

  • 帮助文档

    复制代码
    python -c "help(filter)"
    Help on class filter in module builtins:                                                                                                           
    
    class filter(object)
     |  filter(function or None, iterable) --> filter object
     |  
     |  Return an iterator yielding those items of iterable for which function(item)
     |  is true. If function is None, return the items that are true.
  • 代码举例

    filter()方法与map()类似,和map不同的是filter把传入的函数依次作用于每个元素,然后如果返回值为真则保留该元素。

    python 复制代码
    scores = {'Json':15, 'Naxi':22, 'Yang':22, 'Lyrix':18}
    new_scores = filter(lambda x:x[1] == 22, scores.items())
    for i in new_scores:
        print(i)
    >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
    ('Naxi', 22)
    ('Yang', 22)

d. partial - 简化代码用

一个函数往往需要很多参数,有时候我们仅仅需要改动其中的部分参数,但是其他参数你也得给他补全。比如:

复制代码
# 把字符串转化为整数
int('a', base=16) 

如果你要转换大量的16进制字符串为整数,每次都写base=16就显得很麻烦。你可能会想到创建一个函数就行了:

python 复制代码
def hexstrint(str)
	return(str, base=16)

诶,partial就是可以直接创建一个这样的函数,不需要你自己定义了:

python 复制代码
from functools import partial
hexstr2int = partial(int, base=16) # 第一个参数写你想操作的函数,后面是默认的参数设置成什么
print(hexstr2int('a'))

print(type(hexstr2int))
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
10
<class 'functools.partial'>

使用 partial() 的目的是为简化代码,让代码简洁清晰,但是可能让追踪变得困难。

e. reduce

核心概念:拿着上一步的计算结果,去和下一个元素继续算,直到算完所有元素,最后剩下一个值。

让我们手动模拟一下这段代码的执行过程,这是理解 reduce 最关键的一步。

python 复制代码
from functools import reduce
total = reduce(lambda x, y: x * y, [1, 2, 3, 4])
print(total)
步骤 累积值 (x) (上一步的结果) 当前元素 (y) (列表里的下一个数) 计算过程 (x * y) 新的累积值 (result)
第1轮 1 (列表第1个元素) 2 (列表第2个元素) 2
第2轮 2 (第1轮的结果) 3 (列表第3个元素) 6
第3轮 6 (第2轮的结果) 4 (列表第4个元素) 24
结束 没有下一个元素了 - - 返回 24

关键点:第一次计算时,x为列表第一个元素,y为第二个元素。后续计算时x一直是上一次计算的结果,y是列表还没处理的下一个元素。

!IMPORTANT

reduce 还有一个可选参数 initial。如果你提供了它,过程会稍微变一下,这时候,"雪球"一开始不是列表的第一个元素,而是你给的 10

python 复制代码
reduce(lambda x, y : x + y, [1, 2, 3], 10) # 该式在计算 10 + 1 + 2 + 3

只要记住公式:新结果 = 函数(老结果, 新元素),你就完全掌握它了。

3. 作用域和闭包

程序设计中变量能访问的范围称为作用域(scope),在作用域内函数有效,可以被访问和使用。

在介绍作用域前,先看个内建函数global(),它返回当前运行程序所有的全局变量,类型为字典。

python 复制代码
string0 = "Hello world"
num = 10
name = 'Lin'

from pprint import pprint
pprint(globals())
>>>>>>>>>>>>>>>>>>>>>>
{'__annotations__': {},
 '__builtins__': <module 'builtins' (built-in)>,
 '__cached__': None,
 '__doc__': None,
 '__file__': 'C:\\Users\\nxg01742\\OneDrive - NXP\\Documents\\Doc_Lyrix\\8. '
             'Project summary\\01 Blog\\Python_learn\\try_except\\main.py',
 '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x0000020FCE9F4B50>,
 '__name__': '__main__',
 '__package__': None,
 '__spec__': None,
 'name': 'Lin',
 'num': 10,
 'pprint': <function pprint at 0x0000020FCEBC9EE0>,
 'string0': 'Hello world'}

a. 块作用域 - Python中不存在

什么是"块级作用域"?(其他语言 vs Python)

在很多编程语言(如 C, C++, Java, JavaScript (ES6+ with let/const))中,花括号 {} 包裹的代码块 (如 if, for, while 内部)会形成一个独立的"小房间"。

  • 在这些语言中 :你在 ifwhile 里面定义的变量,出了这个花括号就"失效"了,外面访问不到。
  • 在 Python 中没有这个"小房间"if, for, while, try-except 这些代码块不会创建新的作用域。它们内部的变量直接"泄露"到它们所在的那个外层作用域中。
python 复制代码
dict0 = globals() #返回一个字典
print(len(dict0)) #打印键值对数量
print(dict0.keys()) #打印所有的键

while True:
    # ❌ Python 不会在这里创建一个新的"while 作用域"
    block_var = "lili" 
    break

print(block_var)
print(len(dict0)) #打印键值对数量
print(dict0.keys()) #打印所有的键
>>>>>>>>>>>>>>>>>
10
dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__annotations__', '__builtins__', '__file__', '__cached__', 'dict0'])
lili
11
dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__annotations__', '__builtins__', '__file__', '__cached__', 'dict0', 'block_var'])

b. 局部作用域

通过下面的例子可见,block_var 的作用域只能在函数内部。

python 复制代码
def foo():
    block_var = "01234"
foo() # 函数执行完 -> 栈帧(Stack Frame)销毁 -> 局部变量的引用计数归零 -> 垃圾回收器(GC)回收内存。
print('block_var' in globals()) 
>>>>>>>>>>>>>>>>>>>>>>
False

实际上,Python中只有模块(module),类(class),以及函数(def, lambda)会引入新的作用域,其他代码块如(if elif/else, for/while, try/except)不引入新作用域。

c. 作用域链

用一个例子引入,你可以猜测以下输出,跟你猜测的相同。

python 复制代码
def outer():
    var0, var1 = "ABC", "DEF"

    def inner(): # 为outer的内建函数,只能在outer内使用
        var0 = "abc"
        local_var = "123"

        print(var0)
        print(var1)
        print(local_var)

    print(var0)
    inner()

outer()

变量的查找过程就像一条单向链一样,逐层向上,要么找到变量的定义,要么报错未定义。这种作用域机制称为作用域链。

d. 函数作为返回值

Python中函数名就是一个变量,它指向一个函数对象。因此可以有多个变量指向同一个函数对象。并引用它。

python 复制代码
flist = []
for i in range(3):
    def foo(x):
        print(x + i) # i是全局变量
    flist.append(foo)

for f in flist:
    f(1)
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
3
3
3

在这段代码中,我们以为函数会返回 1 2 3,但是真实的返回值却是3 3 3,为啥?

  • 循环体中的临时变量 i 作为全局变量不会销毁,它的值是 2。
  • Python中,把函数作为返回值时,函数中的全局变量的值并不会被保存,其中全局变量的值是你调用的那一刻该是多少就是多少。

如果我们想让结果为1 2 3,就需要将全局变量i变为内部函数:

python 复制代码
flist = []
for i in range(3):
    def foo(x, y = i):
        print(x + y) # y是局部变量了
    flist.append(foo)

for f in flist:
    f(1)
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
1
2
3

e. 闭包函数

闭包(closure)在Python中可以这样解释:如果在一个内部函数中,对定义它的外部函数中的变量(甚至是外层之外,只要不是全局变量)进行了引用,就可以称该内部函数为闭包函数。所以上面的inner就是个闭包函数,简称闭包。

闭包有如下显著特点:

  • 它是函数内部定义的内嵌函数。
  • 引用了作用域之外的变量,但非全局变量。

如果我们将闭包函数作为外部函数的返回值,然后后外部调用这个闭包函数会怎样呢?

python 复制代码
def offset(n):
    base = n
    def step(i):
        return base + i
    return step

offset1 = offset(1)
offset100 = offset(100)

print(offset1(1))
print(offset100(1))
>>>>>>>>>>>
2
101

奇怪了,为什么在执行完第8行代码后,base明明是局部变量,却在offset执行完毕后没有被释放呢?

答案:当内部函数引用了外部函数的局部变量时,Python会将这些变量从普通的'栈上局部变量'转换为堆上的闭包单元(cell objects),外部函数执行完毕后,其上下文虽然消失了,但由于返回的内部函数依然持有对这些cell的引用,引用计数不为0,因此这些堆上的数据不会被垃圾回收,从而实现了状态的持久化。

f. 四种作用域

Python 的作用域一共有4种,分别是:

  • L(Locals) 局部作用域 :当前函数内部
  • E(Encoding)嵌套/闭包作用域:外层函数的内部(当前函数是被包裹在另一个函数里的)
  • G(Globals)全局作用域:整个文件模块级别
  • B(Build-in)内建作用域:Python 自带的名字比如int, print, len, str等。

Python 解释器查找变量时会按照 L -> E -> G -> B 的顺序查找。

g. 作用域同名互斥性(待补充)

指的是在不同的两个作用域中,若定义了同名变量,那么高优先级的作用域不能同时访问这两个变量,只能访问其中之一。

h. nonloacl 声明

与 global 声明类似,nonlocal 声明可以在闭包中声明使用上一级作用域中的变量。

默认情况 :如果你在闭包内部直接写 i = 10,Python 会认为你在创建一个新的局部变量 i,而不会修改外层的那个 i。这时候确实看起来像"改不了"。

正确做法 :如果你明确告诉 Python "我要修改外层的变量",使用 nonlocal 关键字,就可以成功修改它!

4. 从闭包到装饰器

a. 闭包和变量

一句话:如果你希望一个变量被闭包函数引用,那你就需要保证这个变量不会再被改变。

反例:

python 复制代码
def fun():
    fun_list = []
    for i in range(3): # i 会变成 0, 1, 2
        def inner(x):
           print(i + x, end=' ') # 这里的 i 引用的是外层那个唯一的 i
        fun_list.append(inner)
    return fun_list

fun0 = fun()
# 循环结束时,i 的值定格在 2
for i in fun0:
    i(1)
# 预期:1+0, 1+1, 1+2  =>  1 2 3
# 实际:1+2, 1+2, 1+2  =>  3 3 3

在这个例子中,在inner闭包函数内部使用了上一级函数的内部变量i, 但这个i是一直变化的,所以并没有得到符合预期的结果。修正方法跟以前一样。不赘述了。

b. 装饰器的引入

Python中,闭包函数应用最多的就是装饰器(Decorator)。一个简单的日志生成例子:

python 复制代码
def func(n)
	print(f"from func(), n is {n}!", flush=True) # flush=True表示立即将打印输出

已经有func()了,现在有个新需求,希望可以记录下函数的执行日志,可以在函数中添加一行记录日志的代码来达到目的,但是函数如果很多,这样做就麻烦死了。

简单一点的方式是重新定义一个这样的函数:

python 复制代码
import logging
import sys
logging.basicConfig(
    level=logging.DEBUG,
    stream=sys.stdout  # 指定输出到 stdout
)
logging.basicConfig(level=logging.DEBUG)

def log_test(func):
    func(0)
    logging.debug(f"{func.__name__} is called")

log_test(func)
>>>>>>>>>>>>>>>>>>>>>>>
from func(), n is 0!
DEBUG:root:func is called

这样只是让达成目的所需要改动的代码量少了一丢丢,还是没解决问题。于是引入了装饰器。

5. 装饰器

装饰器的实现方式可以分为装饰器函数装饰器类。即分为使用函数或类对其他对象(通常是函数或类)进行封装(装饰)。

a. 装饰器函数

1. 无参装饰器

使用函数做装饰器的方法如下:

python 复制代码
import logging
def log(func):
    def wapper(*args, **kwargs):
        ret = func(*args, **kwargs)
        logging.debug(f"{func.__name__} is called")
        return ret
    return wapper

def func(n):
    print("from func(), n is %d!" % (n), flush=True)

func = log(func)
func(0)1 1
>>>>>>>>>>>>>>>>>>>>>>
from func(), n is 0!

上面的 wrapper() 是一个闭包,它接收一个函数做参数,并返回一个新的闭包函数。这个函数对传入的函数进行了封装,起到了装饰作用。所以包含了闭包的函数log()被称为装饰器,运用装饰器可以在函数进入和退出时执行特定操作。比如插入日志,性能测试,缓存,权限校验等场景,有了装饰器,就可以抽离出大量与函数功能无关的重复代码。

上面的还是不够简便,Python为装饰器提供了专门的语法糖@符号,无需在调用处修改函数式,只需在顶以前一行加上装饰器。

复制代码

b. 装饰器类

c. 类装饰器

d. 装饰器嵌套

e. 装饰器副作用

f. 内置装饰器

End. 各种Package用法

1. Pydantic(运行时校验/转换框架)

是什么:它是一个基于 Python 类型注解(Type Hints)的数据验证和设置管理库。它能自动校验数据、转换类型、生成文档,是 FastAPI、Django Ninja 等现代框架的核心依赖。

a.自动校验数据 - BaseModel,Field

BaseModel API 页当"字典式索引",用啥查啥:https://docs.pydantic.dev/latest/api/base_model/,对应的**中文文档**:https://docs.pydantic.org.cn/latest/api/base_model/#pydantic.BaseModel.model_copy

!NOTE

Pydantic 与 TypeDict 的区别

  • TypeDict :是静态检查,依赖mypy/Pright等静态检查工具,在写代码阶段检查。
  • Pydantic :实例化阶段检查。运行阶段检查,不需要其他的。
  • 他们都需要依赖静态类型注解。
  • BaseModel------ 数据模型的基类 ,所有 Pydantic 模型都继承自 BaseModel

    python 复制代码
    from pydantic import BaseModel, ValidationError
    
    class Person(BaseModel):
        name: str
        age: int
        active: bool
    
    data = {
        'name': "lyrix",
        'age': 123,      # 非数字字符串
        'active': True    # 既不是 true/false 也不是 1/0、on/off 等
    }
    
    # 例1:
    worker0 = Person(**data)   # 字典解包,实例化为worker0,实例化过程才会触发pydantic校验
    print(worker0)
    
    # 例2:
    worker1 = Person(name = 'lyrix', age = 456, active = False) # 实例化为worker1 触发pydantic校验
    print(worker1)
    
    # 例3:
    try:
        p1 = Person.model_validate(data)  # pydantic-v2 中的方法,显式校验 data 对象是否与 Person 类匹配
    except ValidationError as e:
        print(e)  # 会报 age、active 的校验错误
  • Field ------ 对字段进行精细化控制Field 用于给模型字段添加额外约束或元数据,比如默认值、描述、验证规则等。

    python 复制代码
    from pydantic import BaseModel, Field
    
    class Product(BaseModel):
        id : int = Field(gt=0, description="商品ID必须大于0")
        name : str = Field(min_length=2, max_length=50)
        price : float = Field(ge=0.01, default=1.0)
        tags : list[str] = Field(default_factory=list)
    
    # 示例
    prod = Product(id=101, name="Laptop", price=999.99, tags=["电子", "电脑"])
    print(prod.model_dump())
    常用参数 说明
    ... 标记这个字段没有默认值 ,必须由用户提供。 与该表下面列的default, default_factory这类设置默认值的参数语义上冲突,不能同时用。
    default 设置字段默认值
    default_factory 用函数生成 默认值 (如 list, uuid.uuid4)调用 list()函数 → 得到 []。避免可变默认参数共享问题。
    gt, ge, lt, le 数值范围限制(greater than, greater or equal...)
    min_length, max_length 字符串/列表长度限制
    pattern 正则表达式匹配(如 r'^[a-z]+ $ '
    description 字段描述(用于文档生成,如 FastAPI)

    Pydantic 判断一个字段是否必填,要看有没有默认值。比如:

    情况 是否必填?
    有类型注解 + 没有默认值 ✅ 必填
    有类型注解 + 有默认值(包括 Field(default=xxx) ❌ 可选
    Field(...) 标记了 ✅ 必填(因为 ... 表示"无默认值")
  • BaseSettings 功能在 pydantic V2 版本移到了 pydantic_settings 包中。

    python 复制代码
    from pydantic_settings import BaseSettings
    from pathlib import Path
    
    class Settings(BaseSettings):
        # 基础配置
        DEBUG: bool = False
        # 脚本相关
        SCRIPT_BASE_DIR: Path = Path("/app/scripts")
        DEFAULT_TIMEOUT: int = 30
        # 脚本白名单(逻辑名 -> 文件名)
        SCRIPT_REGISTRY: dict[str, str] = {"print_pwd": "print_pwd.sh"}
    
        model_config = {  # ✅ v2 写法, 配置环境变量前缀
            "env_prefix": "EXECUTOR_",
            "env_file": ".env",  # 支持 .env 文件(本地开发超方便)
            "extra": "ignore"  # 忽略没定义的环境变量,不报错
        }
    
    
    settings = Settings()

    作用就是,假如你在终端运行入戏指令:

    shell 复制代码
    EXECUTOR_DEBUG=true EXECUTOR_DEFAULT_TIMEOUT=100 python your_app.py

    那么:

    复制代码
    print(settings.DEBUG)           # True (bool 类型!)
    print(settings.DEFAULT_TIMEOUT) # 100 (int 类型!)
    print(settings.SCRIPT_BASE_DIR) # /app/scripts (Path 对象)

    所有类型都转换好了,不需要手动 os.getenv() + int() + Path()

b.类型转换 - BaseModel

Pydantic 不直接处理 JSON 字符串,但它能自动将 JSON 兼容的数据(如字典、列表)解析为结构化的 Python 对象,并支持反向序列化为 JSON 兼容格式。(如果提到JSON格式或JSON数据,只要提到JSON,那默认就是字符串,JSON就是特定结构的字符串

Pydantic 让你在 Python 中以类型安全、声明式的方式处理"来自 JSON 的数据"和"要转成 JSON 的数据",但它本身不读写 JSON 字符串------那是 json 模块的工作。(json.loads()json.dumps() 是 Python 标准库 json 模块中两个最核心的函数,它们的作用正好互为逆操作json.loads()用于将JSON字符串加载成Python内置数据结构,json.dumps()用于将Python数据结构释放成JSON字符串)

!IMPORTANT

难道 JSON字符串 和 Python原生数据结构一样?转来转去转个啥劲儿啊?

当你使用 json.loads()json.dumps() 来测试 JSON字符串 和 Python 原生数据结构 之间的转换并用print打印时,你可能会疑惑:这不一样吗?就一个双引号转单引号的工作?

答案:其实 JSON字符串 跟 Python原生数据结构 有天壤之别。一个是字符串。一个是内存中的数据结构。只是被print打印出来看起来一样而已。

来看看 Pydantic 如何与 JSON 协同工作

  1. 首先 FastAPI server 接收了一个 JSON字符串 请求

    复制代码
    ```json
    {
      "name": "Alice",
      "age": 30,
      "is_student": false
    }
    ```
  2. Web 框架(如 FastAPI)先用 json.loads()JSON字符串 转化为Python字典 json.loads()

  3. Pydantic 将这个字典转化为实例验证。

  4. 如果需要返回JSON,Pydantic 可返回一个JSON格式字符串 ;如果需要 Python 内部逻辑处理,Pydantic 也可也返回 Python原生字典,看下面这个完整例子:

    python 复制代码
    import json
    from pydantic import BaseModel
    
    class User(BaseModel):
        name: str
        age: int
        is_student: bool
    
    data = {"name": "Alice", "age": 30, "is_student": False}
    user = User(**data)  # ✅ 自动验证类型、转换(如 "30" → 30)
    
    print(user) # Pydantic 的 BaseModel 重写了 __repr__,使其输出一种类似 Python 函数调用的格式,仅调试使用
    print(user.model_dump()) # 返回 Python 原生字典(dict),用于 Python 内部逻辑处理,传递给其他只接受字典的函数
    print(user.model_dump_json() + "123") # 返回 JSON 格式的字符串(str)
    >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
    name='Alice' age=30 is_student=False
    {'name': 'Alice', 'age': 30, 'is_student': False}
    {"name":"Alice","age":30,"is_student":false}123

    注意:user.model_dump_json() 是 Pydantic 提供的便捷方法,内部确实调用了标准库的 json.dumps()

2. subprocess (创建管理子进程)

是python标准库中用于创建和管理子进程(即运行外部命令或程序)的核心模块。他提供了一种强大而灵活的方式:与操作系统交互。执行shell命令,启动其他程序,捕获输出,传毒输入 等。

  • a, 常用函数/方法

    1. subprocess.run()

      • 用途:最推荐使用的方式,用于运行命令并等待其完成。

      • 特点:可以捕获标准输出(stdout)、标准错误(stderr);支持超时(timeout);可抛出异常(如命令失败)

      • 示例: subprocess.run(args, capture_output=False, text=False, input=None, cwd=None, timeout=None, check=False, shell=False, env=None)

    2. subprocess.call() 基本不用啦

      • 用途:运行命令并返回退出代码 (return code)

      • 注意:不自动剖出异常;已基本被 run() 取代

    3. subprocess.check_call()基本不用啦

      • 用途:运行命令,如果返回非0退出码,则抛出 CalledProcessError,可被except捕获。

      • 等价于:subprocess.run(..., chack=True)

    4. subprocess.check_output()

      • 用途:运行命令并返回 stdout (作为字节串或字符串)

      • 注意:若命令失败会抛出异常。

    5. subprocess.Popen

      • 用途:更底层,更灵活的接口,适用于需要与子进程实时交互(如流式读取输出,发送输入)的场景。

      • 常用方法:

        • .communicate(input=None):发送输入并读取输出,等待进程结束。
        • .poll():检查进程是否结束。
        • .wait():阻塞直到进程结束。
        • .terminate() / .kill():终止进程。
  • b. 关键参数(用于上面的 函数/方法)

    参数 说明
    args 命令参数,可以是字符串(需配合 shell=True)或列表(推荐)
    shell 是否通过 shell 执行(如使用管道、重定向等);默认 False(更安全)
    stdout=subprocess.PIPE 只想看 stdout,让 stderr 直接打印到终端不会被捕获。可实现 输出逐行实时打印subprocess.Popen() + stdout=subprocess.PIPE + with 适合长时间运行需要流式输出的场景。
    stderr=subprocess.PIPE 只想看 stderr,让 stdout 直接打印到终端不会被捕获
    capture_output 若为 True,等价于同时设置: stdout=subprocess.PIPE, stderr=subprocess.PIPE subprocess.run() + capture_output=True适合短命命令、一次性获取全部输出 。 必须用 .communicate() 一次性读完所有输出,无法实现"逐行实时打印"
    text(或 universal_newlines 若为 True,以字符串而非字节形式处理输入/输出
    check 若为 True,命令 失败 (非零退出码)时 抛出异常
    cwd 设置子进程的工作目录
    env 设置环境变量(如 env=dict(os.environ, MY_VAR='value')
  • 综合示例

    下面是个综合性小例子,展示如下功能:

    • 使用 subprocess.run() 执行命令
    • 捕获 stdout 和 stderr
    • 设置工作目录和环境变量
    • 处理超时和错误
    • 使用Popen实现实时输出(可选)
    python 复制代码
    import subprocess
    import os
    import sys
    
    def demo_subprocess():
        print("=== 示例 1: 简单运行命令并捕获输出 ===")
        result = subprocess.run(
            ["echo", "Hello from subprocess!"],
            capture_output=True,
            text=True
        )
        print("stdout:", result.stdout.strip())
    
        print("\n=== 示例 2: 运行可能失败的命令(带 check)===")
        try:
            subprocess.run(["ls", "/nonexistent_dir"], check=True, capture_output=True, text=True)
        except subprocess.CalledProcessError as e:
            print(f"命令失败!退出码: {e.returncode}") # 是子进程的 退出状态码,整数,子进程结束后被设置(代码运行到这行,子进程已经结束)
            print("stderr:", e.stderr.strip())
    
        print("\n=== 示例 3: 设置环境变量和工作目录 ===")
        env = os.environ.copy()   # 直接修改原环境变量
        env["MY_VAR"] = "demo_value" # 新增个自定义环境变量
        result = subprocess.run(
            ["sh", "-c", "echo Working in $(pwd) and MY_VAR=$MY_VAR"],  # 式让 shell 解释执行一段字符串命令
            cwd="/tmp",	# 临时切换工作目录到 /tmp
            env=env,  # 传递自定义变量
            capture_output=True,  # 捕获输出
            text=True
        )
        print("输出:", result.stdout.strip()) # 打印干净结果
    
        print("\n=== 示例 4: 超时控制 ===")
        try:
            # 在 Unix-like 系统上 sleep 5 秒,但设置超时为 2 秒
            subprocess.run(["sleep", "5"], timeout=2)
        except subprocess.TimeoutExpired:
            print("命令超时!")
    
        print("\n=== 示例 5: 使用 Popen 实现实时输出(模拟长时间任务)===")
        # 注意:这里用 'ping' 或 'python -c' 模拟持续输出(跨平台兼容性有限)
        if sys.platform == "win32":
            cmd = ["ping", "-n", "4", "127.0.0.1"]
        else:
            cmd = ["ping", "-c", "4", "127.0.0.1"]
    
        with subprocess.Popen(cmd, stdout=subprocess.PIPE, text=True) as proc:
            for line in proc.stdout: # python对文本文件流的迭代规则,一次返回一行,适用于普通文本文件,标准输入,管道
                print("实时输出:", line.rstrip()) # str.rstrip() 是字符串方法,移除字符串末尾的空白字符(包括 \n, \r, 空格, 制表符等)。因为print会自带应该一个 \n
        print("Ping 结束,退出码:", proc.returncode) # 是子进程的 退出状态码,整数,子进程结束后被设置(代码运行到这行,子进程已经结束)
    
    if __name__ == "__main__":
        demo_subprocess()

3. time(时间模块)

import time 是 Python 标准库中用于处理时间相关操作 的模块,虽然功能不如 datetime 那样面向"日历时间",但它在程序计时、延迟、性能分析、简单时间戳等场景中非常常用。下面给出最实用的 5 种用法及示例:

  • time.time()获取当前时间戳(秒数),返回自 Unix 纪元(1970-01-01 00:00:00 UTC) 到现在的浮点秒数。常用于计算耗时、生成唯一 ID、日志时间戳等。

  • time.sleep(seconds)让程序暂停(休眠),常用于轮询、限流、等待资源就绪等。

  • time.ctime([seconds])将时间戳转为可读字符串

  • time.localtime([seconds]) → 本地时间,返回一个 struct_time 对象(类似命名元组),包含年、月、日、时、分、秒等字段。

  • time.gmtime([seconds]) → UTC 时间,返回一个 struct_time 对象(类似命名元组),包含年、月、日、时、分、秒等字段。

  • time.strftime(format[, t])格式化时间字符串(类似 strftime)

    复制代码
    import time
    
    # 当前时间格式化
    s = time.strftime("%Y-%m-%d %H:%M:%S")
    print(s)  # 2026-02-03 16:35:22
    
    # 自定义时间
    t = time.strptime("2025-12-25", "%Y-%m-%d")
    s2 = time.strftime("%A, %B %d, %Y", t)
    print(s2)  # Thursday, December 25, 2025

    📝 常用格式符:

    • %Y:四位年(2026)
    • %m:月(01-12)
    • %d:日(01-31)
    • %H:小时(00-23)
    • %M:分钟(00-59)
    • %S:秒(00-59)
    • %a / %A:星期缩写 / 全称
    • %b / %B:月份缩写 / 全称

一个带时间戳的小例子:

python 复制代码
import time

def log(msg):
    timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
    print(f"[{timestamp}] {msg}")

log("任务开始")
start = time.time()

for i in range(3):
    log(f"正在处理第 {i+1} 步...")
    time.sleep(0.5)

elapsed = time.time() - start
log(f"任务完成,总耗时 {elapsed:.2f} 秒")
>>>>>>>>>>>>>>>>>>>
[2026-02-03 16:40:00] 任务开始
[2026-02-03 16:40:00] 正在处理第 1 步...
[2026-02-03 16:40:01] 正在处理第 2 步...
[2026-02-03 16:40:01] 正在处理第 3 步...
[2026-02-03 16:40:02] 任务完成,总耗时 1.51 秒

4. Path(路径模块)

Path('.')/Path.cwd() 取的是"当前工作目录(CWD)",而不是"代码文件所在目录"。 Win11中Pycharm从项目根目录启动,所以 . 刚好是项目根。而 Linux 中项目代码中调用打印出来确是文件系统根目录,造成Win11中的代码在Linux中就不能用了。怎么调整一下?

python 复制代码
from pathlib import Path
import os

DEFAULT_PROJECT_ROOT = Path(__file__).resolve().parents[2] # 获取根目录的方法
PROJECT_ROOT = Path(os.getenv("PROJECT_ROOT", str(DEFAULT_PROJECT_ROOT))) # 依据环境变量获取变量值

p = PROJECT_ROOT
listp = [x for x in p.iterdir() if x.is_dir()]
print(listp)

解释

txt 复制代码
__file__:当前文件在磁盘上的绝对路径(可能是相对也可能是绝对路径)
Path(__file__).resolve() :把相对路径转成绝对路径,去掉 .、.. 等符号,(如果是软连接则返回真实路径)
parents[n] :当前文件父目录列表
os.getenv("ENV_KEY", default_value):尝试从系统环境变量读取"ENV_KEY",找不到就返回 default_value

Path 常用功能

  • 构造路径

    复制代码
    from pathlib import Path
    p = Path("app/core/config.py") # 此时p不是一个字符串,而是Path类型的变量
  • 绝对路径 resolve() ,将原路径转为绝对路径

  • 访问父目录 parents / parent

    复制代码
    parents[n]:是当前文件路径中父目录的列表(从当前文件夹一直往上)例如:
    	Path("/home/user/project/app/core/config.py").parents 等于
    	[
     	 	PosixPath('/home/user/project/app/core'),
      		PosixPath('/home/user/project/app'), = .parents[1] = .parent
      		PosixPath('/home/user/project'),  # .parents[2] → 项目根目录
      		PosixPath('/home/user'),
      		PosixPath('/home'),
      		PosixPath('/')
    	]
    
    parent为当前文件路径的父母率。
  • 拼接路径(/ 运算符,最常用

    复制代码
    p = Path("/home/user") / "scripts" / "run.sh"
  • 判断文件/目录

    复制代码
    p.is_file() # 判断是否是文件?
    p.is_dir() # 判断是否是路径?
  • 遍历目录(没明白)

    复制代码
    for x in Path("scripts").iterdir():
        print(x)

    筛选文件:

    复制代码
    [x for x in Path("scripts").iterdir() if x.is_file()]
  • 读取/写入文件(这个跟python标准包中的 open, read write 什么关系?)

    复制代码
    content = Path("readme.txt").read_text()
    Path("out.txt").write_text("hello")
  • 创建目录

    复制代码
    Path("logs").mkdir(exist_ok=True)
  • 查找文件 glob / rglob

    复制代码
    for py in Path("app").rglob("*.py"):
        print(py)
  • 获取文件名和扩展名

    复制代码
    p.name       # config.py
    p.stem       # config
    p.suffix     # .py

5. Logging(日志模块)

参考学习博客:Python Logging模块的高级用法、最佳实践与性能优化

!NOTE

Python提供了内置的Logging模块,使得日志记录变得简单而强大。首先提纲挈领的了解一下概念:

Logger(日志器):决定"要不要记录"

Handler(处理器):决定日志最终输出到哪里(终端、文件、网络)

Formatter(格式器):决定"日志长什么样"

🟦 1. Logger(日志器)到底是什么?

想象你公司有不同部门:研发、前端、后端、运维...

每个部门都有一个"消息发布入口"。

在 Logging 里,每个模块(module.py)都会拿一个日志器:

复制代码
logger = logging.getLogger(__name__)

__name__ 是模块名,这样日志才可以区分:

  • project.api.user
  • project.api.order
  • project.database
  • ...

Logger 的职责是

  1. 决定"日志能否继续往下走"(通过级别过滤)
  2. 把日志派发给它绑定的 Handler

👉 Logger 本身 不负责输出,只是一个"交通岗"。

🟪 2. Handler(处理器)是什么?

Handler 决定日志最终到哪里去。

常见 Handler 有:

  • StreamHandler → 输出到终端(stdout)
  • FileHandler → 输出到文件
  • RotatingFileHandler → 自动切分日志文件
  • SMTPHandler → 邮件发送
  • HTTPHandler → 发送到日志服务器
  • ...

一个 Logger 可以挂 多个 Handler,因此 1 条日志可以:

  • 同时显示在控制台
  • 同时写入文件
  • 同时发送到监控系统

处理器也有自己的级别过滤,例如:level=INFO ,这意味着,哪怕logger是Debug,这条Handler仍然会过滤掉 DEBUG。因此,日志想要输出要满足首先 Logger 允许,然后 Handler 允许。

🟨 3. Formatter(格式器)是什么?

Formatter 决定日志"长什么样"。

比如:2026-02-27 13:22:19,894 - INFO - app.module - login OK长这样的一个log输出由如下格式字符串决定:

复制代码
format=%(asctime)s - %(levelname)s - %(name)s - %(message)s

还有如下占位符:

占位符 含义
%(asctime)s 日志时间
%(levelname)s 日志级别 - 常用
%(name)s 日志器名(模块名)- 常用
%(filename)s 文件名
%(funcName)s 函数名- 常用
%(lineno)d 行号- 常用
%(message)s 具体日志内容

我自用的格式:

ini 复制代码
[formatter_sampleFormatter]
format=[%(asctime)s] <%(levelname)s> %(name)s.%(funcName)s:`%(lineno)d` : %(message)s
datefmt=%H:%M:%S

a. 日志级别

Logging 模块支持以下几个日志级别:

级别 含义
DEBUG 用于详细的调试信息
INFO 用于确认应用程序的正常运行
WARNING 用于指示潜在的问题,但不影响应用程序的正常工作
ERROR 用于指示应用程序中的错误,可能影响部分功能的正常运行
CRITICAL 用于指示严重错误,可能导致应用程序崩溃

使用方法:

python 复制代码
import logging

logging.basicConfig(level=logging.DEBUG)

logging.debug('这是一个DEBUG级别的日志信息')
logging.info('这是一个INFO级别的日志信息')
......

b. 格式化

Logging模块允许开发者对日志进行格式化,以便更好的理解和分析日志内容。可以在日志处理器中指定格式化字符串,其中可包含特定的占位符,如日志级别,时间戳,模块名等。可以在代码中像下面这样用。也可以写在配置文件中,下面会讲。

python 复制代码
import logging
logging.basicConfig(format='%(asctime)s - %(levelname)s - %(message)s', level=logging.DEBUG)
logging.debug('这是一个DEBUG级别的日志信息')

c. 处理程序 handle

Logging模块支持将日志信息发送到不同的处理程序,例如文件,控制台,网络等。通过添加不同的处理程序,可以根据需要将日志信息发送到不同的目的地。(我暂时用不到,只要能立马打印出我想要打印的级别就行了)

d. 过滤器

Logging模块还提供了过滤器的功能,可以根据需求对日志信息进行筛选和过滤。过滤器可以基于日志级别、模块名等条件来过滤日志信息,使得日志记录更加精确和有效。

e. 配置文件(有用,方便)

该模块支持通过logging.config.fileConfig从指定配置文件中加载配置信息,这样就不用修改代码了。

复制代码
# logging.conf
[loggers]
keys=root

[handlers]
keys=consoleHandler

[formatters]
keys=sampleFormatter

[logger_root]
level=DEBUG
handlers=consoleHandler

[handler_consoleHandler]
class=StreamHandler
level=DEBUG
formatter=sampleFormatter
args=(sys.stdout,)

[formatter_sampleFormatter]
format=%(asctime)s - %(levelname)s - %(message)s

解读一下logging.conf文件中的内容,你可以把带中括号的东西提出来:

loggers\] ---------------- \[handlers\] ------------------------------- \[formatters

\[logger_root\] ---------- \[handler_consoleHandler\] ---------- \[formatter_sampleFormatter

我想你已经察觉了这两行的关系,结合文件内容,你肯定发现了,先让前三个[loggers] [handlers] [formatters]指定日志器,处理器和格式器。然后根据其内容得到 [[logger_root] [handler_consoleHandler] [formatter_sampleFormatter],并进行详细设置。

复制代码
[loggers]:keys=root只定义了一个日志器,叫 root(根日志器)。
[handlers]:keys=sampleFormatter:只定义了一个格式器,名字叫 sampleFormatter。
[formatters]:格式
[logger_root] 
	level=DEBUG 中定义日志级别为debug
	handlers=consoleHandler:把上面的 consoleHandler 处理器挂到 root 上。
[handler_consoleHandler]
	class=StreamHandler : 把日志写到流(stdout/stderr)的处理器。
	level=INFO:处理器自己的级别是 INFO,这意味着即使日志器允许 DEBUG,处理器这边也会把 DEBUG 给挡掉。
	formatter=sampleFormatter:用哪个格式器来格式化日志文本。
	args=(sys.stdout,):把日志写到 sys.stdout。注意:如果你写了 sys.stdout,通常需要在 fileConfig 时传入 defaults={'sys': sys}
[formatter_sampleFormatter]
	format=%(asctime)s - %(levelname)s - %(message)s:日志输出格式为"时间 - 级别 - 内容"。
	说明:%(asctime)s 是"时间戳",默认很长(包括日期)。你希望"只要时:分:秒",可以通过 datefmt 控制。
python 复制代码
# 使用配置文件进行日志配置
import logging
import logging.config

# 建议放到应用入口
logging.config.fileConfig('logging.conf') # 从一个 INI 格式的配置文件加载日志配置(定义了日志器,处理器,格式器)

logger = logging.getLogger(__name__) # 获取一个以当前模块名为名字的日志器

logger.debug('这是一个DEBUG级别的日志信息')
logger.info('这是一个INFO级别的日志信息')

!NOTE

logging.conf 文件需要放在你执行命令时的"当前工作目录" (通常就是项目根目录),否则logging.config.fileConfig('logging.conf')会找不到配置文件,例如当你执行python -m app.main时,app所在目录就是当前工作目录。logging.conf 文件要放在与app同级目录。

f. 日志轮转(待补充)

g. 日志归档(待补充)

h. 自定义处理程序(待补充)

自定义的目的地,例如数据库、消息队列等,以满足特定场景下的日志记录需求。(如果我想要分析用户的使用数据,可能用的到)

6. NumPy(科学计算基础库)

什么是 NumPy?

​ NumPy 是一个免费的 Python 编程语言开源库 Numpy教程|菜鸟教程,它功能强大、已经过充分优化,并增加了对大型多维数组(也称为矩阵或张量)的支持。NumPy 还提供了一系列高级数学函数,可与这些数组结合使用。其中包括基本的线性代数、随机模拟、傅立叶变换、三角运算和统计运算。旨在为 Python 提供快速的数字计算。本文将系统梳理 NumPy 在 AI 项目中的几类常用操作。(Sklearn 是基于该库的更高级的机器学习库,能处理包括分类、回归、聚类、降维在内的机器学习任务。具体参考网站:scikit-learn文档scikit-learn中文文档Sklearn 简介 | 菜鸟教程。)

7. argparse(参数解析)

argparse是Python标准库中用于解析命令行参数的模块,提供了简单的方式处理用户输入,帮助用户构建有参数选项和帮助信息的命令行界面。

主要组成分四部分:

  • 创建 ArgumentParser 对象:创建命令行解析器对象。
  • 使用 add_argument() 方法:向解析器中添加命令行参数和相关属性。
  • 调用 parse_args() 方法:用于解析命令行参数,该方法会返回一个*"命名空间对象"*,其包含了解析后的参数和对应的值
  • 使用*"命名空间对象"*中的属性获取命令行参数的值。

看个例子:

python 复制代码
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("version", help="the version you used")
parser.add_argument("square", help="display a square of a given number", type=int)
parser.add_argument("-b", "--board", help="the board you used")
parser.add_argument("-d", "--debug", help="whether turn on the debug")
parser.add_argument("-v", "--verbose",  help="increase output verbosity",
                    action="store_true")
parser.add_argument("-f", "--file", default="knowledge_base.json")
args = parser.parse_args()
print(args.version)
print(args.square**2)
print(args.board) # 在获取命令行参数值时只能用长参数 
print(args.debug)
print(args.verbose)
print(args.file)

看测试:

python 复制代码
usage: test_learning.py [-h] [-b BOARD] [-d DEBUG] [-v] [-f FILE] version square

positional arguments:
  version               the version you used
  square                display a square of a given number

options:
  -h, --help            show this help message and exit
  -b BOARD, --board BOARD
                        the board you used
  -d DEBUG, --debug DEBUG
                        whether turn on the debug
  -v, --verbose         increase output verbosity
  -f FILE, --file FILE

可见,带---前缀的参数都是可选参数,只不过--后写长参数名,-后写与之等价的短参数名。help参数是包自带的其他的是程序员写的。

action决定了参数如何被存储和处理:

  1. 默认action="store"

  2. store_true / store_false 布尔值,不需要转递值

  3. store_const存储常量值

  4. append允许使用多次,追加到列表

  5. append_const每次出现追加一个常量

  6. count计数,记录参数出现次数

  7. version显示版本信息后退出

  8. 用法示例:

    python 复制代码
    ...
    # store(默认)- 存储值
    parser.add_argument("-n", "--name", help="your name")
    # store_true - 布尔标志
    parser.add_argument("-v", "--verbose", action="store_true", help="verbose mode")
    # count - 计数
    parser.add_argument("-d", "--debug", action="count", default=0, help="debug level")
    # append - 追加列表
    parser.add_argument("-f", "--file", action="append", help="input files")
    # store_const - 存储常量
    parser.add_argument("--max", action="store_const", const=100, help="use max value")
    # version
    parser.add_argument("--version", action="version", version="1.0.0")
    ...
    >>>>>>>>>>>>>>>>>>>>>>>>
    # 运行 python example.py -n Alice -v -ddd -f a.txt -f b.txt --max
    args.name = "Alice"        		# store
    args.verbose = True        	# store_true
    args.debug = 3             		# count (出现3次)
    args.file = ["a.txt", "b.txt"]     # append
    args.max = 100             		# store_const
相关推荐
Dfreedom.11 小时前
Scikit-learn 全景解读:机器学习的“瑞士军刀”
python·机器学习·scikit-learn
长乐无暇11 小时前
第18天:for 循环与 range()
后端·python
lkforce11 小时前
MiniMind学习笔记--安装部署
笔记·python·学习·minimind
Cachel wood11 小时前
Macbook M4 pro本地部署大模型|Ollama+Gemma4/Qwen3.5
人工智能·python·自动化·llm·qwen·ollama·gemma4
whitelbwwww11 小时前
标准模板库--STL库
开发语言·c++
枫叶丹411 小时前
【HarmonyOS 6.0】ArkWeb嵌套滚动快速调度策略
开发语言·华为·harmonyos
努力学习的小廉11 小时前
Python 零基础入门——基础语法(二)
android·开发语言·python
YSyuanshuo11 小时前
2026滴鸡精品牌指南:羽本元如何用技术革新挑战传统老牌?
大数据·python