PythonShots
73 subscribers
1 link
Мои конспекты по Python.
Один пост - одна концепция Python.
Download Telegram
Channel created
__slots__

1. Что такое __slots__?

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

Как это работает:
Вместо хранения атрибутов в виде пар ключ-значение в словаре, Python выделяет фиксированную внутреннюю структуру для хранения атрибутов, указанных в __slots__. Каждый атрибут получает своё фиксированное место в этой структуре.

2. Эффективность по памяти и доступ к атрибутам

С использованием __dict__:
Каждый объект имеет словарь для хранения своих атрибутов.
Этот словарь динамичен и позволяет добавлять новые атрибуты в любое время.
Цена за такую гибкость - дополнительные затраты памяти на каждый экземпляр.

С использованием __slots__:
Используется фиксированная компактная структура для хранения только заданных атрибутов.
Не создается __dict__ (если он не создан явно), что позволяет экономить память.
Доступ к атрибутам происходит быстрее за счёт прямого индексирования в фиксированной структуре.

Сравнение использования памяти классов с __dict__ и с __slots__:
import guppy


class Foo:
def __init__(self):
self.a = 1
self.b = 2


class Bar:
__slots__ = ("a", "b")

def __init__(self):
self.a = 1
self.b = 2

# создаем 1 000 000 экземпляров каждого класса
foos = [Foo() for _ in range(1000000)]
bars = [Bar() for _ in range(1000000)]

print(guppy.hpy().heap())

>>> Partition of a set of 3041689 objects. Total size = 221985398 bytes.
>>> Index Count % Size % Cumulative % Kind (class / dict of class)
>>> 0 1000000 33 104000000 47 104000000 47 dict of __main__.Foo
>>> 1 1000000 33 48000000 22 152000000 68 __main__.Bar
>>> 2 1000000 33 48000000 22 200000000 90 __main__.Foo

Для класса Foo дополнительно выделяется память под __dict__.

Пример сравнения скорости доступа к атрибутам:
import timeit


class Foo:
__slots__ = ("foo",)


class Bar:
pass


slotted = Foo()
not_slotted = Bar()


def get_set_delete_fn(obj):
def get_set_delete():
obj.foo = "foo"
del obj.foo

return get_set_delete


print(min(timeit.repeat(get_set_delete_fn(slotted))))
print(min(timeit.repeat(get_set_delete_fn(not_slotted))))
print(
min(timeit.repeat(get_set_delete_fn(not_slotted)))
/ min(timeit.repeat(get_set_delete_fn(slotted)))
)

>>> 0.07146320003084838
>>> 0.08545529993716627
>>> 1.191833761753899

У класса с __slots__ доступ к атрибутам почти на 20% быстрее.

3. Наследование и __slots__

Наследование с одним базовым классом:
Если базовый класс определяет __slots__, производный класс наследует эти слоты. Если в производном классе также определены свои __slots__, он может добавить новые атрибуты, не мешая слотам базового класса.
class Base:
__slots__ = ['a']

class Derived(Base):
__slots__ = ['b']

obj = Derived()
obj.a = 10 # Атрибут, унаследованный от Base
obj.b = 20 # Атрибут, определённый в Derived
print(obj.a, obj.b)

>>> 10, 20


Наследование с множественным базовым классом:

Если несколько базовых классов определяют __slots__, и производный класс наследует их, возможны конфликты в размещении атрибутов. Может возникнуть исключение TypeError, если структура наследования не согласована.
Обычно рекомендуется использовать __slots__ в одном базовом классе или тщательно проектировать и объединять слоты в производном классе.

4. Заключение

Фиксированный набор атрибутов:
В классах с __slots__ нельзя динамически добавлять новые атрибуты (если не добавить 'dict' в список слотов).

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

Повышение производительности:
Экономия памяти и ускорение доступа к атрибутам наиболее заметны при создании большого количества экземпляров классов с фиксированным набором атрибутов.
decorators

Декораторы в Python — это мощный инструмент, позволяющий модифицировать поведение функций или классов без изменения их исходного кода. Они широко используются для добавления дополнительной функциональности, такой как логирование, кэширование, проверка прав доступа и многое другое.

Что такое декоратор?

Декоратор — это функция или класс, принимающий другую функцию или класс в качестве аргумента и возвращающий модифицированную версию этой функции или класса. Это позволяет оборачивать существующие объекты дополнительной функциональностью без изменения их внутренней структуры.

Пример простого декоратора:
def simple_decorator(func):
def wrapper(*args, **kwargs):
print("Что-то происходит до вызова функции.")
result = func(*args, **kwargs)
print("Что-то происходит после вызова функции.")
return result
return wrapper

@simple_decorator
def say_hello():
print("Привет!")

say_hello()

>>> Что-то происходит до вызова функции.
>>> Привет!
>>> Что-то происходит после вызова функции.

В этом примере simple_decorator оборачивает функцию say_hello, добавляя сообщения до и после её выполнения.

Когда использовать декораторы?

Декораторы полезны в следующих ситуациях:
1. Логирование: Отслеживание вызовов функций и их параметров.
2. Кэширование: Сохранение результатов вычислений для ускорения повторных вызовов.
3. Проверка прав доступа: Ограничение доступа к функциям на основе определённых условий.
4. Измерение времени выполнения: Определение производительности функций.

Пример декоратора для измерения времени выполнения:
import time

def time_decorator(func):
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
print(f"Время выполнения {func.name}: {end_time - start_time} секунд")
return result
return wrapper

@time_decorator
def compute_square(n):
return n ** 2

compute_square(10)

>>> Время выполнения compute_square: 0.0 секунд


Преимущества и недостатки декораторов

Преимущества
:
1. Повторное использование кода: Декораторы позволяют применять одну и ту же функциональность к разным функциям или классам без дублирования кода.
2. Чистота кода: Основная логика функции остаётся неизменной, что улучшает читаемость и поддержку кода.
3. Гибкость: Легко добавлять или изменять поведение функций без их непосредственного изменения.

Недостатки:
1. Сложность отладки: Ошибки в декораторах могут быть трудны для выявления и исправления.
2. Потеря метаданных функции: При оборачивании функции декоратором её имя и документация могут быть утрачены. Для сохранения этих данных рекомендуется использовать functools.wraps.

Пример использования functools.wraps:
import functools

def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
"""Внутренняя функция-обёртка."""
return func(*args, **kwargs)
return wrapper

@decorator
def example_func():
"""Пример функции"""
pass

print(example_func.name)
print(example_func.doc)

>>> example_func
>>> Пример функции


Подводные камни

1. Порядок декорирования: Если на функцию применяется несколько декораторов, они выполняются в порядке, обратном их объявлению.
2. Совместимость с методами классов: При декорировании методов классов необходимо учитывать передачу экземпляра (self) в декоратор.

Пример декоратора для метода класса:
def method_decorator(func):
def wrapper(self, *args, **kwargs):
print(f"Вызов метода {func.name} класса {self.class.name}")
return func(self, *args, **kwargs)
return wrapper

class MyClass:
@method_decorator
def say_hello(self):
print("Привет!")

obj = MyClass()
obj.say_hello()

>>> Вызов метода say_hello класса MyClass
>>> Привет!

В этом примере декоратор method_decorator корректно обрабатывает метод класса, принимая self в качестве первого параметра.

Заключение

Декораторы, позволяют элегантно расширять функциональность функций и классов. Правильное применение декораторов способствует созданию чистого, поддерживаемого и расширяемого кода.
decorators

Декораторы и SOLID

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

1. Принцип единственной ответственности (Single Responsibility Principle, SRP)

Каждый класс или функция должен иметь только одну обязанность. Декораторы помогают выделить дополнительную функциональность (например, логирование или проверку прав доступа) из основной логики, обеспечивая разделение ответственности.

Пример:
def logging_decorator(func):
def wrapper(*args, **kwargs):
print(f"Вызов функции {func.__name__}")
return func(*args, **kwargs)
return wrapper

@logging_decorator
def process_data(data):
# Основная логика обработки данных
pass

В этом примере декоратор logging_decorator отвечает за логирование, а функция process_data — за обработку данных, что соответствует принципу SRP.

2. Принцип открытости/закрытости (Open/Closed Principle, OCP)

Сущности должны быть открыты для расширения, но закрыты для изменения. Декораторы позволяют добавлять новую функциональность к существующим функциям или классам без их изменения, что соответствует этому принципу.

Пример:
def authentication_decorator(func):
def wrapper(*args, **kwargs):
# Логика аутентификации
return func(*args, **kwargs)
return wrapper

@authentication_decorator
def access_resource():
# Доступ к ресурсу
pass

Здесь декоратор authentication_decorator расширяет функциональность access_resource без изменения её кода.

Заключение

Декораторы в Python являются мощным инструментом, который при правильном использовании способствует соблюдению принципов SOLID, улучшая модульность, расширяемость и поддерживаемость кода.
__init__.py
Зачем он нужен и как его использовать?

Что такое __init__.py?

Файл __init__.py делает каталог пакетом в Python. Без него Python просто считает каталог обычной папкой, а не пакетом, и относительный импорт внутри него не будет работать.

Основные функции __init__.py

1. Обозначение пакета

Python считает каталог пакетом, если в нем есть __init__.py.
Пример структуры проекта:
my_project/
├── main.py
├── my_package/
│ ├── __init__.py Этот файл делает my_package пакетом
│ ├── module1.py
│ ├── module2.py


Теперь можно импортировать module1:
from my_package import module1


2. Разрешение относительных импортов

Файл __init__.py позволяет использовать относительные импорты внутри пакета:
# my_package/module2.py
from .module1 import hello # 👈 Работает, потому что есть __init__.py


3. Автоматический импорт модулей

Импортирование модулей внутри __init__.py позволяет импортировать функции короче:
# my_package/__init__.py
from .module1 import hello
from .module2 import greet


Теперь в main.py можно просто написать:
from my_package import hello, greet  #  Без указания module1 и module2


4. Запуск кода при импорте пакета

Можно выполнять код сразу при импорте пакета, например, логирование:
# my_package/__init__.py
import logging

logging.basicConfig(level=logging.INFO)
logging.info("my_package загружен!")


При запуске
import my_package


Выведется:
INFO:root:my_package загружен!


5. Глобальные переменные или настройки

Можно задать конфигурации для всего пакета:
# my_package/__init__.py
config = {
"api_key": "123456",
"debug": True
}


Теперь в любом модуле пакета:
from my_package import config
print(config["debug"]) # True


7. Настройка import * с __all__

По умолчанию from my_package import * импортирует все модули. Это может быть небезопасно.
Файл __init__.py позволяет контролировать, что импортируется:
# my_package/__init__.py
from .module1 import hello
from .module2 import greet

__all__ = ["hello", "greet"] # Только эти объекты доступны при import *


⚠️ Чего лучше избегать в __init__.py?

Тяжелых вычислений (замедляет импорт пакета)
Модификации sys.path (используйте правильную структуру пакетов)
Вывода ненужных сообщений в print() (логирование — лучше)
👨‍💻1
Вчера я писал об __init__.py. Все, кто программировал на Python, знают, что можно обойтись и без __init__.py и импортировать модули, как будто они находятся в пакетах. Почему так?

Implicit Namespace Packages

В Python 3.3 была введена поддержка неявных пакетов-пространств имён (namespace packages) благодаря реализации PEP 420. Это позволило создавать пакеты без обязательного наличия файла __init__.py в их директориях. Однако важно понимать, что неявные пакеты и явные пакеты (с __init__.py) имеют различия в поведении и использовании.

Явные и неявные пакеты
С развитием Python были введены два подхода к созданию пакетов:

Явные пакеты

Традиционно, для создания пакета необходимо было разместить файл __init__.py в соответствующей директории. Этот файл мог быть пустым или содержать код инициализации. При импорте пакета Python выполнял код из __init__.py, что позволяло настраивать пространство имен и инициализировать необходимые компоненты.

Преимущества явных пакетов:
1. Явность структуры: Наличие __init__.py делает структуру проекта очевидной и понятной для разработчиков.
2. Совместимость: Многие инструменты и среды разработки ожидают наличие __init__.py для корректной работы с пакетами.

Недостатки явных пакетов:
1. Ограничение на объединение: Невозможно объединить несколько директорий с одинаковым именем в один пакет, если в них присутствует __init__.py.

Неявные пакеты

В Python 3.3 и выше поддерживаются неявные пакеты, позволяющие объединять модули из разных директорий в одно пространство имен. Рассмотрим пример такой организации:

Структура проекта:
project/
|--main.py
├── src/
│ ├── package/
│ │ ├── module1.py
│ │ └── module2.py
└── tests/
└── package/
├── module3.py
└── module4.py

src/package/ и tests/package/ — две директории с модулями, образующие одно пространство имен package.

Содержание файлов:
# src/package/module1.py

def func1():
return "Function 1 from src"


# src/package/module2.py

def func2():
return "Function 2 from src"


# tests/package/module3.py

def func3():
return "Function 3 from tests"


Использование в коде:
import sys
import os

# Добавляем директории src и tests в sys.path,
# чтобы Python мог объединить их содержимое в единое пространство имен.
project_root = os.path.dirname(os.path.abspath(__file__))
sys.path.append(os.path.join(project_root, 'src'))
sys.path.append(os.path.join(project_root, 'tests'))

# Теперь импортируем модули из объединённого пространства имен "package"
from package.module1 import func1
from package.module3 import func3

print(func1())
print(func3())

>>> Function 1 from src
>>> Function 3 from tests


В этом примере модули из директорий src/package/ и tests/package/ объединяются в одно пространство имен package. При импорте package.module1 и package.module3 доступны соответствующие функции. Обратите внимание, что отсутствие файла __init__.py в этих директориях позволяет Python объединять их содержимое в одно пространство имен.

Однако, если в одной из директорий присутствует файл __init__.py, это укажет Python на создание явного пакета, и объединение с модулями из других директорий будет невозможно.

Преимущества неявных пакетов:
1. Гибкость: Возможность распределять компоненты пакета по различным местоположениям и объединять их в одно пространство имен.

Недостатки неявных пакетов:
1. Меньшая явность: Отсутствие __init__.py может затруднить понимание структуры проекта для некоторых разработчиков.
2. Совместимость: Некоторые инструменты и среды разработки могут не поддерживать неявные пакеты, что может привести к проблемам.

Какой подход выбрать?

Выбор между явными и неявными пакетами зависит от специфики проекта:
Для новых проектов: Если требуется гибкость в распределении компонентов и объединении пакетов, можно рассмотреть использование неявных пакетов.
Для существующих проектов: Если проект уже использует явные пакеты и вы хотите сохранить совместимость с различными инструментами, рекомендуется продолжать использовать явные пакеты с __init__.py.
Константы, Final, @final

В языке программирования Python отсутствует встроенное понятие констант — переменных, значение которых нельзя изменить после присвоения. Однако существует соглашение среди разработчиков: имена переменных, предназначенных для постоянных значений, записываются заглавными буквами с использованием символа подчёркивания для разделения слов. Это служит сигналом для других программистов о том, что такие переменные не следует изменять.

Пример:
PI = 3.14159
MAX_CONNECTIONS = 100


В этом примере PI и MAX_CONNECTIONS обозначены как константы. Однако важно понимать, что Python не запрещает изменение их значений во время выполнения программы:
PI = 3  # Python позволит это присвоение


Для усиления контроля над неизменяемостью переменных, классов и методов в Python 3.8 были введены аннотации Final и декоратор @final. Они позволяют явно указать, что определённые элементы кода не предназначены для изменения или наследования.

Аннотация Final

Аннотация Final используется для обозначения переменных или атрибутов, которые не должны изменяться после первоначального присвоения. Например:
from typing import Final

MAX_CONNECTIONS: Final[int] = 100


Здесь MAX_CONNECTIONS объявлена как константа типа int. Статические анализаторы кода, такие как mypy, при попытке изменить значение этой переменной выдадут предупреждение:
MAX_CONNECTIONS = 200  # Статический анализатор сообщит об ошибке


Однако интерпретатор Python выполнит этот код без ошибок, поскольку аннотация Final не накладывает ограничений на уровне выполнения программы.

Декоратор @final

Декоратор @final применяется для обозначения классов или методов, которые не должны быть унаследованы или переопределены. Например:
from typing import final

@final
class BaseClass:
pass

class DerivedClass(BaseClass): # Статический анализатор сообщит об ошибке
pass


В этом случае статический анализатор предупредит о попытке наследования от класса BaseClass, помеченного как @final. Однако интерпретатор Python выполнит этот код без ошибок.

Статические анализаторы кода

Статические анализаторы кода — это инструменты, которые проверяют исходный код на наличие ошибок, потенциальных уязвимостей и несоответствий стандартам кодирования без выполнения программы. Они помогают выявлять проблемы на ранних этапах разработки, что способствует повышению качества и надёжности программного обеспечения. Примеры таких инструментов включают mypy, Pylint и Pyright.

Таким образом, использование аннотаций Final и декоратора @final в сочетании с соглашениями об именовании и статическими анализаторами кода способствует улучшению читаемости, поддерживаемости и надёжности программ на Python.
👍2