跳到主要内容

4 篇博文 含有标签「Python」

查看所有标签

如果你也是一名曾在 Rust 的强类型和所有权机制中找到安全感的开发者,或者是习惯了 Data Class + Function 这种数据与行为分离模式的后端工程师,当你回到 Python 的动态世界时,可能会有一种“裸奔”的不安感。

字典(Dict)满天飞,类型提示(Type Hint)形同虚设,运行时的 KeyError 像一颗颗地雷。

在我的技术栈中,Pydantic 早就不再仅仅是一个简单的“数据验证库”。它是 Python 通往“强类型”和“结构化思维”的唯一桥梁,是它让 Python 拥有了类似 Rust Struct 的严谨。

要把 Pydantic 用好,核心是将它视为 系统边界的守门人。以下是如何在项目中“重度”且“优雅”地使用 Pydantic 的 5 个层级。


层级 1:消灭字典传参 (The Death of Dict)

在传统的 Python 代码中,字典是数据传递的通用货币。这其实是维护的噩梦:你永远不知道 data['user_id'] 到底是 str 还是 int,甚至不知道这个 Key 是否存在。这被称为“Stringly Typed”编程。

做法:强制规定,系统边界以内,严禁裸奔的字典。

所有进入函数的复杂数据,必须在入口处转换为 Pydantic Model。

❌ 修改前 (裸奔的字典)

def process_data(data: dict):
# 没有任何 IDE 提示,如果不看代码实现,不知道 data 里有什么
# 容易因为拼写错误导致运行时崩溃
if data.get('status') == 'active':
return data['items']

✅ 修改后 (Rust 风格的 Struct)

from pydantic import BaseModel, Field
from typing import List, Literal

# 定义数据形状 (Data Shape)
class Item(BaseModel):
name: str
price: float

class Payload(BaseModel):
# 使用 Literal 做枚举约束,类似 Rust 的 enum
status: Literal['active', 'inactive']
items: List[Item] = Field(default_factory=list)

# 函数签名即文档,IDE 甚至能补全 .status
def process_data(payload: Payload) -> List[Item]:
if payload.status == 'active':
return payload.items
return []

收益:你获得了类似静态语言的编译期(IDE 静态检查)安全感。你的函数签名不再撒谎。


层级 2:配置管理 (Rust Config 风格)

做后端和 AI 开发,经常需要读取 API Keys, DB Host, Model Names。许多人习惯用 os.getenv() 甚至硬编码,导致配置分散且不安全。

做法:使用 pydantic-settings

这非常像 Rust 的 Config crate,它将环境变量一次性加载为类型安全的对象。如果配置缺失或类型错误,程序启动即崩溃(Fail Fast),而不是在半夜运行到那行代码时才报错。

from pydantic_settings import BaseSettings

class AppConfig(BaseSettings):
openai_api_key: str
db_host: str = "localhost"
db_port: int = 5432
debug_mode: bool = False

class Config:
env_file = ".env" # 自动读取 .env 文件

# 初始化一次,全局使用单例
config = AppConfig()

# 使用时,完全类型安全
print(config.db_port + 1) # IDE 知道这是 int,完全放心的数学运算

收益:将配置加载从“运行时风险”变成了“启动时检查”。


层级 3:清洗即解析 (Parse, don't validate)

这是一个非常 Functional Programming (FP) 的理念:不要把验证看作是“检查”,而要看作是“数据变换”。

传统的验证是:给我数据 -> 我看看对不对 -> 抛出错误。 解析的思维是:给我数据 -> 我把它变成我要的格式 -> 给我干净的对象。

Pydantic 的 field_validator 允许你在数据实例化的瞬间清洗数据。

场景:AI 训练数据清洗

输入可能是 " 123 ", "123", 或 123,或者是逗号分隔的字符串,但你的领域对象需要统一的 List[str]

from pydantic import BaseModel, field_validator

class UserInput(BaseModel):
tag_list: list[str]

@field_validator('tag_list', mode='before')
@classmethod
def split_string_tags(cls, v):
# 脏活累活在这里处理
# 如果输入是 "a,b,c",自动切分为 ['a', 'b', 'c']
if isinstance(v, str):
return [x.strip() for x in v.split(',')]
return v

# 即使上游传了脏数据,这里也能自动清洗
data = UserInput(tag_list=" ai, python, rust ")
print(data.tag_list)
# Output: ['ai', 'python', 'rust']

收益:你的核心业务逻辑(Function)永远只处理干净、标准的数据。


层级 4:AI 结构化输出 (Structured Output)

这是目前 Pydantic 最前沿 的用法。作为 AI 开发者,你一定遇到过 LLM 输出 JSON 格式不稳定的问题(比如多了一个逗号,或者 key 拼错)。

做法:将 Pydantic 作为 Schema 协议,强制 LLM 输出符合你定义的数据结构。

这在 OpenAI 的 Function Calling 或 instructor 库中是核心模式。

import instructor
from openai import OpenAI
from pydantic import BaseModel

# 1. 定义你想要的结果结构
class UserInfo(BaseModel):
name: str
age: int
interests: list[str]

# 2. Patch 你的 OpenAI 客户端
client = instructor.from_openai(OpenAI())

# 3. 直接请求 Pydantic 对象,而不是文本
user_info = client.chat.completions.create(
model="gpt-4o",
response_model=UserInfo, # 关键:告诉 LLM 我只要这个结构
messages=[
{"role": "user", "content": "张三今年25岁,喜欢Rust和AI"}
],
)

# 4. 拿到的是真正的 Python 对象,不是 dict,也不是 json string
print(user_info.interests) # ['Rust', 'AI']

收益:将非结构化的自然语言(LLM Output)直接固化为代码可用的结构体。这是构建 AI Agent 的基石。


层级 5:作为数据库 Schema (SQLModel)

既然你用 FastAPI,你可能听说过 SQLModel。它是由 FastAPI 作者 Tiangolo 开发的,本质上就是 Pydantic + SQLAlchemy 的合体。

这意味着同一个 Class,既是:

  1. API 验证层 (Pydantic)
  2. 数据库表定义 (SQLAlchemy Table)
  3. 业务数据容器 (Data Class)
from sqlmodel import SQLModel, Field

# 一个类,打通前后端和数据库
class Hero(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
name: str
secret_name: str
age: int | None = None

收益:消除了 DTO (Data Transfer Object) 和 DAO (Data Access Object) 之间的重复定义。极其适合不喜欢写重复样板代码(Boilerplate)的开发者。


总结:你的“Pydantic 进化论”

从脚本小子到架构师,Pydantic 的使用深度折射了你的编程思维变化:

维度以前的做法 (Pythonic / Scripting)你的新做法 (Rust-like / Engineering)
核心思维只要代码能跑就行结构化思维,类型安全
函数传参def f(d: dict)def f(d: MyModel)
配置管理os.environ.get() 拼凑class Settings(BaseSettings)
错误处理运行时随缘报错Fail Fast (启动即检查)
数据清洗函数内手写 if/else@field_validator (Parse, don't validate)
AI 交互Prompt 工程 + 正则提取response_model=MyModel
整体架构逻辑与数据耦合Data Class (Pydantic) + Function (Logic)

一句话建议

在你的下一个 Python 项目中,尝试禁止在函数参数和返回值中使用 dict

强迫自己定义 Pydantic Model 来承载一切数据流动。你会发现,虽然写代码时的按键次数变多了(定义类),但调试代码的时间和运行时崩溃的概率会呈指数级下降。这就是结构化的力量。

鱼雪

YOLO(You Only Look Once)是一种广泛使用的目标检测模型,近年来也逐渐应用于图像分割和姿态估计任务。本篇文章将详细讲解YOLO模型在目标检测、图像分割及姿态估计中的应用,通过代码和预测结果分析帮助您更好地理解和使用YOLO模型。

Ultralytics库的所有预测结果都放在Result对象中,适用于目标检测、图像分割和姿态估计等任务,本文也将详细介绍如何处理不同任务的预测结果。

任务概述与对比

YOLO支持三种主要视觉任务,每个任务都有其独特的输出结构和应用场景:

  1. 目标检测(Object Detection)

    • 输出:边界框(boxes)和类别标签
    • 特点:定位物体位置并进行分类
    • 应用场景:物体识别、车辆检测、人脸检测等
  2. 图像分割(Image Segmentation)

    • 输出:像素级别掩码(masks)和类别标签
    • 特点:提供物体精确的轮廓信息
    • 应用场景:医学图像分析、场景理解等
  3. 姿态估计(Pose Estimation)

    • 输出:人体关键点坐标(keypoints)和骨架连接
    • 特点:识别人体姿态和动作
    • 应用场景:运动分析、姿态追踪、行为监控等

YOLO模型的预测结果对象结构

所有任务的预测结果都封装在Results对象中,Results对象包含以下通用属性:

- orig_img: 原始图像数据
- orig_shape: 原始图像尺寸(,)
- path: 输入图像路径
- save_dir: 结果保存路径
- speed: 预测耗时信息

这些属性帮助我们在不同任务中标准化处理预测结果。

目标检测

目标检测的代码实现

下面的代码演示了如何使用YOLO进行目标检测,识别图像中的物体,并将检测结果(包括边界框和类别标签)绘制在原始图像上。

import os
from ultralytics import YOLO
import cv2
import os
import glob
import shutil

OBJECT_DETECTION_MODEL_PATH = './models/object_detection.onnx'
TASK_NAME = 'detect'

def generate_colors(names):
colors = {}
for name in names:
hash_object = hashlib.md5(name.encode())
hash_int = int(hash_object.hexdigest(), 16)
b = (hash_int & 0xFF0000) >> 16
g = (hash_int & 0x00FF00) >> 8
r = hash_int & 0x0000FF
colors[name] = (b, g, r) # OpenCV 使用 BGR 顺序
return colors

# 单张图像目标检测预测
def predict_single_image_by_detect(image_path, out_image_file):
# 获取输出文件`out_image_path`文件所在的目录
out_dir = os.path.dirname(out_image_path)
os.makedirs(out_dir, exist_ok=True)

image_list = [image_path]
results = model(image_list)

for result in results:
boxes = result.boxes
if boxes is None:
cv2.imwrite(out_image_file, result.orig_img)
continue
boxes_data = boxes.data.cpu().numpy()
names = result.names
class_names = list(names.values())

color_map = generate_colors(class_names)

img = result.orig_img

for box in boxes_data:
x1, y1, x2, y2, score, class_id = box
x1, y1, x2, y2 = map(int, [x1, y1, x2, y2])
class_name = names[int(class_id)]
color = color_map[class_name]
cv2.rectangle(img, (x1, y1), (x2, y2), color, 2)
label = f'{class_name} {score:.2f}'
cv2.putText(img, label, (x1, max(y1 - 10, 0)),
cv2.FONT_HERSHEY_SIMPLEX, 0.9, color, 2)
print(f"图像写入路径: {out_image_file}")
cv2.imwrite(out_image_file, img)

if __name__ == '__main__':
# 预测单张图像
image_path = 'bus.jpg'
out_image_path = image_path + '_predicted.jpg'
predict_single_image_by_detect(image_path, out_image_path)

目标检测结果分析

在目标检测任务中,Results对象中最重要的字段是:

  • boxes:包含边界框的坐标、置信度和类别ID。
  • names:类别标签映射。
  • orig_img:原始图像数据。

每个边界框包含以下六个值:

[x1, y1, x2, y2, score, class_id]
# x1, y1: 左上角坐标
# x2, y2: 右下角坐标
# score: 检测置信度
# class_id: 类别ID

图像分割

图像分割的代码实现

图像分割任务比目标检测更加精细,它不仅需要识别物体的类别,还要提取每个物体的准确轮廓。

import os
import hashlib
import cv2
import numpy as np
from ultralytics import YOLO
import glob
import shutil

SEGMENT_MODEL_PATH = "./models/segmentation.onnx"
TASK_NAME = 'segment'
model = YOLO(SEGMENT_MODEL_PATH, task=TASK_NAME)

# 单张图像的分割模型预测函数
def predict_single_image_by_segment(image_path, out_image_path):
out_dir = os.path.dirname(out_image_path)
os.makedirs(out_dir, exist_ok=True)

results = model.predict(source=image_path)

for result in results:
if result.masks is None:
cv2.imwrite(out_image_path, result.orig_img)
continue
masks = result.masks.data.cpu().numpy()
boxes = result.boxes.data.cpu().numpy()
label_map = result.names
color_map = generate_colors(label_map.values())

img_with_masks = result.orig_img.copy()

for i, mask in enumerate(masks):
mask = mask.astype(np.uint8)
mask = cv2.resize(mask, (result.orig_shape[1], result.orig_shape[0]))

color = np.random.randint(0, 255, (3,), dtype=np.uint8)
colored_mask = np.zeros_like(result.orig_img, dtype=np.uint8)
colored_mask[mask > 0] = color

img_with_masks = cv2.addWeighted(img_with_masks, 1, colored_mask, 0.5, 0)

box_data = boxes[i]
x1, y1, x2, y2 = map(int, box_data[:4])
class_name = label_map[int(box_data[5])]
score = box_data[4]
cv2.rectangle(img_with_masks, (x1, y1), (x2, y2), color_map[class_name], 2)
label = f"{class_name}: {score:.4f}"
cv2.putText(img_with_masks, label, (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2)

cv2.imwrite(out_image_path, img_with_masks)
print(f"Prediction saved to {out_image_path}")

if __name__ == '__main__':
image_path = 'bus.jpg'
out_image_path = image_path + '_segmented.jpg'
predict_single_image_by_segment(image_path, out_image_path)

图像分割结果分析

Results对象的特有字段:

  • masks:实例分割掩码数据。
  • boxes:边界框信息。
  • names:类别标签映射。

掩码数据为二值化图像,需调整到与原图相同的尺寸,并与原图叠加进行可视化。

姿态估计

姿态估计的代码实现

姿态估计的目标是检测人体的关键点,并根据关键点绘制出人体骨架。

import cv2
from ultralytics import YOLO
import os

POSE_MODEL_PATH = './models/pose.onnx'
TASK_NAME = 'pose'
model = YOLO(POSE_MODEL_PATH, task=TASK_NAME)

def predict_single_image_by_pose(image_path, out_image_path):
out_dir = os.path.dirname(out_image_path)
os.makedirs(out_dir, exist_ok=True)

results = model.predict(source=image_path)

for result in results:
if result.keypoints is None:
continue
if result.boxes is None:
continue

orig_img = result.orig_img
keypoints = result.keypoints.data.cpu().numpy()
boxes = result.boxes.data.cpu().numpy()

for box_data, kpts in zip(boxes, keypoints):
for keypoint in kpts:
x, y, score = keypoint
cv2.circle(orig_img, (int(x), int(y)), 3, (255, 0, 0), -1)

for connection in skeleton:
part_a, part_b = connection
if kpts[part_a][2] > 0.5 and kpts[part_b][2] > 0.5:
x1, y1 = int(kpts[part_a][0]), int(kpts[part_a][1])
x2, y2 = int(kpts[part_b][0]), int(kpts[part_b][1])
cv2.line(orig_img, (x1, y1), (x2, y2), (0, 255, 255), 1)

cv2.imwrite(out_image_path, orig_img)

if __name__ == '__main__':
image_path = 'bus.jpg'
out_image_path = image_path + '_posed.jpg'
predict_single_image_by_pose(image_path, out_image_path)

姿态估计结果分析

  • keypoints:包含人体关键点坐标和置信度。
  • boxes:人体检测框。
  • names:通常为'person'类别。

每个关键点包含以下数据结构:

[x, y, confidence]  # 每个关键点包含坐标和置信度

实践建议

  1. 数据预处理

    • 确保输入图像尺寸适合模型。
    • 检查图像格式(OpenCV通常使用BGR格式)。
    • 视需要进行图像增强。
  2. 结果处理注意事项

    • 始终进行空值检查。
    • 将tensor数据转换为numpy格式。
    • 坐标值转换为整数,确保OpenCV兼容性。
  3. 性能优化

    • 尽量批量处理图像以提高效率。
    • 使用GPU加速推理过程。
    • 根据实际需求选择合适的模型大小。
  4. 可视化建议

    • 为不同类别分配固定颜色,以便更好区分。
    • 调整线条的粗细和标签字体大小,保持预测结果可读性。

总结

YOLO在目标检测、图像分割和姿态估计三大任务中的表现令人印象深刻,模型的高度通用性使其成为计算机视觉领域中的热门选择。

  1. 数据结构差异

    • 目标检测:处理boxes数据。
    • 图像分割:同时处理masks和boxes。
    • 姿态估计:处理关键点(keypoints)和骨架结构。
  2. 应用场景

    • 目标检测:适用于物体定位和分类。
    • 图像分割:适用于精确轮廓分析。
    • 姿态估计:适用于人体动作追踪与行为分析。
  3. 通用处理流程

    • 模型加载与初始化。
    • 数据预处理。
    • 结果处理与可视化。
    • 错误与异常检查。
鱼雪

Rye 是一个现代化的 Python 包管理工具,旨在简化 Python 项目的管理和构建流程。 它提供了更快的依赖解析和更简单的项目配置方式。 本文将带您了解如何使用 Rye 来管理您的 Python 项目。

在我们深入介绍安装和基本使用指南之前,重要的是让您了解Rye实际上是什么。 Rye是一个一站式工具。其理念是,作为Python开发人员,您需要了解的只有Rye, 因为Rye是您进入体验的起点。

作为Rye用户,您甚至不需要自己安装Python,因为Rye会替您完成这一切。 这意味着要使用Rye,您只需要安装Rye,其他工作由Rye自己完成。

一旦Rye安装在您的系统上,它可以为您自动安装Python解释器,可以指定版本, 从软件包索引安装软件包,管理虚拟环境并进行幕后管理等。

工具对比

工具优势劣势
Rye- 自动管理虚拟环境
- 依赖解析速度快
- 简化项目初始化和配置
- 相对较新
- 社区支持和生态系统尚未完全成熟
Pip- 简单
- 广泛支持
- 与 PyPI 的直接集成
- 缺乏内置的虚拟环境管理
Conda- 强大的环境管理
- 支持多语言
- 适合科学计算
- 较大的安装包
- 可能引入不必要的复杂性
Poetry- 强大的依赖管理
- 简化的项目配置
- 在大型项目中速度较慢
Pipenv- 自动管理虚拟环境
- 简化的依赖管理
- 速度较慢
- 与其他工具集成性较差

安装 Rye

Rye安装有一个优点,那就是:不需要提前安装Python环境。 不像pippipenvpoetry等工具,需要提前安装Python环境。

虽然Anaconda/MiniConda也不需要提前安装环境,有一些包做了优化加速(比如Numpy之类的), 但是Anaconda系列的包管理工具比较大,不适合所有场景。尤其是Anaconda有一个缺点就是: Anaconda不好升级,导致不能指定高于某个版本的Python环境。

当然Rye也有缺点,就是比较新,可能不像pip那样稳定和广泛支持。 但是Rye的一大好处就是可以严格管理Python依赖包的版本,并且可以将开发依赖和生产依赖分开。 脚本启动等功能,也是Rye的一大特色。(由于Rye是Rust编写,所以,可能是受到Rust的Cargo工具的启发,我猜是这样)

Linux/MacOS安装方式

curl -sSf https://rye.astral.sh/get | bash

Windows上安装Rye

安装连接

安装完成后,您可以通过运行 rye --version 来验证安装是否成功。

创建新项目

使用 Rye 创建新项目非常简单。使用init子命令即可,可以指定init更多的选项,比如指定Python版本等。

只需在命令行中运行以下命令:

rye init your_project_name
# 或
rye init your_project_name --py=3.11 # 指定 Python 版本
# 或
rye init your_project_name -p=3.11 # 简写,指定Python版本

这将创建一个名为 your_project_name 的新目录,并在其中初始化一个新的 Python 项目。

添加依赖

Rye 使得添加和管理项目依赖变得非常简单。要向项目添加依赖,可以使用以下命令:

rye add diffusers
# 或
rye add diffusers --dev # 添加开发依赖
# 或
rye add diffusers==0.30.0 # 指定版本
# 或
rye add "diffusers[torch]" # 指定依赖包的额外功能

这将把 diffusers 包添加到项目的依赖中,并自动更新项目的依赖文件。

移除依赖

如果您需要移除某个依赖,只需运行:

rye remove diffusers

这将从项目中移除 diffusers 包,并更新依赖文件。

管理虚拟环境

Rye 自动为每个项目创建和管理虚拟环境。 您无需手动激活或管理虚拟环境,Rye 会在需要时自动为您处理只要进入到创建的项目目录中,Rye会自动激活虚拟环境。可以执行python -V检查Python版本。

Rye虚拟环境

构建和发布

Rye 还提供了简单的构建和发布工具。要构建项目,可以使用:

rye build

构建完成后,您可以通过以下命令发布项目:

rye publish

确保您已配置好发布所需的凭据。

运行项目脚本

要运行脚本,需要做以下几步:

  1. 需要配置pyproject.toml文件中的scripts字段。
[project.scripts]
my-hello-script = 'hello:main'
  1. 创建脚本文件hello.py,并在其中定义main函数。
def main():
print("Run Rye main")
提示

这里需要注意,要把hello.py文件放在项目的根目录下的src的目录下。

  1. 运行项目脚本
rye run my-hello-script

这里的my-hello-script就是pyproject.toml文件中的scripts字段中的my-hello-script

在项目中使用Python直接运行脚本

由于进入了rye init创建的项目后,就会默认激活虚拟环境,所以,可以直接使用Python运行脚本。

比如,创建一个demo.py文件,内容如下:

def main():
print("Hello Rye")

运行方式:

python demo.py
提示

只要*.py放在项目根目录下,或者项目的其他目录下,都可以直接使用Python运行脚本。

Rye环境中使用python运行脚本

总结

Python 的包管理器一直都是一个痛点问题。Rye 是一个新的工具,它试图解决这个问题。

Rye 是一个强大的工具,为 Python 开发者提供了简洁高效的包管理体验。 通过简化依赖管理、自动化虚拟环境以及提供便捷的构建和发布功能, Rye 帮助开发者更专注于编写代码,而不是管理项目配置。

希望这篇指南能帮助您更好地使用 Rye 来管理您的 Python 项目!如果您有任何问题或建议,欢迎在评论区留言。

鱼雪

使用Poetry做Python的项目管理工具,集成到Github Actions,包含以下内容:

  • Check out 代码仓库
  • 启动Python环境,可以限制版本,也可以使用多个版本
  • 安装Poetry
  • 设置虚拟环境缓存
  • 安装依赖
  • 代码格式化(yapf)
  • 代码静态类型检查(pytype)
  • 代码测试,以及覆盖率测试报告

Github Action workflow 配置如下:

name: CI
on: push

jobs:
ci:
strategy:
fail-fast: false
matrix:
python-version: ["3.8.5"]
poetry-version: ["1.2.2"]
os: [ubuntu-18.04]
runs-on: ${{ matrix.os }}
steps:
- name: Check out repository
uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install Poetry
uses: snok/install-poetry@v1
with:
virtualenvs-create: true
virtualenvs-in-project: true
installer-parallel: true
- name: Load cached venv
id: cached-poetry-dependencies
uses: actions/cache@v3
with:
path: .venv
key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }}

- name: Install Dependencies
if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
run: poetry install --no-interaction --no-root

- name: Code Format
run: poetry run yapf

- name: Type Check
run: poetry run pytype --config=pytype.cfg

- name: Testing and coverage
run: poetry run pytest --cov
鱼雪