Променливи, разклонения, цикли, типове, функции#
Динамично 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
eFalse
, то последната 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)