Оптимизация интерфейсов Python: советы по созданию лаконичного и чистого кода
2 марта 2023 г.За свою карьеру я повидал много интерфейсов Python (как в API, а не в пользовательском интерфейсе а>). Вы можете быстро определить уродливого по его размеру. Ниже приведены несколько рецептов, как сделать аккуратный!
Интерфейсы в Python
Интерфейс – это расплывчатое понятие, которое поначалу может быть трудно понять. Это план взаимодействия с объектом (прочитав несколько ответов здесь не помешает).
Вам вообще нужны интерфейсы в Python?
Есть несколько хороших постов об этом:
- Челси Трой
- Переполнение стека (пожалуйста, прочтите несколько ответов! )
- Диего Барба
Я предполагаю, что вы все это прочитали и решили использовать интерфейсы.
Я думаю, что Протоколы
— лучший способ создания интерфейсов в Python. Это то, что мы собираемся использовать здесь.
Здесь мы собираемся обсудить, что делает интерфейс хорошим.
Интерфейс должен быть маленьким
Вы разрабатываете симулятор зоопарка и сделали интерфейс для "обычного животного":
class Animal(Protocol):
def current_weight(self) -> float: ...
def price(self) -> float: ...
def sleep(self, hours): ...
def eat(self, food): ...
def draw(self, context): ...
def look_for_food(self): ...
def hunger_level(self) -> float: ...
def is_asleep(self) -> bool: ...
def is_awake(self) -> bool: ...
def current_position() -> Tuple[float, float, float]: ...
def current_orientation() -> RotationMatrix: ...
def current_transform3d() -> HomogeneousMatrix: ...
Вас должно насторожить, когда вы видите интерфейс с более чем 4-5 методами.
Скорее всего, он был плохо спроектирован, и проект, в котором он используется, будет слишком запутанным:
- этот интерфейс будет использоваться в совершенно не связанных частях программного обеспечения. Это соединит их вместе.
- будет широко использоваться иерархия классов
- реализации интерфейса будут слишком сложными
- это будет сложно задокументировать и запутать в использовании.
Вы можете подумать: «Очевидно, что животные делают массу разных вещей, поэтому наличие множества методов оправдано». Но вы можете придумать еще десять способов, чтобы добавить к «родовому животному», и этому никогда не будет конца. Искусство состоит в том, чтобы смоделировать сложную вещь с помощью набора небольших несвязанных интерфейсов.
Каковы рецепты уменьшения размера интерфейса?
Семантическое перекрытие
Иногда могут быть некоторые очевидные методы дублирования (например, get_weights
против current_weight
). Но чаще вы найдете методы, которые несколько пересекаются по семантике. Немного подумав, можно удалить некоторые из них.
В приведенном выше примере:
def current_position() -> Tuple[float, float, float]:
pass
def current_orientation() -> RotationMatrix:
pass
def current_transform3d() -> HomogeneousMatrix:
pass
Пользователь извлекает положение и ориентацию из HomogeneousMatrix
, возвращаемого current_transform3d
. Два других геттера можно заменить служебными функциями.
Аналогичная ситуация, вероятно, происходит с:
def is_asleep(self) -> bool:
pass
def is_awake(self) -> bool:
pass
Верно ли, что animal.is_asleep() == not animal.is_awake()
? Если да, то вам лучше удалить один из них.
Развязка интерфейсов
После удаления дубликатов попробуйте разделить интерфейс на независимые части.
Скорее всего, вы не используете все функции Animal
во всех частях вашего программного обеспечения:
1. может существовать функция, обрабатывающая рисование животных, которая использует только current_transform3d
и draw
:
def draw_entity(animal: Animal):
t = animal.current_transform3d()
c = some_drawing_context(t)
animal.draw(c)
2. может существовать алгоритм жизненного цикла животных:
def life_management(animal: Animal):
eating_logic(
animal.hunger_level(), animal.look_for_food(),
animal.eat())
sleeping_logic(animal.is_awake(), animal.sleep())
3. и система управления зоопарком
def purchase_decision(animal: Animal, budget: float) -> bool:
w = animal.current_weight()
p = animal.price()
decide_to_buy(p, w, budget)
«Обычное животное» — это набор совершенно разных интерфейсов, которые можно распутать:
class VisualEntity(Protocol):
def current_transform3d() -> HomogeneousMatrix: ...
def draw(self, context): ...
class BehavingAgent:
def sleep(self, hours): ...
def eat(self, food): ...
def look_for_food(self): ...
def hunger_level(self) -> float: ...
def is_awake(self) -> bool: ...
class ZooAsset:
def current_weight(self) -> float: ...
def price(self) -> float: ...
Теперь вы можете увидеть преимущества множества маленьких интерфейсов перед одним большим:
* Вам не нужно отделять части вашей кодовой базы от рабочих процессов компании. Команда рендеринга не будет зависеть от разработчиков управления зоопарком. * Нет необходимости реализовывать ненужные методы для небольших вариаций «Животных». Изменение ценового алгоритма не приводит к появлению нового вида! * Вы сможете повторно использовать небольшие интерфейсы и утилиты. Вы можете обнаружить, что draw_entity и Purchase_decision работают для вашего объекта Plant.
Теперь вы можете увидеть преимущества множества маленьких интерфейсов перед одним большим.
- Они разделяют части вашей кодовой базы (в то время как у больших есть большая вероятность соединения несвязанных частей). Это, в свою очередь, разъединяет рабочие процессы компании (например, команды не ломают слишком много вещей друг друга)
- При создании экземпляра короткого интерфейса не нужно писать много реализаций. Это отражается на вашем дизайне, потому что теперь вам легче избегать базовых классов и иерархий классов. Что, в свою очередь, продвигает композицию вместо наследования (есть много ресурсов о том, почему это хорошая идея, например, см. здесь а>
- Вы сможете повторно использовать маленькие интерфейсы в большем количестве мест, чем большие. Это, в свою очередь, уменьшает общую площадь поверхности API в вашей кодовой базе, упрощает изучение и упрощает адаптацию.
Рекурсивная семантика
Далее вас должно насторожить, если одна функция вызывает другую в том же интерфейсе. Продолжая пример с зоопарком:
class BehavingAnimal(Protocol):
def lifecycle(self):
""" What happens during 24 hours period """
def eat(self, food):
""" What happens when the animal consumes food """
def sleep(self, hours):
""" What happens when the animal sleeps """
Вряд ли животное проведет сутки без еды и сна. Поэтому высока вероятность того, что eat
и sleep
будут вызываться из реализации lifecycle
. Такой интерфейс объединяет два уровня абстракции. Это два интерфейса в одном, аналогично предыдущему разделу.
Часть «жизненного цикла» используется в некоторых глобальных контекстах приложения, но eat
и sleep
, вероятно, используются только локально внутри него.
Давайте разделим их на части:
class ElementaryAnimal(Protocol):
def eat(self, food): ...
def sleep(self, hours): ...
class LifecycleManagement(Protocol):
def lifecycle(self, animal: ElementaryAnimal): ...
Обратите внимание, что теперь lifecycle
принимает ElementaryAnimal
в качестве аргумента.
Это ясно говорит о том, что жизненный цикл зависит от чего-то, что может есть и спать.
Слишком общий
Еще одна вещь, которой следует избегать, – слишком общий интерфейс:
class Creature(Protocol):
def act(self, *args, **kwargs) -> object:
""" Do anything you want """
def perform_action(creature: Creature):
custom_args = ...
creature.act(*custom_args)
Интерфейс подобен договору между двумя сторонами. Но контракт, который говорит «делай что угодно», так же хорош, как и полное отсутствие контракта. Слишком просто создавать такие интерфейсы на необязательном языке с динамической типизацией, таком как Python, особенно с его функциями аргументов с подстановочными знаками.
К сожалению, такие интерфейсы встречаются в некоторых ситуациях, но мы можем обобщить это как «передачу черного ящика».
Передача «черных ящиков» обычно хуже, чем явных компонентов. Но если вам приходится это делать, то лучше признать его «черноту», пройдя функтора и перейти к более функциональному дизайну (вот подробное руководство а>):
def perform_action(actor: Callable[..., Any]):
custom_args = ...
actor(*custom_args)
Конструкторы в интерфейсе
Дополнительная тема по теме: вы заметили, что __init__
обычно не является частью какого-либо интерфейса?
Почему нет? Похоже, это просто еще один способ, которым вы можете воспользоваться.
Это восходит к C++, где конструкторы не являются виртуальными.
Бьерн Страуструп дает ответ, почему это так:
<цитата>«Виртуальный вызов – это механизм выполнения работы с частичной информацией. Для создания объекта нужна полная информация. Следовательно, «вызов конструктора» не может быть виртуальным».
Мне больше нравятся высокоуровневые рассуждения: конструктор — это функция для создания объектов. Он принадлежит к сфере «создателей объектов». Он просто не может принадлежать уже созданному объекту: вы не должны делать объект из себя, и я не фанат клонирование).
Конструктор можно рассматривать как метод интерфейса фабрики. Другими словами, конструктор и объектный интерфейс — это еще один пример двух связанных уровней абстракции.
Заключение
Подводя итог, хороший интерфейс должен:
- быть небольшим и лаконичным
- избегайте семантического дублирования
- избегайте объединения нескольких частей в одну
- избегайте смешивания нескольких уровней абстракции
- быть конкретным и не слишком общим
- не включать
__init__
В то же время реальная жизнь имеет множество исключений и сложностей. Но это должны быть оправданные исключения, а не отговорки, чтобы поддерживать беспорядочные интерфейсы.
Спасибо за прочтение!
Вы можете найти меня в Twitter или LinkedIn.
Первоначально опубликовано по адресу здесь.
Оригинал