Python как составить класс

Python — это процедурно-ориентированный и одновременно объектно-ориентированный язык программирования.

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

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

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

«Объектно-ориентированный» подразумевает наличие классов. Есть возможность создавать классы, представляющие собой прототипы для будущих объектов.

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

Синтаксис для написания нового класса:

class ClassName:
    'Краткое описание класса (необязательно)'
    # Код ...
  • Для создания класса пишется ключевое слово class, его имя и двоеточие (:). Первая строчка в теле класса описывает его. (По желанию) получить доступ к этой строке можно с помощью ClassName.__doc__
  • В теле класса допускается объявление атрибутов, методов и конструктора.

Атрибут:

Атрибут — это элемент класса. Например, у прямоугольника таких 2: ширина (width) и высота (height).

Метод:

  • Метод класса напоминает классическую функцию, но на самом деле — это функция класса. Для использования ее необходимо вызывать через объект.
  • Первый параметр метода всегда self (ключевое слово, которое ссылается на сам класс).

Конструктор:

  • Конструктор — уникальный метод класса, который называется __init__.
  • Первый параметр конструктора во всех случаях self (ключевое слово, которое ссылается на сам класс).
  • Конструктор нужен для создания объекта.
  • Конструктор передает значения аргументов свойствам создаваемого объекта.
  • В одном классе всегда только один конструктор.
  • Если класс определяется не конструктором, Python предположит, что он наследует конструктор родительского класса.
# Прямоугольник.
class Rectangle :
    'Это класс Rectangle'
    # Способ создания объекта (конструктор)
    def __init__(self, width, height):         
        self.width= width
        self.height = height

    def getWidth(self):        
        return self.width
     
    def getHeight(self):        
        return self.height
 
    # Метод расчета площади.
    def getArea(self):
        return self.width * self.height

Создание объекта с помощью класса Rectangle:

Создание объекта с помощью класса Rectangle

# Создаем 2 объекта: r1 & r2
r1 = Rectangle(10,5)
r2 = Rectangle(20,11)
 
print("r1.width = ", r1.width)
print("r1.height = ", r1.height)
print("r1.getWidth() = ", r1.getWidth())
print("r1.getArea() = ", r1.getArea())
 
print("-----------------")
 
print("r2.width = ", r2.width)
print("r2.height = ", r2.height)
print("r2.getWidth() = ", r2.getWidth())
print("r2.getArea() = ", r2.getArea())

Расчет площади класса Rectangle

Что происходит при создании объекта с помощью класса?

При создании объекта класса Rectangle запускается конструктор выбранного класса, и атрибутам нового объекта передаются значения аргументов. Как на этом изображении:

Конструктор выбранного класса

Конструктор с аргументами по умолчанию

В других языках программирования конструкторов может быть несколько. В Python — только один. Но этот язык разрешает задавать значение по умолчанию.

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

class Person:
    # Параметры возраста и пола имеют значение по умолчанию.
    def __init__(self, name, age=1, gender="Male"):
        self.name = name
        self.age = age 
        self.gender= gender
         
    def showInfo(self):
        print("Name: ", self.name)
        print("Age: ", self.age)
        print("Gender: ", self.gender)

Например:

from person import Person
 
# Создать объект Person.
aimee = Person("Aimee", 21, "Female")
aimee.showInfo()
print(" --------------- ")
 
# возраст по умолчанию, пол.
alice = Person( "Alice" )
alice.showInfo()
 
print(" --------------- ")
 
# Пол по умолчанию.
tran = Person("Tran", 37)
tran.showInfo()

Конструктор с аргументами по умолчанию

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

В Python объект, созданный с помощью конструктора, занимает реальное место в памяти. Это значит, что у него есть точный адрес.

Если объект AA — это просто ссылка на объект BB, то он не будет сущностью, занимающей отдельную ячейку памяти. Вместо этого он лишь ссылается на местоположение BB.

Как происходит сравнение объектов

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

from rectangle import Rectangle


r1 = Rectangle(20, 10)
r2 = Rectangle(20 , 10)
r3 = r1
 
# Сравните r1 и r2
test1 = r1 == r2 # --> False
# Сравните r1 и r3
test2 = r1 == r3 # --> True
 
print ("r1 == r2 ? ", test1) 
print ("r1 == r3 ? ", test2)

print (" -------------- ")
 
print ("r1 != r2 ? ", r1 != r2)
print ("r1 != r3 ? ", r1 != r3)

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

Атрибуты

В Python есть два похожих понятия, которые на самом деле отличаются:

  1. Атрибуты
  2. Переменные класса

Стоит разобрать на практике:

class Player:
    # Переменная класса
    minAge  = 18
    maxAge = 50
     
    def __init__(self, name, age):
        self.name = name
        self.age = age

Атрибут

Объекты, созданные одним и тем же классом, будут занимать разные места в памяти, а их атрибуты с «одинаковыми именами» — ссылаться на разные адреса. Например:

Объекты одного класса занимают разные места в памяти

from player import Player 
 
 
player1 = Player("Tom", 20)
 
player2 = Player("Jerry", 20)
 
print("player1.name = ", player1.name)
print("player1.age = ", player1.age)
 
print("player2.name = ", player2.name)
print("player2.age = ", player2.age)
 
print(" ------------ ")
 
print("Assign new value to player1.age = 21 ")
 
# Присвойте новое значение атрибуту возраста player1.
player1.age = 21
 
print("player1.name = ", player1.name)
print("player1.age = ", player1.age)
 
print("player2.name = ", player2.name)
print("player2.age = ", player2.age)

Изменение значений атрибутов

Python умеет создавать новые атрибуты для уже существующих объектов. Например, объект player1 и новый атрибут address.

from player import Player 
 
 
player1 = Player("Tom", 20)
player2 = Player("Jerry", 20)
 
# Создайте новый атрибут с именем «address» для player1.
player1.address = "USA"
 
print("player1.name = ", player1.name)
print("player1.age = ", player1.age)
print("player1.address = ", player1.address)
 
print(" ------------------- ")
 
print("player2.name = ", player2.name)
print("player2.age = ", player2.age)
 
# player2 е имеет атрибута 'address' (Error!!)
print("player2.address = ", player2.address)

Вывод:

player1.name =  Tom
player1.age =  20
player1.address =  USA
 ------------------- 
player2.name =  Jerry
player2.age =  20
Traceback (most recent call last):
  File "C:/Users/gvido/class.py", line 27, in 
    print("player2.address = ", player2.address)
AttributeError: 'Player' object has no attribute 'address'

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

Обычно получать доступ к атрибутам объекта можно с помощью оператора «точка» (например, player1.name). Но Python умеет делать это и с помощью функции.

Функция Описание
getattr (obj, name[,default]) Возвращает значение атрибута или значение по умолчанию, если первое не было указано
hasattr (obj, name) Проверяет атрибут объекта — был ли он передан аргументом «name»
setattr (obj, name, value) Задает значение атрибута. Если атрибута не существует, создает его
delattr (obj, name) Удаляет атрибут
from player import Player 
 
 
player1 = Player("Tom", 20)
 
# getattr(obj, name[, default])
print("getattr(player1,'name') = " , getattr(player1,"name"))

print("setattr(player1,'age', 21): ")
# setattr(obj,name,value) 
setattr(player1,"age", 21)
print("player1.age = ", player1.age)
 
# Проверка, что player1 имеет атрибут 'address'?
hasAddress =  hasattr(player1, "address")
print("hasattr(player1, 'address') ? ", hasAddress)
 
# Создать атрибут 'address' для объекта 'player1'
print("Create attribute 'address' for object 'player1'")
setattr(player1, 'address', "USA")
print("player1.address = ", player1.address)
 
# Удалить атрибут 'address'.
delattr(player1, "address")

Вывод:

getattr(player1,'name') =  Tom
setattr(player1,'age', 21): 
player1.age =  21
hasattr(player1, 'address') ?  False
Create attribute 'address' for object 'player1'
player1.address =  USA

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

Объекты класса — дочерние элементы по отношению к атрибутам самого языка Python. Таким образом они заимствуют некоторые атрибуты:

Атрибут Описание
__dict__ Предоставляет данные о классе коротко и доступно, в виде словаря
__doc__ Возвращает строку с описанием класса, или None, если значение не определено
__class__ Возвращает объект, содержащий информацию о классе с массой полезных атрибутов, включая атрибут __name__
__module__ Возвращает имя «модуля» класса или __main__, если класс определен в выполняемом модуле.
class Customer:
    'Это класс Customer'
    def __init__(self, name, phone, address):        
        self.name = name
        self.phone = phone
        self.address = address
 
  
john = Customer("John",1234567, "USA")
 
print ("john.__dict__ = ", john.__dict__)
print ("john.__doc__ = ", john.__doc__)
print ("john.__class__ = ", john.__class__)
print ("john.__class__.__name__ = ", john.__class__.__name__) 
print ("john.__module__ = ", john.__module__)  

Вывод:

john.__dict__ =  {'name': 'John', 'phone': 1234567, 'address': 'USA'}
john.__doc__ =  Это класс Customer
john.__class__ =  
john.__class__.__name__ =  Customer
john.__module__ =  __main__

Переменные класса

Переменные класса в Python — это то же самое, что Field в других языках, таких как Java или С#. Получить к ним доступ можно только с помощью имени класса или объекта.

Для получения доступа к переменной класса лучше все-таки использовать имя класса, а не объект. Это поможет не путать «переменную класса» и атрибуты.

У каждой переменной класса есть свой адрес в памяти. И он доступен всем объектам класса.
Переменные класса

from player import Player 
 
 
player1 = Player("Tom", 20)
player2 = Player("Jerry", 20)
 
# Доступ через имя класса.
print ("Player.minAge = ", Player.minAge)
 
# Доступ через объект.
print("player1.minAge = ", player1.minAge) 
print("player2.minAge = ", player2.minAge)
 
print(" ------------ ") 

print("Assign new value to minAge via class name, and print..")
 
# Новое значение minAge через имя класса
Player.minAge = 19
 
print("Player.minAge = ", Player.minAge) 
print("player1.minAge = ", player1.minAge) 
print("player2.minAge = ", player2.minAge)

Вывод:

Player.minAge =  18
player1.minAge =  18
player2.minAge =  18
 ------------ 
Assign new value to minAge via class name, and print..
Player.minAge =  19
player1.minAge =  19
player2.minAge =  19

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

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

from player import Player
 
 
# Вывести список атрибутов, методов и переменных объекта 'Player'
print(dir(Player))
print("nn")
 
player1 = Player("Tom", 20)
player1.address ="USA"

# Вывести список атрибутов, методов и переменных объекта 'player1'
print(dir(player1))

Вывод:

['__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__', 'maxAge', 'minAge']  

  
['__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__', 'address', 'age', 'maxAge', 
'minAge', 'name']

Прежде чем приступить к теории, давайте решим следующую задачу.

Напишем программу, которая будет моделировать объекты класса «Автомобиль». При моделировании необходимо определить степень детализации объектов, которая зависит от действий, выполняемых этими объектами.

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

Выделим важные для нашей программы свойства объектов класса: цвет, средний расход топлива, объём топливного бака, запас топлива, общий пробег.

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

Попробуем описать объекты этого класса с помощью коллекций и функций:

def create_car(color, consumption, tank_volume, mileage=0):
    return {
        "color": color,
        "consumption": consumption,
        "tank_volume": tank_volume,
        "reserve": tank_volume,
        "mileage": mileage,
        "engine_on": False
    }


def start_engine(car):
    if not car["engine_on"] and car["reserve"] > 0:
        car["engine_on"] = True
        return "Двигатель запущен."
    return "Двигатель уже был запущен."


def stop_engine(car):
    if car["engine_on"]:
        car["engine_on"] = False
        return "Двигатель остановлен."
    return "Двигатель уже был остановлен."


def drive(car, distance):
    if not car["engine_on"]:
        return "Двигатель не запущен."
    if car["reserve"] / car["consumption"] * 100 < distance:
        return "Малый запас топлива."
    car["mileage"] += distance
    car["reserve"] -= distance / 100 * car["consumption"]
    return f"Проехали {distance} км. Остаток топлива: {car['reserve']} л."


def refuel(car):
    car["reserve"] = car["tank_volume"]


def get_mileage(car):
    return f"Пробег {car['mileage']} км."


def get_reserve(car):
    return f"Запас топлива {car['reserve']} л."


car_1 = create_car(color="black", consumption=10, tank_volume=55)

print(start_engine(car_1))
print(drive(car_1, 100))
print(drive(car_1, 100))
print(drive(car_1, 100))
print(drive(car_1, 300))
print(get_mileage(car_1))
print(get_reserve(car_1))
print(stop_engine(car_1))
print(drive(car_1, 100))

Вывод программы:

Двигатель запущен.
Проехали 100 км. Остаток топлива: 45.0 л.
Проехали 100 км. Остаток топлива: 35.0 л.
Проехали 100 км. Остаток топлива: 25.0 л.
Малый запас топлива.
Пробег 300 км.
Запас топлива 25.0 л.
Двигатель остановлен.
Двигатель не запущен.

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

Объектно-ориентированное программирование (ООП) позволяет устранить недостатки процедурного подхода. Язык программирования Python является объектно-ориентированным. Это означает, что каждая сущность (переменная, функция и т. д.) в этом языке является объектом определённого класса. Ранее мы говорили, что, например, целое число является в Python типом данных int. На самом деле есть класс целых чисел int.

Убедимся в этом, написав простую программу:

print(type(1))

Вывод программы:

<class 'int'>

Синтаксис создания класса в Python выглядит следующим образом:

class <ИмяКласса>:
    <описание класса>

Имя класса по стандарту PEP 8 записывается в стиле CapWords (каждое слово с прописной буквы).

Давайте перепишем пример про автомобили с использованием ООП. Создадим класс Car и пока оставим в нём инструкцию-заглушку pass:

class Car:
    pass

В классах описываются свойства объектов и действия объектов или совершаемые над ними действия.

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

<имя_объекта>.<имя_атрибута> = <значение>

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

def <имя_метода>(self, <аргументы>):
    <тело метода>

В методах первым аргументом всегда идёт объект self. Он является объектом, для которого вызван метод. self позволяет использовать внутри описания класса атрибуты объекта в методах и вызывать сами методы.

Во всех классах Python есть специальный метод __init__(), который вызывается при создании объекта. В этом методе происходит инициализация всех атрибутов класса. В методы можно передавать аргументы. Вернёмся к нашему примеру и создадим в классе метод __init__(), который будет при создании автомобиля принимать его свойства как аргументы:

class Car:

    def __init__(self, color, consumption, tank_volume, mileage=0):
        self.color = color
        self.consumption = consumption
        self.tank_volume = tank_volume
        self.reserve = tank_volume
        self.mileage = mileage
        self.engine_on = False

Итак, мы создали класс автомобилей и описали метод __init__() для инициализации его объектов. Для создания объекта класса нужно использовать следующий синтаксис:

<имя_объекта> = <ИмяКласса>(<аргументы метода __init__()>)

Создадим в программе автомобиль класса Car. Для этого добавим следующую строку в основной код программы после описания класса, отделив от класса, согласно PEP 8, двумя пустыми строками:

car_1 = Car(color="black", consumption=10, tank_volume=55)

Обратите внимание: наш код стало легче читать, потому что мы видим, что создаётся объект определённого класса, а не просто вызывается функция, из которой возвращается значение-словарь.

Опишем с помощью методов, какие действия могут совершать объекты класса Car. По PEP 8, между объявлением методов нужно поставить одну пустую строку.

class Car:

    def __init__(self, color, consumption, tank_volume, mileage=0):
        self.color = color
        self.consumption = consumption
        self.tank_volume = tank_volume
        self.reserve = tank_volume
        self.mileage = mileage
        self.engine_on = False

    def start_engine(self):
        if not self.engine_on and self.reserve > 0:
            self.engine_on = True
            return "Двигатель запущен."
        return "Двигатель уже был запущен."

    def stop_engine(self):
        if self.engine_on:
            self.engine_on = False
            return "Двигатель остановлен."
        return "Двигатель уже был остановлен."

    def drive(self, distance):
        if not self.engine_on:
            return "Двигатель не запущен."
        if self.reserve / self.consumption * 100 < distance:
            return "Малый запас топлива."
        self.mileage += distance
        self.reserve -= distance / 100 * self.consumption
        return f"Проехали {distance} км. Остаток топлива: {self.reserve} л."

    def refuel(self):
        self.reserve = self.tank_volume

    def get_mileage(self):
        return self.mileage

    def get_reserve(self):
        return self.reserve


car_1 = Car(color="black", consumption=10, tank_volume=55)
print(car_1.start_engine())
print(car_1.drive(100))
print(car_1.drive(100))
print(car_1.drive(100))
print(car_1.drive(300))
print(f"Пробег {car_1.get_mileage()} км.")
print(f"Запас топлива {car_1.get_reserve()} л.")
print(car_1.stop_engine())
print(car_1.drive(100))

Вывод программы:

Двигатель запущен.
Проехали 100 км. Остаток топлива: 45.0 л.
Проехали 100 км. Остаток топлива: 35.0 л.
Проехали 100 км. Остаток топлива: 25.0 л.
Малый запас топлива.
Пробег 300 км.
Запас топлива 25.0 л.
Двигатель остановлен.
Двигатель не запущен.

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

Инкапсуляция заключается в сокрытии внутреннего устройства класса за интерфейсом, состоящим из методов класса. Это необходимо, чтобы не нарушать логику работы методов внутри класса. Если не следовать принципу инкапсуляции и попытаться взаимодействовать с атрибутами напрямую, то могут происходить изменения, которые приведут к ошибкам. Например, если в нашем примере попытаться изменить пробег напрямую, а не с помощью метода drive(), то автомобиль проедет указанный путь даже с пустым баком и без расхода топлива:

car_1 = Car(color="black", consumption=10, tank_volume=55)
car_1.mileage = 1000
print(f"Пробег {car_1.get_mileage()} км.")
print(f"Запас топлива {car_1.get_reserve()} л.")

Вывод программы:

Пробег 1000 км.
Запас топлива 55 л.

Давайте напишем ещё один класс для электромобилей. Их отличие будет заключаться в замене топливного бака на заряд аккумуляторной батареи:

class ElectricCar:

    def __init__(self, color, consumption, bat_capacity, mileage=0):
        self.color = color
        self.consumption = consumption
        self.bat_capacity = bat_capacity
        self.reserve = bat_capacity
        self.mileage = mileage
        self.engine_on = False

    def start_engine(self):
        if not self.engine_on and self.reserve > 0:
            self.engine_on = True
            return "Двигатель запущен."
        return "Двигатель уже был запущен."

    def stop_engine(self):
        if self.engine_on:
            self.engine_on = False
            return "Двигатель остановлен."
        return "Двигатель уже был остановлен."

    def drive(self, distance):
        if not self.engine_on:
            return "Двигатель не запущен."
        if self.reserve / self.consumption * 100 < distance:
            return "Малый заряд батареи."
        self.mileage += distance
        self.reserve -= distance / 100 * self.consumption
        return f"Проехали {distance} км. Остаток заряда: {self.reserve} кВт*ч."

    def recharge(self):
        self.reserve = self.bat_capacity

    def get_mileage(self):
        return self.mileage

    def get_reserve(self):
        return self.reserve

Напишем функцию range_reserve(), которая будет определять для автомобилей классов Car и ElectricCar запас хода в километрах. Функции, которые могут работать с объектами разных классов, называются полиморфными. А сам принцип ООП называется полиморфизмом.

Говоря о полиморфизме в Python, стоит упомянуть принятую в этом языке так называемую «утиную типизацию» (Duck typing). Она получила своё название от шутливого выражения: «Если нечто выглядит как утка, плавает как утка и крякает как утка, это, вероятно, утка и есть» («If it looks like a duck, swims like a duck and quacks like a duck, then it probably is a duck»). В программах на Python это означает, что, если какой-то объект поддерживает все требуемые от него операции, с ним и будут работать с помощью этих операций, не заботясь о том, какого он на самом деле типа.

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

Запас хода в километрах можно вычислить, разделив запас топлива (или заряд батареи) на расход и умножив результат на 100. Определить запас топлива или заряд батареи можно с помощью метода get_reserve(). Для соблюдения принципа инкапсуляции добавим метод get_consumption() в оба класса для получения значения атрибута consumption. Тогда полиморфная функция запишется так:

def range_reserve(car):
    return car.get_reserve() / car.get_consumption() * 100

Полностью программа с классами, полиморфной функцией и пример их использования представлены ниже:

class Car:

    def __init__(self, color, consumption, tank_volume, mileage=0):
        self.color = color
        self.consumption = consumption
        self.tank_volume = tank_volume
        self.reserve = tank_volume
        self.mileage = mileage
        self.engine_on = False

    def start_engine(self):
        if not self.engine_on and self.reserve > 0:
            self.engine_on = True
            return "Двигатель запущен."
        return "Двигатель уже был запущен."

    def stop_engine(self):
        if self.engine_on:
            self.engine_on = False
            return "Двигатель остановлен."
        return "Двигатель уже был остановлен."

    def drive(self, distance):
        if not self.engine_on:
            return "Двигатель не запущен."
        if self.reserve / self.consumption * 100 < distance:
            return "Малый запас топлива."
        self.mileage += distance
        self.reserve -= distance / 100 * self.consumption
        return f"Проехали {distance} км. Остаток топлива: {self.reserve} л."

    def refuel(self):
        self.reserve = self.tank_volume

    def get_mileage(self):
        return self.mileage

    def get_reserve(self):
        return self.reserve

    def get_consumption(self):
        return self.consumption


class ElectricCar:

    def __init__(self, color, consumption, bat_capacity, mileage=0):
        self.color = color
        self.consumption = consumption
        self.bat_capacity = bat_capacity
        self.reserve = bat_capacity
        self.mileage = mileage
        self.engine_on = False

    def start_engine(self):
        if not self.engine_on and self.reserve > 0:
            self.engine_on = True
            return "Двигатель запущен."
        return "Двигатель уже был запущен."

    def stop_engine(self):
        if self.engine_on:
            self.engine_on = False
            return "Двигатель остановлен."
        return "Двигатель уже был остановлен."

    def drive(self, distance):
        if not self.engine_on:
            return "Двигатель не запущен."
        if self.reserve / self.consumption * 100 < distance:
            return "Малый заряд батареи."
        self.mileage += distance
        self.reserve -= distance / 100 * self.consumption
        return f"Проехали {distance} км. Остаток заряда: {self.reserve} кВт*ч."

    def recharge(self):
        self.reserve = self.bat_capacity

    def get_mileage(self):
        return self.mileage

    def get_reserve(self):
        return self.reserve

    def get_consumption(self):
        return self.consumption


def range_reserve(car):
    return car.get_reserve() / car.get_consumption() * 100


car_1 = Car(color="black", consumption=10, tank_volume=55)
car_2 = ElectricCar(color="white", consumption=15, bat_capacity=90)
print(f"Запас хода: {range_reserve(car_1)} км.")
print(f"Запас хода: {range_reserve(car_2)} км.")

Вывод программы:

Запас хода: 550.0 км.
Запас хода: 600.0 км.

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

Данный урок посвящен объектно-ориентированному программированию в Python. Разобраны такие темы как создание объектов и классов, работа с конструктором, наследование и полиморфизм в Python.

Основные понятия объектно-ориентированного программирования

Объектно-ориентированное программирование (ООП) является методологией разработки программного обеспечения, в основе которой лежит понятие класса и объекта, при этом сама программа создается как некоторая совокупность объектов, которые взаимодействую друг с другом и с внешним миром. Каждый объект является экземпляром некоторого класса. Классы образуют иерархии. Более подробно о понятии ООП можно прочитать на википедии.

Выделяют три основных “столпа” ООП- это инкапсуляция, наследование и полиморфизм.

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

Под инкапсуляцией понимается сокрытие деталей реализации, данных и т.п. от внешней стороны. Например, можно определить класс “холодильник”, который будет содержать следующие данные: производитель, объем, количество камер хранения, потребляемая мощность и т.п., и методы: открыть/закрыть холодильник, включить/выключить, но при этом реализация того, как происходит непосредственно включение и выключение пользователю вашего класса не доступна, что позволяет ее менять без опасения, что это может отразиться на использующей класс «холодильник» программе. При этом класс становится новым типом данных в рамках разрабатываемой программы. Можно создавать переменные этого нового типа, такие переменные называются объекты.

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

Под наследованием понимается возможность создания нового класса на базе существующего. Наследование предполагает наличие отношения “является” между классом наследником и классом родителем. При этом класс потомок будет содержать те же атрибуты и методы, что и базовый класс, но при этом его можно (и нужно) расширять через добавление новых методов и атрибутов.

Примером базового класса, демонстрирующего наследование, можно определить класс “автомобиль”, имеющий атрибуты: масса, мощность двигателя, объем топливного бака и методы: завести и заглушить. У такого класса может быть потомок – “грузовой автомобиль”, он будет содержать те же атрибуты и методы, что и класс “автомобиль”, и дополнительные свойства: количество осей, мощность компрессора и т.п..

Полиморфизм

Полиморфизм позволяет одинаково обращаться с объектами, имеющими однотипный интерфейс, независимо от внутренней реализации объекта. Например, с объектом класса “грузовой автомобиль” можно производить те же операции, что и с объектом класса “автомобиль”, т.к. первый является наследником второго, при этом обратное утверждение неверно (во всяком случае не всегда). Другими словами полиморфизм предполагает разную реализацию методов с одинаковыми именами. Это очень полезно при наследовании, когда в классе наследнике можно переопределить методы класса родителя.

Создание классов и объектов

Создание класса в Python начинается с инструкции class. Вот так будет выглядеть минимальный класс.

class C: 
    pass

Класс состоит из объявления (инструкция class), имени класса (нашем случае это имя C) и тела класса, которое содержит атрибуты и методы (в нашем минимальном классе есть только одна инструкция pass).

Для того чтобы создать объект класса необходимо воспользоваться следующим синтаксисом:

имя_объекта = имя_класса()

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

Как уже было сказано выше, класс может содержать атрибуты и методы. Атрибут может быть статическим и динамическим (уровня объекта класса). Суть в том, что для работы со статическим атрибутом, вам не нужно создавать экземпляр класса, а для работы с динамическим – нужно. Пример:

class Rectangle:
    default_color = "green"

    def __init__(self, width, height):
        self.width = width
        self.height = height

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

>>> Rectangle.default_color
'green'

width и height – это динамические атрибуты, при их создании было использовано ключевое слово self. Пока просто примите это как должное, более подробно про self будет рассказано ниже. Для доступа к width и height предварительно нужно создать объект класса Rectangle:

>>> rect = Rectangle(10, 20)
>>> rect.width
10
>>> rect.height
20

Если обратиться через класс, то получим ошибку:

>>> Rectangle.width
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: type object 'Rectangle' has no attribute 'width'

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

Проверим ещё раз значение атрибута default_color:

>>> Rectangle.default_color
'green'

Присвоим ему новое значение:

>>> Rectangle.default_color = "red"
>>> Rectangle.default_color
'red'

Создадим два объекта класса Rectangle и проверим, что default_color у них совпадает:

>>> r1 = Rectangle(1,2)
>>> r2 = Rectangle(10, 20)
>>> r1.default_color
'red'
>>> r2.default_color
'red'

Если поменять значение default_color через имя класса Rectangle, то все будет ожидаемо: у объектов r1 и r2 это значение изменится, но если поменять его через экземпляр класса, то у экземпляра будет создан атрибут с таким же именем как статический, а доступ к последнему будет потерян:

Меняем default_color через r1:

>>> r1.default_color = "blue"
>>> r1.default_color
'blue'

При этом у r2 остается значение статического атрибута:

>>> r2.default_color
'red'
>>> Rectangle.default_color
'red'

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

Методы класса

Добавим к нашему классу метод. Метод – это функция, находящаяся внутри класса и выполняющая определенную работу.

Методы бывают статическими, классовыми (среднее между статическими и обычными) и уровня класса (будем их называть просто словом метод). Статический метод создается с декоратором @staticmethod, классовый – с декоратором @classmethod, первым аргументом в него передается cls, обычный метод создается без специального декоратора, ему первым аргументом передается self:

class MyClass:

    @staticmethod
    def ex_static_method():
        print("static method")

    @classmethod
    def ex_class_method(cls):
        print("class method")

    def ex_method(self):
        print("method")

Статический и классовый метод можно вызвать, не создавая экземпляр класса, для вызова ex_method() нужен объект:

>>> MyClass.ex_static_method()
static method

>>> MyClass.ex_class_method()
class method

>>> MyClass.ex_method()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: ex_method() missing 1 required positional argument: 'self'

>>> m = MyClass()
>>> m.ex_method()
method

Конструктор класса и инициализация экземпляра класса

В Python разделяют конструктор класса и метод для инициализации экземпляра класса. Конструктор класса это метод __new__(cls, *args, **kwargs) для инициализации экземпляра класса используется метод __init__(self). При этом, как вы могли заметить __new__ – это классовый метод, а __init__ таким не является. Метод __new__ редко переопределяется, чаще используется реализация от базового класса object (см. раздел Наследование), __init__ же наоборот является очень удобным способом задать параметры объекта при его создании.

Создадим реализацию класса Rectangle с измененным конструктором и инициализатором, через который задается ширина и высота прямоугольника:

class Rectangle:

    def __new__(cls, *args, **kwargs):
        print("Hello from __new__")
        return super().__new__(cls)

    def __init__(self, width, height):
        print("Hello from __init__")
        self.width = width
        self.height = height


>>> rect = Rectangle(10, 20)
Hello from __new__
Hello from __init__

>>> rect.width
10

>>> rect.height
20

Что такое self?

До этого момента вы уже успели познакомиться с ключевым словом self. self – это ссылка на текущий экземпляр класса, в таких языках как Java, C# аналогом является ключевое слово this. Через self вы получаете доступ к атрибутам и методам класса внутри него:

class Rectangle:

    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

В приведенной реализации метод area получает доступ к атрибутам width и height для расчета площади. Если бы в качестве первого параметра не было указано self, то при попытке вызвать area программа была бы остановлена с ошибкой.

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

Если вы знакомы с языками программирования Java, C#, C++ то, наверное, уже задались вопросом: “а как управлять уровнем доступа?”. В перечисленных языка вы можете явно указать для переменной, что доступ к ней снаружи класса запрещен, это делается с помощью ключевых слов (private, protected и т.д.). В Python таких возможностей нет, и любой может обратиться к атрибутам и методам вашего класса, если возникнет такая необходимость. Это существенный недостаток этого языка, т.к. нарушается один из ключевых принципов ООП – инкапсуляция. Хорошим тоном считается, что для чтения/изменения какого-то атрибута должны использоваться специальные методы, которые называются getter/setter, их можно реализовать, но ничего не помешает изменить атрибут напрямую. При этом есть соглашение, что метод или атрибут, который начинается с нижнего подчеркивания, является скрытым, и снаружи класса трогать его не нужно (хотя сделать это можно).

Внесем соответствующие изменения в класс Rectangle:

class Rectangle:

    def __init__(self, width, height):
        self._width = width
        self._height = height

    def get_width(self):
        return self._width

    def set_width(self, w):
        self._width = w

    def get_height(self):
        return self._height

    def set_height(self, h):
        self._height = h

    def area(self):
        return self._width * self._height

В приведенном примере для доступа к _width и _height используются специальные методы, но ничего не мешает вам обратиться к ним (атрибутам) напрямую.

>>> rect = Rectangle(10, 20)

>>> rect.get_width()
10

>>> rect._width
10

Если же атрибут или метод начинается с двух подчеркиваний, то тут напрямую вы к нему уже не обратитесь (простым образом). Модифицируем наш класс Rectangle:

class Rectangle:

    def __init__(self, width, height):
        self.__width = width
        self.__height = height

    def get_width(self):
        return self.__width

    def set_width(self, w):
        self.__width = w

    def get_height(self):
        return self.__height

    def set_height(self, h):
        self.__height = h

    def area(self):
        return self.__width * self.__height

Попытка обратиться к __width напрямую вызовет ошибку, нужно работать только через get_width():

>>> rect = Rectangle(10, 20)

>>> rect.__width
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Rectangle' object has no attribute '__width'

>>> rect.get_width()
10

Но на самом деле это сделать можно, просто этот атрибут теперь для внешнего использования носит название: _Rectangle__width:

>>> rect._Rectangle__width
10

>>> rect._Rectangle__width = 20

>>> rect.get_width()
20

Свойства

Свойством называется такой метод класса, работа с которым подобна работе с атрибутом. Для объявления метода свойством необходимо использовать декоратор @property.

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

Сделаем реализацию класса Rectangle с использованием свойств:

class Rectangle:

    def __init__(self, width, height):
        self.__width = width
        self.__height = height

    @property
    def width(self):
        return self.__width

    @width.setter
    def width(self, w):
        if w > 0:
            self.__width = w
        else:
            raise ValueError

    @property
    def height(self):
        return self.__height

    @height.setter
    def height(self, h):
        if h > 0:
            self.__height = h
        else:
            raise ValueError

    def area(self):
        return self.__width * self.__height

Теперь работать с width и height можно так, как будто они являются атрибутами:

>>> rect = Rectangle(10, 20)

>>> rect.width
10

>>> rect.height
20

Можно не только читать, но и задавать новые значения свойствам:

>>> rect.width = 50

>>> rect.width
50

>>> rect.height = 70

>>> rect.height
70

Если вы обратили внимание: в setter’ах этих свойств осуществляется проверка входных значений, если значение меньше нуля, то будет выброшено исключение ValueError:

>>> rect.width = -10
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "test.py", line 28, in width
    raise ValueError
ValueError

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

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

Синтаксически создание класса с указанием его родителя выглядит так:

class имя_класса(имя_родителя1, [имя_родителя2,…, имя_родителя_n])

Переработаем наш пример так, чтобы в нем присутствовало наследование:

class Figure:

    def __init__(self, color):
        self.__color = color

    @property
    def color(self):
        return self.__color

    @color.setter
    def color(self, c):
        self.__color = c


class Rectangle(Figure): 

    def __init__(self, width, height, color):
        super().__init__(color)
        self.__width = width
        self.__height = height

    @property
    def width(self):
        return self.__width

    @width.setter
    def width(self, w):
        if w > 0:
            self.__width = w
        else:
            raise ValueError

    @property
    def height(self):
        return self.__height

    @height.setter
    def height(self, h):
        if h > 0:
            self.__height = h
        else:
            raise ValueError 

    def area(self):
        return self.__width * self.__height

Родительским классом является Figure, который при инициализации принимает цвет фигуры и предоставляет его через свойства. Rectangle – класс наследник от Figure. Обратите внимание на его метод __init__: в нем первым делом вызывается конструктор (хотя это не совсем верно, но будем говорить так) его родительского класса:

super().__init__(color)

super – это ключевое слово, которое используется для обращения к родительскому классу.

Теперь у объекта класса Rectangle помимо уже знакомых свойств width и height появилось свойство color:

>>> rect = Rectangle(10, 20, "green")

>>> rect.width
10

>>> rect.height
20

>>> rect.color
'green'

>>> rect.color = "red"

>>> rect.color
'red'

Полиморфизм

Как уже было сказано во введении в рамках ООП полиморфизм, как правило, используется с позиции переопределения методов базового класса в классе наследнике. Проще всего это рассмотреть на примере. Добавим в наш базовый класс метод info(), который печатает сводную информацию по объекту класса Figure и переопределим этот метод в классе Rectangle, добавим  в него дополнительные данные:

class Figure:

    def __init__(self, color):
        self.__color = color

    @property
    def color(self):
        return self.__color

    @color.setter
    def color(self, c):
        self.__color = c

    def info(self):
       print("Figure")
       print("Color: " + self.__color)


class Rectangle(Figure):

    def __init__(self, width, height, color):
        super().__init__(color)
        self.__width = width
        self.__height = height

    @property
    def width(self):
        return self.__width

    @width.setter
    def width(self, w):
        if w > 0:
            self.__width = w
        else:
            raise ValueError

    @property
    def height(self):
        return self.__height

    @height.setter
    def height(self, h):
        if h > 0:
            self.__height = h
        else:
            raise ValueError

    def info(self):
        print("Rectangle")
        print("Color: " + self.color)
        print("Width: " + str(self.width))
        print("Height: " + str(self.height))
        print("Area: " + str(self.area()))

    def area(self):
        return self.__width * self.__height

Посмотрим, как это работает

>>> fig = Figure("orange")

>>> fig.info()
Figure
Color: orange

>>> rect = Rectangle(10, 20, "green")

>>> rect.info()
Rectangle
Color: green
Width: 10
Height: 20
Area: 200

Таким образом, класс наследник может расширять функционал класса родителя.

P.S.

Если вам интересна тема анализа данных, то мы рекомендуем ознакомиться с библиотекой Pandas. На нашем сайте вы можете найти вводные уроки по этой теме. Все уроки по библиотеке Pandas собраны в книге “Pandas. Работа с данными”.
Книга: Pandas. Работа с данными

<<< Python. Урок 13. Модули и пакеты   Python. Урок 15. Итераторы и генераторы>>>

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

Содержание страницы:
1. Создание класса
    1.1. Метод __init__()
    1.2. Создание экземпляра класса
    1.3. Обращение к атрибутам класса
    1.4. Вызов методов класса
2. Работа с классами
    2.1. Прямое изменение значения атрибута
    2.2. Изменение значения атрибута с использованием метода
    2.3. Изменение значения атрибута с приращением
3. Наследование класса
    3.1. Переопределение методов класса-родителя

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

Классы в Python могут моделировать практически все что угодно. Создадим простой класс, который будет описывать конкретный автомобиль:

class Car():
    “””Описание автомобиля”””
    def __init__(self, brand, model):
        “””Инициализирует атрибуты brand и model”””
        self.brand = brand
        self.model = model

    def sold(self):
        “””Автомобиль продан”””
        print(f”Автомобиль {self.brand} {self.model} продан “)

    def discount(self):
        “””Скидка на автомобиль”””
        print(f”На автомобиль {self.brand} {self.model} скидка 5%”)

Разберем код по порядку. В начале определяется класс с именем Car (class Car). По общепринятым соглашение название класса начинается с символа верхнего регистра. Круглые скобки в определение класса пусты, так как класс создается с нуля. Далее идет строка документации с кратким описанием. (“””Описание автомобиля”””). 

1.1. Метод __init__()

Функция, являющаяся частью класса, называется методом. Все свойства функций так же относятся и к методам, единственное отличие это способ вызова метода. Метод __init__() – специальный метод, который автоматически выполняется при создание нового экземпляра. Имя метода начинается и заканчивается двумя символами подчеркивания. Метод __init__() определяется с тремя параметрами: self, brand, model. Параметр self обязателен в определение метода и должен стоять перед всеми остальными параметрами. При создании экземпляра на основе класса Car, необходимо передать только два последних аргумента brand и model.

Каждая из двух переменных self.brand = brand и self.model = model снабжена префиксом self и к ним можно обращаться вызовом self.brand и self.model. Значения берутся из параметров brand и model. Переменные, к которым вы обращаетесь через экземпляры, также называются атрибутами. 

В классе Car также есть два метода: sold() и discount(). Этим методам не нужна дополнительная информация и они определяются с единственным параметром self. Экземпляры, которые будут созданы на базе этого класса смогут вызывать данные методы, которые просто выводят информацию. 

1.2. Создание экземпляра класса 

С помощью класса Car мы можем создавать экземпляры для конкретного автомобиля. Каждый экземпляр описывает конкретный автомобиль и его параметры. 

car_1 = Car(‘Bmw’, ‘X5’)

Создадим переменную car_1 и присвоим ей класс с параметрами автомобиля которые нужно обязательно передать (brand, model). При выполнение данного кода Python вызывает метод __init__ , создавая экземпляр, описывающий конкретный автомобиль и присваивает атрибутам brand и model  переданные значения. Этот экземпляр сохраняется в переменной car_1

1.3. Обращение к атрибутам класса

К атрибутам экземпляра класса мы можем обращаться через запись:

print(f”{car_1.brand}”)
print(f”{car_1.model}”)

В записи используется имя экземпляра класса и после точки имя атрибута (car_1.brand) или (car_1.model). В итоге на экран выведется следующая информация:

Bmw
X5

1.4. Вызов методов класса

После создания экземпляра на основе класса Car можете вызывать любые методы, написанные в классе. Чтобы вызвать метод, укажите экземпляр (car_1) и вызываемый метод после точки:

car_1.sold()
car_1.discount()

При вызове данных методов, Python выполнит код, написанный в этом методе.

Автомобиль Bmw X5 продан 
На автомобиль Bmw X5 скидка 5%

2. Работа с классами на Python

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

class Car():
    “””Описание автомобиля”””
    def __init__(self, brand, model, years):
        “””Инициализирует атрибуты”””
        self.brand = brand
        self.model = model
        self.years = years
        self.mileage = 0

    def get_full_name(self):
        “””Автомобиль”””
        name = f”Автомобиль {self.brand} {self.model} {self.years}”
        return name.title()

    def read_mileage(self):
        “””Пробег автомобиля”””
        print(f”Пробег автомобиля {self.mileage} км.”)

В описание автомобиля есть три атрибута(параметра) это brand, model, years. Также мы создали новый атрибут mileage (пробег) и присвоили ему начальное значение 0. Так как пробег у всех автомобилей разный, в последующем мы сможем изменять этот атрибут. Метод get_full_name будет возвращать полное описание автомобиля. Метод read_mileage будет выводить пробег автомобиля. 

Создадим экземпляр с классом Car и запустим методы:

car_2 = Car(‘audi’, ‘a4’, 2019)
print(car_2.get_full_name())
car_2.read_mileage()

В результате в начале Python вызывает метот __init__() для создания нового экземпляра. Сохраняет название, модель, год выпуска и создает новый атрибут с пробегом равным 0. В итоге мы получим такой результат:

Автомобиль Audi A4 2019
Пробег автомобиля 0 км.

2.1. Прямое изменение значения атрибута

Для изменения значения атрибута можно обратиться к нему напрямую и присвоить ему новое значение. Изменим пробег автомобиля car_2:

car_2 = Car(‘audi’, ‘a4’, 2019)
print(car_2.get_full_name())

car_2.mileage = 38
car_2.read_mileage()

Мы обратились к нашему экземпляру car_2 и связанным с ним атрибутом пробега(mileage) и присвоили новое значение 38. Затем вызвали метод read_mileage() для проверки. В результате мы получим следующие данные. 

Автомобиль Audi A4 2019
Пробег автомобиля 38 км.

2.2. Изменение значения атрибута с использованием метода 

В Python удобнее писать методы, которые будут изменять атрибуты за вас. Для этого вы просто передаете новое значение методу, который обновит значения. Добавим в наш класс Car метод update_mileage() который будет изменять показания пробега. 

class Car():
    “””Описание автомобиля”””
    def __init__(self, brand, model, years):
        “””Инициализирует атрибуты”””
        self.brand = brand
        self.model = model
        self.years = years
        self.mileage = 0

    def get_full_name(self):
        “””Автомобиль”””
        name = f”Автомобиль {self.brand} {self.model} {self.years}”
        return name.title()

    def read_mileage(self):
        “””Пробег автомобиля”””
        print(f”Пробег автомобиля {self.mileage} км.”)

    def update_mileage(self, new_mileage):
        “””Устанавливает новое значение пробега”””
        self.mileage = new_mileage

car_2 = Car(‘audi’, ‘a4’, 2019)
print(car_2.get_full_name())

car_2.read_mileage()
car_2.update_mileage(17100)
car_2.read_mileage()

Вначале выведем текущие показания пробега ( car_2.read_mileage() ). Затем вызовем метод update_mileage() и передадим ему новое значение пробега ( car_2.update_mileage(17100) ). Этот метод устанавливает пробег 17100. Выведем текущие показания ( car_2.read_mileage() ) и у нас получается:

Автомобиль Audi A4 2019
Пробег автомобиля 0 км.
Пробег автомобиля 17100 км.

2.3. Изменение значения атрибута с приращением

Если вместо того, чтобы присвоить новое значение, требуется изменить с значение с приращением, то в этом случаем мы можем написать еще один метод, который будет просто прибавлять пробег к уже имеющемся показаниям. Для этого добавим метод add_mileage в класс Car:

def add_mileage(self, km):
        “””Добавляет пробег”””
        self.mileage += km

Новый метод add_mileage() получает пробег в км и добавлет его к self.mileage

car_2.add_mileage(14687)
car_2.read_mileage()

Пробег автомобиля 31787 км.

В итоге после вызова метода add_mileage() пробег автомобиля в экземпляре car_2 увеличится на 14687 км и станет равным 31787 км. Данный метод мы можем вызывать каждый раз при изменении пробега и передавать новые значение, на которое будет увеличивать основной пробег.

3. Наследование класса в Python

Создавая новые классы не обязательно их создавать с нуля. Новый класс может наследовать свои атрибуты (переменные) и методы (функции принадлежащие классам) от ранее определенного исходного класса ( суперкласса ). Также исходный класс называют родителем, а новый класс – потомком или подклассом. В класс-потомок можно добавлять собственные атрибуты и методы. Напишем новый класс ElectricCar, который будет создан на базе класса Car:

class Car():
    “””Описание автомобиля”””
    def __init__(self, brand, model, years):
        “””Инициализирует атрибуты brand и model”””
        self.brand = brand
        self.model = model
        self.years = years
        self.mileage = 0

    def get_full_name(self):
        “””Автомобиль”””
        name = f”Автомобиль {self.brand} {self.model} {self.years}”
        return name.title()

    def read_mileage(self):
        “””Пробег автомобиля”””
        print(f”Пробег автомобиля {self.mileage} км.”)

    def update_mileage(self, new_mileage):
        “””Устанавливает новое значение пробега”””
        self.mileage = new_mileage

    
    def add_mileage(self, km):
        “””Добавляет пробег”””
        self.mileage += km

class ElectricCar(Car):
    “””Описывает электромобиль”””
    def __init__(self, brand, model, years):
        “””Инициализирует атрибуты класса родителя”””
        super().__init__(brand, model, years)
        # атрибут класса-потомка
        self.battery_size = 100

    def battery_power(self):
        “””Выводит мощность аккумулятора авто”””
        print(f”Мощность аккумулятора {self.battery_size} кВт⋅ч”)

Мы создали класс ElectriCar на базе класса Car. Имя класса-родителя в этом случае ставится в круглые скобки( class ElectricCar(Car) ). Метод __init__  в классе потомка (подклассе) инициализирует атрибуты класса-родителя и создает экземпляр класса Car. Функция super() .- специальная функция, которая приказывает Python вызвать метод __init__() родительского класса Car, в результате чего экземпляр ElectricCar получает доступ ко всем атрибутам класса-родителя. Имя super как раз и происходит из-за того, что класс-родителя называют суперклассом, а класс-потомок – подклассом.          

Далее мы добавили новый атрибут self.battery_size и присвоили исходное значение 100. Этот атрибут будет присутствовать во всех экземплярах класса ElectriCar. Добавим новый метод battery_power(), который будет выводить информацию о мощности аккумулятора.         

Создадим экземпляр класса ElectriCar и сохраним его в переменную tesla_1

tesla_1 = ElectricCar(‘tesla’, ‘model x’, 2021)
print(tesla_1.get_full_name())
tesla_1.battery_power(
)

При вызове двух методов мы получим:

Автомобиль Tesla Model X 2021
Мощность аккумулятора 100 кВт⋅ч

В новый класс ElectriCar мы можем добавлять любое количество атрибутов и методов связанных и не связанных с классом-родителем Car.    

3.1. Переопределение методов класса-родителя

Методы, которые используются в родительском классе можно переопределить в классе-потомке (подклассе). Для этого в классе-потомке определяется метод с тем же именем, что и у класса-родителя. Python игнорирует метод родителя и переходит на метод, написанный в классе-потомке (подклассе). Переопределим метод def get_full_name() чтобы сразу выводилась мощность аккумуляторов. 

class ElectricCar(Car):
    “””Описывает электромобиль”””
    def __init__(self, brand, model, years):
        “””Инициализирует атрибуты класса родителя”””
        super().__init__(brand, model, years)
        # атрибут класса-потомка
        self.battery_size = 100

    def battery_power(self):
        “””Выводит мощность аккумулятора авто”””
        print(f”Мощность аккумулятора {self.battery_size} кВт⋅ч”)

    def get_full_name(self):
        “””Автомобиль”””
        name = f”Автомобиль {self.brand} {self.model} {self.years} {self.battery_size}-кВт⋅ч “
        
return name.title()

 

В результате при запросе полного названия автомобиля Python проигнорирует метод def get_full_name() в классе-родителя Car и сразу перейдет к методу def get_full_name() написанный в классе ElectricCar.          

tesla_1 = ElectricCar(‘tesla’, ‘model x’, 2021)
print(tesla_1.get_full_name())

Автомобиль Tesla Model X 2021 100-Квт⋅Ч

Далее: Файлы и исключения в Python

Назад: Функции в Python

На чтение 32 мин Просмотров 7.2к. Опубликовано 28.02.2022

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

Содержание

  1. Процедурно-ориентированный стиль
  2. Объектно-ориентированный стиль
  3. Абстракция
  4. Инкапсуляция
  5. Наследование
  6. Полиморфизм
  7. Создание класса в Python
  8. Атрибут:
  9. Статические и динамические атрибуты класса
  10. Метод:
  11. Инициализатор:
  12. Что такое self?
  13. Уровни доступа атрибута и метода
  14. Свойства
  15. Сравнение объектов
  16. Атрибуты функции
  17. Встроенные атрибуты класса
  18. Составляющие класса или объекта
  19. Наследование
  20. super()
  21. Переопределение
  22. Документирование классов
  23. Удаление объектов (сбор мусора)

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

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

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

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

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

Абстракция

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

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

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

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

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

Полиморфизм

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

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

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


class MyClass(BaseClass):

    pass

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

— ключевое слово 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:UsersDushenkoAppDataRoamingJetBrainsPyCharm2021.3scratchesscratch.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:UsersDushenkoAppDataRoamingJetBrainsPyCharm2021.3scratchesscratch.py", line 11, in <module>

    print(lada.drive())

File "C:UsersDushenkoAppDataRoamingJetBrainsPyCharm2021.3scratchesscratch.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:UsersDushenkoAppDataRoamingJetBrainsPyCharm2021.3scratchesscratch.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:UsersDushenkoAppDataRoamingJetBrainsPyCharm2021.3scratchesscratch.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:UsersDushenkoAppDataRoamingJetBrainsPyCharm2021.3scratchesscratch.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:UsersDushenkoAppDataRoamingJetBrainsPyCharm2021.3scratchesscratch.py", line 14, in <module>

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

File "C:UsersDushenkoAppDataRoamingJetBrainsPyCharm2021.3scratchesscratch.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:UsersDushenkoAppDataRoamingJetBrainsPyCharm2021.3scratchesscratch.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:UsersDushenkoAppDataRoamingJetBrainsPyCharm2021.3scratchesscratch.py", line 11, in <module>

print(мопс)

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

 

Process finished with exit code 1

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

Добавить комментарий