Alternating Directions in Python List - More Pythonic Solution

1.3k Views Asked by At

I feel this should be simple but I'm stuck on finding a neat solution. The code I have provided works, and gives the output I expect, but I don't feel it is Pythonic and it's getting on my nerves.

I have produced three sets of coordinates, X, Y & Z using 'griddata' from a base data set. The coordinates are evenly spaced over an unknown total area / shape (not necessarily square / rectangle) producing the NaN results which I want to ignore of the boundaries of each list. The list should be traversed from the 'bottom left' (in a coordinate system), across the x axis, up one space in the y direction then right to left before continuing. There could be an odd or even number of rows.

The operation to be performed on each point is the same no matter the direction, and it is guaranteed that the every point which exists in X a point exists in Y and Z as can be seen in the code below.

Arrays (lists?) are of the format DataPoint[rows][columns].

k = 0
for i in range(len(x)):
    if k % 2 == 0:  # cut left to right, then right to left
        for j in range(len(x[i])):
            if not numpy.isnan(x[i][j]):
                file.write(f'X{x[i][j]} Y{y[i][j]} Z{z[i][j]}')
    else:
        for j in reversed(range(len(x[i]))):
            if not numpy.isnan(x[i][j]):
                file.write(f'X{x[i][j]} Y{y[i][j]} Z{z[i][j]}')
    k += 1

One solution I could think of would be to reverse every other row in each of the lists before running the loop. It would save me a few lines, but probably wouldn't make sense from a performance standpoint - anyone have any better suggestions?

Expected route through list:

End════<══════╗
╔══════>══════╝
╚══════<══════╗
Start══>══════╝
3

There are 3 best solutions below

0
CristiFati On BEST ANSWER

Here's a variant:

for i, (x_row, y_row, z_row) in enumerate(zip(x, y, z)):
    if i % 2:
        z_row = reversed(x_row)
        y_row = reversed(y_row)
        z_row = reversed(z_row)
    row_strs = list()
    for x_elem, y_elem, z_elem in zip(x_row, y_row, z_row):
        if not numpy.isnan(x_elem):
            row_strs.append(f"X{x_elem} Y{y_elem} Z{z_elem}")
    file.write("".join(row_strs))

Considerations:

There is no recipe for an optimization that will always perform better than any other. It also depends on the data that the code handles. Here's a list of things that I could think of, without knowing how the data looks like:

  • for index range(len(sequence)): is not a Pythonic way of iterating. Here, the foreach idiom is used. If the index is required, [Python 3.Docs]: Built-in Functions - enumerate(iterable, start=0) could be used
  • This no longer applies because of the previous bullet, but reversed(range(n)) is same as range(n - 1, -1, -1). Don't know whether the latter is faster, but it looks like it would be
  • Iterate over multiple iterables at once, using [Python 3.Docs]: Built-in Functions - zip(*iterables)
  • Don't need k, already have i
  • In general when working with files, it's better to read / write fewer times bigger chunks of data than many times smaller chunks of data (files generally reside on disk and disk operations are slow). However, buffering occurs by default (at Python, OS levels), so this is no longer an issue, but still. But again as always, it's a trade-off between resources (time, memory, ...).
    I chose to write to file once per line (rather than once per element - as it was originally). Of course, there's the 3rd possibility of writing everything at once, but I imagined that for larger data sets, it won't be the best solution
  • Probably, some optimizations could also happen at NumPy level (as it would handle bulk data much faster than Python code (iterating) does), but I'm not an expert in that area, nor do I know how the data looks like
0
warped On

I agree with @Prune, your code looks readable and does what it should do. You could compress it a bit by precomputing the indices, like so (note that this start from the top left):

import numpy as np

# generate some sample data
x = np.arange(100).reshape(10,10)

#precompute both directions
fancyranges = (
    list(range(len(x[0,:]))),
    reversed(list(range(len(x[0,:]))))
)

for a in range(x.shape[0]):
    # call appropriate directions
    for b in fancyranges[a%2]:
        # do things
        print(x[a,b])
0
Ronin On

you can move repeatable code to sub_func for further changes in one place

def func():

    def sub_func():
        # repeatable code
        if not numpy.isnan(x[i][j]):
            print(f'X{x[i][j]}...')

    k = 0
    for i in range(len(x)):
        if k % 2 == 0:  # cut left to right, then right to left
            for j in range(len(x[i])):
                sub_func()
        else:
            for j in reversed(range(len(x[i]))):
                sub_func()
        k += 1


func()