Собеседование
В Python итераторы и генераторы предоставляют возможность эффективного итерирования по элементам последовательности без необходимости загружать все элементы в память одновременно. Они позволяют обрабатывать большие объемы данных или бесконечные последовательности с небольшим использованием памяти. Итераторы: Итератор в Python - это объект, который реализует методы `__iter__()` и `__next__()`. Метод `__iter__()` возвращает сам объект итератора, а метод `__next__()` возвращает следующий элемент последовательности. Когда все элементы последовательности исчерпаны, `__next__()` должен вызвать исключение `StopIteration`. Пример кода:
class MyIterator:
def __iter__(self):
return self
def __next__(self):
# возвращаем последовательность элементов
# до возникновения исключения StopIteration
raise StopIteration
# использование итератора
my_iter = MyIterator()
for item in my_iter:
print(item)
Генераторы: Генераторы в Python - это функции или выражения, которые используют ключевое слово `yield` для возврата элемента из последовательности, а затем приостанавливают свое исполнение. При следующем вызове функции генератора выполнение начинается с позиции, где оно остановилось. Пример кода:
def my_generator():
for i in range(5):
yield i
# использование генератора
gen = my_generator()
for item in gen:
print(item)
Основная разница между итераторами и генераторами состоит в способе их создания и использования. Итераторы обычно реализуются с помощью создания класса, который имеет методы `__iter__()` и `__next__()`. Генераторы же создаются с использованием функции или выражения, в котором присутствует ключевое слово `yield`. Генераторы проще и компактнее в использовании, поскольку итераторы требуют создания отдельного класса и объявления методов. Генераторы также обычно более эффективны с точки зрения использования памяти, так как они не загружают все элементы последовательности в память сразу.
PostgreSQL
Что такое транзакция? Приведите пример, где это может пригодиться. Расскажите про свойства транзакций и уровень изолированности.
Транзакция объединяет последовательность действий в одну операцию и обеспечивает выполнение либо всех действий из последовательности, либо ни одного. Канонический пример — списывание денег с одного счета и зачисление на другой, что требует два update-а, которые гарантированно должны выполниться или не выполниться вместе.
Что такое VACUUM и зачем он нужен в PostgreSQL?
Команда VACUUM высвобождает пространство, занимаемое «мертвыми» кортежами, что актуально для часто используемых таблиц. При обычных операциях в Postgres кортежи, удаленные или устаревшие в результаты обновления, физически не удаляются, а сохраняются в таблице до очистки.
Что такое EXPLAIN? Какая разница между ним и EXPLAIN ANALYZE?
EXPLAIN ANALYZE – в отличие от просто EXPLAIN не только показывает план выполнения запроса, но и непосредственно выполняет запрос и показывает реальное время выполнения
map, filter, reduce - функции высшего порядка (high-order func)
map
применяет функцию ко всем элементам итерируемой последовательности
map(func, list)
на выходе обработанный итерируемый объект
filter
применяет функцию, возвращающую true или false, ко всем элементам итерируемой последовательности
на выходе получается обработанный итерируемый объект, состоящий только из обработанных элементов, для которых функция вернула True
def func(x)
return x%2 == 0
filter(func, list)reduce
применяет функцию к каждой паре элементов итерируемой последовательности до тех пор, пока в последовательности не останется одно число. (функция принимает 2 арг, возвращает 1)
В python 3 и выше reduce вынесли из стандартных функций и теперь она в functools
from functools import reduce def add(x, y): return x + y
numbers = [1, 2, 3, 4, 5] result = reduce(add, numbers) print(result) # 15
Алгоритмы MRO (Method Resolution Order) в Python2 и Python3
В Python2 используя простой алгоритм C3 для определения порядка наследования классов. При этом алгоритме методы дочерних классов имеют высший приоритет, по сравнению с родительскими. Затем в приоритете первый родительский класс, затем второй и т.д.
В Python3 же используется более сложный линеаризированный алгоритм C3, который помогает устранить конфликты, связанные с именами методов и атрибутов.
Python3 MRO:
class A:
pass
class B(A):
pass
class C(A):
pass
class D(B, C):
pass
print(D.__mro__)
(<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)Это означает, что при вызове методов в классе `D`, сначала будут просмотрены методы в `B`, затем в `C`, затем в `A` и, наконец, в базовом классе `object`.
Кроме того, в Python 3.x `super()` возвращает объект-прокси, который управляет не только текущим классом, но и его родительскими классами. В Python 2.x, `super()` возвращает объект-прокси только для текущего класса. Это связано с изменением алгоритма MRO в Python 3.
Функция `super()` используется для обращения к методам предков в иерархии наследования классов. Это очень полезно при множественном наследовании, когда нужно вызвать метод одного из родительских классов, но неизвестно, какой именно. Например, если класс `A` наследуется от классов `B` и `C` и оба класса имеют метод `foo()`, то можно вызвать `foo()` из класса `B` следующим образом:
class B:
def foo(self):
print('B.foo()')
class C:
def foo(self):
print('C.foo()')
class A(B, C):
def foo(self):
super().foo()
В этом примере метод `foo()` класса `A` делает вызов `super().foo()`, который передает вызов родительскому классу `B` метода `foo()`. Если метод `foo()` не был найден в классе `B`, то далее по списку значений атрибутов мы попытаемся найти его в классе `C`.
copy() и copy.deepcopy()
При поверхностном копировании объекта (с помощью метода `copy()`) новые объекты создаются только для первого уровня вложенности атрибутов объекта. Это означает, что при копировании объектов, которые имеют внутренние объекты (например, списки в списке), копии создаются только для самого внешнего уровня объектов. Атрибуты вложенных объектов будут ссылаться на те же объекты, что и оригинал. Пример:
a = [[1, 2], [3, 4]] b = a.copy() a[0][0] = 0 # изменяем оригинал print(b) # выводится [[0, 2], [3, 4]], так как копия ссылается на один и тот же внутренний список [1, 2]
При глубоком копировании объекта (с помощью метода `deepcopy()`), новые объекты создаются для всех уровней атрибутов объекта. Это означает, что копии создаются для всех объектов внутри внешнего объекта. Объекты, которые уже были созданы при копировании других вложенных объектов, не будут повторно копироваться. Пример:
import copy a = [[1, 2], [3, 4]] b = copy.deepcopy(a) a[0][0] = 0 # изменяем оригинал print(b) # выводится [[1, 2], [3, 4]], так как была создана полная копия оригинала
Потоки и процессы в Python. GIL (Global Interpreter Lock)
В Python есть возможность создавать параллельные задачи через потоки (threads) и процессы (processes).
Потоки - это легковесные задачи, которые выполняются внутри процесса. Они могут использовать разделяемые ресурсы и подзадачи могут выполняться параллельно. Однако в Python есть ограничение - Global Interpreter Lock или GIL. GIL - механизм защиты, который предоставляет управление глобальной блокировкой интерпретатора. Каждый поток в Python выполняется внутри своего GIL, и в одно и то же время может выполняться только один поток. Таким образом, в Python потоки не могут полностью использовать многопоточность на мульти-ядерных процессорах. Вместо этого параллельная работа потоков может происходить только в отдельных сегментах времени, что в целом уменьшает эффективность использования ресурсов процессора.
Процессы - это отдельные экземпляры программ, которые обладают своим пространством памяти и выполняются параллельно друг другу на многоядерных процессорах. Это позволяет более эффективно использовать потенциал процессора, но требует более сложной обработки и связи между процессами.
Asyncio
Библиотека `asyncio` в Python не использует ни процессы, ни потоки. Вместо этого, она предоставляет асинхронную модель выполнения, которая позволяет выполнять несколько задач в одном потоке. Такой подход называется "однопоточным многозадачным программированием" (single-threaded multitasking).
Библиотека `asyncio` является частью стандартной библиотеки Python с версии 3.4 и позволяет управлять потоком выполнения задач без использования потоков или процессов. Вместо этого `asyncio` использует понятие корутин (coroutines) - специальных функций, позволяющих приостановить свое выполнение и вернуть управление другой корутине для выполнения.
Это позволяет обрабатывать несколько задач на одном потоке без блокировки, т.е. когда задача ждет завершения выполнения определенной операции. К тому же, `asyncio` позволяет делать все это с использованием неблокирующих асинхронных I/O потоков, что позволяет достичь высокой скорости выполнения и лучшей масштабируемости.
Django (виды views handlers)
В Django есть несколько типов представлений (views), которые позволяют гибко управлять веб-приложениями:
1. Функциональные представления (Function-Based Views) - это наиболее распространенный тип вью в Django. Они определяются как обычные функции Python и принимают объект `request` в качестве аргумента. Они обрабатывают запросы и возвращают ответы.
2. Представления на основе классов (Class-based Views) - это альтернативный способ определения вью в Django. Они представляют собой классы, унаследованные от базового класса `View` или одного из его подклассов. Они обладают большей гибкостью, чем функциональные представления, т.к. позволяют легко переиспользовать код и настраивать поведение вью.
3. Представления общего назначения (Generic Views) - это классы представлений, реализующие общие задачи, такие как отображение списка объектов или деталей объекта. Они упрощают написание кода для этих типов страниц, что позволяет быстро создавать приложения.
4. Представления на основе шаблонов (Template Views) - это представления, которые используют шаблоны Django для генерации HTTP-ответов. Они обычно используют `TemplateView`, как основу и определяютщаблон, который будет использоваться для генерации ответа.
5. Представления на основе аннотаций (Decorator Views) - это представления, которые определяются с помощью декораторов, добавляемых к функции-обработчику запроса. Декораторы позволяют добавлять дополнительную функциональность, такую как проверка прав доступа или обработка ошибок, без необходимости изменения функции-обработчика.
6. Представления на основе REST API (REST API Views) - это представления, которые предназначены для работы с RESTful API. Они используют `ViewSet`, чтобы предоставлять различные действия, такие как «список», «детали», «создание», «обновление», и т.д.
Для того, чтобы создать простое REST API на Django с использованием `ViewSet`, нужно выполнить следующие шаги: 1. Используя Django REST framework (DRF), создать сериализатор для модели.
from rest_framework import serializers
from .models import Book
class BookSerializer(serializers.ModelSerializer):
class Meta:
model = Book
fields = ('id', 'title', 'author', 'description', 'published_date')
2. Создать `ModelViewSet`.
from rest_framework import viewsets
from .models import Book
from .serializers import BookSerializer
class BookViewSet(viewsets.ModelViewSet):
queryset = Book.objects.all()
serializer_class = BookSerializer
3. Зарегистрировать путь в `urls.py`.
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import BookViewSet
router = DefaultRouter()
router.register(r'books', BookViewSet)
urlpatterns = [
path('', include(router.urls)),
]
Теперь в приложении доступно REST API для модели `Book`. API поддерживает все стандартные методы HTTP (GET, POST, PUT, DELETE) и доступен по адресу `http://example.com/books/`. Примеры запросов: - GET http://example.com/books/ - вернет список книг. - POST http://example.com/books/ - создаст новую книгу. - PUT http://example.com/books/1/ - обновит книгу с ID 1. - DELETE http://example.com/books/1/ - удалит книгу с ID 1.
Для каждого CRUD-действия (создание, получение, изменение, удаление) `ModelViewSet` генерирует соответствующие методы => не нужно писать хендлеры обработчики всех CRUD запросов, как это было бы в Function-based views
Select_related и prefetch_related в Django
`select_related` и `prefetch_related` - это методы, которые используются для оптимизации запросов к базе данных в Django. `select_related` - это метод, который позволяет предварительно загружать объекты связанных моделей в одном запросе к базе данных. Это позволяет оптимизировать производительность при работе с объектами модели, имеющих связь ForeignKey или OneToOneField. Например, когда вы получаете список объектов одной модели, которые имеют ForeignKey на другую модель, то при использовании метода select_related эта связанная модель будет загружена из базы данных одним запросом:
# Без использования select_related()
books = Book.objects.filter(category__name='fiction')
for book in books:
print(book.author.name)
# С использованием select_related()
books = Book.objects.filter(category__name='fiction').select_related('author')
for book in books:
print(book.author.name)
`prefetch_related` - это метод, который позволяет предварительно загружать связанные объекты в нескольких запросах к базе данных. В отличие от `select_related`, который работает только с ForeignKey и OneToOneField, `prefetch_related` позволяет работать с любыми типами связей между моделями. Например, когда вы получаете список объектов одной модели, для которых нужно загрузить связанные объекты из другой модели, то при использовании метода `prefetch_related` связанные объекты будут загружены из базы данных несколькими запросами:
# Без использования prefetch_related()
categories = Category.objects.all()
for category in categories:
books = category.books.all()
for book in books:
print(book.author.name)
# С использованием prefetch_related()
categories = Category.objects.all().prefetch_related('books__author')
for category in categories:
books = category.books.all()
for book in books:
print(book.author.name)Аргумент метода `select_related` - это строка, содержащая названия связанных моделей, которые нужно загрузить из базы данных. В качестве аргумента можно указать названия полей, разделенные запятыми:
books = Book.objects.filter(category__name='fiction').select_related('author', 'publisher')
В этом примере мы указываем модели `Author` и `Publisher`, связанные с моделью `Book`, и указываем, что нужно предварительно загрузить связанные объекты этих моделей. Аргумент метода `prefetch_related` - это строка, содержащая названия связанных моделей, которые нужно загрузить из базы данных. В качестве аргумента можно указать названия полей, разделенные запятыми:
categories = Category.objects.all().prefetch_related('books__author', 'books__publisher')
Здесь мы указываем модель `Book`, связанную с моделью `Category`, и указываем, что нужно предварительно загружать связанные объекты моделей `Author` и `Publisher`, связанные с моделью `Book`.
select_related и prefetch_related оптимизирует кол-во запросов путем добавления join'ов в SQL запросы, которые происходят под капотом этих функций.
Serializer и DTO (Data Transfer Object) в Django
Serializer в Django - это инструмент, который позволяет преобразовывать объекты моделей Django в JSON, XML или другой формат данных, который может быть передан по сети. Он используется для создания Web-API в Django.
Сериализаторы Django предоставляют два основных метода: `serialize` (сериализация) и `deserialize` (десериализация) данных. Метод `serialize` принимает объект модели Django и преобразует его в JSON или XML, в зависимости от выбранного формата. Метод `deserialize` принимает JSON или XML и преобразует его в объект модели Django.
Сериализаторы Django позволяют задать правила конвертации данных из моделей в формат, понятный клиентам. Для этого можно использовать различные опции, такие как `fields` для выбора полей, `depth` для вложенного сериализации связанных объектов и т.д.
Data Transfer Object (DTO) - это средство передачи данных, которое используется для обмена данными между клиентом и сервером в приложении. DTO - это объект данных, который представляет собой структуру данных, содержащую информацию о передаваемом объекте.
В Django, сериализаторы являются примером DTO. Они представляют структуру данных, которая может быть передана клиенту. Сериализаторы могут использоваться для передачи данных между клиентом и сервером в формате JSON или XML. Они упрощают процесс обмена данными, и позволяют клиенту и серверу работать с одной и той же структурой данных.
Таким образом, использование сериализаторов в Django является важным компонентом создания Web-API. Они обеспечивают простоту и эффективность передачи данных между клиентом и сервером.
Unit-тесты и py-тесты. Mock
Unit-тесты - тесты, которые проверяют функциональность какого-то отдельного модуля, функции или метода, при этом все остальные зависимости заменяются на заглушки (mocks). Для написания unit-тестов в Python часто используется встроенный модуль `unittest`.
Pytest - это фреймворк для написания тестов на Python, который пользуется большой популярностью в сообществе Python. Он обладает более простым и удобным синтаксисом, чем `unittest`, и более широкими возможностями. Pytest может использовать `unittest`, но также предоставляет свои собственные инструменты для создания и запуска тестов.
Мок (mock) - это объект, который заменяет зависимость в тестовом случае. Он может представлять собой заглушку или замоканный объект, который имитирует реальный объект в приложении. Моки используются для того, чтобы избежать зависимости от реальных объектов в случае их отсутствия или недоступности. В тестовом случае, моки можно использовать для проверки корректной работы кода, который зависит от этих объектов.
Вот пример простого unit-теста на Python с использованием mock:
from unittest.mock import Mock
import my_module
def test_my_function():
my_mock = Mock()
my_mock.my_method.return_value = 'Hello, world!'
result = my_module.my_function(my_mock)
assert result == 'Hello, world!'
В этом тесте `my_function` - это функция, которая ожидает объект, который имеет метод `my_method`. При проверке этой функции в тесте, мы можем использовать объект `Mock` вместо реального объекта, чтобы создать имитацию реального метода. Мы настраиваем метод `my_method` для возвращения строки "Hello, world!", и затем вызываем функцию `my_function` with `my_mock` в качестве аргумента. Мы проверяем, что функция возвращает ожидаемый результат.
Асимптотическая оценка сложности
Асимптотическая оценка сложности - это способ оценки скорости роста времени выполнения или использования памяти программы при увеличении размера входных данных. Она позволяет оценить скорость работы алгоритма при больших объемах данных.
Асимптотическая сложность описывается с помощью математического обозначения "O(N)", где N - размер входных данных. Например, для алгоритма с асимптотической сложностью O(N), время выполнения будет пропорционально N.
Обычно, асимптотическая сложность указывает на то, насколько быстро возрастает время выполнения или использование памяти при увеличении размера входных данных. Отметим, что асимптотическая сложность может быть константной (например, при поиске элемента в массиве из N элементов, время выполнения будет константным O(1)), линейной (O(N)), квадратичной (O(N^2)), кубической (O(N^3)) и т.д.
Для получения асимптотической оценки сложности метода необходимо проанализировать его код и выявить, какие операции выполняются в зависимости от размера входных данных.
1. Определить, какие операции выполняются в методе, например, циклы, вложенные циклы, рекурсивные вызовы, условные операторы и т.д.
2. Определить, какие объемы данных обрабатываются внутри каждой операции, например, сколько элементов массива обрабатывается в цикле.
3. Оценить количество итераций циклов (или рекурсивных вызовов) в зависимости от объема входных данных.
4. Сложить все операции и оценить асимптотическую сложность метода, используя математические обозначения O(N), где N - размер входных данных.
При анализе сложности метода необходимо учитывать, что некоторые операции могут иметь различную сложность в зависимости от конкретных реализаций языка программирования. Например, обращение к элементу массива в языке Python имеет сложность O(1), однако в некоторых других языках может иметь более высокую сложность.