Содержание
Объекты
Помните ли вы, что несколько раз в предыдущих рассказах я упоминал слово "объект" и даже рассказывал, что это такое?
Я намекал на примерно такие понятия, и сейчас скажу их более строго:
объект – это что угодно, на что можно ссылаться переменной; в официальном определении языка говорится гордая фраза, что всякое понятие в питоне задаётся либо объектом, либо отношением между объектами
объект – это любой прямоугольник (или червяк) на "картинках с червяками и стрелочками"
объект – это свалка всего-всего (т.е. переменных и методов)
Когда мы говорим, что объект – это свалка разных штук, мы будем их называть "полями" (если это данные) или "методами" (если это функции).1
Операция точка (".")
Если у нас есть объект x, то получить из него поле или метод y мы можем так: x.y
Пример:
Мы создаём объект – строку hello – указываем на него переменной x. Затем лезем внутрь объекта и добываем из него поле __doc__.
В питоне принято всё, что касается внутренних дел питона, называть в духе __что-то__ (два подчёркивания в начале и два подчёркивания в конце). В поле doc питон складывает строки самодокументации для объекта. (И там же их ищет функция help).
Ещё пример:
Снова создаём такой же объект – строку hello – указываем на него переменной x, добываем из него метод title и вызываем его.
Ещё пример:
Загруженный в питон модуль – это тоже просто такой объект. Командой import sys мы загружаем модуль, делаем из него объект, и указываем на него переменной sys. Затем добываем из этого объекта поле version и распечатываем его.
Точно так же мы можем присваивать значения в поля объекта:
Мы загружаем модуль "sys", кладём его в переменную sys, распечатываем значение поля ps1 из него, затем записываем в него же (поле ps1 в объекте sys) новое значение: "python, ".
sys.ps1 – это приглашение командной строки питона. Соответственно, после того, как мы его изменили, питон теперь приветствует нас именно им.
В некоторые питонские объекты записать новые поля или изменить в них существующие поля не получится.
Диаграммы
Когда разговаривают об объектах, их изображают обычно приблизительно так:
+-------+ | x | |-------+ | y: 2 | | z: 3 | +-------+
Такая картинка могла бы получиться в результате выполнения такого кода2:
Такие диаграммы несколько жульнические: они предполагают, что у объекта есть какое-то имя. На самом деле, его нет: под именем x мы его видим потому, что в переменную x мы его положили. Сам объект о том, под каким именем его видят в разных частях программы, ничего не знает. А в разных частях программы его наверняка видят под разными именами.
Классы
Зачем нужны классы, я сам затрудняюсь ответить3. Наверное, на этот вопрос гораздо лучше ответите вы.
Поэтому я лучше расскажу о том, что это такое и как оно работает.
Класс – это тоже такой объект. Если объект x является представителем класса X, то в объекте x на X стоит специальная ссылка.
Когда мы говорим питону x.y, питон берёт объект x, ищет в нём поле y, если не находит, ищет поле y в X.
То есть: мы можем создать класс и покидать в него много значений, потом создать из него несколько объектов (представителей класса – instances of the class), и каждый из этих объектов будет автоматически наследовать свойства своего родителя – т.е. класса.
Классы создаются в питоне такой конструкцией:
Конструкция pass – это заглушка, которая значит "ничего не делать". Её можно писать где угодно в питонском коде, и она нигде никогда ничего делать не будет. Полезная штука, чтобы питон не ругался на тему неправильных отступов да пустого тела класса.
Классы традиционно называют с большой буквы. И чтобы совсем вас запутать, представителя класса я буду называть той же буквой, но маленькой.
Объекты из классов создаются так:
1 >>> x = X()
То есть мы просто вызвали класс как будто это была функция.
Теперь поясняю суть понятия класс, смотрите внимательно:
Я записал в классе X в поле a значение 1. Затем я записал в объекте x (представителе класса X) в поле b значение 2. Теперь оказалось, что мы через представителя класса видим все значения, которые определены в классе.
На диаграммах принято факт наследования обозначать стрелкой от представителя к классу:
+-------+ --------> +---------+ | x | | class X | +-------+ +---------+ | b: 2 | | a: 1 | +-------+ +---------+
Если мы изменим поле в классе:
То окажется, что то, как мы видим это поле в представителе класса, тоже изменится. При создании представителя класса ничего никуда не копируется, питон просто запоминает, что у этого объекта класс такой-то.
+-------+ --------> +---------+ | x | | class X | +-------+ +---------+ | b: 2 | | a: 3 | +-------+ +---------+
Теперь самое смущающее, но совсем простое:
Если мы присваиваем в представителе класса в поле, которое уже есть в его классе, то мы меняем только представителя класса. Это чертовски логично: что мы написали, то питон и сделал!
+-------+ --------> +---------+ <------- +-------+ | x | | class X | | y | +-------+ +---------+ +-------+ | b: 2 | | a: 3 | | a: 4 | +---------+ +-------+
Отсюда возникает такая вот изредка полезная идиома:
1 >>> x.a = x.a
Если в объекте x поле a уже было, то ничего не изменится. Если же в объекте x поля a ещё не было, то тогда питон отправится искать поле a в классе объекта x – и если найдёт его там, скопирует его в сам объект x. Тем самым, защитит поле a в объекте от изменения поля a в классе.
Наследование
Как и с классами, зачем нужно наследование – вопрос туманный.
А что это такое – сказать просто. Если у класса есть родитель, то класс наследует все свойства родителя. Ровно таким же механизмом, как объект наследует все свойства класса.
То есть, если у нас есть объект x, созданный из класса X, который является потомком класса Y, и мы спрашиваем у питона что-нибудь про x.y, то питон сначала ищет y внутри самого объекта x, потом внутри его класса X, потом внутри его родителя X, и так далее.
Пишется это так:
То есть на самом деле, все классы в питоне являются потомками (прямыми или через несколько поколений) класса object.
|
Возвращаясь к операции точка. По сути – это две операции. Если она используется слева от знака присваивания, то x.y переводится на русский так: запиши внутрь именно x в поле y. В противном случае оно переводится так: прочитай поле y из x или его предков. |
|
Методы
Метод – это почти что совсем просто функция, которая лежит в классе. С одной только маленькой хитростью.
Начнём с примера. Допустим, мы хотим описать иерархию классов, которые бы определяли упорядоченное укоренённое дерево, в листьях которого лежат числа, и описать для этих классов метод, который бы считал сумму листьев дерева.
Начнём с общего предка, про которого мы ничего и сказать не можем, кроме того, что оно – дерево. Далее. Упорядоченное укоренённое дерево по определению – это либо лист, либо внутренняя вершина, которая по сути есть список упорядоченных укоренённых деревьев. Опишем лист. Если у нас дерево является листом, то сумма листьев этого дерева – это значение, приписанное к нашему листу.
+-----------+ +------------+ | Leaf | ------> | Tree | +-----------+ +------------+ | value = 0 | | sum() | +-----------+
Мы описали класс Leaf, потомок класса Tree. В классе Leaf лежит поле value со значением ноль (на самом деле, толку от этого value никакого, я его здесь привожу скорее в роли пояснения к коду), и ещё в нём лежит метод sum.
Когда мы описываем метод в классе, у него всегда должно быть не менее одного аргумента, и первый аргумент должен всегда называться self. (Питон не будет ругаться, если вы назовёте его не self, а как-нибудь иначе. Ругаться буду я – от своего лица и лица дядюшки Гвидо и всего сообщества питонистов. Дабы код было просто читать, первый аргумент метода всегда называется self).
Когда мы вызываем метод на объекте класса, мы передаём ему на один аргумент меньше, чем описали, а в качестве self питон подставляет как раз тот объект, на котором мы вызвали метод. В нашем примере мы описали sum как функцию одного аргумента, а вызываем его так: x.sum(). Аргумент self питон передаст за нас.
Пример:
В тот момент, когда мы говорим питону x.sum(), он ищет sum внутри x, не находит, ищет sum внутри Leaf, находит, вызывает его, подставляет в качестве self то, что слева от точки в вызове, т.е. x; метод sum у нас состоит из одной строки return self.value: так как у нас self is x (помните в питоне операцию is?), то это то же самое, что и return x.value, то есть return 2.
Примерно так всё на самом деле и происходит! (Не верите мне – проверьте дебаггером).
Если дерево не лист, то оно – внутренняя вершина. Сумма листьев для внутренней вершины – это сумма таких сумм для каждого из поддеревьев нашей вершины.
Пример чуть-чуть побольше. Посчитаем сумму листьев вот такого дерева:
* / \ [1] [2]
На диаграмме классов и объектов оно должно будет выглядеть так:
+-----------+ | Tree | +-----------+ ^ ^ | | +-------------+ +---------------+ | Leaf | | Node | +-------------+ +---------------+ | value = 0 | | subtrees = [] | | sum() | | sum() | +-------------+ +---------------+ ^ ^ ^ | | | +-----------+ +-----------+ +-------------------+ |-----------| |-----------| |-------------------| <----- tree | value = 1 | | value = 2 | | subtrees = [*, *] | +-----------+ +-----------+ +-------------|--|--+ ^ ^--------------------------/ | | \-------------------------------------/
Я здесь не стал давать имена конкретным листьям и внутренним вершинам нашего дерева. Получить такую ситуацию средствами питона мы можем так:
Некрасиво, но хотя бы понятно, что происходит. Красиво сделаем чуть попозже. А сейчас ещё раз посмотрим, что происходит, когда мы говорим tree.sum().
>>> tree.sum() |
Входим в функцию:
>>> tree.sum() |
Node.sum() |
Первая строка: result = 0
>>> tree.sum() |
Node.sum() |
Вторая строка: for node in self.subtrees; вспоминаем, что self is tree, следовательно subtrees – это список [x, y], первый элемент x
>>> tree.sum() |
Node.sum() |
Третья строка: result += node.sum(); вспоминаем, что node is x, в объекте x метода sum нету, ищем в классе, в Leaf метод sum есть:
>>> tree.sum() |
Node.sum() |
Leaf.sum |
Входим в функцию:
>>> tree.sum() |
Node.sum() |
Leaf.sum |
Единственная строка: return self.value; вспоминаем, что self is x, в нём сразу есть value, равный 1.
>>> tree.sum() |
Node.sum() |
return 1 |
Получаем результат:
>>> tree.sum() |
Node.sum() |
Следующая итерация цикла, снова вторая строка: for node in self.subtrees
>>> tree.sum() |
Node.sum() |
Снова третья строка: result += node.sum()
>>> tree.sum() |
Node.sum() |
Leaf.sum |
Снова входим в функцию:
>>> tree.sum() |
Node.sum() |
Leaf.sum |
И сразу возвращаем результат:
>>> tree.sum() |
Node.sum() |
return 2 |
И получем его:
>>> tree.sum() |
Node.sum() |
На этом цикл завершается, четвёртая строка: return result
>>> tree.sum() |
return 3 |
Получаем ответ:
3 |
Конструктор класса
Перепишем наши определения дерева заново:
1 class Tree:
2 pass
3
4 class Leaf(Tree):
5
6 def __init__(self, value):
7 self.value = value
8
9 def sum(self):
10 return self.value
11
12 class Node(Tree):
13
14 def __init__(self, subtrees):
15 self.subtrees = subtrees
16
17 def sum(self):
18 result = 0
19 for node in self.subtrees:
20 result += node.sum()
21 return result
Я добавил в каждый из классов метод со странным названием __init__. В питоне все имена, которые начинаются с двух подчёркиваний и завершаются двумя подчёркиваниями – это имена всяких внутренностей питона. (Вспомните ещё и шаманское if __name__ == "__main__"). Обычно, когда мы встречаем такое имя, это значит, что оно будет вызываться (кроме тех случаев, когда мы захотим его вызвать руками) ещё и в каких-то специальных случаях.
Метод __init__ – это тот метод, который вызывается всегда при создании объекта, и он получает все те аргументы, которые мы при создании объекта передаём.
Пример. То же самое дерево, как мы рисовали выше, можно создать теперь с нашими новыми определениями всего одной строкой:
>>> tree = Node([Leaf(1), Leaf(2)]) >>> tree.sum() 3
Ещё один пример:
Казалось бы, конструкции получаются вроде как бы эквивалентные:
Но что происходит на самом деле? На самом деле, в классе X1 мы завели поле x, а в классе X2 у нас пусто, и только метод __init___. Когда мы создаём объект класса X1, мы создаём пустой объект. Когда мы создаём объект класса X2, питон вызывает на нём метод __init___, а посредством его мы в объект x2 (непосредственно в сам объект) записываем поле x.
Картинка оказывается такая (я не нарисовал __init__ внутри X2 из лени, потому, что его сущестование там нас не очень сейчас волнует):
+-------+ +-------+ | X1 | | X2 | +-------+ +-------+ | x = 1 | +-------+ +-------+ ^ ^ | | +-------+ +-------+ |x2 | |x1 | +-------+ +-------+ | x = 1 | +-------+ +-------+
О преобразовании типов
Теперь мы можем вспомнить о том, какие в питоне есть встроенные типы – все они на самом деле классы.
- int
- float
- str
- list
И теперь мы чуть-чуть больше понимаем, что значит конструкция int("1") – мы вызваем конструктор класса int и передаём ему в качестве аргумента строку "1". У встроенных типов в питоне умные конструкторы, которые пытаются свои аргументы всякими осмысленными для человеков способами использовать чтобы создать объект своего типа.
К сожалению, в этом месте есть очень много разных способов говорить об одном и том же. В каноническом описании питона и то, и другое называется "атрибутами", в smalltalk оно называется "слотами" и "сообщениями", в eiffel -- "фичами", в c++ иногда их называют "полями" и "методами", а иногда "членами"... Вероятно, ещё несколько школ именования я забыл или даже не сталкивался с ними. (1)
На самом деле, такой код не сработает. В объекты, созданные функцией object, просто так дописывать новые поля нельзя. Только ТШШ! Я этого не говорил! Притворимся, что так сделать можно! (2)
Так уж сложилось, что на заре возникновения объектно-ориентированного программирования (ООП) людям показалось совершенно необходимым выделить понятие класса; спустя двадцать лет после возникнования ООП в нём случился прорыв и люди сказали: "вау, чтобы программировать объекты достаточно иметь объекты". Это течение начали называть "прототипным" программированием. В очень слабой мере в питоне воплощена и эта идея. (3)