How can a function accept a colon (range operator) as an argument (in Python)?

Question:

I want to do the following: I want to define a recursive function funct in which the last reference of itself return a number of the array temp. The problem is that funct must do an integral of itself (see the code below) and this would be very easy if funct could accept the colon : as an argument. So, I have this (simplified) code so far:

import numpy as np   
import scipy.integrate as ints

temp = np.array([[1,2,3,4,5],[4,0,6,7,8],[7,8,9,10,11],[1,2,3,4,5],[6,7,8,9,10]])

def funct(x,y,n):
    if n>1:
        return (temp[x,y] + ints.cumtrapz(temp[x,:]*funct(:,y,n-1), x=None, dx=1, initial=0)[-1])
    else:
        return temp[x,y]
funct = np.vectorize(funct)
funct(1,1,3)

The problem is that funct can’t accept the colon : as argument, it doesn’t matter if I want to vectorize to funct later in the code.

For instance, If I change this part of the above code

ints.cumtrapz(temp[x,:]*funct(:,y,n-1), x=None, dx=1, initial=0)[-1]) 

for

ints.cumtrapz(temp[x,:]*temp[:,y], x=None, dx=1, initial=0)[-1]) 

I have no problems. I just want to do the last part recursively.

Asked By: Jorge

||

Answers:

First, Python doesn’t have “range operators” like some other languages. The : generates slices, which are a completely different type from ranges. And, more importantly, the : syntax is part of the slicing (aka extended indexing or extended subscription) syntax, it doesn’t stand on its own.

So, the simple way to write your code is to use a slice literal:

Of course you could also avoid all this mess and just use an explicit slice literal:

def funct(x, y, n):
    if n>1:
        return (temp[x,y] + ints.cumtrapz(temp[x,:]*funct(slice(None),y,n-1), x=None, dx=1, initial=0)[-1])
    else:
        return temp[x,y]

So, why isn’t there a syntax for “slice literals” that’s more convenient than calling the slice constructor? Because nobody’s come up with a compelling argument, worked out potential syntax ambiguities, and submitted a patch.*

* Note that Python did add a syntax for ellipsis literals on their own—... is a literal for Ellipsis, the singleton value of type ellipsis. A lot of people wanted that, there were no ambiguities except in code that was already illegal, someone wrote a patch, and it was accepted with little fuss.


While the syntax for extended indexing and the syntax for function calls are somewhat similar, they’re not identical. That means you can’t use function calls as, e.g., a domain-specific language for wrapping up delayed slicing.

One thing you can do is create a slice-wrapper type, to using slicing expressions themselves as such a domain-specific language:

class Slicer:
    def __getitem__(self, idx):
        return idx
s = Slicer()

Now s[:] is a constructor for slice(None), and s[3:23:2, ..., 4] is a constructor for (slice(3, 23, 2), Ellipsis, 4). So you can write things like this:

funct(s[:,y,n-1])

Your funct class will get a tuple of slice objects and integers, which it can later use to index an array by calling its __getitem__ directly.

And you can wrap up more of that if you want. For example:

class SliceCallable(object):
    def __init__(self, f):
        self.f = f
    def __getitem__(self, idx):
        if isinstance(idx, collections.abc.Sequence):
            return self.f(*idx)
        else:
            return self.f(idx)
    def __call__(self, *args):
        return self.f(*args)

@SliceCallable
def funct(x, y, n):
    if n>1:
        return (temp[x,y] + ints.cumtrapz(temp[x,:]*funct[:,y,n-1], x=None, dx=1, initial=0)[-1])
    else:
        return temp[x,y]

Now, funct can be called as either funct(1, 2, 3) or funct[1, 2, 3]—or as funct[:, 2, 3] or funct[4:-1]. That just means that x will be slice(None, None, None) or slice(4, -1, None). And you can use that in an indexing expression; temp[slice(None, None), 3] may not look as nice as temp[:, 3], but it means the same thing.

Answered By: abarnert