7 лучших практик для определения отличных классов
Классы являются фундаментальной концепцией Python и ключом к реализации принципов объектно-ориентированного программирования. Независимо от того, являетесь ли вы новичком или опытным специалистом по Python, понимание того, как эффективно писать классы, имеет решающее значение для создания надежной и удобной в сопровождении программы.
В этом подробном руководстве мы рассмотрим семь основных передовых методов написания отличных классов на Python. Следуя этим рекомендациям, вы сможете создавать хорошо структурированные, повторно используемые и эффективные классы.
1. Знайте, когда НЕ использовать классы
Если нам не нужно писать класс, нам даже не нужно рассматривать все другие лучшие практики.
Python не является чисто объектно-ориентированным языком программирования, и мы не обязаны использовать классы для каждой задачи. Во многих случаях было бы лучше использовать функции, а не классы.
Если задача проста и нам не нужно ни управление состоянием, ни инкапсуляция, то и классы использовать не надо. Например, если нам нужно вычислить квадрат числа, функции более чем достаточно. Для такой простой задачи использование классов не дает значительных преимуществ, но вносит ненужную сложность.
# Using Function def calculate_square(number): return number ** 2 result = calculate_square(5) print(result) # Output: 25 # Using Class class Calculator: def __init__(self, number): self.number = number def calculate_square(self): return self.number ** 2 calculator = Calculator(5) result = calculator.calculate_square() print(result) # Output: 25
Другой случай — когда мы заранее знаем, что не будем создавать несколько экземпляров класса. В этом случае было бы намного проще просто использовать модуль с функциями. Кроме того, с помощью последнего решения мы можем избежать накладных расходов на создание экземпляра класса.
2. Держите свой класс небольшим
Мы всегда должны отдавать предпочтение небольшим классам, а не божественным классам.
Во-первых, это принцип единой ответственности. Согласно этому принципу у класса должна быть только одна причина для изменения. Когда у класса есть сфокусированная ответственность, внесение изменений или устранение проблем становится менее сложным. Кроме того, небольшие классы с четкими обязанностями способствуют лучшей организации кода и снижают вероятность внесения ошибок.
Во-вторых, небольшие классы уменьшают дублирование кода и способствуют повторному использованию кода. Разделяя проблемы на более мелкие классы, мы можем легко повторно использовать их в разных частях нашей кодовой базы.
В-третьих, чем меньше класс, тем меньше у него зависимостей и взаимодействий. Это уменьшает вероятность того, что изменения в одном классе повлияют на многие другие части.
Вот пример класса Бога:
class OrderProcessor: def __init__(self): # Initialization code def process_order(self, order): # Code to process the order # ... def calculate_total(self, order): # Code to calculate the total price of the order # ... def update_inventory(self, order): # Code to update inventory based on the order # ... def send_confirmation_email(self, order): # Code to send order confirmation email to the customer # ... def generate_invoice(self, order): # Code to generate an invoice for the order # ... # ... more methods related to order processing
В этом примере класс OrderProcessor
имеет несколько обязанностей, таких как обработка заказов, расчет общей стоимости, обновление запасов, отправка электронных писем с подтверждением и создание счетов.
Лучшее решение — реорганизовать его в отдельные классы с разными обязанностями.
class OrderProcessor: def __init__(self, order_validator, inventory_manager, email_sender, invoice_generator): self.order_validator = order_validator self.inventory_manager = inventory_manager self.email_sender = email_sender self.invoice_generator = invoice_generator def process_order(self, order): if self.order_validator.validate(order): self.inventory_manager.update_inventory(order) self.email_sender.send_confirmation_email(order) self.invoice_generator.generate_invoice(order) class OrderValidator: def validate(self, order): # Code to validate the order # ... class InventoryManager: def update_inventory(self, order): # Code to update inventory based on the order # ... class EmailSender: def send_confirmation_email(self, order): # Code to send order confirmation email to the customer # ... class InvoiceGenerator: def generate_invoice(self, order): # Code to generate an invoice for the order # ...
В этой рефакторинговой версии обязанности класса OrderProcessor
были выделены в отдельные классы с конкретными задачами. Класс OrderValidator
проверяет заказ, класс InventoryManager
обрабатывает обновления запасов, класс EmailSender
отвечает за отправку электронных писем с подтверждением, а класс InvoiceGenerator
генерирует счета.
Рассмотрим гипотетическую ситуацию, когда возникает новое требование: системе обработки заказов необходимо интегрироваться со сторонним платежным шлюзом. Платежный шлюз требует дополнительных шагов проверки и специальной обработки в процессе обработки заказа.
В версии класса God нам нужно будет изменить класс OrderProcessor
, добавив новые функции, связанные с платежами. Однако это создает риск нарушения существующего кода и делает класс еще более раздутым и сложным.
Напротив, в рефакторинговой версии с использованием отдельных классов, таких как PaymentGateway
, вы можете легко интегрировать новые платежные функции без изменения класса OrderProcessor
. Вы просто создаете новый класс для платежного шлюза и внедряете его в OrderProcessor
в качестве зависимости. Такой подход изолирует обязанности, упрощает обслуживание кодовой базы и упрощает интеграцию новых функций без ущерба для существующей функциональности.
3. Явные атрибуты экземпляра в __init__
Python не заставляет нас определять все атрибуты экземпляра в __init___
. Фактически, дополнительные атрибуты экземпляра могут быть определены в последующих операциях после создания экземпляра. Вот пример:
class Car: def __init__(self, brand): self.brand = brand def set_owner(self, owner): self.owner = owner def set_color(self, color): self.color = color
Однако то, что вы можете, не означает, что вы должны. Определение дополнительных атрибутов вне метода __init__
может сделать код менее понятным и трудным для понимания. Ниже приведены некоторые причины, по которым обычно считается хорошей практикой определять все атрибуты экземпляра в методе __init__
:
- Читаемость и организация кода. Размещение всех определений атрибутов в методе
__init__
обеспечивает четкое и централизованное место для понимания структуры данных класса. Мы можем легко определить и понять инициализируемые атрибуты, просто взглянув на метод__init__
. - Намерение и инициализация. В Python метод
__init__
специально разработан для инициализации состояния объекта. Определяя атрибуты в этом методе, мы ясно говорим, что эти атрибуты необходимы и должны быть установлены при создании экземпляра. Это способствует согласованности и помогает избежать ситуаций, когда некоторые атрибуты отсутствуют или не инициализированы. - Поддерживаемость кода. Размещение определений атрибутов в
__init__
упрощает изменение или расширение класса в будущем. Если мы хотим добавить или удалить атрибуты, нет необходимости выискивать разрозненные определения атрибутов по всему классу. Все изменения можно сделать в одном месте (__init__
). - Предотвращение ошибок. Определение атрибутов в методе
__init__
помогает предотвратить ошибки, связанные с атрибутами. Если к атрибуту обращаются до того, как он был определен, Python вызывает исключениеAttributeError
. Инициализируя атрибуты в__init__
, вы гарантируете их доступность с самого начала и избегаете таких ошибок во время выполнения.
Также рекомендуется использовать None
для атрибутов, которые нельзя установить изначально. Это помогает избежать потенциальных исключений AttributeError
при попытке доступа к этим атрибутам до того, как им было присвоено значение.
Вот улучшенная версия приведенного выше примера:
class Car: def __init__(self, brand): self.brand = brand self.owner = None self.color = None def set_owner(self, owner): self.owner = owner def set_color(self, color): self.color = color
4. Значимое и описательное имя
Классы должны иметь осмысленные и описательные имена. Хорошо названные классы помогают передать назначение и обязанности кода и облегчают вам и другим разработчикам работу с системой и ее расширение в будущем. Это не только улучшает читабельность и понимание вашего кода, но и способствует повторному использованию кода. Если вы дадите своим классам осмысленные имена, другие разработчики с большей вероятностью будут повторно использовать их в своем коде. Это может сэкономить ваше время и усилия, а также может помочь улучшить общее качество вашего кода.
Кроме того, хорошее имя может помочь вам избежать ошибок, давая понять, что должен делать класс. Например, если у вас есть класс, представляющий автомобиль, вы должны назвать его Car
вместо C
. Таким образом, у вас меньше шансов случайно использовать его как Cat
.
Важно отметить, что описательное и осмысленное имя важно не только для классов, но и для их атрибутов и методов.
Руководство по стилю Google Python — хороший ресурс для этой передовой практики.
5. Используйте @property правильно
Вместо геттеров и сеттеров наш код может выглядеть более Pythonic с @property
. Как только свойство определено с помощью декоратора свойств, к нему можно получить доступ и изменить его, как если бы это был обычный атрибут с использованием записи через точку. Однако за кулисами свойство реализуется с помощью функций, которые вызываются при доступе к атрибуту или присвоении ему значения. В приведенном ниже примере показано, как@property
используется для реализации геттеров и сеттеров.
class Circle: def __init__(self, radius): self.__radius: float = radius @property def radius(self): return self.__radius @radius.setter def radius(self, value: float): if value > 0: self.__radius = value else: raise ValueError("Radius must be a positive value.") # Creating an instance of the Circle class my_circle = Circle(5) # Accessing the radius attribute using the getter print(my_circle.radius) # Output: 5 # Using the setter to change the radius my_circle.radius = 7 print(my_circle.radius) # Output: 7 # Trying to set an invalid radius my_circle.radius = -2 # Raises a ValueError
Недвижимость имеет ряд преимуществ. Например, свойства обеспечивают уровень контроля и инкапсуляции над доступом к атрибутам и их модификацией (путем не реализации метода установки).
Однако, как и со многими вещами в жизни, если мы злоупотребляем свойствами, все портится. Когда у класса слишком много свойств, это может увеличить сложность и затруднить понимание и сопровождение кода. При чрезмерном количестве свойств интерфейс класса становится загроможденным, и становится сложно определить, какие свойства являются важными или имеют особое поведение.
В большинстве случаев свойства можно заменить атрибутами экземпляра. Если свойство не предоставляет каких-либо дополнительных преимуществ, таких как проверка, вычисляемые значения или инкапсуляция, нам следует рассмотреть возможность использования обычного атрибута экземпляра. Эта практика упрощает код и делает его более понятным.
6. Используйте внедрение зависимостей
При разработке программного обеспечения важно добиться слабой связи между классами. Это способствует модульности, тестируемости и гибкости. Одним из способов достижения этой цели является использование внедрения зависимостей.
Внедрение зависимостей — это шаблон проектирования, который включает в себя экстернализацию зависимостей и предоставление их классам извне, вместо того, чтобы класс создавал или получал свои зависимости внутри, если в этом нет абсолютной необходимости.
Пример 1. Без внедрения зависимостей (тесно связанные)
class Logger: def log(self, message: str): # Code to log the message print(message) class User: def __init__(self): self.logger = Logger() # Creating an instance of Logger internally def register(self, username: str): # Code to register the user self.logger.log(f"User '{username}' registered successfully.") user = User() user.register("John Cena")
В этом примере класс User
напрямую создает экземпляр класса Logger
в своем методе __init__
. Класс User
тесно связан с конкретной реализацией Logger
. Если мы когда-нибудь захотим изменить или смоделировать класс Logger
в целях тестирования, это станет сложной задачей, потому что класс User
напрямую зависит от конкретного класса Logger
.
Пример 2. С внедрением зависимостей (слабая связь)
from abc import ABC, abstractmethod class ILogger(ABC): @abstractmethod def log(self, message: str): pass class Logger(ILogger): def log(self, message: str): # Code to log the message print(message) class LoggerMeme(ILogger): def log(self, message: str): # Code to log the message print(f'{message} You can\'t see me') class User: def __init__(self, logger: ILogger): self.logger = logger # Injecting the logger dependency def register(self, username: str): # Code to register the user self.logger.log(f"User '{username}' registered successfully.") logger = Logger() user = User(logger) # Injecting the logger dependency into the User class user.register("John Cena") logger_meme = LoggerMeme() user_meme = User(logger_meme) # Injecting the logger dependency into the UserMeme class user_meme.register("John Cena")
Мы внесли изменение в __init__
метод User
. Класс User
больше не отвечает за создание экземпляра Logger
, а вместо этого полагается на объект logger
, предоставленный извне (внедрение зависимостей). Это делает класс User
слабо связанным с классом Logger
и обеспечивает большую гибкость. Мы можем легко поменять местами различные реализации интерфейса ILogger
(например, Logger
и LoggerMeme
) без изменения класса User
. Мокать logger
во время тестирования также становится намного проще.
7. Инкапсуляция
Инкапсуляция — ключевая концепция Python и объектно-ориентированного программирования. По сути, это поощряет сокрытие информации и защиту данных внутри классов. Ниже приведены некоторые преимущества инкапсуляции:
- Предотвратить несанкционированное или непреднамеренное изменение состояния объекта. Это способствует целостности данных и снижает риск ошибок.
- Скройте детали внутренней реализации класса и выставляйте только необходимый интерфейс или общедоступные методы. Эта абстракция защищает потребителей класса от ненужной сложности и обеспечивает четкую границу между внутренней работой и внешним использованием.
В Python мы используем префиксы _
и __
для обозначения предполагаемой доступности атрибутов и методов. Атрибуты и методы, начинающиеся с одиночного подчеркивания _
, обычно считаются «защищенными» или внутренними по отношению к классу, а те, которые начинаются с двойного подчеркивания __
, обычно считаются «личными» для класса.
Примечательно, что в Python нет строгих модификаторов доступа, как в некоторых других языках программирования. В Java нет такой вещи, как private
, а есть только механизм для указания предполагаемой доступности атрибутов и методов. Однако, если мы захотим, мы все еще можем получить доступ к этим атрибутам и методам. Короче говоря, инкапсуляция в Python — это скорее соглашение или соглашение, чем строгое правило. Причина? Гвидо ван Россум, создатель Python, считает, что «программисты — взрослые люди по обоюдному согласию».
class EncapsulationClass: def __init__(self): self.__private_attribute = 23 def __private_method(self): return "This is a private method." def public_method(self): return self.__private_attribute * 3 # Creating an instance of the class obj = EncapsulationClass() # Accessing the public method result = obj.public_method() print(result) # Output: 69 # Trying to access the private attribute directly # Note: Name mangling changes the attribute name to _ClassName__attribute private_attr = obj._EncapsulationClass__private_attribute print(private_attr) # Output: 23 # Trying to call the private method directly # Note: Name mangling changes the method name to _ClassName__method private_method_result = obj._EncapsulationClass__private_method() print(private_method_result) # Output: "This is a private method." # Trying to access the private attribute without name mangling results in AttributeError print(obj.__private_attribute)
В приведенном выше примере у нас есть класс EncapsulationClass
с приватным атрибутом __private_attribute
и приватным методом __private_method
. Префикс с двойным подчеркиванием инициирует изменение имени, которое изменяет имя атрибута или метода на _EncapsulationClass__attribute
или _EncapsulationClass__method
соответственно.
«🔔 Хотите больше таких статей? Подпишите здесь."
Спасибо за прочтение. Я надеюсь, что это руководство будет полезно для вас.
Повышение уровня кодирования
Спасибо, что являетесь частью нашего сообщества! Перед тем, как ты уйдешь:
- 👏 Хлопайте за историю и подписывайтесь на автора 👉
- 📰 Смотрите больше контента в публикации Level Up Coding
- 💰 Бесплатный курс собеседования по программированию ⇒ Просмотреть курс
- 🔔 Подписывайтесь на нас: Twitter | ЛинкедИн | "Новостная рассылка"
🚀👉 Присоединяйтесь к коллективу талантов Level Up и найдите прекрасную работу