DeepSeek总结的使用实体-组件-系统和基于存在性处理进行Python编程18-20

18 --- 添加/移除 = 插入/删除

在标志的世界里,状态转换是一次写入。让一个生物饥饿,设置 is_hungry = True。让它不再饥饿,设置 is_hungry = False。标志一直存在;只有它的值改变了。

在存在性的世界里,状态转换是表之间的移动 。让一个生物饥饿,插入 一行到 hungry 表中。让它不再饥饿,移除这一行。该状态没有要翻转的字段;它只有一个问题:该生物当前是哪个表的一行。

python 复制代码
# 标志(规范的 Python 教程)
def become_hungry_flag(is_hungry: np.ndarray, slot: int) -> None:
    is_hungry[slot] = True

# 存在性
def become_hungry_presence(hungry: list[int], creature_id: int) -> None:
    hungry.append(creature_id)

def stop_being_hungry_presence(hungry: np.ndarray, creature_id: int) -> np.ndarray:
    pos = np.where(hungry == creature_id)[0]
    if pos.size:
        # swap_remove:将最后一个条目移动到释放的槽位,然后删除最后一个
        hungry[pos[0]] = hungry[-1]
        return hungry[:-1]
    return hungry

"但我只是设置了布尔值,问题是什么?"

本章要求你放弃的 Python 惯用法比 is_hungry 更古老、更普遍。它是 creature.alive = False------软删除。每个介绍类的 Python 教程都会教它:当一个东西应该停止被处理时,设置一个布尔值,并在处理它之前检查该布尔值。成千上万的生产代码库正是基于这种模式运行的。

成本是真实的。根据 code/measurement/alive_fraction.py,在 1,000,000 个生物上,不同存活率下的一次运动更新:

存活率 AoS(for c if c.alive numpy 布尔掩码 numpy 存在性(id) 掩码/存在性
1.0 % 10.12 毫秒 0.684 毫秒 0.067 毫秒 10.2 ×
10.0 % 25.65 毫秒 3.868 毫秒 0.747 毫秒 5.2 ×
50.0 % 23.78 毫秒 9.470 毫秒 2.426 毫秒 3.9 ×
90.0 % 32.03 毫秒 3.426 毫秒 4.417 毫秒 0.8 ×
100.0 % 34.16 毫秒 1.616 毫秒 4.968 毫秒 0.3 ×

阅读这些行。在 1% 存活率下------对于像"饥饿"、"濒死"或"刚刚生成"这样的瞬态状态的典型情况------存在性比布尔掩码版本快 10 倍,比 AoS 版本快 150 倍。 随着存活率的上升,差距缩小;大约在 80-90% 存活率时,布尔掩码开始胜出,在 100% 存活率时它更快(numpy 发现全 True 掩码,并使用连续的切片路径而不是花式索引)。

AoS 列在 25-35 毫秒之间持平,与存活率无关。解释器遍历所有 一百万个生物,并在每个生物上支付 getattr(c, "alive") 的成本,即使 99% 的生物稍后被跳过。"软删除"模式节省了实际工作,但从未逃脱每个元素的分发税。

对表格的诚实解读:存在性是瞬态状态的正确默认选择 (低存活率,饥饿/濒死/即将醒来的睡眠状态的常见情况);布尔掩码是近乎通用状态的正确默认选择 (存活率 ≥ 90%);AoS 在所有存活率下都是错误的。没有任何规模范围能让解释器循环胜出。

!NOTE

"存活"比本章使用的范围更广。 在一款大型多人在线角色扮演游戏中,相关的生物集合是玩家渲染半径内的生物------当 CPU 紧张时,半径本身可以动态缩小,根据[第 4 节](#第 4 节)的滴答预算余量来权衡可见生物数量。存在性表是一个查询,而不是一个形而上学状态 ;当系统提出不同问题时,它的条目会改变。"存活"、"饥饿"、"在范围内"、"已订阅"、"本帧活跃"------相同的形态,不同的问题。上面的交叉点数字适用于你的模拟正在提问的任何问题,以及答案恰好具有的任何比例。

值得命名的两个后果

转换是结构性的。 当一个生物越过饥饿阈值时,hungry 表中会实际出现或消失一行。没有就地修改;表增加一行或减少一行。这就是为什么[第 22 节](#第 22 节)(变更缓冲区;清理是批量的)存在的原因------在一个滴答期间的添加和移除必须排队,然后在边界处应用,以便正在进行的迭代不会看到一半的更改。延迟清理模式诞生于本节。

词汇消失了。 没有 set_hungry(True),没有 set_hungry(False),没有 is_hungry() 访问器对。有 become_hungry(插入)和 stop_being_hungry(移除),甚至这些通常也被内联到检测转换的系统中。数据导向程序没有 getter 和 setter;它有在表之间移动行的系统。 没有 @property。没有 __setattr__ 钩子。没有"验证存在于模型上"的装饰器。检测阈值的系统就是 验证,就是 转换,就是审计跟踪。

一个有用的测试:你能在不命名 bool 的情况下描述转换吗?"这个生物变得饥饿了" ------嗯,有什么改变吗?是的:hungry 表增加了一个条目。"这个生物不再饥饿了"------表减少了一个条目。系统中的每个状态变化都有一个结构性的对应物,而这个结构性的对应物就是规范描述。

多表转换

相同的模式处理更丰富的转换。想象一个生物可以处于饥饿、困倦或死亡状态。三个表:hungrysleepydead。一个生物通过在它们之间移动来进行转换。在饥饿时变得困倦,向 sleepy 表添加一行(它可以同时处于两个状态)。死亡从 hungrysleepy 中移除该生物(清理影响所有相关的存在性表)并添加到 dead 中。转换是一个多表操作,但每个表仍然只是一个 id 的 numpy 数组。

这种形态------作为插入和移除的状态变化------是 EBP 为你提供的所有其他东西的先决条件。[第 19 节](#第 19 节) 中的分发是直接遍历表 ,因此表的内容作为世界的规范状态在结构上是必要的。没有需要咨询的标志;只有现在表中有什么。

练习

  1. 饥饿转换。 使用你来自[第 17 节](#第 17 节)的 hungry 表。每个滴答:读取 energy;对于任何低于阈值的生物,追加到一个 hungry_to_add 缓冲区;对于任何返回到高于阈值的生物,追加到一个 hungry_to_remove 缓冲区;在滴答边界应用两者。运行 100 个滴答,能量随机变化;验证 hungry 总是精确地包含当前能量低于阈值的生物。
  2. 运行存活率示例。 uv run code/measurement/alive_fraction.py。注意交叉行------布尔掩码开始击败存在性的存活率。注意 AoS 列没有交叉点;它在每个比例下都失败。
  3. 没有布尔值,没有 setter。 在你的代码中搜索生物上的任何布尔字段。用一个存在性表替换它。setter 和 getter 都消失了。搜索任何包装状态字段的 @property 装饰器;同样的命运。
  4. 第二个存在性状态。 添加一个 sleepy 表。一个生物是困倦的,如果它的能量高到不需要立即进食 。一个生物可以同时处于 sleepyhungry 状态吗?不------根据定义,这两个条件是互斥的。(或者:设计它们,使其互斥。)通过在每滴答后检查 np.intersect1d(hungry, sleepy).size == 0 来验证不变量。
  5. 死亡。 添加一个 dead 表。当一个生物的能量降到零以下时,追加到 dead hungry 中移除(如果存在,也从 sleepy 中移除)。清理逻辑现在是多表的;引入一个小型的 transition_to_dead(ids, hungry, sleepy, dead) 辅助函数,处理所有受影响的存在性表。
  6. 转换日志。 添加 events: list[tuple[int, int, str]](滴答号,生物 id,事件名称)。每次插入/移除都会产生一行。在 100 次滴答后,事件日志是规范历史 ------记录的每个状态变化。这是[第 37 节------日志就是世界](#第 37 节——日志就是世界)的预览。
  7. (挑战) 从日志重建。 仅给定事件日志和初始生物 id,重建最终的 hungrysleepydead 表。重建是一次性的回放;如果它产生与实时模拟相同的表,那么你的转换被正确捕获了。
  8. (挑战) 在你的机器上的交叉点。 在 70% 到 95% 之间更精细地改变存活率重新运行示例------比如说在 70、75、80、85、90、95%。找到在你的硬件上掩码和存在性交叉的存活率。确切的交叉点取决于缓存大小、分支预测器和特定的 numpy 构建。

接下来是什么

[第 19 节------EBP 分发](#第 19 节——EBP 分发) 命名了表成员资格表示使其免费的分发形态。

19 --- EBP 分发

一个需要对饥饿生物采取行动的系统有两种找到它们的方法。

过滤迭代。 遍历所有生物;对于每个生物,问"它饥饿吗?";如果饥饿则进行工作:

python 复制代码
for slot in range(len(creatures)):
    if is_hungry[slot]:
        drive_hunger_behaviour(slot)

基于存在性的分发。 直接遍历 hungry 表;为每个条目进行工作:

python 复制代码
for creature_id in hungry:
    drive_hunger_behaviour(creature_id)

在 numpy 中,两种形态都提升为一次批量操作:

python 复制代码
# 过滤的(基于掩码)
energy[is_hungry] -= HUNGER_BURN_RATE * dt

# EBP(基于存在性)
energy[hungry] -= HUNGER_BURN_RATE * dt

两者产生相同的结果。两者的成本非常不同。

过滤版本为每个生物评估 is_hungry------一次 1,000,000 字节的扫描以找到 100,000 个饥饿的生物。EBP 版本读取 hungry 的 100,000 个条目并直接索引。根据 code/measurement/alive_fraction.py(第 18 节的示例),在 10% 的稀疏度下,存在性版本比布尔掩码版本快 5 倍 ,在 1% 时快 10 倍。大多数模拟器状态是稀疏的------在任何给定的滴答中,一小部分生物在进食,一小部分在繁殖,一小部分在死亡------因此 EBP 的复合优势随处可见。

一个有用的直觉:这就像一个漫无目的的购物者试图记住他们需要什么和一个有购物清单的购物者之间的区别。清单版本更短、更快,并且在构造上是正确的。你不会查看清单来问"这个过道在我的清单上吗?"------你沿着清单走,每个过道访问一次。

简化为"过滤迭代"的三种 Python 反形态

Python 教程教授了几种模式,它们都相当于过滤迭代。每种在页面上看起来不同;它们都咨询每个实体的谓词,而不是遍历存在性表。

1. isinstance 链。 当实体被建模为类层次结构时------Hungry(Creature)Sleepy(Creature)Dead(Creature)------分发通常遍历一个大列表:

python 复制代码
# 反模式:错误的!
for entity in entities:
    if isinstance(entity, Hungry):
        drive_hunger(entity)
    elif isinstance(entity, Sleepy):
        drive_sleep(entity)
    elif isinstance(entity, Dead):
        # 无事可做
        pass

列表包含每个实体;函数体根据每个实体询问类型标签谓词。存在性表版本将其拆分为三个独立的系统,每个系统遍历自己的表。

2. 多态方法分发。 "更 Pythonic"的版本使用动态分发:

python 复制代码
# 反模式:错误的!
for entity in entities:
    entity.update(dt)

其中 Creature.updateHungrySleepyDead 中被覆盖。if/elif 从源代码中消失了;它被隐藏在了 Python 的方法解析顺序中。每次迭代仍然支付属性查找、MRO 遍历和函数调用设置。谓词现在不可见了,但仍然在每个实体上被咨询,并且为每个子类类型跳入不同方法体的缓存损失是真实存在的。EBP 将其替换为三个显式函数,每个函数作用于自己的表。

3. 列表推导式过滤器。 Pythonic 的函数式风格版本:

python 复制代码
# 反模式:错误的!
hungry_creatures = [c for c in creatures if c.is_hungry]
for c in hungry_creatures:
    drive_hunger(c)

看起来 像 EBP------有一个只有饥饿生物的列表------但列表是通过扫描所有 N 个生物并分配一个包含 K 个指针的新 Python 列表来构建的。过滤过程的成本与过滤迭代版本相同,外加一次列表分配。EBP 避免了扫描,因为存在性表在状态转换发生时(第 18 节)保持更新;读取不需要重新计算它。

所有三种反形态都在迭代时咨询谓词。EBP 安排世界,使得谓词在系统运行之前就已经被回答了------表本身就是答案。

EBP 作为一个系统的样子

一个使用 EBP 的系统看起来像:

python 复制代码
def drive_hunger(hungry: np.ndarray,
                 energy: np.ndarray,
                 id_to_slot: np.ndarray,
                 dt: float) -> None:
    """读集合:hungry, id_to_slot.
       写集合:energy(仅在由 hungry 索引的槽位)。"""
    slots = id_to_slot[hungry]
    energy[slots] -= HUNGER_BURN_RATE * dt

读集合被声明。写集合被声明。没有每行的分支;表就是分发器。签名就是契约------正是[第 13 节](#第 13 节)中的系统形态。EBP 不是一个独立的概念;它是当系统的输入是存在性表时所呈现的自然形态。

EBP 也能干净地与并行性组合。一百万个有 100,000 个饥饿的生物可以跨多个进程分割------每个进程取 hungry 的一部分并完成其工作。进程永远不需要咨询不饥饿的生物;它们的读取不冲突。[第 31 节](#第 31 节) 在多进程 + shared_memory 下对此进行了详细阐述。

要点:EBP 是[第 17 节](#第 17 节)的存在性替换标志的替换所产生的结果。你不需要选择使用 EBP------一旦你的状态存在于存在性表中,每个系统都会自然地遍历它们。过滤迭代版本甚至不会出现。

练习

  1. 重读你的存活率数字。 从第 18 节练习 2 中,你有 AoS、布尔掩码和存在性在五个存活率下的测量值。相同的数字讲述了 EBP 的故事:存在性列就是 EBP 分发路径。通过将第 18 节的行标签映射到第 19 节的词汇------"存在性" = "EBP","布尔掩码" = "过滤迭代"来确认。
  2. 在生物上实现两者。 实现 drive_hunger_filtered(creatures, is_hungry, dt)(遍历生物,检查布尔列,应用消耗)和 drive_hunger_ebp(hungry, energy, id_to_slot, dt)(遍历存在性表)。在具有 10% 饥饿生物的 1M 生物世界上运行两者。使用 timeit 对两者计时。注意比率。
  3. isinstance 陷阱。 构建一个 list[Creature],其中一些是 Hungry(Creature),一些是 Sleepy(Creature),一些是普通的 Creature。通过 if isinstance(c, Hungry) 链实现分发。在 1M 生物和 10% 饥饿的情况下对其计时。现在实现 EBP 版本:三个 numpy 存在性表,三个系统函数。对其计时。比率是每个实体咨询谓词的成本。
  4. 多态方法陷阱。 将练习 3 转换为 class Hungry(Creature): def update(self): ... 和单个 for c in creatures: c.update()。对其计时。注意源代码复杂度下降 了(if/elif 消失了),但运行时成本没有------谓词移入了 Python 的方法解析顺序,在那里它仍然在每次迭代中被咨询。
  5. 列表推导式过滤器。 实现 hungry = [c for c in creatures if c.is_hungry],然后是 for c in hungry: drive(c)。对其计时。与 EBP 进行比较。注意过滤过程的成本是过滤迭代版本的成本外加 一次列表分配;EBP 版本两者都不支付,因为 hungry 表是在状态转换时维护的,而不是在读取时。
  6. 一个多状态系统。 一个生物可以是 hungrysleepydead 的任意组合。编写三个 EBP 系统:drive_hungerdrive_sleepdrive_death。每个系统 遍历自己的 存在性表。与一个处理所有三种状态带有 if/elif 的单一过滤循环进行比较。注意 EBP 版本在三个系统之间没有共享状态,并且可以平凡地并行运行它们([第 31 节](#第 31 节))。
  7. (挑战) 一个天真的 EBP 错误。 一个在遍历 hungry 的同时也调用 hungry.append 的系统会破坏迭代。(你从[第 9 节](#第 9 节)和[第 15 节](#第 15 节)知道了这一点。)构建一个小案例来演示这个错误------一个在迭代中"变得饥饿"的生物。然后通过延迟清理修复它:写入 to_become_hungry,在滴答边界应用。

接下来是什么

[第 20 节------空表是免费的](#第 20 节——空表是免费的) 命名了在规模上的结果:成本与活跃行数成正比,而不是与种群数量成正比。

20 --- 空表是免费的

如果一个存在性表是空的,那么遍历它的系统什么也不做。没有行,没有工作。这是[第 19 节](#第 19 节)在极限情况下的结果,也是让模拟器在不断变化的状态下优雅扩展的属性。

具体来说:一个有 1,000,000 个生物、当前没有饥饿生物的模拟在 drive_hunger 中花费 个周期。该系统被连接到 DAG 中,每个滴答运行,获取一个长度为 0 的 hungry id 的 numpy 数组,执行一个对零个元素进行操作的批量操作,然后返回。开销是一次函数调用和一次长度为零的花式索引------以微秒为单位测量,而不是毫秒。

这不是"在空情况下作为一种优化而快速"。这是"作为结构性结果,在空情况下是免费的"。基于标志的版本即使在没有任何标志设置的情况下也会遍历整个生物表,支付全部内存带宽来发现不需要做任何工作。EBP 版本通过一个空表的简单事实被告知没有工作。

Python 默认的失败:每个实体上的可选字段

当一个属性可能不存在时,Python 的教程反应是 disease: Optional[Disease] = None。每个 Creature 都带有这个字段;健康的生物带有 None。这看起来是免费的------毕竟 None 是一个单例------但每个实例仍然支付一个槽位,每次迭代仍然支付一次 getattr,并且存储仍然与种群数量成正比,而不是与流行率成正比。

根据 code/measurement/empty_tables.py,一百万个带有 disease 字段的生物,在四种流行率水平下:

流行率 布局 RSS 进程滴答 患病数量
0.00 % 带有 Optionallist[Creature] 105.9 MB 7.46 毫秒 0
0.00 % numpy SoA + diseased 存在性 26.5 MB 0.02 毫秒 0
0.10 % 带有 Optionallist[Creature] 106.1 MB 11.63 毫秒 1,002
0.10 % numpy SoA + diseased 存在性 26.7 MB 0.06 毫秒 1,002
1.00 % 带有 Optionallist[Creature] 106.7 MB 9.00 毫秒 10,061
1.00 % numpy SoA + diseased 存在性 26.5 MB 0.12 毫秒 10,064
10.00 % 带有 Optionallist[Creature] 113.4 MB 19.17 毫秒 99,841
10.00 % numpy SoA + diseased 存在性 26.6 MB 0.48 毫秒 99,714

首先阅读 0% 行 。在零患病生物 的情况下,可选字段布局仍然消耗 105.9 MB 的 RAM 和每个滴答 7.46 毫秒的"处理疾病"时间。它为不存在的状态支付了全部种群价格。存在性布局支付 0.02 毫秒------函数调用加上一个空的花式索引------以及一个空的 diseased 数组额外消耗约 0 KB。在零流行率下,可选布局比存在性布局慢 365 倍,内存重 4 倍。 可选布局不是在为正在发生的 事情付费;它是在为可能发生的事情付费。

阅读 10% 行 。存在性布局支付 0.48 毫秒------与 100,000 个活跃行成正比。可选布局支付 19 毫秒------与一百万的完整种群 成正比,因为循环遍历每个生物以检查 is None 谓词。随着流行率的上升,比率从 365 倍缩小到 40 倍,但存在性布局始终胜出,并且在典型的稀疏度下(在任何特定滴答中,任何特定状态下的种群比例 ≪ 10%),差距保持很大。

这个教训是普遍的。对于你可能认为是"可选状态"的每个条件------diseaseheld_itemtargetcooldown_untilaimed_atfingerprintlast_login_ipparent_pointer------规范化的 Python 形式是一个单独的存在性表,只包含当前拥有该状态的实体 ,而不是每个实体上的一个 Optional[X] 字段。

基于活动的成本

这种效应在许多状态上复合。一个有二十种可能行为的模拟,每种表示为一个存在性表,只需为实际表现出每种行为的生物比例付费。在大多数滴答中,大多数表几乎是空的。总工作量与所有表中活跃行数的总和 成正比,而不是与种群数量 × 行为数量成正比。对于一个稀疏活跃的世界,这比等效的基于标志的设计便宜一到两个数量级。

一个值得命名的微妙情况:一个空系统 不同于一个缺失的系统 。一个遍历空 hungrydrive_hunger 系统仍然在 DAG 中,仍然被调度,仍然是程序契约的一部分。它只是在这个滴答中做了零行的工作。将其完全从 DAG 中移除会改变契约;当表下一次获得一行时再将其添加回来将需要动态调度,这比一个无操作调用更难。EBP 为你提供了廉价的空间系统,而不是缺失的系统。

三个含义

基于活动的成本。 模拟器的每滴答成本由活跃的 内容决定,而不是由存在的内容决定。一百万个休眠的生物被忽略的成本为零。只有活跃的生物消耗预算。生产中的大多数模拟器都依赖于此------拥有数十万个 NPC 但只有少数在活跃游戏中的游戏世界,拥有数百万个代理但只有少数处于关键阶段的训练模拟,拥有数千个传感器但只有少数处于警报状态的控制系统。

结构稀疏性。 世界被鼓励处于大部分休息状态。将活动分散到许多小型存在性表(许多廉价的空间系统)的设计优于将活动集中在一个大的"活跃生物"标志中的设计。数据导向的思维模式是增加状态的数量(hungrysleepymatingfighting、......),而不是通过一个主开关来门控行为。

持久性也是基于活动的。 一个空的 hungry 表的快照在模式中是一行,在数据中是零行。一个长度为 1,000,000 的 is_hungry: np.ndarray 的快照是 1 MB,无论设置了多少位。备份、复制和重放都受益于相同的属性。

基于标志的思维将空闲对象视为"仍然存在,只是不活跃"。数据导向的思维将空闲对象视为不在表中。区别在于成本:前者为存在的东西付费;后者为正在发生的事情付费。

练习

  1. 对空情况计时。 使用你来自[第 19 节](#第 19 节)的模拟器,运行一个 hungry 为空的滴答。对 drive_hunger 计时。它应该在微秒范围内------函数调用加上空的花式索引,没有内部工作。
  2. 在标志形式下对相同情况计时。is_hungry.sum() == 0 的 1,000,000 个生物世界上运行布尔掩码版本的 drive_hunger。对其计时。应该是毫秒级------掩码扫描仍然遍历整个列,即使没有任何匹配。
  3. 运行示例。 uv run code/measurement/empty_tables.py。首先阅读 0% 行。注意当没有生物患病时可选布局的绝对成本。注意随着流行率下降,可选/存在性的比率扩大。
  4. 每活跃生物成本图。 使用 hungry 大小在 0、100、1,000、10,000、100,000、1,000,000 范围内运行 EBP 模拟器。在每个大小上对 drive_hunger 计时。绘图。该线大致与 K 成线性关系,从接近零开始。
  5. 添加四个更多状态。 添加 sleepymatingfightingidle 作为存在性表,每个都有自己的驱动程序系统。运行一个大多数表都是空(大多数生物处于 idle 状态)的滴答。确认每滴答成本大致仅为 idle 驱动程序的成本,加上可忽略的每系统开销。
  6. 活动直方图。 在每个滴答,为每个存在性表记录 (滴答, 表名, len)。在 1000 次滴答后,绘制 len 随时间的变化图。该图是模拟器的活动概况;平坦的线意味着世界处于休息状态,凸起意味着事件正在触发。
  7. (挑战) 移除空闲系统? 论证为什么从 DAG 中移除一个空系统(而不是让它以零工作运行)是错误的做法。提示:它会改变系统 DAG,如果下一个滴答表非空则会破坏确定性,并增加超过空调用开销的动态调度成本。
  8. (挑战) Optional[X] 扫描。 搜索你拥有的任何 Python 项目。统计数据类上的 Optional[ 类型字段。对于每个字段,问一下:在运行时,实际有多少比例的实例设置了它?如果答案是"几乎没有",那么该字段就是存在性表的候选。

接下来是什么

你已经完成了"基于存在性的处理"。下一个阶段是"内存与生命周期",从[第 21 节------swap_remove](#第 21 节——swap_remove)开始。模拟器即将开始对其表进行结构性更改------大规模的生产级的出生和死亡------而生命周期阶段将使这些更改变得廉价。

相关推荐
磊 子1 小时前
STL之deque和list以及两者与vector的对比
开发语言·c++·list
零梦ing1 小时前
Claude Code 升级后 DeepSeek API 报错 messages[x].role: unknown variant system 终极解决方案
python·claude code·deepseek api 代理
凤山老林1 小时前
DDD(领域驱动设计)在复杂业务系统中的落地指南
java·开发语言·数据库·ddd·领域驱动
凯瑟琳.奥古斯特1 小时前
子查询原理与实战案例解析
开发语言·数据库·职场和发展·数据库开发
Eiceblue1 小时前
Python 操作 Excel:数据分组、分类汇总与取消分组全解
开发语言·python·excel
山上三树2 小时前
C/C++ 高频报错速查表(开发通用版)
c语言·开发语言·c++
Tian_Hang2 小时前
Factory Method | 工厂方法
开发语言·c++
wearegogog1232 小时前
基于MATLAB实现雷达RCS Swerling模型
开发语言·matlab
暴躁小师兄数据学院2 小时前
【AI大模型应用开发工程师特训笔记】第04讲(第9章):文件目录操作
人工智能·笔记·python