所有的代码分析基于 polars 1.43.0 版本
分析polars的DataFrame
DataFrame 是 Polars 的核心二维表格结构,上接 Python API,下接 Column / Series / Arrow 内存。与 Series 的一列不同,DataFrame 的关键在于如何组织多列、维护行数、缓存 Schema,以及如何把”即时 (eager) 操作”统一收敛到惰性查询引擎。本文分析其完整架构和设计亮点。
架构总览
1. Python 层:DataFrame 类
Python 的 DataFrame 定义在:
class DataFrame:展开折叠代码 (212-380 行,共 169 行)
"""
Two-dimensional data structure representing data as a table with rows and columns.
Parameters
----------
data : dict, Sequence, ndarray, Series, or pandas.DataFrame
Two-dimensional data in various forms; dict input must contain Sequences,
Generators, or a `range`. Sequence may contain Series or other Sequences.
schema : Sequence of str, (str,DataType) pairs, or a {str:DataType,} dict
The schema of the resulting DataFrame. The schema may be declared in several
ways:
* As a dict of {name:type} pairs; if type is None, it will be auto-inferred.
* As a list of column names; in this case types are automatically inferred.
* As a list of (name,type) pairs; this is equivalent to the dictionary form.
The order of the schema determines the column order of the frame.
When passing a dict, its insertion order is respected. To override specific
column data types by name without changing column order, use
``schema_overrides`` instead.
If you supply a list of column names that does not match the names in the
underlying data, the names given here will overwrite them. The number
of names given in the schema should match the underlying data dimensions.
If set to `None` (default), the schema is inferred from the data.
schema_overrides : dict, default None
Support type specification or override of one or more columns; note that
any dtypes inferred from the schema param will be overridden.
The number of entries in the schema should match the underlying data
dimensions, unless a sequence of dictionaries is being passed, in which case
a *partial* schema can be declared to prevent specific fields from being loaded.
strict : bool, default True
Throw an error if any `data` value does not exactly match the given or inferred
data type for that column. If set to `False`, values that do not match the data
type are cast to that data type or, if casting is not possible, set to null
instead.
orient : {'col', 'row'}, default None
Whether to interpret two-dimensional data as columns or as rows. If None,
the orientation is inferred by matching the columns and data dimensions. If
this does not yield conclusive results, column orientation is used.
infer_schema_length : int or None
The maximum number of rows to scan for schema inference. If set to `None`, the
full data may be scanned *(this can be slow)*. This parameter only applies if
the input data is a sequence or generator of rows; other input is read as-is.
nan_to_null : bool, default False
If the data comes from one or more numpy arrays, can optionally convert input
data np.nan values to null instead. This is a no-op for all other input data.
height : int or None, default None
Allows constructing DataFrames with 0 width and a specified height. If
passed with data, ensures the resulting DataFrame has this height.
.. warning::
This functionality is considered **unstable**. It may be changed
at any point without it being considered a breaking change.
Notes
-----
Polars explicitly does not support subclassing of its core data types. See
the following GitHub issue for possible workarounds:
https://github.com/pola-rs/polars/issues/2846#issuecomment-1711799869
Examples
--------
Constructing a DataFrame from a dictionary:
>>> data = {"a": [1, 2], "b": [3, 4]}
>>> df = pl.DataFrame(data)
>>> df
shape: (2, 2)
┌─────┬─────┐
│ a ┆ b │
│ --- ┆ --- │
│ i64 ┆ i64 │
╞═════╪═════╡
│ 1 ┆ 3 │
│ 2 ┆ 4 │
└─────┴─────┘
Notice that the dtypes are automatically inferred as polars Int64:
>>> df.dtypes
[Int64, Int64]
To specify a more detailed/specific frame schema you can supply the `schema`
parameter with a dictionary of (name,dtype) pairs...
>>> data = {"col1": [0, 2], "col2": [3, 7]}
>>> df2 = pl.DataFrame(data, schema={"col1": pl.Float32, "col2": pl.Int64})
>>> df2
shape: (2, 2)
┌──────┬──────┐
│ col1 ┆ col2 │
│ --- ┆ --- │
│ f32 ┆ i64 │
╞══════╪══════╡
│ 0.0 ┆ 3 │
│ 2.0 ┆ 7 │
└──────┴──────┘
...a sequence of (name,dtype) pairs...
>>> data = {"col1": [1, 2], "col2": [3, 4]}
>>> df3 = pl.DataFrame(data, schema=[("col1", pl.Float32), ("col2", pl.Int64)])
>>> df3
shape: (2, 2)
┌──────┬──────┐
│ col1 ┆ col2 │
│ --- ┆ --- │
│ f32 ┆ i64 │
╞══════╪══════╡
│ 1.0 ┆ 3 │
│ 2.0 ┆ 4 │
└──────┴──────┘
...or a list of typed Series.
>>> data = [
... pl.Series("col1", [1, 2], dtype=pl.Float32),
... pl.Series("col2", [3, 4], dtype=pl.Int64),
... ]
>>> df4 = pl.DataFrame(data)
>>> df4
shape: (2, 2)
┌──────┬──────┐
│ col1 ┆ col2 │
│ --- ┆ --- │
│ f32 ┆ i64 │
╞══════╪══════╡
│ 1.0 ┆ 3 │
│ 2.0 ┆ 4 │
└──────┴──────┘
Constructing a DataFrame from a numpy ndarray, specifying column names:
>>> import numpy as np
>>> data = np.array([(1, 2), (3, 4)], dtype=np.int64)
>>> df5 = pl.DataFrame(data, schema=["a", "b"], orient="col")
>>> df5
shape: (2, 2)
┌─────┬─────┐
│ a ┆ b │
│ --- ┆ --- │
│ i64 ┆ i64 │
╞═════╪═════╡
│ 1 ┆ 3 │
│ 2 ┆ 4 │
└─────┴─────┘
Constructing a DataFrame from a list of lists, row orientation specified:
>>> data = [[1, 2, 3], [4, 5, 6]]
>>> df6 = pl.DataFrame(data, schema=["a", "b", "c"], orient="row")
>>> df6
shape: (2, 3)
┌─────┬─────┬─────┐
│ a ┆ b ┆ c │
│ --- ┆ --- ┆ --- │
│ i64 ┆ i64 ┆ i64 │
╞═════╪═════╪═════╡
│ 1 ┆ 2 ┆ 3 │
│ 4 ┆ 5 ┆ 6 │
└─────┴─────┴─────┘
"""
_df: PyDataFrame
_accessors: ClassVar[set[str]] = {"plot", "style"} def __init__(
self,
data: FrameInitTypes | None = None,
schema: SchemaDefinition | None = None,
*,
schema_overrides: SchemaDict | None = None,
strict: bool = True,
orient: Orientation | None = None,
infer_schema_length: int | None = N_INFER_DEFAULT,
nan_to_null: bool = False,
height: int | None = None,
) -> None:
if height is not None:
msg = "the `height` parameter of `DataFrame` is considered unstable."
issue_unstable_warning(msg)
if data is None:
self._df = dict_to_pydf(
{}, schema=schema, schema_overrides=schema_overrides
)
if height is not None and self.width == 0:
self._df = PyDataFrame.empty_with_height(height)
elif isinstance(data, dict):
self._df = dict_to_pydf(
data,
schema=schema,
schema_overrides=schema_overrides,
strict=strict,
nan_to_null=nan_to_null,
)
elif isinstance(data, (list, tuple, Sequence)):
self._df = sequence_to_pydf(
data,
schema=schema,
schema_overrides=schema_overrides,
strict=strict,
orient=orient,
infer_schema_length=infer_schema_length,
nan_to_null=nan_to_null,
)
elif isinstance(data, pl.Series):
self._df = series_to_pydf(
data, schema=schema, schema_overrides=schema_overrides, strict=strict
)
elif _check_for_numpy(data) and isinstance(data, np.ndarray):
self._df = numpy_to_pydf(
data,
schema=schema,
schema_overrides=schema_overrides,
strict=strict,
orient=orient,
nan_to_null=nan_to_null,
)
elif _check_for_pyarrow(data) and isinstance(data, pa.Table):
self._df = arrow_to_pydf(
data, schema=schema, schema_overrides=schema_overrides, strict=strict
)
elif _check_for_pandas(data) and isinstance(data, pd.DataFrame):
self._df = pandas_to_pydf(
data, schema=schema, schema_overrides=schema_overrides, strict=strict
)
elif _check_for_torch(data) and isinstance(data, torch.Tensor):
self._df = numpy_to_pydf(
data.numpy(force=False),
schema=schema,
schema_overrides=schema_overrides,
strict=strict,
orient=orient,
nan_to_null=nan_to_null,
)
elif (
not hasattr(data, "__arrow_c_stream__")
and not isinstance(data, Sized)
and isinstance(data, (Generator, Iterable))
):
self._df = iterable_to_pydf(
data,
schema=schema,
schema_overrides=schema_overrides,
strict=strict,
orient=orient,
infer_schema_length=infer_schema_length,
)
elif isinstance(data, pl.DataFrame):
self._df = dataframe_to_pydf(
data, schema=schema, schema_overrides=schema_overrides, strict=strict
)
elif is_pycapsule(data):
self._df = pycapsule_to_frame(
data,
schema=schema,
schema_overrides=schema_overrides,
)._df
else:
msg = (
f"DataFrame constructor called with unsupported type {type(data).__name__!r}"
" for the `data` parameter"
)
raise TypeError(msg)
if height is not None and self.height != height:
from polars.exceptions import ShapeError
msg = f"height of data ({self.height}) does not match specified height ({height})"
raise ShapeError(msg)__init__ 的多路分发逻辑(以 1.43.0 为准):
def __init__(
self,
data: FrameInitTypes | None = None,
schema: SchemaDefinition | None = None,
*,
schema_overrides: SchemaDict | None = None,
strict: bool = True,
orient: Orientation | None = None,
infer_schema_length: int | None = N_INFER_DEFAULT,
nan_to_null: bool = False,
height: int | None = None,
) -> None:
if height is not None:展开折叠代码 (394-496 行,共 103 行)
msg = "the `height` parameter of `DataFrame` is considered unstable."
issue_unstable_warning(msg)
if data is None:
self._df = dict_to_pydf(
{}, schema=schema, schema_overrides=schema_overrides
)
if height is not None and self.width == 0:
self._df = PyDataFrame.empty_with_height(height)
elif isinstance(data, dict):
self._df = dict_to_pydf(
data,
schema=schema,
schema_overrides=schema_overrides,
strict=strict,
nan_to_null=nan_to_null,
)
elif isinstance(data, (list, tuple, Sequence)):
self._df = sequence_to_pydf(
data,
schema=schema,
schema_overrides=schema_overrides,
strict=strict,
orient=orient,
infer_schema_length=infer_schema_length,
nan_to_null=nan_to_null,
)
elif isinstance(data, pl.Series):
self._df = series_to_pydf(
data, schema=schema, schema_overrides=schema_overrides, strict=strict
)
elif _check_for_numpy(data) and isinstance(data, np.ndarray):
self._df = numpy_to_pydf(
data,
schema=schema,
schema_overrides=schema_overrides,
strict=strict,
orient=orient,
nan_to_null=nan_to_null,
)
elif _check_for_pyarrow(data) and isinstance(data, pa.Table):
self._df = arrow_to_pydf(
data, schema=schema, schema_overrides=schema_overrides, strict=strict
)
elif _check_for_pandas(data) and isinstance(data, pd.DataFrame):
self._df = pandas_to_pydf(
data, schema=schema, schema_overrides=schema_overrides, strict=strict
)
elif _check_for_torch(data) and isinstance(data, torch.Tensor):
self._df = numpy_to_pydf(
data.numpy(force=False),
schema=schema,
schema_overrides=schema_overrides,
strict=strict,
orient=orient,
nan_to_null=nan_to_null,
)
elif (
not hasattr(data, "__arrow_c_stream__")
and not isinstance(data, Sized)
and isinstance(data, (Generator, Iterable))
):
self._df = iterable_to_pydf(
data,
schema=schema,
schema_overrides=schema_overrides,
strict=strict,
orient=orient,
infer_schema_length=infer_schema_length,
)
elif isinstance(data, pl.DataFrame):
self._df = dataframe_to_pydf(
data, schema=schema, schema_overrides=schema_overrides, strict=strict
)
elif is_pycapsule(data):
self._df = pycapsule_to_frame(
data,
schema=schema,
schema_overrides=schema_overrides,
)._df
else:
msg = (
f"DataFrame constructor called with unsupported type {type(data).__name__!r}"
" for the `data` parameter"
)
raise TypeError(msg)
if height is not None and self.height != height:
from polars.exceptions import ShapeError
msg = f"height of data ({self.height}) does not match specified height ({height})"
raise ShapeError(msg)关键设计:
_df: PyDataFrame— 和 Series 一样,Python 层不存数据,内部持有 Rust 侧的PyDataFrame__init__多路分发 — 根据data类型(dict、Sequence、np.ndarray、pl.Series、pa.Table、pd.DataFrame、torch.Tensor、迭代器、Arrow Capsule 等)走不同的*_to_pydf构造路径,全部收敛到 Rust 端- schema / schema_overrides 分离 —
schema定列名和顺序,schema_overrides只覆盖类型不改变列序,两者都最终转化为每列的Series构造参数
1.1 dict_to_pydf 的构造细节
def dict_to_pydf(
data: Mapping[str, ArrayLike | NonNestedLiteral | None],
schema: SchemaDefinition | None = None,
*,
schema_overrides: SchemaDict | None = None,
strict: bool = True,
nan_to_null: bool = False,
allow_multithreaded: bool = True,
) -> PyDataFrame:
"""Construct a PyDataFrame from a dictionary of sequences."""
if isinstance(schema, Mapping) and data:
if not all((col in schema) for col in data):
msg = "the given column-schema names do not match the data dictionary"
raise ValueError(msg)
data = {col: data[col] for col in schema}
column_names, schema_overrides = _unpack_schema(
schema, lookup_names=data.keys(), schema_overrides=schema_overrides
)
if not column_names:
column_names = list(data)展开折叠代码 (101-136 行,共 36 行)
if data and _NUMPY_AVAILABLE:
# if there are 3 or more numpy arrays of sufficient size, we multi-thread:
count_numpy = sum(
int(
allow_multithreaded
and _check_for_numpy(val)
and isinstance(val, np.ndarray)
and len(val) > _MIN_NUMPY_SIZE_FOR_MULTITHREADING
# integers and non-nan floats are zero-copy
and nan_to_null
and val.dtype in (np.float32, np.float64)
)
for val in data.values()
)
if count_numpy >= 3:
# yes, multi-threading was easier in python here; we cannot have multiple
# threads running python and release the gil in pyo3 (it will deadlock).
from concurrent.futures import ThreadPoolExecutor
pool_size = thread_pool_size()
with ThreadPoolExecutor(max_workers=pool_size) as pool:
data = dict(
zip(
column_names,
pool.map(
lambda t: (
pl.Series(t[0], t[1], nan_to_null=nan_to_null)
if isinstance(t[1], np.ndarray)
else t[1]
),
list(data.items()),
),
strict=True,
)
) if not data and schema_overrides:
data_series = [
pl.Series(
name,
[],
dtype=schema_overrides.get(name),
strict=strict,
nan_to_null=nan_to_null,
)._s
for name in column_names
]
else:
data_series = [
s._s
for s in _expand_dict_values(
data,
schema_overrides=schema_overrides,
strict=strict,
nan_to_null=nan_to_null,
).values()
]
data_series = _handle_columns_arg(data_series, columns=column_names, from_dict=True)
pydf = PyDataFrame(data_series)
if schema_overrides and pydf.dtypes() != list(schema_overrides.values()):
pydf = _post_apply_columns(
pydf, column_names, schema_overrides=schema_overrides, strict=strict
)
return pydf设计亮点:当 dict 中 numpy 数组足够多且足够大时,在 Python 侧用 ThreadPoolExecutor 并行构造各列 Series。注释解释了原因——pyo3 中多线程运行 Python 代码并释放 GIL 会死锁,所以”Python 里多线程反而更容易”。构造完成后 PyDataFrame(data_series) 一次性传入 Rust。
2. Python ↔ Rust 桥接:PyDataFrame
#[pyclass(frozen, from_py_object)]
#[repr(transparent)]
pub struct PyDataFrame {
pub df: RwLock<DataFrame>,
}与 PySeries 完全对称:
#[pyclass(frozen)]— Python 层面不可变df: RwLock<DataFrame>— Rust 层内部可变的读写锁
Rust 端真正的构造入口是接收 Vec<PySeries>,把每个 Series 转成 Column:
#[pymethods]
impl PyDataFrame {
#[new]
pub fn __init__(columns: Vec<PySeries>) -> PyResult<Self> {
let columns = columns.to_series();
// @scalar-opt
let columns = columns.into_iter().map(|s| s.into()).collect();
let df = DataFrame::new_infer_height(columns).map_err(PyPolarsErr::from)?;
Ok(PyDataFrame::new(df))
}注意 // @scalar-opt 注释:s.into() 是 Series → Column 的转换,其中可能被优化为 ScalarColumn(见第 4 节)。之后 DataFrame::new_infer_height 负责校验。
3. Rust 核心:DataFrame 三字段设计
#[derive(Clone)]
pub struct DataFrame {
height: usize,
/// All columns must have length equal to `self.height`.
columns: Vec<Column>,
/// Cached schema. Must be cleared if column names / dtypes in `self.columns` change.
cached_schema: OnceLock<SchemaRef>,
}整个 DataFrame 只有 3 个字段,这是它和 pandas 最本质的区别:
| 字段 | 作用 | 设计要点 |
|---|---|---|
height: usize | 行数 | 单一事实来源,所有列必须与之等长 |
columns: Vec<Column> | 列集合 | 行级信息(除 height 外)不存储,需要时计算 |
cached_schema: OnceLock<SchemaRef> | 缓存 Schema | 惰性构建,列结构变化时失效 |
3.1 height 取代 index
Polars 的 DataFrame 没有行索引 (index)。“第 n 行” 就是 0..height 范围内的整数。这带来几个好处:
- 行数据天然对齐,不存在 index 与列错位的经典 pandas bug
take/gather/join只需要IdxSize索引数组,无需维护对齐- 列间切换零开销,因为所有列共享同一个
height
3.2 安全构造 vs 不安全构造
pub fn new(height: usize, columns: Vec<Column>) -> PolarsResult<Self> {
validate_columns_slice(height, &columns)
.map_err(|e| e.wrap_msg(|e| format!("could not create a new DataFrame: {e}")))?;
Ok(unsafe { DataFrame::_new_unchecked_impl(height, columns) })
}
/// Height is sourced from first column.
pub fn new_infer_height(columns: Vec<Column>) -> PolarsResult<Self> {
DataFrame::new(columns.first().map_or(0, |c| c.len()), columns)
}
/// Create a new `DataFrame` but does not check the length or duplicate occurrence of the
/// [`Column`]s.
///
/// # Safety
/// [`Column`]s must have unique names and matching lengths.
pub unsafe fn new_unchecked(height: usize, columns: Vec<Column>) -> DataFrame {
if cfg!(debug_assertions) {
validate_columns_slice(height, &columns).unwrap();
}
unsafe { DataFrame::_new_unchecked_impl(height, columns) }new/new_infer_height— 走validate_columns_slice校验(各列等长、无重名列)new_unchecked/_new_unchecked_impl—unsafe,跳过校验,只在内部计算路径使用
设计哲学:对外部 API 严格校验;对内部高频调用路径(如 filter、take 这种每列长度天然一致的操作)用 unsafe 零开销构造,避免每次 collect 都做 O(列数) 的校验。这种”安全边界只设一次,内部信任不变量”的模式在 mod.rs 的 filter / take 中大量出现。
3.3 Schema 惰性缓存
/// Get the schema of this [`DataFrame`].
///
/// # Panics
/// Panics if there are duplicate column names.
pub fn schema(&self) -> &SchemaRef {
let out = self.cached_schema.get_or_init(|| {
Arc::new(
Schema::from_iter_check_duplicates(
self.columns
.iter()
.map(|x| (x.name().clone(), x.dtype().clone())),
)
.unwrap(),
)
});
assert_eq!(out.len(), self.width());
out
}
#[inline]
pub fn cached_schema(&self) -> Option<&SchemaRef> {
self.cached_schema.get()
}
/// Set the cached schema
///
/// # Safetyschema()用OnceLock::get_or_init惰性构建,第一次访问才遍历列生成Schema,之后全部走缓存- 返回
&SchemaRef(Arc<Schema>),克隆零成本 - 失效机制:
columns_mut()会调用clear_schema();而columns_mut_retain_schema()只在列名/类型不变时使用(如只追加 chunk),因此不需要清缓存
底层 Schema 结构用 PlIndexMap 保序:
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "dsl-schema", derive(schemars::JsonSchema))]
pub struct Schema<Field, Metadata> {
fields: PlIndexMap<PlSmallStr, Field>,
metadata: Metadata,
}PlIndexMap<PlSmallStr, Field> 是 indexmap 类型,既支持按名 O(1) 查找,又保留插入顺序——所以 Polars 的列顺序由 schema 保证。
4. Column:Series 与 Scalar 双表示
Column 是 DataFrame 的最小列单元,自 1.x 起它不直接是 Series,而是一个枚举:
/// Currently, there are two ways to represent a [`Column`].
/// 1. A [`Series`] of values
/// 2. A [`ScalarColumn`] that repeats a single [`Scalar`]
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "dsl-schema", derive(schemars::JsonSchema))]
pub enum Column {
Series(SeriesColumn),
Scalar(ScalarColumn),
}Series(SeriesColumn)— 常规列,持有完整的Series(即上一篇文章的Arc<dyn SeriesTrait>)Scalar(ScalarColumn)— 常数列,只存一个Scalar+length,不存整列数据
4.1 ScalarColumn:零成本常量列
/// A [`Column`] that consists of a repeated [`Scalar`]
///
/// This is lazily materialized into a [`Series`].
#[derive(Debug, Clone)]
pub struct ScalarColumn {
name: PlSmallStr,
// The value of this scalar may be incoherent when `length == 0`.
scalar: Scalar,
length: usize,
// invariants:
// materialized.name() == name
// materialized.len() == length
// materialized.dtype() == value.dtype
// materialized[i] == value, for all 0 <= i < length
/// A lazily materialized [`Series`] variant of this [`ScalarColumn`]
materialized: OnceLock<Series>,
}这是一个非常巧妙的优化:
- 常数列(如
pl.lit(1)广播、with_columns加常量、全 null 列)只占 O(1) 内存,不展开成整列 materialized: OnceLock<Series>— 首次需要真实数据时才惰性物化成Series,之后复用- 不变量在注释中明确声明:
name/length/dtype/ 每个值都等于scalar
设计亮点:查询引擎在编译表达式时就能识别常量,Column::Scalar 让常量一直”保持常量”直到被迫物化。聚合、join 等操作可以针对常量做短路优化,而物化后与普通 Series 完全同构,对上层无感知。
4.2 列间操作:并行处理各列
以 filter 为例:
pub fn filter(&self, mask: &BooleanChunked) -> PolarsResult<Self> {
if self.width() == 0 {
filter_zero_width(self.height(), mask)
} else if mask.len() == 1 && self.len() >= 1 {
if mask.all() && mask.null_count() == 0 {
Ok(self.clone())
} else {
Ok(self.clear())
}
} else {
let new_columns: Vec<Column> = self.try_apply_columns_par(|s| s.filter(mask))?;
let out = unsafe {
DataFrame::new_unchecked(new_columns[0].len(), new_columns).with_schema_from(self)
};
Ok(out)
}
}三个分支本身就是性能设计:
- 零列(
width() == 0)— 只按 height 和 mask 计算行数,不需要碰列 - 单元素 mask — mask 是
[true]或[false]时直接返回self.clone()或空表,短路 - 常规路径 —
try_apply_columns_par用 rayon 并行 filter 每一列,最后共享with_schema_from(self)直接复用旧 schema 缓存
核心模式:DataFrame 操作 = 对每列做同样的列内操作,然后并行化。take、slice、sort 等都是这个模式,列数越多并行收益越大。
5. 核心操作设计:eager 复用 Lazy 引擎
这是 Polars 架构上最值得称道的一点:DataFrame 的表达式类操作(select / with_columns / group_by.agg)在 Python 层并不是直接调用 Rust 的 DataFrame 方法,而是先 .lazy() 再 .collect()。
def lazy(self) -> LazyFrame:展开折叠代码 (10413-10446 行,共 34 行)
"""
Start a lazy query from this point. This returns a `LazyFrame` object.
Operations on a `LazyFrame` are not executed until this is triggered
by calling one of:
* :meth:`.collect() <polars.LazyFrame.collect>`
(run on all data)
* :meth:`.explain() <polars.LazyFrame.explain>`
(print the query plan)
* :meth:`.show_graph() <polars.LazyFrame.show_graph>`
(show the query plan as graphviz graph)
* :meth:`.collect_schema() <polars.LazyFrame.collect_schema>`
(return the final frame schema)
Lazy operations are recommended because they allow for query optimization and
additional parallelism.
Returns
-------
LazyFrame
Examples
--------
>>> df = pl.DataFrame(
... {
... "a": [None, 2, 3, 4],
... "b": [0.5, None, 2.5, 13],
... "c": [True, True, False, None],
... }
... )
>>> df.lazy()
<LazyFrame at ...>
""" return wrap_ldf(self._df.lazy()) def select(展开折叠代码 (10450-10532 行,共 83 行)
self, *exprs: IntoExpr | Iterable[IntoExpr], **named_exprs: IntoExpr
) -> DataFrame:
"""
Select columns from this DataFrame.
Parameters
----------
*exprs
Column(s) to select, specified as positional arguments.
Accepts expression input. Strings are parsed as column names,
other non-expression inputs are parsed as literals.
**named_exprs
Additional columns to select, specified as keyword arguments.
The columns will be renamed to the keyword used.
Examples
--------
Pass the name of a column to select that column.
>>> df = pl.DataFrame(
... {
... "foo": [1, 2, 3],
... "bar": [6, 7, 8],
... "ham": ["a", "b", "c"],
... }
... )
>>> df.select("foo")
shape: (3, 1)
┌─────┐
│ foo │
│ --- │
│ i64 │
╞═════╡
│ 1 │
│ 2 │
│ 3 │
└─────┘
Multiple columns can be selected by passing a list of column names.
>>> df.select(["foo", "bar"])
shape: (3, 2)
┌─────┬─────┐
│ foo ┆ bar │
│ --- ┆ --- │
│ i64 ┆ i64 │
╞═════╪═════╡
│ 1 ┆ 6 │
│ 2 ┆ 7 │
│ 3 ┆ 8 │
└─────┴─────┘
Multiple columns can also be selected using positional arguments instead of a
list. Expressions are also accepted.
>>> df.select(pl.col("foo"), pl.col("bar") + 1)
shape: (3, 2)
┌─────┬─────┐
│ foo ┆ bar │
│ --- ┆ --- │
│ i64 ┆ i64 │
╞═════╪═════╡
│ 1 ┆ 7 │
│ 2 ┆ 8 │
│ 3 ┆ 9 │
└─────┴─────┘
Use keyword arguments to easily name your expression inputs.
>>> df.select(threshold=pl.when(pl.col("foo") > 2).then(10).otherwise(0))
shape: (3, 1)
┌───────────┐
│ threshold │
│ --- │
│ i32 │
╞═══════════╡
│ 0 │
│ 0 │
│ 10 │
└───────────┘
"""
from polars.lazyframe.opt_flags import QueryOptFlags return (
self.lazy()
.select(*exprs, **named_exprs)
.collect(optimizations=QueryOptFlags._eager())
)select 的实际实现只有 3 行:
return (
self.lazy()
.select(*exprs, **named_exprs)
.collect(optimizations=QueryOptFlags._eager())
)
with_columns、group_by().agg() 也完全一样:
from polars.lazyframe.opt_flags import QueryOptFlags
return (
self._lgb()
.agg(*aggs, **named_aggs)
.collect(optimizations=QueryOptFlags.none())
)设计亮点:
- 一套引擎,两种模式 — eager 和 lazy 没有两套实现。eager 只是”先建一个最小的查询计划,再立刻 collect”,本质是 lazy 引擎的特例。查询优化、表达式编译、并行调度只有一份代码,避免了维护成本和不一致
QueryOptFlags控制优化开关 — eager 用_eager()关闭那些”只有长链惰性查询才有意义”的优化(如谓词下推、join 重排),但保留表达式内部的执行优化;group_by().agg用none()因为分组语义不能随便重排- 表达式统一 —
select、with_columns都吃Expr,所以传字符串、字面量、pl.col(...)都能被统一编译
而纯数据搬运类操作(filter、take、slice、sort)则走 Rust 原生 DataFrame 方法,因为它们的语义就是”按列重排/筛选”,无需经过表达式层。
6. GroupBy 的结构
eager 的 group_by 返回 Python 的 GroupBy 包装,其 Rust 核心 GroupBy<'a> 是对 DataFrame 的借用视图:
#[derive(Debug, Clone)]
pub struct GroupBy<'a> {
pub df: &'a DataFrame,
pub(crate) selected_keys: Vec<Column>,
// [first idx, [other idx]]
groups: GroupPositions,
// columns selected for aggregation
pub(crate) selected_agg: Option<Vec<PlSmallStr>>,
}df: &'a DataFrame— 借用原表,不复制数据groups: GroupPositions— 预先计算好的分组索引([first_idx, Vec<other_idx>]或切片形式)selected_agg— 待聚合列,延迟到agg阶段
设计亮点:group_by 本身只做两件事——提取分组键 + 哈希分组生成 GroupPositions。真正计算推迟到 .agg(),此时 Python 层又走回 lazy 引擎。分组索引一经生成可被多次复用(多个 agg 共享同一分组),这是”分组一次、聚合多次”的高效基础。
设计亮点总结
-
三字段最小化状态 —
DataFrame { height, columns, cached_schema }没有行索引,height是唯一行级事实来源。列间对齐零成本,杜绝 index 错位类 bug -
Column 双表示(Scalar 优化) — 常量列用
Column::Scalar存 O(1) 数据,OnceLock惰性物化;只有必要时才展开成Series,对上层完全透明 -
Schema 惰性缓存 + 失效协议 —
OnceLock<SchemaRef>首次访问才构建,columns_mut/columns_mut_retain_schema区分是否需要失效,把 Schema 开销降到最低 -
安全边界只设一次 — 对外构造走
validate_columns_slice严格校验;内部new_unchecked信任不变量零开销构造。校验成本从每次 collect 中剥离 -
eager 复用 lazy 引擎 —
select/with_columns/group_by.agg全部lazy().collect(QueryOptFlags._eager()),一套查询引擎两种执行模式,优化开关通过QueryOptFlags精细控制 -
列级并行 —
filter/take/sort用 rayonapply_columns_par并行处理各列,列数越多收益越大 -
Python ↔ Rust 全链路零拷贝 — 数据始终在 Rust 端(
PyDataFrame.df: RwLock<DataFrame>),Python__init__的所有输入路径最终都收敛为PyDataFrame(Vec<PySeries>),构造时甚至支持在 Python 侧多线程并行建列