Iterable

 
# -------------------------------
# ITERABLE
# -------------------------------
# In Python:
#   - Iterable: something you can loop over
#   - Iterator: something that produces values one at a time
# 
# Think of it like this:
#   - An iterable is a book
#   - An iterator is a bookmark that remembers where you are
#
# Iterable:
#   - An iterable is an object that can return its elements one at a time.
#
# Examples:
#   - list
#   - string
#   - tuple
#   - dictionary
#   - se
#
# An iterable is an object that can be passed to iter() to produce an iterator.

A = [1, 2, 3]   # list - iterable
I = iter(A)     # crete an iterator from the iterable

# -----------------------------
# ITERATOR
# ------------------------------
# An iterator remembers its current position 
# and knows how to get the next value.
#
# Each call to next() advances the iterator.

print(next(I))  # 1
print(next(I))  # 2
print(next(I))  # 3

# End of iterator
try: 
    print(next(I))
except StopIteration:
    print("StopIteration")


# ------------------------------
# LOOP MECHANISM
# ------------------------------
# A for-loop:
# 1. Calls iter() 
# 2. Calls next() repeatedly
# 3. Stops on Stop Iterator 

A = [1, 2, 3]

for i in A:
    print(i)

# Equivalent to:
# --------------

I = iter(A)

while True:
    try:
        i = next(I)
        print(i)
    except StopIteration:
        break

Memory Efficiency

 
# ---------------------------------
# MEMORY EFFICIENCY
# ---------------------------------
# An iterator is more efficient than a for loop, 
# because it does not store the collection in memory.

import sys

numbers = list(range(1_000_000))

# Loop
m = sys.getsizeof(numbers)
for n in numbers:
    if n == 10:
        print("Loops: ", n)
        print("Memory:", m) # 8 MB
        break

# Loops:  10
# Memory: 8000056

# Iterator
m = 0
iterator = iter(numbers)
while True:
    n = next(iterator)
    m += sys.getsizeof(n)
    if n == 10:
        print("Iterations: ", n)
        print("Memory:", m) # 300 bytes
        break

# Iterantions:  10
# Memory: 304

Complex Looping

 
# -------------------------------------
# COMPLEX LOOPING
# ------------------------------------- 
# An iterator can be used to implement 
# more complex looping patterns than a for loop.

import json

json_data = """
{
  "name": "John",
  "age": 30,
  "city": "New York",
  "children": [
    {
        "name": "Alice",
        "age": 5
    },
    {
        "name": "Bob",
        "age": 8
    }
  ]
}
"""

def depth_first_traversal(json_obj):
    stack = [json_obj]

    # Loop until there are no iterable elements (such as strings or numbers)
    while stack: 
        
        current = stack.pop()
        yield current

        if isinstance(current, dict):
            stack.extend(current.values())

        elif isinstance(current, list):
            stack.extend(current[::-1]) # new list in reverse order
            
# Parse json
json_obj = json.loads(json_data)

# Display elements
print("Depth-First Traversal:")
for node in depth_first_traversal(json_obj):
    print(node)

"""
Depth-First Traversal:
{'name': 'John', 'age': 30, 'city': 'New York', 'children': [{'name': 'Alice', 'age': 5}, {'name': 'Bob', 'age': 8}]}
[{'name': 'Alice', 'age': 5}, {'name': 'Bob', 'age': 8}]
{'name': 'Alice', 'age': 5}
5
Alice
{'name': 'Bob', 'age': 8}
8
Bob
New York
30
John
"""




References: