В процессе разработки часто возникает необходимость получить информацию о созданном объекте: его атрибутах, методах или просто читабельное строковое представление. Для этого в Python существуют специальные инструменты.

Магические методы __str__() и __repr__()

Когда мы работали с объектами встроенных типов данных, то достаточно легко могли выводить их на экран. Но если мы используем функцию print() с объектом собственного класса, то увидим лишь название класса и адрес этого объекта в памяти компьютера.

Давайте создадим простой класс Student для работы со студентами и выведем на экран объект этого класса:

class Student:
    def __init__(self, name: str, email: str):
        self.name = name
        self.email = email


student1 = Student("Романова Е.А.", "best_of_the_best@example.ru")
print(student1)
# Вывод: <__main__.Student object at 0x...>

Здесь вы видите имя __main__, означающее, что данный класс был определен в запускаемом файле. А также название класса объекта и адрес этого объекта в шестнадцатеричной системе счисления.

Но с помощью специальных методов __str__() и __repr__() мы можем контролировать строковое представление объекта, которое вызывается, например, функцией print():

  • Метод __str__() предназначен для пользователя, поэтому он должен возвращать читаемую и понятную человеку строку. Этот метод вызывает уже знакомая нам функция str(), которая для встроенных типов данных просто преобразует значение объекта в строку.
  • Метод __repr__() (от англ. representation – представление) предназначен для разработчика, поэтому он должен возвращать формальное описание объекта, которое однозначно описывает его и может использоваться для восстановления объекта. Этот метод вызывает функция repr().

Функция

repr(object)

Описание

Возвращает формальное строковое представление объекта object

Параметры

  • object – объект, чьё формальное строковое представление следует получить

Возвращаемое значение

Формальное строковое представление объекта

Давайте добавим методы __str__() и __repr__() в наш класс Student:

class Student:
    def __init__(self, name: str, email: str):
        self.name = name
        self.email = email
        
    def __str__(self) -> str:
        return f"Студент: {self.name}, электронная почта: {self.email}"

    def __repr__(self) -> str:
        return f"Student(name='{self.name}', email='{self.email}')"

Теперь при выводе объекта этого класса на экран функция print() будет вызывать метод __str__():

student2 = Student("Рюрикович И.В.", "iv_the_terr@example.ru")
print(student1)
# Вывод: Студент: Рюрикович И.В., электронная почта: iv_the_terr@example.ru

Если бы мы его не определили, то функция print() вызывала бы метод __repr__(). Также мы можем вызвать этот метод с помощью функции repr():

print(repr(student2))
# Вывод: Student(name=Рюрикович И.В.', email='iv_the_terr@example.ru')

Восстановление объекта и функция eval()

Если строка, возвращаемая методом __repr__() является синтаксически корректным выражением на Python, которое при выполнении создаст такой же объект, то мы можем восстановить этот объект с помощью функции eval().

Функция

eval(expression)

Описание

Возвращает результат выполнения строки expression

Параметры

  • expression – строка, которая должна быть выполнена как команда на Python

Возвращаемое значение

Результат выполнения строки

Функция eval() принимает строку и выполняет ее как выражение на Python. Это позволяет превратить строковое представление объекта, полученное с помощью __repr__(), обратно в сам объект.

Давайте вернемся к нашему классу Student и восстановим объект на основе его строкового представления в методе __repr__():

student3 = Student("Романов А.М.", "the_second@example.ru")    
restored_student = eval(repr(student3))
print(f"Восстановленный объект: {restored_student}")
# Вывод: Восстановленный объект: Студент: Романов А.М., электронная почта: the_second@example.ru

Здесь функция eval() выполняет код, который был возвращен методом __repr__(), поэтому это создаёт новый объект класса Student, полностью идентичный исходному, а функция print() выводит на экран его строковое представление, возвращаемое методом __str__().

На самом деле функция eval() может не только восстанавливать объекты, но и выполнять любые корректные выражения на языке Python:

print(eval("2 + 2"))
# Вывод: 4
print(eval("round(3.1415, 2)"))
# Вывод: 3.14

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

Список атрибутов и методов объекта

Для любого объекта можно получить список всех его атрибутов и методов с помощью функции dir().

Функция

dir(object)

Описание

Возвращает список атрибутов и методов объекта

Параметры

  • object – объект, для которого нужно получить список атрибутов и методов

Возвращаемое значение

Список атрибутов и методов объекта

Например, у нас есть класс MagicItem, описывающий магический предмет в игре:

class MagicItem:
    def __init__(self, name: str, charge: int, casting_time: int = 5):
        self.name = name  # Название
        self.charge = charge  # Заряд
        self.casting_time = casting_time  # Время для использования

    def use(self) -> None:
        # Использует предмет, только если есть заряд
        if self.charge > 0:
            self.charge -= 1
            print(f"{self.name} использован! Осталось: {self.charge}")
        else:
            print(f"{self.name} разряжен. Необходима перезарядка")

Давайте создадим экземпляр класса MagicItem и передадим его функции dir():

artifact = MagicItem("Волшебный посох", 3)
print(dir(artifact))

В результате на экране вы увидите достаточно длинный список всех атрибутов и методов этого объекта:

['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__firstlineno__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__static_attributes__', '__str__', '__subclasshook__', '__weakref__', 'casting_time', 'charge', 'name', 'use']

Этот список содержит все магические методы этого объекта, унаследованные от базового класса object, а также все атрибуты и методы, принадлежащие объекту artifact.

Робот Кеша читает

Объект object является базовым классом, от которого неявно наследуются все остальные классы в Python. Он определяет базовую реализацию для всех магических методов и когда вы создаете новый класс, вы не пишите все эти методы с нуля, а просто переопределяете ту реализацию, которую предоставил object. Также object гарантирует, что любой объект в Python (будет ли это число, строка, список или ваш собственный класс) будет иметь общие методы, такие как __str__() или __repr__().

Поэтому функция dir() может быть полезна при работе с незнакомым кодом, поскольку позволяет быстро исследовать новый объект. Например, если вы получаете некий объект из функции и не знаете, какие методы он поддерживает, вызов dir() немедленно даст вам список всех доступных команд.

Для собственных классов Python предоставляет способ переопределить результат вызова функции dir() с помощью магического метода __dir__(). Он позволяет классу явно контролировать, какие имена (атрибуты и методы) будут включены в список, возвращаемый функцией dir(). Единственным ограничением является то, что метод __dir__() должен возвращать список строк.

Теперь переопределим метод __dir__() в классе MagicItem таким образом, чтобы он возвращал только созданные нами атрибуты и методы:

class MagicItem:
    def __init__(self, name: str, charge: str, casting_time: int = 5):
        self.name = name  # Название
        self.charge = charge  # Заряд
        self.casting_time = casting_time  # Время для использования

    def use(self) -> None:
        """Использует предмет, только если есть заряд."""
        if self.charge > 0:
            self.charge -= 1
            print(f"{self.name} использован! Осталось: {self.charge}")
        else:
            print(f"{self.name} разряжен. Необходима перезарядка")

    def __dir__(self) -> list[str]:
        return ["name", "charge", "casting_time", "use"]

В результате функция dir() вернёт именно тот список, который возвращает метод __dir__():

artifact = MagicItem("Волшебная палочка", 5, 3)
print(dir(artifact))
# Вывод: ['casting_time', 'charge', 'name', 'use']

Это не влияет на доступ к атрибутам – объект по-прежнему содержит все свои данные.

Функции type и isinstance()

Как и при работе со встроенными типами данных, для объектов собственных классов мы также можем вызвать функцию type() для определения к какому классу принадлежит объект и функцию isinstance() для проверки объекта на принадлежность к какому-то классу.

Допустим, у нас есть класс для точки Dot, которая определяется координатами x и y. Функция type() для объекта этого класса вернёт строку <class '__main__.Dot'>, а функция isinstance(), в зависимости от проверяемого класса, вернёт True или False:

class Dot:
    def __init__(self, x: int | float, y: int | float):
        self.x = x
        self.y = y
    

dot1 = Dot(3, 5)
print(type(dot1))
# Вывод: <class '__main__.Dot'>
print(isinstance(dot1, int))
# Вывод: False
print(isinstance(dot1, Dot))
# Вывод: True

Основное различие между функциями type() и isinstance() проявляется при работе с наследованными классами, так как объект дочернего класса, по сути, является также и объектом родительского класса.

Давайте создадим класс для цветной точки ColorDot, который будет наследоваться от класса Dot:

class ColorDot(Dot):
    def __init__(self, x: int | float, y: int | float, color: str):
        super().__init__(x, y)
        self.color = color

Функция isinstance() учитывает иерархию наследования и вернет True, если объект является экземпляром указанного класса (ColorDot) или любого из его родительских классов (Dot):

colored_dot = ColorDot(1, 2, "Красный")
print(isinstance(colored_dot, ColorDot))
# Вывод: True
print(isinstance(colored_dot, Dot))
# Вывод: True

Также функция isinstance() может принимать кортеж классов вторым аргументом. Она вернет True, если объект принадлежит хотя бы одному из них:

print(isinstance(colored_dot, (int, Dot, float)))
# Вывод: True

Функция type(), в свою очередь, просто возвращает класс объекта и не учитывает наследование классов:

print(type(colored_dot))
# Вывод: <class '__main__.ColorDot'>

Атрибут __dict__

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

Допустим, у нас есть класс DatabaseConnection для подключения к базе данных, в таком случае атрибут __dict__ позволяет получить словарь с атрибутами экземпляра класса: имя базы данных db_name и статус is_active:

class DatabaseConnection:
    def __init__(self, db_name: str):
        self.db_name = db_name
        self._is_active = False
        

db1 = DatabaseConnection("my_db")
print(db1.__dict__)
# Вывод: {'db_name': 'my_db', '_is_active': False}

Примеры

Пример 1. Описание книги в библиотеке

Класс Book описывает книгу в библиотеке. Красивое и читабельное описание объекта обеспечивает метод __str__(), а формальное, которое может быть использовано для восстановления функцией eval(), обеспечивает метод __repr__():

class Book:
    """Описывает книгу в библиотеке."""
    
    def __init__(self, title: str, author: str, year: int):
        """Конструктор класса Book.

        Параметры:
            title: Название книги.
            author: Автор.
            year: Год издания.
        """
        self.title = title
        self.author = author
        self.year = year

    def __str__(self) -> str:
        """Возвращает красивое, читаемое описание объекта.
        
        Возвращает:
            Строка в формате "Название (Автор, Год)".
        """
        return f"Книга: '{self.title}' (Автор: {self.author}, Год: {self.year})"

    def __repr__(self) -> str:
        """Возвращает формальное, восстанавливаемое представление объекта.
        
        Возвращает:
            Строка, пригодная для повторного создания объекта через eval().
        """
        return f"Book(title='{self.title}', author='{self.author}', year={self.year})"


book = Book("Война и мир", "Л. Н. Толстой", 1869)

# Получаем описание объекта
print(f"Пользовательское описание книги: {book}")
print(f"Описание книги для разработчика: {repr(book)}")

# Восстанавливает книгу с помощью функции eval()
restored_book = eval(repr(book))
print(f"Исходная и восстановленная книги совпадают: {str(restored_book) == str(book)}")

Вывод:

Пользовательское описание книги: Книга: 'Война и мир' (Автор: Л. Н. Толстой, Год: 1869)
Описание книги для разработчика: Book(title='Война и мир', author='Л. Н. Толстой', year=1869)
Исходная и восстановленная книги совпадают: True

Пример 2. Атрибуты объекта класса соединения с базой данных

Класс DatabaseConnection описывает соединение с базой данных и инициализируется атрибутами с адресом сервера host, именем пользователя user и статусом соединения is_active. При использовании функции dir() имя пользователя скрывается из списка атрибутов объекта, так как атрибут user не указывается в списке, возвращаемом переопределённым методом __dir__(), но полный словарь со всеми атрибутами их значениями возвращает атрибут __dict__:

class DatabaseConnection:
    """Описывает соединение с базой данных."""
    
    def __init__(self, host: str, user: str, is_active: bool = False):
        """Конструктор класса DatabaseConnection.

        Параметры:
            host: Адрес сервера.
            user: Имя пользователя.
            is_active: Статус соединения.
        """
        self.host = host
        self.user = user
        self.is_active = is_active  
    def __dir__(self):
        """Переопределяет результат функции dir(), возвращая только 
        нужные атрибуты.
        """
        # Возвращаем список строк с именами, которые хотим показать
        return ["host", "is_active"]


conn = DatabaseConnection("localhost", "admin")

# Получаем словарь всех атрибутов, определённых для этого объекта
print(f"Список атрибутов экземпляра (функция dir()): {dir(conn)}")
print(f"Словарь с атрибутами экземпляра (атрибут __dict__): {conn.__dict__}")

Вывод:

Список атрибутов экземпляра (функция dir()): ['host', 'is_active']
Словарь с атрибутами экземпляра (атрибут __dict__): {'host': 'localhost', 'user': 'admin', 'is_active': False}

Пример 3. Квадраты и прямоугольники

В системе решения задач прямоугольник представлен классом Rectangle, от которого наследуется класс для квадрата Square. Поэтому для квадрата функция isinstance() возвращает True как при сравнении с классом Rectangle, так и с классом Square:

class Rectangle:
    """Базовый класс. Описывает прямоугольник."""
    
    def __init__(self, width: int, lenght: int):
        """Конструктор класса Rectangle.
        
        Параметры:
            width: Ширина прямоугольника.
            length: Длина прямоугольника.
        """
        self.width = width
        self.lenght = lenght

    def get_area(self) -> int:
        """Возвращает площадь прямоугольника."""
        return self.width * self.lenght


class Square(Rectangle):
    """Описывает квадрат."""
    
    def __init__(self, side: int):
        """Конструктор класса Square.
        
        Параметры:
            side: Сторона квадрата.
        """
        super().__init__(side, side)


rect = Rectangle(5, 10)
sqr = Square(5)

print(f"Класс прямоугольника: {type(rect)}")
print(f"Класс квадрата: {type(sqr)}")
print(f"Квадрат принадлежит (наследуется) классу Rectangle: {isinstance(sqr, Rectangle)}")
print(f"Квадрат принадлежит классу Square: {isinstance(sqr, Square)}")

Вывод:

Класс прямоугольника: <class '__main__.Rectangle'>
Класс квадрата: <class '__main__.Square'>
Квадрат принадлежит (наследуется) классу Rectangle: True
Квадрат принадлежит классу Square: True

Итоги

  • Метод __str__() предназначен для пользователя, поэтому он должен возвращать читаемую и понятную человеку строку. Этот метод вызывает функция str().
  • Метод __repr__() предназначен для разработчика, поэтому он должен возвращать формальное описание объекта, которое однозначно его идентифицирует и может использоваться для восстановления объекта. Этот метод вызывает функция repr().
  • Функция eval() возвращает результат выполнения переданной ей строки как обычного выражения на Python. Она может восстановить объект из строки, возвращаемой методом __repr__().
  • Функция dir() возвращает список всех атрибутов и методов объекта.
  • Функция type() возвращает класс, к которому принадлежит объект.
  • Функция isinstance() проверяет объект на принадлежность к какому-то классу.
  • Атрибут __dict__ хранит все атрибуты объекта и их значения в виде словаря.

Задания для самопроверки

1. Если класс имеет методы __str__() и __repr__(), то какой из них будет вызван функцией print()?

Будет вызван метод __str__(), так как функция print() вызывает метод __rerp__() только при отсутствии метода __str__().

2. Почему даже для только что созданного класса, в котором не определены никакие методы, функция dir() возвращает длинный список магических методов?

Это происходит потому, что в Python все классы неявно наследуются от базового класса object, который определяет базовую реализацию всех магических методов. 

3. Создайте класс для даты Date с атрибутами day для дня, month для месяца и year для года. Реализуйте методы __str__() и __repr__() для этого класса. Создайте экземпляр этого класса и выведите на экран оба его строковых представлений. Воссоздайте экземпляр этого класса с помощью функции eval() и выведите его на экран.

class Date:
    def __init__(self, day: int, month: int, year: int):
        self.day = day
        self.month = month
        self.year = year

    def __str__(self) -> str:
        return f"{self.day:02d}.{self.month:02d}.{self.year}"

    def __repr__(self) -> str:
        return f"Date(day={self.day}, month={self.month}, year={self.year})"


today = Date(day=18, month=12, year=2025)

# Функция print() вызывает метод __str__()
print(today) 
# Вывод: 18.12.2025

# Функция repr() вызывает метод __repr__()
repr_string = repr(today)
print(repr_string) 
# Вывод: Date(day=18, month=12, year=2025)

4. Создайте класс для секретного предмета SecretItem с публичным атрибутом name для имени и приватным атрибутом __code для кода. Переопределите метод __dir__() так, чтобы он возвращал список, содержащий только публичный атрибут name. Создайте объект класса SecretItem и выведите на экран вызов функции dir() для этого объекта.

class SecretItem:
    def __init__(self, name: str, code: str):
        self.name = name
        self.__code = code

    def __dir__(self) -> list[str]:
        return ["name"] 


item = SecretItem("Чертеж", "X-777")
print(dir(item))
# Вывод: ['name']

5. Создайте родительский класс для мебели Furniture и его дочерний класс для стульев Chair. Придумайте минимум один публичный атрибут для каждого из классов. Сделайте так, чтобы класс Chair при инициализации наследовал все атрибуты класса Furniture. Создайте экземпляр класса Chair и проверьте, принадлежит ли он классу Furniture.

class Furniture:
    def __init__(self, color: str):
        self.color = color  # Цвет


class Chair(Furniture):
    def __init__(self, color: str, legs: int = 4):
        super().__init__(color)
        self.legs = legs  # Количество ножек


my_chair = Chair("Синий", 3)
is_furniture = isinstance(my_chair, Furniture)
print(f"Объект 'my_chair' - экземпляр класса Furniture: {is_furniture}")
# Вывод: Объект 'my_chair' - экземпляр класса Furniture: True