Базова теорія Python. Типи даних

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

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

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

Історія

Python — одна з найпопулярніших мов програмування (за індексом TIOBE на цей час — найпопулярніша), що може бути застосована у широкому спектрі задач. Найбільшого визнання мова отримала у сфері web-розробки, автоматизації та у ролі фронт-енду для численної кількості бібліотек для Machine Learning та інших наукових призначень. Саме зі зростом інтересу до ML і пов’язана підвищена популярність Python.

В минулому розробка Python підтримувалася Google, але з часом ключова роль перейшла до Microsoft, які винайняли «батька» мови Гвідо Ван Росума, що створив її у 1989 році. Завдяки підтримці MS розробка пришвидшилась, мажорні релізи стали виходити доволі часто та привносити доволі значні зміни. Команда розробників має чітко визначений план на кілька років, в якому одну з ключових ролей відіграє пришвидшення. Версія 3.11, що вийшла нещодавно, зробила помітний крок у цьому напрямку.

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

Загальні положення

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

def print_value(val):
    print(val)


def _main():
    print(1)
    print("some")

А ось такий — не є припустимим та завершиться із помилкою:

>>> 1 + "1"
Traceback (most recent call last):
  File "/usr/local/Cellar/[email protected]/3.10.8/Frameworks/Python.framework/Versions/3.10/lib/python3.10/code.py", line 90, in runcode
    exec(code, self.locals)
  File "<input>", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'int' and 'str'

Починаючи з третьої версії Python, з’явилася можливість додавати до змінних анотації, їх можна отримати як метадані.

from typing import List, Dict

data: List[str] = ["Some", "values"]


def process(info: Dict[str: int]) -> float:
    info["test"] = 1

    return 0

У цьому прикладі ми вказуємо, що змінна data — це список, що містить рядки, а параметр функції process має бути словником з рядками у ролі ключів та цілими числами як значення. Додатково ми інформуємо що функція має повернути тип float. Ці анотації не змінюють роботу інтерпретатора, і ніякої перевірки не виконується, але це дозволяє створення зовнішніх інструментів перевірки типів, а також спрощує роботу IDE.

Наприклад, популярне середовище розробки PyCharm на основі цих даних у багатьох випадках надає інформацію про порушення при передачі значень, а також покращує підказки при доповненні коду.

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

Стиль написання коду

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

if some_condition:
    action1()
    action2()
    
    action3()
    
action4()

У цьому випадку функції action1, action2 та action3 будуть виконані, тільки якщо some_condition матиме значення True. А функція action4 буде виконуватися завжди, бо вона знаходиться за межами «дії» умовного оператора.

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

  1. Для форматування коду слід використовувати 4 пробіли, не змішуючи їх зі знаками табуляції.
  2. Для змінних та імен функцій і методів використовується snake_case.
  3. Для імен класів використовується UpperCamelCase.
  4. Для глобальних констант використовується SCREAMING_SNAKE_CASE.
  5. Окремі функції та класи, а також секція імпорту відокремлюється двома порожніми рядками.
  6. Методи всередині класу або логічні блоки коду відокремлюються одним порожнім рядком.
  7. За можливості, коментарі слід писати окремим рядком. До речі, коментарі починаються з символу #.

Типи даних

Базові

Числові типи даних в Python поділяються на цілі int, дробні float та комплексні complex.

На відміну від більшості мов програмування, тип int побудований так, що може підтримувати арифметику з дуже великими значеннями, наприклад обчислити факторіал 100000:

f = 1
for i in range(1, 100000):
    f *= i

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

На жаль типи float та complex не мають такої можливості, тож з ними отримати переповнення набагато простіше.

Числові типи даних підтримують усі стандартні математичні оператори. Ділення за допомогою оператора / робиться з результатом типу float, якщо вам потрібно цілочисельне ділення — використовуйте оператор //. Щоб отримати залишок цілочисельного ділення використовується оператор %, для зведення у ступінь — **.

Цілі числа також підтримують побітові оператори: | — або, & — та, ^ — xor, << та >> — бітові зсуви, ~ — побітова інверсія.

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

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

Булеві типи та None

В Python булеві типи даних, як і у більшості мов, представлені за допомогою цілих чисел, також існують дві базові константи True, що дорівнює 1, та False, що дорівнює 0. Можливе їх використання в арифметичних операціях:

>>> 1 + True
2
>>> 2 * False
0
>>> True * False
0
>>> True / 2
0.5

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

При перевірці умов, майже усі не булеві значення мають семантику True, окрім:

  • констант False та None;
  • нуля в будь-яких цифрових типах: 0, 0.0, 0j0, Decimal(0) і так далі;
  • порожніх послідовностей та колекцій: [], "", {}, (), set(), range(0);
  • класів, в яких присутній метод __bool__(), що повертає False або метод __len__(), що повертає 0, бо у цих випадках такий клас попадає під дію першого або третього пунктів цього списку.

Для операцій булевої алгебри використовується стандартний набір з and, or та not.

Оператори and та or обчислюють свої параметри тільки за необхідності. Тобто якщо в лівій частині виразу з and буде False, права не буде обчислюватися, і навпаки, якщо в лівій частині or буде True — права не обчислюватиметься. Це можна використовувати, наприклад для перевірки передумов:

some_condition and do_operation()

Або для завдання «альтернативного значення» для змінної, яка може набувати семантики False:

can_be_none or default_value

Ці можливості треба теж використовувати з обережністю, щоб випадково не отримати хибні результати. Наприклад, якщо ви створили функцію, яка повертає якесь число або None, щоб показати, що обчислення неможливе, використання or призведе до того, що 0 також буде замінятися на інше значення, що може бути неочікуваним.

Треба також мати на увазі, що not має нижчий за не булеві оператори пріоритет. Тому not a == b буде інтерпретовано як not (a == b), а вираз a == not b є синтаксичною помилкою. Щоб уникнути її — задайте пріоритет за допомогою дужок.

Окрім стандартних операторів порівняння ==, !=, >, <, >=, <=, існують також оператори is та is not, що перевіряють ідентичність об’єктів, а не їх значення.

Для позначення «відсутнього» значення використовується команда None. Треба мати на увазі, що для перевірки чогось на «відсутність» треба використовувати is None або is not None для запобігання хибнопозитивних результатів, наприклад, якщо якийсь клас імплементує власну поведінку для порівнянь.

Рядки

Мабуть, головним після числових типом даних в усіх мовах є рядок. В Python другої версії рядок виступав синонімом масиву байтів (та часто використовувався у такій ролі), але з переходом 3 версії повністю на Unicode з’явився новий тип даних bytes, що допомогло прибрати плутанину з кодуванням.

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

>>> "first string"
'first string'
>>> 'second string'
'second string'
>>> 'He said: "Hello!"'
'He said: "Hello!"'
>>> "So, it's OK"
"So, it's OK"
>>> 'What \'bout mixing'
"What 'bout mixing"

Як і в більшості інших мов програмування, в рядках можна задавати esc-послідовності типу \n, \t і інших. Це інколи призводить до небажаного ефекту:

>>> print("c:\temp\new_file.txt")
c:	emp
ew_file.txt

Послідовності \t та \n було інтерпретовано як командні, тому результат є зовсім неочікуваним.

Щоб запобігти цьому треба екранувати зворотний слеш: \\ Але якщо ваш рядок містить багато таких символів, краще використовувати «сирий» рядок, в якому інтерпретатор буде ігнорувати усі esc-послідовності.

reg_str = r"^(\d+)[\/](\d+)$"

Строки підтримують оператор + для конкатенації та *, якщо другим елементом виступає число:

>>> "some" + "body"
'somebody'
>>> "test" * 3
'testtesttest'
>>> "1" + 1
Traceback (most recent call last):
  File "/usr/local/Cellar/[email protected]/3.11.0/Frameworks/Python.framework/Versions/3.11/lib/python3.11/code.py", line 90, in runcode
    exec(code, self.locals)
  File "<input>", line 1, in <module>
TypeError: can only concatenate str (not "int") to str

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

>>> a = ["first"]
>>> a.append("second")
>>> a.append("third")
>>> " ".join(a)
'first second third'
>>> ", ".join(a)
'first, second, third'

Метод join() рядка приймає на вхід список інших рядків, та з’єднує їх разом, використовуючи сам рядок як роздільник. Зворотна операція виконується за допомогою метода split(), який може приймати у ролі параметра рядок, що буде виступати у ролі роздільника. Ось, наприклад, як можна перетворити рядок CSV файлу на список чисел.

>>> line = "35,17,23"
>>> num_list = [int(value) for value in line.split(",")]
>>> print(num_list)
[35, 17, 23]

В цьому прикладі використано іншу потужну можливість Python — списковий вираз, який ми розглянемо незабаром. Поки що вважайте його «апетайзером».

Ще комбінацію split() та join() інколи зручно використовувати для простих маніпуляцій з рядками.

>>> test_str = "Hello, cruel world!"
>>> temp_list = test_str.split()
>>> temp_list[1] = "beautiful"
>>> " ".join(temp_list)
'Hello, beautiful world!'

Якщо викликати split() без параметра, він розділить рядок за усіма «пробільними» символами, що в ньому знаходяться: пробілами, табуляціями тощо. До того ж він ігноруватиме кількість пробільних символів, що буває дуже у пригоді, коли потрібно «почистити» рядок.

>>> ugly_str = "   Some   bad formatted     one!! "
>>> " ".join(ugly_str.split())
'Some bad formatted one!!'

Рядки припускають ітерацію за допомогою циклу for, також присутня можливість отримати один символ з використанням квадратних дужок: some_str[7]. Індекс може мати від’ємне значення, і у цьому випадку він буде рахуватися від краю рядка до початку. При використанні доступу по індексу треба бути уважним, бо хибний індекс призведе до винятку.

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

Зрізи

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

>>> test_str = "Hello, Python world!"
>>> test_str[7:13]
'Python'
>>> test_str[7:]
'Python world!'
>>> test_str[:5]
'Hello'
>>> test_str[::2]
'Hlo yhnwrd'

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

Якщо вказано третє число, то з рядка братиметься кожен n-ий елемент, в нашому прикладі — кожен другий.

Будь-яке з трьох чисел може бути від’ємним. Для чисел, що вказують інтервал, це означає, що рахувати треба від краю рядка до початку. Третє число працює так само як і додатне, беручи кожен n-ий елемент, але результат буде інвертовано. Ось як це виглядає на рядку з минулого прикладу:

>>> test_str[-13:-7]
'Python'
>>> test_str[7:-7]
'Python'
>>> test_str[::-1]
'!dlrow nohtyP ,olleH'
>>> test_str[25:]

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

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

Списки

Одним зі зручних аспектів Python є гарна підтримка різних колекцій. Майже близько до стандартного JSON, списки задаються за допомогою [] та можуть містити будь-які елементи (хоча, звісно, рекомендується не зловживати цим).

>>> [1, 2, 3]
[1, 2, 3]
>>> [1, "some", None, False, 0.0]
[1, 'some', None, False, 0.0]

>>> a = [1, 2, 3]
>>> a.append(4)
>>> print(a)

[1, 2, 3, 4]
>>> a.append([5, 6, 7])
>>> print(a)

[1, 2, 3, 4, [5, 6, 7]]
>>> a.extend([8, 9, 10])
>>> print(a)

[1, 2, 3, 4, [5, 6, 7], 8, 9, 10]
>>> a.append(a)
>>> print(a)
[1, 2, 3, 4, [5, 6, 7], 8, 9, 10, [...]]

Перші два рядки демонструють нам списки з однаковими або різними типами значень.

Для додавання одного елементу наприкінці списку використовується метод append(). Якщо використати його з іншим списком, то другий список буде додано, як один елемент першого. У багатьох випадках потрібно додати елементи з другого списку до першого окремо, тоді використовується метод extend(). Трохи дивно, але майже без практичного використання — список може також містити сам себе.

Завдяки наявності метода pop() список дуже зручно використовувати як стек. Цей метод повертає останній елемент списку, та видаляє його. Метод pop() може також приймати індекс елементу, який треба видалити. Тут у багатьох виникає ідея, що список можна також використовувати як чергу, використавши pop(0) для отримання елементів, і це буде працювати, однак через внутрішню реалізацію, чим ближче елемент до початку списку, тим затратніше буде його видалення. Тому, якщо вам потрібна черга, краще використовувати клас deque з модуля collections, який позбавлено цієї вади. Взагалі, дуже раджу звернути увагу на цей модуль, він містить багато корисних реалізацій різноманітних колекцій.

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

>>> test_list = [1, 2, 3]
>>> test_list [1:1] = [7, 8, 9]
>>> print(test_list)
[1, 7, 8, 9, 2, 3]
>>> test_list[2:4] = []
>>> print(test_list)
[1, 7, 2, 3]

У першому випадку ми додаємо 3 елементи замість другого, а потім вилучаємо частину елементів списку, замінюючи їх на порожній список. Також вилучати елементи можна за допомогою команди del, наприклад del(test_list[3]).

Ще одним потужним інструментом Python виступає автоматичне розпаковування списків, що дозволяє відокремити деякі частини списку у змінні.

>>> [a, b] = [1, 2]
>>> print(a, b)
1 2
>>> [a, *b, c, d] = [1, 2, 3, 4, 5, 6, 7]
>>> print(a, b, c, d)
1 [2, 3, 4, 5] 6 7
>>> [a, *_, b] = [1, 2, 3, 4, 5]
>>> print(a, b)
1 5

Найпростіший випадок — коли змінних стільки ж, скільки елементів у списку. Тоді вони просто отримають відповідні значення зі списку. Хоча тут треба бути уважним, незбіг призведе до помилки. Якщо ж потрібно працювати зі списками невідомої довжини, можна вказати змінну-акумулятор із зірочкою, і тоді ця змінна «збере» усі елементи, що не потрапляють в інші змінні. Якщо ж «решта» елементів вам взагалі не потрібна, можна перенаправити їх у «тимчасову» змінну _, яка, так само як і в інших мовах, виступає у ролі змінної, значення якої нам не потрібне.

Також можлива зворотна операція, за допомогою * можна «розгорнути» масив у послідовність його значень:

>>> a = [1, 2, 3]
>>> ["x", 'y', *a]
['x', 'y', 1, 2, 3]

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

Словники

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

dict1 = {
    "John": 20,
    "Paul": "string",
    "Jones": 37.7,
    20: "dynamic"
}

dict2 = {}
dict2["Ringo"] = "Starr"

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

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

Для доступу до окремого елементу словника можна використовувати звичайний синтаксис: dict1[20]. Якщо ключ не міститься у словнику, буде згенеровано виняток KeyError. Найпростішим методом запобігти винятку є використання методу get(key, default_value=None). Цей метод повертає значення ключа, якщо він присутній, а якщо ні — буде повернено значення за замовчанням:

>>> dict2.get("George", "Harrison")
Harrison

Існують також два додаткові методи запобігання KeyError. Перший — це перевірити, чи є такий ключ у словнику^

def get(dictionary, key_name, default_value=None):
    if key_name in dictionary:
        return dictionary[key_name]
	else:
		return default_value

Другий підхід — це спробувати обробити похибку у разі її появи:

def get(dictionary, key_name, default_value=None):
    try:
        return dictionary[key_name]
	except KeyError:
		return default_value

Цей метод у деяких підручниках рекомендується як більш «пайтонівський», але слід відзначити, що в усіх версіях інтерпретатора окрім свіжої 3.11 час виконання блоку всередині try буде трохи більшим. Та завдяки підпроєкту, спрямованому на пришвидшення інтерпретатора, тепер, якщо не буде згенеровано виняток, ніякого додаткового уповільнення код всередині try не матиме.

Для вилучення пари ключ-значення зі словника використовується вже згадана вище функція del: del(dict1[20])/

Починаючи з версії 3.7, Python гарантує збереження порядку ключів. У попередніх версіях для цього потрібно використовувати клас OrderedDict з модуля collections.

Кортежі

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

tup1 = (1, "Some", True)
tup2 = ("Single",)
tup3 = ()

Зверніть увагу, що для створення кортежу з одного елементу треба поставити після нього кому, щоб відрізнити tuple-літерал від просто якогось виразу у дужках. Для порожнього кортежу цього робити не потрібно.

За винятком факту своєї незмінності (неможливо видалити, додати елемент або змінити його), кортежі підтримують майже усі можливості списків: ітерацію за допомогою циклу for, перевірку на наявність елементу за допомогою in, отримання елементу за індексом, або зрізу.

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

def get_random_coordinates():
    return 2, 5     # I rolled dices

x, y = get_random_coordinates()

Всередині функції два значення int, що ми повертаємо, автоматично пакуються, а при присвоєнні змінним — розпаковуються. Як і у випадку зі списками, можна використовувати змінну-накопичувач, яка отримає усі значення, що не попадуть до інших змінних.

З цікавого — ця можливість робить Python однією з найзручніших мов для розв’язку задачі «змінити значення двох змінних одне на одне»:

a, b = b, a

Множини

Набір типів даних був би не повним без множин. Це — колекція, яка не має доступу за індексом. Будь-який елемент у множині може міститися тільки один раз. Окрім того, множина має швидку операцію перевірки наявності елементу в собі за допомогою in та надає базові функції роботи із множинами. Ще одним обмеженням множин є те, що вони не можуть містити елементи, що змінюються (mutable), такі як словники та списки.

Створити множину можна, використовуючи літерал:

set1 = {1, "some", False}

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

set2 = set([1, 2, 3])

Оскільки множина є ітератором, то можливе, наприклад, її перетворення у список. Це часто використовують, щоб отримати список, який містить тільки унікальні елементи:

>>> a = [1, 2, 2, 1, 3]
>>> b = list(set(a))
>>> print(b)
[1, 2, 3]

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

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

>>> a = {1, 2, 3}
>>> a.intersection((2, 3, 4))
{2, 3}
>>> a | (2, 3, 4)
Traceback (most recent call last):
  File "/usr/local/Cellar/[email protected]/3.11.0/Frameworks/Python.framework/Versions/3.11/lib/python3.11/code.py", line 90, in runcode
    exec(code, self.locals)
  File "<input>", line 1, in <module>
TypeError: unsupported operand type(s) for |: 'set' and 'tuple'

Множини підтримують наступні операції:

  • Об’єднання (a | b або a.union(b)) повертає нову множину, яка містить елементи як з a, так і з b.
  • Перетин (a & b або a.intersection(b)) — множина, яка містить елементи, які присутні одночасно в a та b.
  • Асиметрична різниця (a - b або a.difference(b)), результатом є множина, яка містить елементи, присутні в a та відсутні в b.
  • Симетрична різниця (a ^ b або a.symmetric_difference(b)), результатом є множина, яка містить тільки унікальні елементи з a та b, які не присутні одночасно в обох множинах.
  • Перевірка на відсутність перетинів (a.isdisjoint(b)) повертає True якщо множини не мають спільних елементів. Також цілком логічно True повертається у випадку перевірки двох порожніх множин.
  • Об’єднання на місці (a |= b або a.update(b)) додає в множину a усі елементи b, окрім дублікатів.
  • Додавання елементу a.add("some").
  • Вилучення елементу a.remove("value"), якщо цей елемент відсутній у списку, буде порушено виняток.
  • Безпечне вилучення елементу a.discard("value"), якщо цей елемент відсутній, програма продовжить своє виконання.
  • Отримання елементу з множини a.pop(), оскільки ми не можемо вказати індекс елементу, а також оскільки множина не зберігає їх порядок, можна вважати, що метод видалить випадковий елемент множини та поверне його. Якщо спробувати отримання елементу з порожньої множини, буде порушено виняток.
  • Видалення усіх елементів a.clear().

Також існують «скорочені» оператори |=, &=, -= та ^=, які виконують відповідну дію з двома множинами та потім присвоюють результат множині ліворуч.

Окрім звичайних множин, в Python існують також «заморожені множини», які створюються за допомогою конструктора frozenset. Їх відмінність полягає в тому, що в них немає можливостей зміни самої множини (додавання елементів, вилучення і таке інше). Скорочені оператори для таких множин не модифікують операнд ліворуч, а створюють нову множину та зберігають її у відповідній змінній.

Спискові, словникові та множинні вирази

Англійською ці синтаксичні конструкції звуться «list, dict and set comprenensions». На жаль, гарного влучного перекладу цього терміну немає. Найчастіше в гуглі зустрічаються варіанти, що я виніс у назву розділу, хоча це звучить надто узагальнено.

Зустрічається також переклад «генератори списків», що доволі добре передає суть, але викликає плутанину з власне генераторами, що також присутні в Python.

На мій погляд, — це найцікавіший інструмент з тих, що пропонує Python. Ці вирази допомагають гнучко та виразно описувати трансформацію або створення колекцій. Ось простий приклад:

>>> [x ** 2 for x in range(30) if x % 3 == 0]
[0, 9, 36, 81, 144, 225, 324, 441, 576, 729]

Читати такі вирази треба зсередини, частина з for задає джерело, з якого ми створюватимемо список та змінну, в яку будуть по черзі потрапляти елементи джерела. В ролі останнього може бути будь-який об’єкт, що підтримує ітерацію. В нашому випадку — це генератор range(), який просто повертає числа від 0 до 29. Потім праворуч йде додаткова перевірка елементів, якщо вираз дорівнюватиме False, елемент буде відкинуто та вираз піде на наступну ітерацію. В нашому прикладі ми залишаємо тільки числа, кратні 3. Нарешті ліворуч в нас йде вираз, який обчислюється, та результат цього обчислення додається до майбутнього списку. Потім береться наступний елемент джерела, перевіряється умова і так далі.

Частину з перевіркою умов можна пропустити, тоді в майбутній список попадуть усі елементи.

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

>>> [10 for _ in range(5)]
[10, 10, 10, 10, 10]

Або перетворювати один список на інший, виконуючи над елементами якусь операцію:

>>> [str(i) for i in (4, 8, 15, 16, 23, 42)]
['4', '8', '15', '16', '23', '42']

До речі, попри наявність в Python функції map(), рекомендують використовувати саме спискові вирази, через їх гнучкість.

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

>>> {x / 10: sin(x / 10) for x in range(11)}
{0.0: 0.0, 0.1: 0.09983341664682815, 0.2: 0.19866933079506122, 0.3: 0.29552020666133955, 0.4: 0.3894183423086505, 0.5: 0.479425538604203, 0.6: 0.5646424733950354, 0.7: 0.644217687237691, 0.8: 0.7173560908995228, 0.9: 0.7833269096274833, 1.0: 0.8414709848078965}

У цьому випадку ми будуємо словник табуляції функції sin() від 0 до 1 радіану із кроком 0.1. На жаль, range() підтримує тільки цілочисельні типи даних, тому довелось вдатися до невеликої хитрості.

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

>>> { x * 2 for x in (1, 3, 3, 4, 3, 2, 2)}
{8, 2, 4, 6}

Ще можна написати вираз, ідентичний списковому у звичайних дужках, дехто називає це «tuple comprehension», але це не зовсім коректно. Дійсно, об’єкт, що створюється нагадує кортеж за семантикою (дозволяє ітерацію, але не дозволяє зміни), але він є іншим типом — генератором.

>>> a = (x for x in range(11))
>>> print(a)
<generator object <genexpr> at 0x10272a260>

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

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

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

>>> weapons = [["HIMARS", "CAESAR", "M777"], ["Bayraktar"], ["Javelin", "Stugna"]]
>>> use_all = [weapon for sublist in weapons for weapon in sublist]
>>> print(use_all)
['HIMARS', 'CAESAR', 'M777', 'Bayraktar', 'Javelin', 'Stugna']

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

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

Наразі це все, у наступних частинах я розповідатиму про функції, керування потоком виконання, класи, винятки та їх обробку та інше. Слідкуйте за публікаціями!

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

👍ПодобаєтьсяСподобалось38
До обраногоВ обраному26
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

поки помітив два погані моменти:
— 

Цілі числа також підтримують побітові оператори: | — або, & — та, ^ — xor, << та >> — бітові зсуви, ~ — побітова інверсія

тут замість xor можливо кращє вказати «виключаюче або», бо всі оператори описані українською, а один просто терміном, або всі тоді вказати як «or, and, not, left/right-shift»
— і друге — Мабуть тут помилка перекладу... що означає

буде згенеровано виняток

?
тобто Exception ?

Може тоді якщо ну дууже хочеться перекласти Exception Українською — то кращє вибрати значення «Заперечення» ...

Дякую за зауваження, але я точно не хочу додавати зайві сутності. uk.wikipedia.org/wiki/Обробка_винятків

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

дякую і за ваші коментарі, але не відчуваю себе достатнім авторитетом щоб оскаржувати термін, який є хочаб більш-меньш загальним

До речі трохи пошукавши і поміркувавши як жеж правильніше буде називати Exception Українською прийшов до висновку що навіть не заперечення, а Виключення замість виняток, тобто як Виключна поведінка або ситуація.
І навіть російською «Исключение» ми використовуємо в значенні «Исключительный, как в Исключительное поведение (то есть что то особенное и нестандартное)», а не як «Исключающий, как исключение из правил (то есть нормальное поведение которое не подчиняется общим правилам но является корректным)»

поки помітив два погані моменти:
— 

Цілі числа також підтримують побітові оператори: | — або, & — та, ^ — xor, << та >> — бітові зсуви, ~ — побітова інверсія

тут замість xor можливо кращє вказати «виключаюче або», бо всі оператори описані українською, а один просто терміном, або всі тоді вказати як «or, and, not, left/right-shift»
— і друге — Мабуть тут помилка перекладу... що означає

буде згенеровано виняток

?
тобто Exception ?

Дякую за статтю! Вона мені читається як «мега-концентрована шпаргалка» — aka 1-2 pp. mega cheatsheet.
Побачив в тексті 2 одруківки/помилки:
1.

та <>codeor обчислюють свої параметри

2.

треба використовувати екранування за допомогою \\:

— ми екрануємо одним зворотнім слешем. хіба що тут ви **одразу** навели приклад екранування самого зворотнього слешу

дякую, слеш подвоївся через те що текст кілька разів проходив шлях з маркдауну в інші формати і у зворотній бік, і десь його додатково екранувало

Вивчення Python мені нагадує вивчення італійської мови.
Дуже легко до рівня А2 і дуже важко далі.

ну, побачимо як в мене виходитиме далі розповідати :)

Після якого моменту(теми) стає дуже важко?

Ну, тут залежить від досвіду. Якщо не розуміти як працюють higher order functions, може бути складно з декораторами (три рівні вкладеності і усе таке). Але якщо це розуміти, проблем не буде
Мені знадобилися певні зусилля щоб зрозуміти логіку дескрипторів або метапрограмування. Хоча це не те щоб особливо важко. Цікавіше що за багато років це не знадобилося яж ніяк
Ті самі дженерики та усілякі констрейнти у Свіфті потребують більше ментальних зусиль, але використовются набагато частіше

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

Складним є рефакторинг і оптимізація коду. Коли мало не для кожного кроку є кілька альтернатив які вимагають різної пам’яті та часу на виконання.

ось з цим якраз в Python зазвичай намагаються боротися «має бути один та тільки один спосіб зробити це»

Якраз навпаки. Python передбачає кілька альтернативних способів.
Перегляньте realpython.com/fast-flexible-pandas
Різниця між швидкістю простої loop і numpy.digitize() в 1500 разів.

я перепрошую, але стороні бібліотеки, навіть такі популярні як NumPy/Pandas/etc. не відносяться до безпосередньо Python як мови, про що ми говоримо. Це як обговорювати дизайн Віндовс, спираючись на WinAmp

А для чого тоді Python, якщо не використовувати NumPy/Pandas функціонал?
Будь-яка робота з даними, математикою без них вимагає гіпер досвіду щоб зробити оптимально. Тому Python робиться досить складним починаючи з певного рівня.

«а для чого тоді Wnidows, якщо не використовувати WinAmp?»
обчислення — часте використання Python, але це ніяк не робить NumPy та інше — частиною власне мови
а взагалі, люди на пайтоні пишуть бекенд, скрипти автоматизації, десктоп, тощо

З таким підходом ви першу роботу на Python не знайдете.
Проста задача на агрегацію даних для бекенда без Numpy, Pandas і їм подібним на чистому Python розв’язується дуже складно. В 99% випадках розв’язок буде далеким від оптимального і не підійде для проду.

да якось знайшов, і років 6 працював без NumPy та його друзяк. може тому що більшість бекенду це «дістати з бази, обробити, віддати» а не молотіння виликих багатовимірних таблиць
а більшість складнощів накшталт «чомусь монга рассинхрозувалася з еластиком»
а до NumPy дішов вже тоді коли нам знадобилося рекомендації робити

Так, дійсно, бо співбесіду не пройдеш, бо подібне іноді і питають. Але співбесіда то співбесіда, а щоденні таски — дещо інше.

Білд скрипти писати... Що оптимальність не дуже потрібна у Python я згоден, але Python використовується багато де.

Це важко досягнути, особливо якщо ці способи уже є у сирцевій базі. А так
del a[0],a.pop(0), або list(), []... Мені здається, таких прикладів буде багато.

ну del та pop — роблять різні речі. конструктор list() зазвичай використовується тільки для перетворення, використовувати його замість літералу — дивна ідея
звісно, є певне дублювання, і легасі нікуди не діти, але в цілому команда Python намагається тримати це під контролем

Лучше со старта языки со строгой типизацией

python — мова зі строгою типізацією

Ну... у таких висловах набагато більше маркетингу. На справді питання перше, що вважати типом. Якщо стати на точку зору математичної теорії типів (інформація яка існує лише на етапі перевірки коду, яка недоступна у runtime), то у мові Python є лише один тип даних. Так, це дійсна мова зі строгою типізацією, бо просто не існує другого іншого типу, до якого була в можливість неявно перевести. Якщо тип це частина значення, то тут незрозуміло, що вважати строгою/слабкою типізацією, бо це зміна значення. Так, наводять приклади на кшталт що у JS "x" + 3 це "x3", а у Python це помилка, але це більше схоже на визначення операції. У самому ж Python стиль, коли ми не дуже турбуємося про тип аргументу, а вже сама функція розрулить та зробить перетворення не дуже й рідка. Наприклад, x = 3 : print(x), все ж таки у строго типізованій мові зазвичай треба писати putStrLn $ show x.

Дуже дякую вам за коментарі, але ж в школі ми вивчаємо з початку аріфметику, а не теорію категорій. Так і тут, для навчання багато що із знань буде зайвим. «Строгість» Python полягає в тому що є певний набір правил, які не дозволяють «не осмислено» робити маячню накшталт додавання рядка та числа, або конверсіі з float в int. І в цілому цього розуміння «строгості» достатньо і для навчання і у більшості випадків і потім

Просто є питання, наскільки Python строга мова програмування, дивлячись на 2 * True.

Але головна проблема Python для новачків більше у тому, що Python досить об’ємна та непроста мова, яку можна вчити досить довго проводячи час за читанням таких статей. А потім через пару років з’ясується, що нема ніякого хисту до фіксу багів, й ці два роки пройшли марно. Навіщо новачкам генератори? Це досить великий об’єм, який нагадує анекдот, як на полігоні: «А зараз ми покажемо, як рядовий Іванов буде намагатися усю цю фігню підняти у повітря!»

Щодо арифметики із bool, а також концепції «falsy» зачень мені теж не подобається. але свого часу PEP який робив ці операції більш контрольованими — було відкинуто голосуванням. То ж більшість вирішила що їм ліньки писати if len(some_list) == 0:
А щодо навчання, я не знаю якоїсь мови яка б не мала глибини. Питання тільки в тому, наскільки в неї треба занурюватися

А щодо навчання, я не знаю якоїсь мови яка б не мала глибини.

Plain C: масиви, вказівники, функції, усе.

То ж більшість вирішила що їм ліньки писати

Ну і JS хтось вирішив, що йому ліньки писати str(width) + 'px'. Саме через це рішення переносити термін сильної/слабкої типізації до динамічних мов щоб принизити конкурентів?

Plain C: масиви, вказівники, функції, усе

менеджмент пам’яті, краши чрез це, перхід за гранцию масива, жах від синтаксису вказівника на функцію, відсутність ООП, неможливість нормально моделювати типи даних... чудова мова для новачка, так

щоб принизити конкурентів?

ні, щоб віднізняти різний підхід

ну от я знаю, бо починав ще в 90-ті (хто пам’ятає що таке TechHelp) і що це мені наразі дає? НІЧОГО окрім теплого відчуття гордості за те що я такий в мами розумник. але мені вистачає розуміння що новачку і так складно, а якщо ще й «розуміти як працює комп’ютер»...

А що страшного у менеджменті пам’яттю, особливо на початковому етапі? Користь від цього — розуміння архітектури.
Відсутність ООП... Якщо брати Simula-like ООП то я взагалі не дуже впевнений у його корисності, бо самі жахливі архітектури, які я бачив, були ООП. Якщо брати Python, для мене це більше синтаксичний цукор над dict. Ну а так я не впевнений у тому, що ООП потрібен початківцям, не кажучи про те, що може й не знадобиться взагалі. Більшість
Моделювання типів даних... Не знаю, я про це ніколи не чув, мені не заважає працювати ніяк. Тобто для новачків це 100% можна опустити.

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

Як раз у цьому плані Python містить пастку: його можна вивчати досить довго, вивчити усі типи даних, вивчити ООП при цьому забувши типи даних. Але це усе буде марно, бо згодом з’ясується, що людина не може фіксити баги, тому його велью на проекті буде негативне, це при умові, що він ще знайде роботу. Інша проблема це вправи, бо написаний код на усі випадки життя, його треба лише знайти. Нам треба порахувати кількість слів у рядку? len . split. Це призводить до того, що виконання вправ це хінти, гугління та використання док, щоб знайти ім’я метода та правильно передати параметри. Це добре, але це ніяк не прокачує скіл пошуку багів.

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

ні, щоб віднізняти різний підхід

Там підхід відрізняється лише у тому, як виконується +. Як на мене, то не підхід, то деталі.

Python надзвичайно проста мова, але зазвичай на цю тезу набігають прихильники якогось лямбда куб хаскелізму і починають доводити що це не так і тип взагалі не типовий.
Вивчення пітону потрібно починати з
import this
І тоді не буде ніяких проблем.

Дуже смішно.

Звісно. Якщо змінні не мають типів, яка може бути строга типізація? Навіть порушення анотацій це застереження, а не помилка. Тобто суворість тут на рівні свинки Пеппі))

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

А що таке «тип даних»? Я б про це сказав би декілька слів, бо є дві точки зору. Перша полягає у тому, що тип даних це інформація, яка присутня на стадії аналізу/компіляції сирцевого коду, яка перевіряє певні властивості та виключає деякі класи помилок, яка стає недоступна у runtime. З цієї точки зору в Python є один тип даних, `PyObject`. Є інша точку зору, що тип це певна частина значення, яка показує що там зберігається. Ось у цьому сенсі усі ці типи є у Python. А комент додав більше для того, що люди часто плутають ці два визначення.

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

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