【大模型推理】sglang 源码学习设计模式: 策略和访问者
是一个**“类型分发器”**。你给它一个对象,它会检查这个对象的类型,然后根据预先设定的“规则”,调用与该类型对应的特定函数。它就像一个智能的结构,但更加优雅和可扩展。当你的数据结构相对稳定,但需要频繁地为其定义新的操作时,访问者模式是绝佳的选择。反之,如果你的数据结构经常变动(比如经常增加新的Employee子类),那么使用访问者模式将会是一场灾难。回到你之前的,你可以看到它其实是访问者模式思想
阅读sglang 源码,学习python语法, 和设计模式
class TypeBasedDispatcher:
    def __init__(self, mapping: List[Tuple[Type, Callable]]):
        self._mapping = mapping
    def __call__(self, obj: Any):
        for ty, fn in self._mapping:
            if isinstance(obj, ty):
                return fn(obj)
        raise ValueError(f"Invalid object: {obj}")
 什么意思
这个类是一个基于类型的分发器,它根据输入对象的类型来调用相应的处理函数。
具体来说:
在初始化时,它接收一个列表,列表中的每个元素是一个元组,元组包含一个类型和一个可调用对象。
当实例被调用时(即调用__call__方法),它会遍历初始化时提供的映射列表,检查输入对象的类型是否与列表中的类型匹配(使用isinstance检查)。
如果找到匹配的类型,则调用对应的函数,并返回该函数的结果。
如果没有找到匹配的类型,则抛出ValueError异常。
例如,我们可以这样使用:
假设我们有两个类:A和B,然后我们定义两个处理函数:
  def handle_a(obj):
  print(“处理A类型的对象”)
  def handle_b(obj):
  print(“处理B类型的对象”)
然后创建分发器:
  dispatcher = TypeBasedDispatcher([(A, handle_a), (B, handle_b)])
当我们调用dispatcher(obj)时,如果obj是A的实例,则调用handle_a;如果是B的实例,则调用handle_b。
这种模式常用于需要根据不同类型进行不同处理的场景,类似于多分派(multiple dispatch)的一种简单实现。
这个 TypeBasedDispatcher 类是一个基于类型的分发器,它根据输入对象的类型来决定调用哪个处理函数。
好的,这是一个非常巧妙且实用的工具类。我们来详细解释一下它的含义和用途。
核心功能一句话总结
TypeBasedDispatcher 是一个**“类型分发器”**。你给它一个对象,它会检查这个对象的类型,然后根据预先设定的“规则”,调用与该类型对应的特定函数。
它就像一个智能的 if/elif/else 结构,但更加优雅和可扩展。
它是如何工作的?
- 
  
初始化 (
__init__):- 当你创建一个 
TypeBasedDispatcher实例时,你需要提供一个mapping(映射)。 - 这个 
mapping是一个列表,列表中的每个元素都是一个元组(类型, 函数)。 - 例如:
[(int, handle_integer), (str, handle_string), (list, handle_list)]。 - 这个 
mapping就定义了它的行为规则:“如果遇到整数,就调用handle_integer函数;如果遇到字符串,就调用handle_string函数……” - 它把这个规则列表存储在内部的 
self._mapping变量中。 
 - 当你创建一个 
 - 
  
执行 (
__call__):- 这个类定义了 
__call__方法,这意味着你可以像调用一个普通函数一样“调用”这个类的实例。 - 当你调用它时,例如 
dispatcher(my_object),它会:- 遍历它内部存储的规则列表 (
self._mapping)。 - 对于每一个规则 
(类型, 函数),它会使用isinstance(my_object, 类型)来检查你传入的对象的类型是否与规则中的类型匹配(或者是否是该类型的子类)。 - 一旦找到第一个匹配的规则,它就会立刻调用该规则对应的函数 
函数(my_object),并将函数的返回值作为最终结果返回。 - 如果遍历完所有规则都没有找到匹配的类型,它就会抛出一个 
ValueError异常,告诉你它不认识这个类型的对象。 
 - 遍历它内部存储的规则列表 (
 
 - 这个类定义了 
 
代码逐行分析
class TypeBasedDispatcher:
    # 这是一个“类型分发器”类
    def __init__(self, mapping: List[Tuple[Type, Callable]]):
        # 构造函数,需要传入一个“映射”或“规则”列表
        # mapping 的类型提示是: 一个列表(List),列表里是元组(Tuple),
        # 元组的第一个元素是类型(Type),第二个元素是可调用对象(Callable,通常是函数)
        self._mapping = mapping
    def __call__(self, obj: Any):
        # 这个特殊方法使得类的实例可以像函数一样被调用。
        # 例如:dispatcher_instance(some_object)
        
        # 遍历在构造时传入的规则列表
        for ty, fn in self._mapping:
            # 检查传入的 obj 是否是当前规则中 ty 类型的实例
            # isinstance 也支持继承,比 type(obj) == ty 更好
            if isinstance(obj, ty):
                # 如果是,就调用与该类型关联的函数 fn,并把 obj 作为参数传进去
                # 然后立即返回这个函数调用的结果
                return fn(obj)
                
        # 如果 for 循环正常结束(即没有在中间 return),
        # 说明没有找到任何匹配的类型规则。
        # 此时抛出一个 ValueError 异常,表示无法处理这个对象。
        raise ValueError(f"Invalid object: {obj}")
一个具体的例子
假设我们要处理不同类型的数据,并将其转换成字符串。
# 1. 定义处理不同类型的函数
def handle_integer(i: int):
    return f"这是一个整数: {i}"
def handle_string(s: str):
    return f"这是一个字符串,长度为 {len(s)}: '{s}'"
def handle_list(l: list):
    return f"这是一个列表,包含 {len(l)} 个元素: {l}"
# 2. 创建规则映射
my_rules = [
    (int, handle_integer),
    (str, handle_string),
    (list, handle_list)
]
# 3. 创建分发器实例
process_object = TypeBasedDispatcher(my_rules)
# 4. 使用分发器处理不同类型的对象
print(process_object(100))        # 调用 handle_integer
print(process_object("hello"))    # 调用 handle_string
print(process_object([1, 2, 3]))  # 调用 handle_list
# 如果我们传入一个它不认识的类型,比如 float
try:
    process_object(3.14)
except ValueError as e:
    print(e) # 打印错误信息
输出结果:
这是一个整数: 100
这是一个字符串,长度为 5: 'hello'
这是一个列表,包含 3 个元素: [1, 2, 3]
Invalid object: 3.14
为什么以及在哪里使用它?
- 
  
替代冗长的
if/elif/else:
传统的写法可能是:def process(obj): if isinstance(obj, int): return handle_integer(obj) elif isinstance(obj, str): return handle_string(obj) elif isinstance(obj, list): return handle_list(obj) else: raise ValueError(...)当类型很多时,这个
if/elif链会变得非常长且难以维护。TypeBasedDispatcher将**“判断逻辑”和“处理逻辑”**分离开来,代码更清晰、更具模块性。 - 
  
易于扩展:
如果想增加对dict类型的支持,你不需要修改TypeBasedDispatcher类的任何代码。你只需要:
a. 写一个新的函数handle_dict。
b. 在my_rules列表中增加一个新的元组(dict, handle_dict)。
这符合软件设计的开闭原则(对扩展开放,对修改关闭)。 - 
  
常见应用场景:
- 序列化/反序列化:当解析一个复杂数据结构(如 JSON)时,可以根据值的类型(string, number, array, object)来调用不同的处理函数。
 - 配置文件解析:根据配置项的值的类型执行不同的设置操作。
 - 实现访问者模式 (Visitor Pattern):这是设计模式中访问者模式的一种简化实现,用于在不改变数据结构类的情况下,为它们添加新的操作。
 
 
总之,TypeBasedDispatcher 是一个用于实现“多态分发”的优雅小工具,它让你可以根据对象的类型执行不同的代码路径,同时保持代码的整洁和可扩展性。
不是策略模式吗
  你提出了一个非常棒的问题!这说明你正在从设计模式的角度思考代码结构,这是非常好的习惯。
TypeBasedDispatcher 和 策略模式 (Strategy Pattern) 有相似之处,因为它们都涉及在运行时选择不同的行为。但是,它们的核心意图和实现方式有本质的区别。
TypeBasedDispatcher 更接近 访问者模式 (Visitor Pattern) 的一种简化和函数式实现,而不是策略模式。
让我们来详细对比一下。
核心区别一句话概括
- 策略模式:客户端 (Client) 主动选择一个“策略”(算法),并将其注入到一个“上下文 (Context)”对象中。上下文对象不关心它正在处理的数据的类型,只管用客户端给它的策略去执行任务。
 TypeBasedDispatcher(或访问者模式):分发器本身根据传入数据对象的类型来决定调用哪个函数。决策逻辑在分发器内部,而不是由外部的客户端决定。
详细对比
| 特性 | 策略模式 (Strategy Pattern) | TypeBasedDispatcher (类访问者模式) | 
  
|---|---|---|
| 决策者 | 客户端 (Client) 代码。 | 分发器 (Dispatcher) 对象本身。 | 
| 决策依据 | 通常是外部条件、配置或用户输入。 | 传入对象的类型 (type)。 | 
  
| 关注点 | 封装可互换的算法 (HOW)。如何排序?如何压缩?如何计算? | 封装对不同数据类型的操作 (WHAT)。对整数做什么?对字符串做什么? | 
| 耦合关系 | 上下文对象 (Context) 和具体策略 (Concrete Strategy) 是解耦的。上下文只依赖于策略接口。 | 分发器与它能处理的类型是紧密相关的。它必须“知道”这些类型。 | 
| 比喻 | 工具箱。你(客户端)有一个螺丝刀(上下文),你可以给它换上十字头或一字头(策略),但拧螺丝的动作是一样的。 | 前台接待员。你(数据对象)走到前台,接待员(分发器)看你的证件类型(type),然后告诉你:“你是VIP,请走这边”或“你是普通访客,请在那边排队”。 | 
  
代码示例对比
1. 策略模式示例
假设我们要对一个列表进行排序,但允许客户端选择排序算法。
from abc import ABC, abstractmethod
# 1. 定义策略接口
class SortStrategy(ABC):
    @abstractmethod
    def sort(self, data: list):
        pass
# 2. 定义具体策略
class BubbleSortStrategy(SortStrategy):
    def sort(self, data: list):
        print("Using Bubble Sort")
        # ... 冒泡排序逻辑 ...
        return sorted(data) # 简化实现
class QuickSortStrategy(SortStrategy):
    def sort(self, data: list):
        print("Using Quick Sort")
        # ... 快速排序逻辑 ...
        return sorted(data) # 简化实现
# 3. 定义上下文
class Sorter:
    def __init__(self, strategy: SortStrategy):
        self._strategy = strategy
    def set_strategy(self, strategy: SortStrategy):
        self._strategy = strategy
    def execute_sort(self, data: list):
        return self._strategy.sort(data)
# 4. 客户端代码
my_data = [3, 1, 4, 1, 5, 9]
# 客户端决定使用冒泡排序
sorter = Sorter(BubbleSortStrategy())
sorter.execute_sort(my_data)
# 客户端在运行时改变主意,决定使用快速排序
sorter.set_strategy(QuickSortStrategy())
sorter.execute_sort(my_data)
关键点:Sorter 类(上下文)并不知道自己用的是哪种排序方法。是外部的客户端代码通过 __init__ 或 set_strategy 明确地告诉它用哪个策略。
2. TypeBasedDispatcher 示例(复用之前的例子)
# (handle_integer, handle_string 等函数定义同上)
# 分发器
process_object = TypeBasedDispatcher([
    (int, handle_integer),
    (str, handle_string)
])
# 客户端代码
process_object(123)       # 分发器内部看到是 int,自己决定调用 handle_integer
process_object("world")   # 分发器内部看到是 str,自己决定调用 handle_string
关键点:客户端只管把数据“扔”给 process_object。是 process_object 内部的 isinstance 逻辑根据 123 的类型是 int 来决定调用 handle_integer。客户端没有选择权。
结论
所以,虽然两者都实现了行为的动态选择,但它们的“驱动力”完全不同:
TypeBasedDispatcher是数据驱动 (Data-Driven) 的,其行为由数据的类型决定。- 策略模式是客户端驱动 (Client-Driven) 的,其行为由客户端的选择决定。
 
因此,将 TypeBasedDispatcher 描述为策略模式是不准确的。它是一个基于类型的分发机制,思想上更接近于访问者模式,因为它解决了“如何为一系列不同类型的对象执行操作,而又不修改这些对象的类”的问题。
我们重新审视一下这个类:TypeBasedDispatcher。它根据输入对象的类型,从一组预先注册的类型-函数对中选择一个函数来执行。这实际上是一种分派机制。
现在,我们来区分一下访问者模式和策略模式:
访问者模式:通常用于在一个对象结构上执行某种操作,该操作可以作用于结构中的每一个元素,且可以在不改变各元素类的前提下定义新的操作。它通常涉及双重分派(即根据访问者和元素类型两个因素来决定执行的操作)。
策略模式:定义一系列算法,将每一个算法封装起来,并使它们可以相互替换。策略模式让算法独立于使用它的客户而变化。
在TypeBasedDispatcher中,我们根据对象的类型来选择不同的函数(算法)来执行。这更像是策略模式,其中每个类型对应的函数就是一个具体的策略,而TypeBasedDispatcher就是策略的上下文,它根据输入对象的类型来选择合适的策略。
但是,注意:策略模式通常是在运行时选择算法,而且这些算法通常是完成相同工作的不同方式。而这里,我们根据类型来分派,更接近“分派”的概念,但也可以视为一种策略选择。
然而,这种根据类型来分派的方式,在面向对象设计中,有时也被称为“类型分派”或“多分派”,它并不是典型的策略模式,因为策略模式通常不强调根据类型来分派,而是通过客户端显式地设置策略。
实际上,这个类更接近“类型分派器”或“多方法(multimethods)”的一种简单实现。在多方法中,函数的选择基于多个参数的类型(这里只有一个参数,所以是单分派)。
总结:
它不是典型的访问者模式,因为访问者模式通常要求被访问的元素有一个accept方法,而这里没有。
它也不是典型的策略模式,因为策略模式通常不根据输入类型自动选择,而是由客户端指定策略。
但是,如果我们要从这两个模式中选一个,它更接近策略模式,因为:
它封装了一组可互换的算法(每个类型对应的函数)
它根据上下文(输入对象的类型)来选择算法
不过,它也可以被视为一种“策略模式”的变体,其中策略的选择是基于输入类型的。
所以,我们可以这样理解:这是一个基于类型的策略分派器。它使用策略模式的思想,但策略的选择是根据输入对象的类型自动完成的。
因此,我们可以说它结合了类型分派和策略模式。
但是,请注意,这个类的名称是TypeBasedDispatcher,其目的主要是根据类型进行分派,而策略模式通常不强调自动分派。所以,严格来说,它更偏向于“类型分派”模式,而类型分派可以视为实现策略的一种方式。
在实际应用中,这种模式非常常见,特别是在需要处理多种类型但又不希望使用大量if-else或isinstance检查时。
所以,我们可以这样总结:这个类实现了一个基于类型的分派机制,它类似于策略模式,但策略的选择是基于类型的自动分派。
好的,我们来深入浅出地讲解一下访问者模式 (Visitor Pattern)。这是一个强大但也被认为有些复杂的设计模式。
我会用一个生动的比喻来贯穿整个解释。
比喻:一个专业的会计师团队访问一家公司
想象一下,你有一家公司,公司里有不同类型的员工:经理 (Manager)、工程师 (Engineer)、实习生 (Intern)。这些员工的类构成了你的数据结构,这个结构很稳定,员工的种类不常变动。
现在,你需要对这些员工执行各种操作,比如:
- HR 部门需要为每个人计算年终奖。
 - 财务部门需要审计每个人的税务情况。
 - IT 部门需要统计每个人使用的电脑设备。
 
糟糕的设计(没有使用访问者模式):
  你会怎么做?最直观的想法可能是在每个员工类里都加上这些方法:
class Manager:
    def calculate_bonus(self): ...
    def audit_tax(self): ...
    def check_equipment(self): ...
class Engineer:
    def calculate_bonus(self): ...
    def audit_tax(self): ...
    def check_equipment(self): ...
问题在哪里?
- 违反单一职责原则:员工类不仅要负责自己的核心业务(如管理、开发),还要负责计算奖金、报税等不相关的事情,变得非常臃肿。
 - 难以扩展新操作:如果明年公司要搞一个“评选优秀员工”的活动,你需要修改所有的员工类,给它们都加上一个 
rate_performance()方法。这违反了开闭原则(对扩展开放,对修改关闭)。 
解决方案:访问者模式
访问者模式提出了一个绝妙的想法:将操作(算法)从数据结构中分离出来。
我们把“计算奖金”、“审计税务”这些操作,封装成独立的访问者 (Visitor) 对象。员工对象则提供一个“接待”访问者的方法。
- 数据结构 (员工):只负责一件事——“我在这里,我接受一个访问者”。
 - 访问者 (会计师团队):负责对它访问的每个具体员工执行特定操作。
 
访问者模式的核心角色
- 
  
Visitor (访问者接口):
- 定义了为每一种具体元素(员工)提供的 
visit()方法。 - 例如:
visit_manager(manager),visit_engineer(engineer)。 - 比喻:会计师团队的工作手册,规定了“如何审计经理”、“如何审计工程师”。
 
 - 定义了为每一种具体元素(员工)提供的 
 - 
  
ConcreteVisitor (具体访问者):
- 实现了访问者接口。它就是那个具体的算法或操作。
 - 例如:
BonusCalculatorVisitor(奖金计算器)、TaxAuditorVisitor(税务审计员)。 - 比喻:真正的 HR 专员或财务专员,他们拿着手册去执行具体工作。
 
 - 
  
Element (元素接口):
- 被访问的对象所实现的接口。它声明了一个 
accept()方法,参数是访问者。 - 例如:
Employee接口。 - 比喻:公司规定,所有员工都必须有一个“接待来访者”的流程。
 
 - 被访问的对象所实现的接口。它声明了一个 
 - 
  
ConcreteElement (具体元素):
- 实现了元素接口。
 - 它的 
accept()方法通常只有一行代码:visitor.visit_this_type_of_element(self)。 - 例如:
Manager,Engineer类。 - 比喻:具体的经理或工程师。当会计师来访时,经理会说:“请按照审计经理的流程来审计我”。
 
 - 
  
ObjectStructure (对象结构):
- 一个包含了很多 
Element对象的容器(比如一个列表)。它提供一个方法,可以遍历所有元素,并让每个元素接受访问者。 - 比喻:整个公司的员工名册。
 
 - 一个包含了很多 
 
一个具体的例子:公司员工和不同部门的访问
from abc import ABC, abstractmethod
# 3. Element (元素接口)
class Employee(ABC):
    def __init__(self, name, salary):
        self.name = name
        self.salary = salary
    
    @abstractmethod
    def accept(self, visitor):
        pass
# 4. ConcreteElement (具体元素)
class Manager(Employee):
    def accept(self, visitor):
        # 关键点:调用 visitor 中专门为 Manager 准备的方法
        visitor.visit_manager(self)
class Engineer(Employee):
    def accept(self, visitor):
        # 关键点:调用 visitor 中专门为 Engineer 准备的方法
        visitor.visit_engineer(self)
# 1. Visitor (访问者接口)
class Visitor(ABC):
    @abstractmethod
    def visit_manager(self, manager):
        pass
    
    @abstractmethod
    def visit_engineer(self, engineer):
        pass
# 2. ConcreteVisitor (具体访问者)
class BonusCalculator(Visitor):
    def visit_manager(self, manager):
        bonus = manager.salary * 0.2
        print(f"为经理 {manager.name} 计算奖金: {bonus}")
    def visit_engineer(self, engineer):
        bonus = engineer.salary * 0.1
        print(f"为工程师 {engineer.name} 计算奖金: {bonus}")
class InventoryChecker(Visitor):
    def visit_manager(self, manager):
        print(f"检查经理 {manager.name} 的设备: 1台MacBook Pro, 1台iPhone")
    def visit_engineer(self, engineer):
        print(f"检查工程师 {engineer.name} 的设备: 1台Dell工作站, 2台显示器")
# 5. ObjectStructure (对象结构)
class Company:
    def __init__(self):
        self.employees = []
    def add_employee(self, employee):
        self.employees.append(employee)
    def accept(self, visitor):
        print(f"--- 开始执行 '{visitor.__class__.__name__}' 操作 ---")
        for employee in self.employees:
            employee.accept(visitor)
        print("--- 操作结束 ---\n")
# --- 客户端代码 ---
company = Company()
company.add_employee(Manager("Alice", 20000))
company.add_employee(Engineer("Bob", 15000))
company.add_employee(Engineer("Charlie", 16000))
# HR 团队出动,计算奖金
bonus_visitor = BonusCalculator()
company.accept(bonus_visitor)
# IT 团队出动,检查设备
inventory_visitor = InventoryChecker()
company.accept(inventory_visitor)
输出:
--- 开始执行 'BonusCalculator' 操作 ---
为经理 Alice 计算奖金: 4000.0
为工程师 Bob 计算奖金: 1500.0
为工程师 Charlie 计算奖金: 1600.0
--- 操作结束 ---
--- 开始执行 'InventoryChecker' 操作 ---
检查经理 Alice 的设备: 1台MacBook Pro, 1台iPhone
检查工程师 Bob 的设备: 1台Dell工作站, 2台显示器
检查工程师 Charlie 的设备: 1台Dell工作站, 2台显示器
--- 操作结束 ---
优点和缺点
优点:
- 完美支持开闭原则:增加新的操作非常容易,只需要创建一个新的 
ConcreteVisitor类即可,完全不需要修改现有的Employee类。 - 职责分离:数据结构(员工)和对数据的操作(访问者)被完全分离开,代码更清晰,更易于维护。
 - 集中管理操作:与某个特定操作相关的所有代码都位于一个访问者类中,而不是分散在各个元素类里。
 
缺点:
- 难以增加新的元素:这是访问者模式最大的弊端。如果你想在公司里增加一个 
Intern(实习生)类,你必须:
a. 创建Intern类。
b. 修改Visitor接口,增加一个visit_intern()方法。
c. 修改所有已经存在的ConcreteVisitor类(BonusCalculator,InventoryChecker等),为它们都实现visit_intern()方法。
这同样违反了开闭原则,只是把矛头从“难以增加新操作”转向了“难以增加新元素”。 
总结:什么时候使用?
当你的数据结构相对稳定,但需要频繁地为其定义新的操作时,访问者模式是绝佳的选择。
反之,如果你的数据结构经常变动(比如经常增加新的 Employee 子类),那么使用访问者模式将会是一场灾难。
回到你之前的 TypeBasedDispatcher,你可以看到它其实是访问者模式思想的一种简化、函数式的实现。它也做到了将操作与类型分离,但它通过 isinstance 检查和函数映射来实现,而不是通过 accept 和 visit 的“双重分派”机制。
更多推荐
 
所有评论(0)