На самом деле, с классами мы уже немного знакомы. Правда, вместо того, чтобы создавать объекты придуманных нами классов, мы использовали готовые объекты в Python, созданные до нас.
Класс часто называют проектом объекта определенного типа, то есть шаблоном, который описывает атрибуты (характеристики) объекта и методы, которые можно применять к этому объекту. Например, в Python есть списки, объекты типа list, на которых определены методы .append()
, .extend()
, .pop()
и другие. Это означает, что при разработке языка написали класс, который зафиксировал, что такое список и как с ним работать. Или, что еще более интересно, в Python есть объект NumPy array, массив из библиотеки NumPy, у которого есть набор атрибутов и набор методов. Вспомним, как он выглядит:
import numpy as np
# создаем массив
A = np.array([8, 2, 8])
A
array([8, 2, 8])
Посмотрим на атрибуты .dtype
и .ndim
:
A.dtype # одна характеристика массива
dtype('int64')
A.ndim # другая характеристика массива
1
А теперь применим метод .sort()
, который изменит нам исходный массив, отсортировав его по возрастанию (тут полезно вспомнить, что метод – это функция, определенная на конкретном типе, в данном случае – на массиве):
A.sort() # функция sort(), которая работает на массивах
A
array([2, 8, 8])
Теперь давайте создадим свой класс. Чтобы пример был запоминающимся и более-менее осязаемым, давайте продолжим волшебную тему с предыдущего занятия и создадим класс Cauldron
, который будет описывать котёл. А точнее, этот класс будет определять характеристики котла (атрибуты класса «котёл») и действия, которые мы можем к нему применять (методы класса «котёл»).
Давайте для начала считать, что у котла есть два атрибута – его размер и материал, при этом с самим котлом мы ничего не делаем.
Создание класса начинается с оператора class
, после которого мы указываем его название. После названия мы ставим двоеточие и приступаем к описанию класса. Описание класса всегда начинается с его инициализации – мы должны сообщить Python, что у нас создается новый объект с некоторыми атрибутами.
class Cauldron:
Для инициализации объекта нам нужно написать специальную функцию __init__
(от initialize, обратите внимание на двойной _
вокруг init
) и описать, какие атрибуты у него будут:
# self – указание на сам объект
# атрибуты size и material
class Cauldron:
def __init__(self, size, material):
self.size = size
self.material = material
Что делает (и не делает) код выше? Во-первых, он не создает объект класса Cauldron
, а только описывает его. Во-вторых, он сообщает Python, что для создания объекта класса Cauldron
пользователь должен обязательно указать размер котла и его материал.
Попробуем создать медный котёл среднего размера:
caul = Cauldron('medium', 'copper')
Посмотрим на него:
caul
<__main__.Cauldron at 0x108185f50>
Объект от нас скрыт. Ничего страшного, зато мы можем после названия поставить точку, нажать на Tab и увидеть, какие атрибуты нам доступны! Запросим материал:
caul.material
'copper'
А теперь размер:
caul.size
'medium'
А теперь изменим наш класс – добавим в него атрибут contents
, который будет представлять собой список с содержимым котла. Плюс, допустим, что изначально котёл пустой, то есть по умолчанию в contents
сохранен пустой список.
class Cauldron:
def __init__(self, size, material, contents=[]):
self.size = size
self.material = material
self.contents = contents
Обратите внимание: как обычно, аргументы функции, у которых заданы значения по умолчанию, идут после тех аргументов, у которых значения по умолчанию не заданы.
Создадим обновленный объект caul
:
# не задали contents, но Python не ругается
caul = Cauldron('medium', 'copper')
# потому что по умолчанию там пустой список
caul.contents
[]
Теперь давайте допишем метод .add()
, который будет позволять добавлять ингредиенты в котёл. Несложно догадаться, что внутри этого метода будет использоваться обычный .append()
, так как contents
– это список:
class Cauldron:
def __init__(self, size, material, contents=[]):
self.size = size
self.material = material
self.contents = contents
# обновляем contents
def add(self, x):
self.contents.append(x)
Создадим новый котёл и добавим туда имбирь и змеиные клыки:
caul = Cauldron('medium', 'copper')
caul.add('ginger')
caul.add('snake fangs') # do not write snape fangs, please
Проверим содержимое котла и по совместимости атрибута .contents
:
caul.contents
['ginger', 'snake fangs']
Все добавилось! Теперь напишем метод .engorgio()
, который будет изменять размер котла на huge
и делать все буквы в ингредиентах в contents
заглавными.
class Cauldron:
def __init__(self, size, material, contents=[]):
self.size = size
self.material = material
self.contents = contents
def add(self, x):
self.contents.append(x)
def engorgio(self):
self.size = 'huge'
self.contents = [s.upper() for s in self.contents]
caul = Cauldron('medium', 'copper')
caul.add('ginger')
caul.add('snake fangs')
caul.engorgio()
caul.contents # сработало
['GINGER', 'SNAKE FANGS']
caul.size # тоже сработало
'huge'
Теперь пойдем еще дальше. Напишем метод .expulso()
, который будет «взрывать» котёл, то есть разбивать все элементы в .contents
на буквы.
class Cauldron:
def __init__(self, size, material, contents=[]):
self.size = size
self.material = material
self.contents = contents
def add(self, x):
self.contents.append(x)
def engorgio(self):
self.size = 'huge'
self.contents = [s.upper() for s in self.contents]
def expulso(self):
self.contents = [list(s) for s in self.contents]
Применим сначала .engorgio()
, а потом .expulso()
:
caul = Cauldron('medium', 'copper',
['dittany', 'ginger']) # сразу с contents
caul.engorgio()
caul.expulso()
caul.contents
[['D', 'I', 'T', 'T', 'A', 'N', 'Y'], ['G', 'I', 'N', 'G', 'E', 'R']]
Теперь давайте проверим, что будет, если в какой-нибудь из методов, то есть в какую-нибудь из функций внутри Cauldron()
, мы добавим строку с print()
. Например, сообщим, что метод .engorgio()
должен еще выводить на экран само заклинание.
class Cauldron:
def __init__(self, size, material, contents=[]):
self.size = size
self.material = material
self.contents = contents
def add(self, x):
self.contents.append(x)
def engorgio(self):
self.size = 'huge'
self.contents = [s.upper() for s in self.contents]
print("Engorgio!")
def expulso(self):
self.contents = [list(s) for s in self.contents]
caul = Cauldron('medium', 'copper',
['dittany', 'ginger']) # сразу с contents
caul.engorgio()
Engorgio!
caul.contents # изменились, плюс, выше получили еще подтверждение
Атрибут .contents
изменился, но теперь у нас еще одно подтверждение (сообщение Engorgio!), что метод сработал.
Давайте напишем еще метод .reparo()
, который будет устранять последствия разрушения – склеивать буквы обратно в слова. Этот метод будет более интересным по сравнению с предыдущими, потому что он должен что-то делать только в случае, если мы раньше применили метод .expulso()
и все сломали.
Что это означает технически? Нам нужно проверить, какого типа элементы содержит атрибут .contents
. Если строки, то .expulso()
мы не применяли, и все в порядке, если списки, то .expulso()
был запущен, и необходимо элементы каждого списка внутри contents
склеить.
class Cauldron:
def __init__(self, size, material, contents=[]):
self.size = size
self.material = material
self.contents = contents
def add(self, x):
self.contents.append(x)
def engorgio(self):
self.size = 'huge'
self.contents = [s.upper() for s in self.contents]
print("Engorgio!")
def expulso(self):
self.contents = [list(s) for s in self.contents]
def reparo(self):
if len(self.contents) != 0:
if type(self.contents[0]) is list:
self.contents = ["".join(s) for s in self.contents]
В нашем случае достаточно проверить тип первого элемента в .contents
, потому что если первый элемент является списком, то и остальные тоже – мы сами так определили метод .expulso()
, который разбивает все ингредиенты на буквы.
В коде выше мы дополнительно проверили длину списка в .contents
, чтобы в случае применения метода .reparo()
к пустому котлу, мы не получили ошибку (если список пустой, то Python не сможет найти в нем элемент с индексом 0).
Применяем и проверяем:
# разбиваем полный котел
caul = Cauldron('medium', 'copper',
['dittany', 'ginger'])
caul.expulso()
# восстанавливаем
caul.reparo()
caul.contents
['dittany', 'ginger']
# разбиваем пустой котел
caul = Cauldron('medium', 'copper')
caul.expulso()
# восстанавливаем
caul.reparo()
caul.contents # reparo() ничего не делает, но и ошибки нет
[]
Теперь давайте добавим в Cauldron()
методы другого типа – методы, которые не просто изменяют имеющиеся атрибуты, а добавляют новые или удаляют старые.
Напишем метод .colovaria()
, который будет принимать на вход строку с цветом и добавлять к объекту атрибут color
с введенным значением.
class Cauldron:
# изначально среди атрибутов нет color
def __init__(self, size, material, contents=[]):
self.size = size
self.material = material
self.contents = contents
def add(self, x):
self.contents.append(x)
def engorgio(self):
self.size = 'huge'
self.contents = [s.upper() for s in self.contents]
print("Engorgio!")
def expulso(self):
self.contents = [list(s) for s in self.contents]
def reparo(self):
if len(self.contents) != 0:
if type(self.contents[0]) is list:
self.contents = ["".join(s) for s in self.contents]
# а здесь создаем атрибут color и записываем туда color
def colovaria(self, color):
self.color = color
caul = Cauldron('medium', 'copper', ['dittany', 'ginger'])
Если сейчас напишем caul.
и нажмем Tab, среди перечисленных методов и атрибутов color
мы не увидим. Если вызовем его явно, получим ошибку:
caul.color
--------------------------------------------------------------------------- AttributeError Traceback (most recent call last) <ipython-input-39-7bddfc7855c2> in <module> ----> 1 caul.color AttributeError: 'Cauldron' object has no attribute 'color'
Теперь применим метод .colovaria()
:
caul.colovaria('red')
caul.color # появился!
'red'
Давайте прекратим эксперименты над котлом и напишем последний метод .evanesco()
, который будет не просто присваивать атрибуту .contents
значение None
, а будет удалять этот атрибут совсем. Если его удалим, то дальше ничего интересного сделать с котлом не сможем – все методы у нас работают с содержимым котла в contents
.
class Cauldron:
def __init__(self, size, material, contents=[]):
self.size = size
self.material = material
self.contents = contents
def add(self, x):
self.contents.append(x)
def engorgio(self):
self.size = 'huge'
self.contents = [s.upper() for s in self.contents]
print("Engorgio!")
def expulso(self):
self.contents = [list(s) for s in self.contents]
def reparo(self):
if len(self.contents) != 0:
if type(self.contents[0]) is list:
self.contents = ["".join(s) for s in self.contents]
def colovaria(self, color):
self.color = color
def evanesco(self):
del self.contents
Проверяем:
caul = Cauldron('medium', 'copper', ['dittany', 'ginger'])
caul.evanesco()
caul.contents # нет contents
--------------------------------------------------------------------------- AttributeError Traceback (most recent call last) <ipython-input-43-71c0e9761fc7> in <module> ----> 1 caul.contents # нет contents AttributeError: 'Cauldron' object has no attribute 'contents'
caul.expulso() # разбивать нечего
--------------------------------------------------------------------------- AttributeError Traceback (most recent call last) <ipython-input-44-b2e7ade75814> in <module> ----> 1 caul.expulso() # разбивать нечего <ipython-input-41-48da0ff0e812> in expulso(self) 15 16 def expulso(self): ---> 17 self.contents = [list(s) for s in self.contents] 18 19 def reparo(self): AttributeError: 'Cauldron' object has no attribute 'contents'
Наследование нужно, если мы хотим создать новый класс, но при этом не хотим определять все атрибуты и методы с нуля. Вместо этого мы можем унаследовать атрибуты и методы от какого-нибудь другого, уже существующего, класса, а потом добавить к ним новые.
Наследование, кстати, объясняет, почему у объектов типа pandas DataFrame, pandas Series и NumPy array есть одинаковые методы: датафрейм pandas наследует некоторые методы от Series, а Series, в свою очередь, наследует часть атрибутов и методов у array.
Давайте создадим класс Cauldron_Pers
, который будет описывать не просто котёл, а чей-то личный котёл. Это класс унаследует у Cauldron
все методы и атрибуты, а потом к ним мы добавим новые, например, имя владельца котла и его оценку за зелье.
class Cauldron_Pers(Cauldron):
def __init__(self, size, material, contents=[]):
super().__init__(size, material, contents)
Чтобы Python понимал, от какого класса мы наследуем атрибуты и методы, мы поместили название класса-родителя Cauldron
(parent class, от которого наследуем) в скобки после названия дочернего класса Cauldron_Pers
(child class). После этого мы записали функцию __init__
для инициализации класса, а потом воспользовались функцией super()
. Эта функция наследует все атрибуты и методы класса-родителя. Посмотрим, что получилось:
my = Cauldron_Pers('medium', 'cooper')
my.size # унаследовалось
'medium'
Теперь добавим новые атрибуты, как и задумывали. Имя владельца owner
будет у нас обязательным аргументом, а для оценки mark
выставим значение по умолчанию, равное None
.
class Cauldron_Pers(Cauldron):
def __init__(self, size, material, owner, contents=[], mark=None):
super().__init__(size, material, contents)
self.owner = owner
self.mark = mark
my = Cauldron_Pers('standard', 'copper', 'Alla')
my.owner # работает
'Alla'
my.mark # ничего, так как None
# если прогоним ячейку два раза, каждый ингредиент
# добавится дважды
my.add('dittany')
my.add('lavender')
my.expulso() # даже взрывается
my.contents
[['d', 'i', 't', 't', 'a', 'n', 'y'], ['l', 'a', 'v', 'e', 'n', 'd', 'e', 'r'], ['d', 'i', 't', 't', 'a', 'n', 'y'], ['l', 'a', 'v', 'e', 'n', 'd', 'e', 'r']]
Все методы из Cauldron
работают. Добавим какой-нибудь характерный только для личного котла метод и закончим. Пусть это будет метод .set_mark()
для выставления оценки.
class Cauldron_Pers(Cauldron):
def __init__(self, size, material, owner, contents=[], mark=None):
super().__init__(size, material, contents)
self.owner = owner
self.mark = mark
def set_mark(self, m):
self.mark = m
my = Cauldron_Pers('standard', 'copper', 'Alla')
Было бы оптимистичным ожидать что-то хорошее за взорванный котёл с двумя ингредиентами, поэтому пусть будет так:
my.set_mark(0)
my.mark
0
Важно: если мы создадим метод с таким же названием, что и унаследованный, то новый метод заменит старый. Например, если бы мы определили в Cauldron_Pers
метод .expulso()
, который бы записывал в contents
значение None
, этот метод бы вытеснил унаследованный .expulso()
, разбивающий названия ингредиентов на буквы:
class Cauldron_Pers(Cauldron):
def __init__(self, size, material, owner, contents=[], mark=None):
super().__init__(size, material, contents)
self.owner = owner
self.mark = mark
def set_mark(self, m):
self.mark = m
def expulso(self):
self.contents = None
my = Cauldron_Pers('standard', 'copper', 'Alla')
my.add('fangs')
my.expulso()
print(my.contents) # ничего - None
None
Документация для классов пишется так же, как и документация к функциям, с помощью так называемой docstring:
class Cauldron_Pers(Cauldron):
def __init__(self, size, material, owner, contents=[], mark=None):
"""
Creates a personal cauldron with
the following attributes:
size – a string;
material – a string;
owner – a string;
contents – a list (contents = [] by default);
mark – an integer (mark = None by default).
"""
super().__init__(size, material, contents)
self.owner = owner
self.mark = mark
def set_mark(self, m):
"""
Parameters: m is an integer
Changes attribute mark to m.
"""
self.mark = m
def expulso(self):
self.contents = None
Обратите внимание, общее описание внутри __init__
.
Запросим help по созданному нами классу и по методам внутри него.
help(Cauldron_Pers)
Help on class Cauldron_Pers in module __main__: class Cauldron_Pers(Cauldron) | Cauldron_Pers(size, material, owner, contents=[], mark=None) | | Creates a personal cauldron with | the following attributes: | size – a string; | material – a string; | owner – a string; | contents – a list (contents = [] by default); | mark – an integer (mark = None by default). | | Method resolution order: | Cauldron_Pers | Cauldron | builtins.object | | Methods defined here: | | __init__(self, size, material, owner, contents=[], mark=None) | Initialize self. See help(type(self)) for accurate signature. | | expulso(self) | | set_mark(self, m) | Parameters: m is an integer. | Changes attribute mark to m. | | ---------------------------------------------------------------------- | Methods inherited from Cauldron: | | add(self, x) | | colovaria(self, color) | | engorgio(self) | | evanesco(self) | | reparo(self) | | ---------------------------------------------------------------------- | Data descriptors inherited from Cauldron: | | __dict__ | dictionary for instance variables (if defined) | | __weakref__ | list of weak references to the object (if defined)
В документации есть наше описание, перечень методов, перечень методов, которые унаследованы у другого класса.
При этом мы можем создать объект класса Cauldron_Pers
и запросить помощь по отдельному методу:
my = Cauldron_Pers('standard', 'copper', 'Alla')
help(my.set_mark)
Help on method set_mark in module __main__: set_mark(m) method of __main__.Cauldron_Pers instance Parameters: m is an integer. Changes attribute mark to m.