Python Flashcards
(13 cards)
Что это простыми словами
Язык программирования с большим количеством написанных библиотек под разные задачи – на Python (пайтон) можно писать игры, вебсайты, модели машинного обучения, загрузку хранилища данных.
Он популярен у дата инженеров, потому что задачи для оркестратора Airflow пишут на Python, и у инструмента обработки данных Spark есть интерфейс PySpark.
Также он используется, потому что на нём удобно писать большие модульные системы. Он не самый быстрый с точки зрения производительности, но является стандартом в индустрии – поэтому проще обучить нового сотрудника или найти замену.
Также в DE популярны Java и Scala, редко – Rust или Go:
* Java из-за того что хадуп написан на джаве и раньше под него писали более “низкоуровневый” код. Ближе к тому что работает под капотом – меньше “переводчиков” нужно использовать
* Scala это язык, на котором можно писать “нативно” под Spark, аналогично джаве и хадупу
* Rust или Go используются для написания производительных приложений, например в высокочастотной торговле, когда важна каждая микросекунда
Итератор vs генератор
Итератор это класс, для которого реализованы магические методы iter() и next(). По объектам этого класса можно будет пробегаться циклом или запрашивать next(iter(myObject)).
Вычисляет и хранит всю последовательность в памяти, ей ограничен.
class MyNumbers: def \_\_iter\_\_(self): self.a = 1 return self def \_\_next\_\_(self): x = self.a self.a += 1 return x
Генератор это функция, в которой вместо return итерируемое значение выводится в yield. Хранит только текущее значение и то, как вычислить следующее. Может работать с бесконечно большими последовательностями.
def csv_reader(file_name): for row in open(file_name, "r"): yield row for row in csv_reader(file_name): print(row)
Также генератор можно задать как generator expression (генераторное выражение).
csv_gen = (row for row in open(file_name)) for row in csv_gen: print(row)
Основные коллекции: dict, list, set, tuple +Изменяемые vs неизменяемые типы
dict, словарь – пары ключ-значение, совместим с JSON
db_conf = {“db”: “shop”, “schema”: “sales”, “table”: “orders”, “host”: “my.dbname.private.domain.com”, “port”: 5432}
Значениями могут быть число, строка, список, другой словарь, в принципе почти любой объект. Про ключи смотри “Что можно и нельзя использовать как ключ словаря”.
Поиск в словаре по ключу занимает O(1), т.к. нужно посчитать за константу хэш от ключа и обратиться напрямую по нему. В случае коллизии (когда хэш от двух разных ключей совпадает) занимает O(n).
Пробежаться по ключам словаря можно через
for key in d:
Также через in можно проверить, входит ли ключ в словарь.
Пробежаться сразу по ключам и значениям:
for key, val in d.items():
Получить значение словаря: d[“key”], добавить или перезаписать значение: d[“key”] = “value”.
Если значения нет, выдаст KeyError. Безопасно можно обратиться через **d.get(“key”, <default_return_value>)**. Без указания второго аргумента вернёт **None**, если ключа нет.</default_return_value>
Set это множество, во многом ведёт себя как словарь без значений. Есть только хэшируемые ключи, которые запоминают порядок вставки, но зато через O(1) можно проверить вхождение в сет. Сет хранит только уникальные значения (например, можно проверить, сколько уникальных значений в строке/списке, если преобразовать их в сет). Есть много уникальных методов из реляционной алгебры (union, intersect, except и пр.).
unique_labels = {‘low’, ‘mid’, ‘high’}
Список, list – одномерный массив значений, не обязательно одного типа, но обычно одного. Значениями может быть любой объект. Значения не отсортированы, но хранятся в порядке добавления.
ports = [5432, 8080, 80, 5050]
Добавить значение в конец:** l.append(value).
Извлекаются или переназначаются значения по индексу, то есть порядковому номеру, который начинается с нуля и идёт до n-1: ports[1] = 8080
Также можно обращаться к значениям по индексам “с правого конца”, от -1 до -n**.
Частая ошибка во время исполнения кода IndexError – обратиться по индексу, который не существует в списке.
Пробежаться сразу по индексу и значению:
for i, elem in enumerate(l):
Не используй подход ниже, это не pythonic way
for i in range(len(l)): print(l[i])
Лучше обращайся сразу по for elem in l: или через enumerate.
Мы не узнаем, есть ли искомое значение в массиве, пока не проверим все по порядку, поэтому поиск в списке занимает O(n).
Сортировка занимает в среднем O(n*logn).
Tuple (тапл), или кортеж, это неизменяемый объект, который хранит значения разных типов. Может использоваться для возвращения строки из БД или передачи разнородных данных в одном объекте. Хранит порядок.
row = (‘Alice’, ‘Smith’, 25, ‘2B’, [‘Ru’, ‘Ge’], False)
Для создания кортежа с одним элементом добавь запятую в конец: my_tuple = (‘elem’,)
Обращаться к элементам нужно по индексам, как в списках.
Try except else finally
Используется для обработки ошибок. При написании кода важно предусматривать места, где что-то может пойти не так, и обрабатывать такие ошибки явно. Например, когда мы ожидаем ограниченный набор значений на входе, или проверяем что конфиг должен быть заполнен, или что в случае тайм-аута нужно попробовать отправить запрос ещё раз.
Аналог try-catch из других языков программирования.
- try: попробовать выполнить участок кода
- **except <Exception>**: если ошибка, выполни этот блок кода</Exception>
- else: если исключение не вызвано, выполни код
- finally: вне зависимости от того, вызвано исключение или нет, выполни код
d = {"key": "value"} try: print(d["another_key"]) except KeyError: print(f"there’s no 'another_key' in d keys: {d.keys()}") except Exception as e: raise e("some text I want to print") else: print("do something here") finally: print("почти никто не использует else и finally")
Можно вручную вызвать исключение любого типа, например:
raise TypeError("Only integers are allowed")
+Что можно и нельзя использовать как ключ словаря
Можно всё, что является неизменяемым и у чего реализован магический метод __hash__(), т.е. что при передаче в некоторую хэш-функцию вернёт хэш детерминированным образом. Важно, чтобы при обращении к словарю через ключ можно было посчитать хэш от ключа и найти связанное с ним значение (value) или точно знать, что такого ключа в словаре нет.
Context manager (with)
Используется при работе с контекстами – соединениями к БД или API, с файлами на диске или в объектном хранилище.
Очень важное свойство – вне зависимости от причины выхода из контекста (нормальное завершение работы или ошибка) закрывается коннект. Может пригодиться для работы с соединениями с какими-то внутренними сервисами (обычно уже написан и используется в хуке/операторе airflow и методах работы с Х в пакетах).
Выглядит так: with open("test.txt") as f: data = f.read() with MongoDBConnectionManager('localhost', '27017') as mongo: collection = mongo.connection.SampleDb.test
Для написания своего контекстного менеджера определи магические методы _enter и exit
Сами детали с этими исключениями exc_type, exc_value обычно не спрашивают.
class ContextManager(): def \_\_init\_\_(self): print('init method called') def \_\_enter\_\_(self): print('enter method called') return self def \_\_exit\_\_(self, exc_type, exc_value, exc_traceback): print('exit method called') with ContextManager() as manager: print('with statement block')
Распаковка списков и словарей *args kwargs
Некоторые функции могут работать с неограниченным количеством аргументов. Или мы хотим передать все, но использовать только некоторые из них. Тогда можно “распаковать” список или словарь через * и соответственно. Можешь встретить в операторах airflow, например.
*args представляет позиционные аргументы вида “значение”
kwargs – именованные аргументы вида “ключ-значение”
Слайсы в списках и строках
Полезно в задачах. Позволяет как ножницами разрезать объект на части и вернуть левую или правую. Проще всего понять через собственные эксперименты, вот тебе пара идей, но и другие варианты тоже попробуй. Индексирование как в списках, слева-направо с нуля, справа-налево – с -1.
l = [0,1,2,3,4] l[1:] #[1,2,3,4] l[:1] #[0] l[1:3] #[1, 2] l[1:5:2] #[1, 3] l[1::2] #[1, 3] l[1::-2] #[1] l[::-1] #[4, 3, 2, 1, 0] l[-1:] #[4] l[-2:] #[3, 4]
Type hints и doc-strings
Не влияют на выполнение, нужны для упрощения чтения и доработки, чтобы было понятно, что ожидает функция на входе (какие параметры и каких типов) и что будет на выходе. Ещё раз, если тип не совпадает с указанным, код может сработать и выдать какой-то ответ, это не блокирует выполнение.
При наведении на название функции IDE подтягивает это описание и предлагает автодополнение
Выглядят примерно так:
def get_columns(table:str, schema:str) -> list[str]: """ Returns list of "column datatype" definitions. ""”
Также можно указывать тип для переменной или класса:
db_name: str db_name = get_db_name(context)
List and dict comprehensions
Короткая запись цикла “for” для создания и/или обработки списков и словарей: фильтрация, какая-то трансформация.
[elem for elem in list] сolumns_to_ignore = [“col1”, “col2”] quoted_columns = [“‘“ + col + “’” for col in columns if col not in columns_to_ignore]
{key: val for key, val in d.values()}
Устойчивая конструкция – X for X in object, и только в самом левом месте можно какие-то трансформации над этим Х проводить (ну или фильтровать после object).
Вложенный вариант пишется так: matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]] длинный вариант odd_numbers = [] for row in matrix: for element in row: if element % 2 != 0: odd_numbers.append(element) # короткий вариант odd_numbers = [ element for row in matrix for element in row if element % 2 != 0]
GIL + Async await, асинхронка, сборщик мусора и все связанное
Чаще всего здесь тебя ждут рассказ про сборщик мусора на Python, принцип его устройства, про библиотеку Asyncio и Global Interpreter Lock.
Сборщик мусора на Python реализован через счётчик ссылок. Python считает количество ссылок на объект, и как только все ссылки на объект заканчиваются — программа вычищает объект из памяти. Это необходимо, чтобы удалять из памяти ненужные объекты и оптимизировать использование ресурсов. Помимо подсчёта ссылок, Python также использует механизм выявления циклических ссылок, которые не могут быть удалены стандартным подсчётом. Корректную работу такого сборщика мусора обеспечивает GIL.
GIL — Global Interpreter Lock, особенность языка Python, запрещающая выполнение нескольких потоков параллельно в рамках одного интерпретатора. Это ограничение необходимо для обеспечения безопасности работы с памятью в многопоточных программах. Для обхода GIL можно использовать модуль multiprocessing, который создаёт отдельные процессы и позволяет задействовать несколько ядер процессора одновременно(для ускорения тяжёлых расчётов, например), но это не является полноценным решением проблемы отсутствия многопоточности.
Asyncio — модуль в Python, предназначенная для написания асинхронного кода. Она позволяет выполнять несколько задач одновременно, не блокируя выполнение программы, используя кооперативную многозадачность.
Основные компоненты asyncio включают в себя:
Event Loop (Цикл событий) — основной механизм, который управляет выполнением асинхронных задач.
Coroutines (Корутины) — специальные функции, которые могут приостанавливать своё выполнение с помощью ключевых слов async и await.
Tasks (Задачи) — обёртка над корутинами, позволяющая планировать их выполнение в цикле событий.
Futures (Будущие объекты) — представляют собой результат асинхронной операции, который станет доступным в будущем.
Можно указать на сомнительную производительность и высокую сложность Asyncio и сказать, что лучше пользоваться другими DE-инструментами, а AsyncIo использовать только в случае если необходимо работать с каким-нибудь Web API и для этого нет более подходящих инструментов, чем обычно занимается Back-end разраб.
ООП, основные принципы
Объектно-Ориентированное Программирование - важно рассказать про 4 основных принципа (иногда три, исключают абстракцию, не холиварьте по этому поводу на собесе)
Инкапсуляция
Скрытие внутренней реализации объекта и предоставление доступа к данным только через методы.
Обеспечивает защиту данных и контроль над их изменением.
Де-факто в Python не существует способа сделать метод или объект приватным, но в среде разработчиков принято такие методы всё равно обозначать двумя подчёркиваниями с каждой стороны ( __method__ ) и не менять их вне класса, чтобы соблюдать инкапсуляцию
Наследование
Механизм, позволяющий одному классу (потомку) использовать и расширять функциональность другого класса (родителя).
Обеспечивает повторное использование кода и создание иерархии классов
Тут важно упомянуть про функцию .super() - которая позволяет обращаться к родительскому классу
Полиморфизм - это про взаимодействие функции или класса с разными типами данных и в разных условиях +- одинаково. Например функция len() может возвращать длину строки, или кол-во элементов в списке, получая на вход разные форматы данных(строку, список или ещё что-нибудь). В случае с классами, приведу пример
class Cat: def make_sound(self): print("Meow") class Dog: def make_sound(self): print("Bark") cat1 = Cat() dog1 = Dog() for animal in (cat1, dog1): animal.make_sound() При выполнении код выведет: Meow Bark
Таким образом мы использовали два разных метода с одним и тем же названием на разных классах и получили логически схожий результат, ожидаемый. Это пример полиморфизма классов
Абстракция
Это когда мы переходим от конкретных свойств каждого класса к общим, которые объединяют несколько классов
Пример:
Есть Data Vault модель, таблицы которой описаны классами в Python. В ней есть три сущности Hub, Link, Satellite. У них есть названия - Seller_Hub, Sell_Receipt_Link, Seller_Satellite и какие-то другие общие характеристики, например наличие бизнес-ключа. Когда мы понимаем, что у всех классов есть общие характеристики логическая связь, мы можем создать новый класс-абстракцию, Entity, которая в себе будет содержать все общие характеристики сущностей модели Data Vault(В нашем случае - название и бизнес-ключ). Такой же абстракцией может быть сущность Hub над классами Seller_Hub, Buyer_Hub, Car_Hub
immutable vs mutable
Как вариант можно перечислить: set, dict, list - изменяемые
bool, float, int, str, frozenset, tuple - неизменяемые
Будет круто добавить, что изменяемые всегда лежат в одной и той же ячейке памяти(id() от объекта вернёт всегда один результат, а НЕизменяемые - нет.
Неизменяемые не всегда на 100% не меняются, например:
t = ('holberton', [1, 2, 3]) t[1].append(4) print(t)
Выведет: (‘holberton’, [1, 2, 3, 4])
Cписок внутри кортежа изменился - потому что он изменяемый. Но сам кортеж продолжает ссылаться на тот же список и таким образом он “не изменился”
Также можно добавить, что неизменяемые объекты могут быть ключом в словаре, а изменяемые нет и на неизменяемые можно вешать магический метод __hash__ и по ним быстрее идёт поиск