Основы синтаксиса Python в ООП

Что такое self? Как реализовать метод объекта?

подробнее

self — ссылка на текущий экземпляр класса. Позволяет обращаться к атрибутам и методам объекта (а также класса).

Реализация метода объекта:

class MyClass:
    def __init__(self, value):
        self.value = value
    
    def my_method(self, x):
        return self.value + x

Что такое cls? Как реализовать метод класса?

подробнее

cls — ссылка на сам класс (не экземпляр). Используется в методах класса.

Реализация метода класса:

class MyClass:
    class_attr = 10
    
    @classmethod
    def my_classmethod(cls, x):
        return cls.class_attr + x
    
    @classmethod
    def create_instance(cls, value):
        return cls(value)

@classmethod — декоратор для создания методов класса.
Особенности:

  • Метод принимает cls вместо self
  • Вызывается через класс или экземпляр
  • Метод класса не имеет доступа до аттрибутов объекта (экземпляра класса)

Что такое статический метод?

подробнее

Статический метод — метод, который не имеет доступа ни к self, ни к cls.

Особенности:

  • Декоратор @staticmethod
  • Не принимает автоматических параметров
  • Не имеет доступа к атрибутам класса или экземпляра
  • По сути обычная функция, принадлежащая классу
  • Можно вызывать как через класс, так и через объект (экземпляр класса)

Используется для:

  • Вспомогательных функций
  • Логики, связанной с классом, но не зависящей от его состояния

Пример:

class MyClass:
    @staticmethod
    def utility_function(x, y):
        return x + y

result = MyClass.utility_function(5, 3)  # вызов через класс

Разница между __new__ и __init__?

подробнее
  • __new__ — создает новый экземпляр класса и возвращает его
  • __init__ — инициализирует уже созданный экземпляр

Другими словами: __new__ отвечает за создание объекта, а __init__ — за его настройку.

Подробнее:

  • __new__ (магический метод создания)
    • Статический метод (хотя и не требует декоратора @staticmethod)
    • Первым аргументом принимает класс (cls)
    • Должен вернуть экземпляр класса (обычно вызывая super().new(cls))
    • Вызывается первым при создании объекта
    • Используется редко, в основном для метапрограммирования, singleton’ов, фабричных методов, immutable объектов
    • __new__ должен вернуть экземпляр — иначе __init__ не будет вызван
  • __init__ (конструктор инициализации)
    • Метод экземпляра
    • Первым аргументом принимает self (уже созданный экземпляр)
    • Ничего не возвращает (всегда None)
    • Вызывается вторым после __new__
    • Используется часто для настройки объекта

Примеры:

# Пример 1: Нормальное использование
class Person:
    def __new__(cls, name, age):
        print("Создание экземпляра Person")
        instance = super().__new__(cls)
        return instance
    
    def __init__(self, name, age):
        print("Инициализация Person")
        self.name = name
        self.age = age

person = Person("Алиса", 25)
# Вывод:
# Создание экземпляра Person
# Инициализация Person
# Пример 2: Singleton (одиночка)
class Singleton:
    _instance = None
    
    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
            print("Создан новый экземпляр")
        else:
            print("Возвращен существующий экземпляр")
        return cls._instance
    
    def __init__(self):
        if not hasattr(self, 'initialized'):
            self.initialized = True
            print("Инициализация Singleton")

# Использование
s1 = Singleton()  # Создан новый экземпляр
s2 = Singleton()  # Возвращен существующий экземпляр
print(s1 is s2)   # True
# Пример 3: Immutable объекты
class ImmutablePoint:
    def __new__(cls, x, y):
        instance = super().__new__(cls)
        # Устанавливаем атрибуты до __init__
        instance._x = x
        instance._y = y
        return instance
    
    def __init__(self, x, y):
        # Нельзя изменить атрибуты, они уже установлены
        pass
    
    @property
    def x(self):
        return self._x
    
    @property
    def y(self):
        return self._y

point = ImmutablePoint(1, 2)
# point.x = 5  # Ошибка! Нельзя изменить
# Пример 4: Фабрика объектов
class Animal:
    def __new__(cls, animal_type):
        if animal_type == "dog":
            return Dog()
        elif animal_type == "cat":
            return Cat()
        else:
            return super().__new__(cls)
    
    def speak(self):
        pass

class Dog(Animal):
    def speak(self):
        return "Гав!"

class Cat(Animal):
    def speak(self):
        return "Мяу!"

# Использование
dog = Animal("dog")
cat = Animal("cat")
print(dog.speak())  # Гав!
print(cat.speak())  # Мяу!

Разница между __str__ и __repr__?

подробнее
  • __str__ — возвращает читаемое строковое представление объекта для конечного пользователя. Используется функцией str() и print()
  • __repr__ — возвращает однозначное строковое представление объекта для разработчиков (в идеале, код, который можно выполнить для воссоздания объекта). Используется функцией repr() и в интерактивной оболочке

Пример __repr__:

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age
    
    def __repr__(self):
        return f"Person('{self.name}', {self.age})"

person = Person("Алиса", 25)
print(repr(person))  # Person('Алиса', 25)
# В интерактивной оболочке:
# >>> person
# Person('Алиса', 25)

Порядок приоритета:

  • Если определен __str__ — используется для print() и str()
  • Если __str__ не определен, но есть __repr__ — используется __repr__
  • Если определен __repr__ — используется для repr() и в интерактивной оболочке
  • Если ничего не определено — используется стандартное представление

    class Example:
        def __init__(self, value):
            self.value = value
      
    obj = Example(42)
    print(obj)      # <__main__.Example object at 0x...>
    print(repr(obj)) # <__main__.Example object at 0x...>
    

Как в python реализуются public, private и protected методы и аттрибуты?

подробнее

В python нет модификаторов доступа и всё по умолчанию public. Вместо модификаторов Python использует соглашения об именовании для обозначения уровня доступа.

  • Public (Публичные)
    • Доступ: Отовсюду — изнутри и снаружи класса
    • Обозначение: Обычные имена без подчеркиваний
    • Использование: Для нормального взаимодействия с объектом
  • Protected (Защищенные)
    • Доступ: Внутри класса и его подклассов
    • Обозначение: Одно подчеркивание в начале _attribute
    • Соглашение: “Защищенный” элемент, не рекомендуется использовать извне
    • Важно: Доступ всё ещё возможен, но это считается плохой практикой
    class MyClass:
        def __init__(self):
            self._protected_attr = "Защищенный атрибут"
          
        def _protected_method(self):
            return "Защищенный метод"
      
    class SubClass(MyClass):
        def access_protected(self):
            # Доступ из подкласса разрешен
            return self._protected_attr
      
    obj = MyClass()
    print(obj._protected_attr)      # Работает, но не рекомендуется
    print(obj._protected_method())  # Работает, но не рекомендуется
    
  • Private (Приватные)
    • Доступ: Только внутри самого класса
    • Обозначение: Два подчеркивания в начале __attribute
    • Механизм: Name mangling (искажение имен)
    • Важно: Python изменяет имя атрибута для затруднения доступа (Name Mangling)
      __attribute_ClassName__attribute
    class MyClass:
        def __init__(self):
            self.__private_attr = "Приватный атрибут"
          
        def __private_method(self):
            return "Приватный метод"
          
        def access_private(self):
            # Доступ внутри класса разрешен
            return self.__private_attr
      
    obj = MyClass()
    # print(obj.__private_attr)     # Ошибка! AttributeError
    # print(obj.__private_method()) # Ошибка! AttributeError
      
    # Но доступ всё же возможен через name mangling:
    print(obj._MyClass__private_attr)  # Работает, но не рекомендуется
    

Рекомендации:

  • Используйте public для нормального API класса
  • Используйте protected для внутренних методов, которые могут быть полезны подклассам
  • Используйте private для действительно внутренней реализации
  • Следуйте соглашениям — уважайте договоренности сообщества
  • Не нарушайте инкапсуляцию — не используйте name mangling для доступа к приватным атрибутам

Зачем используется super()?

подробнее

super() — это встроенная функция, которая позволяет получить доступ к методам и атрибутам родительского класса из дочернего класса.
Она используется для:

  • Вызова методов родителя
  • Получения доступо до аттрибутов родителя
  • Избежания дублирования кода
  • Поддержки множественного наследования
  • Правильного разрешения методов (MRO)

super() возвращает специальный объект-прокси, который предоставляет доступ к атрибутам класса-родителя. Это не экземпляр и не сам класс, а промежуточный объект, который делегирует доступ к родительским атрибутам.

class Parent:
    def method(self):
        return "Parent method"
    
    class_attr = "Parent class attribute"

class Child(Parent):
    def test_super(self):
        s = super()  # Возвращает объект super
        print(type(s))  # <class 'super'>
        print(s)        # <super: Child, <Child object>>
        return s

child = Child()
proxy = child.test_super()
# Вывод:
# <class 'super'>
# <super: Child, <__main__.Child object at 0x...>>

Декораторы @property, @attr.setter, @attr.deleter

подробнее
  • @property — превращает метод в свойство (getter)
  • @attr.setter — сеттер для свойства
  • @attr.deleter — делитер для свойства
class MyClass:
    def __init__(self, value):
        self._value = value
    
    @property
    def value(self):
        return self._value
    
    @value.setter
    def value(self, new_value):
        if new_value > 0:
            self._value = new_value

    @value.deleter
    def value(self):
        del self._value

# Использование
obj = MyClass(10)
print(obj.value)    # getter
obj.value = 20      # setter
del obj.value       # deleter

Что такое дескрипторы?

подробнее

Дескриптор - когда у класса есть аттрибут, который реализует хотя бы 1 из методов: __get__(), __set__(), __delete__().
Проще говоря: это умный посредник, который перехватывает обращение к атрибуту и вместо обычного чтения/записи выполняет кастомный код.
Важное ограничение: Дескрипторы работают, только если они определены на уровне класса, а не внутри метода __init__() экземпляра.

Дополнения:

  • @property - это встроенный готовый дескриптор.
  • Если реализован __set__(), дескриптор всегда «сильнее» обычного значения в self.__dict__.

Протокол дескриптора:

  • __get__(self, instance, owner) - вызывается при чтении атрибута.
  • __set__(self, instance, value) - вызывается при присваивании значения.
  • __delete__(self, instance) - вызывается при удалении атрибута.
  • __set_name__(self, owner, name) - не обязательный метод, который вызывается в момент создания класса для каждого дескриптора, что позволяет дескриптору знать имя атрибута, к которому он привязан.

Пример:

class NonNegative:
    def __set_name__(self, owner, name):
        # Сохраняем имя атрибута, чтобы знать, где лежат данные
        self.name = "_" + name

    def __get__(self, instance, owner):
        # instance — это объект (напр. user), owner — класс (User)
        return getattr(instance, self.name)

    def __set__(self, instance, value):
        if value < 0:
            raise ValueError(f"Поле {self.name} не может быть отрицательным!")
        setattr(instance, self.name, value)

class User:
    age = NonNegative()  # дескриптор

# Тест
user = User()
user.age = 25  # __set__ -> Ок
print(user.age)

user.age = -5  # __set__ -> ValueError