Skip to content

Commit 2be4fc7

Browse files
committed
Accessors created with xr.register_*_accessor are cached
1 parent 4654639 commit 2be4fc7

File tree

4 files changed

+81
-13
lines changed

4 files changed

+81
-13
lines changed

doc/internals.rst

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ xarray:
7878

7979
.. literalinclude:: examples/_code/accessor_example.py
8080

81-
This achieves the same result as if the ``Dataset`` class had a property
81+
This achieves the same result as if the ``Dataset`` class had a cached property
8282
defined that returns an instance of your class:
8383

8484
.. python::
@@ -90,9 +90,14 @@ defined that returns an instance of your class:
9090
return GeoAccessor(self)
9191

9292
However, using the register accessor decorators is preferable to simply adding
93-
your own ad-hoc property (i.e., ``Dataset.geo = property(...)``), because it
94-
ensures that your property does not conflict with any other attributes or
95-
methods.
93+
your own ad-hoc property (i.e., ``Dataset.geo = property(...)``), for two
94+
reasons:
95+
96+
1. It ensures that the name of your property does not conflict with any other
97+
attributes or methods.
98+
2. Instances of accessor object will be cached on the xarray object that creates
99+
them. This means you can save state on them (e.g., to cache computed
100+
properties).
96101

97102
Back in an interactive IPython session, we can use these properties:
98103

@@ -110,7 +115,7 @@ Back in an interactive IPython session, we can use these properties:
110115
111116
The intent here is that libraries that extend xarray could add such an accessor
112117
to implement subclass specific functionality rather than using actual subclasses
113-
rather patching in a large number of domain specific methods.
118+
or patching in a large number of domain specific methods.
114119

115120
To help users keep things straight, please `let us know
116121
<https://github.com/pydata/xarray/issues>`_ if you plan to write a new accessor

xarray/core/dataarray.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1376,19 +1376,19 @@ def imag(self):
13761376

13771377
def dot(self, other):
13781378
"""Perform dot product of two DataArrays along their shared dims.
1379-
1379+
13801380
Equivalent to taking taking tensordot over all shared dims.
13811381
13821382
Parameters
13831383
----------
13841384
other : DataArray
13851385
The other array with which the dot product is performed.
1386-
1386+
13871387
Returns
13881388
-------
13891389
result : DataArray
13901390
Array resulting from the dot product over all shared dimensions.
1391-
1391+
13921392
See also
13931393
--------
13941394
np.tensordot(a, b, axes)
@@ -1397,10 +1397,10 @@ def dot(self, other):
13971397
--------
13981398
13991399
>>> da_vals = np.arange(6 * 5 * 4).reshape((6, 5, 4))
1400-
>>> da = DataArray(da_vals, dims=['x', 'y', 'z'])
1400+
>>> da = DataArray(da_vals, dims=['x', 'y', 'z'])
14011401
>>> dm_vals = np.arange(4)
14021402
>>> dm = DataArray(dm_vals, dims=['z'])
1403-
1403+
14041404
>>> dm.dims
14051405
('z')
14061406
>>> da.dims

xarray/core/extensions.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,33 @@ class AccessorRegistrationError(Exception):
66
"""Exception for conflicts in accessor registration."""
77

88

9+
class _CachedAccessor(object):
10+
"""Custom property-like object (descriptor) for caching accessors."""
11+
def __init__(self, name, accessor):
12+
self._name = name
13+
self._accessor = accessor
14+
15+
def __get__(self, obj, cls):
16+
if obj is None:
17+
# we're accessing the attribute of the class, i.e., Dataset.geo
18+
return self._accessor
19+
accessor_obj = self._accessor(obj)
20+
# Replace the property with the accessor object. Inspired by:
21+
# http://www.pydanny.com/cached-property.html
22+
# We need to use object.__setattr__ because we overwrite __setattr__ on
23+
# AttrAccessMixin.
24+
object.__setattr__(obj, self._name, accessor_obj)
25+
return accessor_obj
26+
27+
928
def _register_accessor(name, cls):
1029
def decorator(accessor):
1130
if hasattr(cls, name):
1231
raise AccessorRegistrationError(
1332
'cannot register accessor %r under name %r for type %r '
1433
'because an attribute with that name already exists.'
1534
% (accessor, name, cls))
16-
17-
setattr(cls, name, property(accessor))
35+
setattr(cls, name, _CachedAccessor(name, accessor))
1836
return accessor
1937
return decorator
2038

xarray/test/test_extensions.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,28 @@
1+
try:
2+
import cPickle as pickle
3+
except ImportError:
4+
import pickle
5+
16
import xarray as xr
27

38
from . import TestCase
49

510

11+
@xr.register_dataset_accessor('example_accessor')
12+
@xr.register_dataarray_accessor('example_accessor')
13+
class ExampleAccessor(object):
14+
"""For the pickling tests below."""
15+
def __init__(self, xarray_obj):
16+
self.obj = xarray_obj
17+
18+
619
class TestAccessor(TestCase):
720
def test_register(self):
821

922
@xr.register_dataset_accessor('demo')
1023
@xr.register_dataarray_accessor('demo')
1124
class DemoAccessor(object):
25+
"""Demo accessor."""
1226
def __init__(self, xarray_obj):
1327
self._obj = xarray_obj
1428

@@ -22,10 +36,41 @@ def foo(self):
2236
da = xr.DataArray(0)
2337
assert da.demo.foo == 'bar'
2438

39+
# accessor is cached
40+
assert ds.demo is ds.demo
41+
42+
# check descriptor
43+
assert ds.demo.__doc__ == "Demo accessor."
44+
assert xr.Dataset.demo.__doc__ == "Demo accessor."
45+
assert isinstance(ds.demo, DemoAccessor)
46+
assert xr.Dataset.demo is DemoAccessor
47+
48+
# ensure we can remove it
2549
del xr.Dataset.demo
26-
assert not hasattr(ds, 'demo')
50+
assert not hasattr(xr.Dataset, 'demo')
2751

2852
with self.assertRaises(xr.core.extensions.AccessorRegistrationError):
2953
@xr.register_dataarray_accessor('demo')
3054
class Foo(object):
3155
pass
56+
57+
# it didn't get registered again
58+
assert not hasattr(xr.Dataset, 'demo')
59+
60+
def test_pickle_dataset(self):
61+
ds = xr.Dataset()
62+
ds_restored = pickle.loads(pickle.dumps(ds))
63+
assert ds.identical(ds_restored)
64+
65+
# state save on the accessor is restored
66+
assert ds.example_accessor is ds.example_accessor
67+
ds.example_accessor.value = 'foo'
68+
ds_restored = pickle.loads(pickle.dumps(ds))
69+
assert ds.identical(ds_restored)
70+
assert ds_restored.example_accessor.value == 'foo'
71+
72+
def test_pickle_dataarray(self):
73+
array = xr.Dataset()
74+
assert array.example_accessor is array.example_accessor
75+
array_restored = pickle.loads(pickle.dumps(array))
76+
assert array.identical(array_restored)

0 commit comments

Comments
 (0)