Pydantic库-数据验证和设置管理

在处理来自系统外部的数据,如API、终端用户输入或其他来源时,我们必须牢记开发中的一条基本原则:“永远不要相信用户的输入”。

因此,我们必须对这些数据进行严格的检查和验证,确保它们被适当地格式化和标准化。这样做的目的是为了确保这些数据符合我们的程序所需的输入规范,从而保障项目能够正确且高效地运行。

为什么使用 Python 的 Pydantic 库?
Pydantic 是一个在 Python 中用于数据验证和解析的第三方库,它现在是 Python 使用最广泛的数据验证库。

它利用声明式的方式定义数据模型和Python 类型提示的强大功能来执行数据验证和序列化,使您的代码更可靠、更可读、更简洁且更易于调试。。
它还可以从模型生成 JSON 架构,提供了自动生成文档等功能,从而轻松与其他工具集成。


Pydantic 的一些主要特性
易用性

Pydantic 使用起来简单直观,需要最少的样板代码和配置。它适用于许多流行的 IDE 和静态分析工具,例如 PyCharm、VS Code、mypy 等。Pydantic 可以轻松与其他流行的 Python 库(如 Flask、Django、FastAPI 和 SQLAlchemy)集成,使其易于在现有项目中使用。

类型注解

Pydantic 使用类型注解来定义模型的字段类型,以确保确保数据符合预期的类型和格式。你可以使用Python 内置的类型、自定义类型或者其他Pydantic 提供的验证类型。

数据验证,用户友好的错误

Pydantic 自动根据模型定义进行数据验证。它会检查字段的类型、长度、范围等,并自动报告验证错误,Pydantic 会提供信息丰富且可读的错误消息,包括错误的位置、类型和输入。你可以使用 ValidationError 异常来捕获验证错误。

序列化与反序列化

序列化是将复杂数据结构(如对象、数组、字典等)转换为简单数据格式(如字符串或字节流)的过程。这样做是为了便于存储或传输。反序列化是相反的过程,将简单数据格式还原为复杂数据结构。Pydantic 提供了从各种数据格式(例如 JSON、字典)到模型实例的转换功能。它可以自动将输入数据解析成模型实例,并保留类型安全性和验证规则。

性能高

Pydantic 的核心验证逻辑是用 Rust 编写的,使其成为 Python 中最快的数据验证库之一。它还支持延迟验证和缓存,以提高效率。


要使用可以直接安装

pip install pydantic

Pydantic 使用例子

from datetime import datetime

from pydantic import BaseModel, PositiveInt


class User(BaseModel):
    id: int  #id 的类型是 int ;仅注释声明告知 Pydantic 该字段是必需的。如果可能,字符串、字节或浮点数将被强制转换为整数;否则将引发异常。
    name: str = 'John Doe'  #name 是一个字符串;因为它有默认值,所以不必需。
    signup_ts: datetime | None  #signup_ts 是一个必填的 datetime 字段,但值 None 可以提供;Pydantic 将处理 Unix 时间戳整数(例如 1496498400 )或表示日期和时间的字符串。
    tastes: dict[str, PositiveInt]  #tastes 是一个键为字符串且值为正整数的字典。 PositiveInt 类型是 Annotated[int, annotated_types.Gt(0)] 的简写。


external_data = {
    'id': 123,
    'signup_ts': '2019-06-01 12:22',  #这里的输入是一个 ISO8601 格式的日期时间,Pydantic 将把它转换为一个 datetime 对象。
    'tastes': {
        'wine': 9,
        b'cheese': 7,  #关键在这里是 bytes ,但 Pydantic 会负责将其强制转换为字符串。
        'cabbage': '1',  #同样地,Pydantic 会将字符串 '1' 强制转换为整数 1
    },
}

user = User(**external_data)  #这里通过将外部数据作为关键字参数传递给 User 来创建 User 的实例

print(user.id)  #我们可以将字段作为模型的属性来访问
#> 123
print(user.model_dump())  #我们可以将模型转换为带有 model_dump() 的字典
"""
{
    'id': 123,
    'name': 'John Doe',
    'signup_ts': datetime.datetime(2019, 6, 1, 12, 22),
    'tastes': {'wine': 9, 'cheese': 7, 'cabbage': 1},
}
"""

如果验证失败,Pydantic 会引发一个错误并详细说明哪里出错了:

# continuing the above example...

from pydantic import ValidationError


class User(BaseModel):
    id: int
    name: str = 'John Doe'
    signup_ts: datetime | None
    tastes: dict[str, PositiveInt]


external_data = {'id': 'not an int', 'tastes': {}}  

try:
    User(**external_data)  
except ValidationError as e:
    print(e.errors())
    """
    [
        {
            'type': 'int_parsing',
            'loc': ('id',),
            'msg': 'Input should be a valid integer, unable to parse string as an integer',
            'input': 'not an int',
            'url': 'https://pydantic.com.cn/errors/validation_errors#int_parsing',
        },
        {
            'type': 'missing',
            'loc': ('signup_ts',),
            'msg': 'Field required',
            'input': {'id': 'not an int', 'tastes': {}},
            'url': 'https://pydantic.com.cn/errors/validation_errors#missing',
        },
    ]
    """

简单解释

Pydantic 允许你定义数据模型,这些模型会自动验证输入数据的结构和类型。你只需定义一个类,用 Python 的类型提示标注其字段,Pydantic 就会为你处理验证和序列化。与使用JSON Schema或OpenAPI进行手动验证相比,这大大简化了数据验证过程。同时,Pydantic 也提供了强大的数据转换能力,能将复杂数据结构(如 JSON、字典)轻易转换为Python对象。

场景:API 参数验证和转换在电子商务平台

背景

假设你正在开发一个电子商务平台的后端服务,该服务提供了一个API端点,允许客户提交订单。每个订单都有多个字段,如产品ID、数量、支付方式等。你希望验证这些输入参数的有效性并转换为内部使用的Python对象。

常见技术对比

  • 手动验证:你可以在代码中手动检查每个字段,但这样做很冗长,容易出错。
  • JSON Schema:提供一种结构化的验证方法,但需要额外的定义和解析步骤。
  • Marshmallow:也是一种常用于数据验证的库,但与Pydantic相比,它更侧重于序列化和反序列化,而不是类型安全。

Pydantic 的实际应用

使用 Pydantic,你可以定义一个 Order模型来自动完成这些工作。

from pydantic import BaseModel, Field

class Order(BaseModel):
    product_id: int = Field(..., gt=0)
    quantity: int = Field(..., gt=0, le=100)
    payment_method: str = Field(..., regex="^(credit_card|paypal)$")

功能

  1. 类型检查product_id和 quantity必须是整数。
  2. 范围验证product_id必须大于0,quantity必须在1到100之间。
  3. 正则匹配payment_method只能是 “credit_card” 或 “paypal”。

使用

当客户通过API提交一个订单时,你只需将输入数据传递给这个 Order模型。如果数据无效,Pydantic 将自动抛出一个详细的错误,指出哪个字段无效以及为什么。

order_data = {
    "product_id": 1,
    "quantity": 50,
    "payment_method": "credit_card"
}

try:
    order = Order(**order_data)
except ValidationError as e:
    print(e.json())

这种方式使得代码更简洁,更易于维护,同时提供了强类型和自动验证的优点。与手动验证或使用其他库相比,Pydantic 提供了一个更为高效和直观的解决方案。

示例:用户注册API与Pydantic的数据验证

代码设置

在这个示例中,我们使用 FastAPI 构建一个简单的用户注册 API。FastAPI 与 Pydantic 集成非常紧密,用于请求和响应模型的验证。我们将比较使用 Pydantic 和手动验证的差异。

首先,我们导入必要的模块并设置 FastAPI 应用。

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, EmailStr, Field
from typing import Optional

app = FastAPI()

Pydantic 数据模型

接下来,我们使用 Pydantic 定义一个用户注册的数据模型。

class UserRegister(BaseModel):
    username: str = Field(..., min_length=3, max_length=50)
    email: EmailStr
    password: str = Field(..., min_length=8)
    age: Optional[int] = Field(None, ge=18)

在这个模型中,我们定义了如下字段和验证规则:

  • username: 字符串类型,长度必须在3到50字符之间。
  • email: 必须是有效的电子邮件地址。
  • password: 字符串类型,至少包含8个字符。
  • age: 整数类型,可选,但如果提供必须大于等于18。

FastAPI 路由与验证

使用 Pydantic 模型,我们可以很容易地在 FastAPI 路由中进行数据验证。

@app.post("/register/")
def register(user: UserRegister):
    return {"username": user.username, "email": user.email}

对比:手动验证

如果不使用 Pydantic,数据验证会变得复杂和冗长。例如:

@app.post("/register_manual/")
def register_manual(username: str, email: str, password: str, age: Optional[int] = None):
    if len(username) < 3 or len(username) > 50:
        raise HTTPException(status_code=400, detail="Invalid username length")

    # ...其他字段验证

    return {"username": username, "email": email}

在这个手动验证的示例中,我们需要为每个字段写多行验证代码,这显然不如使用 Pydantic 效率高。

pydantic的核心是模型(Model)

验证数据

一旦你定义了模型,你可以使用它来验证数据。

如果要从字典实例化 User 对象,可以使用字典对象解包.model_validate().model_validate_json()类方法:

if __name__ == '__main__':

    user_data = {
        "id": 123,
        "name": "小卤蛋",
        "age": 20,
        "email": "xiaoludan@example.com",
        'signup_ts': '2024-07-19 00:22',
        'friends': ["公众号:海哥python", '小天才', b''],
        'password': '123456',
        'phone': '13800000000',
        'sex': '男'
    }

    try:
        # user = User(**user_data)
        user = User.model_validate(user_data)
        print(f"User id: {user.id}, User name: {user.name}, User email: {user.email}")
    except ValidationError as e:
        print(f"Validation error: {e.json()}")

都符合模型定义的情况下,您可以像往常一样访问模型的属性:

User id: 123, User name: 小卤蛋, User email: xiaoludan@example.com
1
如果数据不符合模型的定义(以下故意不传 id 字段),Pydantic 将抛出一个 ValidationError。

自定义验证

除了内置的验证器,还可以为模型定义自定义验证器。假设要确保用户年龄在18岁以上,可以使用@field_validator装饰器创建一个自定义验证器:

# ! -*-conding: UTF-8 -*-
from datetime import datetime
from typing import List, Optional
from pydantic import BaseModel, EmailStr, field_validator, ValidationError


def check_name(v: str) -> str:
    """Validator to be used throughout"""
    if not v.startswith("小"):
        raise ValueError("must be startswith 小")
    return v


class User(BaseModel):
    id: int
    name: str = "小卤蛋"
    age: int
    email: EmailStr
    signup_ts: Optional[datetime] = None
    friends: List[str] = []

    validate_fields = field_validator("name")(check_name)
'''上面这行代码是使用field_validator装饰器来为name字段添加一个自定义的验证函数check_name。field_validator装饰器允许你为模型的字段指定一个或多个验证函数,这些函数将在模型实例化时自动调用,以确保字段值符合特定的条件。
"name":指定了要验证的字段名。
check_name:是一个自定义的验证函数,它接受一个字符串参数v,并检查这个字符串是否以"小"开头。如果不是,它将抛出一个ValueError。

下面的@field_validator("age")是一个装饰器,用于为age字段添加一个自定义的验证函数。@field_validator装饰器的工作方式与field_validator类似,但它是作为一个装饰器直接应用于方法上的,而不是作为类属性。
"age":指定了要验证的字段名。
check_age:是一个类方法,它接受一个参数age,并检查这个值是否小于18。如果是,它将抛出一个ValueError。
'''

    @field_validator("age")
    @classmethod
    def check_age(cls, age):
        if age < 18:
            raise ValueError("用户年龄必须大于18岁")
        return age

当尝试创建一个只有12岁的小朋友用户:

if __name__ == '__main__':
    user_data = {
        "id": 123,
        "name": "小卤蛋",
        "age": 12,
        "email": "xiaoludan@example.com",
        'signup_ts': '2024-07-19 00:22',
        'friends': ["公众号:海哥python", '小天才', b''],
    }
    try:
        user = User(**user_data)
    except ValidationError as e:
        print(f"Validation error: {e.json()}")

将得到一个错误:
Validation error: [{"type":"value_error","loc":["age"],"msg":"Value error, 用户年龄必须大于18岁","input":12,"ctx":{"error":"用户年龄必须大于18岁"},"url":"https://errors.pydantic.dev/2.8/v/value_error"}]

或者,当name不是小开头的话也会报错

如果要同时动态校验多个字段,还可以使用model_validator装饰器。
# ! -*-conding: UTF-8 -*-
# @公众号: 海哥python
from datetime import datetime
from typing import List, Optional
from typing_extensions import Self  # 如果python版本不低于3.11,则可以直接从typing中导入Self
from pydantic import BaseModel, ValidationError, EmailStr, field_validator, model_validator


def check_name(v: str) -> str:
    """Validator to be used throughout"""
    if not v.startswith("小"):
        raise ValueError("must be startswith 小")
    return v


class User(BaseModel):
    id: int
    name: str = "小卤蛋"
    age: int
    email: EmailStr
    signup_ts: Optional[datetime] = None
    friends: List[str] = []

    validate_fields = field_validator("name")(check_name)

    @field_validator("age")
    @classmethod
    def check_age(cls, age):
        if age < 18:
            raise ValueError("用户年龄必须大于18岁")
        return age

    @model_validator(mode="after")
    def check_age_and_name(self) -> Self:
        if self.age < 30 and self.name != "小卤蛋":
            raise ValueError("用户年龄必须小于30岁, 且名字必须为小卤蛋")

        return self


if __name__ == '__main__':
    user_data = {
        "id": 123,
        "name": "小小卤蛋",
        "age": 20,
        "email": "xiaoludan@example.com",
        'signup_ts': '2024-07-19 00:22',
        'friends': ["公众号:海哥python", '小天才', b''],
    }
    try:
        user = User(**user_data)
        print(user.model_dump())
    except ValidationError as e:
        print(f"Validation error: {e.json()}")

深入学习以下

https://blog.csdn.net/python_9k/article/details/140711001

https://blog.csdn.net/weixin_43936332/article/details/131627430

python Annotated 注释模块

什么是Annotated模块?

Annotated是Python标准库中的一个模块,它提供了一种注解(Annotation)的实现方式。注解是Python 3.0引入的一种特性,它允许在函数、类和方法的定义中添加额外的信息,这些信息可以用于类型检查、文档生成等用途。Annotated模块通过提供一些装饰器和工具函数,使得注解的使用变得更加简单和便捷。

Annotated模块的使用方法

安装Annotated模块

在使用Annotated模块之前,首先需要安装它。可以通过以下命令使用pip安装Annotated模块:

pip install Annotated

安装完成后,就可以在Python代码中导入Annotated模块了。

基本注解类型

Annotated模块提供了几种基本的注解类型,包括AnnotatedUnionOptional。这些类型可以用于对函数参数、返回值和变量进行注解。

  • Annotated[type, metadata]:用于注解对象的类型并添加元数据。
  • Union[type1, type2, ...]:表示注解对象的类型可以是多个类型中的任意一个。
  • Optional[type]:表示注解对象的类型可以是指定类型或者None

下面的例子演示了如何使用这些基本注解类型:

from Annotated import Annotated, Union, Optional

def greet(name: Annotated[str, "The name of the person"]) -> str:
    return "Hello, " + name

def add(a: int, b: int) -> int:
    return a + b

def divide(a: int, b: Annotated[int, "The divisor"], *, remainder: Optional[bool] = False) -> Union[int, float]:
    if remainder:
        return a % b
    else:
        return a / b

在上面的例子中,greet函数的参数name被注解为字符串类型,并添加了一个描述信息。add函数的参数ab被注解为整数类型,返回值被注解为整数类型。divide函数的参数ab被注解为整数类型,并添加了描述信息,remainder参数被注解为布尔类型,默认值为False,返回值可以是整数或者浮点数类型。

使用注解

在使用Annotated模块时,可以通过调用AnnotatedUnionOptional等类型来添加注解。注解可以用于函数的参数、返回值和变量的声明中。

from typing import Annotated, Union, Optional

def greet(name: Annotated[str, "The name of the person"]) -> str:
    return "Hello, " + name

def divide(a: int, b: Annotated[int, "The divisor"], *, remainder: Optional[bool] = False) -> Union[int, float]:
    if remainder:
        return a % b
    else:
        return a / b

result = greet("Alice")
print(result)  # 输出:Hello, Alice

result = divide(10, 3)
print(result)  # 输出:3.3333333333333335

result = divide(10, 3, remainder=True)
print(result)  # 输出:1

在上面的例子中,通过调用Annotated函数来为函数的参数和返回值添加注解,注解的类型可以是基本类型,也可以是自定义类型。在调用函数时,可以传入符合注解类型的实参。

获取注解信息

使用Annotated模块可以方便地获取函数、方法和类的注解信息。可以通过get_type_hints函数来获取函数中参数和返回值的注解信息。

from Annotated import get_type_hints

def greet(name: Annotated[str, "The name of the person"]) -> str:
    return "

个体工商报账-税务-开票

https://etax.hunan.chinatax.gov.cn/
季度到税务总局电子税务局-我要办税-税费申报及缴纳-在线申报-纳税申报-一般会弹出应申报


再到https://www.etax.chinatax.gov.cn/自然人电子税务局-季度填-经营所得申报中的经营所得A表,每年的前几个月填前一年的经营所得B表

开票类目(比如现代服务-网络技术服务费) 在系统设置-自定义货物与劳务编码-左边加一个自己定的节点名-右边增加商品就填你要开的最小类目(就是网络技术服务费),然后再赋码,就选在那个下面(就是大类目-现代服务)

现在开票也在电子税务局中

【开票业务】-【蓝字发票开具】 立即开票
选择普通发票,其它的都不要选,然后填写信息,项目名称就写水泥后会有提示出来

python插件架构介绍

一、插件架构
在 Python 中,插件架构通常指的是一种软件架构模式,它允许开发者在不改变主应用程序代码的情况下,向应用程序添加新的功能或修改现有功能。这种架构使得应用程序可以通过加载外部模块或组件来扩展其功能,这些外部模块或组件通常被称为“插件”。

Python 的插件架构涉及以下几个关键点:

模块化:Python 支持模块化设计,意味着应用程序可以被分解成独立、可替换、可重用的模块。插件本质上是这些模块的一种,它们遵循预定义的接口或协议。

接口定义:为了让插件能够与主应用程序交互,通常会定义一套接口或抽象基类。插件需要实现这些接口或继承并实现这些基类,从而提供必要的功能。

插件发现:应用程序需要有某种机制来发现可用的插件。这可以通过扫描特定目录、注册表项或使用插件管理器来实现。插件发现过程可能涉及动态加载 Python 模块。

插件加载与激活:一旦发现一个插件,应用程序需要知道如何加载并激活它。在 Python 中,这通常涉及到使用标准库中的 importlib 模块动态加载插件模块,并创建插件实例。

配置和定制:插件系统应该允许插件通过配置文件或环境变量等方式进行定制,以满足不同用户或不同环境的需求。

隔离和安全性:合理的插件架构应该确保插件之间以及插件与主应用程序之间有适当的隔离,以保护应用程序的整体安全性和稳定性。
1
2
3
4
5
6
7
8
9
10
11
Python 中实现插件架构的例子包括:

使用 setuptools 的 entry points:setuptools 提供了 entry points 机制,这是一种用于发现和加载插件的方法。开发者可以在 setup.py 文件中指定 entry points,然后在应用程序中通过 pkg_resources 或 importlib.metadata(Python 3.8+)来发现和加载符合 entry points 的模块。

使用专门的插件框架:如 pluggy(pytest 用它实现了插件系统)、yapsy、pluginbase 等。这些框架提供了插件的发现、加载和管理的更高级抽象。

自定义插件架构:开发者也可以根据自己的需求实现自定义的插件系统。这可能包括定义接口、编写插件加载机制和管理工具等。

利用插件架构,Python 应用程序可以变得更加灵活和可扩展,更容易适应不断变化的需求。
1
2
3
4
5
6
7
二、以pluggy模块,给一个代码案例
pluggy 是一个插件管理框架,它是由 pytest 团队开发的,用于构建可扩展的应用程序。以下是使用 pluggy 构建一个简单插件系统的代码示例:

首先,你需要安装 pluggy。可以使用 pip 进行安装:

pip install pluggy
1
步骤 1: 定义钩子规范
钩子规范是接口的声明,它定义了插件需要实现的方法和所需的参数。这些规范是插件开发者遵循的蓝图,确保了所有插件都有一致的接口。

hookspecs.py

import pluggy

创建一个钩子规范管理器

hookspec = pluggy.HookspecMarker(“myproject”)

class MySpec:
“””一个包含所有钩子规范的类。”””

@hookspec
def my_hook(self, arg1, arg2):
    """一个简单的钩子规范,插件需要实现这个接口。"""
    pass

1
2
3
4
5
6
7
8
9
10
11
12
13
14
步骤 2: 实现插件
开发者根据钩子规范创建插件,提供具体的实现逻辑。

plugins.py

import pluggy

hookimpl = pluggy.HookimplMarker(“myproject”)

class MyPlugin:
“””一个插件实现,它实现了 my_hook 钩子。”””

@hookimpl
def my_hook(self, arg1, arg2):
    print(f"插件被调用,参数为:{arg1}, {arg2}")
    # 在这里执行插件的功能逻辑
    return arg1 + arg2

1
2
3
4
5
6
7
8
9
10
11
12
13
步骤 3: 注册钩子规范、注册插件、调用钩子
接下来,我们需要告诉插件管理器(PluginManager)有哪些钩子规范存在。这样,管理器才能知道哪些钩子可以被调用,以及它们应该接受哪些参数。

main.py

import pluggy
import hookspecs
import plugins

创建一个插件管理器

pm = pluggy.PluginManager(“myproject”)

将钩子规范注册到插件管理器中

pm.add_hookspecs(hookspecs.MySpec)

注册插件

pm.register(plugins.MyPlugin())

调用插件

result = pm.hook.my_hook(arg1=10, arg2=20)
print(result)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
在这个过程中:

钩子规范 提供了一个统一的调用接口。
插件管理器 负责维护插件和钩子的注册信息,并在需要时调用正确的插件。
插件 提供了钩子的具体实现。
1
2
3
这种模式的优点是,主程序不需要知道插件的具体实现细节,只需要按照钩子规范调用接口即可。这允许主程序和插件开发者独立工作,只要遵守共同的规范。此外,可以随时添加或移除插件,而不需要修改主程序的代码,这提高了程序的模块化和可扩展性。

你运行 main.py 文件时,它会创建一个插件管理器,向管理器注册钩子规范和插件,并调用 my_hook 钩子。插件的 my_hook 方法会被执行,并打印参数和返回结果。

这个例子非常简单,但它展示了 pluggy 的基本用法,包括钩子规范的定义、插件的实现和它们的注册与调用。在实践中,pluggy 可以用于构建复杂的插件化系统,例如 pytest 测试框架就是一个很好的例子。

三、与传统继承类的区别
本质上,钩子规范和插件系统与抽象基类(ABCs)和继承确实有一些共同之处,但也存在关键的差异。让我们来探讨一下这两种概念:

相似之处:

接口定义:

抽象基类定义了一组抽象方法,子类必须实现这些方法。

钩子规范定义了一组钩子接口,插件必须实现这些接口。
1
2
3
封装和扩展性:

抽象基类允许通过创建新的子类来扩展功能。

钩子允许通过添加新的插件来扩展功能。
1
2
3
多态性:

在基于继承的系统中,多态性允许程序在运行时根据实际的子类类型来调用相应的方法。

在插件系统中,多态性允许程序在运行时根据注册的插件来调用相应的钩子实现。
1
2
3
差异之处:

松耦合与紧耦合:

继承通常产生紧耦合的关系,因为子类依赖于其基类的定义,且在编译时就确定了类之间的关系。

钩子和插件系统提供了更加松耦合的关系,因为插件可以在运行时动态加载和卸载,不需要在编译时知道具体的实现。
1
2
3
组合和灵活性:

继承可能导致类层次结构变得复杂,而且一个子类只能继承自一个基类(在不支持多重继承的语言中)。

插件系统允许以组合的方式将多个独立的插件组合在一起,每个插件可以独立地实现一个或多个钩子,为同一个钩子提供不同的行为。
1
2
3
动态性:

继承通常在编码阶段决定。类的结构在编译或解释之前就已经固定下来。

钩子和插件系统更加动态,允许在应用程序运行时动态地添加、移除或替换插件。
1
2
3
隔离性:

继承中的子类通常可以访问基类的保护成员,这在某种程度上减少了隔离性。

插件通常只能访问它们需要实现的钩子规范,不会与其他插件或主程序产生直接的依赖关系,从而保持了较高的隔离性。
1
2
3
综上所述,抽象基类和继承机制更适合于那些类结构和层次关系相对固定的场景,而钩子和插件系统提供了更高的灵活性和动态性,更适合于需要运行时扩展和修改的应用程序。两者都是解决代码复用和抽象的有效手段,但选择哪种方式取决于具体的设计需求和上下文环境。

有价值的资源:
https://developer.aliyun.com/article/308565

原文链接:https://blog.csdn.net/ningyanggege/article/details/135663015

LangGraph Studio:可视化调试基于LangGraph构建的AI智能体

之前我们在第一时间介绍过使用LangChain的LangGraph开发复杂的RAG或者Agent应用,随着版本的迭代,LangGraph已经成为可以独立于LangChain核心,用于开发多步骤、面向复杂任务、支持循环的AI智能体的强大框架。

近期LangGraph推出了一个使得复杂AI智能体开发更加高效的工具:LangGraph Studio,一个为可视化测试与调试基于LangGraph构建的AI智能体而设计的集成环境。本文将带领大家初步探索这个新的工具。

  1. 回顾LangGraph并创建测试智能体

LangGraph是用于构建基于LLM的复杂智能体的底层框架(注意LangGraph并非类似于Coze这样的低代码Agent构建平台),它的确更复杂但也更强大(与其类似的是另一家主流框架LlamaIndex推出的Workflows)。主要特点有:

**基于图结构定义的AI工作流
**

**支持复杂的循环与条件分支
**

**细粒度的智能体控制,而非智能体“黑盒子”
**

智能体状态的持久化,可单步控制、暂停与恢复

支持多智能体开发、人机交互工作流

现在让我们参考官方应用构建一个简单的测试智能体,这个智能体的Graph图定义如下:

这是一个非常简单的智能体,流程描述如下:

用户输入问题

调用LLM获得问题答案,并决定是否需要调用搜索工具

如果需要,则调用搜索引擎获得结果,并返回给LLM继续

如果不再需要搜索,则给出答案,流程结束

现在使用LangGraph实现这个智能体:

【定义State】

定义在工作流中传递与保持的“状态”数据,可以理解为全局共享数据:


from typing import TypedDict,TypedDict, Annotated, Sequence   from langgraph.graph import StateGraph, END   from langgraph.graph import add_messages   from langchain_core.messages import BaseMessage   from langchain_openai import ChatOpenAI   from langgraph.prebuilt import ToolNode   from langchain_community.tools.tavily_search import TavilySearchResults      class AgentState(TypedDict):`        `messages: Annotated[Sequence[BaseMessage], add_messages]




【定义Node】

定义一个工作流中的处理节点,这里主要有两个:LLM调用与搜索引擎调用。另外定义一个辅助判断方法,用来决定是否需要调用搜索引擎。

# 调用搜索引擎的工具节点,利用ToolNode构建  
tools = [TavilySearchResults(max_results=1)]  
tool_node = ToolNode(tools)  
  
# 调用大模型  
def call_llm(state):  
    messages = state["messages"]  
    messages = [{"role": "system", "content": "你是一个中文智能小助手。"}] + messages  
    model = ChatOpenAI(temperature=0, model_name="gpt-4o-mini")  
    model = model.bind_tools(tools)  
    response = model.invoke(messages)  
    return {"messages": [response]}  
  
# 一个辅助方法:判断是否需要调用工具  
def should_continue(state):  
    messages = state["messages"]  
    last_message = messages[-1]  
  
    #根据大模型的反馈来决定是结束,还是调用工具  
    if not last_message.tool_calls:  
        return "end"  
    else:  
        return "continue"

【定义Graph】

现在你可以定义Graph – 也就是智能体的工作流。

# 定义一个graph  
workflow = StateGraph(AgentState)  
  
# 增加两个节点  
workflow.add_node("llm", call_llm)  
workflow.add_node("search", tool_node)  
  
# 确定入口  
workflow.set_entry_point("llm")  
  
# 一个条件边,即从llm节点出来的两个分支及条件  
workflow.add_conditional_edges(  
    "llm",  
    should_continue,  
    {  
        "continue": "search",  
        "end": END,  
    },  
)  
  
# search调用后返回llm  
workflow.add_edge("search", "llm")  
  
#编译  
graph = workflow.compile()  
  
#本地测试代码  
if __name__ == "__main__":  
    while True:  
        user_input = input("User: ")  
        print("User: "+ user_input)  
        if user_input.lower() in ["quit", "exit", "q"]:  
            print("Goodbye!")  
            break  
          
        response = graph.invoke({"messages": [("user", user_input)]})  
        print(response["messages"][-1].content)

这里加上了本地测试代码,创建完成后可以在本地运行测试。

  1. LangGraph Studio是什么?

LangGraph Studio是LangChain推出的专门用于复杂智能体与RAG应用可视化、交互与调试的桌面集成开发环境。借助于LangGraph Studio,你可以非常直观的观察所创建的AI智能体的工作流程,并与其进行交互来调试它的多步骤任务,监控各个步骤的状态与输入输出,从而更快的发现故障与修复。

需要注意的几点是:

LangGraph Studio不是一个快速创建LangGraph智能体的开发平台(至少目前还不是)。

LangGraph Studio的调试是针对LangGraph智能体的Cloud部署模式,即:将智能体部署在独立的API Server中,并通过API调用智能体。

使用时,LangGraph Studio会把你的智能体打包成docker image,并在本地启动测试的API Server。原理如下图:

LangGraph Studio使用需要LangSmith的账号,可以去LangSmith免费注册,并获得LangSmith的API_Key。

  1. 用LangGraph Studio在本地加载智能体

【准备工作:Docker安装】

由于LangGraph Studio需要启动一个Docker作为API Server,因此依赖于Docker Engine,请首先安装Docker Desktop,并确保docker-compose的版本为2.22.0或更高版本。

【步骤1:下载LangGraph Studio并安装】

进入LangGraph Studio的github项目地址(搜索langgraph-studio),下载桌面客户端(暂时只有Mac版本,很快支持其他平台)。下载完成后安装打开,并使用LangSmith账户登录(免费账户也可)。

【步骤2:配置项目目录】

为了让LangGraph Studio能够认识并在构建的API Server(Docker Image)中加载你的智能体,你的智能体项目需要满足必要的项目结构,一般类似于:

这里的agent.py为基于LangGraph的智能体代码(参考上文),此外需要三个基本的配置:

langgraph.json:基本配置文件。定义依赖项、环境变量、智能体路径等配置的文件。下图是例子配置,内容很好理解,就是一些路径和指向,请根据自己实际的目录结构进行修改。

requirements.txt:项目依赖。用来在docker中运行Agent。下图是例子配置:

.env:智能体运行时需要的环境变量,比如OPENAI_API_KEY等。这里我们的配置项包括:

【步骤三:用LangGraph Studio加载智能体】

确保Docker后台在运行。

打开LangGraph Studio,使用LangSmith账户登录。

导航到你的langgraph.json文件所在的项目目录,并选择该目录打开。

如果一切正常,一段时间后(构建docker image并启动),你将会看到代理的可视化表示。常见的问题通常和配置错误有关,比如不正确的LangSmith的API Key,或者配置中的目录名称错误等。

用LangGraph Studio调试智能体

LangGraph Studio通过调用本地API Server(docker)中的智能体服务相关接口来向使用者提供一系列跟踪与调试功能,包括:

与智能体对话:发送消息并接受反馈
在左上角菜单中选择需要调试的智能体Graph,然后在下方的Input区域,选择+Message,添加你的输入消息,然后点击Submit,就可以调用智能体:

智能体的响应会显示在右侧区域,会清晰地显示每个步骤的执行情况:

编辑消息
LangGraph Studio一个重要的调试功能是可以查看当前运行线程中的某个步骤的消息,并对其进行编辑后创建一个新的“分支”运行,后续相关的步骤会在此基础上自动重新运行。通过这种方法,你可以观察到不同情况下的不同输出结果。比如,这里我们把这里搜索的结果手工做个修改:

然后点击下方的“Fork”按钮,此时智能体会从该节点生成一个新的“分支”运行,并重新输出结果。你可以点击下方的多个Fork之间的切换箭头来查看不同的结果,这对于智能体调试中观察不同中间结果的影响非常有用:

设置中断
LangGraph Studio允许给需要调用的智能体设置中断(interrupts)来控制工作流程。这有点类似程序调试中的断点与单步执行:你可以在特点节点的前后来暂停工作流的运行以进行观察,然后决定是否继续。这可以用于对智能体的每一步行为进行细粒度观察与控制,以排除某种故障。

在左侧窗口区域右上角的菜单点击Interrupts按钮,然后选择需要中断的节点及时间点(节点前与节点后),然后运行。比如这里对所有节点设置中断,就会发现输出窗格中需要确认“Continue”后才会继续运行,否则将会一直阻塞等待:

运行线程管理
你可以在客户端管理多个智能体运行线程,而不互相影响。在右侧区域左上角菜单中选择线程进行切换,或者创建一个新的线程,打开新的窗口,这样就可以启动一个新的会话。

与LangSmith/VSCode的集成
LangGraph Studio与同属一家的LangSmith有着良好的集成,你可以在LangSmith中打开项目,可以查看到智能体的每一次运行的详细细节、输入输出、耗时、tokens使用、提示词等:

此外,你可以在LangGraph Studio中直接打开VScode对智能体代码进行编辑修改,修改后智能体会实时更新并部署到docker中,然后就可以重新调试,这对于需要反复修改迭代的智能体开发非常有用。

LangGraph Studio提供了一种可视化调试AI智能体的实用方法。它与LangGraph、LangSmith一起组成了一个构建复杂AI智能体的强大工具集。这个工具集既具有了底层开发框架的灵活(相对于低代码开发平台更强大与可控),也兼顾了智能体在跟踪与调试时所需要的简洁易用。
LangGraph Studio:可视化调试基于LangGraph构建的AI智能体

原文链接:https://blog.csdn.net/m0_63171455/article/details/142642259

http://www.360doc.com/content/24/0412/19/47115229_1120203963.shtml

Python中常用的装饰器@classmethod、@abstractmethod、@property和@staticmethod

在Python编程中,装饰器是一种强大而灵活的工具,可以在不修改源代码的情况下修改函数或类的行为。本文将介绍几个常用的装饰器,包括@classmethod@abstractmethod@property@staticmethod,并提供代码示例,以帮助你更好地理解它们的用法。

@classmethod

@classmethod装饰器用于定义类方法(classmethods)。类方法与普通方法不同,它在类层级上操作,而不是在实例层级上。通过类方法,我们可以直接通过类名调用方法,而无需创建类的实例。

以下是一个使用@classmethod装饰器定义类方法的示例:

class MathUtils:
    @classmethod
    def multiply(cls, a, b):
        return a * b

result = MathUtils.multiply(5, 3)
print(result)  # 输出: 15

在上面的示例中,MathUtils类定义了一个类方法multiply,通过@classmethod装饰器标记。类方法的第一个参数通常被命名为cls,它指向类本身。通过类方法,我们可以直接在类层级上进行操作,而无需实例化类。

@abstractmethod

@abstractmethod装饰器用于定义抽象方法(abstract methods)。抽象方法在基类中声明但没有具体实现,需要在派生类中进行实现。如果一个类中包含抽象方法,那么该类必须声明为抽象类,无法实例化。

以下是一个使用@abstractmethod装饰器定义抽象方法的示例:

from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self):
        pass

class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius
    
    def area(self):
        return 3.14 * self.radius ** 2

# 创建 Circle 对象
circle = Circle(5)
print(circle.area())  # 输出: 78.5

在上面的示例中,Shape类是一个抽象基类,其中包含一个抽象方法area。通过使用@abstractmethod装饰器,我们可以声明area方法为抽象方法,无需提供具体实现。派生类Circle继承了Shape类,并实现了area方法,使其具有特定的功能。

@property

@property装饰器用于将一个类方法转换为只读属性(read-only property)。通过使用@property装饰器,我们可以定义一个特殊的方法,使其在使用点符号访问时,像访问属性一样,而不是通过函数调用。

以下是一个使用@property装饰器定义属性的示例:

class Person:
    def __init__(self, name):
        self._name = name
    
    @property
    def name(self):
        return self._name

# 创建 Person 对象
person = Person("John")
print(person.name)  # 输出: John

在上面的示例中,Person类定义了一个属性name,使用@property装饰器将name方法转换为只读属性。这样,我们可以通过属性方式访问name,而无需显式调用方法。

@staticmethod

@staticmethod装饰器用于定义静态方法(staticmethods)。静态方法在类的命名空间中定义,与类的实例无关,因此不需要通过实例来调用。静态方法可以直接通过类名调用。

以下是一个使用@staticmethod装饰器定义静态方法的示例:

class MathUtils:
    @staticmethod
    def add(a, b):
        return a + b

result = MathUtils.add(5, 3)
print(result)  # 输出: 8

在上面的示例中,MathUtils类定义了一个静态方法add,通过@staticmethod装饰器标记。静态方法可以直接通过类名调用,无需实例化类。

总结

装饰器是Python中强大而灵活的工具,可以优化代码结构、提供额外功能,并提高代码的可读性。本文介绍了@classmethod@abstractmethod@property@staticmethod这几个装饰器的使用方法,并提供了相应的代码示例。

希望通过本文的介绍,你能更好地理解这些装饰器的作用,并在自己的代码中灵活应用它们。

Python设计模式-组合模式

组合模式(Composite Pattern)是一种结构型设计模式,它允许你将对象组合成树形结构来表示“部分-整体”的层次结构。组合模式使得客户端可以统一地处理单个对象和对象组合。

组合模式的结构
组合模式主要包含以下几个角色:

组件(Component):定义对象的接口,并实现一些默认行为。声明一个接口,用于访问和管理Leaf和Composite中的子组件。
叶子(Leaf):代表树的叶子节点,叶子节点没有子节点。
组合(Composite):定义有子部件的那些部件的行为,存储子部件。并在组件接口中实现与子部件有关的操作,如添加、删除等。
组合模式的示例
假设我们有一个图形绘制系统,可以绘制简单的形状如圆和方块,也可以将这些形状组合成复杂的图形。我们可以使用组合模式来实现这一需求。

定义组件

from abc import ABC, abstractmethod

class Graphic(ABC):
    @abstractmethod
    def draw(self):
        pass

    def add(self, graphic):
        raise NotImplementedError("This method is not supported")

    def remove(self, graphic):
        raise NotImplementedError("This method is not supported")

    def get_child(self, index):
        raise NotImplementedError("This method is not supported")

定义叶子

class Circle(Graphic):
    def draw(self):
        print("Drawing a circle")

class Square(Graphic):
    def draw(self):
        print("Drawing a square")


定义组合
class CompositeGraphic(Graphic):
    def __init__(self):
        self.children = []

    def draw(self):
        for child in self.children:
            child.draw()

    def add(self, graphic):
        self.children.append(graphic)

    def remove(self, graphic):
        self.children.remove(graphic)

    def get_child(self, index):
        return self.children[index]

使用组合模式
def main():
    # 创建叶子节点
    circle1 = Circle()
    circle2 = Circle()
    square1 = Square()

    # 创建组合节点
    composite1 = CompositeGraphic()
    composite2 = CompositeGraphic()

    # 组合图形
    composite1.add(circle1)
    composite1.add(circle2)

    composite2.add(square1)
    composite2.add(composite1)

    # 绘制组合图形
    composite2.draw()

if __name__ == "__main__":
    main()

在这个示例中,Graphic是抽象组件类,定义了绘制方法。Circle和Square是叶子类,分别实现了绘制方法。CompositeGraphic是组合类,实现了管理子组件的方法,并重写了绘制方法来递归绘制子组件。客户端通过组合叶子节点和组合节点来创建复杂的图形结构,并统一调用draw方法进行绘制。

组合模式的优缺点
优点
统一处理单个对象和组合对象:组合模式使得客户端可以统一地处理单个对象和对象组合,提高了代码的灵活性和可扩展性。
简化客户端代码:客户端代码可以一致地使用组件接口,而不需要关心处理的是单个对象还是组合对象。
符合开闭原则:可以通过增加新的叶子和组合类来扩展系统,而不需要修改现有代码。
缺点
增加复杂性:组合模式会增加系统中类和对象的数量,可能会使系统变得复杂。
难以限制组合层次:有时需要对组合层次进行限制,但组合模式本身没有提供这样的机制。
组合模式的适用场景
表示部分-整体层次结构:当需要表示对象的部分-整体层次结构时,可以使用组合模式。
统一处理单个对象和组合对象:当需要统一处理单个对象和组合对象时,可以使用组合模式。
构建递归结构:当需要构建递归结构(如树形结构)时,可以使用组合模式。
总结
组合模式是一种结构型设计模式,通过将对象组合成树形结构来表示“部分-整体”的层次结构,使得客户端可以统一地处理单个对象和对象组合。组合模式适用于表示部分-整体层次结构、统一处理单个对象和组合对象以及构建递归结构的场景。合理应用组合模式,可以提高系统的灵活性和可扩展性,简化客户端代码。理解并掌握组合模式,有助于在实际开发中构建高效、灵活的系统。

                        
原文链接:https://blog.csdn.net/weixin_55252589/article/details/139074443

Python设计模式-工厂方法模式

工厂方法模式(Factory Method Pattern)是一种创建型设计模式,它定义了一个用于创建对象的接口,让子类决定实例化哪一个类。工厂方法使得一个类的实例化延迟到其子类。通过使用工厂方法模式,可以将对象的创建过程与使用过程分离,从而提高代码的灵活性和可扩展性。

工厂方法模式的结构
工厂方法模式主要包括以下几个角色:

抽象产品(Product):定义产品的接口。
具体产品(ConcreteProduct):实现抽象产品接口的具体产品类。
抽象工厂(Creator):声明工厂方法,用于返回一个产品对象。可以定义一个工厂方法的默认实现。
具体工厂(ConcreteCreator):实现抽象工厂接口,重定义工厂方法以返回一个具体产品实例。
示例
假设我们有一个日志系统,可以记录日志到控制台或文件。我们可以使用工厂方法模式来实现不同日志记录方式的选择和创建。

定义抽象产品和具体产品

from abc import ABC, abstractmethod

class Logger(ABC):
    @abstractmethod
    def log(self, message: str):
        pass

class ConsoleLogger(Logger):
    def log(self, message: str):
        print(f"Console: {message}")

class FileLogger(Logger):
    def __init__(self, filename: str):
        self.filename = filename

    def log(self, message: str):
        with open(self.filename, 'a') as f:
            f.write(f"File: {message}\n")

定义抽象工厂和具体工厂

class LoggerFactory(ABC):
    @abstractmethod
    def create_logger(self) -> Logger:
        pass

class ConsoleLoggerFactory(LoggerFactory):
    def create_logger(self) -> Logger:
        return ConsoleLogger()

class FileLoggerFactory(LoggerFactory):
    def __init__(self, filename: str):
        self.filename = filename

    def create_logger(self) -> Logger:
        return FileLogger(self.filename)

使用工厂方法模式

def main():
    # 创建控制台日志记录器
    console_factory = ConsoleLoggerFactory()
    console_logger = console_factory.create_logger()
    console_logger.log("This is a console log message.")

    # 创建文件日志记录器
    file_factory = FileLoggerFactory("app.log")
    file_logger = file_factory.create_logger()
    file_logger.log("This is a file log message.")

if __name__ == "__main__":
    main()

在这个示例中,Logger是抽象产品,ConsoleLogger和FileLogger是具体产品。LoggerFactory是抽象工厂,ConsoleLoggerFactory和FileLoggerFactory是具体工厂。通过工厂方法模式,我们可以灵活地选择和创建不同类型的日志记录器,而不需要修改客户端代码。

工厂方法模式的优缺点
优点
遵循开闭原则:可以在不修改现有代码的情况下增加新产品。
提高灵活性:可以根据需要在运行时选择和创建具体的产品。
封装对象创建过程:将对象的创建过程封装在工厂类中,减少了客户端代码的复杂性。
缺点
增加代码复杂性:引入更多的类和接口,增加了代码的复杂性。
难以管理:当产品种类增多时,可能会导致工厂类的数量增加,管理起来较为困难。
工厂方法模式的适用场景
创建对象需要较复杂的过程:对象的创建过程较为复杂,包含多个步骤或涉及多个依赖时,可以使用工厂方法模式。
需要灵活地创建不同类型的对象:根据不同的条件或环境,在运行时选择和创建不同类型的对象。
遵循开闭原则:需要在不修改现有代码的情况下增加新产品。
工厂方法模式与简单工厂模式的区别
简单工厂模式:由一个工厂类负责创建所有产品,工厂类通常包含一个静态方法,根据传入的参数来创建具体产品。简单工厂模式不符合开闭原则。
工厂方法模式:将对象创建的职责分散到多个具体工厂类中,每个具体工厂类负责创建一种具体产品。工厂方法模式符合开闭原则。
总结
工厂方法模式是一种创建型设计模式,通过定义一个用于创建对象的接口,将对象的创建过程延迟到子类,从而提高代码的灵活性和可扩展性。通过使用工厂方法模式,可以在不修改现有代码的情况下增加新产品,减少了代码耦合,提高了系统的可维护性。合理应用工厂方法模式,可以显著提升代码质量和设计水平。

原文链接:https://blog.csdn.net/weixin_55252589/article/details/139072230

Python设计模式-简单工厂模式

简单工厂模式(Simple Factory Pattern)是一种创建型设计模式,它通过专门定义一个工厂类来负责创建其他类的实例,而不是在客户端代码中直接实例化对象。这样可以将对象创建的过程与使用对象的过程分离,提高代码的可维护性和可扩展性。

简单工厂模式的结构
简单工厂模式包含以下角色:

工厂类(Factory):负责创建对象的类。根据不同的条件,实例化并返回不同类型的对象。
产品类(Product):由工厂创建的对象,所有创建的对象应实现相同的接口或继承相同的基类。
客户端(Client):使用工厂类来获取产品对象,而不直接实例化产品类。
示例
假设我们要创建一个简单的工厂类来生成不同类型的动物对象。首先,我们定义一个Animal基类,然后定义两个具体的产品类Dog和Cat,最后创建一个工厂类AnimalFactory来生成这些对象。

定义产品类

class Animal:
    def speak(self):
        pass

class Dog(Animal):
    def speak(self):
        return "Woof!"

class Cat(Animal):
    def speak(self):
        return "Meow!"

定义工厂类

class AnimalFactory:
    @staticmethod
    def create_animal(animal_type: str) -> Animal:
        if animal_type == 'dog':
            return Dog()
        elif animal_type == 'cat':
            return Cat()
        else:
            raise ValueError(f"Unknown animal type: {animal_type}")

使用工厂类

def main():
    factory = AnimalFactory()

    dog = factory.create_animal('dog')
    print(dog.speak())  # 输出:Woof!

    cat = factory.create_animal('cat')
    print(cat.speak())  # 输出:Meow!

    try:
        unknown = factory.create_animal('bird')
    except ValueError as e:
        print(e)  # 输出:Unknown animal type: bird

if __name__ == "__main__":
    main()

简单工厂模式的优缺点
优点
单一职责原则:工厂类负责对象的创建,客户端代码负责使用对象,各自关注自己的职责。
提高可维护性:将对象创建的逻辑集中在一个地方,便于修改和维护。
提高可扩展性:如果需要添加新的产品,只需修改工厂类而不需要修改客户端代码。
缺点
违反开闭原则:每次添加新产品时,都需要修改工厂类,增加了工厂类的复杂性。
单一工厂类过于复杂:随着产品种类的增加,工厂类可能变得臃肿,难以维护。
简单工厂模式的适用场景
对象创建过程复杂:如果对象的创建过程较为复杂,或者需要根据不同条件创建不同类型的对象,可以使用简单工厂模式。
客户端不需要知道具体产品类:客户端只需要使用工厂类来获取对象,不需要了解具体的产品类。
总结
简单工厂模式是一种创建型设计模式,通过定义一个工厂类来创建不同类型的对象,将对象创建的过程与使用对象的过程分离,提高代码的可维护性和可扩展性。尽管简单工厂模式有一些缺点,如违反开闭原则,但在某些场景下仍然非常有用。通过合理使用简单工厂模式,可以有效地简化对象的创建过程,提升代码的质量。

原文链接:https://blog.csdn.net/weixin_55252589/article/details/139070003

Python中类的组合使用-组件

组合:在一个类的属性中调用了另一个类,将另一个类的对象作为数据属性,称为类的组合。

这种组合使用的好处在于,它允许你将复杂的功能分解为更小、更易于管理的部分。每个类可以专注于自己的职责,从而提高代码的可读性和可维护性。

这种模式允许你创建一个对象,这个对象可以与创建它的对象进行交互。这是一种常见的设计模式,特别是在复杂的软件系统中,它允许你将不同的功能组织成不同的类,同时保持这些类之间的协作和通信。

简单来说,这种初始化方式是说:“我(当前类的实例)需要一个组件,所以我创建了一个实例,并且把我自己(self)作为参数传给了它,这样它就可以使用我的功能和数据了。”

再来一个例子

# 1、组合实例
class Ojb_1:
    '''假设Ojb_1是一个装备库类,func_name是其中一件装备,装备后加1000战力。'''
    def __init__(self, agg):
        self.agg = agg

    def func_name(self):
        self.agg += 1000                # 在本身的战力上+1000
        return self.agg                 # 返回最终战力

class Ojb_2:
    '''假设Ojb_2是一个角色类,每个角色都有名称等信息'''
    def __init__(self, name, agg):
        self.name = name                # 角色名称
        self.agg = agg                  # 本身的战力
        self.Ojb_1 = Ojb_1(self.agg)    # 重点在这里,将该人物原来的战力传到装备库类,把自己作为参数传给ojb_1的这个实例,这样他能使用自己的数据

if __name__ == '__main__':
    '''假设广深小龙原来战力=500'''
    r1 = Ojb_2('广深小龙', 500)
    res = r1.Ojb_1.func_name()
    print(res)

①Ojb_1是装备库类,func_name是一件装备,人物装备后会增加1000的战力,self.agg=原人物的战力

②Ojb_2是角色类,有角色的名称与原角色的战力等信息,self.Ojb_1是将原人物的战力先传至装备库,只要人物调用装备了func_name就会在原战力基础上增加1000

下面来一个完整的例子:

class ResearchConductor:
    def __init__(self, researcher):
        self.researcher = researcher

    async def conduct_research(self):
        # 这里使用 researcher 对象的属性和方法来执行研究
        print(f"Starting research on: {self.researcher.query}")
        # 假设这里进行了一些异步研究操作
        # 返回研究结果
        return f"Research results for {self.researcher.query}"


class ContextManager:
    def __init__(self, researcher):
        self.researcher = researcher

    async def get_context(self):
        # 获取研究上下文的逻辑
        return "Context for research"


class ReportGenerator:
    def __init__(self, researcher):
        self.researcher = researcher

    async def generate_report(self, context):
        # 生成报告的逻辑
        return f"Report based on context: {context}"


class Researcher:
    def __init__(self, query, config):
        self.query = query
        self.config = config
        self.research_conductor = ResearchConductor(self)  # 组合使用 ResearchConductor
        self.context_manager = ContextManager(self)  # 组合使用 ContextManager
        self.report_generator = ReportGenerator(self)  # 组合使用 ReportGenerator

    async def perform_research(self):
        # 执行研究流程
        context = await self.context_manager.get_context()  # 从 ContextManager 获取上下文
        research_results = await self.research_conductor.conduct_research()  # 从 ResearchConductor 获取研究结果
        report = await self.report_generator.generate_report(research_results)  # 从 ReportGenerator 生成报告
        return report


# 假设这是主程序
import asyncio

async def main():
    # 创建 Researcher 实例
    researcher = Researcher(query="How to learn Python", config={"setting": "value"})
    # 执行研究并生成报告
    report = await researcher.perform_research()
    print(report)

# 运行主程序
asyncio.run(main())

在这个例子中:

  • Researcher 类是主类,它代表了一个研究者,拥有查询和配置。
  • ResearchConductor 类是 Researcher 的一个组件,负责执行研究任务。
  • ContextManager 类是另一个组件,负责获取研究的上下文。
  • ReportGenerator 类是第三个组件,负责基于研究结果生成报告。

每个组件都接收一个 researcher 对象作为参数,并在它们的构造函数中保存这个引用。这样,每个组件都可以访问主 Researcher 实例的属性和方法。

Researcher 类中的 perform_research 方法展示了如何使用这些组件来执行一个完整的研究流程:获取上下文、进行研究、生成报告。

最后,main 函数创建了一个 Researcher 实例,并调用 perform_research 方法来执行研究流程,并打印出生成的报告。这个例子展示了类的组合如何在实际的异步编程中被用来构建模块化的系统。

理解初始化 def __init__(self, researcher):
self.researcher = researcher 你将 self(即当前 Researcher 实例的引用)传递给 ResearchConductor 的构造函数。这意味着 ResearchConductor 类需要一个参数来初始化,这个参数是 Researcher 的一个实例。在 ResearchConductor 类中,这个参数被赋值给 self.researcher,所以,当你在 Researcher 类中创建 ResearchConductor 的实例时,你是在告诉 ResearchConductor:“嘿,这个 Researcher 实例是你的 researcher 对象。”这样,ResearchConductor 类就可以通过 self.researcher 访问 Researcher 实例的所有属性和方法

再来一个例子

class Engine:
    """这是一个简单的引擎类"""
    def __init__(self, power):
        self.power = power
 
    def start(self):
        print(f"引擎启动,功率:{self.power}")
 
class Car:
    """这是一个简单的汽车类,它组合了Engine类"""
    def __init__(self, make, model, engine):
        self.make = make
        self.model = model
        self.engine = engine  # 引擎作为Car类的一个属性
 
    def start_car(self):
        self.engine.start()  # 使用Engine类的start方法
        print(f"汽车 {self.make} {self.model} 启动了。")
 
# 创建一个Engine实例
engine = Engine(200)
 
# 创建一个Car实例,并使用上面创建的Engine实例
car = Car("Ford", "Mustang", engine)
 
# 启动汽车
car.start_car()

在这个例子中,Engine 类定义了一个引擎的基本属性和方法。Car 类则通过将 Engine 实例作为其属性来组合 Engine 类,从而拥有启动汽车所需的功能。当你创建一个 Car 实例并调用 start_car 方法时,它将使用内部 Engine 实例的 start 方法来输出引擎启动的信息。