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__:

  1. Читаемость и организация кода. Размещение всех определений атрибутов в методе __init__ обеспечивает четкое и централизованное место для понимания структуры данных класса. Мы можем легко определить и понять инициализируемые атрибуты, просто взглянув на метод __init__.
  2. Намерение и инициализация. В Python метод __init__ специально разработан для инициализации состояния объекта. Определяя атрибуты в этом методе, мы ясно говорим, что эти атрибуты необходимы и должны быть установлены при создании экземпляра. Это способствует согласованности и помогает избежать ситуаций, когда некоторые атрибуты отсутствуют или не инициализированы.
  3. Поддерживаемость кода. Размещение определений атрибутов в __init__ упрощает изменение или расширение класса в будущем. Если мы хотим добавить или удалить атрибуты, нет необходимости выискивать разрозненные определения атрибутов по всему классу. Все изменения можно сделать в одном месте (__init__).
  4. Предотвращение ошибок. Определение атрибутов в методе __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 и найдите прекрасную работу