Язык Python
ООП в python
Язык python полноценно поддерживает объектно-ориентированную разработку. Минимальный класс в python можно создать следующим образом:
class NewGreatType:
pass
obj = NewGreatType()
type(obj) # <class '__main__.NewGreatType'>
Любой объект в python является объектом какого-либо класса:
type(1) # <class 'int'>
type(int) # <class 'type'>
type(NewGreatType) # <class 'type'>
type(type) # <class 'type'>
Эти примеры показывают, что типы данных сами являются объектами класса type
. Встроенная функция dir
позволяет получить все атрибуты (поля и методы) объекта. Выведем для примера атрибуты объекта False
класса bool
:
dir(False)
# ['__abs__', '__add__', '__and__', '__bool__', '__ceil__', '__class__',
# '__delattr__', '__dir__', '__divmod__', '__doc__', '__eq__',
# '__float__', '__floor__', '__floordiv__', '__format__', '__ge__',
# '__getattribute__', '__getnewargs__', '__gt__', '__hash__',
# '__index__', '__init__', '__init_subclass__', '__int__', '__invert__',
# '__le__', '__lshift__', '__lt__', '__mod__', '__mul__', '__ne__',
# '__neg__', '__new__', '__or__', '__pos__', '__pow__', '__radd__',
# '__rand__', '__rdivmod__', '__reduce__', '__reduce_ex__', '__repr__',
# '__rfloordiv__', '__rlshift__', '__rmod__', '__rmul__', '__ror__',
# '__round__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__',
# '__rtruediv__', '__rxor__', '__setattr__', '__sizeof__', '__str__',
# '__sub__', '__subclasshook__', '__truediv__', '__trunc__', '__xor__',
# 'as_integer_ratio', 'bit_length', 'conjugate', 'denominator',
# 'from_bytes', 'imag', 'numerator', 'real', 'to_bytes']
Довольно много для такого простого объекта. Попробуем вызвать метод __or__
:
False.__or__(False) # False
False.__or__(True) # True
Вызов этого метода эквивалентен использованию оператора or
. Мы обнаружили способ перегрузки операторов в python. Она выполняется с помощью определения "магических" методов, некоторые из которых мы рассмотрим ниже.
Поля и методы
Вспомним класс LorentzVector
, с которым мы работали в разделе про классы в C++ и начнем писать его аналог на python:
class LorentzVector:
""" Релятивистский вектор """
def __init__(self, t, x):
""" x: может быть int, float или list """
self.t = t
self.x = x
def r2(self):
""" Квадрат модуля пространственной компоненты """
if isinstance(self.x, (int, float)):
return self.x**2
return sum(map(lambda a: a**2, self.x))
def inv(self):
""" Релятивистский инвариант """
return self.t**2 - self.r2()
Метод __init__
является конструктором. Объект self
ссылается на сам объект класса (аналог this
в C++) и является обязательным первым аргументом всех нестатических методов, включая конструктор. В конструкторе мы определили два поля класса: self.x
и self.y
. Также мы определили два метода: r2
возвращает квадрат пространственной компоненты вектора, и inv
возвращает релятивистский инвариант, соответствующий вектору.
Все поля и методы класса в python являются публичными. Разделение интерфейса и деталей реализации происходит на уровне соглашения об именах полей и методов: если атрибут не является частью интерфейса, то его имя должно начинаться с двух подчеркиваний, например: __internal_variable
.
Проверим работу класса LorentzVector
:
lv1 = LorentzVector(1, 0.5)
lv1.t # 1
lv1.x # 0.5
lv1.r2() # 0.25
lv1.inv() # 0.75
lv2 = LorentzVector(1, [0.3, 0.4, 0.0])
lv2.t # 1
lv2.x # [0.3, 0.4, 0.0]
lv2.r2() # 0.25
lv2.inv() # 0.75
Атрибуты могут определяться не только в конструкторе, но и в любом другом методе класса. Более того, атрибуты можно определять прямо в пользовательском коде:
lv = LorentzVector(1, 0.5)
hasattr(lv, 'mass') # False
lv.mass = 0.3
hasattr(lv, 'mass') # True
Атрибуты класса (статические атрибуты) определяются сразу после названия класса:
class LorentzVector:
""" Релятивистский вектор """
speed_of_light = 2.99792458e10 # см / с
# ...
LorentzVector.speed_of_light # 29979245800.0
Для задания статического метода необходимо использовать декоратор staticmethod
:
class LorentzVector:
# ...
@staticmethod
def boost_vector(lv):
""" Возвращает boost-вектор для данного вектора """
if isinstance(lv.x, (int, float)):
return lv.x / lv.t
return list(map(lambda x: x / lv.t, lv.x))
Как и следовало ожидать, статический метод не имеет аргумента self
. Декораторы — это инструмент python, позволяющий менять поведение функций. Технически — это функция, которая принимает на вход некоторую функцию, и возвращает новую функцию с тем же набором аргументов.
С помощью декоратора property
можно делать вызов методов, не имеющих аргументов, похожим на обращение к полю класса:
class LorentzVector:
# ...
@property
def r2(self):
""" Квадрат модуля пространственной компоненты """
if isinstance(self.x, (int, float)):
return self.x**2
return sum(map(lambda a: a**2, self.x))
@property
def inv(self):
""" Релятивистский инвариант """
return self.t**2 - self.r2()
lv1 = LorentzVector(1, 0.5)
lv1.t # 1
lv1.x # 0.5
lv1.r2 # 0.25
lv1.inv # 0.75
Магические методы
Интеграция пользовательских классов в среду языка выполняется посредством определения специальных методов. Начнём с рассмотрения примера перегрузки арифметического оператора:
class LorentzVector:
# ...
def __add__(self, rhs):
""" Сложение двух векторов """
if isinstance(self.x, (int, float)):
x_new = self.x + rhs.x
else:
x_new = list(map(lambda a: sum(a), zip(self.x, rhs.x)))
return LorentzVector(self.t + rhs.t, x_new)
Аналогично выполняется перегрузка операторов вычитания (__sub__
), умножения (__mul__
), деления (__div__
), круглых скобок (__call__
), квадратных скобок (__getitem__
) и т.д.
Методы __str__
и __repr__
отвечают за текстовое представление объекта. Метод __str__
вызывается, когда объект передается в функцию print
или в форматированную строку, и служит для "неформального" представления объекта. Метод __repr__
должен возвращать строку, которая содержит всю информацию о состоянии объекта и по которой объект может быть восстановлен. Если определен только метод __repr__
, то он будет вызываться в функции print
вместо метода __str__
.
Если передать объект LorentzVector
в функцию print
, не определяя специальных методов, то мы получим что-то подобное:
lv = LorentzVector(1, 0.5)
print(lv) # __main__.LorentzVector object at 0x7f50a36dfeb0>
Функция print
вывела тип объекта и адрес, по которому он расположен в памяти.
Определим метод __repr__
:
class LorentzVector:
# ...
def __repr__(relf):
""" Текстовое представление вектора """
return f'LorentzVector [{self.t}, {self.x}]'
Получаем теперь:
lv = LorentzVector(1, 0.5)
print(lv) # LorentzVector [1, 0.5]
Мы рассмотрели лишь некоторые из доступных специальных методов. Рекомендуем ознакомиться с полным списком в документации.
Наследование
Язык python позволяет выполнять наследование классов. Класс-потомок имеет доступ ко всем полям и методам класса-предка. Все классы в python являются наследниками класса object
. Об класса object
наш класс LorentzVector
наследует большинство своих атрибутов:
lv = LorentzVector(1, 0.5)
isinstance(lv, object) # True
dir(lv)
# ['__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__', 'inv', 'mass',
# 'r2', 'sum', 't', 'x']
Используем механизм наследования, чтобы реализовать тип релятивистского вектора иначе. Новый тип будет наследником именованного кортежа NamedTuple
:
from typing import NamedTuple
class FourVector(NamedTuple): # <- наследование
""" Релятивистский вектор """
t: float # такой синтаксис используется для задания
r: list # полей кортежа и аннотации их типов
@property
def r2(self):
""" Квадрат модуля пространственной компоненты """
return sum(map(lambda a: a**2, self.r))
@property
def inv(self):
""" Релятивистский инвариант """
return self.t**2 - self.r2()
def __repr__(relf):
""" Текстовое представление вектора """
return f'FourVector [{self.t}, {self.r}]'
Новая реализация яснее показывает структуру нашего типа данных. Для создания объекта FourVector
необходимо задать значения полям t
и r
:
fv1 = FourVector(1, [0.3, 0.4, 0.0])
fv2 = FourVector(t=1, r=[0.3, 0.4, 0.0])
fv3 = FourVector(t=1, r=[0.5])
fv4 = FourVector(t=1, r=0.5)
fv3.r2 # 0.25
fv4.r2 # TypeError: 'float' object is not iterable
При создании объекта fv4
мы нарушили соглашение и передали в поле r
объект float
вместо list
. Это привело к ошибке при вызове метода r2
. При определении класса FourVector
мы использовали аннотацию типов (t: float
).
Больше информации о наследовании в python можно найти в документации.
Полиморфизм в python
Реализация полиморфизма в python сильно отличается от его реализации в C++. Полиморфизм в C++ реализуется с помощью инструментов наследования и шаблонов. Динамическая типизация python позволяет использовать гораздо более гибкие инструменты полиморфизма. Переменные, аргументы функций и атрибуты классов в python могут в разных контекстах иметь разные типы и даже менять тип со временем. Таким образом, все объекты в python изначально полиморфны.
В такой ситуации возникает вопрос о том как описывать ограничения на допустимые типы объектов. Здесь на помощь приходит принцип утиной типизации (duck typing), дословно состоящий в том, что "если что-то выглядит как утка и крякает как утка, значит это утка". Иными словами, если объект предоставляет необходимый интерфейс, то мы можем с ним работать вне зависимости от его типа. Для поддержки этого подхода в python реализованы инструменты для проверки свойств объектов:
# проверяет является ли obj объектом типа int или float
isinstance(obj, (int, float))
# проверяет имеет ли obj атрибут norm
hasattr(obj, 'norm')
# является ли класс FourVector подклассом NamedTuple
issubclass(FourVector, NamedTuple)
Столь гибкая типизация приводит к необходимости качественной документации кода. Хорошим стилем является описание всех контрактов функции или метода в его строке комментария. Значительно улучшает читаемость кода и аннотация типов.
Резюме
В этом разделе мы выполнили краткий обзор инструментов python, реализующих парадигму объектно-ориентированного программирования. Обсудили создание классов; определение полей и методов; статических полей и методов; определение специальных методов, позволяющих интегрировать тип данных в среду языка; кратко рассмотрели наследование классов и принципы реализации полиморфизма в python.
Концепция ООП в python не является основной, как в C++, однако средства ООП составляют важную часть языка, и их понимание необходимо для грамотной разработки на python, поскольку все типы объектов в python являются классами.