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

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

Ключевые черты ООП:

  1. Инкапсуляция — это определение классов — пользовательских типов данных, объединяющих своё содержимое в единый тип и реализующих некоторые операции или методы над ним. Классы обычно являются основой модульности, инкапсуляции и абстракции данных в языках ООП.
  2. Наследование — способ определения нового типа, когда новый тип наследует элементы (свойства и методы) существующего, модифицируя или расширяя их. Это способствует выражению специализации и генерализации.
  3. Полиморфизм позволяет единообразно ссылаться на объекты различных классов (обычно внутри некоторой иерархии). Это делает классы ещё удобнее и облегчает расширение и поддержку программ, основанных на них.

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

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

Типы данных (такие как int, float и др.) в Python являются классами, структуры данных (dict, list, ...) — это также классы.

print(int) print(dict)

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

num = 13 isinstance(num, int)
True

Реализация собственных классов

class Country: pass
print(dir(Country))
['__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__']
russia = Country() # Создаются два экземпляра china = Country() # Каждый является отдельным пространством имен
russia.name = "Russia" # Можно получать/записывать значения атрибутов china.name = "China"

Первым аргументом метод __init__ принимает ссылку на только что созданный экземпляр класса, далее могут идти другие аргументы. Внутри инициализатора мы можем по ссылке self установить так называемые атрибуты экземпляра. В данном случае мы ставим атрибут экземпляра name и присваиваем ему аргумент name — имя страны:

class Country: def __init__(self, name): self.name = name
russia = Country("Russia")
russia.__class__
__main__.Country
class Country: def __init__(self, name): self.name = name # магический метод __str__ переопределяет то, как будет печататься объект def __str__(self): return self.name # repr() однозначное текстовое представление (representation) объекта полезное для отладки def __repr__(self): return f"Country {self.name}" countries = [] country_names = ["Russia", "Kazakhstan", "China", "Italy"] for name in country_names: country = Country(name) countries.append(country) print(countries)
[Country Russia, Country Kazakhstan, Country China, Country Italy]

Классовые переменные

Иногда нужно создать переменную, которая будет работать в контексте класса, но не будет связана с каждым конкретным экземпляром (т.е. будет относиться непосредственно к самому классу, а не к экземпляру). В этом примере count (счётчик стран) — это атрибут класса:

class Country: count = 0 def __init__(self, name, population=None): self.name = name self.population = population or [] Country.count += 1
russia = Country("Russia") china = Country("China") print(Country.count, russia.count, china.__dict__)
2 2 {'name': 'China', 'population': []}

Когда счетчик ссылок на экземпляр класса достигает нуля (мы уже говорили про сборщик мусора в Python и то, что он использует счетчик ссылок), вызывается метод __del__ экземпляра. Это также магический метод, который Python нам предоставляет возможность переопределить:

class Country: def __init__(self, name): self.name = name def __del__(self): print(f"Country {self.name} has been exterminated!") byzantium = Country("Byzantium")
del byzantium
Country Byzantium has been exterminated!

Методы

Методы — это функции, которые действуют в контексте экземпляра класса. Таким образом, они могут менять состояние экземпляра, обращаясь к атрибутам экземпляра или делать любую другую полезную работу.

class Human: def __init__(self, name, age=0): self.name = name self.age = age def _say(self, text): print(text) def say_name(self): self._say(f"Hello, I am {self.name}") def say_how_old(self): self._say(f"I am {self._age} years old") class Country: def __init__(self, name, population=None): self.name = name self.population = population or [] def add_human(self, human): print(f"Welcome to {self.name}, {human.name}!") self.population.append(human)
russia = Country("Russia") settler = Human("Gérard Depardieu", age=69) settler.say_name() russia.add_human(settler)
Hello, I am Gérard Depardieu
Welcome to Russia, Gérard Depardieu!
russia.population
[<__main__.Human at 0x10f52ab00>]

Перегрузка операторов

class Country: def __init__(self, name, population=None): self.name = name self.population = population or [] def __add__(self, other): print(f"Countries {self.name} and {other.name} are allies now!") return Country(f"Alliance of {self.name} and {other.name}")
russia = Country("Russia") belarus = Country("Belarus") (russia + belarus).name
Countries Russia and Belarus are allies now!
'Alliance of Russia and Belarus'

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

class Country: def __init__(self, name=None): self.name = name
class Region(Country): def __init__(self, name, status=""): ''' Вызовем инициализатор родительского класса, используя функцию super() Вызов функции super() без параметров равносилен тому, что мы указали сам класс и передали туда объект self То же самое, что Region.__init__(self) ''' super().__init__(name) self.status = status def get_info(self): return "Название: {}. Статус: {}".format(self.name, self.status) len_obl = Region("Ленинградская область", "область") print(len_obl.get_info())
Название: Ленинградская область. Статус: область

Множественное наследование через классы примеси

import json class ExportJSON: def to_json(self): return json.dumps({"name": self.name, "status": self.status}) class ExReg(Region, ExportJSON): pass
ExReg("Ленинградская область", "область").to_json()
'{"name": "\\u041b\\u0435\\u043d\\u0438\\u043d\\u0433\\u0440\\u0430\\u0434\\u0441\\u043a\\u0430\\u044f \\u043e\\u0431\\u043b\\u0430\\u0441\\u0442\\u044c", "status": "\\u043e\\u0431\\u043b\\u0430\\u0441\\u0442\\u044c"}'
print(issubclass(int, object)) print(issubclass(Region, object)) print(issubclass(Region, Country)) print(issubclass(ExReg, Country))
True
True
True
True

Приватные атрибуты

Также в Python существуют приватные атрибуты. Для того чтобы создать приватный атрибут, необходимо его имя записать через два символа нижнего подчёркивания. Тогда в самом классе к нему можно обращаться так же, а вот для классов-наследников этот атрибут будет уже недо- ступен. На самом деле не так всё просто, Python просто изменяет название и оно доступно в __dict__

Практика

Попробуйте сделать задание на классы из курса Погружение в Python:

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

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

Тип (car_type)Марка (brand)Кол-во пассажирских мест (passenger_seats_count)Фото (photo_file_name)Кузов ДxШxВ, м (body_whl)Грузоподъемность, Тонн (carrying)Дополнительно (extra)
carNissan xTtrail4f1.jpeg2.5
truckManf2.jpeg8x3x2.520
carMazda 64f3.jpeg2.5
spec_machineHitachif4.jpeg1.2Легкая техника для уборки снега

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

  • BaseCar
  • Car(BaseCar)
  • Truck(BaseCar)
  • SpecMachine(BaseCar)

У любого объекта есть обязательный атрибут car_type. Он означает тип объекта и может принимать одно из значений: car, truck, spec_machine.

Также у любого объекта из иерархии есть фото в виде имени файла — обязательный атрибут photo_file_name.

В базовом классе нужно реализовать метод get_photo_file_ext для получения расширения файла (“.png”, “.jpeg” и т.д.) с фото. Расширение файла можно получить при помощи os.path.splitext.

Для грузового автомобиля необходимо разделить характеристики кузова на отдельные составляющие body_length, body_width, body_height. Разделитель — латинская буква x. Характеристики кузова могут быть заданы в виде пустой строки, в таком случае все составляющие равны 0. Обратите внимание на то, что характеристики кузова должны быть вещественными числами.

Также для класса грузового автомобиля необходимо реализовать метод get_body_volume, возвращающий объем кузова в метрах кубических.

Все обязательные атрибуты для объектов Car, Truck и SpecMachine перечислены в таблице ниже, где 1 - означает, что атрибут обязателен для объекта, 0 - атрибут должен отсутствовать.

CarTruckSpecMachine
car_type111
photo_file_name111
brand111
carrying111
passenger_seats_count100
body_width010
body_height010
body_length010
extra001

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

Не важно как вы назовете свои классы, главное чтобы их атрибуты имели имена:

  • car_type
  • brand
  • passenger_seats_count
  • photo_file_name
  • body_width
  • body_height
  • body_length
  • carrying
  • extra

И методы:

  • get_photo_file_ext
  • get_body_volume

У каждого объекта из иерархии должен быть свой набор атрибутов и методов. У класса легковой автомобиль не должно быть метода get_body_volume в отличие от класса грузового автомобиля.

Функция, которая парсит строки входного массива, должна называться get_car_list.

Также обратите внимание, что все значения в csv файле при чтении будут python-строками. Нужно преобразовать строку в int для passenger_seats_count, во float для carrying, а также во float для body_width body_height, body_length.

Также ваша программа должна быть готова к тому, что в некоторых строках данные могут быть заполнены некорректно. Например, число колонок меньше . В таком случае нужно проигнорировать подобные строки и не создавать объекты. Строки с пустым значением для body_whl игнорироваться не должны. Вы можете использовать механизм исключений для обработки ошибок.

Ниже приведен пример с заготовкой кода для выполнения задания.

class CarBase: def __init__(self, brand, photo_file_name, carrying): pass class Car(CarBase): def __init__(self, brand, photo_file_name, carrying, passenger_seats_count): pass class Truck(CarBase): def __init__(self, brand, photo_file_name, carrying, body_whl): pass class SpecMachine(CarBase): def __init__(self, brand, photo_file_name, carrying, extra): pass def get_car_list(csv_filename): car_list = [] return car_list

Вам необходимо расширить функционал исходных классов, дополнить методы нужным кодом и реализовать функцию get_car_list.

Комментарии