Skip to content

语法综合(离线) ​

推导式、解包、闭包、生成器、dataclass、装饰器、异常与上下文管理器——只用 manim 加标准库,离线可跑。

本页 6 条示例。代码引自 corpus/examples/syntax/,截图为真机渲染的终态。

语法综合:推导式 / 解包 / 生成器表达式一起建图 ​

推导式 / 解包 / 生成器表达式建出的图形数量与排布和源码逻辑一致(print 断言为准)。

语法综合:推导式 / 解包 / 生成器表达式一起建图

py
"""语法综合:推导式 / 解包 / 生成器表达式一起建图

@expect 推导式 / 解包 / 生成器表达式建出的图形数量与排布和源码逻辑一致(print 断言为准)。
@note 离线组(不取第三方包,默认套件就跑)。这一组验的是**语法复杂度**而不是某个库:
      真实代码很少一段里只用一个特性——推导式套条件、生成器喂给 VGroup、zip/enumerate/
      解包混着用才是常态,只看简单例子预测不了这些会不会在桥上出问题。
"""

from manim import *
import math

COUNT = 9
palette = [BLUE, GREEN, YELLOW, RED, PURPLE, TEAL, ORANGE, PINK, MAROON]

# 列表推导 + 条件筛选 + 海象运算符
radii = [r for i in range(1, COUNT + 1) if (r := 0.18 * i) <= 1.7]
assert len(radii) == COUNT

# 推导式里直接链式构造(move_to / set_color 都返回自身,见 method/13-copy.py 同款)
circles = [Circle(radius=r).set_color(palette[i % len(palette)]) for i, r in enumerate(radii)]

# 生成器表达式 + zip:位置与物体并行推进
offsets = (0.42 * i for i in range(COUNT))
group = VGroup(*[c.move_to([x, 0, 0]) for c, x in zip(circles, offsets)])

# 字典 / 集合推导:算一条摘要(不画,只 print)
areas = {f"r{i}": round(math.pi * r * r, 2) for i, r in enumerate(radii)}
print("syntax-comprehensions", len(circles), round(sum(areas.values()), 3), len(set(radii)), sorted(areas)[:2])


class ComprehensionsScene(Scene):
    def construct(self):
        self.play(LaggedStart(*[Create(c) for c in group], lag_ratio=0.12))
        self.play(group.animate.shift(UP * 0.35).scale(1.05))
        self.wait(0.3)

语法综合:闭包 / 生成器函数 / itertools 链 ​

闭包 / 生成器函数 / itertools 链建出的图形正常,数量与 itertools 的产物一致。

语法综合:闭包 / 生成器函数 / itertools 链

py
"""语法综合:闭包 / 生成器函数 / itertools 链

@expect 闭包 / 生成器函数 / itertools 链建出的图形正常,数量与 itertools 的产物一致。
@note 曲线不是在这里写死的,而是闭包工厂攒出来、生成器按步长吐采样点——
      「函数里的函数、yield、itertools 串起来」在真实数值代码里到处都是,
      而 plot 收的是这个闭包(不是 lambda 字面量),这条也得验。
"""

from manim import *
import functools
import itertools
import math


def make_wave(amp, freq, phase):
    """闭包工厂:返回一个只认 t 的波形函数。"""
    def wave(t):
        return amp * math.sin(freq * t + phase)
    return wave


def sample(fn, start, stop, count):
    """生成器:按步长吐采样点,交给调用方消费。"""
    step = (stop - start) / (count - 1)
    for i in range(count):
        t = start + i * step
        yield t, fn(t)


wave = make_wave(1.2, 1.6, math.pi / 4)
pairs = list(sample(wave, 0.0, 6.0, 40))
ts = [p[0] for p in pairs]
ys = [p[1] for p in pairs]

# itertools / functools:前缀和、相邻配对、归约——把两条流各取一段做统计
acc = list(itertools.accumulate(ys[:5]))
adj = list(itertools.pairwise(ts[:4]))
peak = functools.reduce(lambda a, b: a if a > b else b, ys)
print("syntax-closures", round(peak, 3), len(acc), len(adj), round(sum(acc), 3))


class ClosuresScene(Scene):
    def construct(self):
        ax = Axes(x_range=[0, 6, 1], y_range=[-1.5, 1.5, 0.5])
        curve = ax.plot(lambda t: float(wave(t)), x_range=[0, 6], color=GREEN)
        self.play(Create(ax))
        self.play(Create(curve))
        self.wait(0.3)

语法综合:dataclass / 装饰器 / 类属性一起建图 ​

dataclass / 装饰器 / 类属性跑通,建出的图形与数据一致。

语法综合:dataclass / 装饰器 / 类属性一起建图

py
"""语法综合:dataclass / 装饰器 / 类属性一起建图

@expect dataclass / 装饰器 / 类属性跑通,建出的图形与数据一致。
@note 用 dataclass 描述一批图元规格、装饰器做参数校验、classmethod 生成角度表、
      property 算面积——面向对象那一套语法跟 manim 混在一起的真实写法。
      校验失败那条走 raise,验证异常能穿到预检。
"""

from manim import *
from dataclasses import dataclass, field
import math


def validated(build):
    """装饰器:先校验半径为正,再建物。"""
    def wrapper(spec):
        if spec.radius <= 0:
            raise ValueError(f"半径必须为正:{spec.radius}")
        return build(spec)
    return wrapper


@dataclass
class Spec:
    radius: float
    color: str
    angles: list = field(default_factory=list)

    @property
    def area(self):
        return math.pi * self.radius ** 2

    @classmethod
    def fan(cls, count):
        """从原点辐射出 count 个方向。"""
        return [2 * math.pi * i / count for i in range(count)]


@validated
def build_ring(spec):
    pts = [[spec.radius * math.cos(a), spec.radius * math.sin(a), 0] for a in spec.angles]
    return VGroup(*[Dot(point=p, color=spec.color) for p in pts])


specs = [
    Spec(radius=0.6, color=BLUE, angles=Spec.fan(6)),
    Spec(radius=1.0, color=GREEN, angles=Spec.fan(6)),
    Spec(radius=1.4, color=YELLOW, angles=Spec.fan(6)),
]
rings = VGroup(*[build_ring(s) for s in specs])
print("syntax-dataclass", len(specs), len(specs[0].angles), round(sum(s.area for s in specs), 3), round(specs[0].area, 3))


class DataclassScene(Scene):
    def construct(self):
        self.play(FadeIn(rings))
        self.play(rings.animate.rotate(math.pi / 6))
        self.wait(0.3)

语法综合:异常处理 / 上下文管理器 / 自定义异常 ​

异常被接住、上下文管理器正常进出(print 断言为准),画面正常。

语法综合:异常处理 / 上下文管理器 / 自定义异常

py
"""语法综合:异常处理 / 上下文管理器 / 自定义异常

@expect 异常被接住、上下文管理器正常进出(print 断言为准),画面正常。
@note 真实代码里「构造失败要兜住」很常见——这里用自定义异常 + try/except/else/finally
      配 contextlib.contextmanager,把一批形状安全建出来,坏参数自动跳过。
      既验语法,也验「用户态异常不该把整段顶掉」。
"""

from manim import *
from contextlib import contextmanager


class ShapeError(Exception):
    """形状参数不合法。"""


@contextmanager
def tracked(label):
    """上下文管理器:进入时记账,退出时补一笔——用 yield 写一遍 try/finally 的味道。"""
    log = [label]
    try:
        yield log
    finally:
        log.append("done")


def safe_square(side):
    if side <= 0:
        raise ShapeError(f"边长必须为正:{side}")
    return Square(side_length=side)


sides = [1.6, 0.0, 1.1, -0.5, 0.8]
shapes = []
errors = []
for i, side in enumerate(sides):
    with tracked(f"sq{i}") as log:
        try:
            shapes.append(safe_square(side))
        except ShapeError as exc:
            errors.append(str(exc))
        else:
            log.append("ok")
        finally:
            log.append("checked")

group = VGroup(*[s.move_to([0.5 * i - 1.0, 0, 0]) for i, s in enumerate(shapes)])
print("syntax-errors", len(shapes), len(errors), errors[0])


class ErrorsScene(Scene):
    def construct(self):
        self.play(LaggedStart(*[FadeIn(s) for s in group], lag_ratio=0.15))
        self.wait(0.3)

语法综合:解包 / 格式化 / 切片与排序 ​

解包 / 格式化 / 切片排序的结果正确(print 断言为准)。

语法综合:解包 / 格式化 / 切片与排序

py
"""语法综合:解包 / 格式化 / 切片与排序

@expect 解包 / 格式化 / 切片排序的结果正确(print 断言为准)。
@note 字符串格式化、星号解包、切片、sorted(key=...)、字典合并这些「胶水语法」
      在真实脚本里密度最高——这里把它们全塞进一段能画出图来的代码,
      看这些语法生成的数值与颜色能不能一路走到画布。
"""

from manim import *
import math

rows = [("a", 0.4, BLUE), ("b", 0.9, GREEN), ("c", 1.3, YELLOW), ("d", 0.7, RED)]

# 星号解包把每行拆成三列
names, values, colors = zip(*rows)
values = list(values)
colors = list(colors)

# 排序(按值倒序)——sorted(key=...) 返回新表
ranked = sorted(zip(names, values), key=lambda pair: pair[1], reverse=True)
# 切片带步长
odds = values[::2]

# 字典合并 + 字典推导 + f-string 格式化
meta = {"n": len(rows), **{name: round(v, 2) for name, v in zip(names, values)}}
summary = " ".join(f"{name}={v:.2f}" for name, v in ranked)
print("syntax-glue", summary, meta["n"], f"{odds[0]:.1f}", round(math.fsum(values), 3))

circles = [Circle(radius=v).set_color(c).move_to([1.2 * i - 1.8, 0, 0]) for i, (v, c) in enumerate(zip(values, colors))]
group = VGroup(*circles)


class GlueScene(Scene):
    def construct(self):
        self.play(LaggedStart(*[GrowFromCenter(c) for c in group], lag_ratio=0.15))
        self.play(group.animate.shift(UP * 0.3))
        self.wait(0.3)

语法综合:遍历 VGroup(for / 推导式 / 越界下标) ​

for 遍历 VGroup 正常收尾(不无限取下去),越界下标抛 IndexError。

语法综合:遍历 VGroup(for / 推导式 / 越界下标)

py
"""语法综合:遍历 VGroup(for / 推导式 / 越界下标)

@expect for 遍历 VGroup 正常收尾(不无限取下去),越界下标抛 IndexError。
@note `for m in group:` 是最常见的 Manim 写法之一,它曾经会**卡死**:适配层越界取不到时
      不抛 IndexError,于是迭代永远取得到「下一个」,越取越多直到把内存吃满(真机事故)。
      这条语料就是那次的回归网——for 遍历、推导式遍历、越界下标(必须抛 IndexError)都要走通。
"""

from manim import *
import math

dots = VGroup(*[Dot(point=[1.2 * i - 2.4, 0, 0], color=BLUE) for i in range(5)])

# for 遍历:走 __iter__(内部靠越界 IndexError 收尾)
moved = 0
for m in dots:
    m.shift(UP * 0.4)
    moved += 1

# 推导式遍历:同样要收尾
ys = [round(float(m.get_center()[1]), 3) for m in dots]

# 越界下标必须当场抛 IndexError,不能悄悄给个空物体(给了就又是那个死循环)
try:
    dots[99]
    tail = "no-error"
except IndexError:
    tail = "index-error"

print("syntax-group-iteration", moved, len(ys), ys[0], tail)


class GroupIterationScene(Scene):
    def construct(self):
        self.play(FadeIn(dots))
        for m in dots:
            m.set_color(YELLOW)
        self.play(dots.animate.shift(DOWN * 0.4))
        self.wait(0.3)

MIT licensed · Manim 与 Pyodide 各依其自身许可(见「安装」)