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

Оглавление

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

В этой статье сравнивается и противопоставляется поддержка объектно-ориентированного программирования в Python и Java. К концу статьи вы сможете применить свои знания об объектно-ориентированном программировании в Python, понять, как переосмыслить свое понимание объектов Java в Python, и использовать объекты по-питоновски.

В ходе этой статьи вы узнаете:

  • Создайте базовый класс на Java и Python
  • Исследуйте, как работают атрибуты объектов в Python и Java
  • Сравните и противопоставьте методы Java и функции Python
  • .
  • Раскройте механизмы наследования и полиморфизма в обоих языках
  • Исследуйте отражение в Python и Java
  • Примените все в полной реализации класса на обоих языках

Эта статья не является учебником по объектно-ориентированному программированию. Скорее, в ней сравниваются объектно-ориентированные возможности и принципы Python и Java. Читатели должны хорошо знать Java, а также быть знакомы с кодированием на Python. Если вы не знакомы с объектно-ориентированным программированием, то ознакомьтесь с Intro to Object-Oriented Programming (OOP) in Python. Все примеры на Python будут работать с Python 3.6 или более поздней версией.

Скачать пример кода: Нажмите здесь, чтобы скачать прокомментированные примеры определений объектов и исходный код для объектов Java и Python в этой статье.

Примеры классов в Python и Java

Для начала вы реализуете один и тот же небольшой класс на Python и Java, чтобы проиллюстрировать различия между ними. По ходу статьи вы будете вносить в них изменения.

Сначала предположим, что у вас есть следующее Car определение класса в Java:

 1public 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. В каждом файле может быть определен только один класс.

Подобный небольшой Car класс написан на языке Python следующим образом:

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

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

Используя эти классы в качестве основы, вы можете изучить основные компоненты классов и объектов.

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

Все объектно-ориентированные языки имеют определенный способ хранения данных об объекте. В Java и Python данные хранятся в атрибутах, которые представляют собой переменные, связанные с конкретными объектами.

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

Декларация и инициализация

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

 1public class Car {
 2    private String color;
 3    private String model;
 4    private int year;

В Python вы и объявляете, и определяете атрибуты внутри класса __init__(), что эквивалентно конструктору Java:

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

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

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

 1>>> import car
 2>>> my_car = car.Car("yellow", "beetle", 1967)
 3>>> print(f"My car is {my_car.color}")
 4My car is yellow
 5
 6>>> my_car.wheels = 5
 7>>> print(f"Wheels: {my_car.wheels}")
 8Wheels: 5

Однако если вы забудете my_car.wheels = 5 в строке 6, то Python выдаст ошибку:

 1>>> import car
 2>>> my_car = car.Car("yellow", "beetle", 1967)
 3>>> print(f"My car is {my_car.color}")
 4My car is yellow
 5
 6>>> print(f"Wheels: {my_car.wheels}")
 7Traceback (most recent call last):
 8  File "<stdin>", line 1, in <module>
 9AttributeError: 'Car' object has no attribute 'wheels'

В Python, когда вы объявляете переменную вне метода, она рассматривается как переменная класса. Обновите класс Car следующим образом:

 1class 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}")
 4My car is yellow
 5
 6>>> print(f"It has {car.Car.wheels} wheels")
 7It has 0 wheels
 8
 9>>> print(f"It has {my_car.wheels} wheels")
10It has 0 wheels

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

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

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

Вы можете ссылаться на my_car.wheels или car.Car.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}")
 6My car is yellow
 7>>> print(f"It has {my_car.wheels} wheels")
 8It has 0 wheels
 9
10>>> print(f"My other car is {my_other_car.color}")
11My other car is red
12>>> print(f"It has {my_other_car.wheels} wheels")
13It 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")
19My car has 4 wheels
20>>> print(f"My other car has {my_other_car.wheels} wheels")
21My 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")
27My car has 5 wheels
28>>> print(f"My other car has {my_other_car.wheels} wheels")
29My other car has 4 wheels

Вы определяете два Car объекта на строках 2 и 3:

  1. my_car
  2. my_other_car

Сначала у обоих объектов нулевые колеса. Когда вы устанавливаете переменную класса с помощью car.Car.wheels = 4 в строке 16, у обоих объектов теперь по четыре колеса. Однако, когда вы устанавливаете переменную экземпляра с помощью my_car.wheels = 5 в строке 24, это влияет только на этот объект.

Это означает, что теперь существует две различные копии атрибута 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;
    }
}

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

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

Общественные и частные

Java контролирует доступ к методам и атрибутам, проводя различие между публичными данными и приватными данными.

В Java предполагается, что атрибуты объявляются как private или protected, если подклассам необходим прямой доступ к ним. Это ограничивает доступ к этим атрибутам из кода за пределами класса. Чтобы обеспечить доступ к атрибутам private, вы объявляете методы public, которые устанавливают и извлекают данные контролируемым образом (подробнее об этом позже).

Напомните из вашего Java-класса выше, что переменная color была объявлена как private. Поэтому этот Java-код выдаст ошибку компиляции в выделенной строке:

Car myCar = new Car("blue", "Ford", 1972);

// Paint the car
myCar.color = "red";

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

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

В Python нет такого понятия private или protected данных, как в Java. В Python все является public. Этот код прекрасно работает с вашим существующим классом Python:

>>> my_car = car.Car("blue", "Ford", 1972)

>>> # Paint the car
... my_car.color = "red"

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

Добавьте следующую строку в ваш класс Python 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 позволяет вам получить доступ к ._cupholders, но такие IDE, как VS Code, могут выдать предупреждение при использовании линтеров, поддерживающих PEP 8. Подробнее о PEP 8 читайте в статье How to Write Beautiful Python Code With PEP 8.

Вот код в VS Code, с выделенным и отображенным предупреждением:

Linter highlighting a PEP8 issue in Python.

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

Чтобы показать этот механизм в действии, изменим класс Python 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.")
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

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

Итак, если атрибуты Java объявляются private, а атрибуты Python предваряются двойными подчеркиваниями, то как обеспечить и контролировать доступ к хранимым в них данным?

Контроль доступа

В Java доступ к атрибутам private осуществляется с помощью setters и getters. Чтобы позволить пользователям раскрашивать свои автомобили, добавьте в свой Java-класс следующий код:

public String getColor() {
    return color;
}

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

Поскольку

.getColor() и .setColor() являются public, любой может вызвать их, чтобы изменить или получить цвет автомобиля. Лучшая практика Java по использованию атрибутов private, доступ к которым осуществляется с помощью public getters и setters, является одной из причин, почему код Java, как правило, более многословен, чем код Python.

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

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

Вы можете увидеть, как работают свойства, добавив одно из них в свой Car класс:

 1class 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 и включаете в него электромобили. Вы объявляете атрибут ._voltage для хранения напряжения батареи в строке 6.

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

Аналогично, в строках с 13 по 15 вы определяете функцию-сеттер, которая также называется voltage(). Однако вы украшаете эту функцию с помощью @voltage.setter. Наконец, вы используете @voltage.deleter для оформления третьей voltage() в строках с 18 по 20, которая позволяет контролируемо удалять атрибут.

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

 1>>> from car import *
 2>>> my_car = Car("yellow", "beetle", 1969)
 3
 4>>> print(f"My car uses {my_car.voltage} volts")
 5My car uses 12 volts
 6
 7>>> my_car.voltage = 6
 8Warning: this can cause problems!
 9
10>>> print(f"My car now uses {my_car.voltage} volts")
11My car now uses 6 volts
12
13>>> del my_car.voltage
14Warning: the radio will stop working!

Обратите внимание, что в выделенных строках выше вы используете .voltage, а не ._voltage. Это указывает Python использовать функции свойств, которые вы определили:

  • Когда вы печатаете значение my_car.voltage в строке 4, Python вызывает .voltage(), украшенное @property.
  • Когда вы присваиваете значение my_car.voltage в строке 7, Python вызывает .voltage(), оформленный как @voltage.setter.
  • Когда вы удаляете my_car.voltage в строке 13, Python вызывает .voltage(), украшенный @voltage.deleter.

Украшения @property, @.setter и @.deleter позволяют контролировать доступ к атрибутам, не требуя от пользователей использования различных методов. Вы даже можете сделать атрибуты свойствами, доступными только для чтения, опустив декорированные функции @.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 служит аналогичной цели. С его помощью вы ссылаетесь на переменные-члены, но, в отличие от this в 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 в приведенном выше коде. Каждый из них либо создает, либо ссылается на атрибуты. Если вы их опустите, то Python создаст локальную переменную вместо атрибута.

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

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

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

Полиморфизм позволяет двум или более объектам вести себя подобно друг другу, что позволяет использовать их взаимозаменяемо. Например, если метод или функция умеет рисовать объект Vehicle, то она также может рисовать объект Car или Boat, поскольку они наследуют свои данные и поведение от Vehicle.

Эти фундаментальные концепции ООП реализованы в Python и Java совершенно по-разному.

Наследство

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 имел эти три атрибута, его можно переопределить так, чтобы он наследовал оба класса Vehicle и Device. Атрибуты color, model и _voltage станут частью нового класса Car.

В .__init__() для Car вы вызываете методы .__init__() для обоих родительских классов, чтобы убедиться, что все инициализировано правильно. После этого вы можете добавить в Car любую другую функциональность. В данном случае вы добавляете атрибут .year, специфичный для объектов Car, а также методы getter и setter для .voltage.

Функционально новый класс Car ведет себя так же, как и всегда. Вы создаете и используете объекты 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 на родительский класс и класс interface:

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

Помните, что каждый class и interface должен жить в своем собственном файле.

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

Переопределение класса Car требует наследования от Vehicle с помощью extend и реализации интерфейса Device с помощью implements. В конструкторе вы вызываете конструктор родительского класса с помощью встроенного super(). Поскольку существует только один родительский класс, он может ссылаться только на конструктор Vehicle. Чтобы реализовать interface, вы пишете getVoltage() с использованием аннотации @Override.

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

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

Типы и полиморфизм

Строгая проверка типов в Java - это то, что определяет ее interface дизайн.

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

Вы можете реализовать зарядку устройства для своих Java-объектов, создав .charge(), который принимает Device для зарядки. Любой объект, реализующий интерфейс Device, может быть передан в .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().

В отличие от строгой типизации переменных в Java, в Python используется концепция duck typing, которая в базовых терминах означает, что если переменная "ходит как утка и крякает как утка, то это утка". Вместо того чтобы определять объекты по типу, Python рассматривает их поведение. Вы можете узнать больше о системе типов Python и утиной типизации в The Ultimate Guide to Python Type Checking.

Вы можете изучить утиную типизацию, реализовав аналогичные возможности зарядки устройств в своем классе Python Device:

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

Методы по умолчанию

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

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

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

toString() возвращает String представление Object. По умолчанию это имя класса и адрес. Этот метод вызывается автоматически, когда Object передается в метод, требующий аргумента String, например System.out.println():

Car car = new Car("yellow", "Beetle", 1969);
System.out.println(car);

При выполнении этого кода по умолчанию .toString() отображается объект car:

Car@61bbe9ba

Не очень полезно, верно? Вы можете улучшить ситуацию, переопределив метод по умолчанию .toString(). Добавьте этот метод в свой класс Java Car:

public String toString() {
    return "Car: " + getColor() + " : " + getModel() + " : " + getYear();
}

Теперь, запустив тот же пример кода, вы увидите следующее:

Car: yellow : Beetle : 1969

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

Для строкового представления объекта Python предоставляет __repr__() и __str__(), о которых вы можете узнать в Pythonic OOP String Conversion: __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

Переопределение метода dunder дало нам более читабельное представление вашего Car. Возможно, вы захотите переопределить и метод .__repr__(), так как он часто бывает полезен для отладки.

Python предлагает гораздо больше dunder-методов. Используя методы dunder, вы можете определить поведение вашего объекта при итерации, сравнении, сложении или сделать объект вызываемым напрямую, а также многое другое.

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

Перегрузка операторов - это переопределение того, как работают операторы Python при работе с пользовательскими объектами. Методы dunder в Python позволяют реализовать перегрузку операторов, чего Java не предлагает вообще.

Модифицируйте свой класс Python Car, добавив в него следующие дополнительные методы dunder:

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

В таблице ниже показана связь между этими методами dunder и операторами Python, которые они представляют:

Dunder Method Operator Purpose
__eq__ == Do these Car objects have the same year?
__lt__ < Which Car is an earlier model?
__add__ + Add two Car objects in a nonsensical way

Когда Python видит выражение, содержащее объекты, он вызывает все определенные методы dunder, соответствующие операторам в выражении. Приведенный ниже код использует эти новые перегруженные арифметические операторы для пары объектов 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 

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

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

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

  • Используйте .getDeclaredMethods() для создания массива всех методов.
  • Пройдитесь по всем найденным методам:
    • Для каждого найденного метода верните true, если метод:
      • начинается со слова get ИЛИ принимает нулевые аргументы
      • AND не возвращает void
      • AND включает имя свойства
    • В противном случае возвращается false.

Вот быстрый и практичный пример:

 1public 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
14public 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. Вместо того чтобы возвращать true, когда правильный метод найден в строке 7 выше, можно вернуть method.invoke(object).

Такая возможность существует и в Python. Однако, поскольку Python не делает различий между функциями и атрибутами, вам придется специально искать записи, которые callable:

>>> 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__(), и затем вызываете ее.

Заключение

Из этой статьи вы узнали, чем отличаются принципы объектно-ориентированного подхода в Python и Java. По мере чтения вы:

  • Создайте базовый класс на Java и Python
  • Исследовали, как работают атрибуты объектов в Python и Java
  • Сравнение и противопоставление методов Java и функций Python
  • Открыли механизмы наследования и полиморфизма в обоих языках
  • Исследовали рефлексию в Python и Java
  • Примените все в полной реализации класса на обоих языках

Если вы хотите узнать больше об ООП в Python, обязательно прочитайте Объектно-ориентированное программирование (ООП) в Python 3.

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

Для того чтобы сравнить несколько конкретных примеров, вы можете щелкнуть на поле ниже, чтобы загрузить наш пример кода и получить полные прокомментированные определения объектов для классов Java Car, Device и Vehicle, а также полные прокомментированные определения для классов Python Car и Vehicle:

Back to Top