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 и найдите прекрасную работу