|
1 | 1 | #!/usr/bin/env python |
2 | 2 | # -*- coding: utf-8 -*- |
3 | | -""" |
4 | | -
|
5 | | -An example/recipe for creating a custom accessor. |
6 | | -
|
7 | | -
|
8 | | -The primary use case for accessors is when a Series contains instances |
9 | | -of a particular class and we want to access properties/methods of these |
10 | | -instances in Series form. |
11 | | -
|
12 | | -Suppose we have a custom State class representing US states: |
13 | | -
|
14 | | -class State(object): |
15 | | - def __repr__(self): |
16 | | - return repr(self.name) |
17 | | -
|
18 | | - def __init__(self, name): |
19 | | - self.name = name |
20 | | - self._abbrev_dict = {'California': 'CA', 'Alabama': 'AL'} |
21 | | -
|
22 | | - @property |
23 | | - def abbrev(self): |
24 | | - return self._abbrev_dict[self.name] |
25 | | -
|
26 | | - @abbrev.setter |
27 | | - def abbrev(self, value): |
28 | | - self._abbrev_dict[self.name] = value |
29 | | -
|
30 | | - def fips(self): |
31 | | - return {'California': 6, 'Alabama': 1}[self.name] |
32 | | -
|
33 | | -
|
34 | | -We can construct a series of these objects: |
35 | | -
|
36 | | ->>> ser = pd.Series([State('Alabama'), State('California')]) |
37 | | ->>> ser |
38 | | -0 'Alabama' |
39 | | -1 'California' |
40 | | -dtype: object |
| 3 | +from pandas.core.base import PandasObject |
41 | 4 |
|
42 | | -We would like direct access to the `abbrev` property and `fips` method. |
43 | | -One option is to access these manually with `apply`: |
44 | 5 |
|
45 | | ->>> ser.apply(lambda x: x.fips()) |
46 | | -0 1 |
47 | | -1 6 |
48 | | -dtype: int64 |
| 6 | +class PandasDelegate(PandasObject): |
| 7 | + """ an abstract base class for delegating methods/properties |
49 | 8 |
|
50 | | -But doing that repeatedly gets old in a hurry, so we decide to make a |
51 | | -custom accessor. This entails subclassing `PandasDelegate` to specify |
52 | | -what should be accessed and how. |
| 9 | + Usage: To make a custom accessor, subclass `PandasDelegate`, overriding |
| 10 | + the methods below. Then decorate this subclass with |
| 11 | + `accessors.wrap_delegate_names` describing the methods and properties |
| 12 | + that should be delegated. |
53 | 13 |
|
54 | | -There are four methods that *may* be defined in this subclass, one of which |
55 | | -*must* be defined. The mandatory method is a classmethod called |
56 | | -`_make_accessor`. `_make_accessor` is responsible doing any validation on |
57 | | -inputs for the accessor. In this case, the inputs must be a Series |
58 | | -containing State objects. |
| 14 | + Examples can be found in: |
59 | 15 |
|
| 16 | + pandas.core.accessors.CategoricalAccessor |
| 17 | + pandas.core.indexes.accessors (complicated example) |
| 18 | + pandas.core.indexes.category.CategoricalIndex |
| 19 | + pandas.core.strings.StringMethods |
| 20 | + pandas.tests.test_accessors |
60 | 21 |
|
61 | | -class StateDelegate(PandasDelegate): |
| 22 | + """ |
62 | 23 |
|
63 | 24 | def __init__(self, values): |
| 25 | + """ |
| 26 | + The subclassed constructor will generally only be called by |
| 27 | + _make_accessor. See _make_accessor.__doc__. |
| 28 | + """ |
64 | 29 | self.values = values |
65 | 30 |
|
66 | 31 | @classmethod |
67 | | - def _make_accessor(cls, data): |
68 | | - if not isinstance(data, pd.Series): |
69 | | - raise ValueError('Input must be a Series of States') |
70 | | - elif not data.apply(lambda x: isinstance(x, State)).all(): |
71 | | - raise ValueError('All entries must be State objects') |
72 | | - return StateDelegate(data) |
73 | | -
|
74 | | -
|
75 | | -With `_make_accessor` defined, we have enough to create the accessor, but |
76 | | -not enough to actually do anything useful with it. In order to access |
77 | | -*methods* of State objects, we implement `_delegate_method`. |
78 | | -`_delegate_method` calls the underlying method for each object in the |
79 | | -series and wraps these in a new Series. The simplest version looks like: |
80 | | -
|
81 | | - def _delegate_method(self, name, *args, **kwargs): |
82 | | - state_method = lambda x: getattr(x, name)(*args, **kwargs) |
83 | | - return self.values.apply(state_method) |
84 | | -
|
85 | | -Similarly in order to access *properties* of State objects, we need to |
86 | | -implement `_delegate_property_get`: |
87 | | -
|
88 | | - def _delegate_property_get(self, name): |
89 | | - state_property = lambda x: getattr(x, name) |
90 | | - return self.values.apply(state_property) |
91 | | -
|
92 | | -
|
93 | | -On ocassion, we may want to be able to *set* property being accessed. |
94 | | -This is discouraged, but allowed (as long as the class being accessed |
95 | | -allows the property to be set). Doing so requires implementing |
96 | | -`_delegate_property_set`: |
97 | | -
|
98 | | - def _delegate_property_set(self, name, new_values): |
99 | | - for (obj, val) in zip(self.values, new_values): |
100 | | - setattr(obj, name, val) |
101 | | -
|
102 | | -
|
103 | | -With these implemented, `StateDelegate` knows how to handle methods and |
104 | | -properties. We just need to tell it what names and properties it is |
105 | | -supposed to handle. This is done by decorating the `StateDelegate` |
106 | | -class with `pd.accessors.wrap_delegate_names`. We apply the decorator |
107 | | -once with a list of all the methods the accessor should recognize and |
108 | | -once with a list of all the properties the accessor should recognize. |
109 | | -
|
110 | | -
|
111 | | -@wrap_delegate_names(delegate=State, |
112 | | - accessors=["fips"], |
113 | | - typ="method") |
114 | | -@wrap_delegate_names(delegate=State, |
115 | | - accessors=["abbrev"], |
116 | | - typ="property") |
117 | | -class StateDelegate(PandasDelegate): |
118 | | - [...] |
119 | | -
|
120 | | -
|
121 | | -We can now pin the `state` accessor to the pd.Series class (we could |
122 | | -alternatively pin it to the pd.Index class with a slightly different |
123 | | -implementation above): |
124 | | -
|
125 | | -pd.Series.state = accessors.AccessorProperty(StateDelegate) |
126 | | -
|
127 | | -
|
128 | | ->>> ser = pd.Series([State('Alabama'), State('California')]) |
129 | | ->>> isinstance(ser.state, StateDelegate) |
130 | | -True |
131 | | -
|
132 | | ->>> ser.state.abbrev |
133 | | -0 AL |
134 | | -1 CA |
135 | | -dtype: object |
136 | | -
|
137 | | ->>> ser.state.fips() |
138 | | -0 1 |
139 | | -1 6 |
140 | | -
|
141 | | ->>> ser.state.abbrev = ['Foo', 'Bar'] |
142 | | ->>> ser.state.abbrev |
143 | | -0 Foo |
144 | | -1 Bar |
145 | | -dtype: object |
146 | | -
|
147 | | -
|
148 | | -
|
149 | | -""" |
150 | | -from pandas.core.base import PandasObject |
151 | | -from pandas.core import common as com |
152 | | - |
153 | | - |
154 | | -class PandasDelegate(PandasObject): |
155 | | - """ an abstract base class for delegating methods/properties |
| 32 | + def _make_accessor(cls, data): # pragma: no cover |
| 33 | + """ |
| 34 | + _make_accessor should implement any necessary validation on the |
| 35 | + data argument to ensure that the properties/methods being |
| 36 | + accessed will be available. |
156 | 37 |
|
157 | | - Usage: To make a custom accessor, start by subclassing `Delegate`. |
158 | | - See example in the module-level docstring. |
| 38 | + _make_accessor should return cls(data). If necessary, the arguments |
| 39 | + to the constructor can be expanded. In this case, __init__ will |
| 40 | + need to be overrided as well. |
159 | 41 |
|
160 | | - """ |
| 42 | + Parameters |
| 43 | + ---------- |
| 44 | + data : the underlying object being accessed, usually Series or Index |
161 | 45 |
|
162 | | - def __init__(self, values): |
163 | | - self.values = values |
164 | | - # #self._freeze() |
| 46 | + Returns |
| 47 | + ------- |
| 48 | + Delegate : instance of PandasDelegate or subclass |
165 | 49 |
|
166 | | - @classmethod |
167 | | - def _make_accessor(cls, data): # pragma: no cover |
| 50 | + """ |
168 | 51 | raise NotImplementedError( |
169 | 52 | 'It is up to subclasses to implement ' |
170 | 53 | '_make_accessor. This does input validation on the object to ' |
171 | 54 | 'which the accessor is being pinned. ' |
172 | 55 | 'It should return an instance of `cls`.') |
| 56 | + # return cls(data) |
173 | 57 |
|
174 | 58 | def _delegate_property_get(self, name, *args, **kwargs): |
175 | 59 | raise TypeError("You cannot access the " |
176 | 60 | "property {name}".format(name=name)) |
177 | 61 |
|
178 | 62 | def _delegate_property_set(self, name, value, *args, **kwargs): |
| 63 | + """ |
| 64 | + Overriding _delegate_property_set is discouraged. It is generally |
| 65 | + better to directly interact with the underlying data than to |
| 66 | + alter it via the accessor. |
| 67 | +
|
| 68 | + An example that ignores this advice can be found in |
| 69 | + tests.test_accessors.TestVectorizedAccessor |
| 70 | + """ |
179 | 71 | raise TypeError("The property {name} cannot be set".format(name=name)) |
180 | 72 |
|
181 | 73 | def _delegate_method(self, name, *args, **kwargs): |
@@ -242,14 +134,8 @@ def create_delegator_method(name, delegate): |
242 | 134 | def func(self, *args, **kwargs): |
243 | 135 | return self._delegate_method(name, *args, **kwargs) |
244 | 136 |
|
245 | | - if callable(name): |
246 | | - # A function/method was passed directly instead of a name |
247 | | - # This may also render the `delegate` arg unnecessary. |
248 | | - func.__name__ = name.__name__ # TODO: is this generally valid? |
249 | | - func.__doc__ = name.__doc__ |
250 | | - else: |
251 | | - func.__name__ = name |
252 | | - func.__doc__ = getattr(delegate, name).__doc__ |
| 137 | + func.__name__ = name |
| 138 | + func.__doc__ = getattr(delegate, name).__doc__ |
253 | 139 | return func |
254 | 140 |
|
255 | 141 | @staticmethod |
@@ -294,13 +180,10 @@ def add_delegate_accessors(cls): |
294 | 180 | else: |
295 | 181 | func = Delegator.create_delegator_method(name, delegate) |
296 | 182 |
|
297 | | - # Allow for a callable to be passed instead of a name. |
298 | | - title = com._get_callable_name(name) |
299 | | - title = title or name |
300 | 183 | # don't overwrite existing methods/properties unless |
301 | 184 | # specifically told to do so |
302 | | - if overwrite or not hasattr(cls, title): |
303 | | - setattr(cls, title, func) |
| 185 | + if overwrite or not hasattr(cls, name): |
| 186 | + setattr(cls, name, func) |
304 | 187 |
|
305 | 188 | return cls |
306 | 189 |
|
|
0 commit comments