Skip to content

Python 的类型系统与 Pydoll

Pydoll 广泛利用 Python 的类型系统来提供出色的 IDE 支持、及早发现错误并使 API 自我记录。本指南将解释类型提示的基础知识,以及 Pydoll 如何使用它们来增强您的开发体验。

类型提示基础

类型提示是可选的注解,用于指定变量、参数或返回值应该是什么类型的值。它们不影响运行时行为,但能启用强大的工具。

简单类型提示

# 基本类型
name: str = "Pydoll"
port: int = 9222
is_headless: bool = False
quality: float = 0.85

# 函数注解
def navigate(url: str, timeout: int = 30) -> bool:
    # ... 实现
    return True

容器类型

from typing import List, Dict, Optional

# 列表和字典
urls: List[str] = ['https://example.com', 'https://google.com']
headers: Dict[str, str] = {'User-Agent': 'MyBot/1.0'}

# 可选值 (可以是 None)
target_id: Optional[str] = None

# 现代语法 (Python 3.9+)
urls: list[str] = ['https://example.com']
headers: dict[str, str] = {'User-Agent': 'MyBot/1.0'}

Python 3.9+ 语法

Pydoll 的代码库使用较旧的 List[]Dict[] 语法以实现向后兼容,但如果您使用的是 Python 3.9+,您可以在代码中使用小写的 list[]dict[]

TypedDict:结构化字典

TypedDict 允许您定义具有特定键和值类型的字典结构。这在 Pydoll 的 CDP 协议定义中被 大量使用

基本 TypedDict

from typing import TypedDict

class UserInfo(TypedDict):
    name: str
    age: int
    email: str

# IDE 完全知道存在哪些键
user: UserInfo = {
    'name': 'Alice',
    'age': 30,
    'email': 'alice@example.com'
}

# 自动补全功能可用!
print(user['name'])  # IDE 建议: name, age, email

Pydoll 如何使用 TypedDict

Pydoll 将 每个 CDP 命令、响应和事件 定义为 TypedDict。这意味着您的 IDE 完全知道哪些属性可用:

# 来自 pydoll/protocol/page/methods.py
class CaptureScreenshotParams(TypedDict, total=False):
    """captureScreenshot 的参数。"""
    format: ScreenshotFormat
    quality: int
    clip: Viewport
    fromSurface: bool
    captureBeyondViewport: bool
    optimizeForSpeed: bool

class CaptureScreenshotResult(TypedDict):
    """captureScreenshot 命令的结果。"""
    data: str

当您调用返回 CDP 响应的方法时,您的 IDE 会自动补全响应键:

async def example():
    response = await tab.take_screenshot(as_base64=True)

    # IDE 知道这是 CaptureScreenshotResponse
    # 并建议 'result' -> 'data'
    screenshot_data = response['result']['data']  # 完整的自动补全!

可选字段与必选字段

TypedDict 使用 NotRequired[] 支持可选字段:

from typing import TypedDict, NotRequired

# 来自 pydoll/protocol/network/methods.py
class GetCookiesParams(TypedDict):
    """用于检索浏览器 cookie 的参数。"""
    urls: NotRequired[list[str]]  # 此字段是可选的

total=False 标志使 所有 字段都可选:

class CaptureScreenshotParams(TypedDict, total=False):
    format: ScreenshotFormat  # 所有字段都可选
    quality: int
    clip: Viewport

自动补全的魔力

当您键入 response[' 时,您的 IDE 会显示所有可用的键及其类型。这就是 TypedDict 的超能力在起作用!

Enums (枚举):类型安全的常量

枚举提供了类型安全的常量,您的 IDE 可以自动补全。Pydoll 广泛使用它们来表示 CDP 的值。

基本枚举

from enum import Enum

class ScreenshotFormat(str, Enum):
    JPEG = 'jpeg'
    PNG = 'png'
    WEBP = 'webp'

# IDE 自动补全可用的格式
format = ScreenshotFormat.PNG  # 类型是 ScreenshotFormat
print(format.value)  # 'png'

Pydoll 的枚举用法

from pydoll.constants import Key
from pydoll.protocol.page.types import ScreenshotFormat
from pydoll.protocol.input.types import KeyModifier

# 查找元素 - 使用 kwargs,而非枚举
element = await tab.find(id='submit-btn')
element = await tab.find(class_name='btn-primary')
element = await tab.find(tag_name='button')

# 键盘输入 - IDE 建议所有键
await element.press_keyboard_key(Key.ENTER)
await element.press_keyboard_key(Key.TAB)
await element.press_keyboard_key(Key.ESCAPE)

# 修饰键是整数枚举 (用于特殊键)
await element.press_keyboard_key(Key.TAB, modifiers=KeyModifier.SHIFT)

# 截图格式枚举
await tab.take_screenshot('file.webp', format=ScreenshotFormat.WEBP)

枚举自动补全

键入 Key.ScreenshotFormat.,您的 IDE 就会显示所有可用选项。再也不用记忆字符串了!

函数重载 (Function Overloads)

重载允许一个函数根据其参数返回不同的类型。Pydoll 使用它来提供精确的类型信息。

基本重载示例

from typing import overload

# 重载签名 (不执行)
@overload
def process(data: str) -> str: ...

@overload
def process(data: int) -> int: ...

# 实际实现
def process(data):
    return data * 2

# IDE 知道返回类型
result1 = process("hello")  # 类型: str
result2 = process(42)       # 类型: int

Pydoll 的重载用法

find()query() 方法根据 find_all 参数返回不同的类型:

# 来自 pydoll/elements/mixins/find_elements_mixin.py
class FindElementsMixin:
    @overload
    async def find(
        self, find_all: Literal[False] = False, **kwargs
    ) -> WebElement: ...

    @overload
    async def find(
        self, find_all: Literal[True], **kwargs
    ) -> list[WebElement]: ...

    async def find(
        self, find_all: bool = False, **kwargs
    ) -> Union[WebElement, list[WebElement]]:
        # 实现...

在您的代码中:

# find_all=False (默认) - IDE 知道返回类型是 WebElement
button = await tab.find(id='submit-btn')
await button.click()  # 单个元素的方法可用!

# find_all=True - IDE 知道返回类型是 list[WebElement]
buttons = await tab.find(class_name='btn', find_all=True)
for btn in buttons:  # IDE 知道这是一个列表!
    await btn.click()

# query() 也是如此
element = await tab.query('#submit-btn')  # 类型: WebElement
elements = await tab.query('.btn', find_all=True)  # 类型: list[WebElement]

智能类型推断

您的 IDE 会根据 find_all 参数自动知道您获取的是单个元素还是列表。无需类型转换或类型断言!

泛型 (Generic Types)

泛型就像“类型容器”,可以与不同类型一起工作,同时保留类型信息。可以把它们想象成能适应您放入任何东西的模板。

理解泛型:一个简单的类比

想象一个可以装任何东西的 Box。没有泛型:

# 没有泛型 - IDE 不知道里面是什么
class Box:
    def __init__(self, content):
        self.content = content

    def get(self):
        return self.content

my_box = Box("hello")
item = my_box.get()  # 类型: Unknown - 可能是任何东西!

使用泛型:

from typing import Generic, TypeVar

T = TypeVar('T')  # T 是一个 "类型占位符"

class Box(Generic[T]):
    def __init__(self, content: T):
        self.content = content

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

# 现在 IDE 完全知道每个盒子里装的是什么
string_box: Box[str] = Box("hello")
item1 = string_box.get()  # 类型: str

number_box: Box[int] = Box(42)
item2 = number_box.get()  # 类型: int

# List 是一个内置的泛型
numbers: list[int] = [1, 2, 3]  # 包含 int 的列表
names: list[str] = ["Alice", "Bob"]  # 包含 str 的列表

泛型简化了类型提示

泛型让您只需编写一个可重用的 list[T],它能适应您放入的任何东西,而无需为每种可能的列表类型编写 Union[List[str], List[int], List[float], ...]

现实世界中的泛型示例

from typing import TypeVar, Generic

T = TypeVar('T')

class Response(Generic[T]):
    """一个通用的 API 响应包装器。"""
    def __init__(self, data: T, status: int):
        self.data = data
        self.status = status

    def get_data(self) -> T:
        return self.data

# 每个响应都保留了其数据类型
user_response: Response[dict] = Response({"name": "Alice"}, 200)
user_data = user_response.get_data()  # 类型: dict

count_response: Response[int] = Response(42, 200)
count = count_response.get_data()  # 类型: int

Pydoll 如何使用泛型

Pydoll 的 CDP 命令系统使用泛型来确保响应类型与命令匹配:

# 来自 pydoll/protocol/base.py
from typing import Generic, TypeVar

T_CommandParams = TypeVar('T_CommandParams')
T_CommandResponse = TypeVar('T_CommandResponse')

class Command(TypedDict, Generic[T_CommandParams, T_CommandResponse]):
    """所有命令的基础结构。"""
    id: NotRequired[int]
    method: str
    params: NotRequired[T_CommandParams]

class Response(TypedDict, Generic[T_CommandResponse]):
    """所有响应的基础结构。"""
    id: int
    result: T_CommandResponse

这意味着当您执行一个命令时,响应类型会被自动推断:

# PageCommands.navigate 返回 Command[NavigateParams, NavigateResult]
command = PageCommands.navigate('https://example.com')

# ConnectionHandler.execute_command 保留了泛型类型
response = await connection_handler.execute_command(command)

# IDE 知道 response['result'] 是 NavigateResult (不仅仅是 "any dict")
frame_id = response['result']['frameId']  # 自动补全可用!
loader_id = response['result']['loaderId']  # 所有字段都已知!

为什么泛型在 Pydoll 中很重要

没有泛型,每个 CDP 响应的类型都只是 dict[str, Any],您将失去所有的自动补全功能。有了泛型,IDE 能根据您发送的命令知道每个响应的确切结构。

联合类型 (Union Types)

联合 (Union) 表示值可能是多种类型之一:

from typing import Union

# 可以是字符串或整数
identifier: Union[str, int] = "user-123"
identifier = 456  # 也有效

# 现代语法 (Python 3.10+)
identifier: str | int = "user-123"

Pydoll 的联合类型用法

# 文件路径可以是字符串或 Path 对象
from pathlib import Path

async def upload_file(files: Union[str, Path, list[Union[str, Path]]]):
    # 处理多种输入类型
    pass

# 所有这些都有效:
await tab.expect_file_chooser('/path/to/file.txt')
await tab.expect_file_chooser(Path('/path/to/file.txt'))
await tab.expect_file_chooser(['/file1.txt', Path('/file2.txt')])

Pydoll 中的实际好处

1. 智能自动补全

您的 IDE 会建议可用的键、方法和值:

from pydoll.protocol.page.events import PageEvent
from pydoll.protocol.network.types import ResourceType
from pydoll.protocol.input.types import KeyModifier
from pydoll.constants import Key

# 自动补全事件名称
await tab.on(PageEvent.LOAD_EVENT_FIRED, callback)
await tab.on(PageEvent.JAVASCRIPT_DIALOG_OPENING, callback)

# 自动补全资源类型
await tab.enable_fetch_events(resource_type=ResourceType.XHR)
await tab.enable_fetch_events(resource_type=ResourceType.DOCUMENT)

# 自动补全按键
await element.press_keyboard_key(Key.ENTER)
await element.press_keyboard_key(Key.TAB, modifiers=KeyModifier.SHIFT)

# 自动补全 find() 中的 kwargs
element = await tab.find(id='submit-btn')  # IDE 建议: id, class_name, tag_name, 等.

2. 及早发现错误

像 mypy 或 Pylance 这样的类型检查器会在运行时之前捕获错误:

# 类型检查器会捕获这个
await tab.take_screenshot('file.png', quality='high')  # 错误: quality 必须是 int

# 类型检查器会捕获这个
event = await tab.find(id='button')
await tab.on(event, callback)  # 错误: event 是 WebElement, 不是 str

# 正确的
await tab.take_screenshot('file.png', quality=90)
await tab.on(PageEvent.LOAD_EVENT_FIRED, callback)

3. 自我记录的代码

类型可作为内联文档:

# 您立即知道每个参数期望什么
async def take_screenshot(
    self,
    path: Optional[str] = None,
    quality: int = 100,
    beyond_viewport: bool = False,
    as_base64: bool = False,
) -> Optional[str]:
    pass

4. CDP 响应导航

自信地浏览复杂的 CDP 响应:

# 来自 pydoll/protocol/browser/methods.py
class GetVersionResult(TypedDict):
    protocolVersion: str
    product: str
    revision: str
    userAgent: str
    jsVersion: str

# 在您的代码中
version_info = await browser.get_version()

# IDE 建议所有可用的键
print(version_info['product'])         # 自动补全!
print(version_info['userAgent'])       # 自动补全!
print(version_info['protocolVersion']) # 自动补全!

类型检查您的代码

使用 Pylance (VS Code)

Pylance 在 VS Code 中提供实时类型检查:

  1. 安装 Pylance 扩展
  2. 在设置中设置类型检查模式:
{
    "python.analysis.typeCheckingMode": "basic"  // 或 "strict"
}

现在您可以获得即时反馈:

from pydoll.browser.chromium import Chrome

async def main():
    async with Chrome() as browser:
        tab = await browser.start()

        # 当您键入时,Pylance 会显示参数类型
        await tab.go_to('https://example.com', timeout=30)

        # Pylance 会对错误的类型发出警告
        await tab.take_screenshot(quality='high')  # 警告!

使用 mypy

运行 mypy 来检查您的整个项目:

pip install mypy
mypy your_script.py

示例输出:

your_script.py:10: error: Argument "quality" to "take_screenshot" has incompatible type "str"; expected "int"
Found 1 error in 1 file (checked 1 source file)

Pydoll 的协议类型系统

Pydoll 的 protocol/ 目录包含整个 Chrome DevTools 协议的全面类型定义:

pydoll/protocol/
├── base.py              # 泛型 Command, Response, CDPEvent 类型
├── browser/
│   ├── events.py        # BrowserEvent 枚举, 事件参数 TypedDicts
│   ├── methods.py       # Browser 方法枚举, 参数/结果 TypedDicts
│   └── types.py         # Browser 域类型 (Bounds, PermissionType, 等.)
├── dom/
│   ├── events.py        # DOM 事件定义
│   ├── methods.py       # DOM 命令定义
│   └── types.py         # DOM 类型 (Node, BackendNode, 等.)
├── page/
│   ├── events.py        # Page 事件 (LOAD_EVENT_FIRED, 等.)
│   ├── methods.py       # Page 方法 (navigate, captureScreenshot, 等.)
│   └── types.py         # Page 类型 (Frame, ScreenshotFormat, 等.)
├── network/
│   └── ...              # Network 域类型
└── ...                  # 其他 CDP 域

示例:完整的类型流

让我们追踪一个从命令到响应的完整类型流:

# 1. 方法枚举 (protocol/page/methods.py)
class PageMethod(str, Enum):
    CAPTURE_SCREENSHOT = 'Page.captureScreenshot'

# 2. 参数 TypedDict (protocol/page/methods.py)
class CaptureScreenshotParams(TypedDict, total=False):
    format: ScreenshotFormat
    quality: int
    clip: Viewport

# 3. 结果 TypedDict (protocol/page/methods.py)
class CaptureScreenshotResult(TypedDict):
    data: str

# 4. 命令创建 (commands/page_commands.py)
class PageCommands:
    @staticmethod
    def capture_screenshot(
        format: Optional[ScreenshotFormat] = None,
        quality: Optional[int] = None,
        ...
    ) -> Command[CaptureScreenshotParams, CaptureScreenshotResult]:
        return {
            'method': PageMethod.CAPTURE_SCREENSHOT,
            'params': {...}
        }

# 5. 在 Tab 中使用 (browser/tab.py)
class Tab:
    async def take_screenshot(...) -> Optional[str]:
        response: CaptureScreenshotResponse = await self._execute_command(
            PageCommands.capture_screenshot(...)
        )
        screenshot_data = response['result']['data']  # 完全类型化!
        return screenshot_data

每一步都保留了类型信息,让您在整个过程中都能获得自动补全和类型检查!

最佳实践

1. 让 Pydoll 的类型引导您

不要抗拒类型,它们是来帮助您的:

# 好的:使用 kwargs (IDE 自动补全参数名称)
element = await tab.find(id='submit-btn')
button = await tab.find(class_name='btn-primary')

# 好的:在适用的地方使用枚举
from pydoll.constants import Key
await element.press_keyboard_key(Key.ENTER)

# 避免:魔法字符串
await element.press_keyboard_key('Enter')  # 没有自动补全,容易出错

2. 在您的 IDE 中探索类型

将鼠标悬停在变量上以查看其类型:

# 悬停在 'response' 上查看: Response[CaptureScreenshotResult]
response = await tab._execute_command(PageCommands.capture_screenshot(...))

# 悬停在 'data' 上查看: str
data = response['result']['data']

3. 不要过度注解

Python 的类型推断很智能,不要注解所有东西:

# 过多
name: str = "Alice"
count: int = 5
is_active: bool = True

# 让 Python 推断简单的字面量
name = "Alice"
count = 5
is_active = True

# 当类型不明显时进行注解
from typing import Optional

result: Optional[WebElement] = await tab.find(id='missing', raise_exc=False)

了解更多

要更深入地了解 Python 的类型系统和 CDP 协议:

类型系统将 Pydoll 从一个简单的自动化库转变为一个 类型安全、自我记录、IDE 友好 的框架。它能在错误发生之前捕获它们,并使探索 API 变得轻而易举!