Что составляет основу ооп в python
Перейти к содержимому

Что составляет основу ооп в python

  • автор:

Object-Oriented Programming in Python

Mohammad waseem

Objected oriented programming as a discipline has gained a universal following among developers. Python, an in-demand follows an object-oriented programming paradigm. It deals with declaring Python classes and objects which lays the foundation of OOPs concepts. This article will walk you through declaring programming language also python classes, instantiating objects from them along with the four methodologies of OOPs.

In this article, the following aspects will be covered in detail:

  1. Introduction to Object-Oriented Programming in Python
  2. Difference between object and procedural oriented programming
  3. What are Classes and Objects?
  4. Object-Oriented Programming methodologies:
  • Inheritance
  • Polymorphism
  • Encapsulation
  • Abstraction

Let’s get started.

Object-Oriented Programming in Python

Object-Oriented Programming is a way of computer programming using the idea of “ objects “ to represents data and methods. It is also, an approach used for creating neat and reusable code instead of a redundant one. The program is divided into self-contained objects or several mini-programs.

Every individual object represents a different part of the application having its own logic and data to communicate within themselves.

Now, to get a more clear picture of why we use oops instead of pop, I have listed down the differences below.

Difference between Object-Oriented and Procedural Oriented Programming

That was all about the differences, moving ahead let’s get an idea of classes and objects.

What are Classes and Objects?

A class is a collection of objects or you can say it is a blueprint of objects defining the common attributes and behavior. Now the question arises, how do you do that?

Well, it logically groups the data in such a way that code reusability becomes easy. I can give you a real-life example- think of an office going ‘employee’ as a class and all the attributes related to it like ‘emp_name’, ‘emp_age’, ‘emp_salary’, ‘emp_id’ as the objects in Python. Let us see from the coding perspective that how do you instantiate a class and an object.

Class is defined under a “Class” Keyword.
Example:

Note: Python is not case-sensitive.

Objects:

Objects are an instance of a class. It is an entity that has state and behavior. In a nutshell, it is an instance of a class that can access the data.

Syntax: obj = class1()

Here obj is the “object “ of class1.

Creating an Object and Class in python:

Example:

Explanation: ‘emp1′ and ‘emp2′ are the objects that are instantiated against the class ‘employee’.Here, the word (__dict__) is a “dictionary” which prints all the values of object ‘emp1’ against the given parameter (name, age, salary). (__init__) acts like a constructor that is invoked whenever an object is created.

I hope now you guys won’t face any problem while dealing with ‘classes’ and ‘objects’ in the future.

With this, let me take you through a ride of Object-Oriented Programming methodologies:

Object-Oriented Programming methodologies:

Object-Oriented Programming methodologies deal with the following concepts.

  • Inheritance
  • Polymorphism
  • Encapsulation
  • Abstraction

Let us understand the first concept of inheritance in detail.

Inheritance:

Ever heard of this dialogue from relatives “you look exactly like your father/mother” the reason behind this is called ‘ inheritance’. From the Programming aspect, It generally means “inheriting or transfer of characteristics from parent to child class without any modification”. The new class is called the derived/child class and the one from which it is derived is called a parent/base class.

Let us understand each of the subtopics in detail.

Single Inheritance:

Single level inheritance enables a derived class to inherit characteristics from a single parent class.

Example:

Output: 22

Explanation:

  • I am taking the parent class and created a constructor (__init__), the class itself is initializing the attributes with parameters(‘name’, ‘age’ and ‘salary’).
  • Created a child class ‘childemployee’ which is inheriting the properties from a parent class and finally instantiated objects ‘emp1′ and ‘emp2′ against the parameters.
  • Finally, I have printed the age of emp1. Well, you can do a hell lot of things like print the whole dictionary or name or salary.

Multilevel Inheritance:

Multi-level inheritance enables a derived class to inherit properties from an immediate parent class which in turn inherits properties from his parent class.

Example:

Output: 22,23

Explanation:

  • It is clearly explained in the code written above, Here I have defined the superclass as employee and child class as childemployee1. Now, childemployee1 acts as a parent for childemployee2.
  • I have instantiated two objects ‘emp1′ and ‘emp2′ where I am passing the parameters “name”, “age”, “salary” for emp1 from superclass “employee” and “name”, “age, “salary” and “id” from the parent class “childemployee1”

Hierarchical Inheritance:

Hierarchical level inheritance enables more than one derived class to inherit properties from a parent class.

Example:

Output: 22,23

Explanation:

  • In the above example, you can clearly see there are two child class “childemployee1” and “childemployee2”. They are inheriting functionalities from a common parent class that is “employee”.
  • Objects ‘emp1′ and ‘emp2′ are instantiated against the parameters ‘name’, ‘age’, ‘salary’.

Multiple Inheritance:

Multiple level inheritance enables one derived class to inherit properties from more than one base class.

Example:

Output: 22,1234

Explanation: In the above example, I have taken a two-parent class “employee1” and “employee2”. And a child class “childemployee”, which is inheriting both parent class by instantiating the objects ‘emp1′ and ‘emp2′ against the parameters of parent classes.

This was all about inheritance, moving ahead in Object-Oriented Programming Python, let’s take a deep dive in ‘polymorphism’.

Polymorphism

You all must have used GPS for navigating the route, Isn’t it amazing how many different routes you come across for the same destination depending on the traffic, from a programming point of view this is called ‘polymorphism’. It is one such OOP methodology where one task can be performed in several different ways. To put it in simple words, it is a property of an object which allows it to take multiple forms.

Polymorphism is of two types:

  • Compile-time Polymorphism
  • Run-time Polymorphism

Compile-time Polymorphism:

A compile-time polymorphism also called static polymorphism which gets resolved during the compilation time of the program. One common example is “method overloading”. Let me show you a quick example of the same.

Example:

Output:

Harshit is his name
3000 is his salary
22 is his age
Rahul is his name
4000 is his salary
23 is his age

Explanation:

  • In the above program, I have created two classes ‘employee1′ and ‘employee2′ and created functions for both ‘name’, ‘salary’ and ‘age’ and printed the value of the same without taking it from the user.
  • Now, welcome to the main part where I have created a function with ‘obj’ as the parameter and calling all the three functions i.e. ‘name’, ‘age’, and ‘salary’.
  • Later, instantiated objects emp_1 and emp_2 against the two classes and simply called the function. Such type is called method overloading which allows a class to have more than one method under the same name.

Run-time Polymorphism:

A run-time Polymorphism is also, called dynamic polymorphism where it gets resolved into the run time. One common example of Run-time polymorphism is “method overriding”. Let me show you through an example for a better understanding.

Example:

Output: no money, has money

Explanation: In the above example, I have created two classes ‘childemployee1’ and ‘childemployee2’ which are derived from the same base class ‘employee’.Here’s the catch one did not receive money whereas the other one gets. Now the real question is how did this happen? Well, here if you look closely I created an empty function and used Pass ( a statement which is used when you do not want to execute any command or code). Now, Under the two derived classes, I used the same empty function and made use of the print statement as ‘no money’ and ‘has money’.Lastly, I created two objects and called the function.

Moving on to the next Object-Oriented Programming Python methodology, I’ll talk about encapsulation.

Encapsulation:

In a raw form, encapsulation basically means binding up of data in a single class. Python does not have any private keyword, unlike Java. A class shouldn’t be directly accessed but be prefixed in an underscore.

Let me show you an example for a better understanding.

Example:

Output:

1234
Traceback (most recent call last):
1234
File “C:/Users/Harshit_Kant/PycharmProjects/test1/venv/encapsu.py”, line 10, in
print(object1.__salary)
AttributeError: ‘employee’ object has no attribute ‘__salary’

Explanation: You will get this question what is the underscore and error? Well, python class treats the private variables as(__salary) which can not be accessed directly.

So, I have made use of the setter method which provides indirect access to them in my next example.

Example:

Output:

earning is:1000000,earning is:1000000,earning is:10000

Explanation: Making Use of the setter method provides indirect access to the private class method. Here I have defined a class employee and used a (__maxearn) which is the setter method used here to store the maximum earning of the employee, and a setter function setmaxearn() which is taking price as the parameter.

This is a clear example of encapsulation where we are restricting the access to private class method and then use the setter method to grant access.

Next up in object-oriented programming python methodology talks about one of the key concepts called abstraction.

Abstraction:

Suppose you booked a movie ticket from BookMyShow using net banking or any other process. You don’t know the procedure of how the pin is generated or how the verification is done. This is called ‘abstraction’ from the programming aspect, it basically means you only show the implementation details of a particular process and hide the details from the user. It is used to simplify complex problems by modeling classes appropriate to the problem. An abstract class cannot be instantiated which simply means you cannot create objects for this type of class. It can only be used for inheriting the functionalities.

Example:

Output: emp_id is 12345

Explanation: As you can see in the above example, we have imported an abstract method and the rest of the program has a parent and a derived class. An object is instantiated for the ‘childemployee’ base class and the functionality of abstract is being used.

This brings us to the end of our article on “Object-Oriented Programming Python”. I hope you have cleared with all the concepts related to Python class, objects, and object-oriented concepts in python. Make sure you practice as much as possible and revert your experience.

If you wish to check out more articles on the market’s most trending technologies like Artificial Intelligence, DevOps, Ethical Hacking, then you can refer to Edureka’s official site.

Do look out for other articles in this series which will explain the various other aspects of Python.

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

Циклы, ветвления и функции – все это элементы структурного программирования. Его возможностей вполне хватает для написания небольших, простых программ и сценариев. Однако крупные проекты часто реализуют, используя парадигму объектно-ориентированного программирования (ООП). Что оно из себя представляет и какие преимущества дает?

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

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

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

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

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

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

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

# Понятия ООП

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

Что такое класс или тип? Проведем аналогию с реальным миром. Если мы возьмем конкретный стол, то это объект, но не класс. А вот общее представление о столах, их назначении – это класс. Ему принадлежат все реальные объекты столов, какими бы они ни были. Класс столов дает общую характеристику всем столам в мире, он их обобщает.

То же самое с целыми числами в Python. Тип int – это класс целых чисел. Числа 5, 100134, -10 и т. д. – это конкретные объекты этого класса.

В языке программирования Python объекты принято называть также экземплярами. Это связано с тем, что в нем все классы сами являются объектами класса type. Точно также как все модули являются объектами класса module. Поэтому во избежании путаницы объекты, созданные на основе обычных классов, называют экземплярами. В этом курсе мы чаще будем такие объекты называть объектами, так как данная терминология более универсальная и используется в других языках.

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

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

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

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

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

Второй смысл инкапсуляции – объединение свойств и поведения в единое целое, т.е. в класс. Инкапсуляция в этом смысле подразумевается самим определением объектно-ориентированного программирования и есть во всех ОО-языках.

Полиморфизм – это множество форм. Однако в понятиях ООП имеется в виду скорее обратное. Объекты разных классов, с разной внутренней реализацией, то есть программным кодом, могут иметь одинаковые интерфейсы. Например, для чисел есть операция сложения, обозначаемая знаком +. Однако мы можем определить класс, объекты которого также будут поддерживать операцию, обозначаемую этим знаком. Но это вовсе не значит, что объекты должны быть числами, и будет получаться какая-то сумма. Операция + для объектов нашего класса может значить что-то иное. Но интерфейс, в данном случае это знак +, у чисел и нашего класса будет одинаков. Полиморфность же проявляется во внутренней реализации и результате операции.

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

ООП в Python: классы и объекты

Понятия класса и объекта являются ключевыми в объектно-ориентированных языках программирования, к которым относится также и язык Python . В нем всё является объектами – и строки, и списки, и словари, и функции, и даже сами классы.

(от англ. class) – это шаблон кода, который используется для описания структуры и создания , т.е. экземпляров этого класса.

По сути классы в Python представляют собой типы данных, а объекты – отдельные экземпляры этих типов. Например, в инструкциях a = ‘abcdef’ и b = ‘ABCDEF’ переменным a и b присваиваются объекты строкового типа данных, т.е. экземпляры встроенного класса string .

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

Благодаря использованию классов Python обладает всеми преимуществами абстрактного подхода в программировании. В частности Питону присущи:

  • (от англ. polymorphism) – способность функций и методов обрабатывать данные разных типов. Простыми примерами полиморфизма могут служить: встроенная функция len(s) , которая может работать с разными типами встроенных последовательностей, возвращая количество их элементов; метод s.count(x) , который подсчитывает количество символов x в строке s (один тип данных) или, например, количество элементов x в списке s (другой тип данных); операторы сложения и умножения, которые для чисел (один тип данных) производят операции сложения и умножения, а для строк (другой тип данных) конкатенацию и повторение конкатенации указанное число раз.
  • (от англ. encapsulation) – механизм, который позволяет объединять данные и методы, работающие с этими данными, в единый объект, скрывая при этом детали реализации от пользователя. Грамотно написанный класс должен ограничивать доступность своих членов и взаимодействовать с пользователем только с помощью своего интерфейса. При этом нужно помнить, что в Python инкапсуляция работает лишь на уровне соглашения между программистами о том, какие атрибуты следует считать общедоступными, а какие – приватными (подробнее об этом мы поговорим чуть ниже).
  • (от англ. inheritance) – еще одна концепция объектно-ориентированного программирования, которая позволяет на основе одного суперкласса создавать множественные подклассы, заимствующие его данные и функциональность с возможностью их изменения и добавления своих данных и функциональности. При этом в Python поддерживается возможность множественного наследования, когда подкласс наследует атрибуты сразу нескольких суперклассов одновременно. Таким образом, наследование способствует повторному использованию уже написанных компонентов программы, помогая избежать избыточности исходного кода.
  • (от англ. composition) – возможность создания классов, включающих в себя вызовы уже существующих классов. В результате при создании объектов такого класса они будут состоять или содержать объекты других классов. Здесь важно не путать композицию с наследованием, в том числе множественным. Ведь при наследовании подкласс получает все атрибуты (т.е. возможности) своих суперклассов, а при композиции класс-агрегатор атрибуты не наследует, он просто создает объекты этих классов для использования, например, в качестве своих атрибутов или локальных переменных своих методов.

Благодаря всем этим преимуществам ООП у программистов появляется возможность параллельной разработки отдельных независимых модулей в виде классов, способных скрывать от внешнего мира детали своего внутреннего устройства и объединяющихся в масштабные комплексные приложения посредством предназначенных для этого интерфейсов. Данный процесс можно наглядно представить на примере из жизни, когда отдельные модули МКС собираются независимо друг от друга разными странами, а затем на орбите собираются в одну монолитную космическую станцию за счет имеющихся интерфейсов, обеспе­чивающих взаимодействие готовых модулей между собой.

Создание классов и объектов

Для создания классов в Python используется инструкция class , которая в общем виде может быть представлена в следующем формате:

В заголовке инструкции сперва записывается служебное слово class , затем через пробел указывается имя класса, которое по принятому в Python соглашению обычно записывается в нотации CupWords , далее в скобках перечисляются имена наследуемых классов (их еще называют суперклассами) и завершается заголовок двоеточием. Если создаваемый класс не наследует других классов, заголовок инструкции разрешается записывать в формате class ClassName: , опуская круглые скобки и записывая двоеточие сразу после имени класса. В любом случае после заголовка записывается вложенный блок инструкций, в котором перечисляются атрибуты данных класса (по умолчанию они доступны для использования всеми создаваемыми экземплярами класса) и атрибуты-методы для обработки его данных, представляющие собой обычные функции, определенные внутри класса и принимающие в качестве первого аргумента параметр self , которому при вызове методов интерпретатор автоматически присваивает объект текущего экземпляра класса. При необходимости в начале тела класса разрешается указывать строку документирования класса, а также конструктор класса __init__() , представляющий собой специальный метод класса, который вызывается автоматически всякий раз при создании нового экземпляра, инициализируя его исходными данными. Изменение имени конструктора запрещено.

После того, как класс будет определен, можно приступать к созданию его отдельных экземпляров, сохраняя их в переменных или структурах данных. Для этого нужно после имени класса просто указать круглые скобки, перечислив в них начальные данные для инициализации создаваемого экземпляра (см. пример №1 ).

Пример №1. Создание классов и объектов в Python.

В нашем примере мы создали класс Person не наследуя никаких других классов, поэтому круглые скобки в заголовке были опущены ( class Person: ). В качестве строки документации мы указали обычную строку в тройных одинарных кавычках, после чего создали атрибут данных класса, присвоив ему название нашей компании ( company = ‘«Okpython»’ ). Этот атрибут стал доступен как из любого экземпляра класса ( person_1.company или person_2.company ), так и напрямую из объекта самого класса ( Person.company ). Далее был определен конструктор класса ( def __init__(self, name=’Немо’): ), который затем при создании экземпляров класса запустился в автоматическом режиме и создал атрибуты данных создаваемых экземпляров (это атрибуты созданных объектов person_1.name и person_2.name ). Тут важно помнить, что наличие в определении класса конструктора с параметрами влечет за собой необходимость в передаче вновь создаваемым экземплярам начальных данных для их инициализации, иначе будет получена ошибка (мы подстраховались, использовав аргумент со значением по умолчанию). В конце тела класса мы добавили еще два метода для установки и получения атрибута данных экземпляров класса ( def set_date(self, date): и def get_date(self): ). Как и говорилось, в качестве первого аргумента у методов был задан параметр self , которому при каждом вызове этих методов интерпретатор автоматически присвоил объекты текущих экземпляров класса (т.е. объекты person_1 и person_2 ).

После создания класса Person мы создали первый экземпляр этого класса, использовав инструкцию вызова объекта класса person_1 = Person(‘Коля’) и указав в качестве начальных данных для нашего конструктора значение аргумента ‘Коля’ . Далее, для доступа к атрибутам созданного объекта мы использовали синтаксис с точкой, сохранив в переменной глобальной области видимости имя сотрудника ( name_1 = person_1.name ), а также установив и затем также сохранив в переменной дату трудоустройства сотрудника в компанию (инструкции person_1.set_date(‘30.05.2022’) и date_1 = person_1.get_date() ). Проделав тоже самое для второго экземпляра, мы сохранили в глобальной переменной название компании, использовав атрибут данных класса ( company = Person.company ), а затем вывели на экран строку документирования класса и данные сотрудников.

Таким образом, процедура создания класса в простейшем случае сводится к объявлению имени класса ClassName в заголовке инструкции class и перечислению в теле класса его атрибутов, состоящих из атрибутов данных и атрибутов-методов. Что касается создания экземпляров класса, то делается это по аналогии с вызовами обычной функции, т.е. при помощи пары круглых скобок после имени класса в формате ClassName(arg_1, arg_2, . arg_n) . После создания экземпляра класса можно начинать пользоваться объявленными атрибутами: получать к ним доступ, изменять, удалять или же добавлять новые атрибуты.

Доступ к атрибутам классов и объектов

Доступ к открытому атрибуту вне класса или объекта может быть получен с помощью имени этого атрибута, указанного через точку после имени класса или объекта в формате obj.attr . Для добавления нового атрибута или изменения значения уже существующего используется привычная нам инструкция присваивания obj.new_attr = value или, соответственно, obj.attr = new_value , а удаление осуществляется инструкцией del obj.attr (см. пример №2 ).

Пример №2. Атрибуты классов и объектов в Python (часть 1).

  • – функция проверяет наличие в объекте object атрибута с именем name , возвращая True в случае успеха и False в противном случае. В качестве аргументов ей передаются объект и строка с именем искомого в нем атрибута (см. пример №3 ).
  • – возвращает значение атрибута name переданного объекта object , если он существует. В противном случае функция возвращает значение по умолчанию default , если оно было передано, или возбуждает исключение AttributeError . В качестве обязательных аргументов функции передаются объект и строка с именем искомого в нем атрибута.
  • – устанавливает новое значение value атрибуту с именем name переданного объекта object , если он существует. В противном случае интерпретатор создает новый атрибут с указанным именем и присваивает ему переданное значение. В качестве первых двух аргументов функции передаются объект и строка с именем искомого в нем атрибута.
  • – удаляет из объекта object атрибут с именем name , если объект позволяет это сделать. В качестве аргументов ей передаются объект и строка с именем искомого в нем атрибута.

Пример №3. Функции для работы с атрибутами классов и объектов.

Здесь важно отметить один нюанс – при обращении к атрибуту объекта, созданного на основе классов, интерпретатор производит поиск этого атрибута сначала в самом экземпляре, а затем, при отсутствии такового, во всех классах, расположенных выше в дереве наследования (см. пример №4 ).

Пример №4. Атрибуты классов и объектов в Python (часть 2).

Как видим, даже при отсутствии атрибута непосредственно в самом экземпляре, интерпретатор не возбуждает ошибку, а сперва пытается отыскать его в самом классе. Поэтому даже после удаления атрибута attr_0 в экземпляре obj_1 при повторной попытке обращения к нему через obj_1.attr_0 интерпретатор выдал не ошибку, а вернул значение общедоступного атрибута, найденного в объекте класса ExampleClass .

Получить доступ или посмотреть текущий набор атрибутов класса или экземпляра можно при помощи словаря атрибутов object.__dict__, который динамически изменяется в процессе изменения как количества доступных атрибутов, так и их значений (см. пример №5 ).

Пример №5. Атрибуты классов и объектов в Python (часть 3).

Стоит добавить, что поскольку в Python объекты присутствуют повсюду, атрибут __dict__ имеется не только у классов, но и у других объектов, например, объектов функций.

Статические методы и методы класса

Как было показано выше, обычные атрибуты-методы классов в момент своего вызова в качестве первого аргумента автоматически получают экземпляр класса, посредством которого они вызываются. Такие методы формально называют , поскольку они подразумевают воздействие на объекты экземпляров класса. Однако в Python присутствуют еще две разновидности методов: , которые при вызове в качестве первого аргумента вместо объекта экземпляра автоматически получают объект самого класса, а также , которые представляют собой обычные функции, определенные внутри класса и не требующие передачи экземпляра класса в качестве первого аргумента. Для получения таких методов служат встроенные функции classmethod(method) и staticmethod(method), которые необходимо вызывать внутри классов. Обе функции помечают переданный объект метода method как специальный, т.е. требующий передачи объекта самого класса для метода класса и, соответственно, не требующий передачи экземпляра класса для статического метода (см. пример №6 ).

Пример №6. Виды методов в классах (часть 1).

Объявить статический метод или метод класса можно более элегантно, если учесть, что обе функции classmethod(method) и staticmethod(method) представляют собой не что иное, как готовые встроенные декораторы. Все что нужно, это использовать синтаксис декораторов, записав перед определениями нужных нам методов строки @classmethod и @staticmethod (см. пример №7 ).

Пример №7. Виды методов в классах (часть 2).

Следует помнить, что методы экземпляра предназначены для обработки данных каждого отдельного экземпляра класса, в то время как методы класса призваны обрабатывать данные самого класса, являющиеся общими сразу для всех экземпляров этого класса. Например, методы класса могут использоваться для подсчета созданных экземпляров класса (см. пример №8 ), вести список экземпляров, находящихся в данный момент в оперативной памяти, изменять существующие или устанавливать новые атрибуты-данных класса. Кроме того, если первый аргумент методов экземпляра при определении принято обозначать через self и его нужно передавать явным образом при вызове через класс, то для первого аргумента методов класса принято использовать имя cls , не указывая его явным образом как при вызове из экземпляров, так и вызове через сам класс (еще раз посмотрите пример №7 ).

Что касается статических методов, то они как и методы класса предназначены для работы с атрибутами класса, а не экземпляра. Но при этом они не предполагают автоматической передачи в качестве первого аргумента объекта класса. Поэтому статические методы могут быть использованы как самые обычные функции, выполняющие какие-либо полезные действия так или иначе связанные с данными класса и определенные в его пространстве имен для более тесной связи с классом, а также уменьшения вероятности конфликта имен в глобальной области видимости. Более того, их можно даже не объявлять статическими при помощи функции staticmethod(method) . В таком случае они останутся доступны посредством объекта класса, но станут недоступны для вызовов из экземпляров класса.

Пример №8. Виды методов в классах (часть 3).

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

Инкапсуляция в классах

По умолчанию все атрибуты и методы класса являются открытыми или публичными (от англ. public). Это значит, что они доступны не только внутри класса, но и за его пределами. Для этого достаточно использовать синтаксис доступа к атрибутам при помощи точки (например, obj.attr ) или предназначенные для этого встроенные функции (например, getattr(obj, ‘attr’) ). Однако бывают случаи, когда требуется ограничить прямой доступ к атрибутам классов из вызывающего кода воизбежание передачи им некорректных значений или случайного удаления. Примером может служить возраст человека, который не может быть отрицательным, или значение атрибута, предназначенного для служебного пользования. В таких случаях вполне логично делать атрибуты закрытыми или приватными (от англ. private), предоставляя доступ к ним посредством интерфейсов взаимодействия в виде специальных методов, осуществляющих необходимые проверки передаваемых им значений. При этом следует помнить, что в Python инкапсуляция в классах (т.е. сокрытие данных) поддерживается лишь на уровне соглашения между программистами. Полностью скрыть реализацию класса от целенаправленных злонамеренных действий извне не получится.

Так атрибуты, предназначенные для внутреннего использования, принято начинать с одного символа подчеркивания (например, _var ). Встретив такой атрибут, другой программист будет иметь в виду, что он не предназначен для публичного использования и в дальнейшем может быть даже удален без предварительного уведомления. В тоже время никто не запрещает изменять такой атрибут на свой страх и риск, надеясь, что это не повлияет на работоспособность класса (см. пример №9 ).

Пример №9. Инкапсуляция в классах (часть 1).

Если нужна большая защищенность атрибута, нужно указывать в начале его имени два знака подчеркивания в формате __attr_name . В результате такой манипуляции атрибут останется доступен внутри класса, но станет недоступен по такому имени за его пределами, т.к. вне класса имена атрибутов, начинающихся с двух знаков подчеркивания, автоматически преобразуются интерпретатором из формата __attr_name в формат _Class__attr_name (см. пример №10 ).

Пример №10. Инкапсуляция в классах (часть 2).

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

  • fget – объект функции, которая будет вызываться при попытке прочитать значение атрибута и возвращать вычисленное значение атрибута;
  • fset – объект функции, которая будет вызываться при попытке выполнить операцию присваивания;
  • fdel – объект функции, которая будет вызываться при попытке удаления атрибута;
  • doc – строка документирования с описанием атрибута, если это необходимо.

Результатом вызова функции property является объект свойства, присваиваемый имени атрибута, которым требуется управлять и который будет находиться в области видимости класса и наследоваться всеми его экземплярами (см. пример №11 ).

Пример №11. Инкапсуляция в классах (часть 3).

В нашем примере мы создали класс с управляемым атрибутом age . Благодаря этому, при каждой попытке обращения к нему по имени person_age.age будет вызываться один из предназначенных для его управления методов. Именно поэтому атрибут остался доступен для чтения, но стал недоступен для удаления. При этом заметьте, что по факту, воизбежание конфликта имен, мы сохранили данные в атрибуте с именем _age , т.к. само имя age было зарезервировано для хранения объекта свойства, возвращаемого функцией property .

Если необходимо запретить изменение значения атрибута или даже сделать его полностью закрытым для доступа из кода за пределами класса, нужно всего лишь изменить поведение отвечающих за это методов, переданных функции property в качестве аргументов (см. пример №12 ).

Пример №12. Инкапсуляция в классах (часть 4).

Следует добавить, что объект свойства, возвращаемый функцией property обладает атрибутами-методами getter(get_method), setter(set_method) и deleter(del_method), которые позволяют определить соответствующие методы для обработки обращений к управляемому атрибуту. При этом первый метод практически никогда не используется, т.к. вызов функции property() уже подразумевает его передачу в качестве первого аргумента (см. пример №13 ).

Пример №13. Инкапсуляция в классах (часть 5).

Опять же, весь процесс можно осуществить при помощи декораторов, которые делают код более простым и приятным для восприятия (см. пример №14 ).

Пример №14. Инкапсуляция в классах (часть 6).

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

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

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

Пример №15. Наследование классов в Python (часть 1).

В нашем примере подкласс Sub_3 наследует атрибуты сразу двух суперклассов Super_1 и Super_2 . Однако поскольку оба суперкласса обладают атрибутом с именем attr_0 , интерпретатор выбрал для подкласса значение атрибута суперкласса Super_1 , т.к. при наследовании поиск атрибутов ведется сперва в самом подклассе, а затем в списке наследуемых суперклассов слева направо. Именно поэтому подкласс Sub_3 унаследовал конструктор первого суперкласса. Если бы нам понадобилось отдать приоритет атрибутам второго суперкласса, его пришлось бы перечислить в скобках первым.

Помимо того, что подклассы наследуют данные и методы своих суперклассов, они могут определять и свои собственные атрибуты. Так в подклассе Sub_3 были определены собственный атрибут данных attr_3 и метод method_3 . Но и это еще не все. При необходимости подклассы могут изменять (специализировать) наследуемые атрибуты или даже полностью замещать их (см. пример №16 ).

Пример №16. Наследование классов в Python (часть 2).

Следует заметить, что в ходе специализации методов в подклассах внутри замещающего метода обычно предусматривается вызов исходной версии метода наследуемого суперкласса. Это позволяет выполнить предусмотренный в суперклассе набор действий по умолчанию без необходимости повторного написания уже имеющегося кода. Так в подклассе SubClass нашего примера мы расширили конструктор и метод method_2 суперкласса, вызвав исходные версии методов вместо того, чтобы заново переписывать их код.

Кстати, доступ к оригинальным методам может быть получен не только напрямую через имя суперкласса, но и через встроенную функцию super([type[, object-or-type]]), возвращающую специальный объект-посредник, делегирующий вызовы метода суперклассу указанного типа type . В качестве необязательных аргументов она принимает тип (т.е. объект подкласса), для которого будет производиться поиск суперкласса, а также конкретный объект экземпляра подкласса или объект самого подкласса, для которого требуется получить доступ к методу (см. пример №17 ).

Пример №17. Наследование классов в Python (часть 3).

Как видим, функция super() может вызываться из подкласса без аргументов. В таком случае интерпретатор автоматически использует в качестве аргументов текущий подкласс и объект self . Кроме того, ее можно вызывать и вне классов, явно передавая требуемый тип и целевой объект.

Теперь рассмотрим подробнее ситуацию, когда подкласс наследует сразу несколько суперклассов с одинаковыми именами атрибутов. Выше мы уже указывали, что в таком случае подклассом будет унаследовано значение атрибута того суперкласса, который перечисляется в заголовке инструкции class первым. Но что делать, когда важны значения атрибутов обоих классов? В этом случае, чтобы гарантировать принадлежность атрибута тому классу, который его использует, необходимо в начале имени атрибута поставить два символа подчеркивания везде, где оно используется этим классом (см. пример №18 ).

Пример №18. Наследование классов в Python (часть 4).

В нашем примере благодаря наличию приставки из двух знаков подчеркивания атрибуты остались доступны по короткому имени внутри класса, но стали недоступны вне класса, т.к. имена этих атрибутов были дополнены интерпретатором именами их классов до _A__y и _B__y , что и показал словарь атрибутов obj.__dict__ после вызова методов obj.set_a() и obj.set_b() . При этом атрибут x , который использовался без спецприставки, был методом obj.set_b() перезаписан.

Композиция классов

Композиция классов – это еще один принцип ООП, использующийся в Python и предоставляющий возможность создания классов, включающих в себя вызовы уже существующих классов (см. пример №19 ).

Пример №19. Композиция классов в Python.

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

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

Магические методы и перегрузка операторов

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

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

Пример №20. Перегрузка операторов в Python.

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

Все магические методы могут свободно наследоваться от суперклассов и переопределяться в подклассах, как и любые другие методы. Если же какой-то метод не реализован в классе, это лишь означает, что соответствующая ему операция не поддерживается данным классом, а попытка применения такой операции будет возбуждать исключение (что мы и увидели при попытке выполнить инструкцию d = a + 5 , содержащую операцию сложения экземпляра a нового типа Num с целым числом).

Перечислим для ознакомления наиболее часто используемые магические методы: __add__ (сложение), __sub__ (вычитание), __mul__ (умножение), __truediv__ (деление), __mod__ (остаток от деления), __pow__ (возведение в степень), __and__ (логическое И), __or__ (логическое ИЛИ), __getattr__ (обращение к атрибуту), __setattr__ (присваивание атрибуту), __delattr__ (удаление атрибута), __getitem__ (доступ к элементу по индексу, извлечение среза, итерации), __setitem__ (присваивание элементу по индексу или срезу), __delitem__ (удаление среза или элемента по индексу), __len__ (длина), __bool__ (проверка логического значения), __lt__ (меньше), __gt__ (больше), __le__ (меньше или равно), __ge__ (больше или равно), __eq__ (равно), __ne__ (не равно), __iter__ (получение итератора), __iadd__ (комбинированный оператор сложения), __next__ (получение следующего элемента итератора), __contains__ (оператор проверки на вхождение in ), __new__ (создание объекта), __init__ (конструктор), __del__ (деструктор), __call__ (вызов функции) и др.

Значительно большее количество имеющихся магических методов можно найти, например, на официальном сайте Python в разделе Data model справочного руководства.

Конструкторы и деструкторы

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

Далее, если созданные объекты после выполнения своего предназначения становятся ненужными, имеет смысл удалять их для высвобождения памяти. В Python для этих целей имеется автоматический сборщик мусора, удаляющий объекты, которые выходят за пределы текущей области видимости или счетчики ссылок на которые становятся равными нулю. Кроме того, ненужные объекты можно удалять и самостоятельно, используя, например, инструкцию del . В любом случае, если ссылок на объект больше не остается, интерпретатор вызывает деструктор – еще один магический метод с именем __del__ , представляющий собой операцию окончательного удаления объектов. Используются деструкторы реже конструкторов, но в некоторых ситуациях их применение бывает весьма оправданным. Так деструкторы обычно определяют в классах, для которых перед стандартным удалением требуется выполнить еще какие-нибудь действия, например, закрыть файл или соединение с базой данных (см. пример №21 ).

Пример №21. Конструкторы и деструкторы в Python.

Обратите внимание, что инструкция del obj не вызывает напрямую метод obj.__del__() , она лишь уменьшает счетчик ссылок для соответствующего объекта на единицу. И только тогда, когда счетчик полностью обнуляется, интерпретатор использует деструктор для окончательной зачистки следов существования ненужного объекта в памяти.

Краткие итоги параграфа

  • Класс – это шаблон кода, который используется для описания структуры и создания объектов, т.е. экземпляров этого класса.
  • Python обладает всеми преимуществами абстрактного подхода в программировании. В частности ему присущи:
    • полиморфизм – способность функций и методов обрабатывать данные разных типов;
    • инкапсуляция – механизм, который позволяет объединять данные и методы, работающие с этими данными, в единый объект, скрывая при этом детали реализации от пользователя;
    • наследование – еще одна концепция объектно-ориентированного программирования, которая позволяет на основе одного суперкласса создавать множественные подклассы, заимствующие его данные и функциональность с возможностью их изменения и добавления своих данных и функциональности;
    • композиция (агрегирование) – возможность создания классов, включающих в себя вызовы уже существующих классов.

    Вопросы и задания для самоконтроля

    1. Каково основное назначение ООП в языке Python ? Показать решение.

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

    2. Перечислите основные компоненты парадигмы ООП в языке Python ? Показать решение.

    Ответ. Полиморфизм, инкапсуляция, наследование и композиция (описание смотрите в начале параграфа).

    3. Что представляет собой класс? Как создать класс? Показать решение.

    Ответ. Класс представляет собой шаблон кода, который используется для описания структуры и создания объектов, т.е. экземпляров этого класса. Для создания классов в Python предназначена инструкция class . В заголовке инструкции указываются имя класса и перечисляемые в скобках через запятую наследуемые суперклассы, а в теле инструкции – атрибуты класса, представляющие собой данные и методы для их обработки.

    4. Как создать экземпляр класса? Показать решение.

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

    5. Для чего используется метод __init__ ? Показать решение.

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

    6. Что представляет из себя первый аргумент в методах классов? Показать решение.

    Ответ. Посредством первого аргумента методы классов получают ссылку на объект экземпляра, который подразумевается при вызове данного метода. Именно поэтому про методы классов говорят, что они «объектно-ориентированные», то есть предназначенны для обработки или изменения объектов. Согласно общепринятым соглашениям в качестве имени первого аргумента используется идентификатор self .

    7. Как создать статический метод? А метод класса? Показать решение.

    Ответ. Для создания методов классов и статических методов служат встроенные функции classmethod(method) и staticmethod(method) , которые необходимо вызывать либо использовать в качестве декораторов внутри классов.

    8. Назовите самый простой способ сделать атрибут класса недоступным по его имени за пределами класса. Показать решение.

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

    9. Для чего предназначена встроенная функция property() ? Какие аргументы она ожидает получить? Можно ли использовать ее в качестве декоратора? Показать решение.

    Ответ. Функция property(fget=None, fset=None, fdel=None, doc=None) позволяет создать свойство класса (управляемый атрибут). Функция принимает четыре аргумента со значениями по умолчанию None : fget – объект функции, которая будет вызываться при попытке прочитать значение атрибута и возвращать вычисленное значение атрибута; fset – объект функции, которая будет вызываться при попытке выполнить операцию присваивания; fdel – объект функции, которая будет вызываться при попытке удаления атрибута; doc – строка документирования с описанием атрибута, если это необходимо. Для использования ее в качестве декоратора, необходимо пометить метод, например, с именем name , отвечающий за получение значения атрибута аннотацией @property , а также при необходимости создать еще два метода с таким же именем для установки значения атрибута и удаления атрибута, использовав соответственно аннотации @name.setter и @name.deleter .

    10. Как выполняется поиск унаследованных атрибутов? Показать решение.

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

    11. Как получить доступ из подкласса к методу суперкласса? Показать решение.

    Ответ. В ходе наследования доступ к оригинальным методам может быть получен как напрямую через имя суперкласса в формате SuperClass.method(self) , так и через встроенную функцию super([type[, object-or-type]]) , возвращающую специальный объект-посредник, делегирующий вызовы метода данного объекта экземпляра или подкласса суперклассу указанного типа type .

    12. Что представляет из себя принцип композиции? Показать решение.

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

    13. Для чего используются магические методы? Как отличить их от обычных методов? Показать решение.

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

    14. В каких случаях деструктор автоматически вызывается интерпретатором? Показать решение.

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

    15. Исправьте в коде все ошибки так, чтобы скрипт заработал. Показать решение.

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

    Самая популярная парадигма современной разработки: обучаем питонистов на кошечках, напитках и вечеринках.

    Иллюстрация: Катя Павловская для Skillbox Media

    Иван Стуков

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

    Зачем придумали ООП

    Краеугольное понятие в ООП — объект. Это такой своеобразный контейнер, в котором сложены данные и прописаны действия, которые можно с этими данными совершать.

    Чтобы понять, чем объекты так полезны и для чего их изобрели, сравним ООП с другой методикой разработки — процедурной. В ней весь код можно поделить на два вида: основную программу и вспомогательные функции, которые могут вызываться как программой, так и другими функциями:

    У такого программирования есть существенный недостаток — части кода сильно зависят друг от друга. Например, основная программа вызывает функцию, та вызывает вторую, та, в свою очередь, — третью. При этом, допустим, вторую функцию могут параллельно вызывать ещё несколько других, а также основная программа. Схематически вся эта процедурная путаница представлена на рисунке:

    Если мы изменим какую-нибудь функцию, то остальные части кода могут быть к этому не готовы — и сломаются. Тогда придётся переписывать ещё и их, а они, в свою очередь, завязаны на другие функции. В общем, проще будет написать новую программу с нуля.

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

    Логика ООП совершенно иная: к основной программе подключаются не функции, а объекты, внутри которых уже лежат собственные переменные и функции. Так выстраивается более иерархичная структура. Переменные внутри объектов называются полями, или атрибутами, а функции — методами.

    Объекты независимы друг от друга и самодостаточны, так что, если мы сломаем что-то в одном объекте, это никак не отразится на других. Более того: даже если мы полностью изменим содержание объекта, но сохраним его поведение, весь код продолжит работать.

    Как работают классы

    Каждый объект в ООП строится по определённому классу — абстрактной модели, описывающей, из чего состоит объект и что с ним можно делать.

    Например, у нас есть класс «Кошка», обладающий атрибутами «порода», «окрас», «возраст» и методами «мяукать», «мурчать», «умываться», «спать». Присваивая атрибутам определённые значения, можно создавать вполне конкретные объекты.

    • Порода = абиссинская.
    • Окрас = рыжий.
    • Возраст = 4.

    Таким образом мы можем создать сколь угодно много разных кошек:

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

    Принципы ООП на Python

    Всё объектно-ориентированное программирование строится на четырёх понятиях: инкапсуляции, наследовании, полиморфизме и абстракциях. Поэтому давайте объявим наш класс «Кошка» и будем объяснять ООП на нём:

    Метод __init__ — инициализатор класса. Он вызывается сразу после создания объекта, чтобы присваивать значения динамическим атрибутам. self — ссылка на текущий объект, она даёт доступ к атрибутам и методам, с которыми вы работаете. Её аналог в других языках программирования — this.

    Примечание 1. Слово self общепринятое, но не обязательное, вместо него можно использовать любое другое. Однако это может запутать тех, кто будет читать ваш код.

    Примечание 2. Названия классов принято писать с прописной буквы, а объектов — со строчной.

    Итак, мы создали класс Cat, в котором объявили три атрибута: порода — breed, цвет — color и возраст — age. А ещё добавили два метода, чтобы наша кошка умела мяукать — meow() и мурчать — purr().

    Давайте создадим пару объектов нашего класса:

    Отлично, теперь, когда у нас есть основа, приступим к изучению принципов ООП.

    Инкапсуляция

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

    Вернёмся к нашим кошечкам. Мы можем разрешить изменять атрибут «возраст», но только в большую сторону, а атрибуты «порода» и «цвет» лучше открыть только для чтения — ведь порода кошки не меняется, а цвет если и меняется, то не по её инициативе.

    В нашем классе «Кошка» мы сделали все атрибуты открытыми, поэтому давайте это исправим:

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

    Нам всё ещё нужно получать доступ к атрибутам, поэтому мы предоставляем его через @property и объявляем для каждого атрибута свой метод — breed, color, age. В каждом из этих методов мы возвращаем значение нашего закрытого атрибута. Это делает его доступным только для чтения.

    И последнее — мы должны позволить пользователям увеличивать возраст кота. Для этого воспользуемся @age.setter и ещё раз объявим метод age, а внутри него напишем простое условие и вернём значение атрибута.

    Теперь создадим экземпляр класса:

    Выведем значения атрибутов:

    И попробуем изменить атрибут age:

    Всё успешно. А теперь сделаем это с другим атрибутом:

    Мы получили ошибку, потому что запретили изменять этот атрибут.

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

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

    Достаточно объявить «Домашнюю кошку» наследником «Кошки» и прописать новые атрибуты и методы — вся остальная функциональность перейдёт от родителя к потомку.

    Давайте объявим новый класс:

    В первой строке мы как раз наследуем все методы и атрибуты класса Cat. А чтобы всё создалось корректно, мы должны вызвать метод super() в методе __init__() и через него заполнить атрибуты класса-родителя. Поэтому мы и передаём в этот метод «породу», «окрас» и «возраст».

    Кроме атрибутов для класса-родителя у класса-потомка есть и собственные атрибуты: «хозяин» — owner и «кличка» — name. Их мы будем использовать только в этом классе, поэтому они будут недоступны для класса-родителя.

    Мы сразу сделали атрибуты класса-потомка закрытыми и объявили для них собственные методы. А также добавили метод ​​getTreat(), которого нет в классе-родителе.

    Давайте создадим объект класса:

    Как видим, у нас работают и новые методы, и старые. Наследование прошло успешно.

    Полиморфизм

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

    Допустим у нас есть два класса — «Кошка» и «Попугай»:

    А теперь пусть у нас есть метод, который ожидает, что ему на вход придёт объект, у которого будет метод sleep:

    Посмотрим, как это будет работать:

    Хотя классы разные, их одноимённые методы работают похожим образом. Это и есть полиморфизм.

    Абстракция

    При создании класса мы упрощаем его до тех атрибутов и методов, которые нужны именно в этом коде, не пытаясь описать его целиком и отбрасывая всё второстепенное. Например, у всех хищников есть метод «охотиться», поэтому все животные, которые являются хищниками, автоматически будут уметь охотиться.

    Рассмотрим класс Predator:

    Этот класс будет общим для всех животных, которые являются хищниками, — например, кошек:

    У кошки есть свои атрибуты: «имя» — name и «окрас» — color. Но при этом она потомок хищников, а значит, умеет охотиться:

    Примеры реализации ООП на Python

    Давайте ещё пофантазируем и посоздаём классы.

    Представьте ситуацию: вашего друга пригласили на пафосную вечеринку в закрытый клуб. Там довольно странный этикет: в разное время все должны пить строго определённые напитки. Причём любой из них, в зависимости от ситуации, все пьют определённым способом: или обычными глотками по 20 мл, или маленькими по 10, или залпом всё, что осталось. Более того: размер глотка для одного и того же напитка может внезапно поменяться по ходу вечеринки.

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

    Для начала создадим класс Drink:

    У любого напитка есть атрибуты: название, стоимость в рублях и объём в миллилитрах. Предположим для простоты, что на нашей вечеринке принято всегда пить из посуды одинакового объёма (200 мл), а остальные атрибуты могут меняться от напитка к напитку.

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

    Создадим объект coffee — экземпляр класса Drink. В нашем примере создание нового объекта обозначает заказ нового напитка:

    Теперь у нас есть объект coffee, который содержит статический атрибут volume, полученный от класса Drink, и динамические атрибуты name и price, которые мы указали при создании объекта. Давайте попробуем к ним обратиться:

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

    К динамическим атрибутам мы так обратиться не сможем.

    Итак, напиток заказан, и с ним нужно что-то делать. Так как вы общаетесь через микронаушник, то не видите, в каком состоянии напиток друга. Что ж, попросим друга сообщить вам об этом. Для этого добавим ещё один метод внутри класса Drink:

    Тусовка делает первый глоток. Скомандуем другу, чтобы он присоединился. Для этого нужен ещё один динамический атрибут remains, информирующий нас, сколько миллилитров напитка осталось. Изначально остаток будет равен объёму посуды. После этого прописываем метод, указывающий товарищу, сколько конкретно глотать в соответствии с этикетом:

    Уровни доступа в Python

    Чтобы нам не приходилось каждый раз проверять, хватает ли напитка для нужного глотка, напишем служебный метод _is_enough. Затем перепишем метод sip и добавим методы small_sip и drink_all:

    Обратите внимание ещё на такой нюанс: в строке coffee.remains = 10 мы извне вмешались в объект и приравняли его атрибут remains к 10. Это удалось потому, что все атрибуты и методы в Python по умолчанию являются публичными, то есть доступными извне.

    Чтобы регулировать вмешательство во внутреннюю работу объекта, в ООП есть несколько уровней доступа: публичный (public), защищённый (protected) и приватный (private). Защищённые атрибуты и методы можно вызывать только внутри класса и его классов-наследников. Приватные — только внутри класса: даже наследники не имеют доступа к ним.

    В Python это реализовано следующим образом: перед защищёнными атрибутами и методами пишут одинарное нижнее подчёркивание (_example), перед приватными — двойное (__example). Именно это мы сделали в методе _is_enough. Одинарным нижним подчёркиванием мы объявили его защищённым.

    При этом в Python само по себе объявление атрибутов и методов защищёнными и приватными не ограничивает доступ к ним извне. Мы всё ещё можем вызвать метод _is_enough из любого места программы:

    Атрибуты и методы, объявленные приватными, вызвать напрямую уже нельзя, но есть обходной путь:

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

    Так что и мы объявим защищёнными атрибуты volume и remains, чтобы помнить: ими стоит пользоваться только внутри класса Drink и его наследников. Теперь всё выглядит так:

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

    Вечеринка идёт полным ходом. Но тут случается непредвиденное: ваш друг, который уже слегка приспособился к местным нравам и даже начал получать удовольствие, внезапно кричит вам в наушник, что вечер вновь перестаёт быть томным. «Они объявили время соков! — паникует он. — А у каждого сока свой вкус, тут чёрт ногу сломит!»

    Действительно. Хьюстон, у нас проблемы. Сок, на первый взгляд, — напиток как напиток: его тоже можно пить глотками и залпом, у него есть цена и объём. Но, как пел гражданин Шнуров, есть один момент: в отличие от любого напитка, у сока появляется новый, специфический атрибут, который не поддерживается классом Drink, — вкус фрукта или ягоды, из которых он выжат.

    Не паникуем: даже из самой сложной ситуации всегда есть как минимум два выхода. Можно, конечно, полностью скопировать класс Drink и изменить в этой копии всё, что нам нужно. Но мы поступим изящнее — создадим класс Juice и сделаем его наследником класса Drink:

    Примечание. Обратите внимание, что из класса-потомка мы не можем напрямую обратиться к приватным атрибутам и методам класса-родителя.

    Создаём объект класса Juice и вызываем в нём методы, унаследованные от родительского класса Drink:

    Родительский класс Drink поделился с потомком своими атрибутами и методами, так что нам не пришлось писать их заново.

    Теперь посмотрим на атрибут name. В классе Drink, когда мы могли заказать что угодно, от кофе и чая до кваса и коктейля, имело смысл каждый раз указывать название напитка. Но в классе Juice название всегда будет одинаковым: «сок». Тогда зачем всё время при заказе сока спрашивать атрибут name?

    Переопределим в классе Juice метод __init__: пусть значением атрибута name всегда будет «сок». И затем снова закажем яблочный сок:

    Что же именно происходит при создании объекта apple_juice?

    1. Мы вызываем инициализатор класса Juice и в скобках передаём ему аргументы price и taste.

    2. Инициализатор класса Juice с помощью функции super() вызывает другой инициализатор — родительского класса Drink.

    3. Инициализатор класса Drink просит передать ему аргументы name и price. В качестве аргумента name он получает статический атрибут _juice_name, который мы прописали в классе Juice. А аргумент price подтягивается из инициализатора класса Juice.

    4. В инициализаторе класса Drink присваиваются значения атрибутам name, price и _remains.

    5. В инициализаторе класса Juice присваивается значение атрибуту taste.

    Если вам всё ещё сложно сориентироваться, что откуда берётся и куда передаётся, посмотрите на эту схему. Разными цветами здесь обозначены пути, по которым атрибутам присваиваются их значения:

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

    Он сообщает нам, что пьёт сок, но не говорит, какой именно. Чтобы получить от друга дополнительную информацию, переопределим метод drink_info родительского класса:

    Так мы реализовали принцип полиморфизма. Неважно, что пьёт наш друг — кофе или сок, мы можем запросить у него информацию о напитке одной и той же командой drink_info. И приятель уже сам будет ориентироваться по ситуации: если он пьёт сок, то сообщит нам его вкус, а если любой другой напиток — его название.

    Примечание. Все классы в Python по умолчанию являются наследниками суперкласса object и наследуют его атрибуты и методы. Такими унаследованными методами, например, являются встроенные __new__, __init__, __del__ и многие другие.

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

    Так как объявить стоимость можно для любого напитка, пропишем метод tell_price в классе Drink — и дочерний класс Juice автоматически унаследует его:

    Теперь проверим, действительно ли он работает с объектами как класса Drink, так и класса Juice:

    Профит, коллеги: ваш друг уходит с вечеринки с новой подружкой и приглашением на следующее мероприятие. А всё благодаря вам и объектно-ориентированному программированию.

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

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