2

I am trying to convert a for loop into an iterator using yield, but I have failed in my attempts. I don't understand exactly why the yield isn't giving me the expected output. Does anyone know what the problem is?

Attempt at using yield:

def iteration_order(dimensions):
    for dim in range(dimensions):
        order = [0, dim, 0]
        yield order
        for j in range(6):
            sgn = 1 if j % 2 == 0 else -1
            idx = j % 3
            for _ in range(dim if j < 5 else dim-1):
                order[idx] += sgn
                yield order
print(list(iteration_order(2))
>>> [[0, 0, 0], [0, 1, 1], [0, 1, 1], [0, 1, 1], [0, 1, 1], [0, 1, 1], [0, 1, 1]]

The code as it should work (when not using yield):

def iteration_order(dimensions):
    full_order = []
    for dim in range(dimensions):
        order = [[0, dim, 0]]
        for j in range(6):
            sgn = 1 if j % 2 == 0 else -1
            idx = j % 3
            for _ in range(dim if j < 5 else dim-1):
                nxt = list(order[-1])
                nxt[idx] += sgn
                order.append(nxt)

        full_order.extend(order)
    return full_order
print(iteration_order(2))
>>> [[0, 0, 0], [0, 1, 0], [1, 1, 0], [1, 0, 0], [1, 0, 1], [0, 0, 1], [0, 1, 1]]
2
  • What is the logic for the expected output? Commented Sep 5, 2023 at 15:01
  • It's used to iterate through a hexagonal coordinate system - so for example with hex_array=np.zeros((dims*2,dims*2)), i can write i=0; for order in iteration_order(dims): hex_array[dims+ordr[0]-ordr[2], dims+ordr[1]-ordr[0]] = i; i += 1 Commented Sep 5, 2023 at 15:03

5 Answers 5

2

The problem you see is because you are using that same list for everything. You might yield it with different value, but the generator still has reference to that list and it modifies it, giving you weird output. If you add .copy() for each yield, they will be unique lists and will behave as expected:

def iteration_order(dimensions):
    for dim in range(dimensions):
        order = [0, dim, 0]
        yield order.copy()
        for j in range(6):
            sgn = 1 if j % 2 == 0 else -1
            idx = j % 3
            for _ in range(dim if j < 5 else dim-1):
                order[idx] += sgn
                yield order.copy()

print(list(iteration_order(2))) # [[0, 0, 0], [0, 1, 0], [1, 1, 0], [1, 0, 0], [1, 0, 1], [0, 0, 1], [0, 1, 1]]
Sign up to request clarification or add additional context in comments.

Comments

2

You'll need to make copies of the list as you go, e.g.,

def iteration_order(dimensions):
    for dim in range(dimensions):
        order = [0, dim, 0]
        yield order
        corder = list(order)  # copy original list
        for j in range(6):
            sgn = 1 if j % 2 == 0 else -1
            idx = j % 3
            corder = list(corder)  # copy updated list
            for _ in range(dim if j < 5 else dim-1):
                corder[idx] += sgn
                yield corder

This gives:

print(list(iteration_order(2)))
[[0, 0, 0], [0, 1, 0], [1, 1, 0], [1, 0, 0], [1, 0, 1], [0, 0, 1], [0, 1, 1]]

as expected.

Comments

1

IIUC, you can do:

def iteration_order(dimensions):
    for dim in range(dimensions):
        order = [[0, dim, 0]]
        for j in range(6):
            sgn = 1 if j % 2 == 0 else -1
            idx = j % 3
            for _ in range(dim if j < 5 else dim - 1):
                nxt = list(order[-1])
                nxt[idx] += sgn
                order.append(nxt)
        yield from order  # <-- yield from


print(list(iteration_order(2)))

Prints:

[[0, 0, 0], [0, 1, 0], [1, 1, 0], [1, 0, 0], [1, 0, 1], [0, 0, 1], [0, 1, 1]]

2 Comments

Wouldn't that defeat purpose of generator by calculating everything immediately?
I guess this partially solves the problem, as only the current dimension values are calculated and then yielded, rather than having everything calculated initially - but its not a perfect solution
1

The issue I believe is down to the fact that you yield something that is not "done" yet and that thing can be modified.

check out:

def iteration_order(dimensions):
    for dim in range(dimensions):
        order = [0, dim, 0]
        yield order
        for j in range(6):
            sgn = 1 if j % 2 == 0 else -1
            idx = j % 3
            for _ in range(dim if j < 5 else dim-1):
                order[idx] += sgn
                yield order

bar = []
for x in iteration_order(2):
    print(x)
    bar.append(x)

print(bar)

resulting in...

[0, 0, 0]
[0, 1, 0]
[1, 1, 0]
[1, 0, 0]
[1, 0, 1]
[0, 0, 1]
[0, 1, 1]
[[0, 0, 0], [0, 1, 1], [0, 1, 1], [0, 1, 1], [0, 1, 1], [0, 1, 1], [0, 1, 1]]

Would you expect the results to be essentially the same? However they are not since the inner most for loop is not "done" with order after having yielded it. The simplest fix is to yield a copy.

def iteration_order(dimensions):
    for dim in range(dimensions):
        order = [0, dim, 0]
        yield order.copy()
        for j in range(6):
            sgn = 1 if j % 2 == 0 else -1
            idx = j % 3
            for _ in range(dim if j < 5 else dim-1):
                order[idx] += sgn
                yield order.copy()

Giving you:

[0, 0, 0]
[0, 1, 0]
[1, 1, 0]
[1, 0, 0]
[1, 0, 1]
[0, 0, 1]
[0, 1, 1]
[[0, 0, 0], [0, 1, 0], [1, 1, 0], [1, 0, 0], [1, 0, 1], [0, 0, 1], [0, 1, 1]]

1 Comment

Of course, didnt think about the modification of the list when yielding. I even had a fix for that in the original function. Thanks
1

As others have indicated you need to avoid mutating the list that you have already yielded; so yield copies.

Unrelated, but it is arguably more elegant if you avoid the if...else expression in the inner loop's range, and always make dim iterations. Move the exceptional case to the beginning -- yielding [0,0,0] except when dimensions is zero:

def iteration_order(dimensions):
    order = [0, 0, 0]
    if dimensions:
        yield order[:]  # Now this is the exceptional case
    for dim in range(1, dimensions):
        order[1] = dim
        for j in range(6):
            sgn = -(j % 2) or 1
            idx = j % 3
            for _ in range(dim):
                yield order[:]
                order[idx] += sgn

1 Comment

Very elegant! Moving the first yield to the beginning does indeed simplify things a lot. The sign evaluation is also brilliant- thanks a lot

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.