Generators و Iterators
Generators و Iterators از قدرتمندترین قابلیتهای پایتون هستند که اجازه میدهند دادههای بزرگ را به شکل lazy (تنبل) پردازش کنیم - بدون بارگذاری همه چیز در حافظه. در این فصل با yield، generator expressions، itertools و الگوهای lazy evaluation آشنا میشویم.
Generators و Iterators از قدرتمندترین قابلیتهای پایتون هستند که اجازه میدهند دادههای بزرگ را به شکل lazy (تنبل) پردازش کنیم – بدون بارگذاری همه چیز در حافظه. در این فصل با yield، generator expressions، itertools و الگوهای lazy evaluation آشنا میشویم.
پروتکل Iterator
هر شیء iterable در پایتون دو متد دارد:
__iter__()– یک iterator برمیگرداند__next__()– مقدار بعدی را برمیگرداند یاStopIterationraise میکند
class CountDown:
def __init__(self, start):
self.current = start
def __iter__(self):
return self
def __next__(self):
if self.current <= 0:
raise StopIteration
self.current -= 1
return self.current + 1
# استفاده
for num in CountDown(3):
print(num)
# 3
# 2
# 1
# دستی
cd = CountDown(2)
print(next(cd)) # 2
print(next(cd)) # 1
print(next(cd)) # StopIteration
Generators – راه سادهتر
بهجای نوشتن کلاس iterator کامل، از yield استفاده کنید:
def countdown(start):
while start > 0:
yield start
start -= 1
# همان رفتار CountDown اما با کد بسیار کمتر
for num in countdown(3):
print(num)
# 3, 2, 1
# Generator یک iterator است
gen = countdown(3)
print(next(gen)) # 3
print(next(gen)) # 2
print(next(gen)) # 1
# print(next(gen)) # StopIteration
return دارد و یکباره خروجی میدهد. تابع generator با yield در همان نقطه pause میشود و در فراخوانی بعدی next() از همان جا ادامه میدهد.
Lazy Evaluation – چرا مهم است؟
مقایسه: خواندن فایل ۱۰ گیگابایتی!
# روش بد - همه فایل در حافظه
def read_all_lines(filename):
with open(filename) as f:
return f.readlines() # همه لاینها در RAM
# مصرف حافظه: 10GB - ممکن است crash کند
lines = read_all_lines("huge.log")
for line in lines:
process(line)
# روش خوب - lazy با generator
def read_lines_lazy(filename):
with open(filename) as f:
for line in f:
yield line # هر لاین بهصورت تنبل
# مصرف حافظه: چند KB - کار میکند
for line in read_lines_lazy("huge.log"):
process(line)
Generator Expressions
مثل list comprehension اما با () بهجای []:
# List comprehension - همه چیز در حافظه
squares_list = [x**2 for x in range(1_000_000)]
# مصرف حافظه: ~30MB
# Generator expression - lazy
squares_gen = (x**2 for x in range(1_000_000))
# مصرف حافظه: ~200 bytes
# هر دو میتوانند تکرار شوند
total = sum(squares_gen) # 333332833333500000
# اما generator یکبار مصرف است!
total2 = sum(squares_gen) # 0 - تمام شده
کاربرد در توابع
# بدون پرانتز اضافی وقتی تنها آرگومان است
total = sum(x**2 for x in range(100))
print(any(x > 50 for x in numbers))
print(max(item.price for item in products))
yield from
برای واگذاری کار به generator دیگر:
def numbers_1_to_3():
yield 1
yield 2
yield 3
def numbers_4_to_6():
yield 4
yield 5
yield 6
def all_numbers():
yield from numbers_1_to_3()
yield from numbers_4_to_6()
print(list(all_numbers())) # [1, 2, 3, 4, 5, 6]
پیادهسازی flatten بازگشتی
def flatten(nested):
"""تبدیل لیست تو در تو به لیست تخت"""
for item in nested:
if isinstance(item, list):
yield from flatten(item) # recursion
else:
yield item
data = [1, [2, 3, [4, 5]], 6, [7, [8, 9]]]
print(list(flatten(data))) # [1, 2, 3, 4, 5, 6, 7, 8, 9]
ماژول itertools
ابزارهای آماده برای کار با iterator:
count, cycle, repeat – بینهایت
from itertools import count, cycle, repeat
# count - شمارش از یک عدد
for i in count(start=10, step=2):
if i > 20:
break
print(i)
# 10, 12, 14, 16, 18, 20
# cycle - تکرار بینهایت
colors = cycle(["قرمز", "سبز", "آبی"])
for _ in range(7):
print(next(colors))
# قرمز, سبز, آبی, قرمز, سبز, آبی, قرمز
# repeat - تکرار یک مقدار
for x in repeat("سلام", times=3):
print(x)
# سلام, سلام, سلام
chain, zip_longest, product
from itertools import chain, zip_longest, product
# chain - چسباندن iterableها
list1 = [1, 2, 3]
list2 = [4, 5, 6]
list3 = [7, 8, 9]
print(list(chain(list1, list2, list3)))
# [1, 2, 3, 4, 5, 6, 7, 8, 9]
# zip_longest - مثل zip اما تا انتهای طولانیترین
names = ["علی", "سارا", "محمد"]
ages = [25, 30]
for n, a in zip_longest(names, ages, fillvalue="نامشخص"):
print(f"{n}: {a}")
# علی: 25
# سارا: 30
# محمد: نامشخص
# product - ضرب دکارتی
sizes = ["S", "M", "L"]
colors = ["قرمز", "آبی"]
for size, color in product(sizes, colors):
print(f"{size}-{color}")
# S-قرمز, S-آبی, M-قرمز, M-آبی, L-قرمز, L-آبی
combinations, permutations
from itertools import combinations, permutations
# combinations - بدون ترتیب، بدون تکرار
items = ["A", "B", "C", "D"]
print(list(combinations(items, 2)))
# [('A','B'), ('A','C'), ('A','D'), ('B','C'), ('B','D'), ('C','D')]
# permutations - با ترتیب
print(list(permutations(items, 2)))
# [('A','B'), ('A','C'), ('A','D'), ('B','A'), ('B','C'), ...]
groupby, takewhile, dropwhile
from itertools import groupby, takewhile, dropwhile
# groupby - گروهبندی متوالی (نیاز به sort قبلی)
data = [
("میوه", "سیب"),
("میوه", "موز"),
("سبزی", "هویج"),
("سبزی", "کاهو"),
]
for category, items in groupby(data, key=lambda x: x[0]):
print(category, [item[1] for item in items])
# میوه ['سیب', 'موز']
# سبزی ['هویج', 'کاهو']
# takewhile - تا وقتی شرط برقرار است
nums = [1, 3, 5, 8, 9, 2]
print(list(takewhile(lambda x: x < 7, nums)))
# [1, 3, 5]
# dropwhile - رد کردن تا وقتی شرط برقرار است
print(list(dropwhile(lambda x: x < 7, nums)))
# [8, 9, 2]
islice – برش از iterator
from itertools import islice, count
# 10 عدد اول از یک generator بینهایت
infinite = count(1)
first_10 = list(islice(infinite, 10))
print(first_10) # [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# مثل slicing لیستها: islice(iter, start, stop, step)
data = range(100)
print(list(islice(data, 10, 20, 2)))
# [10, 12, 14, 16, 18]
ارسال داده به Generator
generator میتواند دوطرفه عمل کند:
def counter():
count = 0
while True:
increment = yield count # دریافت مقدار از send()
if increment is None:
increment = 1
count += increment
c = counter()
print(next(c)) # 0 - راهاندازی
print(next(c)) # 1
print(c.send(10)) # 11
print(c.send(5)) # 16
c.close() # پایان
مثالهای کاربردی
۱. Pagination
def paginate(items, page_size):
"""تقسیم آیتمها به صفحات"""
page = []
for item in items:
page.append(item)
if len(page) == page_size:
yield page
page = []
if page: # آخرین صفحه ناتمام
yield page
# 100 محصول، صفحهبندی 10تایی
products = list(range(1, 101))
for page_num, page in enumerate(paginate(products, 10), 1):
print(f"صفحه {page_num}: {page}")
۲. خواندن فایل بزرگ خط به خط
def read_large_csv(filename):
"""خواندن CSV بهصورت lazy"""
import csv
with open(filename, encoding="utf-8") as f:
reader = csv.DictReader(f)
for row in reader:
yield row
# پردازش فایل 10GB بدون مشکل حافظه
total = 0
for row in read_large_csv("sales.csv"):
total += float(row["amount"])
print(f"مجموع: {total}")
۳. Pipeline داده
def get_lines(filename):
with open(filename, encoding="utf-8") as f:
for line in f:
yield line.strip()
def filter_errors(lines):
for line in lines:
if "ERROR" in line:
yield line
def parse_log(lines):
for line in lines:
parts = line.split("|")
if len(parts) >= 3:
yield {
"time": parts[0],
"level": parts[1],
"message": parts[2]
}
# pipeline تنبل - هر لاین یکبار از کل خط لوله میگذرد
errors = parse_log(filter_errors(get_lines("app.log")))
for error in errors:
print(error["time"], error["message"])
بهترین شیوهها
- برای دادههای بزرگ، همیشه از generator استفاده کنید
- به جای ساخت کلاس iterator، از yield استفاده کنید
- generator یکبار مصرف است – برای استفاده مجدد دوباره بسازید
- از
itertoolsبهجای پیادهسازی دستی استفاده کنید - generator expression وقتی فقط یکبار iterate میکنید کارآمدتر است
جمعبندی
- Iterator با
__iter__و__next__پیاده میشود - Generator با
yieldراه سادهتر ساخت iterator - Lazy evaluation حافظه را بهشدت کاهش میدهد
yield fromبرای واگذاری به generator دیگرitertoolsابزارهای آماده برای کار با iterator- Pipelineهای دادهای با generator بسیار قدرتمند هستند