Məzmuna keç
Educora
İrəli22 dəq24 / 42

Performans, Big-O və collections modulu

Sürətli kod yaz: Big-O yazılışı, siyahı, lüğət və çoxluq əməliyyatlarının mürəkkəbliyi, `timeit` və `cProfile` ilə ölçmə, `Counter`, `defaultdict`, `deque`, `namedtuple` və `itertools`.

Özünü yoxla
Bu dərsdə öyrənəcəksən
  • Big-O yazılışını oxumaq və siyahı, lüğət, çoxluq əməliyyatlarının mürəkkəbliyini bilmək
  • timeit və cProfile ilə kodun sürətini ölçmək
  • Counter, defaultdict, deque, namedtuple və itertools-dan yerində istifadə etmək

Proqram 100 000 sifarişi 100 000 bloklanmış müştərinin siyahısı ilə yoxlayır. Siyahı ilə bu, onlarla saniyə çəkir; bir sətri dəyişib çoxluq işlətsən, iş saniyənin kiçik hissəsində bitir. Python-da sürət nadir hallarda ağıllı fəndlərdən gəlir — o, düzgün verilənlər strukturu və alqoritm seçimindən, həmçinin təxmin etmək əvəzinə ölçməkdən gəlir. Bu dərs hər ikisi üçün alətlər verir.

Big-O: vaxt necə artır

Big-O yazılışı əməliyyatın icra müddətinin verilənlərin ölçüsü n artdıqca necə böyüdüyünü sabit vuruqlara fikir vermədən təsvir edir. O(1) — sabit: n-dən asılı deyil. O(log n) — çox yavaş artır (ikili axtarış). O(n) — n ilə mütənasibdir (siyahını bir dəfə gəzmək). O(n log n) — yaxşı çeşidləmə. O(n²) — eyni verilənlər üzərində iç-içə dövrlər: verilənlər 10 dəfə artanda iş 100 dəfə artır.

Əməliyyatlistdict / set
x in cO(n)O(1)
indeks / açar üzrə müraciətO(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)
* amortizasiya olunmuş, yəni orta hesabla; dict və set üçün göstərilənlər orta haldır.

Lüğət və çoxluq niyə bu qədər sürətlidir? Onlar heş cədvəlləridir: hash(key) elementin harada saxlandığını demək olar ki, birbaşa göstərir, ona görə in verilənləri gəzmir. Siyahının belə göstəricisi yoxdur və elementləri bir-bir müqayisə etməli olur. Siyahının əvvəlinə element əlavə etmək isə qalan bütün elementləri sürüşdürür, buna görə insert(0, x) və pop(0) O(n)-dir.

Təxmin etmə, ölç: timeit və cProfile

timeit kiçik kod parçasını dəfələrlə icra edir və ümumi vaxtı qaytarır — belə ölçmədə təsadüfi kənarlaşmalar hamarlanır. 100 000 ədədlik siyahıda və çoxluqda axtarışı müqayisə edək:

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')
Adətən çoxluq minlərlə dəfə sürətli çıxır; dəqiq rəqəmlər kompüterdən asılıdır.

Bütöv proqram yavaş işləyəndə, nəyisə dəyişməzdən əvvəl vaxtın harada getdiyini öyrən. cProfile hər funksiya çağırışını və ona sərf olunan vaxtı sayır. Onu skript üçün əmr sətrindən işə sal və nəticəni ümumi vaxta görə çeşidlə:

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-ə qədər Fibonaççi ədədlərinin cəmini sadə rekursiya ilə hesablayır. Vaxtlar kompüterdən asılıdır, çağırışların sayı isə yox.

92712/22 cəmi 92 712 çağırış deməkdir, onlardan yalnız 22-si çöldən edilib (qalanları rekursivdir). Profilləyicini koddan da işlətmək olar; burada yalnız çağırışların sayını oxuyuruq — və dekoratorlar dərsindəki lru_cache-in on minlərlə çağırışı necə 21-ə endirdiyini görürük:

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')
▸ Gözlənilən nəticə
fib(20): 21891 calls
fib_cached(20): 21 calls

collections modulu

**Counter** saymaq üçün lüğətdir: ona istənilən iterasiya olunan obyekt ver, o, elementləri sayacaq. most_common(n) ən tez-tez rast gəlinən n elementi qaytarır; olmayan açar KeyError əvəzinə 0 verir; sayğacları toplamaq və çıxmaq da olar:

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']))
▸ Gözlənilən nəticə
[('the', 3), ('and', 2)]
3 0
Counter({'plov': 3, 'dolma': 2, 'kebab': 1})
Counter({'plov': 3, 'kebab': 3, 'dolma': 2})

**defaultdict** olmayan qiyməti ilk müraciətdə ona verdiyin fabrik ilə avtomatik yaradır: list, int, set. Qruplaşdırma üçün idealdır. Diqqətli ol: olmayan açarı hətta oxumaq da onu lüğətə əlavə edir:

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))
▸ Gözlənilən nəticə
{'9A': ['Aysel', 'Leyla', 'Nigar'], '9B': ['Murad', 'Elvin']}
[] 3
by_class['10C'] boş qrup yaratdı — lüğətin uzunluğu 3 oldu.

**deque** (iki tərəfli növbə) hər iki ucdan O(1) müddətində element əlavə edir və silir; siyahıda isə əvvəldən silmək O(n)-dir. maxlen ilə o, yalnız son n elementi saxlayır — «son baxılanlar» siyahıları və sürüşən pəncərələr üçün idealdır. **namedtuple** sahələrinin adları da olan yüngül, dəyişməz kortej yaradır:

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))
▸ Gözlənilən nəticə
Elvin deque(['Aysel', 'Murad', 'Leyla'])
['python', 'quiz', 'profile']
Point(x=3, y=4) 3 4 Point(x=10, y=4)

itertools və Python üslubunda sürət

**itertools** dövrlər üçün C dilində yazılmış sürətli və tənbəl «kərpiclər» toplusudur: ardıcıllıqları birləşdirmək (chain), ardıcıl cəmlər (accumulate), qonşu cütlər (pairwise), kombinasiyalar və dekart hasili, qruplaşdırma:

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])})
▸ Gözlənilən nəticə
[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)
▸ Gözlənilən nəticə
[1, 2, 3, 4, 5]
[1, 3, 5]
Tapşırıq

Mətndə ən çox işlənən 3 sözü söz: say formatında çap et. Böyük-kiçik hərflər və durğu işarələri nəzərə alınmamalıdır. Counter-dən istifadə et.

Tapşırıq · 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'
▸ Gözlənilən nəticə
is: 4
python: 2
fast: 2
Tapşırıq

Siyahıda bir dəfədən çox rast gəlinən ID-ləri artan sıra ilə, sonra isə unikal ID-lərin sayını çap et. İç-içə dövrlər (O(n²)) yox, bir keçid və çoxluqlar (O(n)) işlət.

Tapşırıq · 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')
▸ Gözlənilən nəticə
[105, 230]
5 unique ids

Əsas fikirlər

  • Big-O vaxtın n ilə necə artdığını göstərir; gizli O(n²)-dən, məsələn, dövrün içində siyahı üzərində in-dən qaç.
  • Çoxluq və lüğətdə in orta hesabla O(1), siyahıda O(n)-dir; siyahıda insert(0, x) və pop(0) O(n)-dir — deque işlət.
  • Optimallaşdırmadan əvvəl timeit və cProfile ilə ölç.
  • Counter sayır, defaultdict qruplaşdırır, deque sürətli növbədir, namedtuple kortej sahələrinə ad verir.
  • itertools tənbəl alətlər verir; groupby çeşidlənmiş verilənlər tələb edir; siyahını gəzərkən onu dəyişmə.

Özünü yoxla

10 sual. Hər düzgün cavab XP qazandırır.

1 / 10
n elementli çoxluqda x in s yoxlamasının orta mürəkkəbliyi nədir?