Data Structures & Oddities#
Структурите от данни са класове и обекти с интерфейс чрез който ги използваме, като се абстрахираме от имплементацията им.
Алгоритмите са генерализирани процедури (парчета код/функции) които решават даден проблем (изпълняват дадена задача).
Oddities са модули на Python, които нямат място в друга тема, но са интересни за решаването на проблеми.
Str#
str е поредица от символи. Ще разгледаме:
Стринговете в Python са immutable -> Повечето операции са \(O(n)\)
# Python speaks UTF-8 :) https://en.wikipedia.org/wiki/UTF-8
s = '🐍 is awesomeͤͤͤͤͤ'
print(s)
🐍 is awesomeͤͤͤͤͤ
if 'самобукви'.isalpha():
print('{букви} = alphabetic')
# Можем ли да конвертираме стринга до int?
if '1232367457'.isdigit():
print('{числа} = digit')
# Бърза проверка дали работим с дума или изречение (изречението има паузи и символи)
if '1283912873andLetters'.isalnum():
print('{числа, букви} = alpha-numeric')
# Трябва ли да пишем код който разбира от главни букви?
if 'малкибукви'.islower():
print('{малки букви} = lowercase')
if 'ГОЛЕМИБУКВИ'.isupper():
print('{големи букви} = uppercase')
# Използваме при разбиване на стрингове - if space : do X else do Y
if '\t\n '.isspace():
print('{празни символи} = space')
{букви} = alphabetic
{числа} = digit
{числа, букви} = alpha-numeric
{малки букви} = lowercase
{големи букви} = uppercase
{празни символи} = space
s = '🐍 is awesomeͤͤͤͤͤ'
if s.endswith('e'):
print('s завършва на e') # eͤͤͤ != e
if s.startswith('🐍 is'):
print("s започва с '🐍 is'")
print('is е на индекс', s.find('is')) # Индекс по символ, не по байт
print("'e' се среща", s.count('e'), 'пъти')
s започва с '🐍 is'
is е на индекс 2
'e' се среща 2 пъти
Библиотеката string предоставя някои удобни константи
import string
# Вместо да хардкодваме "abcdef..."
print('all letters:', string.ascii_letters)
print('all lowercase:', string.ascii_lowercase)
print('all digits as a string:', string.digits)
print('all symbols:', string.punctuation)
print('all whitespaces:', string.whitespace, string.whitespace.encode())
all letters: abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
all lowercase: abcdefghijklmnopqrstuvwxyz
all digits as a string: 0123456789
all symbols: !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~
all whitespaces:
b' \t\n\r\x0b\x0c'
Int#
Python няма ограничение за големината на int (C++ long long има ограничение \(2^{63}-1\)). Но операциите с големи числа са бавни - не са \(O(1)\), а \(O(log(number)) = O(number\;of\;digits)\)
Когато стойността надвиши \(2^{63}-1\) числата се обръщат в BigInt.
https://www.codementor.io/@arpitbhayani/how-python-implements-super-long-integers-12icwon5vk
%%time
result = 1
for x in range(1, 100000):
result *= x
# Същият код на C++ минава за total: 0.001 s.
# Но не връща верен отговор :)
CPU times: user 4.23 s, sys: 22.9 ms, total: 4.25 s
Wall time: 4.27 s
# За по голяма прицизност може да използваме `decimal` модула
# По дефолт има прецизност 28 знака след запетаята,
# но може да му дадем повече :)
from decimal import *
getcontext().prec = 56
print(Decimal(22) / Decimal(7))
3.1428571428571428571428571428571428571428571428571428571
Math#
import math
print('Закръгляне нагоре 2.2 ->', math.ceil(2.2))
print('Закръгляне надолу 2.7 ->', math.floor(2.7))
# Може да правим побитово итериране на числото
print('Преброяване на битовете на числото 257 =', math.ceil(math.log2(257)))
# Полезно при построяване на Trie/radix sort/заделяне на hashmap
print('Преброяване на цифрите на числото 14532 =', math.ceil(math.log10(14532)))
# Математически важни функции
print('Най-голям общ делител на 35 и 15 =', math.gcd(35, 15))
print('Можем да смятаме inverse елемента по модул 15^-1 mod 26 =', pow(15, -1, 26))
# Комбинаторика (често се среща в задачи, но има и други)
print('N choose K -> 10 choose 2 =', math.comb(10, 2))
Закръгляне на горе 2.2 -> 3
Закръгляне на долу 2.7 -> 2
Преброяване на битовете на числото 257 = 9
Преброяване на цифрите на числото 14532 = 5
Най-голям общ делител на 35 и 15 = 5
Можем да смятаме inverse елемента по модул 15^-1 mod 26 = 7
N choose K -> 10 choose 2 = 45
math модулът има още много функции на които няма да се спираме, защото не се използват често:
Тригонометрични функции
Хиперболични функции
Гама функция
Конвертиране между градуси и радиани
Други
List#
Саморазширяващ се списък/масив от елементи. Въпреки името list, структурата е динамичен масив, а не свързан списък.
arr = [0] * 10 # инициализиране на списък с 10 елемента O(n)
print('Списък с 10 елемента:', arr)
arr = list() # празен списък O(1)
arr.clear() # O(1) еквивалентно на ' = list()'
print('Празен списък:', arr)
# O(1*) за разлика от конкатенацията на стрингове ('abc' + 'd') която е O(n)
arr.append(1)
arr.insert(0, -1) # O(n)
arr = [-2] + arr # O(n) еквивалентно на горното АКО елементите НЕ СА list
print('До момента имаме:', arr)
arr.reverse() # O(n)
print('Може да обърнем списъка', arr)
arr.sort() # O(n * log n)
print('И да го сортираме', arr)
last_element = arr.pop() # O(1)
print('pop премахва последния елемент и го връща', arr, last_element)
Списък с 10 елемента: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
Празен списък: []
До момента имаме: [-2, -1, 1]
Може да обърнем списъка [1, -1, -2]
И да го сортираме [-2, -1, 1]
pop премахва последния елемент и го връща [-2, -1] 1
Array#
array предоставя по-ефективно използване на паметта от list. Повече прилича на стандартните масиви в други езици. Добавянето на елемент в края на array е \(O(1)\).
Има същите функционалности като list.
При инициализиране трябва да окажем типа на елементите в списъка.
Ако се е стигнало до използване на по-оптимизирани структури в Python най-добре да използваме някоя библиотека като numpy.
%%capture
# %%capture is a jupyter instruction to hide output
from array import array
array('l') # array<32 bit int>
array('u', 'hello \u2641') # array<unicode character>
array('d', [1.0, 2.0, 3.14]) # array<double>
array('I', [1, 2, 3]) # array<unsigned 16 bit int>
Dict#
dict е питонската имплементация на Hash-Table. Повечето операции с dict са \(O(1)\). Изключение правят операциите за обработка на всички елементи, които са \(O(n)\).
Елементите на dict са key-value (ключ - стойност). Ключът задължително трябва да е от тип, който може да се хешира, стойността може да е всякаква.
Забележка: Казваме, че операциите са \(O(1)\), но всъщност зависят от размера на ключа. Обикновено ключовете са малки по размер в паметта и за това на повечето места в литературата се казва че операциите са \(O(1)\), но ако ключовете ни са стрингове с дължина 100,000 символа, то сложността на операциите не е точно \(O(1)\) :)
my_dict = {} # Инициализираме празен dict
my_dict = dict() # dict() == {}
my_dict['is big'] = 'not'
print('dict с 1 елемент', f'{my_dict = }')
del my_dict['is big'] # del изтрива елемент
print('празен dict след като изтрихме ключа "is big"', f'{my_dict = }')
# Ако се опитаме да достъпим или изтрием ключ който не
# съществува ще гръмнем с KeyError
del my_dict['non-existent key']
dict с 1 елемент my_dict = {'is big': 'not'}
празен dict след като изтрихме ключа "is big" my_dict = {}
---------------------------------------------------------------------------
KeyError Traceback (most recent call last)
Cell In [19], line 10
7 print('празен dict след като изтрихме ключа "is big"', f'{my_dict = }')
8 # Ако се опитаме да достъпим или изтрием ключ който не
9 # съществува ще гръмнем с KeyError
---> 10 del my_dict['non-existent key']
KeyError: 'non-existent key'
# Итериране по dict
person_age = {
'Gosho na pochivka': 25,
'Gosho na fmi': 11001,
}
result = []
for x in person_age: # == .keys()
result.append(x)
print('for x in dict:', result)
result = []
for x in person_age.keys():
result.append(x)
print('for x in dict.keys():', result)
result = []
for x in person_age.values():
result.append(x)
print('for x in dict.values():', result)
result = []
for x in person_age.items():
result.append(x)
print('for x in dict.items():', result)
for x in dict: ['Gosho na pochivka', 'Gosho na fmi']
for x in dict.keys(): ['Gosho na pochivka', 'Gosho na fmi']
for x in dict.values(): [25, 11001]
for x in dict.items(): [('Gosho na pochivka', 25), ('Gosho na fmi', 11001)]
# По време на итериране не е позволено да променяме dict
palindromes = {
'ami': 'ima',
'pari': 'irap',
'nema': 'amen',
}
for x in palindromes:
palindromes[x] = 'nali ne mojelo da se promenq e bai xy*'
print(palindromes)
for x in palindromes:
del palindromes[x] # промяна върху ключовете не работи :(
{'ami': 'nali ne mojelo da se promenq e bai xy*', 'pari': 'nali ne mojelo da se promenq e bai xy*', 'nema': 'nali ne mojelo da se promenq e bai xy*'}
---------------------------------------------------------------------------
RuntimeError Traceback (most recent call last)
Cell In [21], line 13
9 palindromes[x] = 'nali ne mojelo da se promenq e bai xy*'
11 print(palindromes)
---> 13 for x in palindromes:
14 del palindromes[x]
RuntimeError: dictionary changed size during iteration
print('въпреки грешката на предишната стъпка изтрихме 1 ключ (на рандъм)', palindromes)
# Тъй като не може да променяме dict докато итерираме ще трябва да създадем нов
changed = {k: 'shte si napravq moi dict togava'
for k, _ in palindromes.items() if k != 'pari'}
print(changed)
въпреки грешката на предишната стъпка изтрихме 1 ключ (на рандъм) {'pari': 'nali ne mojelo da se promenq e bai xy*', 'nema': 'nali ne mojelo da se promenq e bai xy*'}
{'nema': 'shte si napravq moi dict togava'}
# Задача
my_list = [1,2,3,4,5]
my_dict = {1:'a', 2:'b', 3:'c', 4:'d', 5:'e'}
# Коя операция е по-бърза
if 4 in my_list:
print('list')
if 4 in my_dict:
print('dict')
"""Изпълни клетката за хинт"""
import base64
print(base64.b64decode(
'0J7Qv9C10YDQsNGC0L7RgNGK0YIgaW4g0LLRitGA'
'0YXRgyBsaXN0INC1IE8obiksINCwINCy0YrRgNGF'
'0YMgZGljdCDQtSBPKDEqKQ=='.encode()).decode())
Defaultdict#
defaultdict живее в модула collections. Има същата функционалност като dict, но при инициализация трябва да окажем от какъв тип ще са стойностите. При достъпване на ключ, който не съществува, ще се създаде ключа и стойност - дефолтен обект от типа, който задаваме при инициализация.
Обикновено го използваме за удобство - не е нужно да проверяваме дали стойността зад някой ключ е инициализирана.
from collections import defaultdict
my_default_dict = defaultdict(list)
print(my_default_dict)
my_default_dict['what']
print('дори само с достъпване на ключа създаваме дефолтен обект', my_default_dict)
del my_default_dict['what']
for index, char in enumerate('this is a string'):
my_default_dict[char].append(index) # Това ще изгърми с нормален dict
my_default_dict
defaultdict(<class 'list'>, {})
дори само с достъпване на ключа създаваме дефолтен обект defaultdict(<class 'list'>, {'what': []})
defaultdict(list,
{'t': [0, 11],
'h': [1],
'i': [2, 5, 13],
's': [3, 6, 10],
' ': [4, 7, 9],
'a': [8],
'r': [12],
'n': [14],
'g': [15]})
# Задача (advanced)
# Как може да имплементираме Trie чрез defaultdict
import base64
print(base64.b64decode(
'ZGVmIHRyaWUoKToKICAgIHJldHVybi'
'BkZWZhdWx0ZGljdCh0cmllKQo='.encode()).decode())
Counter#
Counter е още един под-клас на dict. При инициализация може да се подаде колекция (или друг обект който може да се итерира и елементите му могат да се хешират), която бива итерирана и елементите ѝ се преброяват.
Елементите на колекцията стават ключове, а стойностите са колко пъти даден елемент е срещнат в колекцията.
from collections import Counter
Counter('string isn\'t a collection, but it\'s iterable')
Counter({'s': 3,
't': 6,
'r': 2,
'i': 5,
'n': 3,
'g': 1,
' ': 6,
"'": 2,
'a': 2,
'c': 2,
'o': 2,
'l': 3,
'e': 3,
',': 1,
'b': 2,
'u': 1})
# Задача
# Напишете функция която извежда най-често срещаните букви на даден стринг
import base64
print(base64.b64decode(
'ZGVmIGNvdW50KHMpOgogICAgY291bnRzID0gbGl'
'zdChDb3VudGVyKHMpLml0ZW1zKCkpCiAgICBjb3'
'VudHMuc29ydChrZXk9bGFtYmRhIHg6IC14WzFdK'
'QogICAgcHJpbnQoY291bnRzKQo='.encode()).decode())
Set#
set е dict, който има само ключове, стойностите не ни интересуват. Добавяне/премахване/проверка дали елемент е в сет-а се случва за \(O(1)\)
set се използва по-рядко от dict, и основно за да намерим уникалните елементи на колекция.
uniques = set() # няма по-кратък начин за инициализране на сет, както {} за dict
# Може директно да го инициализираме от колекция
uniques = set([1, 2, 3, 3, 3, 3, 3, 3])
print('инициализация от колекция', uniques)
uniques.add(1) # добавяме елемент
print('елемент, който вече е в сета няма да се добави отново', uniques)
print('Може да обединим 2 сета', set([1, 2, 3]).union(set([2, 3, 4])))
print('Може да вземем само общите елементи', set([1, 2, 3]) & set([2, 3, 4]))
print('Може да вземем елементите които не са общи',
set([1, 2, 3]) ^ set([2, 3, 4]))
print('Може да проверим дали даден елемент е в сета: 5 in [1,2,3]?', 5 in set(
[1, 2, 3]))
инициализация от колекция {1, 2, 3}
елемент, който вече е в сета няма да се добави отново {1, 2, 3}
Може да обединим 2 сета {1, 2, 3, 4}
Може да вземем само общите елементи {2, 3}
Може да вземем елементите които не са общи {1, 4}
Може да проверим дали даден елемент е в сета: 5 in [1,2,3]? False
# Задача: колко различни букви са използвани в следния стринг
# 'The quick brown fox jumps over the lazy dog'
import base64
print(base64.b64decode(
'bGVuKHNldCgKICAgICdUaGUgcXVpY2sgYnJvd24gZm9'
'4IGp1bXBzIG92ZXIgdGhlIGxhenkgZG9nJwogICAgLm'
'xvd2VyKCkKICAgIC5yZXBsYWNlKCcgJywgJycpCikpC'
'g=='.encode()).decode())
Frozenset#
frozenset = immutable set. Immutability носи плюсове и минуси:
(+) може да хешираме сета (да го използваме като ключ в
dictили да го добавим в другset)(-) не може да го променяме (да добавяме/премахваме елементи)
my_dict = {
set([1, 2, 3]): 'sets are unhashable, trust me',
}
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In [26], line 1
----> 1 my_dict = {
2 set([1, 2, 3]): 'sets are unhashable, trust me',
3 }
TypeError: unhashable type: 'set'
numbers = set([1, 2, 3])
my_dict = {
frozenset(numbers): 'my password',
frozenset('characters'): 'ако подадем string, ще конструираме set от буквите'
}
my_dict
{frozenset({1, 2, 3}): 'my password',
frozenset({'a',
'c',
'e',
'h',
'r',
's',
't'}): 'ако подадем string, ще конструираме set от буквите'}
Stack#
stack (стак/стек) е LIFO (Last In First Out) структура, която ни позволява бърз достъп до последния добавен елемент. В Python няма клас Stack, защото list има всички необходими методи.

stack = []
for x in range(100):
stack.append(x)
first10 = 0
for i in range(10):
first10 += stack.pop() # Достъп до последния елемент е О(1)
print('Сумата на първите 10 елемента е: ', first10) # sum(100 до 90) = 945
Сумата на първите 10 елемента е: 945
Queue#
queue е thread-safe имплементация на опашка (FIFO). Може да я създадем с ограничен размер Queue(maxsize=42) - ако се напълни и се пробваме да добавим нов елемент ще се блокира нишката. maxsize=0 означава че няма лимит и ще се разширява докато имаме памет.
Ако работим в multithreaded среда, то операциите като qsize(), empty(), full() и повечето други нямат гаранции, че връщат винаги точен резултат. Резултатът е близък до точен, но няма гаранция.
Пример: Ако имаме опашка която е пълна и от 1 нишка извикаме full() докато се изпълнява full може някоя друга нишка да извика get() и да премахне елемент - съответно ще получим True когато трябва да е False.

import queue
que = queue.Queue()
for x in range(100):
que.put(x)
print('Елементите ги борим с qsize: ', que.qsize())
total = 0
while not que.empty(): # Работим на 1 нишка така че това е детерминистично
total += que.get()
print('Сумата на всички елементи е: ', total)
for x in range(100):
que.put(x)
first10 = 0
for i in range(10):
first10 += que.get()
print('Сумата на първите 10 елемента е: ', first10) # sum(1 до 10) = 45
Елементите ги борим с qsize: 100
Сумата на всички елементи е: 4950
Сумата на първите 10 елемента е: 45
PriorityQueue#
Какво правим ако искаме опашка, но елементите да са поддедени по някакъв приоритет, не по реда в който ги добавяме? - тадаа PriorityQueue 🎉
PriorityQueue предоставя добавяне/премахване на елемент за \(O(logn)\) и достъп до най-малкия елемент за \(O(1)\).
import queue
import random
numbers = list(range(1, 11)) # [1, 2, ... 10]
random.shuffle(numbers)
print('Разбъркваме числата с random.shuffle()', numbers)
que = queue.PriorityQueue()
for x in numbers:
que.put(x)
print('Данните не са подредени в реда в който ги добавяме, но 1 винаги е в началото:',
que.queue)
top3 = []
for i in range(3):
top3.append(que.get())
print('Приоритетната опашка връща елементите от най-малък към най-голям', top3)
Разбъркваме числата с random.shuffle() [1, 9, 7, 4, 10, 2, 6, 3, 5, 8]
Данните не са подредени в реда в който ги добавяме, но 1 винаги е в началото: [1, 3, 2, 4, 8, 7, 6, 9, 5, 10]
Приоритетната опашка връща елементите от най-малък към най-голям [1, 2, 3]
За съжаление не е възможно да използваме custom функция за сравнение за да направим Max Heap.
Има 2 начина да решим такава задача, единият е да създаваме клас, който да имплементира __lt__/__gt__, и __eq__.
Вторият ще разгледаме в следващата клетка.
But you know - if you like it hard I won’t stop you - https://stackoverflow.com/questions/57487170/is-it-possible-to-pass-a-comparator-to-a-priorityqueue-in-python
heapq#
heapq предоставя основните методи за работа с приоритетни опашки поотделно. Тоест, не е нужно да ползваме цялата функционалност на PriorityQueue, а може да използваме само 1 от методите - например heapq.heapify.
tl;dr как работи приоритетна опашка?
Ковертираме произволен масив до
heapq.heapify()\(O(n)\)Ако искаме да добавим елемент използваме
heapq.heappush()\(O(log n)\)Ако искаме да извадим елемент използваме
heapq.heappop()\(O(log n)\)
import heapq
import random
numbers = list(range(1, 11)) # [1, 2, ... 10]
random.shuffle(numbers)
print('Разбъркваме числата с random.shuffle()', numbers)
heapq.heapify(numbers) # по-бързо от добавянето на елементи 1 по 1
print('Данните не са подредени в реда в който ги вкарваме, но 1 винаги е в началото:',
numbers)
# Добавянето на нов по-малък елемент ще го постави в началото
heapq.heappush(numbers, 0)
print('0 винаги е в началото:', numbers)
top3 = []
for i in range(3):
top3.append(heapq.heappop(numbers))
print('Приоритетната опашка връща елементите от най-малък към най-голям', top3)
Разбъркваме числата с random.shuffle() [4, 2, 9, 5, 3, 8, 1, 7, 10, 6]
Данните не са подредени в реда в който ги вкарваме, но 1 винаги е в началото: [1, 2, 4, 5, 3, 8, 9, 7, 10, 6]
0 винаги е в началото: [0, 1, 4, 5, 2, 8, 9, 7, 10, 6, 3]
Приоритетната опашка връща елементите от най-малък към най-голям [0, 1, 2]
deque#
Ако ни трябва опашка + стек, тоест искаме \(O(1*)\) за добавяне/премахване на елемент в началото използваме deque. Обикновено се имплементира върху Circular Array.
from collections import deque
deq = deque(['опашка ли съм', 'ʞǝꓕɔ иѵи'])
deq.insert(1, '¿?') # Добавянето на елемент на произволна позиция е О(n)
print(deq)
deq.append('у десно')
deq.append('у десно (1)')
deq.appendleft('у лево')
print(deq)
print('последният елемент за О(1):', deq.pop())
print('първият елемент за О(1):', deq.popleft())
deque(['опашка ли съм', '¿?', 'ʞǝꓕɔ иѵи'])
deque(['у лево', 'опашка ли съм', '¿?', 'ʞǝꓕɔ иѵи', 'у десно', 'у десно (1)'])
последният елемент за О(1): у десно (1)
първият елемент за О(1): у лево
# Интересно приложение на deque е имплементирането на round robin
def roundrobin(*iterables):
"roundrobin('ABC', 'D', 'EF') --> A D E B F C"
iterators = deque(map(iter, iterables))
while iterators:
try:
while True:
yield next(iterators[0])
iterators.rotate(-1)
except StopIteration:
iterators.popleft()
# Задача
# Имаме следната имплементация на BFS. Как може да я обърнем до DFS?
from collections import deque
graph = {
'A': ['B', 'C'],
'B': ['D', 'E'],
'C': ['F'],
'D': [],
'E': ['F'],
'F': []
}
def bfs(graph, start):
visited = set()
que = deque()
visited.add(start)
que.append(start)
while que:
s = que.popleft()
print(s, end=" ")
for neighbor in graph[s]:
if neighbor not in visited:
visited.add(neighbor)
que.append(neighbor)
print(base64.b64decode('0JfQsNC80LXQvdGP0LzQsCBkZXF1ZS5wb3BsZWZ0KCk'
'g0YEgZGVxdWUucG9wKCk='.encode()).decode())
Algorithms & Oddities#
Binary Search#
Бързо (\(O(logn)\)) търсене в сортиран масив.
bisect_left = кой е първият индекс на който може да поставим елемента, за да се запази подредбата?
bisect_right = кой е последният индекс на който може да поставим елемента, за да се запази подредбата?
import bisect
numbers = [0, 1, 2, 4, 4, 4, 4, 7, 8, 9]
# 0 1 2 3 4 5 6 7 8 9
print('Ако елемента съществува, връща индекса на първото срещане',
bisect.bisect_left(numbers, 4))
print('Ако елемента съществува, връща първия индекс след последното срещане',
bisect.bisect_right(numbers, 4))
print('Ако елемента не съществува, връща индекса на първото срещане',
bisect.bisect_left(numbers, 5))
print('Ако елемента не съществува, връща първия индекс след последното срещане',
bisect.bisect_right(numbers, 5))
Ако елемента съществува, връща индекса на първото срещане 3
Ако елемента съществува, връща първия индекс след последното срещане 7
Ако елемента не съществува, връща индекса на първото срещане 7
Ако елемента не съществува, връща първия индекс след последното срещане 7
# Задача
# В сортиран list от числа, напишете функция, която намира броя на срещанията на някое число
print(base64.b64decode('ZGVmIGNvdW50X29jY3VycmVuY2VzKGhh'
'eXN0YWNrLCBuZWVkbGUpOgogICAgYmlzZWN0LmJpc2VjdF9yaWd'
'odChoYXlzdGFjaywgbmVlZGxlKSAtIGJpc2VjdC5iaXNlY3RfbG'
'VmdChoYXlzdGFjaywgbmVlZGxlKQo='.encode()).decode())
Copy & Deepcopy#
Python подава аргументите на функциите по референция, така че може да ги променяме. Това не е винаги желаното поведение, така че Python предоставя 2 метода за копиране да обекти.
from copy import copy, deepcopy
def reset_tree():
return {
'value': 'parent',
'child': {
'value': 'child',
}
}
def level_up(root):
tree = root # Това не прави нищо, само преименува променливата
tree['child']['value'] = 'super child'
return tree
tree = reset_tree()
same = level_up(tree)
print(tree)
print(same)
print('Когато подаваме елемент по референция във функцията работим със същия елемент')
print(f'{id(tree) = } == {id(same) = }')
{'value': 'parent', 'child': {'value': 'super child'}}
{'value': 'parent', 'child': {'value': 'super child'}}
Когато подаваме елемент по референция във функцията работим със същия елемент
id(tree) = 4399790912 == id(same) = 4399790912
def level_up_swallow(root):
tree = copy(root)
tree['child']['value'] = 'super child'
return tree
tree = reset_tree()
same_but_different_but_same = level_up_swallow(tree)
print(tree)
print(same_but_different_but_same)
print('(shallow) copy копира обекта, но не копира обектите в него')
print(f'{id(tree) = } != {id(same_but_different_but_same) = }')
{'value': 'parent', 'child': {'value': 'super child'}}
{'value': 'parent', 'child': {'value': 'super child'}}
(shallow) copy копира обекта, но не копира обектите в него
id(tree) = 4698146048 != id(same_but_different_but_same) = 4698275328
def level_up_deep(root):
tree = deepcopy(root)
tree['child']['value'] = 'super child'
return tree
tree = reset_tree()
different = level_up_deep(tree)
print(tree)
print(different)
print('deepcopy създава копие, на обекта и рекурсивно на обектите в него')
print(f'{id(tree) = } != {id(different) = }')
{'value': 'parent', 'child': {'value': 'child'}}
{'value': 'parent', 'child': {'value': 'super child'}}
deepcopy създава копие, на обекта и рекурсивно на обектите в него
id(tree) = 4698416960 != id(different) = 4698412800
Random#
random модулът предоставя методи за избиране на случаен елемент, случайно число, случайни битове и тн.
Обикновено се използва за тестове (генериране на вход/изход).
За криптографски задачи (криптиране на данни, пароли, генериране на токени и тн), не трябва да се
използва random, а secrets модула. За да не навлизаме в разликите, нека да кажем че secrets генерира
по-рандъм изход от random модула :)
import random
# random модулът може да приеме начална стойност, която се използва
# в рандъм алгоритмите, за да върне детерминистичен изход.
# Обикновено се използва за да може даден експеримент/тест да се репродуцира
random.seed(42)
# Това е рандъм число, но заради seed = 42, винаги ще върне 82 в този notebook
random.randint(1, 100)
82
import random
print('рандъм float', random.random())
print('randint [0,1]:', random.randint(0, 1))
# Някои методи в random работят със затворени, някой с отворени интервали.
# Добре е да се провери преди използването им
print('randrange [0,1) не е много рандъм:', random.randrange(0, 1))
# randrange(a,b) = randint(a,b-1)
# Ако имаме колекция от елементи може да използваме choice, за да изберем произволен
# Алтернативният метод е да използваме random.randrange(0, len(collection))
print('Епа кот дойде:', random.choice(['fuck', 'marry', 'kill']))
# Може да задаваме и тежести - 50% ще е първият елемент, 25% втория и 25% третия
print('Понякога рандъм изборът не е много рандъм',
random.choices(
['boiko', 'pak boiko', 'totally not boiko'],
weights=[2, 1, 1]
))
# Понякога трябва да разбъркаме някяква колекция. Например преди да правим quicksort
numbers = list(range(1, 11))
random.shuffle(numbers)
print('shuffle:', numbers)
# При експерименти или когато имаме много данни искаме да изберем произволна подгрупа
print('Избор на 2 елемента без повторения:', random.sample(numbers, 2))
рандъм float 0.11133106816568039
randint [0,1]: 1
randrange [0,1) не е много рандъм: 0
Епа кот дойде: fuck
Понякога рандъм изборът не е много рандъм ['boiko']
shuffle: [8, 3, 6, 7, 1, 4, 5, 10, 9, 2]
Избор на 2 елемента без повторения: [7, 9]
%%capture
import random
# За пълнота трябва да споменем, че random модулът има имплементация на
# повечето видови разпределения от статистиката, но няма да навлизаме в подробности
random.uniform(1, 10)
random.gauss(1, 2)
random.expovariate(1)
random.gammavariate(10, 1)
random.normalvariate(5, 1)
Itertools#
itertools модулът предоставя методи улесняващи итерацията по елементи и често използвани конструкции. Вече сме виждали някой подобни функции като map/filter.
Функциите са доста и улесняват писането на цикли и особено вложени цикли, но понякога отнема повече време да намерим правилната функция от колкото да напишем кода от 0.
Целият списък е тук като има разширение на стандартната библиотека от more-itertools пакета.
import itertools
# Итератор, който се връща в началото на списъка като се изчерпи.
# Наподобява Ring Buffer
cycle = itertools.cycle('ABC')
for _ in range(10):
print(next(cycle), end=', ')
print()
# Итератор, който обединява няколко итератора/колекции
# Може да го използваме за да flatten-нем списък от списъци
for x in itertools.chain('first', ' ', 'second'):
print(x, end=',')
print()
# Към всеки елемент от списъка се прибавя сумата на всички предишни елементи
print(list(itertools.accumulate([-20, +10, +30, -5])))
# Може да го използваме за да генерираме всички префикси на дума
print(list(itertools.accumulate('construct')))
# Генериране на всички уникални двойки. Еквивалентно на 2 вложени цикъла
print(list(itertools.combinations('ABC', 2)))
# Генериране на всички съседни двойки
print(list(itertools.pairwise([1, 5, 7, 10])))
# Всички пермутации
print(list(itertools.permutations([0, 1, 2])))
# A x B - декартово произведение
print(list(itertools.product([1, 2, 3], 'ABC')))
A, B, C, A, B, C, A, B, C, A,
f,i,r,s,t, ,s,e,c,o,n,d,
[-20, -10, 20, 15]
['c', 'co', 'con', 'cons', 'const', 'constr', 'constru', 'construc', 'construct']
[('A', 'B'), ('A', 'C'), ('B', 'C')]
[(1, 5), (5, 7), (7, 10)]
[(0, 1, 2), (0, 2, 1), (1, 0, 2), (1, 2, 0), (2, 0, 1), (2, 1, 0)]
[(1, 'A'), (1, 'B'), (1, 'C'), (2, 'A'), (2, 'B'), (2, 'C'), (3, 'A'), (3, 'B'), (3, 'C')]
Functools#
functools предоставя фунцкии от по-висок ред - декоратори и помощни функции. Концепциите в модула са доста сложни така че ще разгледаме само по-често използваните. В темата за функционално се разлеглежда един от основните методи - reduce.
Друг интересен пример е functools.singledispatch декоратора, който ни позволява да overload-ваме функции базирано на типа на първият им аргумент. Тъй като това не работи за функции с повече от 1 аргумент които да overload-нем, няма да му отделяме време. But it exists!
%%time
import functools
# Нека си дефинираме наивна рекурсивна функция
def fibonacci(n):
if n in [0, 1]:
return 1
return fibonacci(n - 1) + fibonacci(n - 2)
# Имаме O(2 ^ 40) извиквания на фунцкии :(
fibonacci(40)
CPU times: user 19.8 s, sys: 92 ms, total: 19.9 s
Wall time: 20 s
165580141
%%time
import functools
# Mоже да я забързаме ако кешираме извикванията
# Така няма да правим едно и също изчисление 2 пъти
@functools.lru_cache(maxsize=150)
def fibonacci(n):
if n in [0, 1]:
return 1
return fibonacci(n - 1) + fibonacci(n - 2)
fibonacci(40)
fibonacci(150)
# Дори fib(150) което с наивната рекурсия няма да приключи
# никога минава за под 1ms
CPU times: user 62 µs, sys: 89 µs, total: 151 µs
Wall time: 155 µs
16130531424904581415797907386349
# Ако имаме клас, за който искаме да имплементираме сравнение
# Може да имплементираме само __lt__ и __eq__ и останалите методи
# ще се имплементират за нас
# Защото понякога имплементирането им просто не е тривиално ...
import functools
@functools.total_ordering # ще имплементира __gt__, __le__, и тн.
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __eq__(self, other):
return self.x == other.x and self.y == other.y
def __lt__(self, other):
# Имплементира Z-order curve подредба на точките
most_significant_dim = 'x'
if self.less_msb(self.x ^ other.x, self.y ^ other.y):
msd = 'y'
if most_significant_dim == 'x':
return self.x < other.x
else:
return self.y < other.y
def less_msb(self, x, y):
return x < y and x < (x ^ y)
CSV#
Python предоставя няколко модула за работа с данни сериализирани под различен формат. Един от тези модули е за работа с CSV файлове (coma-separated values). Ще разгледаме само пример за парсене на такъв файл, но модулът има много различни функционалности и опции.
import csv
from io import StringIO
contents = StringIO( # == string stream
'''Lyubo,21,Sofia
Alex,22,Sofia
Ivan,49,"Ne Sofia"
''')
reader = csv.reader(contents)
for index, row in enumerate(reader):
print(index, ':', row)
0 : ['Lyubo', '21', 'Sofia']
1 : ['Alex', '22', 'Sofia']
2 : ['Ivan', '49', 'Ne Sofia']
JSON#
Друг популярен формат на данни е JSON … Разбира се в Python има модул json.
import json
my_dict = json.loads('{"foo": {"bar":["baz", null, 1.0, 2]}}')
print('От стринг конструирахме dict, който може да използваме:',
my_dict['foo']['bar'])
print('Обратната операция на сериализиране е удобна за принтене на дебъг съобщения')
print(json.dumps(my_dict, sort_keys=True, indent=4))
От стринг конструирахме dict, който може да използваме: ['baz', None, 1.0, 2]
Обратната операция на сериализиране е удобна за принтене на дебъг съобщения
{
"foo": {
"bar": [
"baz",
null,
1.0,
2
]
}
}
Regex#
Regex или още регулярни изрази са стрингове, които описват някакъв патърн, който може да използваме за търсене в обикновени стрингове.
regex може да запълни материала за цял курс, но ще се опитаме да разгледаме само често използвани примери, без да навлизаме в подробности.
Писането на регулярни изрази обикновено е трудоемка задача, за това препоръчваме инструменти като regexr, които изпълняват regex в реално време и имат обяснение защо даден стринг е match-нал.
import re
# == (.start(), .end())
print(re.search(r'123', '458971985719285712324953928572').span())
ninjas = 'ninjatreenoooinjaforestninjavoicenwvi9ew9ninja'
print('Следва ли стринга този патърн?',
re.match(r'.*ninja$', ninjas) is not None)
print('Намери всички срещания:',
re.findall(r'n.*?a', ninjas))
print('Разбиване на стринг на всяко срещане на патърн:',
re.split(r'n.*?a', ninjas))
print('Субституция на патърн в стринг:',
re.sub(r'n.*?a', '-NINJA-', ninjas))
(16, 19)
Следва ли стринга този патърн? True
Намери всички срещания: ['ninja', 'noooinja', 'ninja', 'nwvi9ew9ninja']
Разбиване на стринг на всяко срещане на патърн: ['', 'tree', 'forest', 'voice', '']
Субституция на патърн в стринг: -NINJA-tree-NINJA-forest-NINJA-voice-NINJA-
Силата на Python#
Ще разгледаме няколко задачи, решени на Python, и на C++
from collections import defaultdict
def twoSum(nums, target: int):
counter = defaultdict(list)
for i, num in enumerate(nums):
counter[num].append(i)
for first in nums:
second = target - first
if second in counter and first != second:
return [counter[first][0], counter[second][0]]
elif second in counter and len(counter[first]) > 1:
return [counter[first][0], counter[second][1]]
return []
Еквивалетното решение на C++:
vector<int> twoSum(vector<int>& nums, int target) {
std::unordered_map<int, std::vector<int>> counter = std::unordered_map<int, std::vector<int>>();
for (int i = 0; i < nums.size(); i++) {
int num = nums[i];
auto it = counter.find(num);
if (it != counter.end()) {
it->second.push_back(i);
}
else {
counter.insert({num, {i}});
}
}
for (const int first: nums) {
int second = target - first;
auto second_it = counter.find(second);
auto first_it = counter.find(first);
if (second_it != counter.end() && first != second) {
return {first_it->second[0], second_it->second[0]};
}
else if(second_it != counter.end() && first_it->second.size() > 1) {
return {first_it->second[0], second_it->second[1]};
}
}
return {};
}
Java решение:
public static int[] twoSum(int[] nums, int target) {
HashMap<Integer, ArrayList<Integer>> counter = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
counter.putIfAbsent(nums[i], new ArrayList<Integer>());
counter.get(nums[i]).add(i);
}
for (int first : nums) {
int second = target - first;
if (counter.containsKey(second) && first != second)
return new int[] { counter.get(first).get(0), counter.get(second).get(0) };
else if (counter.containsKey(second) && counter.get(first).size() > 1)
return new int[] { counter.get(first).get(0), counter.get(second).get(1) };
}
return new int[] {};
}
Забележете разликата в работата с unordered_map спрямо dict.
def isPalindrome(self, s):
s = ''.join(c for c in s.lower() if c.isalnum())
return s == s[::-1]
C++ решение:
bool isPalindrome(const std::string &s) {
std::string filtered;
for (char ch : s) {
if (std::isalnum(ch)) {
filtered += std::tolower(ch);
}
}
// Check if the filtered string is a palindrome
int left = 0, right = filtered.size() - 1;
while (left < right) {
if (filtered[left] != filtered[right]) {
return false;
}
left++;
right--;
}
return true;
}
Java решение:
public static boolean isPalindrome(String s) {
StringBuilder filtered = new StringBuilder();
for (char ch : s.toCharArray()) {
if (Character.isLetterOrDigit(ch)) {
filtered.append(Character.toLowerCase(ch));
}
}
int left = 0, right = filtered.length() - 1;
while (left < right) {
if (filtered.charAt(left) != filtered.charAt(right)) {
return false;
}
left++;
right--;
}
return true;
}
Тук преимуществото на Python е в използванто на .join и slicing-а.