Материал

Методичка по Python

Если каких-то вопросов не хватает, сообщи автору — материал можно дополнять.

Теория

Базовый Python

Типы данных

Неизменяемые:

int
float
bool
str
tuple, например ('apple', 1, 2)
NoneType (тип значения None)
frozenset Изменяемые: list
set
dict

Что может быть ключом словаря

Ключ словаря должен быть хешируемым: иметь стабильное значение хеша и корректно сравниваться с другими ключами. Большинство изменяемых встроенных контейнеров (list, dict, set) не хешируются. Неизменяемость сама по себе не гарантирует хешируемость: например, tuple, содержащий list, также нельзя использовать как ключ. Подробнее — в глоссарии Python.

Что такое lambda-функция

lambda создаёт небольшую анонимную функцию. Она может принимать несколько параметров, но её тело состоит ровно из одного выражения; результат этого выражения возвращается без явного return.

a = [1, 2, 3, 'sss']
print(list(filter(lambda x: isinstance(x, int), a))) # [1, 2, 3]

Контекстные менеджеры

Контекстный менеджер — объект, который реализует протокол __enter__ / __exit__ и управляет входом в контекст и выходом из него. Контекстные менеджеры применяются для работы с файлами, соединениями, блокировками, сессиями и другими ресурсами. Метод выхода вызывается и при обычном завершении блока with, и при исключении, поэтому в нём удобно гарантированно освобождать ресурс.

Пример работы с файлом через with ... as ...:

>>> with open('hello.txt', 'a') as file:
...     file.write('\nHello Python!')
...
... closed = file.closed
... print("Is the file closed?", closed)
...
Is the file closed? True

Реализация контекстного менеджера

Собственный контекстный менеджер можно реализовать методами __enter__ и __exit__:

class File:
    def __init__(self, file_name, mode):
        self.file_obj = open(file_name, mode)

    def __enter__(self):
        return self.file_obj

    def __exit__(self, exc_type, exc_value, traceback):
        self.file_obj.close()

Обработка исключений

Метод __exit__ принимает тип исключения, экземпляр исключения и traceback. Если исключения не было, все три значения равны None. Истинное возвращаемое значение подавляет исключение; ложное значение, включая None, позволяет ему распространиться дальше.

Декоратор contextlib.contextmanager

С помощью contextmanager можно определить контекстный менеджер генераторной функцией вместо отдельного класса:

from contextlib import contextmanager

@contextmanager
def open_file(name):
    f = open(name, 'w')
    try:
        yield f
    finally:
        f.close()

Исключения

Исключения — механизм передачи и обработки информации об ошибочных и других исключительных ситуациях во время выполнения программы. Синтаксис:

try:
    выполняем_операцию()
except Exception as e:
    обработать_исключение(e)
else:
    выполнить_при_успехе()
finally:
    освободить_ресурсы()

Иерархия исключений

  • Base exception
  • SystemExit
  • KeyboardInterrupt
  • Generator exit
  • Exception
  • Stop iteration …

Полная иерархия

Иллюстрация к материалу «Методичка по Python»

  • BaseException - базовое исключение, от которого берут начало все остальные.
  • SystemExit - исключение, порождаемое функцией sys.exit при выходе из программы.
  • KeyboardInterrupt - порождается при прерывании программы пользователем (обычно сочетанием клавиш Ctrl+C).
  • GeneratorExit - порождается при вызове метода close объекта generator.
  • Exception - а вот тут уже заканчиваются полностью системные исключения (которые лучше не трогать) и начинаются обыкновенные, с которыми можно работать.
  • StopIteration - порождается встроенной функцией next, если в итераторе больше нет элементов.
  • ArithmeticError - арифметическая ошибка.
  • FloatingPointError - порождается при неудачном выполнении операции с плавающей запятой. На практике встречается нечасто.
  • OverflowError - возникает, когда результат арифметической операции слишком велик для представления. Не появляется при обычной работе с целыми числами (так как python поддерживает длинные числа), но может возникать в некоторых других случаях.
  • ZeroDivisionError - деление на ноль.
  • AssertionError - выражение в функции assert ложно.
  • AttributeError - объект не имеет данного атрибута (значения или метода).
  • BufferError - операция, связанная с буфером, не может быть выполнена.
  • EOFError - функция наткнулась на конец файла и не смогла прочитать то, что хотела.
  • ImportError - не удалось импортирование модуля или его атрибута.
  • LookupError - некорректный индекс или ключ.
  • IndexError - индекс не входит в диапазон элементов.
  • KeyError - несуществующий ключ (в словаре, множестве или другом объекте).
  • MemoryError - недостаточно памяти.
  • NameError - не найдено переменной с таким именем.
  • UnboundLocalError - сделана ссылка на локальную переменную в функции, но переменная не определена ранее.
  • OSError - ошибка, связанная с системой.
  • BlockingIOError
  • ChildProcessError - неудача при операции с дочерним процессом.
  • ConnectionError - базовый класс для исключений, связанных с подключениями.
  • BrokenPipeError
  • ConnectionAbortedError
  • ConnectionRefusedError
  • ConnectionResetError
  • FileExistsError - попытка создания файла или директории, которая уже существует.
  • FileNotFoundError - файл или директория не существует.
  • InterruptedError - системный вызов прерван входящим сигналом.
  • IsADirectoryError - ожидался файл, но это директория.
  • NotADirectoryError - ожидалась директория, но это файл.
  • PermissionError - не хватает прав доступа.
  • ProcessLookupError - указанного процесса не существует.
  • TimeoutError - закончилось время ожидания.
  • ReferenceError - попытка доступа к атрибуту со слабой ссылкой.
  • RuntimeError - возникает, когда исключение не попадает ни под одну из других категорий.
  • NotImplementedError - можно явно выбросить для ещё не реализованной операции. Для обязательного переопределения методов обычно используют abc.abstractmethod; не путать с константой NotImplemented.
  • SyntaxError - синтаксическая ошибка.
  • IndentationError - неправильные отступы.
  • TabError - смешивание в отступах табуляции и пробелов.
  • SystemError - внутренняя ошибка.
  • TypeError - операция применена к объекту несоответствующего типа.
  • ValueError - функция получает аргумент правильного типа, но некорректного значения.
  • UnicodeError - ошибка, связанная с кодированием / раскодированием unicode в строках.
  • UnicodeEncodeError - исключение, связанное с кодированием unicode.
  • UnicodeDecodeError - исключение, связанное с декодированием unicode.
  • UnicodeTranslateError - исключение, связанное с переводом unicode.
  • Warning - предупреждение.

Декоратор

Декоратор — callable, который применяется к функции или классу, а результат связывается с исходным именем. Декоратор часто возвращает функцию-обёртку, но это не обязательное требование. Пример декоратора на Python:

def decorator_function(func):
    def wrapper():
        print('Входим в обёртку')
        result = func()
        print('Выходим из обёртки')
        return result

    return wrapper

# без сахара - hello_world = decorator_function(hello_world).
@decorator_function
def hello_world():
    print('Hello world!')

hello_world()
# Входим в обёртку
# Hello world!
# Выходим из обёртки

Декоратор с аргументами:

def benchmark(iters):
    def actual_decorator(func):
        import time

        def wrapper(*args, **kwargs):
            total = 0
            for i in range(iters):
                start = time.time()
                return_value = func(*args, **kwargs)
                end = time.time()
                total = total + (end-start)
            print('[*] Среднее время выполнения: {} секунд.'.format(total/iters))
            return return_value

        return wrapper
    return actual_decorator

@benchmark(iters=10)
def fetch_webpage(url):
    import requests
    webpage = requests.get(url)
    return webpage.text

Что такое итератор

Итератор - это поведенческий паттерн проектирования, позволяющих совершать последовательный обход элементов составных объектов, не раскрывая их внутреннего представления В теме итераторов есть 3 составные части:

  • Итерируемый объект предоставляет __iter__(). Для sequence-протокола возможен fallback через __getitem__() с индексами от 0 до IndexError.
  • Итератор реализует __next__() и __iter__(), который возвращает сам итератор. __next__() возвращает следующий элемент или выбрасывает StopIteration.
  • **Итерация **- процесс получения элементов из какого-нибудь источника, например, списка
my_list = [1, 2, 3, 4, 5]
my_iterator = iter(my_list)
print (next(my_iterator))
print (next(my_iterator))
print (next(my_iterator))

Что такое генератор

Генератор — это объект, реализующий протокол итератора, при этом генератор не хранит весь итерируемый набор элементов в памяти, вместо этого производя генерацию элементов “на лету”. Генерация может происходить по алгоритму или при чтении коллекции/файла. range — не генератор, а лениво вычисляемая неизменяемая последовательность. В генераторных функциях используется ключевое слово yield: оно отдаёт значение и приостанавливает выполнение с сохранением состояния. Следующий вызов next() продолжает функцию после yield. **Генераторная функция - **любая функция, которая содержит в себе ключевое слово yield Генераторные выражения

[x * x for x in range(10)]
# [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
(x * x for x in range(10))
# <generator object <genexpr> at 0x7fe76f7e5db0>

Что такое тернарный оператор

some = True
result = 1 if True else 0
print(result) # 1

Чем отличается == от is

== сравнивает значения, is — идентичность объектов. Адрес памяти — деталь реализации, а не семантика оператора is.

a = [1, 2, 3]
b = [1, 2, 3]
print (a == b) # True
print (a is b) # False

Как в python передаются аргументы в функцию

Python использует call by sharing: параметр функции связывается с тем же объектом, который передал вызывающий код. Перепривязка локального имени не меняет имя снаружи, а мутация общего изменяемого объекта видна вызывающему коду.

Что такое аннотации типов, зачем они нужны и когда применяются

Это подсказка типов. Это встречается в функциях и классах. Не проверяют типы. Аннотации нужны для того чтобы другие разработчики видели какой тип данных передаётся в функцию.

Поверхностное и глубокое копирование

Есть модуль copy, содержит функции .copy и deepcopy. Поверхностная копия Поверхностная копия создаёт новый объект и выделяет ему ячейку в памяти
и вставляет в него ссылки, находящиеся в оригинале.

import copy
some_list = [1, [2], 3]
print(some_list is copy.copy(some_list)) # False
print (some_list[1] is copy.copy(some_list)[1]) # True

Глубокая копия
deepcopy рекурсивно копирует составные изменяемые объекты и сохраняет memo, чтобы работать с циклами. Это не означает, что абсолютно каждый вложенный объект обязательно будет новым: неизменяемые объекты и функции могут разделяться, а классы могут настраивать копирование.

import copy
some_list = [1, [2], 3]
print(some_list is copy.deepcopy(some_list)) # False
print (some_list[1] is copy.deepcopy(some_list)[1]) # False

List comprehension

List comprehension - генераторы списков. Синтаксис: [выражение for val in коллекция] Можно применять к итерируемым объектам - list, dict, str и тд.

Что такое docstring

Docstring в Python - это строка документации, которая описывает, что делает функция, метод, модуль или класс Python. Данная строка располагается в начале определения объекта и используется для генерации документации автоматически. В других словах, docstring используется для создания описания API и содержит информацию о том, как использовать функцию или метод, какие аргументы они принимают и какие значения возвращают.

def add_numbers(a, b):
    """
    This function takes in two numbers and returns their sum
    """
return a + b

Замыкания

Замыкание (closure) - функция, которая находится внутри другой функции и ссылается на переменные, которые объявлены в теле внешней функции. При этом внутренняя функция создается каждый раз во время выполнения внешней с новыми ссылками на переменные из внешней функции. Если необходимо изменить изменяемую переменную - все происходит как обычно, однако при работе с неизменяемыми типами может возникнуть ошибка UnboundLocalError:

In [31]: def func1():
    ...:     a = 1
    ...:     b = 'line'
    ...:     c = [1, 2, 3]
    ...:
    ...:     def func2():
    ...:         c.append(4)
    ...:         a = a + 1
    ...:         return a, b, c
    ...:
    ...:     return func2
    ...:

In [32]: call_func = func1()

In [33]: call_func()
---------------------------------------------------------------------------
UnboundLocalError                         Traceback (most recent call last)
<ipython-input-33-9288e4e0f32f> in <module>
----> 1 call_func()

<ipython-input-31-56414e2c364b> in func2()
      6     def func2():
      7         c.append(4)
----> 8         a += 1
      9         return a, b, c
     10

UnboundLocalError: local variable 'a' referenced before assignment

In [34]: for item in call_func.__closure__:
    ...:     print(item, item.cell_contents)
    ...:
<cell at 0xb12174c4: str object at 0xb732d720> line
<cell at 0xb1217af4: list object at 0xb11e5dac> [1, 2, 3, 4]

Если необходимо присвоить свободной переменной другое значение, необходимо явно объявить ее как nonlocal:

In [40]: def func1():
    ...:     a = 1
    ...:     b = 'line'
    ...:     c = [1, 2, 3]
    ...:
    ...:     def func2():
    ...:         nonlocal a
    ...:         c.append(4)
    ...:         a += 1
    ...:         return a, b, c
    ...:
    ...:     return func2
    ...:

In [41]: call_func = func1()

In [42]: call_func()
Out[42]: (2, 'line', [1, 2, 3, 4])

In [43]: for item in call_func.__closure__:
    ...:     print(item, item.cell_contents)
    ...:
<cell at 0xb11fc6bc: int object at 0x836bef0> 2
<cell at 0xb11fcdac: str object at 0xb732d720> line
<cell at 0xb11fc56c: list object at 0xb117fe2c> [1, 2, 3, 4]

Sets, tuples

Множества (sets)

Множество - коллекция, эквивалентная множествам в математике, хранящая неупорядоченный набор уникальных неизменяемых данных.

  • Множества неупорядочены
  • Множества реализованы на основе HashTable, поэтому хранить может только хешируемые (неизменяемые) значения - числа, строки, кортежи и тд
  • Элементы во множестве уникальны
  • Есть обычный set и frozenset. Set изменяемая структура, Frozenset является неизменяемой структурой
  • По аналогии с list comprehension есть set comprehension
  • Set является подтипом Collection, поэтому в нем определены операции проверке на вхождение (a in set), длина коллекции, по ним можно итерироваться (iterable)

Кортежи (tuples)

Кортежи, как и списки, предназначены для хранения наборов данных любого типа. Однако, в отличие от списков, они являются неизменяемыми.

  • Кортежи не поддерживают добавление и удаление элементов
  • Не позволяют изменять элементы (a[0]=10)
  • Но при этом позволяют изменять внутренние изменяемые объекты
  • Кортежи реализованы так же, как и lists - с помощью массивов и ссылок на объекты
  • Переиспользование пустого tuple и внутренние freelist-оптимизации — детали конкретной версии CPython, а не гарантии языка. Прикладной код не должен от них зависеть.

*args и **kwargs

  • args и *kwargs - это специальные параметры в Python, которые позволяют передавать переменное количество аргументов в функцию. Параметр *args используется для передачи переменного количества аргументов без ключевого слова. Он представляет собой кортеж из всех дополнительных аргументов, переданных функции. Параметр **kwargs используется для передачи переменного количества именованных аргументов. Он представляет собой словарь из всех дополнительных именованных аргументов, переданных функции.

Как просмотреть методы объекта?

Чтобы посмотреть все методы и атрибуты, связанные с определенным объектом в Python, можно использовать функцию dir()

globals() и locals()

globals() возвращает mapping глобального пространства имён текущего модуля. Встроенные имена находятся в отдельном пространстве builtins, хотя модуль обычно содержит ссылку __builtins__. locals() возвращает mapping текущего локального пространства имён. Правила отражения изменений в этом mapping зависят от области видимости, поэтому использовать его для изменения локальных переменных не стоит.

x = 5
y = 10

def my_func(z):
    a = 3
    print(globals()) # выводит все глобальные переменные
    print(locals()) # выводит все локальные переменные

my_func(7)

слайс(slice)

Слайс (slice) - это способ извлечения определенной части последовательности (например, строки, списка, кортежа) с использованием индексации. Синтаксис для создания слайса:

sequence[start:end:step]

где start - индекс, с которого начинается извлечение (включительно), end - индекс, на котором заканчивается извлечение (не включая его), и step - шаг для извлечения элементов (по умолчанию равен 1).

пустой список нельзя использовать как аргумент по умолчанию

Значения по умолчанию для аргументов функции вычисляются только 1 раз, когда функция определяется, а не каждый раз, когда она вызывается. Таким образом, если вы попытаетесь использовать изменяемый тип данных (например, список) как аргумент по умолчанию для функции, то каждый вызов функции, который изменяет это значение, также изменит значение по умолчанию для всех последующих вызовов функции. Это может привести к разным сюрпризам и неожиданным последствиям. Пустой список - это изменяемый тип данных в Python, поэтому его использование в качестве аргумента по умолчанию не рекомендуется. Вместо этого лучше использовать None в качестве значения по умолчанию и создавать новый пустой список внутри функции, если требуется список. Типо того:

def my_function(my_list=None):
    if my_list is None:
        my_list = []
    # do something with my_list
    return my_list

метод id()

Функция id() возвращает целочисленный идентификатор объекта, уникальный в течение жизни этого объекта. В CPython это часто адрес памяти, но стандарт языка этого не гарантирует; после уничтожения объекта значение может быть переиспользовано. Например, если у вас есть две переменные, которые ссылаются на один и тот же объект, то их идентификаторы будут равны:

a = [1, 2, 3]
b = a
print(id(a))
print(id(b))

pdb

pdb - это интерактивный отладчик для Python, с помощью которого можно перемещаться по коду во время запуска вашей программы, смотреть и изменять значения переменных, построчно навигироваться по коду (в том числе углубляться во вложенности кода), назначать брейкпоинты и все прочие операции присущие отладчику. Модуль pdb предоставляет интерфейс командной строки, который можно использовать для взаимодействия с кодом Python во время его выполнения. Вы можете войти в режим pdb в своей программе Python, вставив следующую строку кода там, где вы хотите остановить отладчик:

import pdb;
pdb.set_trace()

try…except…else

Ветка else в конструкции try ... except ... else будет выполнена только в том случае, если исключения не было возбуждено в блоке try. Если в блоке try произошло исключение, то выполнение программы переходит к соответствующему блоку except, и ветка else пропускается. Если блок except не указан, то исключение будет возбуждено дальше, а программа завершится с сообщением об ошибке.

a, b = map(int, input().split())
try:
    print(a / b)
except ZeroDivisionError:
    print('Деление на ноль')
else:
    print('Ошибки не было')  # это выводится, если исключения не возникло

Как dict и set реализованы внутри? Какова сложность получения элемента? Сколько памяти потребляет каждая структура?

Dict и Set реализованы в виде хэш-таблицы. Хэш-таблица - это структура данных, которая использует хэш-функцию для преобразования ключа в индекс в массиве, где хранятся значения. Затем элемент добавляется в массив по соответствующему индексу. Так работает хэш-таблица: Иллюстрация к материалу «Методичка по Python» img В CPython получение, добавление и удаление элемента dict/set обычно имеют среднюю сложность O(1), а в патологическом худшем случае — O(n).

Как аргументы передаются в функции: по значению или по ссылке?

В Python аргументы передаются по ссылке на объект. Это означает, что когда вы передаете объект в качестве аргумента функции, функция получает ссылку на этот объект, а не его копию. Если вы модифицируете объект внутри функции, эти изменения будут отражены и вне функции, так как обе переменные (внутри и вне функции) ссылаются на один и тот же объект в памяти. Однако, если внутри функции вы присваиваете новое значение аргументу, это не изменит значение переменной, которую вы использовали при вызове функции, потому что эта переменная по-прежнему ссылается на тот же объект в памяти. Например:

def increment(x):
    x += 1
return x

y = 10
print(increment(y)) # Output: 11
print(y) # Output: 10

Как ускорить существующий код python?

Чтобы ускорить существующий код на Python, можно использовать несколько подходов:

  • Векторизация: векторизация позволяет оптимизировать код, который выполняет большое количество операций над массивами данных, например, использование библиотеки NumPy.
  • Выбор правильных структур данных: выбор правильных структур данных и алгоритмов может значительно ускорить выполнение кода. Например, использование словарей может быть более эффективным, чем использование списков.
  • Выбор runtime/компиляции: CPython уже компилирует исходный код в байткод, а .pyc в основном ускоряет импорт. Для подходящих нагрузок можно исследовать PyPy, Cython, Nuitka или нативные расширения и обязательно измерить результат.
  • Конкурентность: потоки и async обычно полезны для I/O-bound задач; CPU-bound код в обычной CPython чаще распараллеливают процессами или выносят в нативный код, освобождающий GIL.
  • Параллелизм: параллельное выполнение задач на нескольких ядрах процессора может ускорить выполнение кода.
  • Оптимизация: такие инструменты, как cProfile и line_profiler, могут помочь оптимизировать код, выявляя узкие места в его выполнении и предоставляя информацию о времени выполнения каждой строки кода. Компромиссы: если выполнение кода нельзя ускорить до приемлемого уровня, можно рассмотреть возможность использования компромиссов, например, уменьшить количество данных, обрабатываемых кодом, или упростить логику выполнения задачи.

Python — это императивный или декларативный язык?

Python является императивным языком программирования. В императивном программировании программист составляет последовательность команд, которые выполняются компьютером. Python также поддерживает некоторые функциональные и объектно-ориентированные концепции программирования, однако основной подход в языке является императивный. **"Императивный язык" **это термин, который относится к классу языков программирования, использующих прямые команды для управления компьютером, в отличие от декларативных языков. В императивных языках программист явно описывает действия, которые нужно выполнить компьютеру, а не просто описывает желаемый результат. Примеры императивных языков программирования это Java, C, C++, Python и JavaScript. Декларативный язык позволяет описать желаемый результат, не задавая пошаговый алгоритм его достижения. Примеры декларативных языков и нотаций — SQL и HTML.

Какие функции из collections и itertools вы используете?

В модулях collections и itertools в Python есть множество полезных функций, которые могут использоваться в различных задачах. Некоторые из наиболее часто используемых функций включают:

defaultdict: это удобный способ создания словаря с заданным значением по умолчанию для любого ключа, который еще не был добавлен в словарь.

from collections import defaultdict
d = defaultdict(int)
print(d['apple'])

d = defaultdict(list)
print(d['apple'])

d = defaultdict(set)
print(d['apple'])

# вывод:
# 0
# []
# set()

Counter: это удобный способ подсчета количества встречаемых элементов в списке или другом итерируемом объекте. Он возвращает объект, который можно использовать как словарь, где ключами являются элементы, а значения - количество их вхождений.

from collections import Counter
cnt = Counter(['red', 'blue', 'red', 'green', 'blue', 'blue'])
print(cnt)
print(dict(cnt))

# вывод:
# Counter({'blue': 3, 'red': 2, 'green': 1})
# {'red': 2, 'blue': 3, 'green': 1}

namedtuple: можно создать именованный кортеж с заданными полями, что может быть удобно для работы с данными, которые имеют структуру, но не требуют создания класса.

from collections import namedtuple

Point = namedtuple("Point", "x y")
print(issubclass(Point, tuple))

point = Point(2, 4)
print(point)

print(point.x)
print(point.y)

print(point[0])
print(point[1])

# вывод:
# True
# Point(x=2, y=4)
# 2
# 4
# 2
# 4

itertools.chain: позволяет конкатенировать несколько итерируемых объектов в единый итератор.

from itertools import chain

chained = chain('ab', [33])
print(next(chained))
print(next(chained))
print(next(chained))
# вывод:
# a
# b
# 33

for i in chain('1', [1, 2, 3], {3, 4}):
    print(i)
# вывод:
# 1
# 1
# 2
# 3
# 3
# 4

for i in chain('1', [1, {2, (5, '6')}, 3], {3, 4}):
    print(i)
# вывод:
# 1
# 1
# {2, (5, '6')}
# 3
# 3
# 4

itertools.groupby: позволяет группировать элементы итерируемого объекта по заданному ключу.

groupby() объединяет только соседние элементы с одинаковым ключом. Для глобальной группировки вход обычно предварительно сортируют по тому же ключу.

from itertools import groupby

grouper = lambda item: item['country']

data = [
    {'city': 'Москва', 'country': 'Россия'},
    {'city': 'Новосибирск', 'country': 'Россия'},
    {'city': 'Пекин', 'country': 'Китай'},
]

for key, group in groupby(data, key=grouper):
    print(key, *group)

# Россия {'city': 'Москва', 'country': 'Россия'} {'city': 'Новосибирск', 'country': 'Россия'}
# Китай {'city': 'Пекин', 'country': 'Китай'}

itertools.combinations и itertools.permutations: генерируют все различные комбинации или перестановки элементов из-заданного множества.

from itertools import combinations

print(list(combinations('123', 2)))

# [('1', '2'), ('1', '3'), ('2', '3')]
from itertools import permutations

print(list(permutations('123', 2)))

# [('1', '2'), ('1', '3'), ('2', '1'), ('2', '3'), ('3', '1'), ('3', '2')]

О-нотация

Списки (Lists):

**Операция** **Временная сложность (в Big O)**
Доступ по индексу O(1)
Вставка в начало O(n)
Вставка в конец O(1) (в среднем)
Вставка в середину O(n)
Удаление по индексу O(n)
Поиск элемента O(n)
Сортировка O(n log n)
### Множества (Sets) и Словари (Dictionaries):
**Операция** **Временная сложность (в Big O)**
Доступ по ключу O(1) (в среднем)
Вставка O(1) (в среднем)
Удаление по ключу O(1) (в среднем)
### Строки (Strings):
**Операция** **Временная сложность (в Big O)**
Доступ по индексу O(1)
Конкатенация O(n)
Поиск подстроки O(n \* m), где n - длина строки, m - длина подстроки
Сравнение O(n)
Изменение по индексу Невозможно: `str` неизменяем
### Операции с числами

Для int произвольной точности стоимость арифметики зависит от числа разрядов. Оценка O(1) допустима только при явном предположении, что размер числа ограничен машинным словом.

**Операция** **Временная сложность (в Big O)**
Сложение O(n) по числу разрядов
Вычитание O(n) по числу разрядов
Умножение Зависит от числа разрядов и алгоритма
Деление Зависит от числа разрядов и алгоритма
## Многопоточность и многопроцессорность Чем отличается мультипроцессинг, мультитрединг и асинхронность, гил ### Процессы и потоки Запуск нового приложения на компьютере запускает процесс. Процессы работают на уровне операционной системы независимо и изолированно друг от друга. Процессы можно разделить на системные - те, которые обеспечивают функционирование системы и пользовательские - приложения и таски, запущенные пользователем.
**Процесс** - это экземпляр программы во время выполнения, независимый объект, работающий на уровне операционной системы, которому операционная система выделяет ресурсы. Процесс является абстракцией-контейнером, которая сама по себе ничего не выполняет, но содержит в себе весь контекст для выполнения, ресурсы, файлы и **потоки.
** **Поток** - это сущность операционной системы, процесс выполнения инструкций на процессоре набора инструкций - программного кода. Общее назначение потоков - параллельное выполнение на процессоре 2 и более задач. У потоков есть приоритет выполнения, переключение между ними и выделение процессорного времени контролируется операционной системой. Потоки имеют общую память и контекст выполнения ### Многопоточность и многопроцессорность ![Иллюстрация к материалу «Методичка по Python»](/materials/python-03.png) По определению: **Многопоточность** (multithreading) - вариант реализации вычислений, при котором выполнение некоторой прикладной задачи запускается и выполняется в нескольких потоках. **Многопроцессорность** - вариант реализации вычислений, при котором выполнение некоторой прикладной задачи выполняется в нескольких независимых процессах. ### CPU-bound и I/O-bound задачи **CPU-bound** - задачи, завязанные на ресурсах и мощности CPU **I/O-bound** - задачи, завязанные на прочих более медленных устройствах или ресурсах компьютера, например диск или сетевой ресурс. **Многопоточность** полезна для I/O-bound задач, таких как чтение с диска, запросы к базе данных или сетевые операции: пока один поток ждёт, ОС может выполнять другой. **Мультипроцессинг** актуален для cpu-bound задач, тяжелых процессорных вычислений. ### GIL GIL (Global Interpreter Lock) — механизм обычной сборки CPython, который позволяет только одному потоку одновременно исполнять Python-байткод в одном интерпретаторе. Многие операции ввода-вывода и часть C-расширений освобождают GIL, поэтому потоки остаются полезны для I/O-bound-задач. Для CPU-bound Python-кода обычно используют процессы, нативные библиотеки или free-threaded-сборку CPython.

Начиная с Python 3.13 существует отдельная free-threaded-сборка CPython, в которой GIL можно отключить. Поэтому утверждение про «один поток байткода» нужно относить к обычной сборке CPython, а не к языку Python вообще. См. глоссарий Python и документацию free threading.

CPython использует подсчёт ссылок вместе со сборщиком циклического мусора. sys.getrefcount() показывает количество ссылок, включая временную ссылку, переданную самой функции:

>>> import sys
>>> a = []
>>> b = a
>>> sys.getrefcount(a)
3

GIL упрощает защиту части внутреннего состояния обычной сборки CPython, но не делает пользовательский многопоточный код автоматически потокобезопасным и не исключает race condition или deadlock. Общие изменяемые данные всё равно требуют корректной синхронизации. Вопросы

  1. Что такое многопоточность и какие преимущества она может предоставить при разработке программного обеспечения?
  • Многопоточность — выполнение нескольких потоков в одном процессе. Она помогает отзывчивости и I/O-bound concurrency; реальный параллелизм CPU-кода зависит от реализации Python, сборки интерпретатора и того, освобождает ли нативный код GIL.
  1. Как создать поток в Python? Какие способы существуют для создания потоков?
  • В Python потоки можно создать, используя модуль threading. Для создания потока можно определить класс, наследующийся от threading.Thread, или создать экземпляр threading.Thread с указанием функции, которая будет выполняться в потоке.
  1. Что такое Global Interpreter Lock (GIL) в Python и как он влияет на многопоточность? Какие последствия могут возникнуть при использовании многопоточности в Python из-за GIL?
  • GIL (Global Interpreter Lock) - это механизм, используемый в CPython (стандартной реализации Python), который гарантирует, что только один поток исполняет байт-код Python в определенный момент времени. Это означает, что в одном процессе Python может выполняться только одна инструкция Python за раз. Из-за GIL многопоточность в CPython может не давать полной параллельности и не приводит к улучшению производительности для CPU-интенсивных задач, но может быть полезной для I/O-интенсивных задач или задач, в которых блокировки освобождают GIL.
  1. Какие модули Python вы знаете для работы с многопоточностью?
  • Для работы с потоками используется модуль threading. Модуль multiprocessing запускает отдельные процессы и относится к многопроцессорности, а не к многопоточности.
  1. Какие способы синхронизации потоков вы знаете в Python?
  • Для синхронизации потоков используются threading.Lock, RLock, Condition, Semaphore, Event и Barrier. Потокобезопасную очередь предоставляет модуль queue. Отдельного threading.Mutex в стандартной библиотеке нет; обычный Lock выполняет роль mutex.

Примеры

Использование блокировки (Lock):

import threading

# Создаем блокировку
lock = threading.Lock()

# Функция, которая будет выполняться в потоке
def worker():
    # Захватываем блокировку
    lock.acquire()
    try:
        # Критическая секция, в которой происходит общий доступ к ресурсам
        print("Работник начал работу")
        # ...
        print("Работник закончил работу")
    finally:
        # Освобождаем блокировку
        lock.release()

# Создаем и запускаем поток
thread = threading.Thread(target=worker)
thread.start()

Использование условной переменной (Condition):

import threading

# Создаем условную переменную
condition = threading.Condition()

# Общий ресурс
shared_resource = []

# Поток, который добавляет элемент в общий ресурс
def producer():
    with condition:
        # Проверяем условие перед выполнением операции
        while len(shared_resource) >= 5:
            condition.wait()  # Ожидаем сигнала
        shared_resource.append("Новый элемент")
        condition.notify()  # Оповещаем другие потоки

# Поток, который удаляет элемент из общего ресурса
def consumer():
    with condition:
        # Проверяем условие перед выполнением операции
        while len(shared_resource) == 0:
            condition.wait()  # Ожидаем сигнала
        item = shared_resource.pop()
        condition.notify()  # Оповещаем другие потоки
        return item

# Создаем и запускаем потоки
producer_thread = threading.Thread(target=producer)
consumer_thread = threading.Thread(target=consumer)
producer_thread.start()
consumer_thread.start()

Использование семафора (Semaphore):

import threading

# Создаем семафор с максимальным количеством разрешений 2
semaphore = threading.Semaphore(2)

# Функция, которая будет выполняться в потоке
def worker():
    # Запрашиваем разрешение
    semaphore.acquire()
    try:
        # Критическая секция, в которой происходит общий доступ к ресурсам
        print("Работник начал работу")
        # ...
        print("Работник закончил работу")
    finally:
        # Освобождаем разрешение
        semaphore.release()

# Создаем и запускаем потоки
thread1 = threading.Thread(target=worker)
thread2 = threading.Thread(target=worker)
thread3 = threading.Thread(target=worker)
thread1.start()
thread2.start()
thread3.start()

Использование мьютекса (Mutex):

import threading

# Создаем мьютекс
mutex = threading.Lock()

# Общий ресурс
shared_resource = 0

# Функция, которая будет выполняться в потоке
def worker():
    global shared_resource
    # Захватываем мьютекс
    mutex.acquire()
    try:
        # Критическая секция, в которой происходит общий доступ к ресурсу
        shared_resource += 1
    finally:
        # Освобождаем мьютекс
        mutex.release()

# Создаем и запускаем потоки
threads = []
for _ in range(5):
    thread = threading.Thread(target=worker)
    thread.start()
    threads.append(thread)

# Ждем завершения всех потоков
for thread in threads:
    thread.join()

print(shared_resource)

Использование очереди (Queue):

import threading
import queue
import time

# Создаем очередь
q = queue.Queue()

# Функция, которая будет выполняться в потоке
def worker():
    while True:
        # Получаем элемент из очереди (блокирующая операция)
        item = q.get()
        if item is None:
            break
        # Обработка элемента
        print("Обработка элемента:", item)
        # Имитация длительной операции
        time.sleep(1)
        # Отмечаем элемент как обработанный
        q.task_done()

# Создаем и запускаем потоки
threads = []
for _ in range(5):
    thread = threading.Thread(target=worker)
    thread.start()
    threads.append(thread)

# Наполняем очередь элементами
for item in range(10):
    q.put(item)

# Ждем, пока все элементы будут обработаны
q.join()

# Добавляем в очередь пустые элементы для остановки потоков
for _ in range(5):
    q.put(None)

# Ждем завершения всех потоков
for thread in threads:
    thread.join()
  1. Что такое асинхронное программирование и какие преимущества оно может предоставить при разработке программного обеспечения?
  • Асинхронное программирование — модель конкурентного выполнения, в которой задача добровольно отдаёт управление во время ожидания. Это особенно полезно для большого числа I/O-bound операций, но само по себе не означает параллельное выполнение CPU-кода.
  1. Какие модули Python вы знаете для работы с асинхронностью?
  • В Python для работы с асинхронностью можно использовать модули asyncio и aiohttp. Модуль asyncio предоставляет основные средства для создания асинхронных приложений, включая event loop (цикл событий), корутины и сопрограммы. Модуль aiohttp предоставляет функциональность для асинхронного веб-программирования.
  1. Что такое event loop (цикл событий) в асинхронном программировании? Как он работает в Python?
  • Event loop (цикл событий) - это основной компонент асинхронного программирования. Он является центральным механизмом, который управляет выполнением асинхронных задач. Event loop в Python обрабатывает события, такие как ввод/вывод, таймеры и вызовы функций, и диспетчеризует их на соответствующие обработчики. Event loop позволяет эффективно использовать ресурсы процессора и управлять потоком выполнения асинхронных задач.
  1. В чем разница между многопоточностью и асинхронностью? Какой подход лучше использовать в каких ситуациях?
  • Многопоточность и асинхронность — разные модели конкурентного выполнения.
  • Потоки управляются ОС и удобны для блокирующего I/O. В обычной GIL-enabled CPython CPU-bound Python-код обычно не ускоряется от потоков; для него используют процессы, нативный код, освобождающий GIL, или free-threaded build.
  • Асинхронность предполагает выполнение задачи без блокирования потока. Задачи выполняются независимо друг от друга, и управление передается от одной задачи к другой, когда одна из них ожидает завершения операции ввода/вывода или другого события. Асинхронность полезна для I/O-интенсивных задач, когда задачи могут ожидать ввода/вывода без блокирования остальных задач.
  • Выбор зависит от workload и доступных библиотек: async и потоки обычно решают I/O-bound задачи, процессы и подходящий нативный код — CPU-bound задачи.
  1. Какие проблемы могут возникнуть при использовании асинхронности? Как их можно решить?
  • При использовании асинхронности могут возникать следующие проблемы:
  • Сложность отладки: Асинхронный код может быть сложнее отлаживать из-за переключений контекста между задачами. Для облегчения отладки рекомендуется использовать инструменты и библиотеки, предназначенные для асинхронного программирования, такие как отладчики, асинхронные трассировщики и логгеры.
  • Гонки данных: Если не синхронизировать доступ к общим данным, могут возникнуть гонки данных и неопределенное поведение. Для предотвращения гонок данных можно использовать механизмы синхронизации, такие как блокировки или условные переменные, чтобы гарантировать правильное взаимодействие между асинхронными задачами.
  • Блокирующие операции: Некоторые операции могут блокировать выполнение других задач, что может ухудшить производительность. Для решения этой проблемы можно использовать асинхронные версии операций или делегировать блокирующие операции в отдельные потоки или процессы.
  • Потеря исключений: При использовании асинхронности и цикла событий может быть сложно обрабатывать исключения, так как они не могут быть просто проброшены вверх по стеку вызовов. Для обработки исключений в асинхронном коде рекомендуется использовать конструкцию try/except вокруг асинхронных операций и обработчики исключений, предоставляемые асинхронными библиотеками. 11.* Зачем использовать asyncio, если есть потоки?
  1. Потоки дороже - каждый требует стек, системные ресурсы, контекст-переключение ядра.
  2. GIL тормозит многопоточность - одновременно исполняется только один Python-байткод.
  3. Асинхронная модель может снижать накладные расходы при большом числе одновременно ожидающих I/O-соединений, но итог зависит от workload, библиотек и архитектуры deployment.
  4. Чёткий контроль - в asyncio ты явно управляешь, когда задача отдаёт управление (await), чего невозможно добиться в threading.

Asyncio

Asyncio

Корутина — объект, который возвращает функция, объявленная через async def, если в ней нет yield. Само объявление создаёт coroutine function, а её вызов — coroutine object. Если в async def используется yield, это уже асинхронный генератор. Корутины можно выполнять через asyncio или другую совместимую async-библиотеку.

import asyncio


async def fun1(x):
    print(x**2)
    await asyncio.sleep(3)
    print('fun1 завершена')
coroutine = fun1(2)
print(type(coroutine))
# <class 'coroutine'>
coroutine.close()

await приостанавливает текущую корутину до завершения awaitable-объекта и позволяет циклу событий выполнять другие готовые задачи. Когда результат становится доступен, выполнение продолжается с места остановки.

async def g():
    # Pause here and come back to g() when f() is ready
    r = await f()
    return r

Task оборачивает корутину и планирует её выполнение в цикле событий. Задачу можно отменить методом cancel() или дождаться её завершения через await:

import asyncio

async def nested():
    return 42

async def main():
    # Schedule nested() to run soon concurrently
    # with "main()".
    task = asyncio.create_task(nested())

    # "task" can now be used to cancel "nested()", or
    # can simply be awaited to wait until it is complete:
    await task

asyncio.run(main())

Future — низкоуровневый awaitable-объект, представляющий будущий результат асинхронной операции. asyncio.Task наследуется от Future, а не наоборот. При await future текущая корутина приостанавливается до установки результата или исключения. Прикладному коду обычно следует создавать задачи через asyncio.create_task(), а не создавать Future вручную. Подробнее — в документации asyncio.

Память, сборка мусора

Управление памятью

Выделенную виртуальную память CPython использует для:

  1. Собственной корректной работы
  2. Стека вызываемых функций и их аргументов
  3. Хранилища данных, представленных в виде кучи. Python не управляет памятью напрямую, вместо этого он обращается в диспетчер памяти, который управляет хранилищем данных. Диспетчер памяти выделяет память с помощью аллокаторов с помощью специальной стратегии. Память выделяется динамически во время работы программы.

Сборка мусора

Освобождение памяти происходит с помощью 2 механизмов: **счетчика ссылок **и сборщика мусора. Python управляет объектами с помощью подсчета ссылок. Диспетчер памяти отслеживает количество ссылок на каждый объект в программе. Когда счетчик ссылок падает до нуля, это означает, что объект более никем не используется и его можно удалить, освободив память. Однако счетчик ссылок неспособен отследить ситуации с циклическими ссылками (когда объект ссылается сам на себя или 2 объекта друг на друга) Garbage Collector (GC) - сборщик мусора работает во время выполнения программы и освобождает память, если счетчик ссылок упал до 0.

Счетчик ссылок увеличивается когда используется:

  1. Оператор присваивания =
  2. Передача аргументов
  3. Вставка нового объекта в контейнер, например list или dict Счетчик также уменьшается, когда ссылка на объект переназначается, когда ссылка на объект выходит из области видимости функции, когда удаляется объект. Поколения сборки

CPython сочетает подсчёт ссылок со сборщиком циклического мусора. Детали поколений зависят от версии: например, в Python 3.14 поколение 1 удалено, а сборщик работает с молодым и старым поколениями. Поэтому в прикладном коде лучше не полагаться на фиксированное количество поколений. Актуальные детали описаны в документации модуля gc.

ООП

Абстрактные классы и интерфейсы в Python

Для абстрактных базовых классов стандартная библиотека предоставляет модуль abc. Обычно класс наследуют от ABC, а абстрактные методы помечают @abstractmethod.

Интерфейс описывает ожидаемые операции и свойства объекта. В Python его можно выразить через duck typing, typing.Protocol или абстрактный базовый класс. ABC может содержать как абстрактные методы, так и готовую реализацию. Класс с нереализованными абстрактными методами нельзя инстанцировать.

Что такое метаклассы

Иллюстрация к материалу «Методичка по Python» В Python классы — это тоже объекты, создающие экземпляры. Классы создаются **метаклассами, **т.е классы являются экземплярами метаклассов

>>> class Foo:
...     pass
...
>>> isinstance(Foo, type)
True

В Python можно динамически создавать классы на ходу. Именно это и делает Python во время выполнения оператора класс. Для создания классов используется функция **type. **В качестве параметра функция принимает описание класса и возвращает класс.

type(class_name, base_classes, attributes)

Например:

>>> MyShinyClass = type('MyShinyClass', (), {})
>>> print(MyShinyClass)
<class '__main__.MyShinyClass'>
>>> print(MyShinyClass())
<__main__.MyShinyClass object at 0x8997cec>

Что такое метакласс?

Метакласс - это штука, создающая классы. Фабрика классов. **type - **встроенный метакласс, который использует Python. Он является основой классов, например стандартных вроде str:

>>> name = 'bob'
>>> name.__class__
<class 'str'>

>>> name.__class__.__class__
<class 'type'>

Выбор метакласса

В Python 3 метакласс указывается аргументом metaclass в заголовке класса:

class Foo(Bar, metaclass=SomethingMeta):
    pass

Python выбирает метакласс из явно указанного значения и метаклассов базовых классов. Выбранный метакласс должен быть совместим с метаклассами всех баз. Если metaclass не указан и специальных базовых классов нет, используется type. Атрибут __metaclass__ относится к синтаксису Python 2 и в Python 3 не выбирает метакласс.

Пользовательские метаклассы

Пример метакласса, который конвертит все атрибуты к верхнему регистру:

class UpperAttrMetaclass(type):

    def __new__(upperattr_metaclass, future_class_name,
                future_class_parents, future_class_attr):

        uppercase_attr = {}
        for name, val in future_class_attr.items():
            if not name.startswith('__'):
                uppercase_attr[name.upper()] = val
            else:
                uppercase_attr[name] = val

        # reuse the type.__new__ method
        # this is basic OOP, nothing magic in there
        return type.__new__(upperattr_metaclass, future_class_name,
                            future_class_parents, uppercase_attr)

В основном метаклассы используются для создания API. Типичным примером является Django ORM. Можно написать что-то вроде этого:

class Person(models.Model):
    name = models.CharField(max_length=30)
    age = models.IntegerField()

Но если написать так:

guy = Person(name='bob', age='35')
print(guy.age)

Что такое дескрипторы

Это механизм в Python, который позволяет настраивать доступ к атрибутам объектов. Они используются для определения поведения при доступе, изменении или удалении атрибута объекта.

Реализуются через три метода

  • get(self, instance, owner) - вызывается при доступе к атрибуту
  • set(self, instance, value) - вызывается при изменении атрибута
  • delete(self, instance) - вызывается при удалении атрибута Дескрипторы могут быть определены как отдельный класс или внутри другого класса. Они используются для атрибутов со специальным поведением при чтении, изменении или удалении.
class Descriptor:
        def __get__(self, instance, owner):
                print(f"Getting attribute: {instance} from {owner}")
                return instance._value
        def __set__(self, instance, value):
                print(f"Setting attribute: {value} to {instance}")
                instance._value = value
        def __delete__(self, instance):
                print(f"Deleting attribute from {instance}")
                del instance._value
class MyClass:
        attribute = Descriptor()
        def __init__(self, value):
                self._value = value
my_object = MyClass(42)
print(my_object.attribute)
# Output: Getting attribute: <__main__.MyClass object ...> from <class '__main__.MyClass'>
# 42
my_object.attribute = 23
# Output: Setting attribute: 23 to <__main__.MyClass object at 0x10f5d5d90>
del my_object.attribute
# Output: Deleting attribute from <__main__.MyClass object at 0x10f5d5d90>

Что такое diamond problem MRO, MRO3

MRO (method resolution order) — порядок, в котором Python ищет атрибуты и методы в классе и его базовых классах. Python 3 использует C3-линеаризацию; порядок можно посмотреть в SomeClass.__mro__ или через SomeClass.mro(). Подробнее — в Python MRO HOWTO. Diamond Problem - это проблема, которая может возникнуть при множественном наследовании, когда два или более родительских классов имеют общего предка.
Если дочерний класс пытается унаследовать от двух таких родительских классов,
то возникает неоднозначность, какой метод использовать из общего предка, что
может привести к ошибкам и неожиданным результатам.

class A1():
    def who_am_i(self):
        print("I am A1")

class A2():
    def who_am_i(self):
        print("I am A2")

class A3():
    def who_am_i(self):
        print("I am A3")

class B(A1, A2):
    pass

class C(A3):
    pass

class D(B, C):
    pass

d1 = D()
d1.who_am_i()
# I am A1
# D.__mro__: D, B, A1, A2, C, A3, object

SOLID

S – Single Responsibility (Принцип единственной ответственности) Каждый класс должен отвечать только за одну операцию. O — Open-Closed (Принцип открытости-закрытости) Классы должны  быть открыты для расширения, но закрыты для модификации. L — Liskov Substitution (Принцип подстановки Барбары Лисков) Если П является подтипом Т, то любые объекты типа Т, присутствующие в программе, могут заменяться объектами типа П без негативных последствий для функциональности программы. I — Interface Segregation (Принцип разделения интерфейсов) Не следует ставить клиент в зависимость от методов, которые он не использует. D — Dependency Inversion (Принцип инверсии зависимостей) Модули верхнего уровня не должны зависеть от модулей нижнего уровня. И те, и другие должны зависеть от абстракций. Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.

__init__ vs __new__

Основное различие между этими двумя методами состоит в том, что __new__ обрабатывает создание объекта, а __init__ обрабатывает его инициализацию. __new__ вызывается автоматически при вызове имени класса (при создании экземпляра), тогда как __init__ вызывается каждый раз, когда экземпляр класса возвращается __new__, передавая возвращаемый экземпляр в __init__ в качестве параметра self, поэтому даже если вы сохранили экземпляр где-нибудь глобально/статически и возвращали его каждый раз из __new__, для него все-равно будет каждый раз вызываться __init__. Из вышесказанного вытекает что сначала вызывается __new__, а потом __init__

Принципы ООП

  • Абстракция
  • Наследование
  • Инкапсуляция
  • Полиморфизм Наследование  — способ создания класса. Его суть заключается в том, что функциональность нового класса наследуются от уже существующего класса. Новый класс называется производным (дочерним). Существующий — базовым (родительским). Инкапсуляция Инкапсуляция отделяет публичный API объекта от деталей реализации. Python не запрещает доступ к атрибутам на уровне языка: _name — соглашение о непубличном API, а __name включает name mangling для защиты от случайных конфликтов в наследниках. Полиморфизм  — особенность ООП, позволяющая использовать одну функцию для разных форм (типов данных). Абстракция используется, чтобы скрыть внутренние характеристики функции от пользователей.

Инкапсуляция

В Python нет принудительных уровней доступа public, protected и private.

  • name обычно относится к публичному API;
  • _name по соглашению считается внутренней деталью;
  • __name преобразуется в _Class__name, но остаётся доступным и не является механизмом безопасности.

Magic method

Магические методы - специальные методы в Python, которые позволяют определить поведение объекта класса при вызовах операторов и свойства объектов при их взаимодействии Для их обозначения используется обрамление двойным подчеркиванием: __new__

Создание и удаление объектов

new(cls, [, …]) - метод для создания объекта класса. Принимает на вход класс и возвращает объект init (self, [, …]) - метод для инициализации объекта, полученного из new init_subclass (cls) - позволяет переопределить создание сабкласса, например добавить новые атрибуты автоматически del(self) - finalizer с негарантированными временем и контекстом вызова. Для освобождения ресурсов используйте context manager (with) или явный close(), а не полагайтесь на __del__.

Общие свойства объектов

repr(self) - информация об объекте, определяет поведение функции repr(), больше предназначен для отладочного машинно-ориентированного вывода str(self) - информация об объекте, определяет поведение функции str(), больше предназначен для чтения людьми bytes(self) - информация об объекте в байтах format(self) - определение для функции format()

Другие категории, которые можно определить magic методами:

  1. Сравнение между объектами: >, <, ==, ≥, ≤,
  2. Доступ к атрибутам объекта
  3. Последовательности: len, getitem, setitem, delitem, missing, reversed
  4. Унарные операторы, арифметические операторы
  5. Преобразования типов
  6. Контекст менеджеры
  7. и тд

Mixin

Mixin - это класс, предоставляющий реализации методов для повторного использования другими классами. Это некая альтернатива множественному наследованию нескольких родительских классов, которое может усложнять и раздувать код. Миксин позволяет переиспользовать логику, не добавляя сложных связей между классами. Python не предоставляет специального синтаксиса для миксинов: технически это обычные классы, используемые через множественное наследование. Жёсткого требования наследоваться только от object нет. На практике миксин делают небольшим, сфокусированным на одном поведении и совместимым с кооперативным вызовом super().

class GraphicalEntity:
    def __init__(self, pos_x, pos_y, size_x, size_y):
        self.pos_x = pos_x
        self.pos_y = pos_y
        self.size_x = size_x
        self.size_y = size_y


class ResizableMixin:
    def resize(self, size_x, size_y):
        self.size_x = size_x
        self.size_y = size_y


class ResizableGraphicalEntity(GraphicalEntity, ResizableMixin):
    pass

rge = ResizableGraphicalEntity(5, 4, 200, 300)
rge.resize(1000, 2000)

@classmethod, @staticmethod, @property

@classmethod@staticmethod, and @property - это декораторы методов класса в языке Python. @classmethod используется для создания методов, которые будут работать с классом в целом, а не с отдельным экземпляром. В качестве первого параметра этот метод принимает класс, а не экземпляр объекта, и часто используется для создания фабричных методов и методов, которые работают с класс-уровнем методов. @staticmethod декоратор работает подобно @classmethod, но он не получает доступ к классу в качестве первого параметра. @property декоратор используется для создания свойств объекта, которые можно получить и задать, но выглядят как обычные атрибуты объекта. Это позволяет управлять доступом к атрибутам объекта, установив условиями доступа и возможностью заложить дополнительную логику при чтении, установке или удалении атрибута. Например, явное использование декораторов может выглядеть так:

class MyClass:
    def __init__(self, value):
        self._value = value

    @classmethod
    def from_string(cls, input_string):
        value = cls.process_input_string(input_string)
        return cls(value)

    @staticmethod
    def process_input_string(input_string):
        return int(input_string)

    @property
    def value(self):
        return self._value

    @value.setter
    def value(self, new_value):
        if new_value < 0:
            raise ValueError("Value must be positive")
        self._value = new_value

__slots__

__slots__ позволяет явно указать набор атрибутов экземпляра и в некоторых случаях уменьшить расход памяти. Когда вы определяете класс, Python создает для каждого экземпляра этого класса словарь, который содержит все его атрибуты. Это может быть выгодным в том случае, если у вас много различных атрибутов, но может привести к большому расходу памяти, если вы создаете много экземпляров класса с небольшим количеством атрибутов. Атрибут __slots__ позволяет определить, какие атрибуты должны быть на самом деле созданы для каждого экземпляра класса, и в какой момент их можно будет получить. Отдельного __dict__ может не быть, если он не унаследован и не включён в slots явно. Подкласс без __slots__ снова получит словарь. Универсальной гарантии ускорения нет — это стоит измерять. Например, если у вас есть класс Person с атрибутами name и age, вы можете определить __slots__ следующим образом:

class Person:
    __slots__ = ('name', 'age')

    def __init__(self, name, age):
        self.name = name
        self.age = age

Экземпляры именно этого класса без наследуемого __dict__ смогут хранить name и age; поведение подклассов зависит от их собственных slots.

Задачи

Фибоначи

def fib(n):
    if n <= 1:
        return n
    return fib(n - 1) + fib(n - 2)
def fib(n):
    a, b = 0, 1
    for _ in range(n):
        a, b = b, a + b
    return a

Самый частый элемент

from collections import Counter
def counter(n):
    cnt = Counter(n)
    return cnt.most_common(1)[0][0]

Декоратор

from time import monotonic

def decorator(func):
    def wrapper(*args, **kwargs):
        start = monotonic()
        result = func(*args, **kwargs)
        elapsed = monotonic() - start
        print(f'Время выполнения функции: {elapsed:.3f} с')
        return result

    return wrapper

@decorator
def func(arr):
    return len(arr[1])

Напишите декоратор, который будет перехватывать ошибки и повторять функцию максимум N раз.

import functools
def retry(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        max_retries = 3
        for i in range(max_retries):
            try:
                result = func(*args, **kwargs)
                return result
            except Exception as e:
                print(f'Error occurred: {e}. Retrying ({i+1}/{max_retries})...')
        raise Exception(f'Function {func.__name__} failed after {max_retries} attempts.')
    return wrapper

Наследование, ООП

class A:
    def do(self):
        print('Hello from A')

class B(A):
    pass

b = B()
b.do()

Вернуть самое большое значение в словаре

def func(dct):
        new_dct = dict(sorted(dct.items(), key=lambda item: item[1]))
        return sorted(dct.items(), key=lambda item: item[1])

Вернуть число которое встречается только 1 раз

def func(num):
        for i in num:
                if num.count(i) == 1:
                        return i

Пересечение двух массивов

def func(num1, num2):
        lst = []
        for i in num1:
                if i in num2:
                        lst.append(i)
        return list(set(lst))
# ----------------------
def func(num1, num2):
        return list(set(num1)&set(num2))
# & - обозначает побитовую операцию "И" между двумя целыми числами.

Вернуть индекс не повторяющейся буквы в строке

def firstUniqChar(s):
        for i in s:
                if s.count(i) == 1:
                        return s.index(i)

Палиндром

def isPalindrome(s):
        arr = [i.lower() for i in s if i.isalnum()]
        return arr == list(reversed(arr))

Setter getter что выведет

class Variable:
    def __init__(self, name, value):
        self._name = name
        self._value = value

    @property
    def value(self):
        print(self._name, 'GET', self._value)
        return self._value

    @value.setter
    def value(self, value):
        print(self._name, 'SET', self._value)
        self._value = value


var_1 = Variable('var_1', 'val_1')
var_2 = Variable('var_2', 'val_2')
var_1.value, var_2.value = var_2.value, var_1.value

# var_2 GET val_2
# var_1 GET val_1
# var_1 SET val_1
# var_2 SET val_2

Официальные источники