Tip
所有的代码分析基于 polars 1.43.0 版本
分析polars的数据类型
数据类型
- 数据类型的定义在
py-polars/src/polars/datatypes/classes.pyGitHub
展开折叠代码 (59-1514 行,共 1456 行)
class DataTypeClass(type):
"""Metaclass for nicely printing DataType classes."""
def __repr__(cls) -> str:
return cls.__name__
def _string_repr(cls) -> str:
return _dtype_str_repr(cls)
# Methods below defined here in signature only to satisfy mypy
@classmethod
def base_type(cls) -> DataTypeClass: # noqa: D102
...
@classmethod
def is_(cls, other: PolarsDataType) -> bool: # noqa: D102
...
@classmethod
def is_numeric(cls) -> bool: # noqa: D102
...
@classmethod
def is_decimal(cls) -> bool: # noqa: D102
...
@classmethod
def is_integer(cls) -> bool: # noqa: D102
...
@classmethod
def is_object(cls) -> bool: # noqa: D102
...
@classmethod
def is_signed_integer(cls) -> bool: # noqa: D102
...
@classmethod
def is_unsigned_integer(cls) -> bool: # noqa: D102
...
@classmethod
def is_float(cls) -> bool: # noqa: D102
...
@classmethod
def is_temporal(cls) -> bool: # noqa: D102
...
@classmethod
def is_nested(cls) -> bool: # noqa: D102
...
@classmethod
def is_extension(cls) -> bool: # noqa: D102
...
@classmethod
def from_python(cls, py_type: PythonDataType) -> PolarsDataType: # noqa: D102
...
@classmethod
def to_python(cls) -> PythonDataType: # noqa: D102
...
@classmethod
def to_dtype_expr(cls) -> pl.DataTypeExpr: # noqa: D102
...
class DataType(metaclass=DataTypeClass):
"""Base class for all Polars data types."""
def _string_repr(self) -> str:
return _dtype_str_repr(self)
@overload # type: ignore[override]
def __eq__( # pyrefly: ignore[bad-override]
self, other: pl.DataTypeExpr
) -> pl.Expr: ...
@overload
def __eq__(self, other: PolarsDataType) -> bool: ...
def __eq__(self, other: pl.DataTypeExpr | PolarsDataType) -> pl.Expr | bool:
if isinstance(other, pl.DataTypeExpr):
return self.to_dtype_expr() == other
elif type(other) is DataTypeClass:
return issubclass(other, type(self))
else:
return isinstance(other, type(self))
def __hash__(self) -> int:
return hash(self.__class__)
def __repr__(self) -> str:
return self.__class__.__name__
@classmethod
def base_type(cls) -> type[Self]:
"""
Return this DataType's fundamental/root type class.
Examples
--------
>>> pl.Datetime("ns").base_type()
Datetime
>>> pl.List(pl.Int32).base_type()
List
>>> pl.Struct([pl.Field("a", pl.Int64), pl.Field("b", pl.Boolean)]).base_type()
Struct
"""
return cls
@classinstmethod
def is_(self, other: PolarsDataType) -> bool:
"""
Check if this DataType is the same as another DataType.
This is a stricter check than `self == other`, as it enforces an exact
match of all dtype attributes for nested and/or uninitialised dtypes.
Parameters
----------
other
the other Polars dtype to compare with.
Examples
--------
>>> pl.List == pl.List(pl.Int32)
True
>>> pl.List.is_(pl.List(pl.Int32))
False
"""
return self == other and hash(self) == hash(other)
@classmethod
def is_numeric(cls) -> bool:
"""Check whether the data type is a numeric type."""
return issubclass(cls, NumericType)
@classmethod
def is_decimal(cls) -> bool:
"""Check whether the data type is a decimal type."""
return issubclass(cls, Decimal)
@classmethod
def is_integer(cls) -> bool:
"""Check whether the data type is an integer type."""
return issubclass(cls, IntegerType)
@classmethod
def is_object(cls) -> bool:
"""Check whether the data type is an object type."""
return issubclass(cls, ObjectType)
@classmethod
def is_signed_integer(cls) -> bool:
"""Check whether the data type is a signed integer type."""
return issubclass(cls, SignedIntegerType)
@classmethod
def is_unsigned_integer(cls) -> bool:
"""Check whether the data type is an unsigned integer type."""
return issubclass(cls, UnsignedIntegerType)
@classmethod
def is_float(cls) -> bool:
"""Check whether the data type is a floating point type."""
return issubclass(cls, FloatType)
@classmethod
def is_temporal(cls) -> bool:
"""Check whether the data type is a temporal type."""
return issubclass(cls, TemporalType)
@classmethod
def is_nested(cls) -> bool:
"""Check whether the data type is a nested type."""
return issubclass(cls, NestedType)
@classmethod
def is_extension(cls) -> bool:
"""Check whether the data type is an extension type."""
return issubclass(cls, BaseExtension)
@classmethod
def from_python(cls, py_type: PythonDataType) -> PolarsDataType:
"""
Return the Polars data type corresponding to a given Python type.
Notes
-----
Not every Python type has a corresponding Polars data type; in general
you should declare Polars data types explicitly to exactly specify
the desired type and its properties (such as scale/unit).
Examples
--------
>>> pl.DataType.from_python(int)
Int64
>>> pl.DataType.from_python(float)
Float64
>>> from datetime import tzinfo
>>> pl.DataType.from_python(tzinfo) # doctest: +SKIP
TypeError: cannot parse input <class 'datetime.tzinfo'> into Polars data type
"""
from polars.datatypes._parse import parse_into_dtype
return parse_into_dtype(py_type)
@classinstmethod
def to_python(self) -> PythonDataType:
"""
Return the Python type corresponding to this Polars data type.
Examples
--------
>>> pl.Int16().to_python()
<class 'int'>
>>> pl.Float32().to_python()
<class 'float'>
>>> pl.Array(pl.Date(), 10).to_python()
<class 'list'>
"""
from polars.datatypes import dtype_to_py_type
return dtype_to_py_type(self)
@classinstmethod
def to_dtype_expr(self) -> pl.DataTypeExpr:
"""
Return a :class:`DataTypeExpr` with a static :class:`DataType`.
Examples
--------
>>> pl.Int16().to_dtype_expr().collect_dtype({})
Int16
"""
from polars._plr import PyDataTypeExpr
return pl.DataTypeExpr._from_pydatatype_expr(PyDataTypeExpr.from_dtype(self))
class NumericType(DataType):
"""Base class for numeric data types."""
@classmethod
def max(cls) -> pl.Expr:
"""
Return a literal expression representing the maximum value of this data type.
Examples
--------
>>> pl.select(pl.Int8.max() == 127)
shape: (1, 1)
┌─────────┐
│ literal │
│ --- │
│ bool │
╞═════════╡
│ true │
└─────────┘
"""
return pl.Expr._from_pyexpr(plr._get_dtype_max(cls))
@classmethod
def min(cls) -> pl.Expr:
"""
Return a literal expression representing the minimum value of this data type.
Examples
--------
>>> pl.select(pl.Int8.min() == -128)
shape: (1, 1)
┌─────────┐
│ literal │
│ --- │
│ bool │
╞═════════╡
│ true │
└─────────┘
"""
return pl.Expr._from_pyexpr(plr._get_dtype_min(cls))
class IntegerType(NumericType):
"""Base class for integer data types."""
class SignedIntegerType(IntegerType):
"""Base class for signed integer data types."""
class UnsignedIntegerType(IntegerType):
"""Base class for unsigned integer data types."""
class FloatType(NumericType):
"""Base class for float data types."""
class TemporalType(DataType):
"""Base class for temporal data types."""
class NestedType(DataType):
"""Base class for nested data types."""
class ObjectType(DataType):
"""Base class for object data types."""
class Int8(SignedIntegerType):
"""8-bit signed integer type."""
class Int16(SignedIntegerType):
"""16-bit signed integer type."""
class Int32(SignedIntegerType):
"""32-bit signed integer type."""
class Int64(SignedIntegerType):
"""64-bit signed integer type."""
class Int128(SignedIntegerType):
"""
128-bit signed integer type.
.. warning::
This functionality is considered **unstable**.
It is a work-in-progress feature and may not always work as expected.
It may be changed at any point without it being considered a breaking change.
"""
class UInt8(UnsignedIntegerType):
"""8-bit unsigned integer type."""
class UInt16(UnsignedIntegerType):
"""16-bit unsigned integer type."""
class UInt32(UnsignedIntegerType):
"""32-bit unsigned integer type."""
class UInt64(UnsignedIntegerType):
"""64-bit unsigned integer type."""
class UInt128(UnsignedIntegerType):
"""128-bit unsigned integer type.
.. warning::
This functionality is considered **unstable**.
It is a work-in-progress feature and may not always work as expected.
It may be changed at any point without it being considered a breaking change.
"""
class Float16(FloatType):
"""16-bit floating point type.
.. warning::
Regular computing platforms do not natively support `Float16` operations,
and compute operations on `Float16` will be significantly slower as a result
than operation on :class:`Float32` or :class:`Float64`.
As such, it is recommended to cast to `Float32` before doing any compute
operations, and cast back to `Float16` afterward if needed.
"""
class Float32(FloatType):
"""32-bit floating point type."""
class Float64(FloatType):
"""64-bit floating point type."""
class Decimal(NumericType):
"""
Decimal 128-bit type with an optional precision and non-negative scale.
Parameters
----------
precision
Maximum number of digits in each number.
If set to `None` (default), the precision is set to 38 (the maximum
supported by Polars).
scale
Number of digits to the right of the decimal point in each number.
"""
precision: int
scale: int
def __init__(
self,
precision: int | None = None,
scale: int = 0,
) -> None:
if precision is None:
precision = 38
self.precision = precision
self.scale = scale
def __repr__(self) -> str:
return (
f"{self.__class__.__name__}(precision={self.precision}, scale={self.scale})"
)
def __eq__(self, other: PolarsDataType) -> bool: # type: ignore[override]
# allow comparing object instances to class
if type(other) is DataTypeClass and issubclass(other, Decimal):
return True
elif isinstance(other, Decimal):
return self.precision == other.precision and self.scale == other.scale
else:
return False
def __hash__(self) -> int:
return hash((self.__class__, self.precision, self.scale))
class Boolean(DataType):
"""Boolean type."""
class String(DataType):
"""UTF-8 encoded string type."""
# Allow Utf8 as an alias for String
Utf8 = String
class Binary(DataType):
"""Binary type."""
class Date(TemporalType):
"""
Data type representing a calendar date.
Notes
-----
The underlying representation of this type is a 32-bit signed integer.
The integer indicates the number of days since the Unix epoch (1970-01-01).
The number can be negative to indicate dates before the epoch.
"""
class Time(TemporalType):
"""
Data type representing the time of day.
Notes
-----
The underlying representation of this type is a 64-bit signed integer.
The integer indicates the number of nanoseconds since midnight.
"""
@classmethod
def max(cls) -> pl.Expr:
"""
Return a literal expression representing the maximum value of this data type.
Examples
--------
>>> pl.select(pl.Time.max() == 86_399_999_999_999)
shape: (1, 1)
┌─────────┐
│ literal │
│ --- │
│ bool │
╞═════════╡
│ true │
└─────────┘
"""
return pl.Expr._from_pyexpr(plr._get_dtype_max(cls))
@classmethod
def min(cls) -> pl.Expr:
"""
Return a literal expression representing the minimum value of this data type.
Examples
--------
>>> pl.select(pl.Time.min() == 0)
shape: (1, 1)
┌─────────┐
│ literal │
│ --- │
│ bool │
╞═════════╡
│ true │
└─────────┘
"""
return pl.Expr._from_pyexpr(plr._get_dtype_min(cls))
class Datetime(TemporalType):
"""
Data type representing a calendar date and time of day.
Parameters
----------
time_unit : {'us', 'ns', 'ms'}
Unit of time. Defaults to `'us'` (microseconds).
time_zone
Time zone string, as defined in zoneinfo (to see valid strings run
`import zoneinfo; zoneinfo.available_timezones()` for a full list).
When used to match dtypes, can set this to "*" to check for Datetime
columns that have any (non-null) timezone.
Notes
-----
The underlying representation of this type is a 64-bit signed integer.
The integer indicates the number of time units since the Unix epoch
(1970-01-01 00:00:00). The number can be negative to indicate datetimes before the
epoch.
"""
time_unit: TimeUnit
time_zone: str | None
def __init__(
self, time_unit: TimeUnit = "us", time_zone: str | tzinfo | None = None
) -> None:
if time_unit not in ("ms", "us", "ns"):
msg = (
"invalid `time_unit`"
f"\n\nExpected one of {{'ns','us','ms'}}, got {time_unit!r}."
)
raise ValueError(msg)
if isinstance(time_zone, tzinfo):
time_zone = str(time_zone)
self.time_unit = time_unit
self.time_zone = time_zone
def __eq__(self, other: PolarsDataType) -> bool: # type: ignore[override]
# allow comparing object instances to class
if type(other) is DataTypeClass and issubclass(other, Datetime):
return True
elif isinstance(other, Datetime):
return (
self.time_unit == other.time_unit and self.time_zone == other.time_zone
)
else:
return False
def __hash__(self) -> int:
return hash((self.__class__, self.time_unit, self.time_zone))
def __repr__(self) -> str:
class_name = self.__class__.__name__
return (
f"{class_name}(time_unit={self.time_unit!r}, time_zone={self.time_zone!r})"
)
class Duration(TemporalType):
"""
Data type representing a time duration.
Parameters
----------
time_unit : {'us', 'ns', 'ms'}
Unit of time. Defaults to `'us'` (microseconds).
Notes
-----
The underlying representation of this type is a 64-bit signed integer.
The integer indicates an amount of time units and can be negative to indicate
negative time offsets.
"""
time_unit: TimeUnit
def __init__(self, time_unit: TimeUnit = "us") -> None:
if time_unit not in ("ms", "us", "ns"):
msg = (
"invalid `time_unit`"
f"\n\nExpected one of {{'ns','us','ms'}}, got {time_unit!r}."
)
raise ValueError(msg)
self.time_unit = time_unit
def __eq__(self, other: PolarsDataType) -> bool: # type: ignore[override]
# allow comparing object instances to class
if type(other) is DataTypeClass and issubclass(other, Duration):
return True
elif isinstance(other, Duration):
return self.time_unit == other.time_unit
else:
return False
def __hash__(self) -> int:
return hash((self.__class__, self.time_unit))
def __repr__(self) -> str:
class_name = self.__class__.__name__
return f"{class_name}(time_unit={self.time_unit!r})"
class Categories:
"""
A named collection of categories for :py:class:`Categorical`.
Two categories are considered equal (and will use the same physical mapping of
categories to strings) if they have the same name, namespace and physical backing
type, even if they are created in separate calls to `Categories`.
.. warning::
This functionality is currently considered **unstable**. It may be
changed at any point without it being considered a breaking change.
Parameters
----------
name
The name of this `Categories`. If set to `None` or an empty string, this
refers to the global categories.
namespace
An optional namespace for this `Categories`. Defaults to the empty string.
If the name is empty or `None` indicating the global categories, the
namespace must also be empty.
physical : {UInt8, UInt16, UInt32}
The physical type used to represent the categories. Defaults to
:py:class:`UInt32`.
See Also
--------
Categorical
Examples
--------
A `Categories` instance can be indexed using either string or integer keys:
>>> fruit = pl.Categories("fruit")
>>> s = pl.Series(["apple", "banana", "orange"], dtype=pl.Categorical(fruit))
>>> fruit[0]
'apple'
>>> fruit["apple"]
0
All `Categories` objects with the same name, namespace and physical type
share the same mapping, even if they're created separately:
>>> fruit2 = pl.Categories("fruit")
>>> fruit2["banana"]
1
To get a list of all categories, you can iterate over the `Categories` instance:
>>> list(fruit)
['apple', 'banana', 'orange']
.. note::
Because the categories are backed by a concurrent data structure, physical
category values may be reserved before they are assigned a string lexical
value if concurrent queries are running. As a result, the resulting `Series`
may contain `None` values.
The `Categories` instance is only a weak reference to the actual
mapping stored in Polars. If no actual data exists using this mapping (like
a `Series` or `DataFrame`), the mapping is cleaned up by Polars:
>>> del s
>>> print(fruit["apple"])
None
If you wish to keep a persistent mapping, simply keep alive some object which
uses the mapping, e.g. `keepalive = pl.Series([], dtype=pl.Categorical(fruit))`.
"""
_categories: PyCategories
def __init__(
self,
name: str | None = None,
namespace: str = "",
physical: PolarsDataType = pldt.UInt32,
) -> None:
if name is None or name == "":
assert namespace == "", "global categories may not specify a namespace"
assert physical == pldt.UInt32, (
"global categories may not specify a physical type"
)
self._categories = PyCategories.global_categories()
return
if physical == pldt.UInt32:
internal_phys = "u32"
elif physical == pldt.UInt16:
internal_phys = "u16"
elif physical == pldt.UInt8:
internal_phys = "u8"
else:
msg = "Categorical physical must be one of pl.UInt(8|16|32)"
raise TypeError(msg)
self._categories = PyCategories(name, namespace, internal_phys)
@staticmethod
def _from_py_categories(py_categories: PyCategories) -> Categories:
self = Categories.__new__(Categories)
self._categories = py_categories
return self
@staticmethod
def random(
namespace: str = "", physical: PolarsDataType = pldt.UInt32
) -> Categories:
"""
Creates a new `Categories` with a random name.
Parameters
----------
namespace
An optional namespace for this `Categories`. Defaults to the empty string.
physical : {UInt8, UInt16, UInt32}
The physical type used to represent the categories. Defaults
to :py:class:`UInt32`.
"""
if physical == pldt.UInt32:
internal_phys = "u32"
elif physical == pldt.UInt16:
internal_phys = "u16"
elif physical == pldt.UInt8:
internal_phys = "u8"
else:
msg = "Categorical physical must be one of pl.UInt(8|16|32)"
raise TypeError(msg)
return Categories._from_py_categories(
PyCategories.random(namespace, internal_phys)
)
def name(self) -> str:
"""The name of this `Categories`."""
return self._categories.name()
def namespace(self) -> str:
"""The namespace of this `Categories`."""
return self._categories.namespace()
def physical(self) -> PolarsDataType:
"""The physical type used to represent the categories."""
phys = self._categories.physical()
if phys == "u8":
return pldt.UInt8
elif phys == "u16":
return pldt.UInt16
elif phys == "u32":
return pldt.UInt32
else:
msg = "unknown physical dtype"
raise RuntimeError(msg)
def is_global(self) -> bool:
"""Returns whether this refers to the global categories."""
return self._categories.is_global()
def __getitem__(self, key: str | int | None) -> str | int | None:
# TODO: In 2.0, this should raise KeyError instead of returning if key is a str.
# and an IndexError should be raised if the int key is larger than self.len().
# TODO: In 2.0, this should raise TypeError instead of returning if key is None.
if key is None:
return key
elif isinstance(key, str):
return self._categories.get_cat(key)
elif isinstance(key, int):
return self._categories.cat_to_str(key)
else:
msg = f"invalid key type {type(key)}; expected str or int"
raise TypeError(msg)
def __contains__(self, item: str | int) -> bool:
if isinstance(item, str):
return self._categories.get_cat(item) is not None
elif isinstance(item, int):
return self._categories.cat_to_str(item) is not None
else:
return False
def __iter__(self) -> Iterator[str | None]:
for i in range(self._categories.num_cats_upper_bound()):
yield self._categories.cat_to_str(i)
def to_series(self) -> Series:
"""
Return a :class:`Series` containing all categories in this `Categories`.
The categories are ordered by their physical category value.
.. note::
Because the categories are backed by a concurrent data structure, physical
category values may be reserved before they are assigned a string lexical
value if concurrent queries are running. As a result, the resulting `Series`
may contain `None` values.
Examples
--------
>>> fruit = pl.Categories("fruit")
>>> s = pl.Series(["apple", "banana", "orange"], dtype=pl.Categorical(fruit))
>>> fruit.to_series()
shape: (3,)
Series: 'fruit' [str]
[
"apple"
"banana"
"orange"
]
"""
return pl.Series(self.name(), list(self), dtype=String)
def to_dict(self) -> dict[str, int]:
"""
Return a dictionary mapping category strings to their physical category values.
Examples
--------
>>> fruit = pl.Categories("fruit")
>>> s = pl.Series(["apple", "banana", "orange"], dtype=pl.Categorical(fruit))
>>> fruit.to_dict()
{'apple': 0, 'banana': 1, 'orange': 2}
"""
return {cat: i for i, cat in enumerate(self) if cat is not None}
def __repr__(self) -> str:
name = self.name()
namespace = self.namespace()
phys = self.physical()
if self._categories.is_global():
return "Categories()"
elif namespace == "" and phys == pldt.UInt32:
return f'Categories("{name}")'
else:
return f'Categories(name="{name}", namespace="{namespace}", physical=pl.{phys})'
def __hash__(self) -> int:
return hash(self._categories)
def __eq__(self, other: object) -> bool:
return isinstance(other, Categories) and self._categories == other._categories
def __getstate__(self) -> tuple[str, str, PolarsDataType]:
return self.name(), self.namespace(), self.physical()
def __setstate__(self, state: tuple[str, str, PolarsDataType]) -> None:
self.__dict__ = Categories(*state).__dict__
class Categorical(DataType):
"""
A categorical encoding of a set of strings.
Parameters
----------
categories
The categories used for this type; must be a :py:class:`Categories`
instance, or a string which is interpreted as the name of a
:py:class:`Categories`. If not provided, the global categories
(`pl.Categories()`) are used.
For legacy reasons if the string is either `"physical"` or `"lexical"`,
it is ignored and a warning is issued. If you wish to use a `Categories`
named `"physical"` or `"lexical"`, please pass it using
:py:class:`Categories` explicitly.
ordering : {'lexical', 'physical'}
This used to specify how this type was ordered, but now does nothing.
.. deprecated:: 1.32.0
Parameter is now ignored. Always behaves as if `'lexical'` was passed.
See Also
--------
Categories
"""
ordering: CategoricalOrdering | None
categories: Categories
def __init__(
self,
categories: Categories | str | None = None,
*,
ordering: CategoricalOrdering | None = None,
) -> None:
# Because we supported the positional 'ordering' arg in the past, we
# need to check for this in the categories argument.
if isinstance(categories, str):
if categories == "physical" or categories == "lexical":
from polars._utils.deprecation import issue_deprecation_warning
msg = (
"the ordering parameter on Categorical is deprecated. The ordering is now always lexical."
"\n\nIf you meant to use a Categories named 'physical' or 'lexical', pass it using pl.Categories('physical') or pl.Categories('lexical')."
)
issue_deprecation_warning(msg, version="1.32.0")
categories = Categories()
else:
categories = Categories(name=categories)
if ordering is not None:
from polars._utils.deprecation import issue_deprecation_warning
issue_deprecation_warning(
"the ordering parameter on Categorical is deprecated. The ordering is now always lexical.",
version="1.32.0",
)
self.ordering = "lexical"
if categories is None:
self.categories = Categories()
else:
self.categories = categories
def __repr__(self) -> str:
if self.categories.is_global():
return f"{self.__class__.__name__}"
else:
return f"{self.__class__.__name__}({self.categories!r})"
def __eq__(self, other: PolarsDataType) -> bool: # type: ignore[override]
# allow comparing object instances to class
if type(other) is DataTypeClass and issubclass(other, Categorical):
return self.categories.is_global()
elif isinstance(other, Categorical):
return self.categories == other.categories
else:
return False
def __hash__(self) -> int:
return hash((self.__class__, self.categories))
class Enum(DataType):
"""
A fixed categorical encoding of a unique set of strings.
Parameters
----------
categories
The categories in the dataset; must be a unique set of strings, or an
existing Python string-valued enum.
Examples
--------
Explicitly define enumeration categories:
>>> pl.Enum(["north", "south", "east", "west"])
Enum(categories=['north', 'south', 'east', 'west'])
Initialise from an existing Python enumeration:
>>> from http import HTTPMethod
>>> pl.Enum(HTTPMethod)
Enum(categories=['CONNECT', 'DELETE', 'GET', 'HEAD', 'OPTIONS', 'PATCH', 'POST', 'PUT', 'TRACE'])
""" # noqa: W505
categories: Series
def __init__(self, categories: Series | Iterable[str] | type[enum.Enum]) -> None:
if isclass(categories) and issubclass(categories, enum.Enum):
for enum_subclass in (enum.Flag, enum.IntEnum):
if issubclass(categories, enum_subclass):
enum_type_name = categories.__name__
msg = f"Enum categories must be strings; `{enum_type_name}` values are integers"
raise TypeError(msg)
enum_values = [
getattr(v, "value", v) for v in categories.__members__.values()
]
categories = pl.Series(values=enum_values)
elif not isinstance(categories, pl.Series):
categories = pl.Series(values=categories)
if categories.is_empty():
self.categories = pl.Series(name="category", dtype=String)
return
if categories.has_nulls():
msg = "Enum categories must not contain null values"
raise TypeError(msg)
if (dtype := categories.dtype) != String:
msg = f"Enum categories must be strings; found data of type {dtype}"
raise TypeError(msg)
if categories.n_unique() != categories.len():
duplicate = categories.filter(categories.is_duplicated())[0]
msg = f"Enum categories must be unique; found duplicate {duplicate!r}"
raise ValueError(msg)
self.categories = categories.rechunk().alias("category")
def __eq__(self, other: PolarsDataType) -> bool: # type: ignore[override]
# allow comparing object instances to class
if type(other) is DataTypeClass and issubclass(other, Enum):
return True
elif isinstance(other, Enum):
return self.categories.equals(other.categories)
else:
return False
def __hash__(self) -> int:
return hash((self.__class__, tuple(self.categories)))
def __repr__(self) -> str:
class_name = self.__class__.__name__
return f"{class_name}(categories={self.categories.to_list()!r})"
def union(self, other: Enum) -> Enum:
"""
Union of two Enums.
.. deprecated:: 1.38
`Enum.union()` is deprecated and will be removed in version 2.0.
Enums are ordered sets and union cannot preserve both orderings.
"""
from polars._utils.deprecation import issue_deprecation_warning
issue_deprecation_warning(
"`Enum.union()` is deprecated and will be removed in version 2.0. "
"Enums are ordered sets and union cannot preserve both orderings.",
version="1.38",
)
return Enum(
F.concat((self.categories, other.categories)).unique(maintain_order=True)
)
__or__ = union
class Object(ObjectType):
"""Data type for wrapping arbitrary Python objects."""
class Null(DataType):
"""Data type representing null values."""
class Unknown(DataType):
"""Type representing DataType values that could not be determined statically."""
class List(NestedType):
"""
Variable length list type.
Parameters
----------
inner
The `DataType` of the values within each list.
Examples
--------
>>> df = pl.DataFrame(
... {
... "integer_lists": [[1, 2], [3, 4]],
... "float_lists": [[1.0, 2.0], [3.0, 4.0]],
... }
... )
>>> df
shape: (2, 2)
┌───────────────┬─────────────┐
│ integer_lists ┆ float_lists │
│ --- ┆ --- │
│ list[i64] ┆ list[f64] │
╞═══════════════╪═════════════╡
│ [1, 2] ┆ [1.0, 2.0] │
│ [3, 4] ┆ [3.0, 4.0] │
└───────────────┴─────────────┘
"""
inner: PolarsDataType
def __init__(self, inner: PolarsDataType | PythonDataType) -> None:
self.inner = polars.datatypes.parse_into_dtype(inner)
def __eq__(self, other: PolarsDataType) -> bool: # type: ignore[override]
# This equality check allows comparison of type classes and type instances.
# If a parent type is not specific about its inner type, we infer it as equal:
# > list[i64] == list[i64] -> True
# > list[i64] == list[f32] -> False
# > list[i64] == list -> True
# allow comparing object instances to class
if type(other) is DataTypeClass and issubclass(other, List):
return True
elif isinstance(other, List):
return self.inner == other.inner
else:
return False
def __hash__(self) -> int:
return hash((self.__class__, self.inner))
def __repr__(self) -> str:
class_name = self.__class__.__name__
return f"{class_name}({self.inner!r})"
class Array(NestedType):
"""
Fixed length list type.
Parameters
----------
inner
The `DataType` of the values within each array.
shape
The shape of the arrays.
width
The length of the arrays.
.. deprecated:: 0.20.31
The `width` parameter for `Array` is deprecated. Use `shape` instead.
Examples
--------
>>> s = pl.Series("a", [[1, 2], [4, 3]], dtype=pl.Array(pl.Int64, 2))
>>> s
shape: (2,)
Series: 'a' [array[i64, 2]]
[
[1, 2]
[4, 3]
]
"""
inner: PolarsDataType
size: int
shape: tuple[int, ...]
def __init__(
self,
inner: PolarsDataType | PythonDataType,
shape: int | tuple[int, ...] | None = None,
*,
width: int | None = None,
) -> None:
if width is not None:
from polars._utils.deprecation import issue_deprecation_warning
issue_deprecation_warning(
"the `width` parameter for `Array` is deprecated. Use `shape` instead.",
version="0.20.31",
)
shape = width
elif shape is None:
msg = "Array constructor is missing the required argument `shape`"
raise TypeError(msg)
inner_parsed = polars.datatypes.parse_into_dtype(inner)
inner_shape = inner_parsed.shape if isinstance(inner_parsed, Array) else ()
if isinstance(shape, int):
self.inner = inner_parsed
self.size = shape
self.shape = (shape,) + inner_shape
elif isinstance(shape, tuple) and isinstance(shape[0], int): # type: ignore[redundant-expr]
if len(shape) > 1:
inner_parsed = Array(inner_parsed, shape[1:])
self.inner = inner_parsed
self.size = shape[0]
self.shape = shape + inner_shape
else:
msg = f"invalid input for shape: {shape!r}"
raise TypeError(msg)
def __eq__(self, other: PolarsDataType) -> bool: # type: ignore[override]
# This equality check allows comparison of type classes and type instances.
# If a parent type is not specific about its inner type, we infer it as equal:
# > array[i64] == array[i64] -> True
# > array[i64] == array[f32] -> False
# > array[i64] == array -> True
# allow comparing object instances to class
if type(other) is DataTypeClass and issubclass(other, Array):
return True
elif isinstance(other, Array):
if self.shape != other.shape:
return False
else:
return self.inner == other.inner
else:
return False
def __hash__(self) -> int:
return hash((self.__class__, self.inner, self.size))
def __repr__(self) -> str:
# Get leaf type
dtype = self.inner
while isinstance(dtype, Array):
dtype = dtype.inner
class_name = self.__class__.__name__
return f"{class_name}({dtype!r}, shape={self.shape})"
@property
def width(self) -> int:
"""The size of the Array."""
from polars._utils.deprecation import issue_deprecation_warning
issue_deprecation_warning(
"the `width` attribute for `Array` is deprecated. Use `size` instead.",
version="0.20.31",
)
return self.size
class Field:
"""
Definition of a single field within a `Struct` DataType.
Parameters
----------
name
The name of the field within its parent `Struct`.
dtype
The `DataType` of the field's values.
"""
name: str
dtype: PolarsDataType
def __init__(self, name: str, dtype: PolarsDataType) -> None:
self.name = name
self.dtype = polars.datatypes.parse_into_dtype(dtype)
def __eq__(self, other: Field) -> bool: # type: ignore[override]
return (self.name == other.name) & (self.dtype == other.dtype)
def __hash__(self) -> int:
return hash((self.name, self.dtype))
def __repr__(self) -> str:
class_name = self.__class__.__name__
return f"{class_name}({self.name!r}, {self.dtype})"
class Struct(NestedType):
"""
Struct composite type.
Parameters
----------
fields
The fields that make up the struct. Can be either a sequence of Field
objects or a mapping of column names to data types.
Examples
--------
Initialize using a dictionary:
>>> dtype = pl.Struct({"a": pl.Int8, "b": pl.List(pl.String)})
>>> dtype
Struct({'a': Int8, 'b': List(String)})
Initialize using a list of Field objects:
>>> dtype = pl.Struct([pl.Field("a", pl.Int8), pl.Field("b", pl.List(pl.String))])
>>> dtype
Struct({'a': Int8, 'b': List(String)})
When initializing a Series, Polars can infer a struct data type from the data.
>>> s = pl.Series([{"a": 1, "b": ["x", "y"]}, {"a": 2, "b": ["z"]}])
>>> s
shape: (2,)
Series: '' [struct[2]]
[
{1,["x", "y"]}
{2,["z"]}
]
>>> s.dtype
Struct({'a': Int64, 'b': List(String)})
"""
fields: list[Field]
def __init__(self, fields: Sequence[Field] | SchemaDict) -> None:
if isinstance(fields, Mapping):
self.fields = [Field(name, dtype) for name, dtype in fields.items()]
else:
self.fields = list(fields)
def __eq__(self, other: PolarsDataType) -> bool: # type: ignore[override]
# The comparison allows comparing objects to classes, and specific
# inner types to those without (eg: inner=None). if one of the
# arguments is not specific about its inner type we infer it
# as being equal. (See the List type for more info).
if isclass(other) and issubclass(other, Struct):
return True
elif isinstance(other, Struct):
return self.fields == other.fields
else:
return False
def __hash__(self) -> int:
return hash((self.__class__, tuple(self.fields)))
def __iter__(self) -> Iterator[tuple[str, PolarsDataType]]:
for fld in self.fields:
yield fld.name, fld.dtype
def __reversed__(self) -> Iterator[tuple[str, PolarsDataType]]:
for fld in reversed(self.fields):
yield fld.name, fld.dtype
def __repr__(self) -> str:
class_name = self.__class__.__name__
return f"{class_name}({dict(self)})"
def to_schema(self) -> OrderedDict[str, PolarsDataType]:
"""Return Struct dtype as a schema dict."""
return OrderedDict(self)
class BaseExtension(DataType):
"""
Base class for extension data types.
.. warning::
This functionality is considered **unstable**. It may be changed at any
point without it being considered a breaking change.
See Also
--------
Extension
polars.register_extension_type
"""
def __init__(
self, name: str, storage: PolarsDataType, metadata: str | None = None
) -> None:
self._name = name
self._storage = storage
self._metadata = metadata
@classmethod
def ext_from_params(
cls, name: str, storage: PolarsDataType, metadata: str | None
) -> Any:
"""Creates an Extension type instance from its parameters."""
slf = cls.__new__(cls)
slf._name = name
slf._storage = storage
slf._metadata = metadata
return slf
def ext_name(self) -> str:
"""Returns the name of this extension type."""
return self._name
def ext_storage(self) -> PolarsDataType:
"""Returns the storage type for this extension type."""
return self._storage
def ext_metadata(self) -> str | None:
"""Returns the metadata for this extension type."""
return self._metadata
def _string_repr(self) -> str:
"""
Return a short string representation of the extension type.
This should be lowercase and if feasible show parameters in brackets,
for example i64, str, datetime[ns], etc. This is used when displaying
dataframes in a human-readable format, so brevity is important.
This function starts with an underscore for historical reasons; it is
intended to be overridden by subclasses.
"""
s = self.ext_name().lower()
if len(s) <= 12:
return s
else:
return s[:10] + ".."
def __repr__(self) -> str:
md = self.ext_metadata()
if md is not None:
return f"{self.__class__.__name__}({self.ext_name()!r}, {self.ext_storage()!r}, {md!r})"
else:
return f"{self.__class__.__name__}({self.ext_name()!r}, {self.ext_storage()!r})"
# It's not recommended to override the below methods.
def __hash__(self) -> int:
return hash((self.ext_name(), self.ext_storage(), self.ext_metadata()))
@overload # type: ignore[override]
def __eq__(self, other: pl.DataTypeExpr) -> pl.Expr: ...
@overload
def __eq__(self, other: PolarsDataType) -> bool: ...
def __eq__(self, other: pl.DataTypeExpr | PolarsDataType) -> pl.Expr | bool:
if isinstance(other, pl.DataTypeExpr):
return self.to_dtype_expr() == other
else:
return (
isinstance(other, BaseExtension)
and self.ext_name() == other.ext_name()
and self.ext_storage() == other.ext_storage()
and self.ext_metadata() == other.ext_metadata()
)
def __getstate__(self) -> tuple[str, PolarsDataType, str | None]:
return self.ext_name(), self.ext_storage(), self.ext_metadata()
def __setstate__(self, state: tuple[str, PolarsDataType, str | None]) -> None:
self.__dict__ = type(self).ext_from_params(*state).__dict__
class Extension(BaseExtension):
"""
Generic extension data type.
When `UNKNOWN_EXTENSION_TYPE_BEHAVIOR` is set to `"load_as_extension"`, any
non-registered extension type will be loaded as this type.
.. warning::
This functionality is considered **unstable**. It may be changed at any
point without it being considered a breaking change.
See Also
--------
BaseExtension
polars.register_extension_type
"""classDiagram
class DataTypeClass {
<<元类>>
}
DataTypeClass <-- DataType : 元类
DataType <|-- NumericType
DataType <|-- Boolean
DataType <|-- String
DataType <|-- Binary
DataType <|-- TemporalType
DataType <|-- ObjectType
DataType <|-- Null
DataType <|-- Unknown
DataType <|-- NestedType
DataType <|-- BaseExtension
DataType <|-- Categorical
DataType <|-- Enum
NumericType <|-- IntegerType
NumericType <|-- FloatType
NumericType <|-- Decimal
IntegerType <|-- SignedIntegerType
IntegerType <|-- UnsignedIntegerType
SignedIntegerType <|-- Int8_16_32_64_128
UnsignedIntegerType <|-- UInt8_16_32_64_128
FloatType <|-- Float16_32_64
TemporalType <|-- Date
TemporalType <|-- Time
TemporalType <|-- Datetime
TemporalType <|-- Duration
ObjectType <|-- Object
NestedType <|-- List
NestedType <|-- Array
NestedType <|-- Struct
BaseExtension <|-- Extension
Categorical *--> Categories : 组合(categories)
Struct *--> Field : 组合(fields)
Array --> Array : 递归(inner)
- 这里主要是py的定义,对应的rust定义在
crates/polars-core/src/datatypes/dtype.rsGitHub
pub enum DataType {
Boolean,
UInt8,
UInt16,
UInt32,
UInt64,
UInt128,
Int8,
Int16,
Int32,
Int64,
Int128,
Float16,
Float32,
Float64,
/// Fixed point decimal type optional precision and non-negative scale.
/// This is backed by a signed 128-bit integer which allows for up to 38 significant digits.
/// Meaning max precision is 38.
#[cfg(feature = "dtype-decimal")]
Decimal(usize, usize), // (precision, scale), invariant: 1 <= precision <= 38.
/// String data
String,
Binary,
BinaryOffset,
/// A 32-bit date representing the elapsed time since UNIX epoch (1970-01-01)
/// in days (32 bits).
Date,
/// A 64-bit date representing the elapsed time since UNIX epoch (1970-01-01)
/// in the given timeunit (64 bits).
Datetime(TimeUnit, Option<TimeZone>),
/// 64-bit integer representing difference between times in milliseconds or nanoseconds
Duration(TimeUnit),
/// A 64-bit time representing the elapsed time since midnight in nanoseconds
Time,
/// A nested list with a fixed size in each row
#[cfg(feature = "dtype-array")]
Array(Box<DataType>, usize),
/// A nested list with a variable size in each row
List(Box<DataType>),
/// A generic type that can be used in a `Series`
/// &'static str can be used to determine/set inner type
#[cfg(feature = "object")]
Object(&'static str),
Null,
#[cfg(feature = "dtype-categorical")]
Categorical(Arc<Categories>, Arc<CategoricalMapping>),
// It is an Option, so that matching Enum/Categoricals can take the same guards.
#[cfg(feature = "dtype-categorical")]
Enum(Arc<FrozenCategories>, Arc<CategoricalMapping>),
#[cfg(feature = "dtype-struct")]
Struct(Vec<Field>),
#[cfg(feature = "dtype-extension")]
Extension(ExtensionTypeInstance, Box<DataType>),
// some logical types we cannot know statically, e.g. Datetime
Unknown(UnknownKind),
}- 每种类型(如 Int8Type、StringType)都实现此 trait,通过关联类型指明底层存储:
crates/polars-core/src/datatypes/mod.rsGitHub
pub unsafe trait PolarsDataType: Send + Sync + Sized + 'static {
type Physical<'a>: std::fmt::Debug + Clone;
type OwnedPhysical: std::fmt::Debug + Send + Sync + Clone + PartialEq;
type ZeroablePhysical<'a>: Zeroable + From<Self::Physical<'a>>;
type Array: for<'a> StaticArray<
ValueT<'a> = Self::Physical<'a>,
ZeroableValueT<'a> = Self::ZeroablePhysical<'a>,
>;
type IsNested;
type HasViews;
type IsStruct;
type IsObject;
/// Returns the DataType variant associated with this PolarsDataType.
/// Not implemented for types whose DataTypes have parameters.
fn get_static_dtype() -> DataType
where
Self: Sized;
}- 对于这个trait,不同的的类型实现的层级不一样,比如
| 层级 | trait | 举例 | 说明 |
|---|---|---|---|
| 0 | PolarsDataType | DateType, DatetimeType | 所有类型都实现 |
| 1 | PolarsPhysicalType | Int32Type, StringType | 只有物理类型实现,对应 ChunkedArray<Self> |
| 2 | PolarsNumericType | Int32Type, Float64Type | 数值类型,关联 type Native = i32 |
| 3 | PolarsIntegerType / PolarsFloatType | Int32Type, Float32Type | 整数/浮点标记 |
其中逻辑类型(DateType, DatetimeType, DurationType, TimeType, DecimalType)只实现 PolarsDataType,不实现 PolarsPhysicalType——它们没有自己的 ChunkedArray,而是复用底层物理类型。
- 各类型的实际定义:
crates/polars-core/src/datatypes/mod.rsGitHub
impl_polars_num_datatype!(PolarsIntegerType, UInt8Type, UInt8, u8, u8);
impl_polars_num_datatype!(PolarsIntegerType, UInt16Type, UInt16, u16, u16);
impl_polars_num_datatype!(PolarsIntegerType, UInt32Type, UInt32, u32, u32);
impl_polars_num_datatype!(PolarsIntegerType, UInt64Type, UInt64, u64, u64);
#[cfg(feature = "dtype-u128")]
impl_polars_num_datatype!(PolarsIntegerType, UInt128Type, UInt128, u128, u128);
impl_polars_num_datatype!(PolarsIntegerType, Int8Type, Int8, i8, i8);
impl_polars_num_datatype!(PolarsIntegerType, Int16Type, Int16, i16, i16);
impl_polars_num_datatype!(PolarsIntegerType, Int32Type, Int32, i32, i32);
impl_polars_num_datatype!(PolarsIntegerType, Int64Type, Int64, i64, i64);
#[cfg(feature = "dtype-i128")]
impl_polars_num_datatype!(PolarsIntegerType, Int128Type, Int128, i128, i128);
#[cfg(feature = "dtype-f16")]
impl_polars_num_datatype!(PolarsFloatType, Float16Type, Float16, pf16, pf16);
impl_polars_num_datatype!(PolarsFloatType, Float32Type, Float32, f32, f32);
impl_polars_num_datatype!(PolarsFloatType, Float64Type, Float64, f64, f64);
impl_polars_datatype!(StringType, DataType::String, Utf8ViewArray, 'a, &'a str, Option<&'a str>, String, TrueT);
impl_polars_datatype!(BinaryType, DataType::Binary, BinaryViewArray, 'a, &'a [u8], Option<&'a [u8]>, Box<[u8]>, TrueT);
impl_polars_datatype!(BinaryOffsetType, DataType::BinaryOffset, BinaryArray<i64>, 'a, &'a [u8], Option<&'a [u8]>, Box<[u8]>, FalseT);
impl_polars_datatype!(BooleanType, DataType::Boolean, BooleanArray, 'a, bool, bool, bool, FalseT);
#[cfg(feature = "dtype-decimal")]
impl_polars_datatype!(DecimalType, unimplemented!(), PrimitiveArray<i128>, 'a, i128, i128, i128, FalseT);
impl_polars_datatype!(DatetimeType, unimplemented!(), PrimitiveArray<i64>, 'a, i64, i64, i64, FalseT);
impl_polars_datatype!(DurationType, unimplemented!(), PrimitiveArray<i64>, 'a, i64, i64, i64, FalseT);
impl_polars_datatype!(CategoricalType, unimplemented!(), PrimitiveArray<u32>, 'a, u32, u32, u32, FalseT);
impl_polars_datatype!(DateType, DataType::Date, PrimitiveArray<i32>, 'a, i32, i32, i32, FalseT);
impl_polars_datatype!(TimeType, DataType::Time, PrimitiveArray<i64>, 'a, i64, i64, i64, FalseT);
impl_polars_categorical_datatype!(Categorical8Type, UInt8Type, u8, U8);
impl_polars_categorical_datatype!(Categorical16Type, UInt16Type, u16, U16);
impl_polars_categorical_datatype!(Categorical32Type, UInt32Type, u32, U32);数值和字符串类型直接用宏声明:
- impl_polars_num_datatype! → 生成空 struct + PolarsDataType + PolarsNumericType + PolarsIntegerType/PolarsFloatType
- impl_polars_datatype! → 只生成 PolarsDataType(供逻辑类型、String、Boolean 等用)
ChunkedArray<T>— 实际数据容器
crates/polars-core/src/chunked_array/mod.rsGitHub
pub struct ChunkedArray<T: PolarsDataType> {
pub(crate) field: Arc<Field>,
pub(crate) chunks: Vec<ArrayRef>,
pub(crate) flags: StatisticsFlagsIM,
length: usize,
null_count: usize,
_pd: std::marker::PhantomData<T>,
}crates/polars-core/src/datatypes/mod.rsGitHub
pub type ArrayChunked = ChunkedArray<FixedSizeListType>;
pub type ListChunked = ChunkedArray<ListType>;
pub type BooleanChunked = ChunkedArray<BooleanType>;
pub type UInt8Chunked = ChunkedArray<UInt8Type>;
pub type UInt16Chunked = ChunkedArray<UInt16Type>;
pub type UInt32Chunked = ChunkedArray<UInt32Type>;
pub type UInt64Chunked = ChunkedArray<UInt64Type>;
#[cfg(feature = "dtype-u128")]
pub type UInt128Chunked = ChunkedArray<UInt128Type>;
pub type Int8Chunked = ChunkedArray<Int8Type>;
pub type Int16Chunked = ChunkedArray<Int16Type>;
pub type Int32Chunked = ChunkedArray<Int32Type>;
pub type Int64Chunked = ChunkedArray<Int64Type>;
#[cfg(feature = "dtype-i128")]
pub type Int128Chunked = ChunkedArray<Int128Type>;
#[cfg(feature = "dtype-f16")]
pub type Float16Chunked = ChunkedArray<Float16Type>;
pub type Float32Chunked = ChunkedArray<Float32Type>;
pub type Float64Chunked = ChunkedArray<Float64Type>;
pub type StringChunked = ChunkedArray<StringType>;
pub type BinaryChunked = ChunkedArray<BinaryType>;
pub type BinaryOffsetChunked = ChunkedArray<BinaryOffsetType>;
#[cfg(feature = "object")]
pub type ObjectChunked<T> = ChunkedArray<ObjectType<T>>;T 是 Int8Type、StringType 这样的类型标记。所有计算(求和、过滤、cast 等)都是通过 impl<T: PolarsNumericType> ChunkedArray<T> 这类泛型实现。
Logical<L, P>— 逻辑类型包装
crates/polars-core/src/chunked_array/logical/mod.rsGitHub
pub struct Logical<Logical: PolarsDataType, Physical: PolarsDataType> {
pub phys: ChunkedArray<Physical>,
pub dtype: DataType,
_phantom: PhantomData<Logical>,
}例子:Logical<DateType, Int32Type> 对 ChunkedArray<Int32Type> 添加日期语义。DateType 不实现 PolarsPhysicalType,所以没有 ChunkedArray<DateType>。
- Series — 统一接口
crates/polars-core/src/series/mod.rsGitHub
#[derive(Clone)]
#[must_use]
pub struct Series(pub Arc<dyn SeriesTrait>);SeriesTrait 定义了 rename、dtype、chunks、cast、filter、take 等所有操作,每个物理类型通过 SeriesWrap<ChunkedArray<T>> 实现它。
其底层核心用的是Arrow,几个关键原因:
- 零拷贝互操作 — Arrow 是跨语言的标准列式内存格式。Polars 和 PyArrow、DuckDB、Spark 等可以共享同一块内存,无需序列化/反序列化。数据从 Python 传到 Rust 零拷贝。
- 标准化内存布局 — 空值位图、offsets、buffers 都是定义好的。Polars 只需要对接 arrow-rs 这个成熟的 Rust 实现,不用自己处理位操作、变长编码等底层细节。
- 不可变 + 分片 (chunked) — Arrow 数组是不可变的,Polars 在上面包装 ChunkedArray:追加数据就是 push 一个新 chunk,零拷贝切片就是共享同一个 ArrayRef,天然适合并行计算。
- 列式 + SIMD 友好 — Arrow 的列式布局让向量化计算(sum、filter、sort 等)能充分发挥 SIMD 和缓存局部性优势。
- 生态 — Parquet/IPC/ Flight/ADBC 等格式和协议都基于 Arrow 构建,Polars 可以直接复用大量 I/O 逻辑,不需要为每种格式单独实现一套内存模型。
如果用 Rust 原生类型(
Vec<i32>、Vec<String>),就得自己实现空值位图、类型统一 dispatch、跨语言 FFI 层、多 chunk 管理——这些 Arrow 已经全部标准化了,Polars 专注于做它擅长的计算引擎和 API 设计。