accessor.py 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  1. # -*- coding: utf-8 -*-
  2. """
  3. accessor.py contains base classes for implementing accessor properties
  4. that can be mixed into or pinned onto other pandas classes.
  5. """
  6. import warnings
  7. from pandas.util._decorators import Appender
  8. class DirNamesMixin(object):
  9. _accessors = frozenset()
  10. _deprecations = frozenset(
  11. ['asobject', 'base', 'data', 'flags', 'itemsize', 'strides'])
  12. def _dir_deletions(self):
  13. """ delete unwanted __dir__ for this object """
  14. return self._accessors | self._deprecations
  15. def _dir_additions(self):
  16. """ add additional __dir__ for this object """
  17. rv = set()
  18. for accessor in self._accessors:
  19. try:
  20. getattr(self, accessor)
  21. rv.add(accessor)
  22. except AttributeError:
  23. pass
  24. return rv
  25. def __dir__(self):
  26. """
  27. Provide method name lookup and completion
  28. Only provide 'public' methods
  29. """
  30. rv = set(dir(type(self)))
  31. rv = (rv - self._dir_deletions()) | self._dir_additions()
  32. return sorted(rv)
  33. class PandasDelegate(object):
  34. """
  35. an abstract base class for delegating methods/properties
  36. """
  37. def _delegate_property_get(self, name, *args, **kwargs):
  38. raise TypeError("You cannot access the "
  39. "property {name}".format(name=name))
  40. def _delegate_property_set(self, name, value, *args, **kwargs):
  41. raise TypeError("The property {name} cannot be set".format(name=name))
  42. def _delegate_method(self, name, *args, **kwargs):
  43. raise TypeError("You cannot call method {name}".format(name=name))
  44. @classmethod
  45. def _add_delegate_accessors(cls, delegate, accessors, typ,
  46. overwrite=False):
  47. """
  48. Add accessors to cls from the delegate class.
  49. Parameters
  50. ----------
  51. cls : the class to add the methods/properties to
  52. delegate : the class to get methods/properties & doc-strings
  53. acccessors : string list of accessors to add
  54. typ : 'property' or 'method'
  55. overwrite : boolean, default False
  56. overwrite the method/property in the target class if it exists
  57. """
  58. def _create_delegator_property(name):
  59. def _getter(self):
  60. return self._delegate_property_get(name)
  61. def _setter(self, new_values):
  62. return self._delegate_property_set(name, new_values)
  63. _getter.__name__ = name
  64. _setter.__name__ = name
  65. return property(fget=_getter, fset=_setter,
  66. doc=getattr(delegate, name).__doc__)
  67. def _create_delegator_method(name):
  68. def f(self, *args, **kwargs):
  69. return self._delegate_method(name, *args, **kwargs)
  70. f.__name__ = name
  71. f.__doc__ = getattr(delegate, name).__doc__
  72. return f
  73. for name in accessors:
  74. if typ == 'property':
  75. f = _create_delegator_property(name)
  76. else:
  77. f = _create_delegator_method(name)
  78. # don't overwrite existing methods/properties
  79. if overwrite or not hasattr(cls, name):
  80. setattr(cls, name, f)
  81. def delegate_names(delegate, accessors, typ, overwrite=False):
  82. """
  83. Add delegated names to a class using a class decorator. This provides
  84. an alternative usage to directly calling `_add_delegate_accessors`
  85. below a class definition.
  86. Parameters
  87. ----------
  88. delegate : object
  89. the class to get methods/properties & doc-strings
  90. acccessors : Sequence[str]
  91. List of accessor to add
  92. typ : {'property', 'method'}
  93. overwrite : boolean, default False
  94. overwrite the method/property in the target class if it exists
  95. Returns
  96. -------
  97. callable
  98. A class decorator.
  99. Examples
  100. --------
  101. @delegate_names(Categorical, ["categories", "ordered"], "property")
  102. class CategoricalAccessor(PandasDelegate):
  103. [...]
  104. """
  105. def add_delegate_accessors(cls):
  106. cls._add_delegate_accessors(delegate, accessors, typ,
  107. overwrite=overwrite)
  108. return cls
  109. return add_delegate_accessors
  110. # Ported with modifications from xarray
  111. # https://github.com/pydata/xarray/blob/master/xarray/core/extensions.py
  112. # 1. We don't need to catch and re-raise AttributeErrors as RuntimeErrors
  113. # 2. We use a UserWarning instead of a custom Warning
  114. class CachedAccessor(object):
  115. """
  116. Custom property-like object (descriptor) for caching accessors.
  117. Parameters
  118. ----------
  119. name : str
  120. The namespace this will be accessed under, e.g. ``df.foo``
  121. accessor : cls
  122. The class with the extension methods. The class' __init__ method
  123. should expect one of a ``Series``, ``DataFrame`` or ``Index`` as
  124. the single argument ``data``
  125. """
  126. def __init__(self, name, accessor):
  127. self._name = name
  128. self._accessor = accessor
  129. def __get__(self, obj, cls):
  130. if obj is None:
  131. # we're accessing the attribute of the class, i.e., Dataset.geo
  132. return self._accessor
  133. accessor_obj = self._accessor(obj)
  134. # Replace the property with the accessor object. Inspired by:
  135. # http://www.pydanny.com/cached-property.html
  136. # We need to use object.__setattr__ because we overwrite __setattr__ on
  137. # NDFrame
  138. object.__setattr__(obj, self._name, accessor_obj)
  139. return accessor_obj
  140. def _register_accessor(name, cls):
  141. def decorator(accessor):
  142. if hasattr(cls, name):
  143. warnings.warn(
  144. 'registration of accessor {!r} under name {!r} for type '
  145. '{!r} is overriding a preexisting attribute with the same '
  146. 'name.'.format(accessor, name, cls),
  147. UserWarning,
  148. stacklevel=2)
  149. setattr(cls, name, CachedAccessor(name, accessor))
  150. cls._accessors.add(name)
  151. return accessor
  152. return decorator
  153. _doc = """\
  154. Register a custom accessor on %(klass)s objects.
  155. Parameters
  156. ----------
  157. name : str
  158. Name under which the accessor should be registered. A warning is issued
  159. if this name conflicts with a preexisting attribute.
  160. See Also
  161. --------
  162. %(others)s
  163. Notes
  164. -----
  165. When accessed, your accessor will be initialized with the pandas object
  166. the user is interacting with. So the signature must be
  167. .. code-block:: python
  168. def __init__(self, pandas_object): # noqa: E999
  169. ...
  170. For consistency with pandas methods, you should raise an ``AttributeError``
  171. if the data passed to your accessor has an incorrect dtype.
  172. >>> pd.Series(['a', 'b']).dt
  173. Traceback (most recent call last):
  174. ...
  175. AttributeError: Can only use .dt accessor with datetimelike values
  176. Examples
  177. --------
  178. In your library code::
  179. import pandas as pd
  180. @pd.api.extensions.register_dataframe_accessor("geo")
  181. class GeoAccessor(object):
  182. def __init__(self, pandas_obj):
  183. self._obj = pandas_obj
  184. @property
  185. def center(self):
  186. # return the geographic center point of this DataFrame
  187. lat = self._obj.latitude
  188. lon = self._obj.longitude
  189. return (float(lon.mean()), float(lat.mean()))
  190. def plot(self):
  191. # plot this array's data on a map, e.g., using Cartopy
  192. pass
  193. Back in an interactive IPython session:
  194. >>> ds = pd.DataFrame({'longitude': np.linspace(0, 10),
  195. ... 'latitude': np.linspace(0, 20)})
  196. >>> ds.geo.center
  197. (5.0, 10.0)
  198. >>> ds.geo.plot()
  199. # plots data on a map
  200. """
  201. @Appender(_doc % dict(klass="DataFrame",
  202. others=("register_series_accessor, "
  203. "register_index_accessor")))
  204. def register_dataframe_accessor(name):
  205. from pandas import DataFrame
  206. return _register_accessor(name, DataFrame)
  207. @Appender(_doc % dict(klass="Series",
  208. others=("register_dataframe_accessor, "
  209. "register_index_accessor")))
  210. def register_series_accessor(name):
  211. from pandas import Series
  212. return _register_accessor(name, Series)
  213. @Appender(_doc % dict(klass="Index",
  214. others=("register_dataframe_accessor, "
  215. "register_series_accessor")))
  216. def register_index_accessor(name):
  217. from pandas import Index
  218. return _register_accessor(name, Index)