Хабрахабр

[Из песочницы] Объектно-ориентированное программирование в Java и Python: сходства и отличия

Представляю вашему вниманию перевод статьи “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-файл может содержать только один публичный класс.
Такой же класс в Python будет выглядеть так: Имя исходного Java-файла должно соответствовать имени хранящегося в нем класса, поэтому мы обязаны назвать файл Car.java.

1 class Car: 2 def __init__(self, color, model, year): 3 self.color = color 4 self.model = model 5 self.year = year

Сохраним этот файл как car.py.
Используя эти классы как основу, продолжим исследование основных компонентов классов и объектов. В Python вы можете объявить класс где угодно и когда угодно.

Атрибуты объекта

И в 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 слабо типизированы, и атрибуты не являются исключением.

Например, можно добавить объекту Car новый атрибут wheels следующим образом: Переменные можно создать и за пределами метода init(), но это не будет лучшим решением и может привести к труднообнаруживаемым багам.

1 >>> import car 2 >>> my_car = car.Car("yellow", "beetle", 1967) 3 >>> print(f"My car is ") 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'

Давайте изменим класс Car: В Python если объявить переменную за пределами метода, то она будет рассматриваться как переменная класса.

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 обращение к переменной класса происходит по следующему синтаксису:

  1. Имя файла, содержащего класс (без расширения .py)
  2. Точка
  3. Имя класса
  4. Точка
  5. Имя переменной

Car.wheels. Поскольку мы сохранили класс Car в файле car.py, мы обращаемся к переменной класса wheels в 6-й строчке таким образом: car.

Car.wheels: Работая с переменной wheels, необходимо быть обратить внимание на то, что изменение значения переменной экземпляра класса my_car.wheels не ведет к изменению переменной класса car.

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

На 16-й строке мы установили переменную класса: car. На 2-й и 3-й строчках мы определили два объекта Car: my_car и my_other_car.
Сначала свойство wheels у обоих объектов равно нулю. Однако, затем когда на 24-й строке мы меняем свойство объекта my_car.wheels = 5, свойство второго объекта остается нетронутым. Car.wheels = 4, у обоих объектов теперь по 4 колеса.

Это означает, что теперь у нас две различные копии атрибута wheels:

  1. Переменная класса, которая применяется ко всем объектам Car
  2. Конкретная переменная экземпляра класса, которая применяется только к объекту 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; }
}

Можно обратиться к ним и через экземпляр класса, как в Python, но это не будет лучшим решением. Обычно мы обращаемся к статичным переменным в Java через имя класса.

Одной из причин, почему Java «многословнее» Python-а, является понятие публичных (public) и приватных (private) методов и атрибутов. Наш Java-класс начинает удлиняться.

Публичные и приватные

Таким образом мы ограничиваем доступ к ним извне. Java управляет доступом к методам и атрибутам, различая публичные и приватные данные.
В Java ожидается, что атрибуты будут объявлены как приватные (или защищенные — protected, если нужно обеспечить к ним доступ потомкам класса). Чтобы предоставить доступ к приватным атрибутам, мы объявляем публичные методы, которые устанавливают или получают эти данные (подробнее об этом – чуть позже).

Следовательно, нижеприведенный код не скомпилируется: Вспомним, что в нашем Java-коде переменная color была объявлена приватной.

Car myCar = new Car("blue", "Ford", 1972); // Paint the car
myCar.color = "red";

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

Рекомендуется объявлять их приватными, а затем использовать публичные методы, наподобие getColor() и getModel(), как и было указано в тексте кода выше. Однако, в Java не приветствуется объявление атрибутов публичными.

В 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'

Когда Python видит атрибут с двойным подчеркиванием в самом начале, он меняет его, добавляя в начало имя класса с подчеркиванием. Так почему же атрибут __cupholders не существует?
Дело вот в чем. Для того чтобы обратиться к атрибуту напрямую, необходимо также изменить имя:

>>> print(f"It has {my_car._Car__cupholders} cupholders")
It has 6 cupholders

Теперь возникает вопрос: если атрибут Java-класса объявлен приватным и атрибуту Python-класса предшествует в имени двойное подчеркивание, то как достучаться до этих данных?

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

Для того чтобы пользователь перекрасил-таки свою машину, добавим следующий кусок кода в Java-класс: В Java мы получаем доступ к приватным атрибутам при помощи сеттеров (setters) и геттеров (getters).

public String getColor() { return color;
} public void setColor(String color) { this.color = color;
}

Использование приватных атрибутов, к которым мы получаем доступ публичными геттерами и сеттерами, — одна из причин большей «многословности» Java в сравнении с Python. Поскольку методы getColor() и setColor() – публичные, то любой пользователь может вызвать их и получить / изменить цвет машины.

Поскольку всё – публичное, мы может достучаться к чему угодно, когда угодно и откуда угодно. Как было показано выше, в 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). Однако бывает и так, что мы хотим контролировать доступ к атрибутам.

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

Работу свойств можно увидеть на следующем примере класса 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

В строке 6 объявляется атрибут _voltage, чтобы хранить в нем напряжение батареи. В данном примере мы расширяем понятие класса Car, включая электромобили.

Используя декоратор @property, мы превращаем его в геттер, к которому теперь любой пользователь получает доступ. В строках 9 и 10 для контролируемого доступа мы создаем функцию voltage() и возвращаем значение приватной переменной.

Однако, мы ее декорируем по-другому: voltage.setter. В строках 13-15 мы определяем функцию, так же носящую название voltage(). Наконец, в строках 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!

Так мы указываем Python-у на то, что следует применять свойства, которые только что определили: Обратите внимание, что мы используем voltage, а не _ voltage.

  • Когда в 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;
}

Мы использовали ключевое слово this в первом примере для того, чтобы различать атрибут и параметр с одинаковым именем color. Поскольку в классе Car есть атрибут под названием 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

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

Отличие в том, как мы используем self и this в Python и Java, происходит из-за основных различий между двумя языками и от того, как они именуют переменные и атрибуты.

Методы и функции

Разница между рассматриваемыми языками заключается в том, что в Python есть функции, а в Java их нет.

В Python следующий код отработает без проблем (и используется повсеместно):

>>> def say_hi():
... print("Hi!")
... >>> say_hi()
Hi!

Эта функция не содержит ссылки на self, что означает, что это глобальная функция, а не функция класса. Мы можем вызвать say_hi() из любого места видимости. Она не сможет изменять или сохранять какие-нибудь данные какого-либо класса, но может использовать локальные и глобальные переменные.

Функции не существует за пределами класса, и по определению все Java-функции — это методы. В противоположность, каждая написанная нами строчка на Java принадлежит какому-нибудь классу. На Java ближе всего к чистой функции находится статичный метод:

public class Utils { static void SayHi() { System.out.println("Hi!"); }
}

SayHi() вызывается из любого места без предварительного создания экземпляра класса Utils. Utils. Однако, это всё равно не функция в том смысле, в котором является say_hi() в Python. Поскольку мы вызываем SayHi() без создания объекта, ссылки this не существует.

Наследование и полиморфизм

Благодаря первому, объекты получают (другими словами, наследуют) атрибуты и функциональные возможности других объектов, создавая иерархию от более общих объектов к более конкретным. Наследование и полиморфизм – две фундаментальные концепции в ООП. Оба объекта наследуют поведение одного родительского объекта или множества родительских объектов. Например, и класс 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

В классе Device имеется атрибут _voltage. В классе Vehicle определены атрибуты color и model. Класс Car происходит от этих двух классов, и атрибуты color, model и _voltage теперь являются частью нового класса.

После этого мы можем добавить классу Car любую желаемую функциональность. В методе init() класса Car вызываются методы init() обоих родительских классов, чтобы все данные проинициализировались должным образом. В данном примере мы добавим атрибут 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 должны быть размещены в своем собственном файле.

Однако для добавления функциональных возможностей Device нам нужно создать интерфейс, определяющий метод получения напряжения (voltage) устройства. Как и в вышеприведенном примере с Python, мы создаем новый класс Vehicle для хранения общих данных и функционала, присущих транспортным средствам.

В конструкторе класса мы вызываем конструктор родителя при помощи super(). Класс Car создается путем наследования от класса Vehicle с использованием ключевого слова extends и реализации интерфейса Device с использованием ключевого слова implements. Для реализации интерфейса переопределяем getVoltage() с помощью аннотации Override. Поскольку родительский класс только один, мы обращаемся к конструктору класса Vehicle.

Интерфейсы всего лишь определяют методы — они не могут определять данные экземпляра класса или детали реализации. Вместо повторного использования кода из Device, как это делается в Python, Java требует, чтобы мы реализовывали один и тот же функционал в каждом классе, который реализует интерфейс.

Причина кроется в типах данных и проверке типов. Так почему же это происходит с Java?

Типы данных и полиморфизм

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

Любой объект, реализующий интерфейс Device, может быть передан методу charge(). Реализуем зарядку устройства для наших Java-объектов при помощи создания метода charge(), который принимает в качестве параметра переменную типа Device.

Создадим следующий класс в файле под названием 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().

переводчика). В отличие от статической типизации (в оригинале — strict variable typing, то есть строгая типизация, но Python тоже относится к языкам со строгой типизацией) переменных, принятой в 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

Поскольку в классе Device имеется такой атрибут, то и в любом его классе-наследнике (Car и Phone) тоже будет этот атрибут, и, следовательно, этот класс выведет сообщение о зарядке. charge() проверяет существование в объекте атрибута _voltage. У классов, которые не унаследовались от Device (как Rhino), не будет этого атрибута, и они не будут заряжаться, что хорошо, поскольку для жизни носорога (rhino) электрическая зарядка смертельно опасна.

Дефолтные методы

Потомки могут эти методы либо переопределять, либо использовать по умолчанию. Все классы в Java имеют своим предком класс Object, который содержит определенный набор методов и передает их своим потомкам. Класс Object включает в себя следующие методы:

class Object { boolean equals(Object obj) { ... } int hashCode() { ... } String toString() { ... } }

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

По умолчанию это имя класса и адрес в памяти. 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-класс наследует эти методы, и мы можем, переопределив их, изменить их поведение. В Python подобный функционал обеспечивается набором так называемых магических методов (dunder — аббревиатура для double underscore).

Однозначное представление объекта возвращается методом repr(), в то время как str() возвращает его в удобочитаемом виде. В Python для строкового представления объекта имеется два метода: 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 позволяют реализовать перегрузку операторов, чего Java не предлагает вообще. Перегрузка операторов в Python означает возможность в классах переопределять различные операторы языка.

Изменим наш 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))

Данная таблица показывает связи между этими магическими методами и операторами, которые они представляют:

Когда 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

Изучение атрибутов объекта

Чтобы получить конкретные сведения о данном атрибуте или функции, используем getattr(): В Python при помощи dir() мы видим все атрибуты и функции, содержащиеся в объекте (включая магические методы).

>>> 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 имеются аналогичные возможности, однако контроль доступа и типобезопасность, заложенные в языке, усложняют дело.

Однако, поскольку ни один из атрибутов класса Car не является публичным, этот код возвращает пустой массив: getFields() извлекает список всех общедоступных атрибутов.

Field[] fields = car.getClass().getFields();

Поскольку публичные атрибуты будут иметь соответствующий get-метод, один из способов обнаружить, что класс содержит определенное свойство, может выглядеть таким образом: Java рассматривает атрибуты и методы как отдельные сущности, поэтому публичные методы извлекаются при помощи getDeclaredMethods().

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.

Вызов методов через рефлексию

Вспомним, что getDeclaredMethods() возвращает массив объектов типа Method. И в Java, и в Python имеются механизмы для вызова методов через рефлексию.
В вышеприведенном Java-примере вместо возвращения значения true в случае, если свойство найдено, можно было вызвать метод напрямую. В строке 7 вместо возвращения значения true, когда найден метод, можно вернуть method.invoke(object). Объект Method сам содержит метод invoke(), который вызывает Method.

Однако, поскольку 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__

Нижеприведенный код найдет метод объекта str() и вызовет его через рефлексию: Методы Python проще в управлении и вызове, чем в Java.

>>> 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

Мы получаем значение атрибута объекта, используя getattr(), и проверяем при помощи callable(), является ли оно вызываемой функцией. В данном примере проверяется каждый атрибут, возвращаемый функцией dir(). Если это так, то можно проверить, является ли его имя str (), и затем вызвать его.

Теги
Показать больше

Похожие статьи

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

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть