Python 数据可视化最常见的做法是:拿到数据,手动调颜色、设坐标轴、写循环拼图层——代码越写越像一份"画布操作手册"。声明式图表的思路恰恰相反:你只描述数据本身的结构和含义,渲染细节交给库去推断。与此同时,很多 Python 开发者天天用 for x in something,却对"可迭代对象"和"迭代器"的区别一笔带过,遇到 StopIteration 或消费一次就空的序列才临时查文档。这两件事看似无关,其实都指向同一个核心——理解协议,让工具替你干活。
声明式图表:说清楚数据,图自己长出来
传统命令式画图(典型如 matplotlib 的面向对象 API)本质是"一步步指挥画笔":
# 命令式:手动控制每个视觉元素
import matplotlib.pyplot as plt
fig, ax = plt.subplots()
ax.bar(["A", "B", "C"], [10, 20, 15], color="steelblue")
ax.set_ylabel("Count")
ax.set_title("Category Counts")
plt.show()
声明式图表则只声明数据字段与字段角色,视觉映射由库的默认规则自动完成。Python 生态里最成熟的声明式方案是 Altair——它基于 Vega-Lite 规范,用 JSON schema 描述图表语义,再由前端渲染。
下面是一个最小可运行的 Altair 示例:
# 声明式:只说"X 是类别,Y 是计数,用柱状图"
import altair as alt
import pandas as pd
data = pd.DataFrame({
"category": ["A", "B", "C"],
"count": [10, 20, 15]
})
chart = alt.Chart(data).mark_bar().encode(
x="category",
y="count"
)
chart.save("category_counts.html") # 浏览器打开即可查看
关键差异一目了然:
- 命令式代码里你写了
color="steelblue"、set_ylabel、set_title——全是视觉指令。 - 声明式代码里你只写了
x="category", y="count"——字段语义定下来,Altair 自动推断轴标签、刻度、颜色。
当你想换图表类型,改动极小:
# 换成点图:只改 mark
alt.Chart(data).mark_point().encode(x="category", y="count")
声明式的边界与取舍
声明式并非万能。以下场景需要权衡:
- 高度定制的外观(如非标准布局、自定义注释箭头):Altair 的
encode和configure_*能覆盖大部分需求,但极端定制仍需回到命令式或直接写 Vega JSON。 - 大数据量:Altair 默认将数据内嵌到 JSON spec 中,超过 5000 行时需启用
alt.data_transformers.enable('json')或使用 URL 引用,否则 HTML 文件膨胀。 - 交互复杂度:Vega-Lite 支持选择、过滤、绑定等交互,但比 ECharts/D3 的自由度低。如果需求是复杂仪表盘,可能需要叠加 Panel 或 Streamlit。
迭代器 vs 可迭代对象:一个协议,两种身份
第二个话题是 Python 里最常被混淆的概念对:Iterable 与 Iterator。
一句话区分:
- 可迭代对象(Iterable):实现了
__iter__()方法,返回一个迭代器。你可以对它调用iter(),也可以用for循环遍历。列表、字典、字符串、range对象都是可迭代对象。 - 迭代器(Iterator):同时实现了
__iter__()和__next__()。它是"有状态的游标",每次next()推进一步,耗尽后抛出StopIteration,且不可重置。
用代码验证身份:
from collections.abc import Iterable, Iterator
my_list = [1, 2, 3]
print(isinstance(my_list, Iterable)) # True — 列表是可迭代对象
print(isinstance(my_list, Iterator)) # False — 列表不是迭代器
my_iter = iter(my_list) # 调用 __iter__,拿到迭代器
print(isinstance(my_iter, Iterator)) # True
print(next(my_iter)) # 1
print(next(my_iter)) # 2
print(next(my_iter)) # 3
# next(my_iter) # StopIteration
最容易踩的坑:迭代器只能消费一次。
nums = iter([1, 2, 3])
for n in nums:
print(n) # 1 2 3
for n in nums:
print(n) # 什么也不输出——迭代器已耗尽
而可迭代对象可以反复遍历,因为每次 for 循环隐式调用 iter(),生成一个全新的迭代器:
nums_list = [1, 2, 3]
for n in nums_list:
print(n) # 1 2 3
for n in nums_list:
print(n) # 1 2 3 — 列表每次返回新迭代器
为什么这个区分很实际
- 生成器是迭代器:
yield函数返回的生成器对象同时有__iter__和__next__,用完即空。如果你需要多次遍历,把生成器结果先存进列表,或者用itertools.tee分叉。 - 自定义容器:写
__iter__就够了,不必写__next__。让__iter__返回一个独立的迭代器对象(可以是生成器),容器本身保持可重复遍历。 reversed()与len():可迭代对象不一定支持这两个操作。迭代器更不支持——它没有长度,也不能倒退。
一个正确的自定义可迭代容器示例:
class Countdown:
def __init__(self, start):
self.start = start
def __iter__(self):
# 每次调用返回一个全新生成器 → 可重复遍历
n = self.start
while n > 0:
yield n
n -= 1
for i in Countdown(3):
print(i) # 3 2 1
for i in Countdown(3):
print(i) # 3 2 1 — 依然正常
把声明式和协议思维串起来
声明式图表和迭代器协议看似不同领域,底层逻辑一致:你只需要遵守协议、声明意图,具体执行交给运行时。
- Altair 里你声明
x="category",Vega-Lite 引擎推断轴和刻度——你不必手动set_xticks。 - Python 里你声明
__iter__,for循环机制自动调用iter()→next()→StopIteration——你不必手动管理游标状态。
反过来,当你违反协议(把迭代器当可迭代对象反复遍历,或在 Altair 里把数据当视觉参数硬编码),问题就会冒出来。
实践清单
下次写数据可视化或处理序列时,可以快速自查:
- 图表选型:数据关系是分类-数值、时间序列、还是分布?先定
mark_type和encode字段,再考虑配色和标注——别反过来。 - 大数据 Altair:数据超过几千行时,在脚本开头加
alt.data_transformers.enable('json'),避免 HTML 爆炸。 - 迭代器消费:任何
iter(something)或生成器表达式返回的结果,只遍历一次。需要复用就先list()收集。 - 自定义容器:只写
__iter__(返回生成器即可),不要把__next__写在容器自身上——否则容器变成一次性迭代器。 - 类型检查:
collections.abc.Iterable和Iterator是判断身份的可靠工具,比hasattr(obj, '__iter__')更准确(因为__iter__可能返回非迭代器)。
声明式和协议思维都是"少写代码、多表达意图"的路子。理解边界在哪里,才能在省力的同时不掉坑。