-
Notifications
You must be signed in to change notification settings - Fork 32
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #147 from SemyonSinchenko/stalkermustang-python-cl…
…asses-lec10 python lec10 about OOP
- Loading branch information
Showing
5 changed files
with
227 additions
and
0 deletions.
There are no files selected for viewing
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,225 @@ | ||
--- | ||
jupytext: | ||
formats: md:myst | ||
text_representation: | ||
extension: .md | ||
format_name: myst | ||
kernelspec: | ||
display_name: Python 3 | ||
language: python | ||
name: python3 | ||
--- | ||
|
||
(python_l10)= | ||
|
||
# Объекты, классы и методы | ||
|
||
## Описание лекции | ||
|
||
В этой лекции мы расскажем про: | ||
- основы ООП: что это и зачем? | ||
- инстанс класса | ||
- на что указывает `self` | ||
- методы классов в уже знакомых примерах | ||
- атрибуты классов | ||
|
||
## ООП: Объектно-Ориентированное Программирование | ||
Что же такое объектно-ориентированное программирование? Судя по названию, ключевую роль здесь играют объекты, на которые ориентируется дальнейший процесс программирования. Если мы взглянем на реальный мир, то для нас он предстанет в виде множества объектов, обладающих определенными свойствами, взаимодействующих между собой и вследствие этого изменяющимися. Эта привычная для взгляда человека картина мира была перенесена в программирование. Python - это объектно-ориентированный язык программирования, и **все в нем является объектами**. | ||
|
||
**Объект** - это набор данных и инструкций в памяти комьютера, в том числе: | ||
1. Тип объекта; | ||
2. Данные, формирующие объект (контент внутри него); | ||
3. Методы. | ||
|
||
```{figure} /_static/pythonblock/oop_l10/class_and_obj1.png | ||
:name: class_and_obj1 | ||
:width: 250px | ||
Так можно проиллюстрировать соотношение "класс - объекты". Сверху указаны общие прототипы (прямоугольник, звезда, треугольник), снизу - уникальные экземпляры, со своим цветом и деталями. | ||
``` | ||
|
||
Как определить тип мы уже знаем. И с объектами на самом деле мы знакомы с самого начала - ведь даже целочисленная переменная `5`, имеющая `type(5) = int`, представляет собой объект. Напомним, что говорилось в курсе ранее: **методы - это те же функции, только существующие не сами по себе, а являющиеся частью класса**. | ||
|
||
```{code-cell} ipython3 | ||
example_list = list(["что-то", "с"]) | ||
print(f"Метод append имеет тип {example_list.append}") | ||
# вот так можно вызвать метод, изменяя сам экземпляр класса | ||
example_list.append("чем-то") | ||
# а вот так можно обратиться к атрибуту конкретного объекта | ||
attr_value = example_list.__doc__ | ||
print(f"Значение атрибута: {attr_value}") | ||
``` | ||
|
||
`list` - это класс, и в первой строчке кода выше создается **объект, или экземпляр класса** `example_list`. Он хранит свой набор данных (контент). Метод `append` общий для всех списков, однако меняет только конкретный экземляр. Обратите внимание: этот метод ничего не возвращает, он меняет сам объект и его контент. Не все методы работают схожим образом, и придется запоминать принцип работы самых распространненых методов. К примеру, сложение чисел или строк внутри Python также осуществляется с помощью методов, однако это не меняет саму переменную, как было рассказано в более ранних лекциях: | ||
|
||
```{code-cell} ipython3 | ||
# создадим два объекта класса int | ||
a = 3 | ||
b = int(4) | ||
# неявно вызовем метод сложения | ||
a + b | ||
# проверим, что содержимое объектов не изменилось | ||
print(f"{a = }, {b = }") | ||
``` | ||
|
||
Конкретно в данном случае логика работы обусловлена свойствами `mutable`/`immutable` объектов, о чем также упоминалось в одной из предшествующих лекций. `int` неизменяем, и потому при вызове метода сложения возвращает новый объект - ведь самого себя изменить не получится. `list` же может меняться, и `append` меняет содержимое `inplace`. | ||
|
||
```{figure} /_static/pythonblock/oop_l10/class_and_obj_house.png | ||
:name: class_and_obj_house | ||
:width: 450px | ||
Другой житейский пример - по одному чертежу (классу) можно построить разные (но похожие!) дома, в которых разные семьи, убранство, разные обои. | ||
``` | ||
|
||
|
||
## Определение классов | ||
Давайте попробуем создать свой первый класс: | ||
|
||
```{code-cell} ipython3 | ||
# определим классы | ||
class List: | ||
def __init__(self, initial_content = None): | ||
# указывать в виде значения по умолчанию mutable-объекты неправильно | ||
if initial_content is None: | ||
initial_content = [] | ||
self.content = initial_content | ||
def append(self, new_element): | ||
# изменение производится inplace, с заменой собственного контента | ||
self.content = self.content + [new_element] | ||
class Int: | ||
def __init__(self, value): | ||
self.value = value | ||
def add(self, second_value): | ||
# изменение не производится, создается новый объект и он же возвращается | ||
return Int(self.value + second_value.value) | ||
# воспроизведем примеры кода выше для List | ||
example_list = List(["что-то", "с"]) | ||
print(f"{example_list.content = }") | ||
example_list.append("чем-то") | ||
print(f"{example_list.content = }") | ||
print(f"{type(example_list) = }") | ||
# и для Int | ||
a = Int(3) | ||
b = Int(4) | ||
a.add(b) | ||
print(f"После простого сложения: {a.value = }, {b.value = }") | ||
c = a.add(b) | ||
print(f"После записи в переменную результата: {c.value = }") | ||
``` | ||
|
||
Что важно заметить и отметить в этом примере: | ||
1. Как объявить класс? С помощью ключевого слова `class`, указания названия класса (таким образом создается новый тип данных) и перечисления методов: | ||
```python | ||
class ИмяКласса: | ||
<код_тела_класса> | ||
``` | ||
2. Обязательно определить метод `__init__`. Это метод, вызываемый при создании нового объекта класса и отвечающий за определение его контента. В нем через запятую, как и в обычной функции, перечислить необходимые переменные и, по возможности, их значения по умолчанию; | ||
3. Каждый метод должен первым аргументом принимать **экземпляр объекта** (по соглашению он называется `self`). При этом при вызове метода Python автоматически подставит сам объект, делать это руками не нужно. По сути под капотом происходит следующее: | ||
|
||
```{code-cell} ipython3 | ||
# вызываем метод класса - не объекта! - и передаем первым аргументом объект, | ||
# который при обычных обстоятельствах будет записан в self | ||
result = Int.add(Int(3), Int(4)) | ||
print(f"{result.value}") | ||
``` | ||
4. **Атрибутом называется переменная, хранящаяся в экземпляре класса**; | ||
5. В рамках методов атрибуты объекта доступны с использованием ключевого слова `self`. К примеру, `self.data`; | ||
6. Вне методов атрибуты доступны у экземпляров через точку: `some_object.attr_name`; | ||
7. Для создания объекта класса нужно указать имя класса (типа) и передать аргументы: `Int(3)` - как раз оно; | ||
8. Методы как могут возвращать что-либо (в том числе новый объект того же класса), так и ничего не возвращать. | ||
|
||
В заключение еще одна картинка, иллюстрирующая суть класса, экземпляра, атрибутов и методов: | ||
|
||
```{figure} /_static/pythonblock/oop_l10/class_and_obj_pika.png | ||
:name: class_and_obj_pika | ||
:width: 450px | ||
Who's that pokemon? | ||
``` | ||
|
||
## Пример использования для хранения состояние | ||
В курсе будет встречаться подобный паттерн, поэтому хочется остановиться поподробнее. Допустим, есть некоторая среда, заданная состоянием, и есть объекты, существующие в этой среде, как-то взаимодействующие между собой (и со средой). Есть набор функций, который осуществляет эти взаимодействия. Тогда удобно ввести некоторый объект `system` класса `System`, который хранит эту информацию, и передавать его как параметр в методы расчета чего-либо (никакой конкретной логики тут не заложено, не пытайтесь понять, за что отвечает `alpha` и прочие переменные - пример абстрактный): | ||
|
||
```{code-cell} ipython3 | ||
# класс системы, с ее параметрами и состоянием | ||
class System: | ||
def __init__(self, param_vector, alpha, gamma, multiplier = 2.0, energy = 0.0): | ||
self.param_vector = param_vector | ||
self.alpha = alpha | ||
self.gamma = gamma | ||
self.multiplier = multiplier | ||
self.energy = energy | ||
# начальное состояние | ||
self.system_state = [ | ||
(alpha - gamma) * multiplier * param | ||
for param | ||
in self.param_vector | ||
] | ||
def first_action(self, object): | ||
# взаимодействие с одним объектом: увеличение энергии, изменение состояния системы | ||
self.gamma -= object.energy | ||
self.energy += object.energy | ||
self.system_state = [param - self.alpha * self.energy for param in self.system_state] | ||
def second_action(self, list_of_object): | ||
# взаимодействие с несколькими объектами - увеличение их энергии | ||
for obj in list_of_object: | ||
obj.energy += self.gamma * self.multiplier | ||
class Object: | ||
def __init__(self, energy = 10.0): | ||
# объект хранит только один атрибут | ||
self.energy = energy | ||
# симуляция одного цикла в системе | ||
def one_system_cycle(system: System, objects = None): | ||
if objects is not None: | ||
system.second_action(objects) | ||
# убираем один последний объект | ||
objects = objects[:-1] | ||
else: | ||
objects = [Object(val) for val in range(5)] | ||
for obj in objects: | ||
system.first_action(obj) | ||
return objects | ||
params = [1.0, 3.0, 4.15, 0.0] | ||
alpha = 5 | ||
gamma = 0.18 | ||
system = System(params, alpha, gamma, energy = 10) | ||
# изначально объектов нет, они появятся после первого цикла | ||
objects = None | ||
for cycle_num in range(1, 4): | ||
print(f"Состояние системы на итерации {cycle_num}: {system.system_state}.", end="\t") | ||
objects = one_system_cycle(system, objects) | ||
print(f"Всего объектов: {len(objects)}", end="\t") | ||
total_object_energy = sum([obj.energy for obj in objects]) | ||
print(f"Суммарная энергия объектов: {total_object_energy}") | ||
``` | ||
Обратите внимание на то, что функция `one_system_cycle` не возвращает `system` - ведь мы прямо во время итераций меняем ее состояние, и в последующие моменты времени эти изменения сохраняются, то есть состояние остается. Таким образом, эту систему (среду) - а именно объект, экземпляр класса `System` - как параметр **можно передавать в десятки функций, и каждая из них будет видеть актуальное состояние**. | ||
|
||
|
||
|
||
## Что мы узнали из лекции | ||
- Объекты одного класса отличаются между собой атрибутами, которые придают уникальности; | ||
- Методы классов позволяют описывать логику взаимодействия, **обновлять состояние**; | ||
- Для объявления класса нужно определить **как минимум один метод** `__init__`; | ||
- Классы без методов можно использовать как **удобное хранилище разнородной информации** о схожих объектах (описать разных студентов и их оценки). | ||
|
||
Это далеко не все, что можно написать по теме классов и объектов, однако это тот необходимый минимум, что потребуется для прохождения курса. Больше примеров и деталей можно найти, например, по ссылкам: [пример с несколькими взаимодействующими объектами](https://younglinux.info/oopython/ooprogramm), [что такое наследование и зачем оно нужно](https://younglinux.info/oopython/inheritance). |