Базова теорія 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, запобігаючи виникненню похибок, рекомендується слідувати загальноприйнятим правилам.
- Для форматування коду слід використовувати 4 пробіли, не змішуючи їх зі знаками табуляції.
- Для змінних та імен функцій і методів використовується
snake_case
. - Для імен класів використовується
UpperCamelCase
. - Для глобальних констант використовується
SCREAMING_SNAKE_CASE
. - Окремі функції та класи, а також секція імпорту відокремлюється двома порожніми рядками.
- Методи всередині класу або логічні блоки коду відокремлюються одним порожнім рядком.
- За можливості, коментарі слід писати окремим рядком. До речі, коментарі починаються з символу
#
.
Типи даних
Базові
Числові типи даних в 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», щоб не пропустити нові технічні статті
49 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів