Unable to append to array element

Viewed 68

I'm trying to append a string to the end of an array element by using the following function:

def spread(farm):
    #writing potential fruit spread
    for y in range(0,len(farm[0])):
        for x in range(0,len(farm[0])):
            #if this current space is a tree, write the potential spaces accordingly
            if farm[y][x] == "F" or farm[y][x] == "W" or farm[y][x] == "G" or farm[y][x] == "J" or farm[y][x] == "M":
                for b in [-1,0,1]:
                    #making sure the y-coord is within the bounds of the farm
                    if y+b >= 0 and y+b < len(farm[0]):
                        for a in [-1,0,1]:
                            #making sure the x-coord is within the bounds of the farm and the selected space is not a tree
                            if x+a >= 0 and x+a < len(farm[0]) and farm[y+b][x+a] != "F" and farm[y+b][x+a] != "W" and farm[y+b][x+a] != "G" and farm[y+b][x+a] != "J" and farm[y+b][x+a] != "M":
                                #if space is blank, write over the space outright
                                if farm[y+b][x+a] == "_":
                                    farm[y+b][x+a] = farm[y][x].lower()
                                else:
                                    #wherein my troubles lie :(
                                    farm[y+b][x+a] = farm[y+b][x+a] + farm[y][x].lower()
    return farm

with the following input, an array (in farm):

[['_' '_' '_' 'F' '_' '_' '_']
 ['_' '_' '_' 'W' '_' '_' '_']
 ['_' '_' '_' '_' '_' '_' '_']
 ['_' '_' '_' 'J' '_' '_' '_']
 ['_' '_' '_' '_' '_' '_' '_']
 ['_' 'G' '_' '_' '_' 'F' '_']
 ['W' '_' '_' '_' '_' '_' 'G']]

What the function is supposed to do is to simulate spreading fruit trees. Every tree (represented by a capital letter) will spread to the adjacent squares (represented by a lowercase character or underscore). However, the very last line handles the case in which the selected array element is not an underscore. What is supposed to happen is that it will append the string to the end of the array element instead of replacing it, but instead appends nothing. The output is supposed to look something like this:

[['_' '_' 'fw' 'F' 'fw' '_' '_']
 ['_' '_' 'fw' 'W' 'fw' '_' '_']
 ['_' '_' 'wj' 'wj' 'wj' '_' '_']
 ['_' '_' 'j' 'J' 'j' '_' '_']
 ['g' 'g' 'jg' 'j' 'jf' 'f' 'f']
 ['gw' 'G' 'g' '_' 'f' 'F' 'fg']
 ['W' 'gw' 'g' '_' 'f' 'fg' 'G']]

But instead it outputs this:

[['_' '_' 'f' 'F' 'f' '_' '_']
 ['_' '_' 'f' 'W' 'f' '_' '_']
 ['_' '_' 'w' 'w' 'w' '_' '_']
 ['_' '_' 'j' 'J' 'j' '_' '_']
 ['g' 'g' 'j' 'j' 'j' 'f' 'f']
 ['g' 'G' 'g' '_' 'f' 'F' 'f']
 ['W' 'g' 'g' '_' 'f' 'f' 'G']]

What am I doing wrong?

2 Answers

As noted, Numpy has its own string type, which limits the length of the contained text, so that the data can be stored in a neat "rectangular" way without indirection. (This means for example that Numpy can simply do math to calculate where any element will be, rather than chasing pointers for multiple indices.)

It is possible to work around this by explicitly specifying dtype=object when we create the array. This means that Numpy will store pointers to Python objects in its internal representation; this loses a lot of the benefits, but may still allow you to write overall faster and more elegant code depending on the task.

Let's try to implement that here. My first suggestion will be to use empty '' strings for the empty spots on the farm, rather than '_'; this removes a bunch of special cases from our logic (and as we all know, "special cases aren't special enough to break the rules").

Thus, we start with:

farm = np.array([
    ['', '', '', 'F', '', '', ''],
    ['', '', '', 'W', '', '', ''],
    ['', '', '', '', '', '', ''],
    ['', '', '', 'J', '', '', ''],
    ['', '', '', '', '', '', ''],
    ['', 'G', '', '', '', 'F', ''],
    ['W', '', '', '', '', '', 'G']
], dtype='object')

The primary way that Numpy helps us here is that it can efficiently:

  • Apply operatiors and functions to each element of the array elementwise.
  • Slice the array in one or more dimensions.

My approach is as follows:

  1. Create a function that tells us what saplings get planted from the local tree.

  2. Use Numpy to create an array of all the saplings that get planted from their corresponding trees, across the farm.

  3. Create a function to plant saplings at a location offset from their source trees, by slicing the sapling array and "adding" (+, but it's string concatenation of course) the new saplings to a corresponding slice of the farm.

  4. Iterate over the directions that the saplings can be planted, to do all the planting.

So, let's go through that....

The first step is pretty straightforward:

# Determine the saplings that will be planted, if any, from a given source plot.
# Handling the case where multiple trees are already present, is left as an exercise.
def sapling_for(plot):
    return plot.lower() if plot in 'FGJMW' else ''

Now we need to apply that to the entire array. Applying operators like + is automatic. (If you have two arrays x and y with the same number of dimensions and the same size in each dimension, you can just add them with x + y and everything is added up elementwise. Notice that x * y is not "matrix multiplication", but element-wise multiplication.) However, for user-defined functions, we need a bit of extra work - we can't just pass our farm to sapling_for (after all, it doesn't have a .lower() method, for just one of many problems). It looks like:

saplings = np.vectorize(sapling_for)(farm)

Okay, not too difficult. Onward to the slicing. This is a bit tricky. We can easily enough get, for example, the north-west slice of the saplings: it is saplings[:-1, :-1] (i.e., everything except the last row and column). Notice we are not doing two separate index operations - this is Deep NumPy Magic (TM), and we need to do things NumPy's way.

My idea here is that we can represent saplings "spreading" to the southeast by taking this northwest slice and adding it to a southeast slice of the farm: farm[1:, 1:] += saplings[:-1, :-1]. We could simply do that eight times for each compass direction of spread. But what about a generalization?

It's a little trickier, since e.g. 1: doesn't mean anything by itself. It does, however, have a built-in Python representation: the native slice type, which we can also use for Numpy indexing (and for indexing built-in sequence types, too!). So I wrote a helper function to create those:

def get_slice(dx):
    return slice(dx, None, None) if dx >= 0 else slice(None, dx, None)

These are similar to range objects: the parameters are the start point, end point and "step". The idea here is that a negative value will give a slice taking that many items off the end, while a positive value will take them off the front.

That lets us write a general function to add a slice of one array to a shifted position (in the opposite corner) of a "base" array:

def add_shifted(base, to_add, dx, dy):
    base[get_slice(dx), get_slice(dy)] += to_add[get_slice(-dx), get_slice(-dy)]

Hopefully the logic is clear enough. Finally, we can iterate over all the (dx, dy) pairs that make sense for spreading the saplings: everywhere within one space, except for (0, 0).

for dx in (-1, 0, 1):
    for dy in (-1, 0, 1):
        if dx != 0 or dy != 0:
            add_shifted(farm, saplings, dx, dy)

And we're done.

A NumPy array with a string dtype will silently truncate any strings you try to store that are too big for the dtype. Use a list of lists.

Related