Python Flashcards
В чём заключается разница между конкурентностью (concurrency) и параллельностью (parallelism)? Какие подходы к многопоточности и многопроцессности существуют в Python?
Теги: (собес)
- Конкурентность (Многопоточность) — Систему называют конкурентной, если она может поддерживать наличие двух или большего количества действий, находящихся в процессе выполнения в одно и то же время. Конкурентные системы ограничены скоростью подсистемы ввода/вывода (I/O bound). В Python имеется два механизма, которыми можно воспользоваться для организации конкурентного выполнения кода: Многопоточность, Модуль threading.
- Как работает threading?
– Python создает отдельные потоки, и планировщик ОС решает, какой поток исполнять в данный момент. Но из-за GIL только один поток выполняет Python-код в момент времени.
– Потоки (threads) работают внутри одного процесса.
– Все потоки разделяют память (переменные, ресурсы).
– Но! GIL (Global Interpreter Lock) мешает выполнять Python-код в нескольких потоках одновременно (реально работает только один поток в любой момент времени).
– Подходит для: I/O-bound задач (загрузка файлов, сетевые запросы, работа с БД).
– Создаётся несколько потоков, и они конкурируют за исполнение кода.
– Если в коде есть I/O (sleep, работа с файлами, сетью), потоки не блокируют друг друга. - Почему работает с GIL: GIL временно освобождается при выполнении операций ввода-вывода, что позволяет другим потокам выполняться в это время. Но для CPU-bound задач многопоточность неэффективна.
import threading
import time
def task(name):
print(f”Task {name} started”)
time.sleep(2) # Имитация задержки (например, ожидание ответа сервера)
print(f”Task {name} finished”)
threads = []
for i in range(5):
t = threading.Thread(target=task, args=(i,))
threads.append(t)
t.start()
for t in threads:
t.join() # Ждём завершения всех потоков
print(“All tasks completed”)
- Параллельность (Многопроцессность) — Систему называют параллельной, если она может поддерживать наличие двух или большего количества действий, выполняемых в точности в одно и то же время. Параллельные системы ограничены возможностями центрального процессора (CPU bound). Возможность параллельного выполнения кода: Модуль multiprocessing.
- Как работает multiprocessing?
– Каждый процесс работает независимо, без GIL.
– Они не разделяют память (нужно использовать multiprocessing.Queue для обмена данными).
– Можно задействовать все ядра процессора.
- Почему работает с GIL:
Каждый процесс запускает свой собственный интерпретатор Python и имеет собственную копию памяти, поэтому GIL не влияет на многопроцессные программы.
from multiprocessing import Process, cpu_count
import time
def task(name):
print(f”Task {name} started”)
time.sleep(2) # Имитация вычислений
print(f”Task {name} finished”)
processes = []
for i in range(cpu_count()): # Количество процессов = кол-ву ядер процессора
p = Process(target=task, args=(i,))
processes.append(p)
p.start()
for p in processes:
p.join() # Ждём завершения всех процессов
print(“All tasks completed”)
Что такое GIL в Python, как он влияет на работу потоков и почему?
Теги: #wildberries #okko
Global Interpreter Lock (GIL) — это механизм интерпретатора CPython (самого распространённого Python‑runtime), который гарантирует, что в каждый момент времени только один поток выполняет байткод Python.
Проще говоря, GIL — это глобальная блокировка на уровне интерпретатора, которая «препятствует» одновременному исполнению байткода несколькими потоками. При этом потоки могут переключаться между собой (операционная система даёт квант времени), но внутри самого Python‑интерпретатора выполнится только один поток.
Почему так сделали?
Внутренние структуры интерпретатора Python не являются потокобезопасными (например, управление памятью, сборка мусора). GIL упрощает реализацию, не вынуждая разработчиков окружать каждую структуру данными mutex’ами. Это историческое решение, призванное упростить и ускорить разработку CPython.
Влияние на работу потоков:
Если задача «процессорная» (многочисленные вычисления на Python уровне), прирост от использования нескольких потоков минимален, потому что GIL не позволяет реально параллелить Python‑код на нескольких ядрах. Если задача «I/O bound» (много операций ввода‑вывода, ожидание сети/диска), использование потоков может дать выигрыш (пока один поток «блокируется» на I/O, интерпретатор переключается на другой поток). Для реального CPU‑параллелизма в Python обычно прибегают к многопроцессной архитектуре (модуль multiprocessing), либо используют расширения (например, NumPy, написанную на C), которые могут обходить GIL для своих внутренних вычислений.
Как устроено асинхронное программирование в Python? Что такое async/await, event loop и как это связано с GIL?
Теги: #okko
Асинхронное программирование в Python реализовано через механику корутин и ключевые слова async/await. Вместо создания потоков Python использует один поток и чередует задачи вручную. В двух словах:
Event loop (цикл событий) Это объект, который управляет выполнением набора корутин (асинхронных задач). Цикл событий последовательно «пробуждает» корутины, когда у них есть данные для обработки, и «засыпает» их, когда они ждут I/O. В стандартной библиотеке: asyncio.get_event_loop() или создание asyncio.new_event_loop(). Корутины Функции, определённые через async def, внутри которых можно использовать await. await говорит интерпретатору, что в этот момент корутина уступает управление циклу событий (например, когда ждёт ответ из сети). import asyncio import asyncpg import time
tables = [“users”, “orders”, “products”]
async def fetch_data(table, conn):
print(f”Отправка запроса в {table}…”)
result = await conn.fetchval(f”SELECT COUNT(*) FROM {table}”)
print(f”Таблица {table}: {result} записей”)
async def main():
start = time.time()
conn = await asyncpg.connect( database="mydatabase", user="myuser", password="mypassword", host="localhost", port="5432" ) # Запускаем запросы параллельно tasks = [fetch_data(table, conn) for table in tables] await asyncio.gather(*tasks) await conn.close() print(f"Время выполнения: {time.time() - start:.2f} сек")
asyncio.run(main())
Где GIL? Даже в asyncio мы всё ещё имеем дело с однопоточным выполнением кода, потому что GIL мешает параллельному выполнению байткода. Асинхронность в Python достигается за счёт переключения корутин во время ожидания I/O, а не одновременной работы на нескольких ядрах. Тем не менее, для задач «I/O bound» это часто эффективно, потому что мы не ждём блокирующих вызовов, а переключаемся на другие задачи.
Таким образом, async/await + event loop позволяют писать конкурентный код, но не параллельный (на уровне процессора) — если говорить о чистом Python. Параллельность по CPU достигается через процессы (или нативный код, выходящий за пределы GIL).
Что такое хэш-функция и где она применяется?
Теги: (собес)
Хэш-функция — это функция, которая из произвольного набора данных (строка, файл и т.д.) вычисляет короткое числовое (или строковое) значение фиксированного размера. Основные свойства хорошей хэш-функции:
Односторонность (для криптографических функций): сложно (или невозможно за разумное время) восстановить исходные данные из хэша. Низкая вероятность коллизий: разные входные данные с небольшой вероятностью дают одинаковый хэш. Быстрота вычисления (в большинстве случаев).
Применения:
Структуры данных (например, dict и set в Python используют хеши для ключей) — это даёт амортизированное O(1) время доступа. Криптография: контроль целостности (SHA256, MD5 и т.д. для проверки, не был ли файл изменён). Поиск дубликатов: в больших наборах данных иногда используют хэш (хэш-суммы) для сравнения.
В Python у каждого объекта (если он хешируемый) есть метод __hash__(), который участвует в работе словарей и множеств..
Что такое бинарные деревья и сбалансированные деревья? В чём их различия и зачем нужно балансировать дерево?
Теги: (собес)
Что такое бинарные деревья и сбалансированные деревья? В чём их различия и зачем нужно балансировать дерево?
Бинарное дерево Структура, в которой каждая вершина имеет не более двух потомков (левый и правый ребёнок). Наиболее распространённые операции: вставка, поиск, удаление. Сбалансированное дерево Бинарное дерево считается «сбалансированным», если высота левого и правого поддерева для всех узлов не сильно различается (в классическом AVL-дереве — разница высот поддеревьев не больше 1). Примеры структур: AVL, Red-Black Tree, B‑деревья (не совсем бинарные, но тоже сбалансированные).
Зачем балансировка?
В несбалансированном бинарном дереве (особенно если данные вставлялись уже отсортированными) дерево может выродиться в список с высотой ~n. Тогда операции поиска/вставки/удаления будут O(n). Сбалансированные деревья стараются удерживать высоту ~O(log n), давая логарифмическое время на основные операции.
Таким образом, балансировка обеспечивает устойчиво высокую производительность при работе со структурой на больших объёмах данных.
Как оценивать сложность алгоритмов ? Какие существуют основные классы сложности?
Теги: (собес)
Как оценивать сложность алгоритмов (Big O)? Какие основные классы сложности существуют?
Big O — нотация, показывающая, как изменяется время (или память) работы алгоритма в зависимости от размера входных данных n. Основные классы:
O(1): Константная сложность. Время выполнения алгоритма не зависит от размера входных данных. Например, доступ к элементу массива по индексу. O(log n): Логарифмическая сложность. Время выполнения алгоритма растет медленно с увеличением размера входных данных. Например, бинарный поиск в отсортированном массиве. O(n): Линейная сложность. Время выполнения алгоритма пропорционально размеру входных данных. Например, просмотр всех элементов в массиве. O(n log n): Линейно-логарифмическая сложность. Время выполнения алгоритма растет быстрее, чем линейно, но медленнее, чем квадратично. Например, сортировка слиянием (merge sort). O(n^2): Квадратичная сложность. Время выполнения алгоритма зависит от квадрата размера входных данных. Например, сортировка пузырьком (bubble sort). O(n^3): Кубическая сложность. Время выполнения алгоритма зависит от размера входных данных в кубе. Например, алгоритмы, которые имеют три вложенных цикла, такие как некоторые методы многомерной обработки данных. O(n!): Факториальная сложность. Это самая высокая степень роста времени выполнения алгоритма. Время выполнения алгоритма растет факториально от размера входных данных. Этот тип сложности встречается, например, при переборе всех возможных комбинаций элементов, что делает его чрезвычайно неэффективным для больших значений n.
Почему это нужно?
Позволяет понять, как алгоритм «масштабируется» при увеличении объёма данных. На собеседованиях важно уметь определять Big O для базовых операций структуры данных (списки, словари, деревья) и базовых алгоритмов (сортировки, поиска).
В чём различие между поверхностным копированием (copy) и глубоким копированием (deepcopy) объектов в Python?
Теги: (собес)
- Поверхностное (copy): Создаёт новый объект, но вложенные элементы (например, списки внутри списка) остаются общими для оригинала и копии. Если изменить вложенный элемент в копии, то изменение отразится и в оригинале. Вложенные объекты (например, списки внутри списков) не копируются, а просто передаются по ссылке. Метод: copy.copy(obj)
- Глубокое (deepcopy): Создаёт новый объект и новые копии всех вложенных объектов. Оригинал и копия становятся полностью независимыми — изменение одного не влияет на другой.
Какие типы данных в Python являются изменяемыми (mutable), а какие — неизменяемыми (immutable)?
Теги: #lamoda #Cian #Билайн #wildberries #betboom
В Python все данные представлены в виде объектов, и у каждого объекта есть идентификатор (id), тип (type) и значение (value).
Разделение объектов на изменяемые (mutable) и неизменяемые (immutable) определяет, можно ли менять их значение после создания, не изменяя идентификатор
- Неизменяемые: int, float, bool, str, tuple, frozenset.
- Изменяемые: list, dict, set и т.д.
Почему это важно?
Неизменяемые объекты легче хешировать (можно использовать как ключи в dict). Изменяемые объекты нельзя использовать как ключ словаря (если меняется объект, меняется и его хэш). При работе со строками нужно помнить, что любая «изменение» строки фактически создаёт новую строку.
Что произойдёт, если использовать список в качестве ключа и изменить его? Можно ли использовать инстанс класса как ключ?
Теги: #lamoda #Cian #Билайн #wildberries #betboom
Что произойдёт, если использовать список в качестве ключа и изменить его? Можно ли использовать инстанс класса как ключ?
Список в качестве ключа В Python нельзя напрямую использовать список как ключ словаря, потому что список — изменяемый объект и не имеет корректной реализации \_\_hash\_\_. Если попробовать d[[1,2,3]] = "test", получите TypeError: unhashable type: 'list'. Изменение списка, который чудом оказался ключом Если бы (теоретически) список был хешируемым, а затем мы изменили его, это нарушило бы принцип неизменности хэша. Словарь не смог бы найти элемент по этому ключу. В реальности Python это запрещает. Инстанс класса как ключ Можно, если у класса определён (или унаследован от родителя) корректный метод \_\_hash\_\_(), а также \_\_eq\_\_(). При этом объекты класса должны вести себя как неизменяемые в контексте полей, которые участвуют в хэшировании. По умолчанию, объекты пользовательских классов хешируются по id объекта (адрес в памяти), что фактически работает, но если вы меняете поля, \_\_eq\_\_ может быть неконсистентен с \_\_hash\_\_. Для правильного поведения нужно аккуратно определить эти методы.
Итог:
Списки нельзя использовать как ключ из‑за их «mutability» и отсутствия хеша. Инстансы классов — можно, если правильно реализована хешируемость и неизменность значимых полей.
+Что можно и нельзя использовать как ключ словаря
Можно всё, что является неизменяемым и у чего реализован магический метод __hash__(), т.е. что при передаче в некоторую хэш-функцию вернёт хэш детерминированным образом. Важно, чтобы при обращении к словарю через ключ можно было посчитать хэш от ключа и найти связанное с ним значение (value) или точно знать, что такого ключа в словаре нет.
Какие есть подходы для расширения логики в коде? В чём плюсы и минусы первого подхода vs второго?
Теги: #Cian
- Наследование (extends): класс может унаследовать — использовать по умолчанию — переменные и методы своего предка. Наследование транзитивно: класс может наследоваться от другого класса, который наследуется от третьего, и так далее вплоть до базового класса (обычно — Object), возможно, неявного.
Плюсы:
* ✅ Повторное использование кода: подкласс автоматически получает все поля и методы базового класса, что снижает дублирование кода.
* ✅ Полиморфизм: можно использовать объект подкласса там, где ожидается объект базового класса. Например, если есть метод, принимающий базовый класс, он сможет работать и с любым подклассом без изменений.
Минусы:
* ❌ Жёсткая связь: изменения в базовом классе могут нарушить работу всех подклассов.
* ❌ Сложные иерархии: если иерархия становится глубокой, код труднее поддерживать и понимать.
- Композиция (has-a): Композиция — это когда один объект предоставляет другому свою функциональность частично или полностью. Композиция в Python Вместо наследования, когда один класс является потомком другого, композиция предполагает, что один класс включает в себя объект другого класса и делегирует ему выполнение некоторых задач.
class Engine:
def __init__(self, horsepower):
self.horsepower = horsepower
def start(self): return "Двигатель запущен"
class Car:
def __init__(self, brand, engine):
self.brand = brand
self.engine = engine # Композиция — включаем объект Engine внутрь Car
def start(self): return f"{self.brand}: {self.engine.start()}"
Создаем объект двигателя
engine = Engine(150)
Создаем объект автомобиля, передавая в него двигатель
car = Car(“Toyota”, engine)
print(car.start()) # Вывод: Toyota: Двигатель запущен
Плюсы:
* ✅ Гибкость: можно “собрать” объект из множества других объектов, изменяя поведение во время выполнения.
* ✅ Слабая связь: изменение одного компонента не затрагивает другие классы, которые его используют.
* ✅ Повторное использование: один и тот же компонент можно использовать в разных классах.
Минусы:
* ❌ Больше кода: требуется писать дополнительный код для связи между объектами (например, через делегирование методов).
* ❌ Может быть менее очевидным: взаимодействие между объектами через композицию иногда сложнее понять, чем простую иерархию.
* ❌ Управление зависимостями: нужно следить за созданием и жизненным циклом вложенных объектов.
Что произойдёт, если в функции указать изменяемый объект (например, список) в качестве значения по умолчанию? Когда происходит инициализация таких объектов? Почему это может приводить к нежелательным эффектам?
Теги: #Cian
Если в функции указать изменяемый объект (например, список) в качестве значения по умолчанию, он создаётся только один раз — в момент объявления функции, а не при каждом её вызове. Это значит, что все вызовы функции будут использовать один и тот же объект. Если изменить этот объект в одном вызове (например, добавить элемент в список), изменения сохранятся и будут видны при следующих вызовах. Это может привести к неожиданным ошибкам.
Пример с неожиданным поведением:
def add_item(item, my_list=[]):
my_list.append(item)
return my_list
print(add_item(1)) # Выведет: [1]
print(add_item(2)) # Выведет: [1, 2] — неожиданный результат!
print(add_item(3)) # Выведет: [1, 2, 3]
Что такое итератор в Python, как работает протокол итерации? Чем генератор отличается от итератора и как они реализуются “под капотом”?
Теги: #Cian #Yandex
- Итератор — это объект, который реализует методы __iter__() (возвращает сам себя) и __next__() (возвращает следующий элемент или возбуждает StopIteration).
Итерируемые объекты — это объекты, у которых есть метод __iter__(), возвращающий итератор. Все последовательности являются итерируемыми объектами, но не все итерируемые объекты являются последовательностями.
Последовательности — это итерируемые объекты, элементы которых упорядочены и доступны по индексу. К ним относятся списки, кортежи, строки, range. Помимо последовательностей, итерируемыми также являются множества, словари, файлы и другие коллекции, но они не гарантируют сохранение порядка элементов.
- Протокол итерации: при проходе циклом for или вызове iter(), Python запрашивает у объекта итератор (__iter__), а затем вызывает у итератора метод __next__, пока не будет получен StopIteration.
- Генератор – это частный случай итератора, создаваемый с помощью yield, который автоматически запоминает своё состояние (место последнего вызова yield) и вычисляет значения лениво, то есть по мере запроса, не загружая всю последовательность в память, что делает его более эффективным для работы с большими данными.Генератор автоматически реализует протокол итерации, не требуя писать методы вручную. Генератор это функция, в которой вместо 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)
Что такое декораторы в Python, как они работают и как можно реализовать декоратор без синтаксического сахара (@)?
Теги: #Cian #Билайн
- Декораторы — это функции (или объекты, ведущие себя как функции), которые принимают на вход другую функцию и возвращают обёрнутую функцию с дополнительной логикой.
- Работают за счёт того, что интерпретатор при виде @decorator фактически выполняет func = decorator(func).
- Без “@” декоратор можно применить напрямую:
def decorator(func):
def wrapper(args, **kwargs):
# дополнительная логика
return func(args, **kwargs)
return wrapper
def my_func():
pass
my_func = decorator(my_func) # без синтаксического сахара
Чем Pandas отличается от Apache Spark? В каких случаях лучше использовать Pandas, а в каких — Spark?
Теги: #Билайн
- Pandas: библиотека для анализа данных на одной машине. Данные хранятся в оперативной памяти, удобный API для обработки средних и небольших наборов данных.
- Apache Spark: распределённая система обработки данных на кластере. Подходит для огромных объёмов (Big Data), требующих масштабирования на несколько узлов.
- Используйте Pandas для локального анализа относительно небольших данных. Берите Spark, когда объём данных не помещается в память одной машины или нужен параллелизм/распределённость.
Как в Python устроена обработка исключений? Какие есть блоки, и в каком порядке они выполняются? Можно ли отлавливать ошибки определённого типа?
Теги: #Билайн
- try: блок кода, в котором может возникнуть ошибка.
- except: обрабатывает исключение, если оно произошло. Можно указать конкретный тип (например, except ValueError:) или несколько типов.
- else: выполняется, если в блоке try не произошло исключений.
- finally: выполняется всегда (и при ошибке, и при её отсутствии).
- Порядок: try → при ошибке → except, если нет ошибок → else, в любом случае → finally.
Какие основные принципы ООП применяются в Python и как именно?
Теги: #Билайн
- Инкапсуляция — это когда объект хранит данные внутри себя и не даёт их менять напрямую, но разрешает работать с ними через специальные методы. Допустим, у нас есть объект список (list).
Его элементы хранятся внутри, но мы не можем напрямую управлять тем, как они там лежат. Зато у нас есть методы:.append(10) – добавляет элемент
.remove(3) – удаляет элемент
.sort() – сортирует
Мы не лезем внутрь списка, а просто вызываем готовые методы. Это и есть инкапсуляция! Инкапсуляция скрывает данные и контролирует доступ к ним (как банкомат).
- Наследование: Наследование позволяет создавать новые классы на основе существующих. Класс-наследник получает все свойства и методы родительского класса и может добавлять новые или переопределять старые. Это упрощает повторное использование кода.
- Полиморфизм: это возможность использовать одинаковые методы для объектов разных классов. Например, встроенная функция len() работает и со списками, и со строками, и с кортежами, возвращая их длину.
- Абстракция позволяет скрыть детали реализации и дать только важный интерфейс. Python активно использует это в стандартных классах и библиотеках. Пример 1: Файловая система (open())
Когда мы открываем файл:
with open(“data.txt”, “r”) as f:
content = f.read()
Мы не думаем о том, как Python работает с файловыми дескрипторами или буферизацией.
Python абстрагирует детали, давая нам понятный интерфейс – open(), read(), write(). Абстракция скрывает сложность и даёт удобный интерфейс (как requests.get() вместо работы с TCP/IP). Они похожи, но инкапсуляция больше про защиту данных, а абстракция — про упрощение работы.
Какие парадигмы программирования, кроме ООП, вы знаете? Что такое функциональное программирование и как его идеи реализуются в Python?
Теги: #Билайн
- Процедурное программирование — это подход, при котором программа строится из последовательных инструкций, объединённых в процедуры или функции. Код выполняется пошагово, а данные хранятся в переменных. Например, простая функция в Python для вывода приветствия — это пример процедурного программирования.
- Императивное программирование делает акцент на описании того, как программа должна работать. Оно подразумевает работу с состоянием программы через переменные, использование циклов, условий и инструкций. Процедурное программирование является частным случаем императивного. В Python императивный стиль часто проявляется через циклы и операции над списками. Например, чтобы возвести каждый элемент списка в квадрат, мы можем использовать цикл for и изменяемую переменную для накопления результата.
- Декларативное программирование, в отличие от императивного, фокусируется на описании того, что нужно получить, а не как это сделать. Примеры декларативного подхода включают SQL, регулярные выражения и шаблоны HTML. В Python декларативный стиль может проявляться через list comprehensions, где мы описываем желаемый результат, не углубляясь в детали выполнения. Например, [x ** 2 for x in numbers if x % 2 == 0] создаёт новый список квадратов чётных чисел из исходного списка.
- Функциональное программирование (ФП) — это парадигма, в которой вычисления выражаются как применение функций к аргументам. Основные принципы ФП — это использование чистых функций (которые не изменяют внешние переменные и всегда дают одинаковый результат для одних и тех же аргументов), неизменяемость данных (immutable), отсутствие побочных эффектов, композиция функций и активное использование рекурсии вместо циклов. В Python функциональные концепции поддерживаются частично, но вполне достаточно для практического применения.
Что такое контекстный менеджер в Python, как он реализуется с помощью with? Можно ли “сымитировать” контекстный менеджер без with и каким образом?
Теги: #Билайн
- Контекстный менеджер — объект с методами __enter__ и __exit__, позволяющий безопасно выделять и освобождать ресурсы (например, открывать и закрывать файлы). Используется при работе с контекстами – соединениями к БД или 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’)
- Использование через with: при входе в блок вызывается __enter__, а при выходе (включая исключения) — __exit__.
- Без with можно вручную вызывать методы __enter__ и __exit__, но with более удобен и безопасен, т.к. гарантирует освобождение ресурсов.
Какова временная сложность операций (поиск, вставка, удаление) в dict и set в Python? За счёт чего достигается такая эффективность?
Теги: #Yandex
- В среднем O(1) для поиска, вставки и удаления.
- Эффективность достигается за счёт хеширования ключей и размещения их в «хеш-таблицах». При хороших хеш-функциях коллизии минимальны, и операции работают за постоянное время.
Что такое PyTest и для чего он используется? Какие преимущества у PyTest по сравнению со стандартным unittest?
Теги: #Rubbles
PyTest — это инструмент для автоматизированного тестирования в Python. Он помогает проверять, правильно ли работает код, автоматически запуская тесты.
🔹 Главные преимущества PyTest перед unittest:
✔ В PyTest тесты — это просто функции, тогда как в unittest необходимо наследоваться от unittest.TestCase.
✔ В PyTest не нужно использовать методы self.assertEqual, self.assertTrue и другие. Достаточно использовать стандартные assert
✔ Автоматический запуск — PyTest сам находит тесты в проекте.
✔ Гибкость — можно настраивать запуск тестов и повторно использовать данные с помощью системы фикстур
✔ Дополнительные возможности — плагины для параллельного выполнения тестов и красивых отчетов.
📌 Пример теста:
Представим, что есть функция, которая считает сумму чисел. Тест проверяет, что при сложении 2 + 3 получается 5. Если результат совпадает — тест проходит, если нет — показывает ошибку.
Что выведет следующий код и почему? Что здесь не так и как это исправить?
def func(a: int, b: list[int] = []):
b.append(a)
print(b)
func(1)
func(2)
Теги: #Cian
[1]
[1, 2]
* потому что список по умолчанию создаётся один раз при объявлении функции и сохраняется между вызовами.
* Чтобы избежать накопления значений, нужно использовать None и создавать новый список внутри:
def func(a, b=None):
if b is None:
b = []
b.append(a)
print(b)
Почему передавать в качестве параметра метода какой-то “тяжёлый” класс (например, Client) так, как показано ниже, считается плохой практикой?
def __init__(self, client: Client = Client()):
…
Теги: #Cian
Тут в Client() создаётся объект один раз при загрузке кода. Если несколько объектов Service не передадут свой client, они будут использовать один и тот же клиент, что может привести к проблемам.
- Объект создаётся один раз при загрузке модуля
В Python значения по умолчанию вычисляются один раз при загрузке модуля, а не при каждом вызове функции или метода. Это означает, что один и тот же экземпляр Client будет использоваться во всех новых объектах, где не передан свой client.
Пример:
class Client:
def __init__(self):
self.id = id(self)
class Service:
def __init__(self, client: Client = Client()):
self.client = client
s1 = Service()
s2 = Service()
print(s1.client.id == s2.client.id) # True (один и тот же объект!)
Это нежелательное поведение, так как изменения в client одного объекта затронут все другие объекты, использующие значение по умолчанию.
2 Потенциальные побочные эффекты
Если Client выполняет сетевые запросы, читает файлы, открывает соединения с базой данных или создаёт сложные объекты, это создаст непредсказуемое поведение.
Пример: если Client открывает соединение с БД, то один и тот же объект будет переиспользоваться в разных экземплярах Service, что может привести к проблемам с многопоточностью или неожиданным закрытиям соединения.
Как исправить?
1. Использование None и явного создания объекта
Лучший вариант — передавать None и создавать объект внутри __init__, если его не передали:
class Service:
def __init__(self, client: Client = None):
self.client = client if client else Client() # Создаём объект при каждом вызове конструктора
Теперь каждый объект Service получит новый экземпляр Client, если не передан свой.
Какие основные типы данных есть в Python?
Теги: (собес)
- Числовые: int, float, complex.
- Строки: str.
- Логический тип: bool (True/False).
- Последовательности: list, tuple, range.
- Множества: set, frozenset.
- Словари: dict.
- NoneType: None.
- Пользовательские классы (объекты), в том числе из стандартной библиотеки и сторонних модулей.
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’,)
Обращаться к элементам нужно по индексам, как в списках.
Как работает память в питоне? #HalltapeRoadmapDE
Память в Python управляется автоматически с помощью сборщика мусора (Garbage Collector, GC) и системы счётчика ссылок (Reference Counting).
🔹 Основные механизмы:
Счётчик ссылок Каждый объект в Python имеет счётчик ссылок (sys.getrefcount()). Когда счётчик ссылок падает до 0 — объект удаляется. Сборщик мусора (GC) сборщик мусора (Garbage Collector, GC)
Python использует автоматический сборщик мусора для управления памятью и удаления объектов, которые больше не используются.
Основные механизмы:
Удаление циклических ссылок Если два или более объекта ссылаются друг на друга, но больше нигде не используются, они могут остаться в памяти. GC находит такие циклы и удаляет их. Поколенческий алгоритм (Generational GC) Python делит объекты на три "поколения": Gen 0 (молодые объекты): быстро проверяются и удаляются при первом проходе. Gen 1, Gen 2 (долгоживущие объекты): проверяются реже, так как считаются более стабильными. Этот подход ускоряет работу, так как свежесозданные объекты чаще подвержены удалению, чем те, что используются дольше.