0

The decision by python and apparently numpy to not return an object after its mutation is a source of frequent inconvenience. This question will be based on a different approach: to use a builder pattern so that we can do this:

x = np.random.randint(0,10+1,50).sort()[:5]

and end up with the least five values from a random set of ten numbers. Instead the above results in a runtime error due to subscripting None after the sort. I am wondering if there were either a library that provides that builder pattern atop numpy or some way to coerce all mutation numpy methods to return self instead of None.

For a single mutation the numpy method might suffice:

x = np.sort(np.random.randint(0,10+1,50))[:5]

But that approach does not scale when a series of methods are required. E.g.

x = np.resize(np.sort(np.random.randint(0,10+1,50))[:5],[5,1])

That quickly becomes difficult not only to write but also to read: we are asked to both write and read the code "inside out" from right to left.

Update A "winner" was declared below. Making a slight modification - renaming from ChainWrapper to Wrp just for brevity - here is the usage on both a list and an (numpy) ndarray:

Wrp(list(range(0,10,2))).append(8).append(10).insert(0, "hello").reverse().unwrap()[0:2]
# list:[10, 8]
import numpy as np
Wrp(np.linspace(0, 9, 10)).reshape(5, 2)[1:3, 0])
#  np.array: array([2., 4.])

The point is : append, insert reverse and reshape return None . The Wrp detects that and returns self instead. Frequently I want to write a lambda that performs more than one operation on a list. It was not possible to do so without the above.

WestCoastProjects
  • 58,982
  • 91
  • 316
  • 560

1 Answers1

2

Not returning a mutated object is a Pythonism that reminds you of the fact that no new object was created. Practically all standard library functions that do internal mutation return None for this reason.

You could write a wrapper of your own (and with some getattr magic, make it automatic), but it's probably not quite worth it.

EDIT: If you need this just for chaining, you could do something like

def chain(a, f):
    f(a)
    return a

x = chain(
    np.random.randint(0,10+1,50),
    lambda m: m.sort(),
)[:5]

or even fancier,

def hyperchain(val, *fs):
    for f in fs:
        res = f(val)
        if res is not None:
            val = res
    return val

to let you chain value-returning things and None-returning things:

x = hyperchain(
    np.random.randint(0,10+1,50),
    lambda m: m.sort(),
    lambda m: m[:5],
)

EDIT 2: Here's the aforementioned getattr wrapper idea -- not saying this is a good idea, or perfect, but here we go:

from functools import wraps


class ChainWrapper:
    def __init__(self, target):
        self._target = target

    def __getattr__(self, key):
        attr = getattr(self._target, key)
        if callable(attr):

            @wraps(attr)
            def wrapped_func(*args, **kwargs):
                retval = attr(*args, **kwargs)
                if retval is None:
                    retval = self
                return retval

            return wrapped_func
        return attr

    def __str__(self):
        return self._target.__str__()

    def __repr__(self):
        return f"<chain-wrapped {self._target!r}"

    def unwrap(self):
        return self._target

    # TODO: implement other things such as __getitem__ and __setitem__
    #       to just proxy through


l = [1, 2, 4, 8]

lw = ChainWrapper(l)
print(lw.append(8).append(10).insert(0, "hello").reverse())

This outputs

[10, 8, 8, 4, 2, 1, 'hello']
AKX
  • 152,115
  • 15
  • 115
  • 172
  • I am aware of this being a design decision in `python`. I can not go back in time and debate on that at the time that decision were made: it's something I get to deal with (frequently..) . It leads to not being able to use the mutating methods in expressions either . I would be fine to go and wrap the entire `numpy` : but that will not work either since third party libraries will return unwrapped versions of `ndarrays`. – WestCoastProjects Mar 15 '20 at 13:46
  • Thanks: the `chaining` approach is something I do use: e.g. I have contributed to `pipe` https://julienpalard.github.io/Pipe/ and forked/enhanced the `scalaps` as `infixpy` https://github.com/javadba/infixpy . But explicitly saying `lambda` time and again is unwieldy. It gets tiring for a functional leaning programmer to use this language but I'm trying.. – WestCoastProjects Mar 15 '20 at 14:07
  • You alluded to `__getattr__` : I am actually more interested in _that_ presently. Would you mind to explore that further? – WestCoastProjects Mar 15 '20 at 19:54
  • That will still have the problem of you needing to re-wrap any numpy array you get from another library, so it'd be unwieldy. But see e.g. https://stackoverflow.com/questions/26091833/proxy-object-in-python – AKX Mar 15 '20 at 20:02
  • > unwieldy . Yes agreed the __getattr__ approach is limited in usefulness. I am writing an entire library that extends `ndarray` so at least within _that_ library I can use this approach – WestCoastProjects Mar 15 '20 at 20:05
  • Welp, added that idea too :) – AKX Mar 15 '20 at 20:15
  • wow that's great! I _really_ don't care if this were considered _pythonic_ or not. I am just trying to find ways to _tolerate_ the language while being able to use the v good _numpy_ library and its friends. This is one small step that direction. In addition to the _collections chaining_ libraries mentioned earlier there are some nice enhancements up my sleeve for usability of `ndarray`s – WestCoastProjects Mar 15 '20 at 20:18
  • Glad I could help. As an aside, you may wish to add something to check if `retval` is _not_ `None`, it may need to be re-wrapped in a new ChainWrapper... – AKX Mar 15 '20 at 20:19
  • I am running into issues with the _not_ `None` case: how should the following be fixed to "re-wrap": `return retval` – WestCoastProjects Mar 15 '20 at 21:06
  • Maybe `if type(retval) == type(self._target): return ChainWrapper(retval)` or similar! Or you can always return a `ChainWrapper(retval)`... – AKX Mar 15 '20 at 21:29
  • fyi for `ndarray` this approach covers only a fraction of the bases: in particular methods associated with `views` such as `clip`, `resize`, `reshape` and `slicing are different animals . But v instructive! And it _is_ quite useful for builtin's like `list`, `dict` – WestCoastProjects Mar 15 '20 at 23:00