İçeriğe geç
Educora
İleri22 dk24 / 42

Performans, Büyük O ve collections modülü

Hızlı kod yaz: Büyük O gösterimi, liste, sözlük ve küme işlemlerinin maliyeti, `timeit` ve `cProfile` ile ölçüm, `Counter`, `defaultdict`, `deque`, `namedtuple` ve `itertools`.

Kendini test et
Bu derste öğreneceklerin
  • Büyük O gösterimini okumak ve liste, sözlük, küme işlemlerinin maliyetini bilmek
  • timeit ve cProfile ile kodun hızını ölçmek
  • Counter, defaultdict, deque, namedtuple ve itertools'u yerinde kullanmak

Bir program 100 000 siparişi, engellenmiş 100 000 müşterinin listesiyle karşılaştırıyor. Listeyle bu onlarca saniye sürer; tek bir satırı değiştirip küme kullanırsan iş saniyenin küçük bir kesrinde biter. Python'da hız nadiren zekice hilelerden gelir; doğru veri yapısını ve algoritmayı seçmekten ve tahmin etmek yerine ölçmekten gelir. Bu ders ikisi için de araçlar verir.

Büyük O: süre nasıl artar

Büyük O gösterimi, bir işlemin çalışma süresinin veri boyutu n ile nasıl büyüdüğünü, sabit çarpanları göz ardı ederek tanımlar. O(1) — sabit: n'ye bağlı değildir. O(log n) — çok yavaş büyür (ikili arama). O(n) — n ile orantılıdır (listeyi bir kez dolaşmak). O(n log n) — iyi bir sıralama. O(n²) — aynı veri üzerinde iç içe döngüler: 10 kat fazla veri, 100 kat fazla iş demektir.

İşlemlistdict / set
x in cO(n)O(1)
indeks / anahtarla erişimO(1)O(1)
append, add, d[k] = vO(1)*O(1)
insert(0, x), pop(0)O(n)—
sondan pop()O(1)—
sorted(c)O(n log n)O(n log n)
* amorti edilmiş, yani ortalama olarak; dict ve set için verilenler ortalama durumdur.

Sözlük ve küme neden bu kadar hızlı? Bunlar hash tablolarıdır: hash(key), elemanın nerede saklandığını neredeyse doğrudan gösterir; bu yüzden in veriyi taramaz. Listenin böyle bir göstergesi yoktur ve elemanları tek tek karşılaştırmak zorundadır. Listenin başına eleman eklemek ise diğer tüm elemanları kaydırır; bu nedenle insert(0, x) ve pop(0) O(n)'dir.

Tahmin etme, ölç: timeit ve cProfile

timeit, küçük bir kod parçasını birçok kez çalıştırır ve toplam süreyi döndürür; böylece rastgele dalgalanmalar yumuşar. 100 000 sayılık bir listede ve kümede aramayı karşılaştıralım:

Python
import timeit

setup = 'data_list = list(range(100_000)); data_set = set(data_list)'
t_list = timeit.timeit('99_999 in data_list', setup=setup, number=200)
t_set = timeit.timeit('99_999 in data_set', setup=setup, number=200_000) / 1000
print(f'list: {t_list * 1000:.2f} ms for 200 lookups')
print(f'set:  {t_set * 1000:.4f} ms for 200 lookups')
print(f'the set is about {t_list / t_set:,.0f} times faster')
Genellikle küme binlerce kat hızlı çıkar; kesin sayılar bilgisayara bağlıdır.

Bütün bir program yavaş olduğunda, bir şeyi değiştirmeden önce sürenin nereye gittiğini bul. cProfile her fonksiyon çağrısını ve içinde geçen süreyi sayar. Onu bir betik için komut satırından çalıştır ve raporu toplam süreye göre sırala:

Terminal
$ python -m cProfile -s cumulative slow.py
28656
         92741 function calls (51 primitive calls) in 0.019 seconds

   Ordered by: cumulative time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000    0.019    0.019 {built-in method builtins.exec}
        1    0.000    0.000    0.019    0.019 slow.py:1(<module>)
        1    0.000    0.000    0.019    0.019 slow.py:4(main)
        1    0.000    0.000    0.019    0.019 {built-in method builtins.sum}
       23    0.000    0.000    0.019    0.001 slow.py:5(<genexpr>)
 92712/22    0.019    0.000    0.019    0.001 slow.py:1(fib)
slow.py, 0'dan 21'e kadar Fibonacci sayılarının toplamını saf özyinelemeyle hesaplar. Süreler bilgisayara bağlıdır, çağrı sayıları değildir.

92712/22, toplam 92 712 çağrı demektir; bunların yalnızca 22'si dışarıdan yapılmıştır (geri kalanı özyinelemelidir). Profil aracı koddan da kullanılabilir; burada yalnızca çağrı sayılarını okuyoruz ve dekoratörler dersindeki lru_cache'in on binlerce çağrıyı nasıl 21'e indirdiğini görüyoruz:

Python
import cProfile
import pstats
from functools import lru_cache

def fib(n):
    return n if n < 2 else fib(n - 1) + fib(n - 2)

@lru_cache(maxsize=None)
def fib_cached(n):
    return n if n < 2 else fib_cached(n - 1) + fib_cached(n - 2)

for func in (fib, fib_cached):
    profiler = cProfile.Profile()
    profiler.runcall(func, 20)
    stats = pstats.Stats(profiler).stats
    calls = sum(v[1] for (_, _, name), v in stats.items() if name == func.__name__)
    print(f'{func.__name__}(20): {calls} calls')
▸ Beklenen çıktı
fib(20): 21891 calls
fib_cached(20): 21 calls

collections modülü

**Counter**, saymak için bir sözlüktür: ona herhangi bir yinelenebilir nesne ver, elemanları sayar. most_common(n) en sık geçen n elemanı döndürür; olmayan bir anahtar KeyError yerine 0 verir; sayaçlar toplanıp çıkarılabilir bile:

Python
from collections import Counter

words = Counter('the cat and the hat and the bat'.split())
print(words.most_common(2))
print(words['the'], words['dog'])

votes = Counter(['plov', 'dolma', 'plov', 'kebab', 'plov', 'dolma'])
print(votes)
print(votes + Counter(['kebab', 'kebab']))
▸ Beklenen çıktı
[('the', 3), ('and', 2)]
3 0
Counter({'plov': 3, 'dolma': 2, 'kebab': 1})
Counter({'plov': 3, 'kebab': 3, 'dolma': 2})

**defaultdict**, eksik bir değeri ilk erişimde, verdiğin fabrikayla otomatik olarak oluşturur: list, int, set. Gruplama için idealdir. Dikkat: eksik bir anahtarı okumak bile onu sözlüğe ekler:

Python
from collections import defaultdict

students = [('Aysel', '9A'), ('Murad', '9B'), ('Leyla', '9A'), ('Elvin', '9B'), ('Nigar', '9A')]
by_class = defaultdict(list)
for name, group in students:
    by_class[group].append(name)
print(dict(by_class))
print(by_class['10C'], len(by_class))
▸ Beklenen çıktı
{'9A': ['Aysel', 'Leyla', 'Nigar'], '9B': ['Murad', 'Elvin']}
[] 3
by_class['10C'] boş bir grup oluşturdu; sözlüğün uzunluğu 3 oldu.

**deque** (çift uçlu kuyruk) her iki uçtan O(1) sürede eleman ekler ve siler; listede ise baştan silmek O(n)'dir. maxlen ile yalnızca son n elemanı tutar; “son görüntülenenler” listeleri ve kayan pencereler için idealdir. **namedtuple**, alanlarının adları da olan hafif ve değiştirilemez bir demet oluşturur:

Python
from collections import deque, namedtuple

queue = deque(['Aysel', 'Murad'])
queue.append('Leyla')
queue.appendleft('Elvin')
print(queue.popleft(), queue)

recent = deque(maxlen=3)
for page in ['home', 'lessons', 'python', 'quiz', 'profile']:
    recent.append(page)
print(list(recent))

Point = namedtuple('Point', ['x', 'y'])
p = Point(3, 4)
print(p, p.x, p[1], p._replace(x=10))
▸ Beklenen çıktı
Elvin deque(['Aysel', 'Murad', 'Leyla'])
['python', 'quiz', 'profile']
Point(x=3, y=4) 3 4 Point(x=10, y=4)

itertools ve Python tarzı hız

**itertools**, döngüler için C ile yazılmış hızlı ve tembel yapı taşlarından oluşan bir settir: dizileri birleştirme (chain), birikimli toplamlar (accumulate), komşu çiftler (pairwise), kombinasyonlar ve Kartezyen çarpım, gruplama:

Python
from itertools import accumulate, chain, combinations, groupby, pairwise, product

print(list(chain([1, 2], (3, 4), 'ab')))
print(list(accumulate([5, 3, 8, 2])))
print(list(pairwise([10, 12, 15, 11])))
print(list(combinations('ABC', 2)))
print(len(list(product(range(10), repeat=4))))
fruits = sorted(['cherry', 'apple', 'blueberry', 'avocado', 'banana'])
print({k: list(g) for k, g in groupby(fruits, key=lambda w: w[0])})
▸ Beklenen çıktı
[1, 2, 3, 4, 'a', 'b']
[5, 8, 16, 18]
[(10, 12), (12, 15), (15, 11)]
[('A', 'B'), ('A', 'C'), ('B', 'C')]
10000
{'a': ['apple', 'avocado'], 'b': ['banana', 'blueberry'], 'c': ['cherry']}
Python
numbers = [1, 2, 2, 3, 4, 4, 5]
for n in numbers:
    if n % 2 == 0:
        numbers.remove(n)
print(numbers)

numbers = [1, 2, 2, 3, 4, 4, 5]
numbers = [n for n in numbers if n % 2 != 0]
print(numbers)
▸ Beklenen çıktı
[1, 2, 3, 4, 5]
[1, 3, 5]
Alıştırma

Metinde en sık geçen 3 sözcüğü sözcük: sayı biçiminde yazdır. Büyük-küçük harf ve noktalama işaretleri yok sayılmalıdır. Counter kullan.

Alıştırma · Python
from collections import Counter
import re

text = '''Python is fast to write. Python is fun.
Fast code is good, but correct code is better.'''
# 1) turn the text into a list of lower-case words
# 2) print the 3 most common words as 'word: count'
▸ Beklenen çıktı
is: 4
python: 2
fast: 2
Alıştırma

Birden fazla kez geçen kimlikleri artan sırayla, ardından benzersiz kimliklerin sayısını yazdır. İç içe döngüler (O(n²)) değil, tek geçiş ve kümeler (O(n)) kullan.

Alıştırma · Python
ids = [105, 230, 105, 999, 412, 230, 7, 105]
seen = set()
duplicates = set()
# one pass: if an id is already in seen, it is a duplicate

print(sorted(duplicates))
print(len(seen), 'unique ids')
▸ Beklenen çıktı
[105, 230]
5 unique ids

Önemli noktalar

  • Büyük O, sürenin n ile nasıl büyüdüğünü gösterir; bir döngü içinde liste üzerinde in gibi gizli O(n²)'lerden kaçın.
  • Küme ve sözlükte in ortalama O(1), listede O(n)'dir; listede insert(0, x) ve pop(0) O(n)'dir; deque kullan.
  • İyileştirmeden önce timeit ve cProfile ile ölç.
  • Counter sayar, defaultdict gruplar, deque hızlı bir kuyruktur, namedtuple demet alanlarına ad verir.
  • itertools tembel araçlar sunar; groupby sıralı veri ister; bir listeyi dolaşırken asla değiştirme.

Kendini test et

10 soru. Her doğru cevap XP kazandırır.

1 / 10
n elemanlı bir kümede x in s denetiminin ortalama maliyeti nedir?