UV: The Fast Rust-based Python Package Manager
A practical guide to uv, the fast Rust-based Python package manager
Python’s iteration system is one of its most powerful features, allowing for elegant and memory-efficient data processing. This article explores the core concepts of iterables, iterators, and generators - three closely related components that make Python’s iteration protocol both flexible and powerful.
In Python, basic data types inherit from the iterable class and can produce iterators through the iter(iterable) function. These iterators maintain independent states and return appropriate values via next(iterator) during iteration.
The relationship between iterables and iterators can be thought of as a vending machine (iterable) and the drinks it dispenses (iterator). Once an iterator is consumed, it’s discarded. However, separating iterables and iterators often requires more code than necessary for many use cases.
This is where generators come in - they combine the functionality of iterables and iterators in a clean, efficient way. Generators are particularly advantageous when handling large datasets as they create values dynamically rather than storing them all in memory.
Python’s collections module defines the foundation for these iteration components:
collections.abc module provides Container, Collection, and Sequence as framework classesAn iterable is any object that can be iterated over, including basic data types, files, and strings:
assert issubclass(str, iterable)
assert issubclass(io.TextIOWrapper, iterable)
Simply put, an iterable is anything that can be used in a for loop. The range() function also returns an iterable.
To qualify as an iterable, an object must:
__iter__ abstract methodprint(iterable._abstractmethods_)
>>> frozenset({'__iter__'})
An iterator is an object that maintains state and returns elements one by one until exhaustion. Each iterator maintains its own independent state:
assert iter(i) != iter(i)
To qualify as an iterator, an object must:
__iter__ that returns itself__next__ to define the next value when called with next(iterator)print(iterator._abstractmethods_)
>>> frozenset({'__next__'})
Here’s a simple example of creating custom iterable and iterator classes:
from random import randint
class RandomIntIterable:
def __init__(self, n):
self.n = n
def __iter__(self):
return RandomIntIterator(self.n)
class RandomIntIterator:
def __init__(self, n):
self.count = 0
self.n = n
def __iter__(self):
return self
def __next__(self):
if self.count < self.n:
self.count += 1
return randint(1, 100)
else:
raise StopIteration
In Python’s class hierarchy:
print(issubclass(iterator, iterable))
>>> True
Generators combine the functionality of iterators and iterables, and are commonly used in practical Python development. They’re stored in the collections.abc module like the other classes.
Generators are defined as functions, not classes. They use yield instead of return and significantly reduce code complexity:
from random import randint
def random_number_generator(n):
count = 0
while count < n:
yield randint(1, 100)
count += 1
Similar to list comprehensions, Python allows for generator comprehensions:
from random import randint
g = (randint(1, 100) for _ in range(5))
These are very concise, but may be limited for complex implementations.
The inheritance relationship between these classes can be summarized as:
iterable > iterator > Generator
When a function takes a single argument expression, the parentheses in generator expressions can be omitted:
print(sum([n**2 for n in range(1, 11)])) # List comprehension
print(sum(n**2 for n in range(1, 11))) # Generator expression (better performance)
Python’s iteration system provides a powerful framework for processing data efficiently. By understanding the differences and relationships between iterables, iterators, and generators, you can write more elegant and memory-efficient Python code. Generators in particular offer a clean solution for many iteration patterns, combining the power of iterables and iterators with minimal syntax.
For most practical applications, generators provide the best balance of functionality and simplicity, especially when working with large datasets where memory efficiency is important.
for 문을 아무 생각 없이 쓰다 보면, 그 뒤에서 파이썬이 어떻게 값을 하나씩 꺼내 주는지는 의외로 모르고 지나치기 쉽죠. 파이썬의 이터레이션(iteration) 시스템은 가장 강력한 기능 중 하나로, 우아하면서도 메모리를 아끼는 데이터 처리를 가능하게 해줍니다. 이 글에서는 그 이터레이션 프로토콜을 떠받치는 세 형제, 즉 이터러블(iterable), 이터레이터(iterator), 제너레이터(generator)의 핵심 개념을 하나씩 짚어 보겠습니다.
파이썬의 기본 자료형은 iterable 클래스를 상속하고 있어서, iter(iterable) 함수로 이터레이터를 뽑아낼 수 있습니다. 이렇게 만들어진 이터레이터는 저마다 독립적인 상태를 유지하면서, 순회하는 동안 next(iterator)가 호출될 때마다 적절한 값을 하나씩 돌려주죠.
이터러블과 이터레이터의 관계는 자판기(이터러블)와 거기서 뽑혀 나오는 음료(이터레이터)에 비유하면 딱 맞습니다. 이터레이터는 한 번 다 마시면(소진되면) 그냥 버려지거든요. 그런데 이터러블과 이터레이터를 굳이 나눠서 다루려면 코드가 필요 이상으로 길어지는 경우가 많습니다.
바로 이 지점에서 제너레이터가 등장합니다. 제너레이터는 이터러블과 이터레이터의 기능을 깔끔하고 효율적으로 하나로 합쳐 주는데요. 특히 대용량 데이터를 다룰 때, 값을 몽땅 메모리에 쌓아 두지 않고 필요할 때마다 즉석에서 만들어 내기 때문에 아주 유리합니다.
파이썬의 collections 모듈이 이 이터레이션 구성 요소들의 밑바탕을 정의합니다.
collections.abc 모듈은 Container, Collection, Sequence를 프레임워크 클래스로 제공한다이터러블은 기본 자료형, 파일, 문자열까지 포함해 순회할 수 있는 모든 객체를 말합니다.
assert issubclass(str, iterable)
assert issubclass(io.TextIOWrapper, iterable)
쉽게 말하면, for 루프에 넣어 돌릴 수 있는 건 전부 이터러블이에요. range() 함수도 이터러블을 돌려주죠.
이터러블이 되려면 객체가 다음 두 가지를 갖춰야 합니다.
__iter__ 추상 메서드를 구현할 것print(iterable._abstractmethods_)
>>> frozenset({'__iter__'})
이터레이터는 상태를 유지하면서, 다 떨어질 때까지 요소를 하나씩 꺼내 주는 객체입니다. 각 이터레이터는 저마다 독립적인 상태를 갖고 있죠.
assert iter(i) != iter(i)
이터레이터가 되려면 객체가 다음을 갖춰야 합니다.
__iter__를 구현할 것next(iterator)로 호출될 때 다음 값을 정하는 __next__를 구현할 것print(iterator._abstractmethods_)
>>> frozenset({'__next__'})
직접 이터러블, 이터레이터 클래스를 만들어 보면 감이 옵니다. 간단한 예시를 볼게요.
from random import randint
class RandomIntIterable:
def __init__(self, n):
self.n = n
def __iter__(self):
return RandomIntIterator(self.n)
class RandomIntIterator:
def __init__(self, n):
self.count = 0
self.n = n
def __iter__(self):
return self
def __next__(self):
if self.count < self.n:
self.count += 1
return randint(1, 100)
else:
raise StopIteration
파이썬의 클래스 계층을 보면 이렇습니다.
print(issubclass(iterator, iterable))
>>> True
제너레이터는 이터레이터와 이터러블의 기능을 하나로 묶어 주는데, 실무 파이썬 개발에서 정말 자주 쓰입니다. 다른 클래스들처럼 collections.abc 모듈에 들어 있죠.
제너레이터는 클래스가 아니라 함수로 정의합니다. return 대신 yield를 쓰는 게 포인트인데, 덕분에 코드가 훨씬 단출해집니다.
from random import randint
def random_number_generator(n):
count = 0
while count < n:
yield randint(1, 100)
count += 1
리스트 컴프리헨션처럼, 파이썬은 제너레이터 컴프리헨션도 지원합니다.
from random import randint
g = (randint(1, 100) for _ in range(5))
무척 간결하긴 한데, 로직이 복잡해지면 이 문법만으로는 한계가 있습니다.
지금까지 본 클래스들의 상속 관계를 한 줄로 요약하면 이렇습니다.
iterable > iterator > Generator
함수가 인자로 표현식 하나만 받을 때는, 제너레이터 표현식의 괄호를 생략해도 됩니다.
print(sum([n**2 for n in range(1, 11)])) # 리스트 컴프리헨션
print(sum(n**2 for n in range(1, 11))) # 제너레이터 표현식 (성능 더 좋음)
파이썬의 이터레이션 시스템은 데이터를 효율적으로 다루는 강력한 틀을 제공합니다. 이터러블, 이터레이터, 제너레이터의 차이와 관계를 이해하고 나면 훨씬 우아하고 메모리도 아끼는 코드를 짤 수 있죠. 그중에서도 제너레이터는 이터러블과 이터레이터의 힘을 최소한의 문법으로 합쳐, 웬만한 이터레이션 패턴을 깔끔하게 풀어 줍니다.
대부분의 실무 상황, 특히 메모리 효율이 중요한 대용량 데이터를 다룰 때라면, 제너레이터가 기능과 단순함 사이에서 가장 좋은 균형점을 잡아 줄 거예요.