- Big-O yazılışını oxumaq və siyahı, lüğət, çoxluq əməliyyatlarının mürəkkəbliyini bilmək
timeitvəcProfileilə kodun sürətini ölçməkCounter,defaultdict,deque,namedtuplevə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əliyyat | list | dict / set |
|---|---|---|
x in c | O(n) | O(1) |
| indeks / açar üzrə müraciət | O(1) | O(1) |
append, add, d[k] = v | O(1)* | O(1) |
insert(0, x), pop(0) | O(n) | — |
sondan pop() | O(1) | — |
sorted(c) | O(n log n) | O(n log n) |
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:
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')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ə:
$ 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:
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:
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:
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']}
[] 3by_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:
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:
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']}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]
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.
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
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.
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ə
inorta hesabla O(1), siyahıda O(n)-dir; siyahıdainsert(0, x)vəpop(0)O(n)-dir —dequeişlət. - Optimallaşdırmadan əvvəl
timeitvəcProfileilə ölç. Countersayır,defaultdictqruplaşdırır,dequesürətli növbədir,namedtuplekortej sahələrinə ad verir.itertoolstə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.
x in s yoxlamasının orta mürəkkəbliyi nədir?