2

I am wondering about the most pythonic way to implement a function that can act in different modes, i.e. perform a slightly different task depending on which mode it is called in.

For example, I have the following to extract a subset of a numpy array based on a set of input indices. Typically I will want to pass in these indices as a list or tuple of the form [xMin, xMax, yMin, yMax], however I may sometimes want to provide a centerpoint and a width/height, as ([xCoord, yCoord], [width, height]). (Note that this is spatial data & xMin, yMin etc. refer to the spatial coordinates of the array bounds.)

def get_subset(array, *args):
    ## If window extents are given
    if len(args) == 1:
        [(xMin,xMax,yMin,yMax)] = args

        ## Set NULL extents to array bounds
        if xMin is None: xMin = array.xMin
        if xMax is None: xMax = array.xMax
        if yMin is None: yMin = array.yMin
        if yMax is None: yMax = array.yMax

        ## Convert window extents to array indices
        winLx = int(xMin - array.xMin)
        winRx = int(xMax - array.xMin)
        winBy = int(array.yMax - yMin)
        winTy = int(array.yMax - yMax)

    ## If window centroid and dimensions are given
    elif len(args) == 2:
        [(easting,northing),(width,height)] = args

        # Convert input coordinates into array indices
        xCell = int(easting - array.xMin)
        yCell = int(array.yMax - northing)

        # Generate L(eft), R(ight), T(op) and B(ottom) window extents
        winLx = xCell - width//2
        winRx = xCell + width//2
        winBy = yCell + height//2
        winTy = yCell - height//2

    subset = array.data[winTy:winBy, winLx:winRx, :]

Is there a better, more concise or more pythonic way to do this? In the past I have tried using a mode argument to my function and then using if loops to get the functionality I want (something like get_subset(array, window, mode='extent')), which ends up being not so different from what I have shown above. However I wonder if there is a nice way to use decorators or some other python functionality to accomplish this.

corvus
  • 556
  • 7
  • 18
  • 6
    Why not use two separate functions? – Martijn Pieters Mar 10 '18 at 16:44
  • 5
    The more Pythonic way is to *not have a single function perform two different things*. – Martijn Pieters Mar 10 '18 at 16:45
  • **S**RP - comes along with **O**CP, **L**SP, **I**SP and **D**IP: [SOLID](https://en.wikipedia.org/wiki/SOLID_(object-oriented_design)) – Patrick Artner Mar 10 '18 at 16:52
  • I had begun to wonder if trying to squeeze it into one function was a bad idea. I suppose my question is: is it possible to separate these two functionalities while having them incorporated into a single function that can do what i want? This seems reasonable since conceptually there is no difference in the process, only a difference in how the window is constructed. This is why I wondered if there is a handy way to use decorators, e.g. to handle input from 2 different window-constructor methods & deliver the window to a single subset-extraction method — or maybe this is still bad form? – corvus Mar 10 '18 at 17:52
  • Also, thanks for the link, @PatrickArtner, it is helpful. – corvus Mar 10 '18 at 17:54

1 Answers1

3

Option A) You split up the function into two functions.

get_subset_using_extents(array, extents)
get_subset_using_centroid_and_dimension(array, centroid, dimension)

Benefits:

  • Straight and to the point
  • Seems to be the most popular solution suggested in your question's comments

Option B) The option you proposed.

def get_subset(array, *args):

    if len(args) == 1:
        return getsubset_using_extents(array, args[0])
        #Or just lay out all of the code here like you had done

    elif len(args) == 2:
        return get_subset_using_centroid_and_dimension(array, arg[0], arg[1])
        #Or just lay out all of the code here like you had done

    else:
        raise TypeError("get_subset() takes either 2 or 3 arguments")

Benefits:

  • get_subset is easier to remember than two long named functions.

Cons:

  • You're unable to use keyword arguments.
  • When reading code that is calling this function, it's not obvious which mode is being used.

Option C) Mode-specific items are stored in a dictionary.

def get_subset(array, mode, options):

    if mode == "extent":
        return getsubset_using_extents(array, options["extent"])

    elif mode == "centroid_dimensions":
        return get_subset_using_centroid_and_dimension(array, options["centroid"], 
                   options["dimensions"])

    else:
        raise SubsetException("Invalid Mode: " + mode)

Benefits

  • The function name is easy to remember, and because the caller is required to state the mode used it is still obvious how the subset will be obtained.

  • Allows you to easily add/change/remove options without changing the function signature.

  • Forces people to explicitly name the option keys when they are using your function. This isn't so important in your case, but this technique can be used to prevent function calls that look like this some_function(true, true, false, true). (Edit: Just found out that you can also do this.)


Thoughts

Option A

Because there are two distinct modes that do not have any overlapping options to them, this is the option I would go with.

Option B

I would never use this option. Psuedo-overloading a function like this just isn't a Pythonic thing to do.

Option C

This isn't needed in your case, but if the two modes had lots of options, some of which where shared between various modes, then this would be a good pattern to consider.

hostingutilities.com
  • 8,894
  • 3
  • 41
  • 51