Files
2026-07-13 13:17:40 +08:00

147 lines
4.4 KiB
Python

import collections.abc
import typing
import numpy as np
import pandas as pd
import pyarrow as pa
from pandas._libs import lib
from pandas._typing import ArrayLike, Dtype, PositionalIndexer, TakeIndexer, npt
import ray.data._internal.object_extensions.arrow
from ray.util.annotations import PublicAPI
# See https://pandas.pydata.org/docs/development/extending.html for more information.
@PublicAPI(stability="alpha")
class PythonObjectArray(pd.api.extensions.ExtensionArray):
"""Implements the Pandas extension array interface for the Arrow object array"""
def __init__(self, values: collections.abc.Iterable[typing.Any]):
vals = list(values)
self.values = np.empty(len(vals), dtype=object)
self.values[:] = vals
@classmethod
def _from_sequence(
cls,
scalars: collections.abc.Sequence[typing.Any],
*,
dtype: typing.Union[Dtype, None] = None,
copy: bool = False,
) -> "PythonObjectArray":
return PythonObjectArray(scalars)
@classmethod
def _from_factorized(
cls, values: collections.abc.Sequence[typing.Any], original: "PythonObjectArray"
) -> "PythonObjectArray":
return PythonObjectArray(values)
def __getitem__(self, item: PositionalIndexer) -> typing.Any:
return self.values[item]
def __setitem__(self, key, value) -> None:
self.values[key] = value
def __len__(self) -> int:
return len(self.values)
def __eq__(self, other: object) -> ArrayLike:
if isinstance(other, PythonObjectArray):
return self.values == other.values
elif isinstance(other, np.ndarray):
return self.values == other
else:
return NotImplemented
def to_numpy(
self,
dtype: typing.Union["npt.DTypeLike", None] = None,
copy: bool = False,
na_value: object = lib.no_default,
) -> np.ndarray:
result = self.values
if copy or na_value is not lib.no_default:
result = result.copy()
if na_value is not lib.no_default:
result[self.isna()] = na_value
return result
@property
def dtype(self) -> pd.api.extensions.ExtensionDtype:
return PythonObjectDtype()
@property
def nbytes(self) -> int:
return self.values.nbytes
def __arrow_array__(self, type=None):
return ray.data._internal.object_extensions.arrow.ArrowPythonObjectArray.from_objects(
self.values
)
def isna(self) -> np.ndarray:
return pd.isnull(self.values)
def take(
self,
indices: TakeIndexer,
*,
allow_fill: bool = False,
fill_value: typing.Any = None,
) -> "PythonObjectArray":
if allow_fill and fill_value is None:
fill_value = self.dtype.na_value
result = pd.core.algorithms.take(
self.values, indices, allow_fill=allow_fill, fill_value=fill_value
)
return self._from_sequence(result, dtype=self.dtype)
def copy(self) -> "PythonObjectArray":
return PythonObjectArray(self.values)
@classmethod
def _concat_same_type(
cls, to_concat: collections.abc.Sequence["PythonObjectArray"]
) -> "PythonObjectArray":
values_to_concat = [element.values for element in to_concat]
return cls(np.concatenate(values_to_concat))
@PublicAPI(stability="alpha")
@pd.api.extensions.register_extension_dtype
class PythonObjectDtype(pd.api.extensions.ExtensionDtype):
@classmethod
def construct_from_string(cls, string: str):
if string != "python_object()":
raise TypeError(f"Cannot construct a '{cls.__name__}' from '{string}'")
return cls()
@property
def type(self):
"""
The scalar type for the array, e.g. ``int``
It's expected ``ExtensionArray[item]`` returns an instance
of ``ExtensionDtype.type`` for scalar ``item``, assuming
that value is valid (not NA). NA values do not need to be
instances of `type`.
"""
return object
@property
def name(self) -> str:
return "python_object()"
@classmethod
def construct_array_type(cls: type) -> type:
"""
Return the array type associated with this dtype.
"""
return PythonObjectArray
def __from_arrow__(
self, array: typing.Union[pa.Array, pa.ChunkedArray]
) -> PythonObjectArray:
return PythonObjectArray(array.to_pylist())