Python class

Основы

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

Процедурно-ориентированный стиль

Процедурно-ориентированная парадигма – это такой подход к программированию, когда код строится на основе функций в математическом смысле этого слова.

Объектно-ориентированный стиль

ООП в Python 3 подразумевает построение кода, оперирующего такими понятиями как python классы и объекты, при этом сама программа создается как некоторая совокупность объектов, которые взаимодействую друг с другом. Объектно-ориентированное программирование ещё долгое время будет являться передовой, если даже не основной, парадигмой программирования. Прямая связь ООП с реальным миром позволяет глубже понимать устройство и принципы работы, как самого языка, так и написания кода в целом, а так же облегчает проектирование внутренней архитектуры.

В данной парадигме есть ряд принципов, которые необходимо знать и понимать.

Абстракция

Абстракция — это модель, в которой уделено внимание основным характеристикам, а остальные опущены. Это упрощение, которое помогает нашему восприятию, один из способов бороться со сложностью.

Инкапсуляция

Согласно этому принципу вся реализация скрывается внутри абстракции, а внешние пользователи взаимодействуют только с интерфейсом. Это даёт разработчику свободу менять различные детали реализации, не боясь, что у кого-то из пользователей что-то сломается. С другой стороны, всё, что входит в интерфейс (публичные атрибуты) должно быть сохранено в неприкосновенности – каждый пользователь должен быть уверен, что интерфейс не изменится со временем.

Наследование

Наследование – возможность создать новый класс, базирующийся на уже созданном другом классе. В таком случае класс–потомок получает все методы и атрибуты от класса-родителя. Далее, как правило, классу-потомку добавляют новые детали.

Полиморфизм

Полиморфизм – это подход, в котором у класса-наследника можно перегрузить методы и атрибуты класса-родителя. При этом можно оперировать всеми потомками класса, точно зная, что они имеют его атрибуты, пусть и видоизменённые.

Создание класса в Python

Классы создаются следующим образом:

То есть указывается:

— ключевое слово class

— имя класса

— в круглых скобках имя класса-родителя (если родителя нет, скобки не пишутся)

— непосредственно тело класса

Атрибут:

Атрибут — это переменная, принадлежащая классу. Другое название – поле класса.

Статические и динамические атрибуты класса

Атрибуты бывают динамическим и статическим. Статический атрибут относится к самому классу. Динамический принадлежит объектам класса. Пример статического атрибута:

class Car:
    number_of_wheels = 4


print('number_of_wheels:', Car.number_of_wheels)
# Вывод:

number_of_wheels: 4

Здесь мы получаем доступ к атрибуту number_of_wheels, обращаясь к классу напрямую.

Динамический атрибут:

class Car:

    def __init__(self, color='Red', speed=250):
        self.color = color
        self.speed = speed


lada = Car()
print(lada.color)
# Вывод:
Red

Здесь мы сперва создаём объект lada класса Car , а затем получаем доступ к атрибуту color, обращаясь к объекту класса.

При этом, можно получить доступ к статическому атрибуту через объект, но к динамическому через класс нельзя:

class Car:
    number_of_wheels = 4

    def __init__(self, color='Red', speed=250):
        self.color = color
        self.speed = speed


lada = Car()
print(lada.number_of_wheels)
print(Car.color)
# Вывод:
4

Traceback (most recent call last):

File "C:\Users\Dushenko\AppData\Roaming\JetBrains\PyCharm2021.3\scratches\scratch.py", line 11, in <module>

    print(Car.color)

AttributeError: type object 'Car' has no attribute 'color'

 

Process finished with exit code 1

Существует особенность работы со статическими атрибутами, про которую не стоит забывать: если изменить данное значение у объекта, то оно изменится только для данного объекта. Если изменить значение статического атрибута у класса – оно изменится для всех объектов. Если значение атрибута было изменено, оно становится невосприимчивым к изменениям атрибута у класса. Посмотрите на пример:


class Car:
    number_of_wheels = 4

    def __init__(self, color='Red', speed=250):
        self.color = color
        self.speed = speed


lada = Car()
kia = Car()
lada.number_of_wheels = 3
print('lada:', lada.number_of_wheels)
print('kia:', kia.number_of_wheels)
Car.number_of_wheels = 5
print('Car.number_of_wheels = 5')
print('lada:', lada.number_of_wheels)
print('kia:', kia.number_of_wheels)
# Вывод:
lada: 3

kia: 4

Car.number_of_wheels = 5

lada: 3

kia: 5

Сперва мы создали два объекта класса Car: lada и kia. Затем изменили количество колёс у объекта lada. Если посмотреть на первые две строки вывода, становится ясно, что данный атрибут изменился у объекта lada, но остался прежним у объекта kia. Далее меняем количество колёс для всего класса Car. Обратимся к последним двум строкам вывода. У объекта kia значение атрибута изменилось на то, которое теперь установлено у класса, однако, у объекта lada значение по-прежнему 3.

Ещё одним интересным моментом является то, что атрибуты можно создавать уже после создания объекта, обращаясь к ним «через точку»:

class Car:

    def __init__(self):
        self.direction = 'не определено'

    def drive(self):
        return f'Еду. Направление - {self.direction}'


lada = Car()
lada.drive = 'Восток'
lada.цвет = 'Розовенький'
print('Направление:', lada.drive)
print('Цвет:', lada.цвет)

# Вывод:
Направление: Восток

Цвет: Розовенький

Здесь мы добавляем новый атрибут «цвет» уже после создания объекта lada.

Кроме того, тем же способом в атрибут можно передать ссылку на функцию и использовать его как метод (о них мы сейчас поговорим):

def example():
    print('Йа пример')

class Car:

    def __init__(self):
        self.direction = 'не определено'

    def drive(self):
        return f'Еду. Направление - {self.direction}'


lada = Car()
lada.example = example
lada.example()

# Вывод:
Йа пример

В данном случае мы заранее определили функцию example(), потом добавили ссылку на неё в атрибут example уже созданного объекта lada и вызвали его. Но! Всё это чёрная магия и делать так не стоит.

Метод:

Метод – это функция, принадлежащая классу. Вызывается через обращение к объекту класса. Если опуститься к низкоуровневым подробностям, то метод – это атрибут, который можно вызвать (то есть имеет свой атрибут __call__). Первым аргументом всегда передаётся ссылка на объект, которую принято называть в Python self. Рассмотрим пример:

class Car:

    def drive(self, direction):
        return f'Еду. Направление - {direction}'

lada = Car()
print(lada.drive('Москва'))
# Вывод:
Еду. Направление – Москва

Здесь мы определили метод drive, имеющий два параметра: ссылку на объект (self ) и направление движения (direction). Запомните, когда мы вызываем метод у объекта, ссылка на объект, для которого вызывается метод, автоматически передаётся в аргументы.

Кроме обычных методов бывают ещё статические и методы класса. Они объявляются в теле класса при помощи декораторов @staticmethod и @classmethod соответственно.

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

class Car:
    @staticmethod
    def drive(direction):
        return f'Еду. Направление - {direction}'

lada = Car()
print(lada.drive('Москва'))
print(Car.drive('Юг'))
# Вывод:
Еду. Направление - Москва

Еду. Направление – Юг

Метод класса получает первым аргументом ссылку на класс, которую принято называть cls. Такие методы относятся к классам, а значит, могут оперировать статическими атрибутами, но не имеют доступа к объектам. Однако, вызывать их можно и через обращение к объектам. Пример:

class Car:
    direction = 'Москва'
    @classmethod
    def drive(cls):
        return f'Еду. Направление - {cls.direction}'

lada = Car()
print(lada.drive())
print(Car.drive())
# Вывод:
Еду. Направление - Москва

Еду. Направление – Москва

Обращение к статическому атрибуту внутри метода класса осуществляется через обращение к ссылке на класс cls. Если попытаться обратиться к динамическому атрибуту, получим исключение:

class Car:

    @classmethod
    def drive(cls):
        return f'Еду. Направление - {self.direction}'

    def __init__(self, direction):
        self.direction = direction

lada = Car('Москва')
print(lada.drive())
print(Car.drive())
# Вывод:
Traceback (most recent call last):

File "C:\Users\Dushenko\AppData\Roaming\JetBrains\PyCharm2021.3\scratches\scratch.py", line 11, in <module>

    print(lada.drive())

File "C:\Users\Dushenko\AppData\Roaming\JetBrains\PyCharm2021.3\scratches\scratch.py", line 5, in drive

return f'Еду. Направление - {self.direction}'

NameError: name 'self' is not defined

 

Process finished with exit code 1

Инициализатор:

Инициализатор – метод объекта, который вызывается сразу после его создания. В Пайтоне он представлен дандер методом __init__. Этот дандер метод Вы будете использовать и встречать в чужом коде чаще всего в объектно-ориентированном программировании. Как и у всех методов экземпляра класса, первым аргументом передаётся self. Аргументы, которые перечислены в инициализаторе, должны быть переданы при создании объекта в круглых скобках:

class Car:

    def __init__(self, direction):
        print('Направление:', direction)

lada = Car('Москва')
# Вывод:
Направление: Москва

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

class Car:

    def __init__(self, direction):
        pass
lada = Car('Москва')
print(lada.direction)
# Вывод:
Traceback (most recent call last):

File "C:\Users\Dushenko\AppData\Roaming\JetBrains\PyCharm2021.3\scratches\scratch.py", line 6, in <module>

    print(lada.direction)

AttributeError: 'Car' object has no attribute 'direction'

 

Process finished with exit code 1

Чтобы это стало возможным необходимо самостоятельно создать атрибут объекта (при помощи self) и присвоить ему значение:

class Car:

    def __init__(self, direction):
        self.direction = direction
lada = Car('Москва')
print(lada.direction)
# Вывод:
Москва

Стоит помнить, что __init__() не должен возвращать никаких значений, иначе:

class Car:

    def __init__(self, direction):
        self.direction = direction
        return 1
    
lada = Car('Москва')
print(lada.direction)
# Вывод:
Traceback (most recent call last):

File "C:\Users\Dushenko\AppData\Roaming\JetBrains\PyCharm2021.3\scratches\scratch.py", line 7, in <module>

    lada = Car('Москва')

TypeError: __init__() should return None, not 'int'

 

Process finished with exit code 1

В некоторых языках программирования инициализаторов может быть несколько. В Python у класса __init__() указывается только один раз. Аргументам, указанным в __init__() можно задать значения по умолчанию, как и у любой функции.

Кроме инициализатора __init__() есть ещё и конструктор класса – метод класса __new__(). Он вызывается перед созданием объекта. Но его используют очень редко. Выглядит это так:

class Car:
    def __new__(cls, *args):
        print('Сейчас создам объект')
        return super(Car, cls).__new__(cls)

    def __init__(self, direction):
        self.direction = direction

lada = Car('Москва')
print(lada.direction)
# Вывод:
Сейчас создам объект

Москва

Что такое self?

Вы уже видели этот аргумент в примерах выше. self – это ссылка на текущий экземпляр класса (объект). Если Вы знакомы с другими языками программирования, активно использующими объектно-ориентированный стиль, то можете узнать в self аналог this. При помощи этой ссылки Вы можете обратится к атрибутам и методам объекта внутри класса, то есть в момент, когда объект ещё не существует. Стоит отметить, что self, как и cls (такая же ссылка, но не на объект, а на класс) – не ключевые слова языка Питон, а всеобщая договорённость. Другими словами, вместо self и cls можно использовать любое другое слово, но это не приветствуется – Вы ведь хотите, чтоб Вас понимали другие? Пример:

class Car:

    def __init__(ссылка_на_объект, direction):
        ссылка_на_объект.direction = direction

lada = Car('Москва')
print(lada.direction)
# Вывод:
Москва

В этом примере я заменил self на «ссылка_на_объект» и всё продолжает работать штатно.

Поскольку это просто переменная, хранящая ссылку, её можно возвращать из метода, менять, присваивать атрибутам и тому подобное. На таких «фокусах», к примеру, построен паттерн «builder». Немного трюков:

class A:

    def give_self(self):
        return self

class B(A):
    pass

class C(A):

    def __init__(self):
        self.i = self

    def say(self):
        print('Hello from C!')

a = A()
b = B()
c = C()

print('A:', a)
print('B:', b)
print('C:', c)

b = a.give_self()
print('B:', b)
print('A == B:', a == b)
print('A is B:', a is b)
c.i.i.i.i.i.i.i.i.say()
a = c.give_self()
print('a.i.i.i.i.i.i.i.i.say():')
a.i.i.i.i.i.i.i.i.say()
# Вывод:
A: <__main__.A object at 0x000002681DE91FD0>

B: <__main__.B object at 0x000002681DE91FA0>

C: <__main__.C object at 0x000002681DE91F70>

B: <__main__.A object at 0x000002681DE91FD0>

A == B: True

A is B: True

Hello from C!

a.i.i.i.i.i.i.i.i.say():

Hello from C!

Уровни доступа атрибута и метода

В отличие от многих других строгих языков программирования, в Python инкапсуляция и уровни доступа реализованы на уровне договорённостей. Это означает, что программист лишь устанавливает маркер в имени атрибута в виде одного или двух нижних подчёркиваний в начале имени и он означает, что это приватный атрибут. Использовать его извне не стоит, но такая возможность остаётся. Это часть философии Пайтона – мы сами несём ответственность за свои действия. Подробнее об уровнях доступа можно прочитать в нашем уроке про PEP8.

Свойства

Свойство – метод, который «снаружи» выглядит как атрибут. Вот так:

class Car:

    @property
    def drive(self, direction='Север'):
        return f'Еду. Направление - {direction}'

lada = Car()
print(lada.drive)

# Вывод:
Еду. Направление – Север

В этом листинге мы использовали декоратор @property. Видите, обращение к методу drive теперь выполнено без круглых скобок в конце? Да, метод волшебным образом превратился в атрибут. Однако, изменить его значение не получится:

class Car:

    @property
    def drive(self, direction='Север'):
        return f'Еду. Направление - {direction}'

lada = Car()
lada.drive = 'Восток'
print(lada.drive)

# Вывод:
Traceback (most recent call last):

File "C:\Users\Dushenko\AppData\Roaming\JetBrains\PyCharm2021.3\scratches\scratch.py", line 8, in <module>

    lada.drive = 'Восток'

AttributeError: can't set attribute

 

Process finished with exit code 1

Смысл свойств в том, что мы можем создать геттеры и сеттеры для таких методов.

— Геттер – функция, которая вызывается при попытке получить значение свойства

— Сеттер – функция, которая вызывается при попытке изменить значение свойства

Пример:

class Car:

    def set_direction(self, direction):
        if type(direction) != str:
            print(type(direction), 'is not string')
            self.direction = 'не определено'
            return
        self.direction = direction

    def get_drive(self):
        return f'Еду. Направление - {self.direction}'

    drive = property(get_drive, set_direction)

lada = Car()
lada.drive = 'Восток'
print(lada.drive)

# Вывод:
Еду. Направление – Восток

Здесь мы определили сеттер set_direction() и геттер get_drive(), а после этого создали свойство drive при помощи функции property. Сеттеры чаще всего используются для валидации вводимых аргументов, как и в этом примере. Теперь, если свойству попытаться установить значение не подходящего типа, сеттер не даст это сделать:

class Car:

    def set_direction(self, direction):
        if type(direction) != str:
            print(type(direction), 'is not string')
            self.direction = 'не определено'
            return
        self.direction = direction

    def get_drive(self):
        return f'Еду. Направление - {self.direction}'

    drive = property(get_drive, set_direction)

lada = Car()
lada.drive = 1
print(lada.drive)

# Вывод:
<class 'int'> is not string

Еду. Направление - не определено

Для удобства можно использовать не функцию property(), а декоратор @property:

class Car:

    def __init__(self):
        self.direction = 'не определено'

    @property
    def drive(self):
        return f'Еду. Направление - {self.direction}'

    @drive.setter
    def drive(self, direction):
        if type(direction) != str:
            print(type(direction), 'is not string')
            return

        self.direction = direction

    @drive.deleter
    def drive(self):
        print('Пока!')


lada = Car()
lada.drive = 'Восток'
print(lada.drive)
del lada.drive

# Вывод:
Еду. Направление - Восток

Пока!

Декорируемый @property метод drive становится геттером (именно поэтому в первом примере мы не смогли изменить значение – был определён только геттер). Какой метод считать сеттером обозначаем декоратором @drive.setter. @drive.deleter отмечает метод, который будет вызван при удалении.

Геттеры используют для какой-то обработки непосредственно перед тем, как вернуть значение свойства. К примеру, это может быть логирование или добавление текущего времени в возвращаемое значение.

Применение свойств повышает надёжность Вашего кода и считается хорошим тоном. Есть и другая сторона – они увеличивают размер кода. Использовать ли этот инструмент Вам придётся решать самостоятельно исходя из поставленных задач.

Сравнение объектов

Оператор is нужен, чтобы узнать, ссылаются ли два объекта на одно и то же место в памяти. Он вернет True, если это так. Оператор is not вернет True, если сравнить 2 объекта, которые ссылаются на разные места в памяти.

class Car:

    pass

lada = Car()
kia = Car()
print('lada is kia:', lada is kia)
print('lada is not kia:', lada is not kia)
zhigul = lada
print('lada is zhigul:', lada is zhigul)
print('lada is not zhigul:', lada is not zhigul)

# Вывод:
lada is kia: False

lada is not kia: True

lada is zhigul: True

lada is not zhigul: False

Атрибуты функции

Обычно получать доступ к атрибутам объекта можно с помощью оператора «точка» (например, ‘Строка’.__doc__). Но Python предоставляет возможность делать это и с помощью встроенных функций:

— getattr() — Возвращает значение атрибута или значение по умолчанию, если первое не было указано

— hasattr() – проверяет, есть ли у объекта аргумент, переданный в функцию вторым аргументом

— setattr – устанавливает значение атрибута или, если такого атрибута нет, создаёт его.

— delattr – удаляет указанный атрибут

class Car:
    pass

lada = Car()
setattr(lada, 'цвет', 'Розовенький')
print('hasattr:', hasattr(lada, 'цвет'))
print('getattr:', getattr(lada, 'цвет'))
delattr(lada, 'цвет')
print('hasattr:', hasattr(lada, 'цвет'))

# Вывод:
hasattr: True

getattr: Розовенький

hasattr: False

Встроенные атрибуты класса

Python classes содержат встроенные атрибуты, которые хранят некоторую полезную информацию.

  • __dict__ — словарь, содержащий пространство имен класса.

class Car:
    pass

lada = Car()
print('__dict__:', Car.__dict__)

# Вывод:
__dict__: {'__module__': '__main__', '__dict__': <attribute '__dict__' of 'Car' objects>, '__weakref__': <attribute '__weakref__' of 'Car' objects>, '__doc__': None}

  • __doc__ — строка документации класса. None если, документация отсутствует.

class Car:
    '''Это строка документации'''
    pass

lada = Car()
print('__doc__:', Car.__doc__)

# Вывод:
__doc__: Это строка документации

  • __name__ — имя класса.

class Car:
    pass

lada = Car()
print('__name__:', Car.__name__)

# Вывод:
__name__: Car

  • __module__ — имя модуля, в котором определяется класс.

class Car:
    pass

lada = Car()
print('__module__:', Car.__module__)

# Вывод:
__module__: __main__

  • __bases__ — кортеж, содержащий базовые классы, в порядке их появления. Кортеж будет пустым, если наследование не было.

class A:
    pass

class B(A):
    pass

class C(B, A):
    pass

print('__bases__:', C.__bases__)

# Вывод:
__bases__: (<class '__main__.B'>, <class '__main__.A'>)

  • __mro__ — Порядок разрешения методов в множественном наследовании.

class A:
    pass

class B(A):
    pass

class C(B, A):
    pass

print('__mro__:', C.__mro__)

# Вывод:
__mro__: (<class '__main__.C'>, <class '__main__.B'>, <class '__main__.A'>, <class 'object'>)

Составляющие класса или объекта

В Python присутствует функция dir, которая выводит список всех методов, атрибутов и переменных класса или объекта.

class C():
    atr_1 = 1

    def __init__(self, atr_2=2):
        self.atr_2 = atr_2

c = C()
setattr(c, 'atr_3', 3)
print('dir(C):', dir(C))
print('dir(c):', dir(c))

# Вывод:
dir(C): ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'atr_1']

dir(c): ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'atr_1', 'atr_2', 'atr_3']

Наследование

Мы уже говорили о наследовании, как о принципе объектно-ориентированного подхода к программированию в начале статьи. Напомню, что наследование – это когда мы создаём новый класс на основе другого. Такой класс-потомок получает (наследует) от класса-родителя его методы и атрибуты. В момент создания некоторые атрибуты и методы изменяют (расширяют). Так же классу-потомку можно добавить новые атрибуты и методы. Давайте посмотрим, как это делается на практике:

class A():

    def __init__(self, atr='Атрибут класса A'):
        self.atr = atr

    def method_A(self):
        return 'Метод класса A'

class B(A):

    def method_B(self):
        return 'Метод класса B'


b = B()
print('b.atr:', b.atr)
print('b.method_A():', b.method_A())
print('b.method_B():', b.method_B())

# Вывод:
b.atr: Атрибут класса A

b.method_A(): Метод класса A

b.method_B(): Метод класса B

В этом примере мы создали класс А. Этот класс будет родителем. Затем создаём класс В, унаследованный от класса А. Для этого при объявлении класса В указываем «А» в скобках. Теперь объекты класса В имеют, как атрибуты и методы родительского класса (atr, method_A), так и свои собственные (method_B), что явно следует из вывода программы.

У класса может быть несколько классов-родителей. Такое наследование называется множественным:

class A():

    def method_A(self):
        return 'Метод класса A'

class B():

    def method_B(self):
        return 'Метод класса B'


class C(A, B):

    def method_C(self):
        return 'Метод класса C'

c = C()
print('c.method_A():', c.method_A())
print('c.method_B():', c.method_B())
print('c.method_C():', c.method_C())

# Вывод:
c.method_A(): Метод класса A

c.method_B(): Метод класса B

c.method_C(): Метод класса C

Как Вы можете видеть, при множественном наследовании класс-потомок (класс С) получает атрибуты и методы всех своих родителей (method_A() от класса А и method_В() от класса В).

Проверить что один класс является потомком другого можно при помощи функции issubclass(). Все классы в Python являются наследниками от класса object. Давайте в этом убедимся:

class A():
    pass

print('isinstance(A, object):', isinstance(A, object))

# Вывод:
isinstance(A, object): True

Первый аргумент – имя класса, который проверяем, второй – предполагаемый класс-родитель.

super()

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

class A():
    def method_A(self):
        print('method А')

class B(A):
    def method_B(self):
        print('method В')
        super().method_A()

b = B()
b.method_B()

# Вывод:
method В

method А

Здесь мы вызываем родительский метод method_A() внутри собственного метода method_B() класса В. Это удобно тем, что мы не заботимся об имени родителя. super() означает «родитель, как бы он не назывался».

Переопределение

Переопределение методов и атрибутов (его ещё называют перегрузкой) – практическая реализация принципа полиморфизма. Чтобы это сделать, достаточно объявить в классе-наследнике метод с тем же названием, что и в базовом классе:

class A():
    def method_A(self):
        print('method А')

class B(A):
    def method_A(self):
        print('method В')

b = B()
b.method_A()

# Вывод:
method В

В этом примере мы в классе В() переопределили унаследованный от класса А метод method_A(). Таким образом, класс наследник может расширять функционал класса родителя.

Для чего это нужно?

— Причина №1. Чтоб не дублировать код. Представьте, что у Вас есть несколько классов, имеющих некоторые одинаковые атрибуты. Следуя принципу «не повторяйся» стоит создать класс-родитель, в нём определить эти общие атрибуты и сделать все схожие классы его потомками:

# Было:
class Овчарка():
    def фас(self):
        print(self.__class__, end=': ')
        print('Р-р-ррр!')

    def служить(self):
        print(self.__class__, end=': ')
        print('Гав!')

class Питбуль():
    def фас(self):
        print(self.__class__, end=': ')
        print('Р-р-ррр!')

овчарка = Овчарка()
питбуль = Питбуль()

овчарка.фас()
питбуль.фас()
овчарка.служить()

# Вывод:
<class '__main__.Овчарка'>: Р-р-ррр!

<class '__main__.Питбуль'>: Р-р-ррр!

<class '__main__.Овчарка'>: Гав!

# Выделяем повторяющийся метод в класс-родитель:

class Собака():
    def фас(self):
        print(self.__class__, end=': ')
        print('Р-р-ррр!')

class Овчарка(Собака):

    def служить(self):
        print(self.__class__, end=': ')
        print('Гав!')

class Питбуль(Собака):
    pass

овчарка = Овчарка()
питбуль = Питбуль()

овчарка.фас()
питбуль.фас()
овчарка.служить()

# Вывод:
<class '__main__.Овчарка'>: Р-р-ррр!

<class '__main__.Питбуль'>: Р-р-ррр!

<class '__main__.Овчарка'>: Гав!

— Причина №2. Для декларирования интерфейса. Часто создают класс, в котором перечислены методы (пустые) и от него наследуются другие классы, в которых переопределяются методы. В месте, где над объектами классов-потомков производятся какие-то манипуляции, сперва проверяют что эти классы действительно являются потомками базового класса, определённого в начале. Такие базовые классы называют абстрактными. Всё это необходимо для того, чтоб быть уверенным, что в объектах реализованы необходимые методы. Простой пример:

class Собака():

    def цвет(self):
        assert False

class Овчарка(Собака):

    def служить(self):
        print(self.__class__, end=': ')
        print('Гав!')

овчарка = Овчарка()
if issubclass(овчарка.__class__, Собака):
    print(овчарка.цвет())
овчарка.служить()

# Вывод:
Traceback (most recent call last):

File "C:\Users\Dushenko\AppData\Roaming\JetBrains\PyCharm2021.3\scratches\scratch.py", line 14, in <module>

    print(овчарка.цвет())

File "C:\Users\Dushenko\AppData\Roaming\JetBrains\PyCharm2021.3\scratches\scratch.py", line 4, in цвет

    assert False

AssertionError

Здесь мы не только создали базовый класс, но и добавили вызов исключения в метод «цвет». Это заставляет все классы-потомки переопределить данный метод:

class Собака():

    def цвет(self):
        assert False

class Овчарка(Собака):

    def служить(self):
        print(self.__class__, end=': ')
        print('Гав!')

    @property
    def цвет(self):
        self._цвет = 'Странный'
        return self._цвет

овчарка = Овчарка()
if issubclass(овчарка.__class__, Собака):
    print('цвет:', овчарка.цвет)
овчарка.служить()

# Вывод:
цвет: Странный

<class '__main__.Овчарка'>: Гав!

Обратите внимание на условие if issubclass(). Оно позволяет удостоверится что объект принадлежит к классу, унаследованному от класса Собака, а значит содержит метод «цвет». Конечно, всегда остаются возможности выстрелить себе в ногу:

class Собака():

    def цвет(self):
        assert False

class Овчарка(Собака):

    def служить(self):
        print(self.__class__, end=': ')
        print('Гав!')


овчарка = Овчарка()

del овчарка.цвет

if issubclass(овчарка.__class__, Собака):
    print('цвет:', овчарка.цвет)
овчарка.служить()

# Вывод:
Traceback (most recent call last):

File "C:\Users\Dushenko\AppData\Roaming\JetBrains\PyCharm2021.3\scratches\scratch.py", line 14, in <module>

    del овчарка.цвет

AttributeError: цвет

 

Process finished with exit code 1

В основном, все эти заботы с абстрактными классами нужны когда над кодом работает более одного разработчика. Если проект небольшой, использовать их не стоит – не усложняйте код без веских причин.

Документирование классов

В Python есть замечательный инструмент под названием строки документации или docstrings. Это инструмент, позволяющий создавать многострочное описание функций и классов, которое автоматически сохраняется как атрибут объекта. Для их создания необходимо указать описание в тройных одинарных кавычках в самом начале тела класса или функции. Доступ к данной документации можно получить через дандер атрибут __doc__ или функцию help():

class Собака():
    '''Друг человека'''
    pass


мопс = Собака()
print('__doc__:', мопс.__doc__)
print('\n', 'help:')
help(мопс)
# Вывод:
__doc__: Друг человека

 

help:

Help on Собака in module __main__ object:

 

class Собака(builtins.object)

|  Друг человека

|

|  Data descriptors defined here:

|

|  __dict__

|      dictionary for instance variables (if defined)

|

|  __weakref__

|      list of weak references to the object (if defined)

Подробнее об этом Вы можете узнать в нашем уроке Комментарии в Python.

Удаление объектов (сбор мусора)

В Python можно удалить ссылку на любой объект. Для этого используется ключевое слово del. При этом у объекта счётчик ссылок уменьшится на 1. Когда счётчик ссылок достигнет нуля, внутренний механизм языка под названием сборщик мусора (Garbage Collector) удалит объект из памяти.

За то, как будет произведено удаление ссылки, отвечает дандер метод __del__() (называется деструктор) и его можно переопределить:

class Собака():

    def __del__(self):
        print(f'Удаляю {self.__class__.__name__}')


мопс = Собака()
второй_мопс = мопс
del мопс
print(второй_мопс)
print(мопс)
# Вывод:
Удаляю Собака

<__main__.Собака object at 0x000002496713A9D0>

 

Traceback (most recent call last):

File "C:\Users\Dushenko\AppData\Roaming\JetBrains\PyCharm2021.3\scratches\scratch.py", line 11, in <module>

print(мопс)

NameError: name 'мопс' is not defined

 

Process finished with exit code 1

Здесь мы добавили в деструктор вывод в консоль об удалении объекта. Обратите внимание на вывод. После удаления переменная «мопс» с ссылкой на объект класса «Собака» не существует. Об этом говорит исключение «NameError». Однако, ссылка на тот же объект в переменной «второй_мопс» всё ещё жива, так что объект всё ещё в памяти.

Оцените статью
О Python на русском языке
Добавить комментарий

Adblock
detector