იტერატორები და კონტეინერები Python-ში¶
შესავალი¶
იტერირებადი ობიექტი (iterable) – ობიექტი, რომელზეც შეგვიძლია იტერაცია for ციკლით (მაგ.: list, tuple, dict, set, str).
იტერატორი (iterator) – ობიექტი, რომელიც ინახავს მიმდინარე პოზიციას და აბრუნებს მომდევნო ელემენტს __next__() მეთოდით, სანამ წარმოიშვება StopIteration.
- Iterable → უზრუნველყოფს
__iter__()მეთოდს, რომელიც აბრუნებს იტერატორს - Iterator → უზრუნველყოფს
__iter__()და__next__()მეთოდებს
nums = [10, 20, 30] # iterable (სია)
it = iter(nums) # ვიღებთ iterator-ს
print(next(it)) # 10
print(next(it)) # 20
print(next(it)) # 30
# next(it) გამოიწვევს StopIteration
კონტეინერები (Containers) და სექვენსები (Sequences)¶
კონტეინერი არის ტიპი, რომელიც ინახავს სხვა ობიექტებს. ძირითადი ჯგუფები:
- Sequences (მიმდევრობები): list, tuple, str, range
- Sets (სიმრავლეები): set, frozenset
- Mappings (ასოც. კოლექციები): dict
მახასიათებლები:
- სექვენსებს აქვთ ინდექსაცია და წესრიგი (list, tuple, str).
- set არ ინახავს დუბლიკატებს და ელემენტების წესრიგი განსაზღვრული არაა.
- dict ინახავს გასაღები→მნიშვნელობა წყვილებს (Python 3.7+ ინარჩუნებს ჩასმის წესრიგს).
Iterable vs Iterator პროტოკოლი¶
class CountDown:
def __init__(self, start):
self.current = start
def __iter__(self): # iterable → iterator-სვე აბრუნებს
return self
def __next__(self): # iterator
if self.current <= 0:
raise StopIteration
val = self.current
self.current -= 1
return val
for x in CountDown(3): # 3, 2, 1
print(x)
გენერატორები (Generators)¶
გენერატორი არის ფუნქცია, რომელიც იყენებს yield-ს iterator-ის შესაქმნელად.
მეხსიერებას ხარჯავს „ზარმაცად“ – ელემენტებს აბრუნებს მოთხოვნისამებრ.
def squares(n):
for i in range(n):
yield i * i
g = squares(4) # generator
print(next(g)) # 0
print(list(g)) # [1, 4, 9]
გენერატორის გამოსახულება (Generator Expression)¶
სასარგებლო ჩაშენებული იტერატორები¶
enumerate(iterable, start=0)– ინდექსით იტერაციაzip(a, b, ...)– პარალელური იტერაციაreversed(seq)– შებრუნებული იტერაცია (მხოლოდ სექვენსებზე)map(func, iterable)– ფუნქციის გამოყენება ყოველ ელემენტზეfilter(func, iterable)– ფილტრაცია ბულეანი ფუნქციით
names = ["Ana", "Gio", "Nino"]
for i, n in enumerate(names, start=1):
print(i, n)
a = [1, 2, 3]
b = [10, 20, 30]
print(list(zip(a, b))) # [(1, 10), (2, 20), (3, 30)]
print(list(map(lambda x: x*2, a))) # [2, 4, 6]
print(list(filter(lambda x: x%2==1, a)))# [1, 3]
itertools — ძლიერი იტერაციული ინსტრუმენტები¶
from itertools import count, cycle, repeat, chain, islice, takewhile
# უსასრულო მრიცხველი (ფრთხილად!) + islice-ით შეზღუდვა
print(list(islice(count(start=5, step=2), 5))) # [5, 7, 9, 11, 13]
# გამეორება
print(list(islice(repeat("Hi"), 3))) # ['Hi', 'Hi', 'Hi']
# ციკლური გაშვება
print(list(islice(cycle("AB"), 5))) # ['A','B','A','B','A']
# ჯაჭვი
print(list(chain([1,2], [3,4]))) # [1, 2, 3, 4]
# პირობით შეწყვეტამდე
print(list(takewhile(lambda x: x<10, count(0,3)))) # [0,3,6,9]
მეხსიერება: სია vs იტერატორი¶
- სია ინახავს ყველა ელემენტს მეხსიერებაში.
- იტერატორი/გენერატორი გამოითვლის ელემენტს „მოყვანისას“.
დიდ მონაცემებზე/სტრიმებზე ჯობს გენერატორები.
# სიის შემქმნელი comprehension
squares_list = [x*x for x in range(10_000)] # მეტ მეხსიერებას ხარჯავს
# გენერატორის გამოსახულება
squares_gen = (x*x for x in range(10_000)) # მინიმალური მეხსიერება
[1] პრაქტიკული დავალება #4¶
ამოცანა:
დაწერე მოდულარული პროგრამა, რომელიც:
1) ქმნის იტერატორს EvenRange(start, stop) — აბრუნებს მხოლოდ ლუწ რიცხვებს [start, stop) დიაპაზონში.
2) იყენებს გენერატორს chunked(iterable, size) — აბრუნებს ელემენტებს ჯგუფებად (ლისტებად) მოცემული ზომით.
3) main ბლოკში zip + enumerate-ით აჩვენებს ჯგუფებს ინდექსებით.
4) ანალიზისთვის აჩვენე list vs გენერატორის მეხსიერებითი განსხვავება.
კოდი:
class EvenRange:
def __init__(self, start, stop):
self.current = start if start % 2 == 0 else start + 1
self.stop = stop
def __iter__(self):
return self
def __next__(self):
if self.current >= self.stop:
raise StopIteration
val = self.current
self.current += 2
return val
def chunked(iterable, size):
buf = []
for item in iterable:
buf.append(item)
if len(buf) == size:
yield buf
buf = []
if buf:
yield buf
def main():
er = EvenRange(3, 15) # 4,6,8,10,12,14
groups = chunked(er, 3) # [[4,6,8], [10,12,14]]
for idx, grp in enumerate(groups, start=1):
print(f"ჯგუფი {idx}:", grp)
# მეხსიერების შედარების იდეა (სქემატურად)
squares_list = [x*x for x in range(1_000_00)] # დიდი სია
squares_gen = (x*x for x in range(1_000_00)) # გენერატორი
print("სიის ზომა (ელემენტების რაოდენობა):", len(squares_list))
# გენერატორს ზომა არ აქვს წინასწარ – ელემენტები „მოაქვს“ საჭიროებისას
# print(len(squares_gen)) # არიმისაჭირო/შეუძლებელია პირდაპირ
if __name__ == "__main__":
main()
რჩევები:
- საჭიროებისამებრ itertools.islice-ით შეგიძლიათ „დაჭრათ“ უსასრულო/დიდი იტერატორები.
- zip_longest (itertools) გამოგადგებათ არათანაბარი სიგრძის კოლექციების პარალელური იტერაციისას.
განხილვა–ანალიზი¶
EvenRangeგვაჩვენებს iterator პროტოკოლის მინიმალურ იმპლემენტაციას.chunkedგენერატორი მუშაობს სტრიმულირებადი მოცემულობით და არ საჭიროებს წინასწარ მთლიან სიის შექმნას.enumerate+zip/chain-ის კომბინაციები საშუალებას იძლევა ეფექტურად დავამუშავოთ სტრუქტურირებული სტრიმები.- დიდი მოცულობის მონაცემებზე გენერატორები მეხსიერების მხრივ ბევრად ეფექტიანია, თუმცა თუ ელემენტებზე მრავალჯერადი წვდომა გვჭირდება, შესაძლოა სია უკეთესი იყოს.
დასკვნა: იტერატორები და გენერატორები Python-ში არის „ზარმაცი გამოთვლების“ საყრდენი — საშუალებას გაძლევთ დაწეროთ სწრაფი, მეხსიერებით ეკონომიური და მკაფიო კოდი.