Привет! Представляю вашему вниманию перевод статьи “Object-Oriented Programming in Python vs Java” автора Джона Финчера.
Реализация объектно-ориентированного программирования (ООП) в языках Java и Python отличается. Принцип работы с объектами, типами переменных и прочими языковыми возможностями может вызвать затруднение при переходе с одного языка на другой. В данной статье, которая может быть полезной как для Java-программистов, желающих освоить Python, так и для Python-программистов, имеющих цель лучше узнать Java, приводятся основные сходства и отличия этих языков, применительно к ООП.
Подробнее – под катом.
Примеры классов в Python и Java
Для начала давайте реализуем простейший класс в Python и Java, чтобы проиллюстрировать некоторые отличия в этих языках, и будем постепенно вносить в этот класс изменения.
Представим, что у нас есть следующее определение класса Car в Java:
1 public class Car {
2 private String color;
3 private String model;
4 private int year;
5
6 public Car(String color, String model, int year) {
7 this.color = color;
8 this.model = model;
9 this.year = year;
10 }
11
12 public String getColor() {
13 return color;
14 }
15
16 public String getModel() {
17 return model;
18 }
19
20 public int getYear() {
21 return year;
22 }
23 }
Имя исходного Java-файла должно соответствовать имени хранящегося в нем класса, поэтому мы обязаны назвать файл Car.java. Каждый Java-файл может содержать только один публичный класс.
Такой же класс в Python будет выглядеть так:
1 class Car:
2 def __init__(self, color, model, year):
3 self.color = color
4 self.model = model
5 self.year = year
В Python вы можете объявить класс где угодно и когда угодно. Сохраним этот файл как car.py.
Используя эти классы как основу, продолжим исследование основных компонентов классов и объектов.
Атрибуты объекта
Во всех объектно-ориентированных языках данные об объекте где-то хранятся. И в Python, и в Java эти данные хранятся в атрибутах, которые являются переменными, связанными с конкретными объектами.
Одним из наиболее значительных отличий между Python и Java является то, как они определяют атрибуты класса и объекта и как эти языки управляют ими. Некоторые из этих различий вызваны ограничениями, налагаемыми языками, в то время как другие связаны с более эффективной практикой.
Объявление и инициализация
В Java мы объявляем атрибуты (с указанием их типа) внутри класса, но за пределами всех методов. Перед тем, как использовать атрибуты класса, мы должны их определить:
1 public class Car {
2 private String color;
3 private String model;
4 private int year;
В Python же мы объявляем и определяем атрибуты внутри метода класса init(), который является аналогом конструктора в Java:
1 def __init__(self, color, model, year):
2 self.color = color
3 self.model = model
4 self.year = year
Указывая перед именем переменных ключевое слово self, мы говорим Python-у, что это атрибуты. Каждый экземпляр класса получает свою копию. Все переменные в Python слабо типизированы, и атрибуты не являются исключением.
Переменные можно создать и за пределами метода init(), но это не будет лучшим решением и может привести к труднообнаруживаемым багам. Например, можно добавить объекту Car новый атрибут wheels следующим образом:
1 >>> import car
2 >>> my_car = car.Car("yellow", "beetle", 1967)
3 >>> print(f"My car is {my_car.color}")
4 My car is yellow
5
6 >>> my_car.wheels = 5
7 >>> print(f"Wheels: {my_car.wheels}")
8 Wheels: 5
Однако, если мы забудем указать в 6-й строке выражение my_car.wheels = 5, то получим ошибку:
1 >>> import car
2 >>> my_car = car.Car("yellow", "beetle", 1967)
3 >>> print(f"My car is {my_car.color}")
4 My car is yellow
5
6 >>> print(f"Wheels: {my_car.wheels}")
7 Traceback (most recent call last):
8 File "<stdin>", line 1, in <module>
9 AttributeError: 'Car' object has no attribute 'wheels'
В Python если объявить переменную за пределами метода, то она будет рассматриваться как переменная класса. Давайте изменим класс Car:
1 class Car:
2
3 wheels = 0
4
5 def __init__(self, color, model, year):
6 self.color = color
7 self.model = model
8 self.year = year
Теперь изменится использование переменной wheels. Вместо обращения к ней через объект, мы обращаемся к ней, используя имя класса:
1 >>> import car
2 >>> my_car = car.Car("yellow", "beetle", 1967)
3 >>> print(f"My car is {my_car.color}")
4 My car is yellow
5
6 >>> print(f"It has {car.Car.wheels} wheels")
7 It has 0 wheels
8
9 >>> print(f"It has {my_car.wheels} wheels")
10 It has 0 wheels
Примечание: в Python обращение к переменной класса происходит по следующему синтаксису:
- Имя файла, содержащего класс (без расширения .py)
- Точка
- Имя класса
- Точка
- Имя переменной
Поскольку мы сохранили класс Car в файле car.py, мы обращаемся к переменной класса wheels в 6-й строчке таким образом: car.Car.wheels.
Работая с переменной wheels, необходимо быть обратить внимание на то, что изменение значения переменной экземпляра класса my_car.wheels не ведет к изменению переменной класса car.Car.wheels:
1 >>> from car import *
2 >>> my_car = car.Car("yellow", "Beetle", "1966")
3 >>> my_other_car = car.Car("red", "corvette", "1999")
4
5 >>> print(f"My car is {my_car.color}")
6 My car is yellow
7 >>> print(f"It has {my_car.wheels} wheels")
8 It has 0 wheels
9
10 >>> print(f"My other car is {my_other_car.color}")
11 My other car is red
12 >>> print(f"It has {my_other_car.wheels} wheels")
13 It has 0 wheels
14
15 >>> # Change the class variable value
16 ... car.Car.wheels = 4
17
18 >>> print(f"My car has {my_car.wheels} wheels")
19 My car has 4 wheels
20 >>> print(f"My other car has {my_other_car.wheels} wheels")
21 My other car has 4 wheels
22
23 >>> # Change the instance variable value for my_car
24 ... my_car.wheels = 5
25
26 >>> print(f"My car has {my_car.wheels} wheels")
27 My car has 5 wheels
28 >>> print(f"My other car has {my_other_car.wheels} wheels")
29 My other car has 4 wheels
На 2-й и 3-й строчках мы определили два объекта Car: my_car и my_other_car.
Сначала свойство wheels у обоих объектов равно нулю. На 16-й строке мы установили переменную класса: car.Car.wheels = 4, у обоих объектов теперь по 4 колеса. Однако, затем когда на 24-й строке мы меняем свойство объекта my_car.wheels = 5, свойство второго объекта остается нетронутым.
Это означает, что теперь у нас две различные копии атрибута wheels:
- Переменная класса, которая применяется ко всем объектам Car
- Конкретная переменная экземпляра класса, которая применяется только к объекту my_car.
Из-за этого можно случайно сослаться не на тот экземпляр и сделать малозаметную ошибку.
В Java эквивалентом атрибута класса является статичный (static) атрибут:
public class Car {
private String color;
private String model;
private int year;
private static int wheels;
public Car(String color, String model, int year) {
this.color = color;
this.model = model;
this.year = year;
}
public static int getWheels() {
return wheels;
}
public static void setWheels(int count) {
wheels = count;
}
}
Обычно мы обращаемся к статичным переменным в Java через имя класса. Можно обратиться к ним и через экземпляр класса, как в Python, но это не будет лучшим решением.
Наш Java-класс начинает удлиняться. Одной из причин, почему Java «многословнее» Python-а, является понятие публичных (public) и приватных (private) методов и атрибутов.
Публичные и приватные
Java управляет доступом к методам и атрибутам, различая публичные и приватные данные.
В Java ожидается, что атрибуты будут объявлены как приватные (или защищенные — protected, если нужно обеспечить к ним доступ потомкам класса). Таким образом мы ограничиваем доступ к ним извне. Чтобы предоставить доступ к приватным атрибутам, мы объявляем публичные методы, которые устанавливают или получают эти данные (подробнее об этом – чуть позже).
Вспомним, что в нашем Java-коде переменная color была объявлена приватной. Следовательно, нижеприведенный код не скомпилируется:
Car myCar = new Car("blue", "Ford", 1972);
// Paint the car
myCar.color = "red";
Если не указать уровень доступа к атрибутам, то по умолчанию он будет установлен как package protected, что ограничивает доступ к классам в пределах пакета. Если же мы хотим, что вышеуказанный код заработал, то придется сделать атрибут публичным.
Однако, в Java не приветствуется объявление атрибутов публичными. Рекомендуется объявлять их приватными, а затем использовать публичные методы, наподобие getColor() и getModel(), как и было указано в тексте кода выше.
В противоположность, в Python отсутствуют понятия публичных и приватных данных. В Python всё – публичное. Этот питоновский код сработает на ура:
>>> my_car = car.Car("blue", "Ford", 1972)
>>> # Paint the car
... my_car.color = "red"
Вместо приватных переменных в Python имеется понятие непубличных (non-public) переменных экземпляра класса. Все переменные, названия которых начинаются с одинарного подчеркивания, считаются непубличными. Это соглашение об именах затрудняет доступ к переменной, но это всего лишь соглашение об именах, и мы все равно можем обратиться к переменной напрямую.
Добавим следующую строку в наш питоновский класс Car:
class Car:
wheels = 0
def __init__(self, color, model, year):
self.color = color
self.model = model
self.year = year
self._cupholders = 6
Мы можем получить доступ к переменной _cupholders напрямую:
>>> import car
>>> my_car = car.Car("yellow", "Beetle", "1969")
>>> print(f"It was built in {my_car.year}")
It was built in 1969
>>> my_car.year = 1966
>>> print(f"It was built in {my_car.year}")
It was built in 1966
>>> print(f"It has {my_car._cupholders} cupholders.")
It has 6 cupholders.
Python позволяет получить доступ к такой переменной, правда, некоторые среды разработки вроде VS Code выдадут предупреждение:
Кроме этого, в Python для того, чтобы скрыть атрибут, используется двойное подчеркивание в начале названия переменной. Когда Python видит такую переменную, он автоматически меняет ее название, чтобы затруднить к ней прямой доступ. Однако, этот механизм всё равно не мешает нам обратиться к ней. Продемонстрируем это следующим примером:
class Car:
wheels = 0
def __init__(self, color, model, year):
self.color = color
self.model = model
self.year = year
self.__cupholders = 6
Теперь если мы обратимся к переменной __cupholders, мы получим ошибку:
>>> import car
>>> my_car = car.Car("yellow", "Beetle", "1969")
>>> print(f"It was built in {my_car.year}")
It was built in 1969
>>> my_car.year = 1966
>>> print(f"It was built in {my_car.year}")
It was built in 1966
>>> print(f"It has {my_car.__cupholders} cupholders.")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Car' object has no attribute '__cupholders'
Так почему же атрибут __cupholders не существует?
Дело вот в чем. Когда Python видит атрибут с двойным подчеркиванием в самом начале, он меняет его, добавляя в начало имя класса с подчеркиванием. Для того чтобы обратиться к атрибуту напрямую, необходимо также изменить имя:
>>> print(f"It has {my_car._Car__cupholders} cupholders")
It has 6 cupholders
Теперь возникает вопрос: если атрибут Java-класса объявлен приватным и атрибуту Python-класса предшествует в имени двойное подчеркивание, то как достучаться до этих данных?
Управление доступом
В Java мы получаем доступ к приватным атрибутам при помощи сеттеров (setters) и геттеров (getters). Для того чтобы пользователь перекрасил-таки свою машину, добавим следующий кусок кода в Java-класс:
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
Поскольку методы getColor() и setColor() – публичные, то любой пользователь может вызвать их и получить / изменить цвет машины. Использование приватных атрибутов, к которым мы получаем доступ публичными геттерами и сеттерами, — одна из причин большей «многословности» Java в сравнении с Python.
Как было показано выше, в Python мы можем получить доступ к атрибутам напрямую. Поскольку всё – публичное, мы может достучаться к чему угодно, когда угодно и откуда угодно. Мы можем получать и устанавливать значения атрибутов напрямую, обращаясь по их имени. В Python мы можем даже удалять атрибуты, что немыслимо в Java:
>>> my_car = Car("yellow", "beetle", 1969)
>>> print(f"My car was built in {my_car.year}")
My car was built in 1969
>>> my_car.year = 1966
>>> print(f"It was built in {my_car.year}")
It was built in 1966
>>> del my_car.year
>>> print(f"It was built in {my_car.year}")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Car' object has no attribute 'year'
Однако бывает и так, что мы хотим контролировать доступ к атрибутам. В таком случае нам на помощь приходят Python-свойства (properties).
В Python свойства обеспечивают управляемый доступ к атрибутам класса при помощи декораторов (decorators). Используя свойства, мы объявляем функции в питоновских классах подобно геттерам и сеттерам в Java (бонусом идет удаление атрибутов).
Работу свойств можно увидеть на следующем примере класса Car:
1 class Car:
2 def __init__(self, color, model, year):
3 self.color = color
4 self.model = model
5 self.year = year
6 self._voltage = 12
7
8 @property
9 def voltage(self):
10 return self._voltage
11
12 @voltage.setter
13 def voltage(self, volts):
14 print("Warning: this can cause problems!")
15 self._voltage = volts
16
17 @voltage.deleter
18 def voltage(self):
19 print("Warning: the radio will stop working!")
20 del self._voltage
В данном примере мы расширяем понятие класса Car, включая электромобили. В строке 6 объявляется атрибут _voltage, чтобы хранить в нем напряжение батареи.
В строках 9 и 10 для контролируемого доступа мы создаем функцию voltage() и возвращаем значение приватной переменной. Используя декоратор @property, мы превращаем его в геттер, к которому теперь любой пользователь получает доступ.
В строках 13-15 мы определяем функцию, так же носящую название voltage(). Однако, мы ее декорируем по-другому: voltage.setter. Наконец, в строках 18-20 мы декорируем функцию voltage() при помощи voltage.deleter и можем при необходимости удалить атрибут _voltage.
Декорируемые функции носят одинаковые имена, указывая на то, что они управляют доступом к одному и тому же атрибуту. Эти имена функций также становятся именами атрибутов, используемых для получения их значений. Вот как это работает:
1 >>> from car import *
2 >>> my_car = Car("yellow", "beetle", 1969)
3
4 >>> print(f"My car uses {my_car.voltage} volts")
5 My car uses 12 volts
6
7 >>> my_car.voltage = 6
8 Warning: this can cause problems!
9
10 >>> print(f"My car now uses {my_car.voltage} volts")
11 My car now uses 6 volts
12
13 >>> del my_car.voltage
14 Warning: the radio will stop working!
Обратите внимание, что мы используем voltage, а не _ voltage. Так мы указываем Python-у на то, что следует применять свойства, которые только что определили:
- Когда в 4-й строке выводим значение my_car.voltage, Python вызывает функцию voltage(), декорированную @property.
- Когда в 7-й строке присваиваем значение my_car.voltage, Python вызывает функцию voltage(), декорированную voltage.setter.
- Когда в 13-й строке удаляем my_car.voltage, Python вызывает функцию voltage(), декорированную voltage.deleter.
Вышеприведенные декораторы дают нам возможность контролировать доступ к атрибутам без использования различных методов. Можно даже сделать атрибут свойством только для чтения (read-only), убрав декорированные функции @.setter и @.deleter.
self и this
В Java класс ссылается сам на себя, используя ключевое слово this:
public void setColor(String color) {
this.color = color;
}
this подразумевается в Java-коде. Его в принципе даже необязательно писать, кроме случаев, когда имена переменных совпадают.
Сеттер можно написать и так:
public void setColor(String newColor) {
color = newColor;
}
Поскольку в классе Car есть атрибут под названием color и в области видимости нет больше переменных с таким именем, ссылка на это имя срабатывает. Мы использовали ключевое слово this в первом примере для того, чтобы различать атрибут и параметр с одинаковым именем color.
В Python ключевое слово self служит аналогичной цели: обращение к членам-атрибутам, но в отличие от Java, оно обязательно:
class Car:
def __init__(self, color, model, year):
self.color = color
self.model = model
self.year = year
self._voltage = 12
@property
def voltage(self):
return self._voltage
Python требует написания self в обязательном порядке. Каждый self либо создает, либо обращается к атрибуту. Если мы пропустим его, то Python просто создаст локальную переменную вместо атрибута.
Отличие в том, как мы используем self и this в Python и Java, происходит из-за основных различий между двумя языками и от того, как они именуют переменные и атрибуты.
Методы и функции
Разница между рассматриваемыми языками заключается в том, что в Python есть функции, а в Java их нет.
В Python следующий код отработает без проблем (и используется повсеместно):
>>> def say_hi():
... print("Hi!")
...
>>> say_hi()
Hi!
Мы можем вызвать say_hi() из любого места видимости. Эта функция не содержит ссылки на self, что означает, что это глобальная функция, а не функция класса. Она не сможет изменять или сохранять какие-нибудь данные какого-либо класса, но может использовать локальные и глобальные переменные.
В противоположность, каждая написанная нами строчка на Java принадлежит какому-нибудь классу. Функции не существует за пределами класса, и по определению все Java-функции — это методы. На Java ближе всего к чистой функции находится статичный метод:
public class Utils {
static void SayHi() {
System.out.println("Hi!");
}
}
Utils. SayHi() вызывается из любого места без предварительного создания экземпляра класса Utils. Поскольку мы вызываем SayHi() без создания объекта, ссылки this не существует. Однако, это всё равно не функция в том смысле, в котором является say_hi() в Python.
Наследование и полиморфизм
Наследование и полиморфизм – две фундаментальные концепции в ООП. Благодаря первому, объекты получают (другими словами, наследуют) атрибуты и функциональные возможности других объектов, создавая иерархию от более общих объектов к более конкретным. Например, и класс Car (машина), и класс Boat (лодка) являются конкретными типами класса Vehicle (транспортное средство). Оба объекта наследуют поведение одного родительского объекта или множества родительских объектов. В этом случае их называют дочерними объектами.
Полиморфизм, в свою очередь, — это возможность работы с разными объектами с помощью одной и той же функции или метода.
Обе эти фундаментальные ООП-концепции реализованы в Java и Python совершенно по-разному.
Наследование
Python поддерживает множественное наследование, то есть создание класса более чем от одного родителя.
Чтобы продемонстрировать это, разделим класс Car на две категории: одну – для транспортных средств и одну – для машин, использующих электричество:
class Vehicle:
def __init__(self, color, model):
self.color = color
self.model = model
class Device:
def __init__(self):
self._voltage = 12
class Car(Vehicle, Device):
def __init__(self, color, model, year):
Vehicle.__init__(self, color, model)
Device.__init__(self)
self.year = year
@property
def voltage(self):
return self._voltage
@voltage.setter
def voltage(self, volts):
print("Warning: this can cause problems!")
self._voltage = volts
@voltage.deleter
def voltage(self):
print("Warning: the radio will stop working!")
del self._voltage
В классе Vehicle определены атрибуты color и model. В классе Device имеется атрибут _voltage. Класс Car происходит от этих двух классов, и атрибуты color, model и _voltage теперь являются частью нового класса.
В методе init() класса Car вызываются методы init() обоих родительских классов, чтобы все данные проинициализировались должным образом. После этого мы можем добавить классу Car любую желаемую функциональность. В данном примере мы добавим атрибут year, а также геттер и сеттер для _voltage.
Функциональность нового класса Car осталась прежней. Мы можем создавать и использовать объекты класса, как это делали несколькими примерами ранее:
>>> from car import *
>>> my_car = Car("yellow", "beetle", 1969)
>>> print(f"My car is {my_car.color}")
My car is yellow
>>> print(f"My car uses {my_car.voltage} volts")
My car uses 12 volts
>>> my_car.voltage = 6
Warning: this can cause problems!
>>> print(f"My car now uses {my_car.voltage} volts")
My car now uses 6 volts
Язык Java же, в свою очередь, поддерживает только одиночное наследование, что означает, что классы в Java могут наследовать данные и поведение только от одного родительского класса. Зато в Java возможно наследование от множества интерфейсов. Интерфейсы обеспечивают группу связанных методов, которые нужно реализовать, позволяя дочерним классам вести себя сходным образом.
Чтобы увидеть это, разделим Java-класс Car на родительский класс и интерфейс:
public class Vehicle {
private String color;
private String model;
public Vehicle(String color, String model) {
this.color = color;
this.model = model;
}
public String getColor() {
return color;
}
public String getModel() {
return model;
}
}
public interface Device {
int getVoltage();
}
public class Car extends Vehicle implements Device {
private int voltage;
private int year;
public Car(String color, String model, int year) {
super(color, model);
this.year = year;
this.voltage = 12;
}
@Override
public int getVoltage() {
return voltage;
}
public int getYear() {
return year;
}
}
Не забываем, что каждый класс и каждый интерфейс в Java должны быть размещены в своем собственном файле.
Как и в вышеприведенном примере с Python, мы создаем новый класс Vehicle для хранения общих данных и функционала, присущих транспортным средствам. Однако для добавления функциональных возможностей Device нам нужно создать интерфейс, определяющий метод получения напряжения (voltage) устройства.
Класс Car создается путем наследования от класса Vehicle с использованием ключевого слова extends и реализации интерфейса Device с использованием ключевого слова implements. В конструкторе класса мы вызываем конструктор родителя при помощи super(). Поскольку родительский класс только один, мы обращаемся к конструктору класса Vehicle. Для реализации интерфейса переопределяем getVoltage() с помощью аннотации Override.
Вместо повторного использования кода из Device, как это делается в Python, Java требует, чтобы мы реализовывали один и тот же функционал в каждом классе, который реализует интерфейс. Интерфейсы всего лишь определяют методы — они не могут определять данные экземпляра класса или детали реализации.
Так почему же это происходит с Java? Причина кроется в типах данных и строгой проверке типов.
Типы данных и полиморфизм
Каждый класс и каждый интерфейс в Java имеет тип. Следовательно, если два Java-объекта реализуют один и тот же интерфейс, считается, что они имеют один и тот же тип по отношению к этому интерфейсу. С помощью этого механизма можно взаимозаменяемо использовать различные классы, в чем и заключается полиморфизм.
Реализуем зарядку устройства для наших Java-объектов при помощи создания метода charge(), который принимает в качестве параметра переменную типа Device. Любой объект, реализующий интерфейс Device, может быть передан методу charge().
Создадим следующий класс в файле под названием Rhino.java:
public class Rhino {
}
Теперь создадим файл Main.java с методом charge() и посмотрим, чем отличаются объекты классов Car и Rhino.
public class Main{
public static void charge(Device device) {
device.getVoltage();
}
public static void main(String[] args) throws Exception {
Car car = new Car("yellow", "beetle", 1969);
Rhino rhino = new Rhino();
charge(car);
charge(rhino);
}
}
Вот что мы получим, когда попытаемся скомпилировать код:
Information:2019-02-02 15:20 - Compilation completed with
1 error and 0 warnings in 4 s 395 ms
Main.java
Error:(43, 11) java: incompatible types: Rhino cannot be converted to Device
Поскольку в классе Rhino не реализован интерфейс Device, его нельзя передать в качестве параметра в charge().
В отличие от строгой типизации переменных, принятой в Java, в Python используется концепция утиной типизации, которая в общем виде звучит так: если переменная «ходит как утка и крякает как утка, то это и есть утка» (на самом деле звучит немного иначе: "если нечто выглядит как утка, плавает как утка и крякает как утка, то это, вероятно, и есть утка" – прим. переводчика). Вместо идентификации объектов по типу, Python проверяет их поведение.
Лучше понять утиную типизацию поможет следующий аналогичный пример зарядки устройства на Python:
>>> def charge(device):
... if hasattr(device, '_voltage'):
... print(f"Charging a {device._voltage} volt device")
... else:
... print(f"I can't charge a {device.__class__.__name__}")
...
>>> class Phone(Device):
... pass
...
>>> class Rhino:
... pass
...
>>> my_car = Car("yellow", "Beetle", "1966")
>>> my_phone = Phone()
>>> my_rhino = Rhino()
>>> charge(my_car)
Charging a 12 volt device
>>> charge(my_phone)
Charging a 12 volt device
>>> charge(my_rhino)
I can't charge a Rhino
charge() проверяет существование в объекте атрибута _voltage. Поскольку в классе Device имеется такой атрибут, то и в любом его классе-наследнике (Car и Phone) тоже будет этот атрибут, и, следовательно, этот класс выведет сообщение о зарядке. У классов, которые не унаследовались от Device (как Rhino), не будет этого атрибута, и они не будут заряжаться, что хорошо, поскольку для жизни носорога (rhino) электрическая зарядка смертельно опасна.
Дефолтные методы
Все классы в Java имеют своим предком класс Object, который содержит определенный набор методов и передает их своим потомкам. Потомки могут эти методы либо переопределять, либо использовать по умолчанию. Класс Object включает в себя следующие методы:
class Object {
boolean equals(Object obj) { ... }
int hashCode() { ... }
String toString() { ... }
}
По умолчанию equals() сравнивает адреса в памяти текущего объекта с объектом, переданным в качестве параметра, hashCode() вычисляет уникальный идентификатор, который так же использует адрес в памяти текущего объекта. Эти методы активно используются в Java в различных контекстах. Например, коллекциям, которые сортируют объекты на основе их значений, нужны оба этих метода.
toString() возвращает строковое представление объекта. По умолчанию это имя класса и адрес в памяти. Этот метод вызывается автоматически, когда объект передается в качестве параметра в метод, требующий строковый аргумент, например, System.out.println():
Car car = new Car("yellow", "Beetle", 1969);
System.out.println(car);
Запустим этот код и увидим дефолтное строковое представление объекта car:
Car@61bbe9ba
Не очень информативно, не правда ли? Давайте усовершенствуем вывод, переопределив метод toString(). Добавим следующий метод в класс Car:
public String toString() {
return "Car: " + getColor() + " : " + getModel() + " : " + getYear();
}
Теперь, запустив предыдущий пример, увидим следующее:
Car: yellow : Beetle : 1969
В Python подобный функционал обеспечивается набором так называемых магических методов (dunder — аббревиатура для double underscore). Каждый Python-класс наследует эти методы, и мы можем, переопределив их, изменить их поведение.
В Python для строкового представления объекта имеется два метода: repr() и str(). Однозначное представление объекта возвращается методом repr(), в то время как str() возвращает его в удобочитаемом виде. Это примерно как hashcode() и toString() в Java.
Как и в Java, в Python имеется дефолтная реализация магических методов:
>>> my_car = Car("yellow", "Beetle", "1966")
>>> print(repr(my_car))
<car.Car object at 0x7fe4ca154f98>
>>> print(str(my_car))
<car.Car object at 0x7fe4ca154f98>
Чтобы улучшить читаемость, переопределим метод str() в Python-классе Car:
def __str__(self):
return f'Car {self.color} : {self.model} : {self.year}'
Результат будет выглядеть намного приятнее:
>>> my_car = Car("yellow", "Beetle", "1966")
>>> print(repr(my_car))
<car.Car object at 0x7f09e9a7b630>
>>> print(str(my_car))
Car yellow : Beetle : 1966
Переопределение магического метода дало нам более читаемое представление объекта. Можно также переопределить метод repr(), это полезно для отладки.
Python предоставляет нам гораздо большее количество магических методов, переопределяя которые, можно изменить поведение объекта во время итерации, операций сравнения и сложения или непосредственного вызова объекта.
Перегрузка операторов
Перегрузка операторов в Python означает возможность в классах переопределять различные операторы языка. Магические методы Python позволяют реализовать перегрузку операторов, чего Java не предлагает вообще.
Изменим наш Python-класс Car следующим образом:
class Car:
def __init__(self, color, model, year):
self.color = color
self.model = model
self.year = year
def __str__(self):
return f'Car {self.color} : {self.model} : {self.year}'
def __eq__(self, other):
return self.year == other.year
def __lt__(self, other):
return self.year < other.year
def __add__(self, other):
return Car(self.color + other.color,
self.model + other.model,
int(self.year) + int(other.year))
Данная таблица показывает связи между этими магическими методами и операторами, которые они представляют:
Магический метод | Оператор | Смысл |
---|---|---|
eq | == | В один ли год выпущены объекты Car? |
lt | < | Какой из объектов Car более раннего выпуска? |
add | + | Добавить два объекта Car без особого смысла |
Когда Python видит выражение, содержащее объекты, он вызывает магический метод, соответствующий операторам в выражении.
В нижеуказанном коде используются новые перегруженные арифметические функции над двумя объектами класса Car:
>>> my_car = Car("yellow", "Beetle", "1966")
>>> your_car = Car("red", "Corvette", "1967")
>>> print (my_car < your_car)
True
>>> print (my_car > your_car)
False
>>> print (my_car == your_car)
False
>>> print (my_car + your_car)
Car yellowred : BeetleCorvette : 3933
Существует гораздо большее количество операторов, которые можно перегрузить при помощи магии, что позволяет разнообразить поведение объекта так, как это не делают базовые дефолтные методы Java.
Рефлексия
Рефлексия – это изучение объекта или класса внутри самого объекта или класса. И Java, и Python предоставляют способы исследования атрибутов и методов в классе.
Изучение типа объекта
В обоих рассматриваемых языках имеются способы проверить тип объекта. В Python мы используем type()для отображения типа переменной и isinstance () для определения, является ли данная переменная экземпляром или потомком определенного класса:
>>> my_car = Car("yellow", "Beetle", "1966")
>>> print(type(my_car))
<class 'car.Car'>
>>> print(isinstance(my_car, Car))
True
>>> print(isinstance(my_car, Device))
True
В Java мы вызываем метод getClass() для определения типа и используем instanceof для проверки на принадлежность классу:
Car car = new Car("yellow", "beetle", 1969);
System.out.println(car.getClass());
System.out.println(car instanceof Car);
Получаем следующее:
class com.realpython.Car
true
Изучение атрибутов объекта
В Python при помощи dir() мы видим все атрибуты и функции, содержащиеся в объекте (включая магические методы). Чтобы получить конкретные сведения о данном атрибуте или функции, используем getattr():
>>> print(dir(my_car))
['_Car__cupholders', '__add__', '__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__',
'_voltage', 'color', 'model', 'voltage', 'wheels', 'year']
>>> print(getattr(my_car, "__format__"))
<built-in method __format__ of Car object at 0x7fb4c10f5438>
В Java имеются аналогичные возможности, однако контроль доступа и типобезопасность, заложенные в языке, усложняют дело.
getFields() извлекает список всех общедоступных атрибутов. Однако, поскольку ни один из атрибутов класса Car не является публичным, этот код возвращает пустой массив:
Field[] fields = car.getClass().getFields();
Java рассматривает атрибуты и методы как отдельные сущности, поэтому публичные методы извлекаются при помощи getDeclaredMethods(). Поскольку публичные атрибуты будут иметь соответствующий get-метод, один из способов обнаружить, что класс содержит определенное свойство, может выглядеть таким образом:
1) использовать getDeclaredMethods() для генерации массива всех методов
2) перебрать все эти методы:
- для каждого обнаруженного метода вернуть true, если метод:
-
- начинается со слова get или принимает ноль аргументов;
-
- и не возвращает void;
-
- и включает в себя название свойства;
- в противном случае вернуть false.
Вот пример на скорую руку:
1 public static boolean getProperty(String name, Object object) throws Exception {
2
3 Method[] declaredMethods = object.getClass().getDeclaredMethods();
4 for (Method method : declaredMethods) {
5 if (isGetter(method) &&
6 method.getName().toUpperCase().contains(name.toUpperCase())) {
7 return true;
8 }
9 }
10 return false;
11 }
12
13 // Helper function to get if the method is a getter method
14 public static boolean isGetter(Method method) {
15 if ((method.getName().startsWith("get") ||
16 method.getParameterCount() == 0 ) &&
17 !method.getReturnType().equals(void.class)) {
18 return true;
19 }
20 return false;
21 }
getProperty() – это точка входа. Вызовем ее с именем атрибута и объекта. Она вернет true, если свойство будет найдено, иначе вернет false.
Вызов методов через рефлексию
И в Java, и в Python имеются механизмы для вызова методов через рефлексию.
В вышеприведенном Java-примере вместо возвращения значения true в случае, если свойство найдено, можно было вызвать метод напрямую. Вспомним, что getDeclaredMethods() возвращает массив объектов типа Method. Объект Method сам содержит метод invoke(), который вызывает Method. В строке 7 вместо возвращения значения true, когда найден метод, можно вернуть method.invoke(object).
Эта возможность существует также и в Python. Однако, поскольку Python не делает различий между функциями и атрибутами, нужно специально искать сущности, которые можно вызвать:
>>> for method_name in dir(my_car):
... if callable(getattr(my_car, method_name)):
... print(method_name)
...
__add__
__class__
__delattr__
__dir__
__eq__
__format__
__ge__
__getattribute__
__gt__
__init__
__init_subclass__
__le__
__lt__
__ne__
__new__
__reduce__
__reduce_ex__
__repr__
__setattr__
__sizeof__
__str__
__subclasshook__
Методы Python проще в управлении и вызове, чем в Java. Нижеприведенный код найдет метод объекта str() и вызовет его через рефлексию:
>>> for method_name in dir(my_car):
... attr = getattr(my_car, method_name)
... if callable(attr):
... if method_name == '__str__':
... print(attr())
...
Car yellow : Beetle : 1966
В данном примере проверяется каждый атрибут, возвращаемый функцией dir(). Мы получаем значение атрибута объекта, используя getattr(), и проверяем при помощи callable(), является ли оно вызываемой функцией. Если это так, то можно проверить, является ли его имя str (), и затем вызвать его.
Автор: YernarShambayev