Библиотека классов Python: модульная структура и повторное использование кода
Основные варианты организации библиотеки классов
Эффективное решение: пакет с модулями и __init__.py
Как создать библиотеку классов, которую можно легко импортировать и расширять?
Наиболее удобный способ - группировка классов в пакете. Пакет представляет собой директорию с файлом __init__.py. Этот файл может быть пустым или содержать код для импорта подмодулей. Такой подход позволяет логически разделить классы по функциональности и избежать загромождения одного файла.
Пример структуры:
mylib/
__init__.py
shapes.py
colors.pyатрибуты класса python (атрибуты классов и объектов в python)
В shapes.py определим классы фигур:
class Circle:
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14159 * self.radius ** 2
class Square:
def __init__(self, side):
self.side = side
def area(self):
return self.side ** 2библиотека классов python (библиотека классов в python)
В colors.py - классы цветов:
class Red:
hex = "#FF0000"
class Blue:
hex = "#0000FF"
метод объекта python (методы объектов в python)
В __init__.py можно написать:
from .shapes import Circle, Square
from .colors import Red, Blueметод call python (метод __call__ в python)
Теперь импорт выглядит просто:
from mylib import Circle, Red
c = Circle(5)
print(c.area())Python структура объекта (структура объекта в python)
78.53975
Python создание объектов (создание объектов в python)
Это решение избавляет от необходимости запоминать полные пути к модулям и упрощает рефакторинг.
Типичные проблемы:
- Циклический импорт: когда модуль A импортирует из B, а B из A. Решение - вынести общие классы в третий модуль или использовать импорт внутри функций.
- Случайное создание pycache: если забыть __init__.py, пакет не будет распознан интерпретатором (в Python 3.3+ без него пакет называется «пространством имён», что может приводить к неожиданностям). Рекомендуется всегда добавлять __init__.py.
Вариант 1: Один модуль со всеми классами
Как сделать библиотеку классов, когда классов мало и их не нужно разделять?
Поместить все классы в один файл, например mylib.py. Это самый простой старт. Недостаток - при росте проекта файл становится нечитаемым, сложно поддерживать.
class Circle:
...
class Square:
...
class Red:
...Self object python (объект self в python)
Импорт: from mylib import Circle.
Проблема: при импорте всего модуля загружаются все классы, даже если нужен только один. Это увеличивает время загрузки и использование памяти. Решение - использовать импорт только нужных имён.
Вариант 2: Разделение на модули без __init__.py
Как организовать классы в подпапки без централизованного импорта?
Создаётся директория, в ней несколько файлов. Импорт выполняется по полному пути: from mylib.shapes import Circle. Если __init__.py отсутствует, такой импорт всё равно работает (начиная с Python 3.3), но пакет становится «неявным пространством имён». Это может затруднить рефакторинг и обнаружение пакета инструментами.
Ошибка: если позже добавить __init__.py, импорт может сломаться, если в __init__.py нет нужных подмодулей. Также сторонние библиотеки могут ожидать наличие __init__.py.
Вариант 3: Контроль экспорта через __all__
Как ограничить список имён, доступных при импорте from mylib import *?
В модуле или __init__.py определить список __all__.
__all__ = ['Circle', 'Square'] # только эти имена будут импортированыObject attribute python (атрибуты объекта в python)
Теперь from mylib import * импортирует только Circle и Square. Это повышает контроль над публичным API библиотеки.
Вариант 4: Использование относительных импортов внутри пакета
Как внутри пакета ссылаться на соседние модули?
Внутри модуля пакета можно использовать относительные импорты: from .shapes import Circle или from ..other import SomeClass. Это делает код независимым от внешнего расположения пакета. Однако относительные импорты не работают, если модуль запущен как скрипт напрямую.
Типичная ошибка: при запуске модуля из командной строки (python mylib/shapes.py) относительный импорт вызовет ImportError. Решение - использовать абсолютные импорты или запускать модуль как часть пакета (python -m mylib.shapes).
Вариант 5: Создание фабрики классов
Как динамически выбирать класс в зависимости от параметра?
Создать функцию-фабрику, которая возвращает экземпляр нужного класса.
# в __init__.py
from .shapes import Circle, Square
def create_shape(shape_type, *args):
if shape_type == 'circle':
return Circle(*args)
elif shape_type == 'square':
return Square(*args)
else:
raise ValueError(f'Unknown shape: {shape_type}')Python call method (вызов метода в python)
Использование:
from mylib import create_shape
s = create_shape('circle', 10)
print(s.area())Python класс данных (класс данных в python)
314.159
Это упрощает создание объектов и инкапсулирует логику выбора класса.
Расширенные примеры библиотеки классов с иерархией и паттернами
Рассмотрим библиотеку, реализующую систему плагинов. Будет использоваться абстрактный базовый класс, регистрация классов и динамическое создание объектов.
Пример 1: Абстрактный базовый класс для плагинов
Создадим пакет plugins с модулями base.py, plugin_a.py, __init__.py.
base.py:
from abc import ABC, abstractmethod
class Plugin(ABC):
@abstractmethod
def run(self, data):
pass
class PluginMeta(type):
"""Метакласс, автоматически регистрирующий подклассы"""
_registry = {}
def __new__(mcs, name, bases, namespace):
cls = super().__new__(mcs, name, bases, namespace)
if not getattr(cls, '_abstract', False): # не регистрировать ABC
mcs._registry[name.lower()] = cls
return cls
@classmethod
def get_plugin(mcs, name):
return mcs._registry.get(name)
plugin_a.py:
from .base import Plugin, PluginMeta
class PluginA(Plugin, metaclass=PluginMeta):
def run(self, data):
return f"PluginA processed: {data}"
__init__.py:
from .base import Plugin, PluginMeta
from .plugin_a import PluginA
# При импорте PluginA регистрируется автоматически
Теперь можно получать плагин по имени:
from plugins import PluginMeta
pl_cls = PluginMeta.get_plugin('plugina')
if pl_cls:
instance = pl_cls()
print(instance.run('test'))
PluginA processed: test
Этот подход позволяет легко добавлять новые плагины - достаточно создать класс, наследующий от Plugin с метаклассом PluginMeta, и он автоматически регистрируется. Не требуется ручного добавления в реестр.
Пример 2: Композиция и делегирование в библиотеке геометрических фигур
Создадим классы, использующие композицию вместо глубокого наследования.
class Color:
def __init__(self, rgb):
self.rgb = rgb
def __repr__(self):
return f'Color({self.rgb})'
class Shape:
def __init__(self, color):
self.color = color
class Circle(Shape):
def __init__(self, radius, color):
super().__init__(color)
self.radius = radius
def area(self):
return 3.14 * self.radius ** 2
# Использование
red = Color((255,0,0))
c = Circle(5, red)
print(c.color, c.area())
Color((255,0,0)) 78.5
Такая композиция упрощает тестирование и замену компонентов. Например, можно легко подменить цвет, не меняя форму.
Пример 3: Использование dataclasses для неизменяемых объектов библиотеки
Для представления данных (например, точки на плоскости) удобно использовать dataclass.
from dataclasses import dataclass
@dataclass(frozen=True)
class Point:
x: float
y: float
class Line:
def __init__(self, start: Point, end: Point):
self.start = start
self.end = end
def length(self):
return ((self.end.x - self.start.x)**2 + (self.end.y - self.start.y)**2)**0.5
p1 = Point(0,0)
p2 = Point(3,4)
line = Line(p1, p2)
print(line.length())
5.0
Dataclasses автоматически генерируют методы __init__, __repr__, __eq__. Флаг frozen=True делает объекты хэшируемыми, что полезно для использования в множествах и словарях.
Пример 4: Менеджер контекста для работы с ресурсами в библиотеке
Реализуем класс, который управляет соединением с базой данных (упрощённо).
class DatabaseConnection:
def __enter__(self):
print("Opening connection...")
self.conn = "connection_object"
return self.conn
def __exit__(self, exc_type, exc_val, exc_tb):
print("Closing connection...")
self.conn = None
# Использование
with DatabaseConnection() as conn:
print(f"Working with {conn}")
Opening connection... Working with connection_object Closing connection...
Такой подход гарантирует освобождение ресурса даже при исключениях.