Базова теорія Python. Керування потоком виконання

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті

Привіт, мене звуть Павло Дмитрієв. Я працюю iOS-розробником у компанії Postindustria та маю багаторічний досвід роботи з Python. Нещодавно я вирішив зібрати у серії статей на DOU усю базу, яку потрібно знати, якщо плануєш працювати з цією мовою програмування.

У першій частині описав базову теорію Python. Типи даних. Втім, цілком зрозуміло, що навіть найглибшого розуміння типів даних не достатньо для створення додатків. Тож цілком логічно, що наступною темою в моєму імпровізованому посібнику слід розглянути оператори Python, котрі дозволяють керувати потоком виконання.

Оператор if

Зазвичай, вивчення програмування починається з концепції змінних, а потім йде «головний» метод керування виконанням, умовний оператор.

«Мінімальний» варіант може складатися тільки з гілки if:

from sys import exit

age = int(input("Enter your age: "))
if age < 18:
    print("Too young")
    exit(-1)
        
print("Some alcohol advertisement")

Якщо перевірка умови age < 18 дасть результат True, буде виконано код всередині блоку цього оператора. В нашому випадку — програма завершиться. Якщо ж результатом перевірки буде False, інтерпретатор продовжить виконання з наступної за блоком if команди.

Як і всюди, до блоку if можна додати ще else. У цьому випадку, якщо умова при обчисленні дасть false, буде виконано код з цього блоку.

sanity = input("Are you sane (y/n)? ")
if sanity == "y":
    print("Lucky you")
else:
    print("Welcome to the club")

Найпотужніший варіант цього оператора в Python може мати багато «гілок», кожна з яких починається з оператора elif та містить умову. Якщо умова в першому блоці if має результат False, інтерпретатор по черзі обирає кожну наступну гілку та перевіряє її умову. Якщо умова матиме значення True, буде виконаний код в цій гілці, та інтерпретатор більше не перебиратиме інші elif. Якщо жодна з гілок не мала умови, що була б істинною, то за наявності блоку else будуть виконані команди з нього. Якщо ж і його немає, інтерпретатор просто перейде до наступної за умовним оператором команди.

num = int(input("Enter number: "))
if num > 1:
    print("A lot!")
elif num == 1:
    print("Just one")
elif num < 0:
    print("I don't want negatives")
else:
    print("Zero")

Така гнучкість оператора інколи призводить до його розростання, що шкодить легкості читання коду. Тому додаючи черговий elif краще слідкувати за загальним розміром цього блоку.

Ще одна трохи меньш відома можливість оператору if це те, що його можна використовувати як заміну тернаного оператора.

is_logged_in = True
user = "Jack" if is_logged_in else "Guest"

Порядок трохи відрізняється від традицйного для ?:, тому що умова іде посередені, але свою задачу він виконує так само. Якщо умова істина, то оператор повертає перше значення, інакше — останнє. В нашому випадку значенням змінної стане "Jack".

Оператор match

Кілька версій тому, в Python з’явилася нова потужна альтернатива розгалуженому оператору elif, що прийшла з інших сучасних мов — це оператор зіставлення із шаблоном.

Принцип його роботи доволі простий. На початку стоїть оператор match, який задає вираз, що далі буде зіставлятися із шаблоном. В тілі цього оператора за допомогою низки операторів case перераховуються варіанти шаблонів, з яким зіставляється вираз. Якщо вираз відповідає шаблону, то виконується вже тіло відповідного case та інші шаблони не перевіряються. Ось найпростіший приклад зіставлення.

def handle_keyboard(scancode: str):
    match scancode.lower():
        case "w" | Const.SC_UP:
            print("Going up")
        case "s" | Const.SC_DOWN:
            print("Going down")
        case "a" | Const.SC_LEFT:
            print("Going left")
        case "d" | Const.SC_RIGHT:
            print("Going right")
        case " ":
            print("Jumping")
        case _:
            print("Unknown key")

Зверніть увагу, що ми можемо задавати кілька шаблонів, розділюючи їх |. Також є шаблон _, якому відповідатиме будь-який вираз. Інакше кажучи, case _: — це «місцевий» аналог фінального else, гілка, що буде виконана, якщо жодна з інших не спрацювала.

Якби можливості case полягали б тільки в простому порівнянні, цей оператор був би просто «синтаксичним цукром», але уся його потужність полягає в можливості «захоплювати» частину виразу, що зіставляється у локальні змінні.

class TreeNode:    # 1
    __match_args__ = ("value", "parent", "children")  # 2

    def __init__(self, value, parent=None):
        self.value = value
        self.parent = parent
        self.children = []

    def add_item(self, value):
        child = TreeNode(value, self)
        self.children.append(child)
        return child


def leaf_type(node):
    match node:    # 3
        case TreeNode(_, None, _):
            print("Root")
        case TreeNode(_, _, children) if len(children) > 0:
            print(f"Inner with {len(children)} children")
        case TreeNode(_, _, []):
            print("Leaf")


if __name__ == "__main__":
    root = TreeNode("root")
    child = root.add_item("child")
    sub_child = child.add_item("subchild")

    leaf_type(root)
    leaf_type(child)
    leaf_type(sub_child)

Це доволі великий фрагмент коду, тож давайте його розберемо. На початку (1) ми створюємо доволі просту реалізацію дерева за допомогою класу, який містить посилання на «батьківський» вузол, масив посилань на дочірні вузли та якесь значення. Це, звісно, не дуже ефективна реалізація, але вона підійде нам для ілюстрації.

Для простоти сприйняття я не став наводити анотації типів, хоча в сучасних версіях Python доволі добре описуються навіть типи, що можуть бути використані як дженеріки.

За замовчуванням, клас не придатний для використання в операторі match. Якщо точніше — придатний, але користі з цього буде мало. Щоб змінити цю ситуацію, нам треба вказати, які саме з полів класу беруть участь в зіставленні шаблону. Ми це робимо в (2), присвоюючи імена цих полів як кортеж рядків змінній __match_args__ . Оскільки Python — мова динамічна, цього цілком достатньо і нам не потрібна якась особлива шаблонна магія.

Тепер розберемо безпосереднє зіставлення шаблону (3). Для шаблона що відповідає класу, ми вказуємо його ім’я, а потім в дужках описуємо що саме ми «очікуємо» від тих його полів, які ми обрали для зіставлення. Зрозуміло, що поля у цьому випадку йдуть саме в тому порядку, в якому їх було описано в __match_args__ .

Перший case вказує, що нам не важливі значення value та children — це робиться за допомогою оператора _. Єдине, що ми тут перевіряємо, — чи містить поле parent значення None. Зрозуміло, що в цьому випадку це коріння дерева.

Другий case складніше, бо ми маємо перевірити, що в полі children є принаймні одне значення, що робить наш вузол внутрішнім. Як можна побачити з шаблону, нас не цікавлять значення в полях value та parent. Взагалі, нам треба було б перевірити, що parent не дорівнює None, але тут це не потрібно, бо такі вузли підпадуть під перший шаблон, тому що вони є «кореневими». Тому будь-який вузол, що доходить до другого шаблону, точно не є коренем, і ми можемо зосередитися на перевірці інших умов.

Щоб зробити щось зі значенням будь-якого поля, нам потрібно його захопити в локальну змінну, що ми й робимо, вказуюючи ім’я змінної, у нашому випадку це змінна із передбачуваною назвою children. Зверніть увагу, що в нашому випадку це локальна змінна і вона могла б мати будь-яке ім’я, що не має обов’язково збігатися з іменем поля яке ми «захоплюємо».

Тож при виконанні цього шаблону, ми отримаємо список дочірніх вузлів у локальній змінній і використаємо ще одну можливість шаблонів — додаткову умову. Для цього просто пишемо після шаблону if та вказуємо потрібні умовні оператори. В нашому випадку, це перевірка len(children) > 0. Шаблон буде відповідати значенню, тільки коли додаткові умови теж матимуть значення True. Потім ми можемо зручно скористатися захопленим значенням, щоб надрукувати корисну інформацію про кількість дітей в цього вузла.

Ну і, нарешті, ми перевіряємо, чи дорівнює значення поля класу children порожньому списку. Звісно, ми могли б використати тут і шаблон за замовчуванням, бо якщо вузол не є коренем чи внутрішнім вузлом, то він — є вільним «листом» дерева, але для ілюстрації ми зробимо цю додаткову перевірку.

Виконання цього коду дає такі результати:

Root
Inner with 1 children
Leaf

Взагалі, комбінування потужних можливостей оператора match дозволяє робити деякі перевірки дуже гнучкими. Щоб потренувати розуміння шаблонів, пропоную вам подивитися на наступний приклад, який я зробив навмисно багатослівним, щоб продемонструвати різні аспекти використання match (дуже дякую Copilot за його допомогу). Зверніть також увагу на те, як зіставляються із шаблоном кортежі та словники.

point1 = {"x": 1, "y": 2}
point2 = {"x": 3, "y": 6}

match (point1, point2):
    case ({'x': 0, 'y': _}, {'x': 0, 'y': _}):
        print("Both points are on the Y axis")
    case ({'x': _, 'y': 0}, {'x': _, 'y': 0}):
        print("Both points are on the X axis")
    case ({'x': 0, 'y': 0}, {'x': 0, 'y': 0}):
        print("Both points are at the origin")
    case ({'x': x1, 'y': _}, {'x': x2, 'y': _}) if x1 == x2:
        print("Both points are on the same vertical line")
    case ({'x': _, 'y': y1}, {'x': _, 'y': y2}) if y1 == y2:
        print("Both points are on the same horizontal line")
    case ({'x': x1, 'y': y1}, {'x': x2, 'y': y2}) if x1 == x2 and y1 == y2:
        print("Both points are the same")
    case ({'x': x1, 'y': y1}, {'x': x2, 'y': y2}) if x1/y1 == x2/y2:
        print("Both points are on the same line from 0:0")
    case _:
        print("Points are unrelated")

Цикли

Колись у давні часи усе керування потоком виконання зазвичай зводилось до комбінації команд перевірки умов та переходів за адресою. І якщо вам потрібен був цикл, ви просто перевіряли умову, якщо вона відповідала бажаному, ви переходили на код за циклом, а якщо ні — виконання просто йшло далі, допоки не зустрічався зворотний перехід на перевірку умов.

З цими двома операторами доволі просто було скласти будь-який цикл: з постумовою, з передумовою, зі змінною-лічильником тощо. Але зараз ми маємо мови програмування високого рівня, і Python пропонує нам два зручні цикли: while та for.

Почнемо з while — це класичний цикл з передумовою. Спочатку перевіряться булевий вираз в рядку while, якщо він дорівнює True, виконується тіло циклу. Ось простий приклад, знайомий кожному програмісту:

a = 1
b = 1
while b <= 10:
    a *= b
    b += 1

Очікувано всередині циклу в нас працюють два спеціалізовані оператори. break завершує виконання циклу та одразу переходить на операторів, що йдуть за ним. continue завершує виконання поточної ітерації (весь код за ним не буде виконуватися), та знов повертає керування на перевірку умови на початку циклу, і якщо вона все ж істинна, виконується нова ітерація циклу. Таким чином ми можемо легко сконструювати цикл з постумовою.

with open("main.py", "r") as in_file:
    while True:
        line = in_file.readline()
        print(line, end="")
        if not line:
            break

Про керування контекстом та оператор with ми говоритимемо вже в наступному розділі, поки що нас цікавить тільки те, що цей рядок відкриває файл з диска для читання та зберігає його хендлер в змінній in_file. Далі в нас іде нескінченний цикл while, в якому ми читаємо рядок із файлу, виводимо його на екран (без виводу символу \n наприкінці, як це робить print за замовчуванням), а потім перевіряємо, чи не є рядок пустим (це означатиме, що файл закінчився).

Якщо ця перевірка істинна, ми завершуємо цикл. Зазвичай, логічніше було б розмістити перевірку перед виконанням print, але це б вже не був цикл з передумовою. Тож інколи цикли з інтерумовою бувають доцільнішими.

Одна дуже цікава особливість циклів у Pyhon — це можливість мати гілку else, її буде виконано тільки у тому випадку, коли цикл завершився, вичерпавши себе (коли умова стала False), тобто він не буде виконаний у випадку завершення через break або, наприклад, return. Візьмемо для прикладу ще один усім знайомий алгоритм.

def binary_search(arr, x):
    low, high = 0, len(arr) - 1

    while low <= high:
        mid = (low + high) // 2

        if arr[mid] < x:
            low = mid + 1
        elif arr[mid] > x:
            high = mid - 1
        else:
            return mid
    else:
        return None  # Element was not found

В цьому випадку «нормальне» закінчення циклу означає, що ми не знайшли потрібний елемент, і блок else циклу як раз і виконує необхідні дії.

Своєю чергою, цикл for в Python відрізняється від інших мов, він призначений тільки для ітерування за будь-яким значенням, що реалізує потрібні для цього методи. Розглянемо приклад:

words = ["defenestration", "decapitation", "immolation"]
for word in words:
    print(word, len(word))

Його виконання дасть нам наступний результат:

defenestration 14
decapitation 12
immolation 10

Тож оператор for бере перше значення зі свого другого параметру, присвоює його змінній, що вказана першим параметром і виконує своє тіло. Потім бере друге, третє і так далі до повного вичерпання цього джерела. Оскільки в Python багато типів даних мають необхідну для ітерації семантику, то ітеруватися можна за списками, кортежами, словниками, множинами, відкритими файлами, за вибіркою з бази даних тощо.

Цикл for також підтримує оператори break та continue, а ще може містити гілку else. Усе це працює так само, за винятком того, що виклик continue призводить не до перевірки умови, а до спроби отримати наступне значення з об’єкта, за яким ми ітеруємося.

students = ["Bohdan", "Vanya", "Ivanna", "Krzysztof"]
for student in students:
    if student == "Krzysztof":
        print("Krzysztof is in the class")
        break
    elif student == "Vanya":
        print("Skipping Vanya...")
        continue
else:
    print("Krzysztof is not in the class")

Менеджери контексту

В програмуванні дуже часто зустрічається простий патерн керування ресурсами: отримати ресурс, щось з ним зробити, після цього (або у випадку помилки) — звільнити ресурс. Ми, звісно, ще не говорили про обробку винятків, але наведу простий приклад, як це робиться за допомогою try…except. Для прикладу візьмемо найпростіший ресурс — відкритий файл.

out_file = open("test.txt", "w")
try:
    out_file.write("some data")
except Exception as ex:
    print(f"Error while writing to the file: {ex}")
finally:
    out_file.close()

Оскільки цей підхід вживається дуже часто, в Python можна спрощувати це за допомогою менеджерів контексту. Контекст — фрагмент коду, який ми «огортаємо» менеджером, що має два методи: вхід та вихід. Вхід виконується перед виконанням коду, що міститься у контексті. Під час обробки входу менеджер контексту відкриває ресурс, потрібний огорнутому коду. Вихід за будь-якої умови виконується якщо код в контексті припинив виконання. Це може бути добігання до кінця або вихід через виняток, у будь-якому випадку менеджер гарантовано отримає можливість для закриття ресурсу.

Ось приклад, як можна використати менеджер контексту для запису у файл:

with open("test.txt", "w") as out_f:
    out_f.write("some data")

В цьому випадку немає обробки винятку, але файл буде гарантовано закрито. Першим параметром оператору with іде той самий менеджер контексту, другим — локальна змінна, що отримає значення, що повертає обробник входу в контекст. Зазвичай це якийсь ресурс, що відкривається для цього контексту.

Також можливо одночасно використати декілька менеджерів.

with open("input.txt") as in_file, open("output.txt", "w") as out_file:
    copy_files(in_file, out_file)

Менеджери контексту поширені в API різних бібліотек Python, тому що дозволяють чітко відокремити якусь зміну та обмежити час її життя певною ділянкою коду. Ось, наприклад, як бібліотека Decimal використовує менеджер контексту для зміни власних налаштувань.

print(Decimal(1) / Decimal(17))
with localcontext() as ctx:
    ctx.prec = 42
    print(Decimal(1) / Decimal(17))

Про всяк випадок нагадаю, що Decimal — це тип даних, який дозволяє працювати з раціональними числами без втрати точності через бінарне представлення числа. Цей тип даних працює повільніше та займає більше пам’яті, ніж звичайний float, але є незамінним, наприклад, при обробці грошових сум.

У наведеному прикладі ми змінюємо «точність», тобто кількість десятинних позицій у результаті. Менеджер контексту у такому випадку використовується, щоб локалізувати цю змінну та аби вона не вплинула на решту коду. Виконання дасть нам наступний результат:

0.05882352941176470588235294118
0.0588235294117647058823529411764705882352941

Доволі часто менеджери контексту використовують бібліотеки для юніт-тестування, бо там як раз важливо як отримати ресурс, так і «очистити» його потім. Але бувають і цікавіші застосування:

from pytest import raises


with raises(ZeroDivisionError):
    print(1 / 0)


with raises(ZeroDivisionError):
    print("It's ok")

Ось так в бібліотеці pytest можна перевірити, чи викидає код очікуваний виняток. В першій перевірці код буде виконано без якихось побічних ефектів, бо він як раз і дає очікуваний виняток. У другому випадку ми матимемо виняток Failed: DID NOT RAISE <class 'ZeroDivisionError'>.

Тепер про те, коли та як створювати свої менеджери контексту. Відповідь на перше питання доволі проста. Менеджер контексту потрібен тоді, коли у вас є парні операції, які мають «компенсувати» одна одну. Наприклад: відкрити-закрити, змінити-скасувати зміну, створити-видалити, розпочати-припинити тощо.

Найнаочніший метод створення менеджерів контексту — це клас, що матиме два відповідні методи: __enter__ та __exit__. Як ми побачимо далі, це дуже розповсюджений метод створення різних сутностей в Python.

Розглянемо простий приклад: менеджер контексту, що відповідає файлу, відкритому на запис.

class WritableFile:
    def __init__(self, file_path):      # 1
        self.file_path = file_path

    def __enter__(self):                # 2
        self.file_handle = open(self.file_path, "w")
        return self.file_handle

    def __exit__(self, exc_type, exc_val, exc_tb):
        if self.file_handle:            # 3
            self.file_handle.close()


with WritableFile("test.txt") as log_file:
    log_file.write("Application started\n")

Для початку клас має конструктор (1), що приймає як параметр ім’я файлу. Коли виконання доходить до блоку with, його буде виконано першим.

Потім буде виконано «вхід» в наш контекст, для цього буде виконаний код в методі __enter__ (2), а те що він поверне, буде збережено в змінній, що вказана після as. В нашому випадку, ми відкриваємо файл на запис, зберігаємо його хендл в змінній класу, та повертаємо його ж як значення, і воно буде збережено в змінній log_file.

Після цього виконується код всередині блоку, та після його завершення викликається метод __exit__(self, exc_type, exc_val, exc_tb) (3). Він отримує три параметри, які мають не порожні значення у випадку, якщо всередині блоку виник виняток. Код в методі __exit__ отримує клас винятку, його значення та об’єкт, що зберігає його стектрейс.

Метод __exit__ може повертати значення, яке вказує, що Python має далі робити у випадку, якщо в коді всередині with виник виняток. Якщо значення, що повертає __exit__ дорівнює True, інтерпретатор вважатиме, що всередині __exit__ вже виконано обробку винятку, та продовжить виконання коду з рядка, що йде за блоком with. Якщо ж повернути значення False, інтерпретатор продовжить стандартну процедуру пошуку блока обробки вгору за стеком виклику. Але про це ми говоритимемо в іншому розділі.

Тож, якщо ви не хочете, щоб робота з файлом призводила до винятків, а помилки просто виводилися на екран, це можна зробити наступним чином:

def __exit__(self, exc_type, exc_value, exc_tb):
    if self.file_handle:
        self.file_handle.close()    
	if isinstance(exc_value, OSError):
    	print(f"Exception occurred: {exc_type}")
    	print(f"Exception message: {exc_value}")
    	return True

У цьому випадку, ми спочатку закриваємо файл (головна вимога до цього методу — прибрати те, що було створено в __enter__). Потім ми дивимось, чи є наш виняток класом OSError або його нащадком, і у випадку якщо це так, ми просто виводимо інформацію та повертаємо True, сигналізуючи, що ми вжили заходи щодо обробки винятку.

Усі інші винятки, що не належать до OSError ми не обробляємо, в їхньому випадку метод поверне None (стандартну поведінка Python, якщо функція добігла краю, але не вказано якогось значення для повернення). А оскільки None має False семантику, то почнеться стандартна процедура обробки винятку інтерпретатором.

Також можна зробити менеджер контексту, який нічого не передає всередину коду, тоді оператор with записується без частинки as. Наведу приклад:

from time import sleep, perf_counter


class TimeMeasurer:
    def __enter__(self):
        self.start = perf_counter()

    def __exit__(self, exc_type, exc_val, exc_tb):
        print(f"Time elapsed: {perf_counter() - self.start}")


with TimeMeasurer():
    sleep(0.5)

Виконання цього коду дає наступний результат:

Time elapsed: 0.5050897500477731

Як часто буває в Python, існує також альтернативний метод створення менеджерів контексту. Він виглядає компактніше, бо для нього потрібно написати тільки одну функцію. Одразу наведу приклад, як це виглядатиме для нашого WritableFile:

from contextlib import contextmanager


@contextmanager
def writeable_file(path):
    out_f = open(path, "w")
    try:
        yield out_f
    except OSError:
        print("Silencing error")
    finally:
        out_f.close()


with writeable_file("test.txt") as log_file:
    log_file.write("test")

Для початку, нам потрібен декоратор contextmanager зі стандартної бібліотеки contextlib. Про декоратори ми говоритимемо пізніше, а зараз, якщо коротко, — це «обгортка» над функцією, що змінює її поведінку. В нашому випадку — перетворюючи її зі звичайної функції на менеджера контексту, тобто отой клас з методами входу-виходу, що ми розглядали раніше.

Всередині функція доволі зрозуміла. Для початку, якщо потрібно, ми створюємо ресурс, який буде використовуватися в блоці. Потім ми використовуємо оператор yield (ви вже здогадуєтеся, що про нього ми говоритимемо пізніше), щоб «віддати» створений ресурс «нагору» тій обгортці, що для нас зробив декоратор.

На цьому наша функція тимчасово втрачає контроль і починає діяти оператор with, він збереже відкритий файл у змінній log_file і виконає своє тіло. Після завершення керування буде повернуто функції менеджеру і буде виконуватися код після виклику yield.

Якщо при виконанні коду всередині with не було винятків, просто спрацює блок finally та закриє файл. Якщо ж було виключення, то почнеться його обробка. Так само як і в прикладі на основі класу, ми обробляємо тільки OSError («ковтаючи» його), а усі інші можливі типи виключень підлягають звичайній обробці.

Наведу ще один приклад, який бачив в якісь статті щодо тестування в Python. Через те, що Python — динамічна мова програмування, ми можемо навіть підміняти системні функції. Звісно, в реальному коді так робити не треба, для цього навіть є назва, що характеризує ставлення розробників Python до цього: «monkey patching». Для реального коду, звісно, краще використовувати DI. Але в нашому випадку це буде і корисним прикладом менеджерів контексту, і демонстрацією можливостей динамічної мови програмування.

from contextlib import contextmanager
from time import time


@contextmanager
def mock_time(stub_value):
    global time
    system_time = time
    time = lambda: stub_value
    yield
    time = system_time


print(time())


with mock_time(42.0):
    print(time())


print(time())

В тілі нашого менеджера контексту ми для початку вказуємо, що хочемо працювати з ідентифікатором time з глобального простору імен. В нашому випадку це і буде системна функція time(). Далі ми зберігаємо поточне посилання на вбудовану функцію, щоб потім повернути все в початковий стан. І далі ми підміняємо системну функцію нашою власною (тут головне, щоб збіглася сигнатура).

Далі повертаємо керування коду в блоці with, ніякого ресурсу ми йому не передаємо, а після його виконання відновлюємо й повертаємо на законне місце системну функцію. Далі робимо три виклики функції time: до підміни, під час, та після.

В результаті ми отримаємо наступне:

1678108118.691078
42.0
1678108118.6910982

Як бачите, менеджер контексту тут гарантує, що ми «випадково» не забудемо повернути на своє місце важливу системну функцію.

Оператор pass

Останній за порядком, але не за значенням, — оператор, що не робить нічого. Він так і зветься pass. За синтаксисом Python, багато з операторів потребують блок коду, і його відсутність буде помилкою. Наприклад, не може бути функції без тіла, тож якщо ви хочете спочатку зробити «заглушку», а вже згодом додати функціональність, треба вказати в тілі саме оператор pass.

def fix_all_errors_in_db():
    pass	# will do it later
Або ж відомий «антипатерн» Python:
try:
    some_action()
except Exception:
    pass	# we don't care about errors

Так в реальному коді робити теж не треба, бо хтось з колег, хто проведе пару годин, намагаючись зрозуміти, що йде не так — навряд чи буде вам вдячний.

👍ПодобаєтьсяСподобалось15
До обраногоВ обраному10
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

У коді про дерево коменти 1, 2, 3 починаються чогось з //.

Дякую, Swift постійно намагається вилізти уперед :)

Дякую за фахову статтю👍

Питання про match points: що мається на увазі, коли написано, що точки є on the same line? І коли вони are not related?

що мається на увазі, коли написано, що точки є on the same line?

що вони лежать на одній прямій що також проходить через центр координат

І коли вони are not related?

що між ними немає жодного із зв’яків наведених вище

перша формуліровка дійсно не зовсім коректна в прикладі, але так її запропонував Copilot. зараз подумаю як її сформулювати точніше

Я вважаю що основна складність Python, особливо для новачка — це вбудовані бібліотеки і неочевидні оптимізації накшталт sum(range(N)) яка працює значно швидше ніж якщо ітеруватися списком і додавати поелементно.

ПС
Якщо вже керувати потоком — чому не згадати тернарний оператор?

Чи будете розглядати подібні теми?

Дякую.

Дякую за пропозицію. Моя мінімальна ціль — викласти основи мови. Потім якщо ще буде можливість — взятися за «тонкощі»
Що до Пайтонівської реалізації тернарного оператора, так, мабуть треба згадати. Зараз додам

Статті точно не для новачків, скоріше для систематизації знань, які отримав в процесі користування тим самим пітоном, але хаотично.
Тобто розглядати статтю як «Вступ до Python» я б точно не став.
Але дуже цікаво, наприклад, pattern matching я ще не використовував.
Ну і да, теж чекаю на asyncio просто тому, що зараз його використовую, але більше «інтуітивно», ніж воно того вимагає.

Тут може я не правий, але «базова теорія» для мене не є «вводним курсом», це скоріше щось накшталт матеріалу для досвідченого програміста, що хоче дізнатися про нову мову, або того хто пройшов шлях початківця і хоче систематезувати знання
Щодо match, то його поява для мене була дуже добрим знаком, бо в Swift, що є моєю основною мовою зараз, без нього нікуди

У вас є досвід з asyncio? Було б цікаво почитати статтю на дану тему. Можна ще порівняти asynchronous programming в різних мовах.

Ну, в цілому asyncio є в планах (коли мова дійде до асинхроності), але до цього ще кілька великих розділів. А ставити розділ про асинхроність раніше, скажімо, функцій — не дуже логічно

Підписатись на коментарі