0

I noticed a difference between the for loop and the while loop when trying to remove items from a layout. Here is a little code to illustrate:

test.kv

BoxLayout:
    orientation: 'vertical'
    spacing: 20

    BoxLayout:
        orientation: 'vertical'
        spacing: 20
        id : box
        Button:
            text: 'BUTTON1'

        Button:
            text: 'BUTTON2'

        Button:
            text: 'BUTTON3'

    Button:
        text: 'BUTTON'
        size_hint: None, None
        size: dp(100), dp(50)
        pos_hint: {'center_x': 0.5, 'center_y': 0.1}
        on_release: app.del_button()

With a for loop: main.py

from kivy.app import App

class TestApp(App):

    def del_button(self):
        children = self.root.ids.box.children
        for i in children:
            self.root.ids.box.remove_widget(i)

if __name__ == '__main__':
    TestApp().run()

In this first case, when I press the 'BUTTON', the first and third buttons are removed, but the 'BUTTON2' remains visible.

With a while loop: main.py

from kivy.app import App

class TestApp(App):

    def del_button(self):
        children = self.root.ids.box.children
        i = 0
        while i < len(children):
            self.root.ids.box.remove_widget(children[i])

if __name__ == '__main__':
    TestApp().run()

In this second case, all the buttons are removed directly.

With the for loop, not all elements are removed the first time, while the while loop does the job well. Why this strange behavior?

Thank you in advance for your answers.

Tobin
  • 2,029
  • 2
  • 11
  • 19
  • I'm not sure this is quite a duplicate of [this question](https://stackoverflow.com/questions/1207406/), [this one](https://stackoverflow.com/questions/6022764/), or [this one](https://stackoverflow.com/questions/6500888/). The answers do explain the problem, once you realize that you're trying to delete from the same list you're iterating over, but the alternatives they suggest aren't really necessary here. – abarnert Aug 25 '18 at 20:57
  • Also, notice that in your `while` loop, you're never incrementing `i`, so you're just removing `children[0]` over and over. – abarnert Aug 25 '18 at 20:58

2 Answers2

3

The for statement creates an iterator to use in its looping. The iterator is created at the start of the loop (See for loop documentation), so it may not be correct if you have changed the list after the start of the loop.

The while loop, however, does not use an iterator and will not be confused by the changing list structure. In fact, a simpler implementation of your del_button() method could be:

def del_button(self):
    children = self.root.ids.box.children
    while len(children) > 0:
        self.root.ids.box.remove_widget(children[0])
John Anderson
  • 35,991
  • 4
  • 13
  • 36
1

for loop

1st Iteration

Before the for loop, children contains a list of three memory location of button widgets, [Button3, Button2, Button1].

In the first iteration, i is referencing the first item in children and it removed widget, Button3.

2nd Iteration

Before the second iteration, children contains a list of two memory location of button widgets, [Button2, Button1].

In the second iteration, i is referencing the second item in children and it removed widget, Button1.

3rd Iteration

Before the third iteration, children contains a list of two memory location of button widgets, [Button2].

Since there is no third item in children, the for loop exit.

While loop

Since i (index) always remains at 0, therefore, it was successful in removing all the three button widgets because it is always referencing the first item in the list, children. self.root.ids.box.remove_widget(children[i]) is equivalent to self.root.ids.box.remove_widget(children[0])

Solution

The solution for the for loop is use reversed(self.root.ids.box.children).

Snippet

def del_button(self):
    for child in reversed(self.root.ids.box.children):
        self.root.ids.box.remove_widget(child)
Community
  • 1
  • 1
ikolim
  • 15,721
  • 2
  • 19
  • 29