Open In Colab

Променливи, разклонения, цикли, типове, функции#

Динамично vs. статично типизиране. Round 1#

Python е динамично-типизиран език.

Това означава, че за разлика от статично-типизираните езици (като например C++, Java или C#), променливите (които биват наричани имена/names/ в Python) биват проверявани за коректността на типа им при изпълнението на програмата, а не при компилацията (каквато и няма в Python, понеже кодът се интерпретира вместо да се компилира).

Друга особеност е, че типът на променливата може да се промени по време на изпълнението на програмата и също така не се декларира предварително.

# I want it to be a number
a = 42

a
42
# No, sorry, changed my mind, let it be a string
a = "a string"

a
'a string'
# Actually l... You know what? Screw it. I don't need it.
del a

a   # 💥
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[3], line 4
      1 # Actually l... You know what? Screw it. I don't need it.
      2 del a
----> 4 a   # 💥

NameError: name 'a' is not defined

Note: Относно Jupyter notebooks (тетрадки):

  1. Всичко, което се изпълнява в една клетка, се запазва в паметта на ядрото (kernel) и може да бъде използвано в следващите клетки.

  2. Ако последният ред от клетка връща стойност (различна от None), тя се показва като резултат от изпълнението на клетката.

Т.е. горните три клетки, изпълнени една след друга, са еквивалентни на следния Python скрипт:

# I want it to be a number
a = 42
print(a)

# No, sorry, changed my mind, let it be a string
a = "a string"
print(a)

# Actually l... You know what? Screw it. I don't need it.
del a
print(a)  # 💥
42
a string
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[4], line 11
      9 # Actually l... You know what? Screw it. I don't need it.
     10 del a
---> 11 print(a)  # 💥

NameError: name 'a' is not defined

Note 2: с print(...) се пише на изхода на програмата (STDOUT), а с input(...) се чете от входа (STDIN).

Какви типове има?#

Числа#

Съществуват три основни вградени типа, които описват числови стойности: int, float и complex.

  • int - цели числа

  • float - реални числа (с плаваща запетая)

  • complex - комплексни числа

И за трите типа са дефинирани аритметичните оператори +, -, *, /, както и ** (степенуване).

whole = -42
fraction = 0.999
imag = 3 + 2j  # this is the complex number (3 + 2i)

1j ** 2
(-1+0j)

⚠️ Резултатът на /, приложен между два int-а е float. Всички от останалите оператори запазват резултата в int.

4 / 2
2.0
1 / 3
0.3333333333333333

За целочислени сметки имаме и операторите // и % (целочислено деление и остатък от деление).

// е и операторът, който трябва да ползваме, ако искаме при делението на два int-а да получим отново int.

7 / 2
3.5
7 // 2
3
7 % 2
1

Hey Siri, how much is 0 divided by 0?

0 / 0
---------------------------------------------------------------------------
ZeroDivisionError                         Traceback (most recent call last)
Cell In[11], line 1
----> 1 0 / 0

ZeroDivisionError: division by zero

С безкрайността обаче нямаме такъв проблем (math.inf е специален float, който има за цел да бъде еквивалентен на \(\infty\) откъм аритметични операции):

from math import inf

1 / inf
0.0

Размерът на int не е фиксиран както в повечето статично-типизирани езици, при които програмистът избира дали да използва 32-битов, 64-битов или някаква друга размерност за целочислена променлива. Тук integer overflow се избягва като динамично се изчислява размерът на паметта, нужен за съхранението на число с произволна големина.

from sys import getsizeof

print("size of 1 is",            getsizeof(1),            "bytes")
print("size of 2 ** 16 is",      getsizeof(2 ** 16),      "bytes")
print("size of 2 ** 30 is",      getsizeof(2 ** 30),      "bytes")
print("size of 2 ** 30 ** 5 is", getsizeof(2 ** 30 ** 5), "bytes")
size of 1 is 28 bytes
size of 2 ** 16 is 28 bytes
size of 2 ** 30 is 32 bytes
size of 2 ** 30 ** 5 is 3240028 bytes

Обърнете внимание, че изписаните резултати са в байтове, а не битове. Причината числото \( 1 \) да заема цели 28 байта например е понеже в python всичко е обект и си има своите член-данни и методи, дори и типове като int.

От друга страна, float винаги заема фиксиран брой байтове. Причината за това е, че прецизността е константна - до 18 знака след десетичната запетая. В случай, че не ни е достатъчна такава прецизност, могат да се използват типове от някои вградени и не-вградени библиотеки, като например decimal.Decimal, с който можем да боравим с до 28 знака след десетичната запетая.

Целочислени литерали могат да се задават и в двоична, осмична и 16-ична бройни системи, използвайки като префикс съответно 0b, 0o и 0x:

a = 0b1010
b = 0o12
c = 0xA

print("a =", a)
print("b =", b)
print("c =", c)
a = 10
b = 10
c = 10

Низове#

Текстовите низове в Python имат тип str. Те могат да бъдат с произволна дължина, като в литерал се обграждат или с ", или с ' (без значение).

s1 = "abra"
s2 = 'cad'

s1 + s2 + s1 + '!'
'abracadabra!'

Многоредови низови литерали биват задавани с тройни кавички, също както специалните многоредови коментари, наричани docstring-ове (повече за тях в следваща лекция):

multiple_rows_text = """This text contains multiple rows.
This is the second one.
Here comes the third one.
Back and fourth."""

multiple_rows_text
'This text contains multiple rows.\nThis is the second one.\nHere comes the third one.\nBack and fourth.'

При едноредови низове, ако редът става прекалено дълъг можем и да разделим низа на части така:

too_long = "This is a very very very very very very very " \
           "very very very very very very very very very " \
           "long snake 🐍."

too_long
'This is a very very very very very very very very very very very very very very very very long snake 🐍.'

Друг вариант, ако не ви харесват обратните наклонени черти (line-continuation characters) и нагласената индентация, е да се ползват скоби.

Note: тези два начина са приложими не само за дълги низове, а и за дълги редове като цяло, например аритметични/булеви изрази, comprehension-и и т.н.

also_too_long = (
    "This is a very very very very very very very "
    "very very very very very very very very very "
    "long snake 🐍."
)

also_too_long
'This is a very very very very very very very very very very very very very very very very long snake 🐍.'

Подържат се Unicode символи, но трябва да се внимава при взимането на дължината им (например емоджитата с флагове кодират двоен брой байтове, което доведе до това):

moyai_and_china = "🗿🇨🇳"

print("the length of it =", len(moyai_and_china))
# never ignore the red flags...
the length of it = 3

Escape символът както обикновено е \. Някои специални символи по този начин написани са:

  • \n - нов ред (Line Feed, ASCII стойност 13 (0x0D))

  • \t - хоризонтална табулация (ASCII стойност 9)

  • \r - Carriage Return, ASCII стойност 10 (0x0A)

С \' или \" се запазват и кавичките в случай, че видът кавичка съвпада с тази, ограждаща низа.

Самата обратно-наклонена черта се въвежда с \\.

s = 'Is it readable?\nYesn\'t\n'

s
"Is it readable?\nYesn't\n"

Ако знаем 16-ичния код на конкретен Unicode символ, можем да го запишем след \u:

rip_lemmy = "A\u2660"

rip_lemmy
'A♠'

В случай пък че ни трябва конкретен байт в низа, чийто 16-ичен код знаем, може да се използва \x. Например \x00 е null-byte, a \x1B - ESC.

not_your_c_string = "You can safely have \x00 in the middle of a string"

print(not_your_c_string)
You can safely have in the middle of a string

Относно ASCII кодове, съществуват две функции за преобразуване от и във кода на символа: chr и ord съответно.

print("ord('A') =", ord("A"))
print("chr(65) =", chr(65))

# don't worry about not understanding the following line,
# it's just to catch a glimpse of the mighty Python's power
alphabet_kebab = "-".join(chr(c) for c in range(ord("A"), ord("Z")+1))

print(alphabet_kebab)
ord('A') = 65
chr(65) = A
A-B-C-D-E-F-G-H-I-J-K-L-M-N-O-P-Q-R-S-T-U-V-W-X-Y-Z

Съществува и тип за низ от байтове - bytes. Има си и литерал, който е като този на str, но просто с едно b преди отварящите кавички.

Конвертирането му от и до str става единственo чрез подбиране на правилен енкодинг (UTF-8 или друг) и може да хвърли грешка.

bytes_literal = b"You can use ASCII characters as well as \x5C\x78 syntax, e.g. \x00\x01, etc."
bytes_literal
b'You can use ASCII characters as well as \\x syntax, e.g. \x00\x01, etc.'
type(bytes_literal)
bytes
str_from_bytes = bytes_literal.decode("utf-8")
str_from_bytes
'You can use ASCII characters as well as \\x syntax, e.g. \x00\x01, etc.'
type(str_from_bytes)
str
bytes_from_str = str_from_bytes.encode("utf-8")
bytes_from_str
b'You can use ASCII characters as well as \\x syntax, e.g. \x00\x01, etc.'
deadbeef = b"\xDE\xAD\xBE\xEF"
deadbeef.decode("utf-8")
---------------------------------------------------------------------------
UnicodeDecodeError                        Traceback (most recent call last)
Cell In[29], line 2
      1 deadbeef = b"\xDE\xAD\xBE\xEF"
----> 2 deadbeef.decode("utf-8")

UnicodeDecodeError: 'utf-8' codec can't decode byte 0xbe in position 2: invalid start byte

Интерполацията на низове е възможна по три основни начина:

  • Форматиране с % (old-school)

  • str.format (от Python 3 насам, back-port-нат към Python 2.7)

  • f-strings (от Python 3.6 насам)

"The %s programming language was created in %d by %s." % ("C", 1972, "Dennis Ritchie")
'The C programming language was created in 1972 by Dennis Ritchie.'

str.format:

from math import pi

"{} only knows the first 5 decimal places of π: {:.5f}.".format("MC Ton4ou", pi)
'MC Ton4ou only knows the first 5 decimal places of π: 3.14159.'
chinese_template = "由于{reason}{subject}被屏蔽"
english_template = "{subject} is blocked due to {reason}"

subject = "@gosho_the_dove"
reason = "spam"

print(chinese_template.format(subject=subject, reason=reason))
print(english_template.format(subject=subject, reason=reason))
由于spam,@gosho_the_dove被屏蔽
@gosho_the_dove is blocked due to spam

f-strings:

from time import time

name = "bot.py"

f"Program {name} finished execution at {time():.2f} unix time."
'Program bot.py finished execution at 1726320488.99 unix time.'
some_variable = 0xdeadbeef

f"Debug info: {some_variable=}"  # the '=' here puts the name as well as the value
'Debug info: some_variable=3735928559'

Низовете са колекции и могат да бъдат използвани като такива. Повече за тях в следваща лекция, засега може да ги мислим като масиви от символи.

s = "Hello"

length = len(s)     # the length
first_char = s[0]  # the first character (element @ index 0)

print(f"{length=}, {first_char=}")

for char in s:
    print("===" + char + "===")
length=5, first_char='H'
===H===
===e===
===l===
===l===
===o===

Обработката на низове става сравнително лесно чрез предоставените ни вградени функции и методи:

replace:

"Deutschland über alles!".replace("ü", "ue")
'Deutschland ueber alles!'

split:

post_hastags = "#nofilter #nomakeup #feltcutemightdeletelater #f4f #follow4follow"

# first remove all '#', then split using a space as a delimiter into a list of strings
# (those operators can be chained)
tags_list = post_hastags.replace("#", "").split(" ")

# `tags_list` is of type `list` - more on this type later
tags_list
['nofilter', 'nomakeup', 'feltcutemightdeletelater', 'f4f', 'follow4follow']

join:

# `join` accepts an iterable (like a `list`) as an argument
# and glues all the elements together using the string it's called on
"; ".join(tags_list)
'nofilter; nomakeup; feltcutemightdeletelater; f4f; follow4follow'

lower, upper, capitalize:

"SpOnGeBoB squarepants".lower()
'spongebob squarepants'
"SpOnGeBoB squarepants".upper()
'SPONGEBOB SQUAREPANTS'
"SpOnGeBoB squarepants".capitalize()
'Spongebob squarepants'

Булеви#

Тип bool има две възможни стойности: True и False.

За разлика от повечето други езици, вместо !, || и && използваме not, or и and. Както си и трябва.

t = True
f = not t

f
False
not f
True
t or f
True
t and f
False
t and not f
True

⚠️ or и and всъщност не връщат винаги bool, а стойността на първия операнд отляво надясно, след който каквито и да са стойностите на другите резултатът остава същия:

1 or 2 or 3
1
1 and 2 and 3
3
1 or 0 or 3
1
1 and 0 and 3
0

Можем да се възползваме от това в кода ни с изрази и функции, чието изпълнение зависи от предходния резултат:

def func1():
    print("Function 1 is being executed.")
    return True

def func2():
    print("Function 2 is being executed.")
    return True

if func1() or func2():
    print("OK")
Function 1 is being executed.
OK

None#

None е стойност (и тип), обозначаваща липса на стойност (🤔).

Note: Когато една функция не преминава през return израз, тя всъщност връща None като резултат.

result_of_print = print("The print function isn't intended to return a specific value.")
print(result_of_print)
The print function isn't intended to return a specific value.
None

type#

type е функция, която ни връща типа на дадена стойност:

type(42)
int

Но всичко в Python е обект, включително и функциите:

type(print)
builtin_function_or_method

Следователно, type също си има тип:

type(type)
type

😵‍💫

type(type(type(type(type(type)))))
type

tuple, list, set, dict: четирите вградени колекции#

  • tuple: наредена \(n\)-торка (още кортеж), която не може да се променя (immutable)

  • list: списък от елементи, който може да се променя (mutable)

  • set: множество от елементи без наредба и повторения (HashSet) (mutable)

  • dict: речник от ключове и стойности (HashMap) (mutable)

Елементите и на четирите колекции могат да бъдат от различни типове.

t = (1, "b", False)  # tuples are with () or without any brackets
t
(1, 'b', False)
t = 1, "b", False
t
(1, 'b', False)
l = [1, "b", False]  # lists are with []
l
[1, 'b', False]
s = {1, "b", False}  # sets are with {}
s
{1, False, 'b'}
d = {"a": 1, "b": 2, "c": 3, "幸": None}  # dicts are also with {}
d
{'a': 1, 'b': 2, 'c': 3, '幸': None}

⚠️ {} е празен dict, а не празен set. За празен set се ползва конструктора set().

type({})
dict

⚠️ Понеже нормалните скоби са и… нормални скоби, които обособяват израз и връщат стойността му, tuple с един елемент се дефинира със запетайка след елемента. Освен това няма нужда от обграждащи скоби при задаване на tuple с повече от нула елементи.

one_element_tuple = (42,)
one_element_tuple
(42,)
one_element_tuple_again = 42,
one_element_tuple_again
(42,)

Въпрос: Как да създадем празен tuple? 🤔

Кога кое да ползваме? Rule of thumb:

  • tuple - когато искаме функция да върне няколко наредени неща / списък, който не може да бъде променян и може да бъде хеширан / анонимен dataclass (проста структурка с няколко елемента)

  • list - когато искаме нареден списък, който да може да бъде променян и може да има повторения

  • set - когато наредбата не ни трябва и нямаме повторения / когато искаме да проверим за принадлежност на елемент към множество (\( O(1) \) vs. \( O(n) \) за list)

  • dict - когато искаме да асоциираме дадени ключове с дадени стойности

Ключовата дума in е полезна при работа с такива колекции. Тя връща bool който ни казва дали даден елемент се среща вътре:

impostor = 1j
squad = [0, 1j, 2, 3, 4, 5]

impostor in squad
True
members = {"A": 0, "B": 1j, "C": 2, "D": 3, "E": 4, "F": 5}

# `in` searches in the dictionary's keys, not values
impostor in members
False
impostor in members.values()
True
impostor in members.keys()
False

in може да се ползва и за str:

"mile" in "smiles"
True

Дължината им се взима с len:

len([0, 1, 2])
3
len((0, 1, 2))
3
len({1, 1, 1, 1, 1, 2, 3})
3
len("abracadabra")
11

tuple и list поддържат индексиране, като за tuple това е само read-only:

t = ("a", "b", "c")
t[0]
'a'
l = ["a", "b", "c"]
l[0]
'a'
l[0] = "A"
l
['A', 'b', 'c']
t[0] = "A"  # 💥
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[77], line 1
----> 1 t[0] = "A"  # 💥

TypeError: 'tuple' object does not support item assignment

По същия начин става взимането/записването на стойност в dict. Съществува и метода get който ни предоставя по-безопасен начин при достъпването на несъществуващи ключове:

d = {}  # an empty dictoinary
d
{}
d["a"] = 1
d
{'a': 1}
d["a"]
1
d.get("a")
1
print(d.get("b"))  # using `print`` because Jupyter doesn't show `None` as output
None
d.get("b", "default value")
'default value'
d["b"]  # 💥
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
Cell In[84], line 1
----> 1 d["b"]  # 💥

KeyError: 'b'

Pro tip: collections.defalutdict е dict със зададена стойност по подразбиране, която се връща при липсващ ключ.

Mutable vs Immutable#

Имената в Python сочат към някаква стойност в паметта.

name = 1
name += 1000

name
1001

Горното парче код не променя стойността на 1, а кара name да сочи към нова стойност - 1001.

Това е така, понеже int е immutable тип (такъв който не може да си променя стойността). Immutable са още всички числа, низове, tuple, True, False, None и т.н.

Mutable типовете пък са list, set, dict и почти всичко останало.

a = 1
b = a
b += 1

print("a = ", a)
print("b = ", b)
a =  1
b =  2
l1 = [1, 2]
l2 = l1
l2.append(3)

print("l1 = ", l1)
print("l2 = ", l2)
l1 =  [1, 2, 3]
l2 =  [1, 2, 3]

Важно е да се отбележи също, че валидни ключове за dict и елементи за set са само immutable стойности. По-точно, тези, които имат имплементация на хеширане (методът __hash__).

l = [1, 2]
d = {l: 1}  # 💥
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[88], line 2
      1 l = [1, 2]
----> 2 d = {l: 1}  # 💥

TypeError: unhashable type: 'list'

Unpacking#

Unpacking-ът ни позволява да извлечем всички елементи от колекции в няколко различни други имена.

firstname, lastname = "Вовеки", "Веков"

tup = 1, 2, 3
a, b, c = tup

print(f"{firstname=}, {lastname=}")
print(f"{a=}, {b=}, {c=}")
firstname='Вовеки', lastname='Веков'
a=1, b=2, c=3

Pro tip: размяната на стойностите на две променливи в Python не изисква да дефинираме трета:

a, b = 1, 2
a, b = b, a

print(f"{a=}, {b=}")
a=2, b=1

За да посочим име, в което да се присвоят всички останали неприсвоени елементи от колекцията, която unpack-ваме, използваме астериск * (няма нищо общо с pointer, спокойно). Типът на такава променлива винаги става list.

head, *tail = [1, 2, 3, 4, 5]
first, *inbetween, last = 1, 2, 3, 4, 5, 6, 7, 8, 9, 10

print(f"{head=}, {tail=}")
print(f"{first=}, {inbetween=}, {last=}")
head=1, tail=[2, 3, 4, 5]
first=1, inbetween=[2, 3, 4, 5, 6, 7, 8, 9], last=10

Това може да бъде приложимо при слепване на колекции, като за dict ползваме **:

original_ingredients = ["water", "salt", "sugar"]
new_ingredients = ["flour", "eggs", "butter", *original_ingredients]

new_ingredients
['flour', 'eggs', 'butter', 'water', 'salt', 'sugar']
studio_band = {"GUITAR": "Vasko", "MICROPHONE": "Ceca"}
live_band = {"BASS": "Pesho", "DRUMS": "亚历山大", **studio_band}

live_band
{'BASS': 'Pesho', 'DRUMS': '亚历山大', 'GUITAR': 'Vasko', 'MICROPHONE': 'Ceca'}

Конвертиране между типове#

В C/C++/Java трябва да ползваме atoi, atof, strtol, strtof, Integer.parseInt, Float.parseFloat и т.н. за конвертиране на числа от низове и обратно.

В С/С++/С# имаме и синтаксис от рода на (int)..., (float)..., (string)... и т.н.

Тук няма нужда от това.

Просто използваме конструкторите на типовете (да припомним, че в Python всичко е обект, и си има конструктор):

str(42)
'42'
int("42")
42
int(42.69)
42
float(42)
42.0
bool(42)
True
bool(0)
False
list("абвгдежзийклмнопрстуфхцчшщъьюя")
['а',
 'б',
 'в',
 'г',
 'д',
 'е',
 'ж',
 'з',
 'и',
 'й',
 'к',
 'л',
 'м',
 'н',
 'о',
 'п',
 'р',
 'с',
 'т',
 'у',
 'ф',
 'х',
 'ц',
 'ч',
 'ш',
 'щ',
 'ъ',
 'ь',
 'ю',
 'я']
tuple([1, 2, 3])
(1, 2, 3)
list((1, 2, 3))
[1, 2, 3]
set([1, 1, 1, 2, 3, 3, 3, 3, 3])
{1, 2, 3}
dict([("a", 1), ("b", 2), ("c", 3)])
{'a': 1, 'b': 2, 'c': 3}

Блокове#

В Python блоковете от код се отделят не чрез къдрави скоби, а с индентация.

За съжаление на хората, предпочитащи скоби, това няма да се промени в бъдещи версии на езика:

from __future__ import braces
  Cell In[105], line 1
    from __future__ import braces
    ^
SyntaxError: not a chance

Важно е да се отбележи, че всеки блок започва след двуеточие и всеки негов ред започва с определен брой интервали (или една табулация) по-навътре от предходния блок. За да бъде валдна индентацията трябва да е консистентна в целия файл, в противен случай ще се хвърли IndentationError. Общоприето е в Python да си използват 4 интервала за тази цел.

if True:
    print("This line is ok")
        print("But this one is not")
  Cell In[106], line 3
    print("But this one is not")
    ^
IndentationError: unexpected indent

Side note: едноредови блокове могат да се запишат и на същия ред след двуеточието. Често срещана конвенция обаче е да не се пишат по този начин.

if True: print("This will run")
This will run

Контролни структури#

if#

age = 21

if age == 18:
    print("Barely legal")
elif age < 18:
    print("Sorry, you are not allowed.")
else:
    print("You good.")
You good.

Няма нужда от скоби около условията (и не пишете такива, за да не ви се караме за глупости).

Едно условие се оценява на False, когато стойността му е False, None, 0, 0.0, 0j, '', b'', [], {} и всички празни контейнери. Всички други стойности се оценяват до True.

Съществува и едноредова версия на if с else, която има за цел да служи като тернарния оператор в повечето езици:

num = 420
statement = "odd" if num % 2 == 1 else "definitely not odd"

statement
'definitely not odd'

Операторите за сравнение са ==, !=, <, >, <=, >=. Съществува и <>, който е просто различен запис на !=.

В Python е възможно комбинирането им. Например 1 < 2 and 2 < 3 може да бъде записано като 1 < 2 < 3.

code = 404

if 100 <= code < 200:
    print("Informational response")
elif 200 <= code < 300:
    print("OK response")
elif 300 <= code < 400:
    print("Redirection")
elif 400 <= code < 500:
    print("Client error")
elif 500 <= code < 600:
    print("Server error")
else:
    print("Not a valid HTTP status code")
Client error

Ключовата дума is сравнява по референция, докато == сравнява по стойност:

a = [1, 2, 3]
b = [1, 2, 3]

if a == b:
    print("a and b are equal")
else:
    print("a and b are not equal")

if a is b:
    print("a points to the same thing as b")
else:
    print("a does not point to the same thing as b")
a and b are equal
a does not point to the same thing as b

Side note: сравнение с None може да бъде направено и по двата начина, но за четимост се ползва по-често is.

result = None

if result is not None:  # clear 'nuff, innit 
    print(f"We got a result, it is {result}.")
else:
    print("No result.")
No result.

⚠️ Трябва да се внимава винаги с очакваната прецизност на числата с плаваща запетая:

if 0.1 + 0.2 == 0.3:
    print("Естествено, че ще влезе тука")
else:
    print(f"Да, ама не: 0.1 + 0.2 == {0.1 + 0.2}")
Да, ама не: 0.1 + 0.2 == 0.30000000000000004

while#

num = 0
while num < 20:
    num += 1
    current_output = f"{num}: "

    if num % 3 == 0 and num % 5 == 0:
        current_output += "FizzBuzz"
    elif num % 3 == 0:
        current_output += "Fizz"
    elif num % 5 == 0:
        current_output += "Buzz"

    print(current_output)
1: 
2: 
3: Fizz
4: 
5: Buzz
6: Fizz
7: 
8: 
9: Fizz
10: Buzz
11: 
12: Fizz
13: 
14: 
15: FizzBuzz
16: 
17: 
18: Fizz
19: 
20: Buzz

continue прекъсва изпълнението на текущaта итерация и продължава със следващата.

break прекъсва изпълнението на цикъла.

n = 0
while True:
    if n > 100:
        break

    n += 1

    if n % 3 != 0 or n % 5 != 0:
        continue
    
    print(n)
15
30
45
60
75
90

Възможно е да се напише и else блок след тялото на while-a. Тогава той ще се изпълни само ако условието в while-a стане False без да бъде прекъсвано изпълнението на цикъла чрез break или return:

# try re-running the cell with and without "queen" in the list
stekchi = ["king", "emperor", "tsar", "queen", "kaiser", "sultan", "pharaoh", "khan"]

while stekchi:  # while the list is not empty
    last_element = stekchi.pop()  # pop() both removes and returns an element, by default the last one

    if last_element == "queen":
        print("I want to break free!")
        break

    print(f"There is a {last_element}.")
else:
    print("No queen found.")
There is a khan.
There is a pharaoh.
There is a sultan.
There is a kaiser.
I want to break free!

for#

for-циклите в Python итерират върху дадена колекция:

ingredients = ["eggs", "milk", "flour", "sugar"]

for i in ingredients:
    print(f"I will need {i}")
I will need eggs
I will need milk
I will need flour
I will need sugar
ingredients_price = {"eggs": 0.98, "milk": 1.23, "flour": 1.59, "sugar": 0.88}

for ingr, price in ingredients_price.items():  # IMPORTANT: .items() returns a tuple of (key, value)
    print(f"We got {ingr} for ${price}")
We got eggs for $0.98
We got milk for $1.23
We got flour for $1.59
We got sugar for $0.88

Tip: Питонският начин при обхождане да боравим и с индекса, и със самата стойност на колекцията, е чрез enumerate:

for i, ingr in enumerate(ingredients):  # enumerate() returns a tuple of (index, element)
    print(f"Ingredient {i+1}: {ingr}")
Ingredient 1: eggs
Ingredient 2: milk
Ingredient 3: flour
Ingredient 4: sugar

range ни позволява да създадем колекция от числа от началото до края на даден интервал през определена стъпка:

for i in range(20):
    print(i, end=" ")
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 
for i in range(10, 20):
    print(i, end=" ")
10 11 12 13 14 15 16 17 18 19 
for i in range(10, 20, 3):
    print(i, end=" ")
10 13 16 19 
for i in range(20, 10, -3):
    print(i, end=" ")
20 17 14 11 

По същия начин както при while-циклите, else може да бъде използван и с for:

for n in range(2, 20):
    for x in range(2, n):
        if n % x == 0:
            print(f"{n} = {x} * {n // x}")
            break
    else:
        print(f"{n} is a prime number")
2 is a prime number
3 is a prime number
4 = 2 * 2
5 is a prime number
6 = 2 * 3
7 is a prime number
8 = 2 * 4
9 = 3 * 3
10 = 2 * 5
11 is a prime number
12 = 2 * 6
13 is a prime number
14 = 2 * 7
15 = 3 * 5
16 = 2 * 8
17 is a prime number
18 = 2 * 9
19 is a prime number

match (version 3.10+ !!!)#

В Python switch-case нарочно няма. От версия 3.10 насам обаче съществува match, което е по-скоро подобно на switch или match във функционалните езици за програмиране. С него можем да съпоставяме структурата на изрази в различни случаи, които ни интересуват, елиминирайки нуждата от вложени if-ове, проверки за типа на променливи, сложни проверки за размерности и структура на колекции и др. Това е мощно ново свойство на езика, за което повече информация може да намерим тук.

Функции#

def f(x):
    return x ** x

f(8)
16777216

def е ключовата дума, която започва дефинията на функция. След името и списък от аргументите в скоби, следва блок с код, който се изпълнява при извикването ѝ.

При достигане на return или крaя на блокът от код, изпълнението на функцията прекъсва. Във втория случай или когато след return няма нищо посочено, функцията връща None.

def procedure():
    print("I am Procedure.")
    print("I shall be the reigning lord of the kingdom of side effects.")
    print("I don't return anything.")
    print("...Or do I?")

result = procedure()
print(result)
I am Procedure.
I shall be the reigning lord of the kingdom of side effects.
I don't return anything.
...Or do I?
None
def greeting(name):
    if not name:
        return
    
    print(f"Hello, {name}!")

greeting("")
greeting("John Smith")
Hello, John Smith!

“Можем и рекурсия, ако можем рекурсия” - Валери Божинов

(btw GitHub Copilot каза, че е от тука, което е close enough (are we obsolete already dammit))

def factorial(n):
    if n <= 0:
        return 1
    
    return n * factorial(n - 1)


factorial(69)  # https://www.youtube.com/watch?v=kw6l_uTakRA
171122452428141311372468338881272839092270544893520369393648040923257279754140647424000000000000000

Аргументите на функциите могат да имат стойности по подразбиране:

def add(a, b=0):
    return a + b
add(1)
1
add(1, 2)
3

Аргументите на фунцкиите в Python могат да се подават по два начина:

  1. позиционeн (positional) (определени от реда, в който са подадени)

  2. именован (keyword) (определени от името на аргумента, следвано от =)

След именован аргумент не можем да подадем позиционен.

def my_custom_print(text="", terminator="\n", capitalize=False):
    new_text = text.capitalize() if capitalize else text
    print(new_text, end=terminator)

my_custom_print()
my_custom_print("hello")
my_custom_print("hello", capitalize=True)
my_custom_print("hello", "!\n", capitalize=True)
my_custom_print("hello", terminator="!\n", capitalize=True)
my_custom_print("hello", capitalize=True, terminator="!\n")
my_custom_print(capitalize=True, terminator="!\n", text="hello")
# my_custom_print(capitalize=True, "\t")  # 💥 positional argument can't follow keyword argument
hello
Hello
Hello!
Hello!
Hello!
Hello!

Можем да укажем функцията да приема произволен брой позиционни и именовани аргументи, като използваме * и ** съответно (споко, това отново няма нищо общо с пойнтъри!).

*args и **kwargs са само конвенция, можем да използваме каквито и да е имена, но е важно да се отбележи, че * и ** са задължителни.

*args е tuple с всички позиционни аргументи, които не са били консумирани от другите аргументи.

**kwargs е dict с всички именовани аргументи, които не са били консумирани от другите аргументи.

def variadic_args(fixed_arg1, fixed_arg2, *args, **kwargs):
    print(f"{fixed_arg1=}")
    print(f"{fixed_arg2=}")
    print(f"{args=}")
    print(f"{kwargs=}")

variadic_args(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, name="Pesho", age=20)
fixed_arg1=1
fixed_arg2=2
args=(3, 4, 5, 6, 7, 8, 9, 10)
kwargs={'name': 'Pesho', 'age': 20}

Pro tip: Можем да забраним позиционните или именованите аргументи преди/след дадено място чрез вмъкване на / или * в сигнатурата на функцията. Как точно:

Ако имаме

def func(a, b, c, /, d, e, f, *, g, h, i):
    ...

това означава, че:

  • a, b и c могат да бъдат подадени само позиционно

  • d, e и f могат да бъдат подадени както позиционно, така и именовано

  • g, h и i могат да бъдат подадени само именовано

def submit_experiment(experiment_name, *, environment_name, user_name):
    # everything after the * MUST BE provided as a keyword argument
    print(f"Experiment {experiment_name} submitted by {user_name} in {environment_name} environment.")

submit_experiment("GPT-6", environment_name="Azure ML", user_name="OpenAI")
submit_experiment("BgGPT", user_name="INSAIT", environment_name="unknown")  # notice the order
submit_experiment("LandcoreGPT", "FMI", "AJ")  # 💥
Experiment GPT-6 submitted by OpenAI in Azure ML environment.
Experiment BgGPT submitted by INSAIT in unknown environment.
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[134], line 7
      5 submit_experiment("GPT-6", environment_name="Azure ML", user_name="OpenAI")
      6 submit_experiment("BgGPT", user_name="INSAIT", environment_name="unknown")  # notice the order
----> 7 submit_experiment("LandcoreGPT", "FMI", "AJ")  # 💥

TypeError: submit_experiment() takes 1 positional argument but 3 were given
def c_function(_a, _b, _c, /):
    return _a + _b + _c

print(c_function(1, 2, 3))
print(c_function(1, 2, _c=3))  # 💥
6
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[135], line 5
      2     return _a + _b + _c
      4 print(c_function(1, 2, 3))
----> 5 print(c_function(1, 2, _c=3))  # 💥

TypeError: c_function() got some positional-only arguments passed as keyword arguments: '_c'

Обхвати на видимост#

Всякo име може да бъде свързанo със стойност (binding). Съществуват операции, които променят свързването, като =.

Свързването на име със стойност може да се проверява с две функции, връщащи речници: locals и globals, които пазят стойностите на всички имена в локалния и в глобалния обхват на видимост съответно.

Едно име дефинирано в локален обхват (scope) не се вижда от глобалния такъв:

global_one = 1

def foo():
    local_one = 2
    print(locals())


foo()
print(globals()["global_one"])  # `print(globals())` will not have a very pretty output in the Jupyter notebook but you can try it
print(globals()["local_one"])
{'local_one': 2}
1
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
Cell In[136], line 10
      8 foo()
      9 print(globals()["global_one"])  # `print(globals())` will not have a very pretty output in the Jupyter notebook but you can try it
---> 10 print(globals()["local_one"])

KeyError: 'local_one'

Всеки блок от код си има своя област на видимост, в която стоят локално дефинираните имена. Ако една функция не може да намери дадена променлива в локалния си scope, търси в обграждащия (глобалния) за променлива със същото име:

global_one = 1

def foo():
    print(global_one)

foo()
1

По подразбиране пренасочването на имена става в локалния scope. Използването на ключовата дума global позволява пренасочването на глобални имена. Това обаче въобще не е добра практика.

global_one = 1

def foo():
    global_one = 2
    print(global_one)
    print(locals())

foo()

print(globals()["global_one"])
2
{'global_one': 2}
1

Аргументите на функциите отиват в locals речника, достъпен от тялото им (съответно не и извън него):

def fn(x, y, *args, **kwargs):
    print(locals())

fn(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, name="Pesho", age=20)
{'x': 1, 'y': 2, 'args': (3, 4, 5, 6, 7, 8, 9, 10), 'kwargs': {'name': 'Pesho', 'age': 20}}

В случай, че имаме вложени блокове, ключовата дума nonlocal позволява пренасочване на име, дефинирано в обграждащ блок (за разлика от global, където името е дефинирано в глобалния обхват). Също както global, изполването му е лоша практика.

Pro tip: няма нужда да “декларираме” празна променлива преди даването ѝ на стойност в if (стига всички пътища да дават стойност), например:

def some_function(x):
    # няма нужда тука преди if-a да пишем например
    # result = ""

    if x:
        result = f"{x} is truethy"
    else:
        result = f"{x} is falsy"

    return result

print(some_function([]))
print(some_function([0]))
[] is falsy
[0] is truethy

Задачи#

Не носят точки, но са полезно упражнение.

Задача 1#

Създайте функция, която приема число, указващо брой секунди и го връща трансформирано в наредена тройка от часове, минути и секунди.

Например

  • 3661 -> (1, 1, 1), понеже 3661 секунди са 1hr 1min 1sec.

  • 86399 -> (23, 59, 59), понеже 86399 секунди са 23hr 59min 59sec. и т.н.

def seconds_to_time(seconds):
    pass  # write your code here


print(seconds_to_time(0) == (0, 0, 0))
print(seconds_to_time(1) == (0, 0, 1))
print(seconds_to_time(69) == (0, 1, 9))
print(seconds_to_time(420) == (0, 7, 0))
print(seconds_to_time(3661) == (1, 1, 1))
print(seconds_to_time(86399) == (23, 59, 59))

Задача 2#

Създайте функция, която да връща броя на гласните в даден текст (нека за улеснение считаме за гласни само a, e, i, o, u).

def number_of_vowels(text):
    pass  # write your code here


print(number_of_vowels("grrrrgh!") == 0)
print(number_of_vowels("The quick brown fox jumps over the lazy dog.") == 11)
print(number_of_vowels("MONTHY PYTHON") == 2)

Задача 3#

Създайте функция, която проверява дали дадено българско ЕГН е валидно.

По-конкретно, функцията трябва:

  • да има за първи задължителен параметър ЕГН-то (очаква се да е str, но може да се опитате и да го направите да работи едновременно и за str, и за int).

  • да има именован параметър bypass_checksum, който по подразбиране е False. Ако е True, функцията трябва да НЕ проверява валидността на последната цифра, а да я счита за валидна по подразбиране.

  • да връща True ако ЕГН-то е валидно, False в противен случай. За валидно ЕГН се смята такова, при което:

    • цифрите за месец и ден съответстват на валидна дата. Това означава цифрите на месеца да са в интервала [1, 12] за хора, родени 1900-2000г. и в интервала [41, 52] за хора, родени след 2000г., като пожелание може да проверявате и за интервала [21, 32] за хора, родени преди 1900г.

    • Ако bypass_checksum e False, то последната 1 цифра трябва да е валиден checksum на останалите. Алгоритъмът за изчисляване на последната цифра може да намерите тук.

def is_valid_UCN(ucn, *, bypass_checksum=False):
    pass  # write your code here

print(is_valid_UCN("6101057509") == True)
print(is_valid_UCN("6101057500", bypass_checksum=True) == True)
print(is_valid_UCN("6101057500") == False)
print(is_valid_UCN("6913136669") == False)