acccessor extending approach limits functional programming approach, make direct monkey-patching also possible · Issue #1080 · pydata/xarray (original) (raw)

Hi, thatnks for creating and continuing development of xarray. I'm in the process of converting my own functions and classes to it which did something very similar (label indexing, plotting, etc.) but was inferior in many ways.

Right now I'm designing a set of functions for digital signal processing (I need them the most, though inteprolation is also important), mostly lowpass/highpass filters and spectrograms based on scipy.signal. Initially I started writing a dsp accessor with such methods, but later I realized, that this accessor approach makes it quite hard to do something like dataset.apply(lowpass, 5). Instead, one has to do something like dataset.apply(lambda d: d.dsp.lowpass(0.5)) which is less convenient than the clear functional programming apply approach.

I agree that making sure that adding a method to the class does not overwrite something else is a good idea, but that can be done for single methods as well. It would be even possible to save replaced method somewhere and replace them later if requested. The great advantage is that the added methods can still be first-class functions as well.

Such methods cannot save state as easily as accessor methods, but in many cases that is not necessary.

I actually implemented something similar for my DataArray-like class (before xarray existed, now I'm trying to convert to xarray) with such plugin handling (below with slight modifications for DataArray). Let me know what you think.

'''Module for handling various DataArray method plugins''' from xarray import DataArray from types import FunctionType

map: name of patched method -> stack of previous methods

_REPLACED_METHODS = {}

def patch_dataarray(method_func): '''Sets method_func as a method of the DataArray class

The method name is inferred from method_func.__name__

Can be used as decorator for functions that should be added to the
DataArray class as methods, for example::

    @patch_dataarray
    def square(self, arg):
        return self**2
    
The decorated function then becomes a method of the class, so
these two are equivalent::

    foo(sig) == sig.foo()

'''
method_name = method_func.__name__
method_stack = _REPLACED_METHODS.setdefault(method_name, [])
method_stack.append(getattr(DataArray, method_name, None))
setattr(DataArray, method_name, method_func)
return method_func

def restore_method(method_func): '''Restore a previous version of a method of the DataArray class''' method_name = method_func.name try: method_stack = _REPLACED_METHODS[method_name] except KeyError: return # no previous method to restore previous_method = method_stack.pop(-1) if previous_method is None: delattr(DataArray, method_name) else: setattr(DataArray, method_name, previous_method)

def unload_module_patches(module): '''Restore previous versions of methods found in the given module''' for name in dir(module): obj = getattr(module, name) if isinstance(obj, FunctionType): restore_method(obj)

def patch_dataarray_wraps(func, func_name=None): '''Return a decorator that patches DataArray with the decorated function

and copies the name of the func and adds a line to the docstring
about wrapping the function
'''
if func_name is None:
    func_name = func.__name__
def updater(new_func):
    '''copy the function name and add a docline'''
    new_func.__name__ = func_name
    new_func.__doc__ = (('Wrapper around function %s\n\n' % func_name)
                        + new_func.__doc__)
    return patch_dataarray(new_func)
return updater