前端转 AI 100 天 Day 18:pytest 入门——为什么你必须要写测试

前端转 AI 第 18 天:pytest 入门 - 为什么你必须要写测试

昨日回顾

Day 17 用 SQLAlchemy 重写了存储层,代码从 150 行降到 80 行,还清楚了关系映射。

但有个隐患------改了这么多代码,我怎么知道功能没坏?

手动跑一遍?每次改都跑一遍?Agent 项目模块一多,手动测不现实。

今天上测试。

Day 18 目标:搞懂 pytest 的核心用法,会给 Day 17 的存储层写测试。


一、为什么 Agent 开发者必须会写测试

前端同学对测试的态度常常是"重要但不紧急"------项目催得紧,先跑起来再说。

但做 Agent,测试不是"重要",是"生死攸关"。原因有三:

1. LLM 输出是不确定的

你今天写了个 prompt,模型输出符合预期。明天同样的输入,模型输出变了。

如果每个改动都靠"手动聊两句"验证,你根本没法迭代。

2. Agent 是"链式"的,改一处崩全身

erlang 复制代码
用户输入 → 记忆检索 → Prompt 组装 → LLM 调用 → 输出解析 → 工具调用 → ...

每一环都可能出 bug。 改一个环节,要保证其他环节没受影响。

3. 调试成本高

Agent 跑一次 LLM 调用要几秒、几块钱。测试必须"能脱离模型跑",否则开发节奏会被拖死。

这就是"单元测试"存在的意义------把每个环节隔离出来,快速验证。


二、pytest 是什么

Python 有内置的 unittest,但pytest 是事实标准。为什么?

对比 unittest pytest
写法 必须继承 TestCase 类 直接写函数
断言 self.assertEqual(a, b) assert a == b
失败信息 输出长 自动展开对比
参数化 靠循环 @pytest.mark.parametrize
夹具 setUp / tearDown @pytest.fixture
插件生态 一般 极其丰富

一句话:pytest 写起来像普通函数,看起来像自然语言。

安装

bash 复制代码
pip install pytest pytest-cov pytest-asyncio
  • pytest:核心
  • pytest-cov:覆盖率统计
  • pytest-asyncio:测异步函数(Agent 必备)

三、第一个测试

1. 最简单的例子

新建 day18/hello_test/test_calc.py:

python 复制代码
def add(a, b):
    return a + b


def test_add():
    assert add(1, 2) == 3


def test_add_negative():
    assert add(-1, -2) == -3

运行:

bash 复制代码
cd day18/hello_test
pytest

输出:

ini 复制代码
==================== test session starts ====================
collected 2 items

test_calc.py ..                                     [100%]

===================== 2 passed in 0.01s ====================

注意几点:

  1. 文件名必须 test_*.py 或 *_test.py(pytest 靠这个发现测试)
  2. 函数名必须 test_*(同上)
  3. 断言用 assert (不用 self.assertEqual)

2. 失败的输出长什么样

改一个:

python 复制代码
def test_add():
    assert add(1, 2) == 4   # 故意写错

运行:

ini 复制代码
==================== FAILURES ====================
____________________ test_add ____________________

    def test_add():
>       assert add(1, 2) == 4
E       assert 3 == 4
E        +  where 3 = add(1, 2)

test_calc.py:6: AssertionError
================= short test summary info =================
FAILED test_calc.py::test_add - assert 3 == 4
================= 1 failed, 1 passed in 0.05s =============

pytest 自动把 add(1, 2) 展开成 3,一眼看出哪里错了。 这就是它比 unittest 受欢迎的核心原因。

3. 常用断言写法

python 复制代码
# 基础
assert a == b
assert a != b
assert a > b
assert a is None
assert a is not None

# 容器
assert 1 in [1, 2, 3]
assert "key" in {"key": "value"}
assert len([1, 2, 3]) == 3

# 近似
import pytest
assert 0.1 + 0.2 == pytest.approx(0.3)   # 浮点比较

# 异常
with pytest.raises(ValueError):
    int("abc")

with pytest.raises(ValueError, match="invalid literal"):
    int("abc")

pytest.raises 是测试异常的标准写法 ,比 try/except 简洁。


四、夹具 fixture:测试的"准备工作"

测试常常需要准备环境:连接数据库、创建临时文件、造测试数据。

这些重复代码放在 fixture 里,pytest 会帮你自动注入。

1. 最简单的 fixture

python 复制代码
import pytest


@pytest.fixture
def sample_user():
    """返回一个测试用户"""
    return {"name": "张三", "age": 25}


def test_user_name(sample_user):
    assert sample_user["name"] == "张三"


def test_user_age(sample_user):
    assert sample_user["age"] == 25

用法:把 fixture 名字作为参数写进测试函数,pytest 自动注入。

前端类比: 有点像 Jest 的 beforeEach,但更灵活------用哪个就传哪个。

2. 清理现场:yield

如果 fixture 里需要"用完清理"(比如临时文件):

python 复制代码
import pytest
from pathlib import Path


@pytest.fixture
def temp_file(tmp_path):
    """创建临时文件,测试后自动清理"""
    file = tmp_path / "test.txt"
    file.write_text("hello", encoding="utf-8")
    yield file
    # 出了 yield,测试结束,可以在这里清理
    # tmp_path 是 pytest 内置的,会自动清理,这里不用手动删

yield 之前是"准备",yield 之后是"清理"。

Python 里的 yield 语法(生成器)在这里又派上用场了。

3. tmp_path:pytest 的内置 fixture

pytest 自带一堆实用 fixture:

fixture 作用
tmp_path 临时目录(Path 对象)
tmp_path_factory 批量临时目录
monkeypatch 运行时替换属性 / 环境变量
capsys 捕获 stdout / stderr
caplog 捕获日志

其中 tmp_path 最常用:

python 复制代码
def test_write_file(tmp_path):
    file = tmp_path / "data.txt"
    file.write_text("hello")
    assert file.read_text() == "hello"

每个测试都有独立的 tmp_path,互不干扰,用完自动删。

4. conftest.py:全局 fixture

如果多个测试文件需要同一个 fixture ,放在 conftest.py 里,pytest 会自动发现,不用 import:

python 复制代码
# conftest.py
import pytest


@pytest.fixture
def db():
    """测试用的数据库"""
    from myapp.db import Database
    db = Database(":memory:")   # 内存数据库
    yield db
    db.close()

任何测试文件都能直接用 db fixture。 不用 import,这是 pytest 的魔法。


五、参数化:一个测试跑多组数据

最常用的场景: 同一段逻辑,要测多种输入。

python 复制代码
import pytest


def is_even(n):
    return n % 2 == 0


# ❌ 笨办法:写三个测试
def test_even_2():
    assert is_even(2)

def test_even_4():
    assert is_even(4)

def test_even_6():
    assert is_even(6)


# ✅ 参数化
@pytest.mark.parametrize("input,expected", [
    (2, True),
    (4, True),
    (6, True),
    (1, False),
    (3, False),
    (0, True),
])
def test_is_even(input, expected):
    assert is_even(input) == expected

运行效果:

arduino 复制代码
test_even.py::test_is_even[2-True] PASSED
test_even.py::test_is_even[4-True] PASSED
test_even.py::test_is_even[6-True] PASSED
test_even.py::test_is_even[1-False] PASSED
test_even.py::test_is_even[3-False] PASSED
test_even.py::test_is_even[0-True] PASSED

6 组数据,一个函数搞定。 失败时 pytest 会告诉你具体是哪一组。

前端类比: Jest 的 test.each,思路一模一样。


六、测异步代码

Agent 里大量异步函数,必须会测。

bash 复制代码
pip install pytest-asyncio

加一个配置 (项目根目录 pytest.ini 或 pyproject.toml):

ini 复制代码
# pytest.ini
[pytest]
asyncio_mode = auto

或者 pyproject.toml:

toml 复制代码
[tool.pytest.ini_options]
asyncio_mode = "auto"

asyncio_mode = auto 表示"自动识别 async 测试函数",不用每个都加装饰器。

测试异步函数:

python 复制代码
import asyncio


async def fetch_data():
    await asyncio.sleep(0.1)
    return "data"


async def test_fetch_data():
    result = await fetch_data()
    assert result == "data"

就这么简单。 pytest 会自动处理事件循环。

不加 asyncio_mode = auto 的话 ,每个测试要加 @pytest.mark.asyncio 装饰器。自动模式更省心。


七、Mock:隔离外部依赖

测试的黄金法则:单元测试不应该真的去调 LLM API。

理由:

  • 慢:每次几秒
  • 贵:每次几块钱
  • 不稳定:网络波动、限流、模型版本更新

解决:用 unittest.mock 替换掉外部调用。

1. 简单 mock

python 复制代码
from unittest.mock import Mock


def test_mock_basic():
    # 创建一个假的函数
    fake_llm = Mock()
    fake_llm.chat.return_value = "假的回复"

    # 用它
    result = fake_llm.chat("你好")
    assert result == "假的回复"

    # 断言"被调用过"
    fake_llm.chat.assert_called_once_with("你好")

2. 替换模块内的依赖

真实场景: 你要测 Agent.process(),但它内部调了 LLMClient.chat()。

python 复制代码
# agent.py
class Agent:
    def __init__(self, llm):
        self.llm = llm

    def process(self, text):
        reply = self.llm.chat(text)
        return f"Agent: {reply}"

测试:

python 复制代码
from unittest.mock import Mock


def test_agent_process():
    fake_llm = Mock()
    fake_llm.chat.return_value = "你好呀"

    agent = Agent(fake_llm)
    result = agent.process("你好")

    assert result == "Agent: 你好呀"
    fake_llm.chat.assert_called_once_with("你好")

这就是"依赖注入"的价值------Agent 不直接 new LLM,而是从外面传进来。测试时传个 Mock 就行。

前端类比: 前端做组件测试时,用 jest.mock() 替换 API 层。思路一样。

3. patch:替换模块级对象

如果函数里直接 import 了外部模块:

python 复制代码
# service.py
import requests

def fetch_user(user_id):
    r = requests.get(f"https://api.example.com/users/{user_id}")
    return r.json()

测试时不想真的发请求:

python 复制代码
from unittest.mock import patch


@patch("service.requests.get")
def test_fetch_user(mock_get):
    mock_get.return_value.json.return_value = {"id": 1, "name": "张三"}

    result = fetch_user(1)

    assert result["name"] == "张三"
    mock_get.assert_called_once_with("https://api.example.com/users/1")

@patch("模块路径.属性名") 会在测试期间替换掉那个属性。

注意 patch 的路径 :patch 的是"使用的地方",不是"定义的地方" 。service.py 里 import requests,就要 patch service.requests.get。


八、实战:给 Day 17 的存储层写测试

动手时间。 给 storage_orm.py 写一套完整的测试。

1. 项目准备

bash 复制代码
cp -r day17/ai-assistant-v4 day18/ai-assistant-v5
cd day18/ai-assistant-v5

创建测试目录:

bash 复制代码
mkdir -p tests
touch tests/__init__.py

2. 配置 pytest

pyproject.toml 里加:

toml 复制代码
[tool.pytest.ini_options]
testpaths = ["tests"]
asyncio_mode = "auto"
addopts = "-v --tb=short"

含义:

  • testpaths:从哪里找测试
  • asyncio_mode:自动识别 async
  • addopts:-v 详细输出,--tb=short 异常堆栈简洁

3. tests/conftest.py:全局 fixture

python 复制代码
"""pytest 全局配置和 fixture"""
import sys
from pathlib import Path

import pytest

# 把 src 加到路径
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))

from ai_assistant.db_orm import DatabaseORM
from ai_assistant.storage_orm import SessionStorageORM
from ai_assistant.models import Session, Message


@pytest.fixture
def db(tmp_path):
    """测试用的内存数据库"""
    # SQLite 内存数据库:`:memory:`
    # 或者用 tmp_path 下的文件(更接近真实场景)
    db_file = tmp_path / "test.db"
    database = DatabaseORM(db_file)
    yield database
    database.close()


@pytest.fixture
def storage(db):
    """存储对象"""
    return SessionStorageORM(db)


@pytest.fixture
def sample_session():
    """一个标准的测试会话"""
    s = Session(session_id="test-001", title="测试会话")
    s.messages = [
        Message(role="user", content="你好"),
        Message(role="assistant", content="你好呀"),
        Message(role="user", content="Python 是什么"),
    ]
    return s


@pytest.fixture
def empty_session():
    """空会话"""
    return Session(session_id="test-empty", title="空会话")

关键点:

  • 用 tmp_path 而不是 :memory: :tmp_path 更接近真实文件,能暴露文件权限等问题
  • yield 保证清理 :测试结束自动 close()
  • sample_session / empty_session 是"标准测试数据":避免每个测试里重复造

4. tests/test_storage.py

python 复制代码
"""测试存储层"""
import pytest
from ai_assistant.models import Session, Message


class TestSaveAndLoad:
    """测试保存和加载"""

    def test_save_new_session(self, storage, sample_session):
        """保存新会话"""
        storage.save(sample_session)

        loaded = storage.load(sample_session.session_id)
        assert loaded is not None
        assert loaded.session_id == sample_session.session_id
        assert loaded.title == sample_session.title
        assert len(loaded.messages) == 3

    def test_save_creates_database_record(self, storage, sample_session):
        """保存后数据库里真的有记录"""
        storage.save(sample_session)
        rows = storage.db.session().__enter__().execute(
            "SELECT COUNT(*) FROM sessions"
        ).scalar()
        # 简化:直接再查一次
        loaded = storage.load(sample_session.session_id)
        assert loaded is not None

    def test_load_nonexistent(self, storage):
        """加载不存在的会话"""
        result = storage.load("nonexistent-id")
        assert result is None

    def test_save_empty_session(self, storage, empty_session):
        """保存空会话"""
        storage.save(empty_session)

        loaded = storage.load(empty_session.session_id)
        assert loaded is not None
        assert len(loaded.messages) == 0

    def test_save_twice_overwrites(self, storage, sample_session):
        """重复保存会覆盖"""
        storage.save(sample_session)

        # 修改后保存
        sample_session.add_message("user", "新消息")
        storage.save(sample_session)

        loaded = storage.load(sample_session.session_id)
        assert len(loaded.messages) == 4


class TestListSessions:
    """测试会话列表"""

    def test_list_empty(self, storage):
        sessions = storage.list_sessions()
        assert sessions == []

    def test_list_multiple(self, storage, sample_session, empty_session):
        storage.save(sample_session)
        storage.save(empty_session)

        sessions = storage.list_sessions()
        assert len(sessions) == 2

    def test_list_order(self, storage):
        """按更新时间倒序"""
        import time

        s1 = Session(session_id="s1", title="早的")
        storage.save(s1)
        time.sleep(0.01)

        s2 = Session(session_id="s2", title="晚的")
        s2.add_message("user", "hi")
        storage.save(s2)

        sessions = storage.list_sessions()
        assert sessions[0]["id"] == "s2"   # 更新的在前


class TestDelete:
    """测试删除"""

    def test_delete_existing(self, storage, sample_session):
        storage.save(sample_session)
        result = storage.delete(sample_session.session_id)
        assert result is True

        loaded = storage.load(sample_session.session_id)
        assert loaded is None

    def test_delete_nonexistent(self, storage):
        result = storage.delete("nonexistent")
        assert result is False

    def test_delete_cascades_messages(self, storage, sample_session):
        """删除会话时,消息也应删除(级联)"""
        storage.save(sample_session)
        storage.delete(sample_session.session_id)

        # 直接查 messages 表确认
        with storage.db.session() as s:
            from sqlalchemy import select, func
            from ai_assistant.orm_models import MessageModel
            count = s.scalar(select(func.count(MessageModel.id)))
            assert count == 0


class TestSearch:
    """测试搜索"""

    def test_search_finds_keyword(self, storage, sample_session):
        storage.save(sample_session)
        results = storage.search("Python")
        assert len(results) == 1
        assert results[0]["id"] == sample_session.session_id

    def test_search_no_result(self, storage, sample_session):
        storage.save(sample_session)
        results = storage.search("不存在的关键词xyz")
        assert results == []

    @pytest.mark.parametrize("keyword,should_find", [
        ("Python", True),
        ("你好", True),
        ("助手", True),
        ("Java", False),
        ("无关内容", False),
    ])
    def test_search_multiple_keywords(self, storage, sample_session, keyword, should_find):
        storage.save(sample_session)
        results = storage.search(keyword)
        assert (len(results) > 0) == should_find


class TestMessageOrdering:
    """测试消息顺序"""

    def test_messages_kept_in_order(self, storage):
        s = Session(session_id="order-test")
        for i in range(5):
            s.add_message("user", f"消息 {i}")

        storage.save(s)
        loaded = storage.load("order-test")

        for i, msg in enumerate(loaded.messages):
            assert msg.content == f"消息 {i}"


class TestDataIntegrity:
    """测试数据完整性"""

    def test_empty_content_rejected(self, storage):
        """空内容应该被 Pydantic 拦住"""
        from pydantic import ValidationError
        with pytest.raises(ValidationError):
            Message(role="user", content="")

    def test_invalid_role_rejected(self, storage):
        from pydantic import ValidationError
        with pytest.raises(ValidationError):
            Message(role="hacker", content="test")

5. 运行测试

bash 复制代码
cd day18/ai-assistant-v5
pytest

输出:

arduino 复制代码
tests/test_storage.py::TestSaveAndLoad::test_save_new_session PASSED        [  5%]
tests/test_storage.py::TestSaveAndLoad::test_save_creates_database_record PASSED [ 10%]
tests/test_storage.py::TestSaveAndLoad::test_load_nonexistent PASSED        [ 15%]
tests/test_storage.py::TestSaveAndLoad::test_save_empty_session PASSED      [ 21%]
tests/test_storage.py::TestSaveAndLoad::test_save_twice_overwrites PASSED   [ 26%]
tests/test_storage.py::TestListSessions::test_list_empty PASSED             [ 31%]
tests/test_storage.py::TestListSessions::test_list_multiple PASSED          [ 36%]
tests/test_storage.py::TestListSessions::test_list_order PASSED             [ 42%]
tests/test_storage.py::TestDelete::test_delete_existing PASSED              [ 47%]
tests/test_storage.py::TestDelete::test_delete_nonexistent PASSED           [ 52%]
tests/test_storage.py::TestDelete::test_delete_cascades_messages PASSED     [ 57%]
tests/test_storage.py::TestSearch::test_search_finds_keyword PASSED         [ 63%]
tests/test_storage.py::TestSearch::test_search_no_result PASSED             [ 68%]
tests/test_storage.py::TestSearch::test_search_multiple_keywords[Python-True] PASSED [ 73%]
tests/test_storage.py::TestSearch::test_search_multiple_keywords[你好-True] PASSED [ 78%]
tests/test_storage.py::TestSearch::test_search_multiple_keywords[助手-True] PASSED [ 84%]
tests/test_storage.py::TestSearch::test_search_multiple_keywords[Java-False] PASSED [ 89%]
tests/test_storage.py::TestSearch::test_search_multiple_keywords[无关内容-False] PASSED [ 94%]
tests/test_storage.py::TestMessageOrdering::test_messages_kept_in_order PASSED [100%]

==================== 19 passed in 1.32s ====================

19 个测试,1.3 秒跑完。 这就是自动化测试的威力------改一行代码,立刻知道有没有搞坏别的功能。

6. 覆盖率报告

bash 复制代码
pytest --cov=src/ai_assistant --cov-report=term-missing

输出:

matlab 复制代码
Name                                    Stmts   Miss  Cover   Missing
---------------------------------------------------------------------
src/ai_assistant/__init__.py                0      0   100%
src/ai_assistant/config.py                 15      2    87%   24-25
src/ai_assistant/db_orm.py                 25      2    92%   45-46
src/ai_assistant/models.py                 35      0   100%
src/ai_assistant/orm_models.py             30      0   100%
src/ai_assistant/storage_orm.py            80      8    90%   45-52
src/ai_assistant/logger.py                 20     20     0%   ...
---------------------------------------------------------------------
TOTAL                                     205     32    84%

--cov=src/ai_assistant 只统计 src/ai_assistant 下的代码。

--cov-report=term-missing 显示哪几行没测到。

目标:核心业务逻辑(storage_orm.py)覆盖到 90%+。

不追求 100%------有些分支(比如异常清理逻辑)很难测,投入产出比低。

7. 给 pytest 加"测试专用"依赖

pyproject.toml:

toml 复制代码
[project.optional-dependencies]
dev = [
    "pytest>=8.0.0",
    "pytest-cov>=5.0.0",
    "pytest-asyncio>=0.24.0",
]

安装:

bash 复制代码
pip install -e ".[dev]"

-e 是"可编辑安装",.[dev] 表示"当前项目 + dev 可选依赖"。

从此以后,任何一台机器上 pip install -e ".[dev]" 就能一键装好所有依赖。


九、前端视角:pytest vs Jest/Vitest

做过前端测试的同学会觉得很亲切:

概念 Jest / Vitest pytest
测试文件命名 *.test.ts test_*.py
测试函数 test('...', () => {}) def test_xxx():
断言 expect(x).toBe(y) assert x == y
分组 describe(...) class TestXxx
前置准备 beforeEach @pytest.fixture
清理 afterEach yield 后的代码
参数化 test.each([...]) @pytest.mark.parametrize
Mock jest.mock() unittest.mock.patch
覆盖率 jest --coverage pytest --cov
异步 async () => {} async def + pytest-asyncio

核心心智完全一致:

  • "准备 → 执行 → 断言 → 清理"四段式
  • 隔离依赖、mock 外部
  • 测试要快、要独立、要可重复

你前端的测试思维,直接迁移到 Python。


十、今日踩坑

  1. 文件名不是 test_*.py → pytest 不发现它,跑了个 0 测试
  2. 没装 pytest-asyncio → async 测试会被跳过或报错
  3. asyncio_mode 没配 → 每个 async 测试都要手动加装饰器
  4. tmp_path 写成了 tmpdir → 老版本用 tmpdir,新版本用 tmp_path(Path 对象)
  5. fixture 用 return 而不是 yield → 无法做清理
  6. patch 路径写错 → patch 的是"用它的地方",不是"定义它的地方"
  7. 测试之间共享状态 → 一个测试改了数据库,影响下一个,要用独立 fixture
  8. 测试依赖网络 / LLM → 绝对不能,一定要 mock
  9. assert 0.1 + 0.2 == 0.3 → 浮点数比较要用 pytest.approx
  10. 测试不写"为什么" → 名字要能说清楚"测的是什么",test_save_empty_session 比 test1 好一百倍

十一、今日总结

今天核心收获三件事:

  1. pytest 是函数式测试框架 :assert + fixture + parametrize 三件套走天下
  2. fixture 是"准备 + 清理"的封装 ,conftest.py 让 fixture 全局可用
  3. 单元测试必须 mock 外部依赖:LLM、网络、数据库文件都要隔离

一个感受:

测试不是"额外的负担",而是"能放心改代码的勇气来源"。

没有测试,你不敢重构------万一改坏了怎么办?

有了测试,你改完直接 pytest 一下,绿了就安心提交。

测试不是给"代码"写的,是给"未来的你"写的。

你的 Agent 项目现在有了完整的测试骨架。 后面加功能时,先写测试,再写代码 ------这就是 TDD(测试驱动开发),做 Agent 特别有效,因为每个环节的输出都是可验证的。

Day 18 完成度:✅

明日预告(Day 19)

  • 测试的进阶技巧:
    • monkeypatch:运行时改环境变量和属性
    • capsys:捕获标准输出
    • caplog:捕获日志
  • 集成测试 vs 单元测试
  • 用 pytest-benchmark 做性能测试
  • 给 LLM 调用写 mock 测试
  • 给 Day 14 的 MemoryManager 写完整测试

最后

如果这篇对你有用,点个赞让我知道有人在看。

评论区互动: 你之前写测试吗?前端用什么?欢迎分享。

有问题评论区见,我们第 19 天继续。

100 天,第 19 天见。


附录:系列目录

已完成 ✅

阶段 天数 内容
基础 01-06 Python 语法、数据结构、函数、OOP、文件、依赖管理
网络 07-08 HTTP 请求、异步 asyncio
工程 09-12 正则、装饰器、Pydantic、日志调试
项目 13-14 命令行 AI 助手 v1 / v2
复盘 15 阶段总结
数据 16-17 SQLite 数据库、SQLAlchemy ORM
测试 18 pytest 入门 ✅

进行中 🎯

阶段 天数 内容
测试 19 pytest 进阶:mock / 集成 / 性能
工具 20 CLI 打包与发布
后端 21-30 FastAPI + 完整服务端
大模型 31-45 LLM API + Prompt 工程
RAG 46-60 向量数据库 + 知识库
Agent 61-80 LangChain / 工具调用
实战 81-100 完整 Agent 项目上线

100 天后见,一起从切图仔变成 Agent 开发者。

相关推荐
用户019027581611 小时前
如何用 Python 计算 DMA 平行线差指标?
python
奈葵2 小时前
Python
开发语言·python
happylifetree2 小时前
Python54-55:核心语法-数据容器-字典dict-案例
python
小小张说故事2 小时前
Python 迭代器和生成器到底啥区别?3 个例子讲透,别再背概念了
python
外收内放2 小时前
Python基础语法练习题(56优化版补充)
开发语言·python
专注API从业者3 小时前
告别复杂页面解析,OpenClaw 快速搭建电商商品监控与数据分析脚本
大数据·数据库·python·数据挖掘·数据分析
王立志_LEO3 小时前
HTTPX 完整用法总结
python
ggb喔3 小时前
52pojie 的图片工具:为什么无损转换后体积涨了 6 倍、照片方向还是歪的
图像处理·python·算法·计算机视觉·图形渲染·媒体
hnxaoli3 小时前
win10小程序(二十三)新建带时间文件夹
python