diff --git a/lib/iris/fileformats/cf.py b/lib/iris/fileformats/cf.py index dd10184d35..a8016b958e 100644 --- a/lib/iris/fileformats/cf.py +++ b/lib/iris/fileformats/cf.py @@ -63,16 +63,23 @@ class CFVariable(object): '''Name of the netCDF variable attribute that identifies this CF-netCDF variable''' def __init__(self, name, data): - if not isinstance(data, netCDF4.Variable): - raise TypeError('%s expects a netCDF4 Variable data instance' % self.__class__.__name__) + # Accessing the list of netCDF attributes is surprisingly slow. + # Since it's used repeatedly, caching the list makes things + # quite a bit faster. + self._nc_attrs = data.ncattrs() + self.cf_name = name '''NetCDF variable name''' + self.cf_data = data '''NetCDF4 Variable data instance''' + self.cf_group = None '''Collection of CF-netCDF variables associated with this variable''' + self.cf_terms_by_root = {} '''CF-netCDF formula terms that his variable participates in''' + self.cf_attrs_reset() @staticmethod @@ -125,9 +132,14 @@ def __ne__(self, other): return self.cf_name != other.cf_name def __getattr__(self, name): - if name in self.cf_data.ncattrs(): + # Accessing netCDF attributes is surprisingly slow. Since + # they're often read repeatedly, caching the values makes things + # quite a bit faster. + if name in self._nc_attrs: self._cf_attrs.add(name) - return getattr(self.cf_data, name) + value = getattr(self.cf_data, name) + setattr(self, name, value) + return value def __getitem__(self, key): return self.cf_data.__getitem__(key) @@ -140,19 +152,23 @@ def __repr__(self): def cf_attrs(self): """Return a list of all attribute name and value pairs of the CF-netCDF variable.""" - return tuple([(attr, self.getncattr(attr)) for attr in sorted(self.ncattrs())]) + return tuple((attr, self.getncattr(attr)) + for attr in sorted(self._nc_attrs)) def cf_attrs_ignored(self): """Return a list of all ignored attribute name and value pairs of the CF-netCDF variable.""" - return tuple([(attr, self.getncattr(attr)) for attr in sorted(set(self.ncattrs()) & _CF_ATTRS_IGNORE)]) + return tuple((attr, self.getncattr(attr)) for attr in + sorted(set(self._nc_attrs) & _CF_ATTRS_IGNORE)) def cf_attrs_used(self): """Return a list of all accessed attribute name and value pairs of the CF-netCDF variable.""" - return tuple([(attr, self.getncattr(attr)) for attr in sorted(self._cf_attrs)]) + return tuple((attr, self.getncattr(attr)) for attr in + sorted(self._cf_attrs)) def cf_attrs_unused(self): """Return a list of all non-accessed attribute name and value pairs of the CF-netCDF variable.""" - return tuple([(attr, self.getncattr(attr)) for attr in sorted(set(self.ncattrs()) - self._cf_attrs)]) + return tuple((attr, self.getncattr(attr)) for attr in + sorted(set(self._nc_attrs) - self._cf_attrs)) def cf_attrs_reset(self): """Reset the history of accessed attribute names of the CF-netCDF variable.""" diff --git a/lib/iris/tests/test_cf.py b/lib/iris/tests/test_cf.py index dfc6adaa75..a4ec765766 100644 --- a/lib/iris/tests/test_cf.py +++ b/lib/iris/tests/test_cf.py @@ -21,10 +21,45 @@ # import iris tests first so that some things can be initialised before importing anything else import iris.tests as tests +import unittest + +import mock + import iris import iris.fileformats.cf as cf +class TestCaching(unittest.TestCase): + def test_cached(self): + # Make sure attribute access to the underlying netCDF4.Variable + # is cached. + name = 'foo' + nc_var = mock.MagicMock() + cf_var = cf.CFAncillaryDataVariable(name, nc_var) + self.assertEqual(nc_var.ncattrs.call_count, 1) + + # Accessing a netCDF attribute should result in no further calls + # to nc_var.ncattrs() and the creation of an attribute on the + # cf_var. + # NB. Can't use hasattr() because that triggers the attribute + # to be created! + self.assertTrue('coordinates' not in cf_var.__dict__) + _ = cf_var.coordinates + self.assertEqual(nc_var.ncattrs.call_count, 1) + self.assertTrue('coordinates' in cf_var.__dict__) + + # Trying again results in no change. + _ = cf_var.coordinates + self.assertEqual(nc_var.ncattrs.call_count, 1) + self.assertTrue('coordinates' in cf_var.__dict__) + + # Trying another attribute results in just a new attribute. + self.assertTrue('standard_name' not in cf_var.__dict__) + _ = cf_var.standard_name + self.assertEqual(nc_var.ncattrs.call_count, 1) + self.assertTrue('standard_name' in cf_var.__dict__) + + @iris.tests.skip_data class TestCFReader(tests.IrisTest): def setUp(self):