Изучение специальных методов в классах Python

PythonBeginner
Практиковаться сейчас

Введение

В этой лабораторной работе вы изучите некоторые специальные методы Python, часто называемые "dunder" методами из-за их имен с двойным подчеркиванием. Вы получите практическое понимание того, как эти методы позволяют настраивать поведение ваших классов и объектов.

Вы узнаете о методе __new__ для управления созданием экземпляров и методе __del__ для уничтожения объектов. Вы также увидите, как использовать __slots__ для оптимизации использования памяти и ограничения атрибутов, а также как сделать экземпляры вашего класса вызываемыми, как функции, с помощью метода __call__. С помощью практических примеров вы научитесь писать более эффективный и выразительный код на Python.

Это Guided Lab, который предоставляет пошаговые инструкции, чтобы помочь вам учиться и практиковаться. Внимательно следуйте инструкциям, чтобы выполнить каждый шаг и получить практический опыт. Исторические данные показывают, что это лабораторная работа уровня начальный с процентом завершения 100%. Он получил 100% положительных отзывов от учащихся.

Понимание и использование метода __new__

На этом шаге вы изучите метод __new__. В то время как __init__ обычно используется для инициализации атрибутов объекта после его создания, __new__ — это метод, который фактически создает экземпляр в первую очередь. Он вызывается перед __init__.

Вот ключевые различия:

  • __new__ — это статический метод, который принимает класс (cls) в качестве первого аргумента. Он отвечает за создание и возврат нового экземпляра класса.
  • __init__ — это метод экземпляра, который принимает экземпляр (self) в качестве первого аргумента. Он инициализирует только что созданный объект и ничего не возвращает.

Обычно вам не нужно переопределять __new__, поскольку реализации по умолчанию из класса object достаточно. Однако он полезен в продвинутых случаях, таких как реализация паттерна Singleton или создание экземпляров неизменяемых типов (immutable types).

Давайте посмотрим на __new__ в действии. Вы создадите класс Dog, который выводит сообщение во время создания экземпляра.

Сначала откройте файл dog_cat.py в проводнике файлов слева от IDE.

Добавьте следующий код в файл dog_cat.py. Этот код определяет базовый класс Animal и подкласс Dog, который переопределяет метод __new__.

## File Name: dog_cat.py

class Animal:
    def __init__(self, name):
        self._name = name
        print(f'Initializing {self._name} in Animal.')

    def say(self):
        print(self._name + ' is saying something')

class Dog(Animal):
    ## Первый параметр — cls, который ссылается на сам класс.
    ## Он также должен принимать любые аргументы, переданные конструктору.
    def __new__(cls, name, age):
        print('A new Dog instance is being created.')
        ## Вызываем метод __new__ родительского класса для создания экземпляра.
        instance = super().__new__(cls)
        return instance

    def __init__(self, name, age):
        print(f'Initializing {name} in Dog.')
        ## Вызывается __init__ родительского класса для установки имени.
        super().__init__(name)
        self.age = age

    def say(self):
        print(self._name + ' is making a sound: wang wang wang...')

## Создаем экземпляр Dog
print("Creating a Dog object...")
d = Dog('Buddy', 5)
print("Dog object created.")
print(f"Dog's name: {d._name}, Age: {d.age}")

Сохраните файл (Ctrl+S или Cmd+S).

Теперь откройте терминал в вашей IDE (вы можете использовать меню Terminal > New Terminal). Запустите скрипт, чтобы увидеть порядок вызова методов.

python ~/project/dog_cat.py

Вы увидите следующий вывод. Обратите внимание, что __new__ вызывается первым для создания экземпляра, а затем вызываются методы __init__ для его инициализации.

Creating a Dog object...
A new Dog instance is being created.
Initializing Buddy in Dog.
Initializing Buddy in Animal.
Dog object created.
Dog's name: Buddy, Age: 5

Это демонстрирует, что __new__ контролирует создание объекта, а __init__ настраивает его после этого.

Реализация и тестирование метода __del__

На этом шаге вы узнаете о методе __del__. Этот метод называется финализатором или деструктором. Он вызывается, когда счетчик ссылок на объект падает до нуля, что означает, что он вот-вот будет уничтожен сборщиком мусора Python (garbage collector). Он часто используется для задач очистки, таких как закрытие сетевых соединений или файловых дескрипторов.

Вы можете удалить ссылку на объект, используя оператор del. Когда последняя ссылка исчезает, __del__ вызывается автоматически.

Давайте добавим метод __del__ в наш класс Dog, чтобы увидеть, когда объект уничтожается.

Снова откройте файл dog_cat.py. Замените все содержимое файла следующим кодом. Эта версия удаляет код, который создает экземпляр Dog (чтобы избежать его создания при импорте модуля), и добавляет метод __del__ в класс Dog.

## File Name: dog_cat.py

class Animal:
    def __init__(self, name):
        self._name = name

    def say(self):
        print(self._name + ' is saying something')

class Dog(Animal):
    def __new__(cls, name, age):
        instance = super().__new__(cls)
        return instance

    def __init__(self, name, age):
        super().__init__(name)
        self.age = age

    def say(self):
        print(self._name + ' is making a sound: wang wang wang...')

    ## Добавьте этот метод в класс Dog
    def __del__(self):
        print(f'The Dog object {self._name} is being deleted.')

Сохраните файл dog_cat.py.

Теперь давайте создадим отдельный скрипт для тестирования этого поведения. Откройте файл test_del.py в проводнике файлов.

Добавьте следующий код в test_del.py. Этот скрипт создаст два экземпляра Dog, а затем явно удалит один из них.

## File Name: test_del.py

from dog_cat import Dog
import time

print("Creating two Dog objects: d1 and d2.")
d1 = Dog('Tom', 3)
d2 = Dog('John', 5)

print("\nDeleting reference to d1...")
del d1
print("Reference to d1 deleted.")

## Сборщик мусора может запуститься не сразу.
## Мы добавляем небольшую задержку, чтобы дать ему время поработать.
time.sleep(1)

print("\nScript is about to end. d2 will be deleted automatically.")

Сохраните файл. Теперь запустите скрипт test_del.py в терминале.

python ~/project/test_del.py

Наблюдайте за выводом. Сообщение __del__ для Tom появляется после вызова del d1. Сообщение для John появляется в самом конце, так как объект d2 собирается сборщиком мусора по завершении работы скрипта.

Creating two Dog objects: d1 and d2.

Deleting reference to d1...
The Dog object Tom is being deleted.
Reference to d1 deleted.

Script is about to end. d2 will be deleted automatically.
The Dog object John is being deleted.

Примечание: Точное время запуска сборки мусора может варьироваться. __del__ вызывается, когда объект собирается, а не обязательно сразу после использования del.

Управление атрибутами с помощью __slots__

На этом шаге вы узнаете о __slots__. По умолчанию Python хранит атрибуты экземпляра в специальном словаре, называемом __dict__. Это позволяет добавлять новые атрибуты к объекту в любое время. Однако эта гибкость требует дополнительной памяти.

Определив атрибут __slots__ в вашем классе, вы можете указать фиксированный список атрибутов, которые могут быть у экземпляров. Это имеет два основных следствия:

  1. Экономия памяти: Python использует более компактную внутреннюю структуру вместо __dict__ для каждого экземпляра, что может значительно снизить потребление памяти, особенно при создании большого количества объектов.
  2. Ограничение атрибутов: Вы больше не сможете добавлять к экземпляру атрибуты, не перечисленные в __slots__. Это помогает предотвратить опечатки и обеспечивает строгую структуру объекта.

Давайте создадим пример, чтобы увидеть, как работает __slots__. Откройте файл slots_example.py в проводнике файлов.

Добавьте следующий код в slots_example.py:

## File Name: slots_example.py

class Player:
    ## Определяем разрешенные атрибуты с помощью __slots__
    __slots__ = ('name', 'level')

    def __init__(self, name, level):
        self.name = name
        self.level = level

## Создаем экземпляр Player
p1 = Player('Hero', 10)

## Доступ к разрешенным атрибутам
print(f"Player name: {p1.name}")
print(f"Player level: {p1.level}")

## Теперь попытаемся добавить новый атрибут, которого НЕТ в __slots__
print("\nTrying to add a 'score' attribute...")
try:
    p1.score = 100
    print(f"Player score: {p1.score}")
except AttributeError as e:
    print(f"Caught an error: {e}")

## Также проверим, есть ли у экземпляра атрибут __dict__
print("\nChecking for __dict__...")
try:
    print(p1.__dict__)
except AttributeError as e:
    print(f"Caught an error: {e}")

Сохраните файл. Теперь запустите скрипт slots_example.py в терминале.

python ~/project/slots_example.py

Вывод показывает, что вы можете присваивать значения name и level, но попытка присвоить значение score вызывает AttributeError. Это также подтверждает, что у экземпляра нет __dict__.

Player name: Hero
Player level: 10

Trying to add a 'score' attribute...
Caught an error: 'Player' object has no attribute 'score'

Checking for __dict__...
Caught an error: 'Player' object has no attribute '__dict__'

Это демонстрирует, как __slots__ может обеспечить фиксированный набор атрибутов и оптимизировать память за счет исключения словаря экземпляра.

Сделать экземпляры вызываемыми с помощью __call__

На этом шаге вы изучите метод __call__. В Python объекты, которые можно "вызывать" с помощью круглых скобок () как функции, известны как вызываемые объекты (callable objects). Функции и методы по своей природе являются вызываемыми.

По умолчанию экземпляры классов не являются вызываемыми. Однако, если вы определите специальный метод __call__ в классе, его экземпляры становятся вызываемыми. Когда вы вызываете экземпляр как функцию, выполняется код внутри его метода __call__. Это полезно для создания объектов, которые ведут себя как функции, но при этом могут сохранять свое внутреннее состояние.

Давайте создадим класс, экземпляры которого можно вызывать. Откройте файл callable_instance.py в проводнике файлов.

Добавьте следующий код в callable_instance.py:

## File Name: callable_instance.py

class Greeter:
    def __init__(self, greeting):
        ## Это состояние хранится вместе с экземпляром
        self.greeting = greeting
        print(f'Greeter initialized with "{self.greeting}"')

    ## Определяем метод __call__, чтобы сделать экземпляры вызываемыми
    def __call__(self, name):
        ## Этот код выполняется при вызове экземпляра
        print(f"{self.greeting}, {name}!")

## Создаем экземпляр Greeter
hello_greeter = Greeter("Hello")

## Проверяем, является ли экземпляр вызываемым, используя встроенную функцию callable()
print(f"Is hello_greeter callable? {callable(hello_greeter)}")

## Теперь вызываем экземпляр так, как если бы это была функция
print("\nCalling the instance:")
hello_greeter("Alice")
hello_greeter("Bob")

## Создаем еще один экземпляр с другим состоянием
goodbye_greeter = Greeter("Goodbye")
print("\nCalling the second instance:")
goodbye_greeter("Charlie")

Сохраните файл. Теперь запустите скрипт callable_instance.py в терминале.

python ~/project/callable_instance.py

Вывод показывает, что экземпляр hello_greeter действительно является вызываемым. Каждый раз, когда вы его вызываете, выполняется метод __call__, использующий состояние (self.greeting), которое было установлено во время инициализации.

Greeter initialized with "Hello"
Is hello_greeter callable? True

Calling the instance:
Hello, Alice!
Hello, Bob!
Greeter initialized with "Goodbye"

Calling the second instance:
Goodbye, Charlie!

Это демонстрирует, как __call__ позволяет создавать объекты с сохраняемым состоянием, похожие на функции, что является мощной возможностью в объектно-ориентированном программировании.

Резюме

В этой лабораторной работе вы изучили несколько мощных специальных методов Python. Вы узнали, как использовать __new__ для управления процессом создания экземпляра, что дает вам точку перехвата до вызова __init__. Вы реализовали метод __del__ для определения логики очистки, которая выполняется при сборке мусора объекта. Вы также использовали __slots__ для оптимизации памяти и обеспечения строгой модели атрибутов путем предотвращения создания словарного объекта экземпляра (__dict__). Наконец, вы заставили ваши объекты вести себя как функции, реализовав метод __call__. Освоив эти "дундер" методы (dunder methods), вы сможете писать более гибкие, эффективные и идиоматичные (Pythonic) классы.