Open In Colab

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

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

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

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

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

# Actually l... You know what? Screw it. I don't need it.
del a
print("a = ", a)  # 💥
a =  42
a =  a string
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
/var/folders/7x/gdf0mbts74n8p_ckv_b_mfc40000gp/T/ipykernel_18944/3048814692.py in <module>
      9 # Actually l... You know what? Screw it. I don't need it.
     10 del a
---> 11 print("a = ", a)  # 💥

NameError: name 'a' is not defined

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

Числа#

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

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

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

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

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

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

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

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

print(4 / 2)
print(1 / 3)
2.0
0.3333333333333333

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

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

print(7 / 2)
print(7 // 2)
print(7 % 2)
3.5
3
1

Hey Siri, how much is 0 divided by 0?

print(0 / 0)
---------------------------------------------------------------------------
ZeroDivisionError                         Traceback (most recent call last)
/var/folders/7x/gdf0mbts74n8p_ckv_b_mfc40000gp/T/ipykernel_18944/2444630372.py in <module>
----> 1 print(0 / 0)

ZeroDivisionError: division by zero

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

from math import inf

print(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'

print(s1 + s2 + s1 + '!')
abracadabra!

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

this_text_is_too_long_to_fit_on_a_single_line = """This text is too long to fit on a single line
so I'm going to break it up into multiple lines
so that I can see how it looks
This string has been sponsored by Github Copilot."""

print(this_text_is_too_long_to_fit_on_a_single_line)
This text is too long to fit on a single line
so I'm going to break it up into multiple lines
so that I can see how it looks
This string has been sponsored by Github Copilot.

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

moyai = "🗿🇨🇳"
print("the length of it =", len(moyai))  # 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'
print(s)
Is it readable?
Yesn't

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

rip_lemmy = "A\u2660"
print(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))

alphabet = [chr(c) for c in range(ord('A'), ord('Z') + 1)]
alphabet_kebab = "-".join(alphabet)
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."
print("bytes_literal =", bytes_literal)

str_from_bytes = bytes_literal.decode("utf-8")
print("str_from_bytes =", str_from_bytes)

bytes_from_str = str_from_bytes.encode("utf-8")
print("bytes_from_str =", bytes_from_str)

deadbeef = b"\xDE\xAD\xBE\xEF"
deadbeef.decode("utf-8")  # it's dead
bytes_literal = b'You can use ASCII characters as well as \\x syntax, e.g. \x00\x01, etc.'
str_from_bytes = You can use ASCII characters as well as \x syntax, e.g. , etc.
bytes_from_str = b'You can use ASCII characters as well as \\x syntax, e.g. \x00\x01, etc.'
---------------------------------------------------------------------------
UnicodeDecodeError                        Traceback (most recent call last)
/var/folders/7x/gdf0mbts74n8p_ckv_b_mfc40000gp/T/ipykernel_18944/3353544668.py in <module>
      9 
     10 deadbeef = b"\xDE\xAD\xBE\xEF"
---> 11 deadbeef.decode("utf-8")  # it's dead

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 насам)

from math import pi
from time import time

archaic = "This year %s turns %x hex-years old." % ("AJ", 24)
print(archaic)

classic = "{} only knows the first 5 decimal places of π: {:.5f}.".format("Ton4ou", pi)
print(classic)

name = "bot.py"
fstr = f"Program {name} finished execution at {time()} unix time."
print(fstr)

some_variable = 0xdeadbeef
debug_fstr = f"Debug info: {some_variable=}"  # the '=' here puts the name as well as the value
print(debug_fstr)
This year AJ turns 18 hex-years old.
Ton4ou only knows the first 5 decimal places of π: 3.14159.
Program bot.py finished execution at 1661079240.858315 unix time.
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===

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

with_umlauts = "Deutschland über alles!"
no_umlauts = with_umlauts.replace("ü", "ue")  # returns a new string in which all occurrences of "ü" are replaced with "ue"
print(f"{no_umlauts=}")

post_hastags = "#nofilter #nomakeup #felt_cute_might_delete_later #f4f #follow4follow"
tags = post_hastags.replace("#", "").split(" ")  # first removes all #'s, then splits using a space as a delimiter into a list of strings
marked_tags_list = " ".join(f"({tag})" for tag in tags)  # surrounds each element of the list with brackets and concatenates them with spaces
print(f"{marked_tags_list=}")

allowed_inputs = ["y", "yes", "proceed", "correct", "da", "yy", "yesh", "yass"]
user_input = "Yes"  # hardcoded for simplicity
if user_input.lower() in allowed_inputs:
    print("Access granted.")
else:
    print("Access denied.")

# .lower() converts the string to lowercase, .upper() converts it to uppercase
# there is also .capitalize() which capitalizes only the first letter of the string
no_umlauts='Deutschland ueber alles!'
marked_tags_list='(nofilter) (nomakeup) (felt_cute_might_delete_later) (f4f) (follow4follow)'
Access granted.

Булеви#

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

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

t = True
f = not t

print(f"{f = }")
print(f"{t or f = }")
print(f"{t and f = }")
print(f"{t and not f = }")
f = False
t or f = True
t and f = False
t and not f = True

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

a, b, c = 1, 2, 3

print(f"{a or b or c = }")
print(f"{a and b and c = }")
print(f"{a or 0 or c = }")
print(f"{a and 0 and c = }")
a or b or c = 1
a and b and c = 3
a or 0 or c = 1
a and 0 and c = 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 е стойност (и тип), обозначаваща липса на стойност (🤔).

Когато една функция не преминава през 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\)-торка (още кортеж), която не може да се променя

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

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

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

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

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

print(f"{t = }")
print(f"{l = }")
print(f"{s = }")
print(f"{d = }")
t = (1, 'b', False)
l = [1, 'b', False]
s = {False, 1, 'b'}
d = {'a': 1, 'b': 2, 'c': 3, '幸': None}

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

type({})
dict

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

a_tuple = (42,)
definitely_not_a_tuple = (42)
print(f"{a_tuple = }")
print(f"{definitely_not_a_tuple = }")

btw_brackets_are_for_plebs = 42,
print(f"{btw_brackets_are_for_plebs = }")
a_tuple = (42,)
definitely_not_a_tuple = 42
btw_brackets_are_for_plebs = (42,)

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

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

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

  • set - когато наредбата не ни трябва и нямаме повторения

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

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

impostor = 1j

squad = [0, 1j, 2, 3, 4, 5]
print(f"{impostor in squad = }")

members = {"A": 0, "B": 1j, "C": 2, "D": 3, "E": 4, "F": 5}  # `in` searches in the dictionary's keys, not values
print(f"{impostor in members = }")
print(f"{impostor in members.values() = }")
impostor in squad = True
impostor in members = False
impostor in members.values() = True

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

"mile" in "smiles"
True

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

len([0, 1, 2])
3

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

t = (1, 2, 3)
l = [1, 2, 3]
print(f"{t[0] = }")
print(f"{l[0] = }")

l[0] = -1
print(f"{l = }")

t[0] = -1  # 💥
t[0] = 1
l[0] = 1
l = [-1, 2, 3]
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
/var/folders/7x/gdf0mbts74n8p_ckv_b_mfc40000gp/T/ipykernel_18944/165187029.py in <module>
      7 print(f"{l = }")
      8 
----> 9 t[0] = -1  # 💥

TypeError: 'tuple' object does not support item assignment

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

d = {}
d["a"] = 1

print(f"{d['a'] = }")
print(f"{d.get('a') = }")

print(f"{d.get('b') = }")     # returns None if not present
print(f"{d.get('b', 0) = }")  # returns the provided default value if not present
print(f"{d['b'] = }")         # 💥
d['a'] = 1
d.get('a') = 1
d.get('b') = None
d.get('b', 0) = 0
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
/var/folders/7x/gdf0mbts74n8p_ckv_b_mfc40000gp/T/ipykernel_18944/1199089680.py in <module>
      7 print(f"{d.get('b') = }")     # returns None if not present
      8 print(f"{d.get('b', 0) = }")  # returns the provided default value if not present
----> 9 print(f"{d['b'] = }")         # 💥

KeyError: 'b'

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

Mutable vs Immutable#

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

name = 1
name += 1000
print(name)
1001

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

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

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

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

a = [1, 2]
b = a
b.append(3)
print(f"{a=}, {b=}")  # if you think they can be different now, you are dead wrong
a=1, b=2
a=[1, 2, 3], b=[1, 2, 3]

Важно е да се отбележи също, че валидни ключове за dict и елементи за set са само immutable стойности.

l = [1, 2]
d = {l: 1}
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
/var/folders/7x/gdf0mbts74n8p_ckv_b_mfc40000gp/T/ipykernel_18944/1443935741.py in <module>
      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]

studio_band = {"GUITAR": "Vasko", "MICROPHONE": "Ceca"}
live_band = {"BASS": "Pesho", "DRUMS": "亚历山大", **studio_band}

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

Блокове#

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

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

from __future__ import braces
  File "/var/folders/7x/gdf0mbts74n8p_ckv_b_mfc40000gp/T/ipykernel_18944/3905450354.py", 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")
  File "/var/folders/7x/gdf0mbts74n8p_ckv_b_mfc40000gp/T/ipykernel_18944/1618609151.py", 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, '', [], (), {} и всички празни контейнери. Всички други стойности се оценяват до True.

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

num = 420
statement = "odd" if num % 2 == 1 else "definitely not odd"
print(f"{num} is {statement}")
420 is 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 is the same thing as b")
else:
    print("a is not the same thing as b")
a and b are equal
a is not 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:

s = "aababcbccbbbabbabccbbaabbacccaababbc"
# s = 1000 * "s"

letters = []
i = 0

while i < len(s):
    current_letter = s[i]
    i += 1

    if i > 100 and len(letters) <= 1:
        print("I want to break free!")
        break

    if letters and current_letter == letters[-1]:
        continue

    letters.append(current_letter)
else:
    print("".join(letters))
ababcbcbababcbabacababc

for#

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

ingredients = ["eggs", "milk", "flour", "sugar"]
ingredients_price = {"eggs": 0.98, "milk": 1.23, "flour": 1.59, "sugar": 0.88}

for i in ingredients:
    print(f"I will need {i}")

for ingr, price in ingredients_price.items():
    print(f"We got {ingr} for ${price}")
I will need eggs
I will need milk
I will need flour
I will need sugar
We got eggs for $0.98
We got milk for $1.23
We got flour for $1.59
We got sugar for $0.88

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

for i in range(20):
    print(i, end=" ")

print()

for i in range(10, 20):
    print(i, end=" ")

print()

for i in range(10, 20, 3):
    print(i, end=" ")

print()

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

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

for n in range(2, 20):
    for x in range(2, n):
        if n % x == 0:
            print(n, '=', x, '*', n // x)
            break
    else:
        print(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

print(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!
def factorial(n):
    if n <= 0:
        return 1
    
    return n * factorial(n - 1)

print(factorial(70))
11978571669969891796072783721689098736458938142546425857555362864628009582789845319680000000000000000

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

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

print(add(1))
print(add(1, 1))
1
2
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")  # y tho
hello
Hello
Hello!
Hello!
Hello!
Hello!

Функции могат да приемат произволен брой аргументи, които биват два типа: позиционни (обикновено кръщавани args) и именовани (обикновено кръщавани kwargs (от англ. keyword arguments)). След именован аргумент не можем да подадем позиционен.

Позиционните стават достъпни като tuple, а именованите - като 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}

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

Всяк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)
/var/folders/7x/gdf0mbts74n8p_ckv_b_mfc40000gp/T/ipykernel_18944/700200652.py in <module>
      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"])
     11 

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, изполването му е лоша практика.

Задачи#

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

Задача 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).

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

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

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

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

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

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