Kodomo

Пользователь

Объекты

Помните ли вы, что несколько раз в предыдущих рассказах я упоминал слово "объект" и даже рассказывал, что это такое?

Я намекал на примерно такие понятия, и сейчас скажу их более строго:

  1. объект – это что угодно, на что можно ссылаться переменной; в официальном определении языка говорится гордая фраза, что всякое понятие в питоне задаётся либо объектом, либо отношением между объектами

  2. объект – это любой прямоугольник (или червяк) на "картинках с червяками и стрелочками"

  3. объект – это свалка всего-всего (т.е. переменных и методов)

Когда мы говорим, что объект – это свалка разных штук, мы будем их называть "полями" (если это данные) или "методами" (если это функции).1

Операция точка (".")

Если у нас есть объект x, то получить из него поле или метод y мы можем так: x.y

Пример:

   1 >>> x = "hello"
   2 >>> print(x.__doc__)

Мы создаём объект – строку hello – указываем на него переменной x. Затем лезем внутрь объекта и добываем из него поле __doc__.

В питоне принято всё, что касается внутренних дел питона, называть в духе __что-то__ (два подчёркивания в начале и два подчёркивания в конце). В поле doc питон складывает строки самодокументации для объекта. (И там же их ищет функция help).

Ещё пример:

   1 >>> x = "hello"
   2 >>> x.title()
   3 "HELLO"

Снова создаём такой же объект – строку hello – указываем на него переменной x, добываем из него метод title и вызываем его.

Ещё пример:

   1 >>> import sys
   2 >>> print(sys.version)

Загруженный в питон модуль – это тоже просто такой объект. Командой import sys мы загружаем модуль, делаем из него объект, и указываем на него переменной sys. Затем добываем из этого объекта поле version и распечатываем его.

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

   1 >>> import sys
   2 >>> sys.ps1
   3 ">>> "
   4 >>> sys.ps1 = "python, "
   5 python, print "hello"
   6 hello

Мы загружаем модуль "sys", кладём его в переменную sys, распечатываем значение поля ps1 из него, затем записываем в него же (поле ps1 в объекте sys) новое значение: "python, ".

sys.ps1 – это приглашение командной строки питона. Соответственно, после того, как мы его изменили, питон теперь приветствует нас именно им.

В некоторые питонские объекты записать новые поля или изменить в них существующие поля не получится.

Диаграммы

Когда разговаривают об объектах, их изображают обычно приблизительно так:

+-------+
| x     |
|-------+
| y: 2  |
| z: 3  |
+-------+

Такая картинка могла бы получиться в результате выполнения такого кода2:

   1 >>> x = object()
   2 >>> x.y = 2
   3 >>> x.z = 3

Такие диаграммы несколько жульнические: они предполагают, что у объекта есть какое-то имя. На самом деле, его нет: под именем x мы его видим потому, что в переменную x мы его положили. Сам объект о том, под каким именем его видят в разных частях программы, ничего не знает. А в разных частях программы его наверняка видят под разными именами.

Классы

Зачем нужны классы, я сам затрудняюсь ответить3. Наверное, на этот вопрос гораздо лучше ответите вы.

Поэтому я лучше расскажу о том, что это такое и как оно работает.

Класс – это тоже такой объект. Если объект x является представителем класса X, то в объекте x на X стоит специальная ссылка.

Когда мы говорим питону x.y, питон берёт объект x, ищет в нём поле y, если не находит, ищет поле y в X.

То есть: мы можем создать класс и покидать в него много значений, потом создать из него несколько объектов (представителей класса – instances of the class), и каждый из этих объектов будет автоматически наследовать свойства своего родителя – т.е. класса.

Классы создаются в питоне такой конструкцией:

   1 class X(object):
   2     pass

Конструкция pass – это заглушка, которая значит "ничего не делать". Её можно писать где угодно в питонском коде, и она нигде никогда ничего делать не будет. Полезная штука, чтобы питон не ругался на тему неправильных отступов да пустого тела класса.

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

Объекты из классов создаются так:

   1 >>> x = X()

То есть мы просто вызвали класс как будто это была функция.

Теперь поясняю суть понятия класс, смотрите внимательно:

   1 >>> X.a = 1
   2 >>> x.b = 2
   3 >>> print(x.a)
   4 1
   5 >>> print(x.b)
   6 2
   7 >>> print(X.a)
   8 1
   9 >>> print(X.b)
  10 ...
  11 AttributeError: 'X' object has no attribute 'y'

Я записал в классе X в поле a значение 1. Затем я записал в объекте x (представителе класса X) в поле b значение 2. Теперь оказалось, что мы через представителя класса видим все значения, которые определены в классе.

На диаграммах принято факт наследования обозначать стрелкой от представителя к классу:

+-------+ --------> +---------+
| x     |           | class X |
+-------+           +---------+
| b: 2  |           | a: 1    |
+-------+           +---------+

Если мы изменим поле в классе:

   1 >>> X.a = 3
   2 >>> print(x.a)
   3 3

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

+-------+ --------> +---------+
| x     |           | class X |
+-------+           +---------+
| b: 2  |           | a: 3    |
+-------+           +---------+

Теперь самое смущающее, но совсем простое:

   1 >>> x.a = 4
   2 >>> print(x.a)
   3 4
   4 >>> print(X.a)
   5 3
   6 >>> y = X()
   7 >>> print(y.a)
   8 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, и так далее.

Пишется это так:

   1 class Y(object):
   2     pass
   3 
   4 class X(Y):
   5     pass
   6 
   7 x = X()

То есть на самом деле, все классы в питоне являются потомками (прямыми или через несколько поколений) класса object.

(!)

Возвращаясь к операции точка. По сути – это две операции. Если она используется слева от знака присваивания, то x.y переводится на русский так: запиши внутрь именно x в поле y. В противном случае оно переводится так: прочитай поле y из x или его предков.

(!)

Методы

Метод – это почти что совсем просто функция, которая лежит в классе. С одной только маленькой хитростью.

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

   1 class Tree(object):
   2     pass

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

   1 class Leaf(Tree):
   2 
   3     value = 0
   4 
   5     def sum(self):
   6         return self.value

+-----------+         +------------+
| Leaf      | ------> | Tree       |
+-----------+         +------------+
| value = 0 |
| sum()     |
+-----------+

Мы описали класс Leaf, потомок класса Tree. В классе Leaf лежит поле value со значением ноль (на самом деле, толку от этого value никакого, я его здесь привожу скорее в роли пояснения к коду), и ещё в нём лежит метод sum.

Когда мы описываем метод в классе, у него всегда должно быть не менее одного аргумента, и первый аргумент должен всегда называться self. (Питон не будет ругаться, если вы назовёте его не self, а как-нибудь иначе. Ругаться буду я – от своего лица и лица дядюшки Гвидо и всего сообщества питонистов. Дабы код было просто читать, первый аргумент метода всегда называется self).

Когда мы вызываем метод на объекте класса, мы передаём ему на один аргумент меньше, чем описали, а в качестве self питон подставляет как раз тот объект, на котором мы вызвали метод. В нашем примере мы описали sum как функцию одного аргумента, а вызываем его так: x.sum(). Аргумент self питон передаст за нас.

Пример:

   1 >>> x = Leaf()
   2 >>> x.value = 2
   3 >>> x.sum()
   4 2

В тот момент, когда мы говорим питону x.sum(), он ищет sum внутри x, не находит, ищет sum внутри Leaf, находит, вызывает его, подставляет в качестве self то, что слева от точки в вызове, т.е. x; метод sum у нас состоит из одной строки return self.value: так как у нас self is x (помните в питоне операцию is?), то это то же самое, что и return x.value, то есть return 2.

Примерно так всё на самом деле и происходит! (Не верите мне – проверьте дебаггером).

Если дерево не лист, то оно – внутренняя вершина. Сумма листьев для внутренней вершины – это сумма таких сумм для каждого из поддеревьев нашей вершины.

   1 class Node(Tree):
   2 
   3     subtrees = []
   4 
   5     def sum(self):
   6         result = 0
   7         for node in self.subtrees:
   8             result += node
   9         return result

Пример чуть-чуть побольше. Посчитаем сумму листьев вот такого дерева:

   *
  / \
[1]  [2]

На диаграмме классов и объектов оно должно будет выглядеть так:

               +-----------+
               | Tree      |
               +-----------+
                ^         ^
                |         |
   +-------------+      +---------------+
   | Leaf        |      | Node          |
   +-------------+      +---------------+
   | value = 0   |      | subtrees = [] |
   | sum()       |      | sum()         |
   +-------------+      +---------------+
     ^         ^                  ^
     |         |                  |
+-----------+ +-----------+  +-------------------+
|-----------| |-----------|  |-------------------| <----- tree
| value = 1 | | value = 2 |  | subtrees = [*, *] |
+-----------+ +-----------+  +-------------|--|--+
     ^             ^--------------------------/
     |                                     |
     \-------------------------------------/

Я здесь не стал давать имена конкретным листьям и внутренним вершинам нашего дерева. Получить такую ситуацию средствами питона мы можем так:

   1 x = Leaf()
   2 x.value = 1
   3 y = Leaf()
   4 y.value = 2
   5 tree = Node()
   6 tree.subtrees = [x, y]

Некрасиво, но хотя бы понятно, что происходит. Красиво сделаем чуть попозже. А сейчас ещё раз посмотрим, что происходит, когда мы говорим tree.sum().

>>> tree.sum()

Входим в функцию:

>>> tree.sum()

Node.sum()
self = tree

Первая строка: result = 0

>>> tree.sum()

Node.sum()
self = tree
result = 0

Вторая строка: for node in self.subtrees; вспоминаем, что self is tree, следовательно subtrees – это список [x, y], первый элемент x

>>> tree.sum()

Node.sum()
self = tree
result = 0
node = x

Третья строка: result += node.sum(); вспоминаем, что node is x, в объекте x метода sum нету, ищем в классе, в Leaf метод sum есть:

>>> tree.sum()

Node.sum()
self = tree
result = 0 + node.sum()
node = x

Leaf.sum

Входим в функцию:

>>> tree.sum()

Node.sum()
self = tree
result = 0 + node.sum()
node = x

Leaf.sum
self = x

Единственная строка: return self.value; вспоминаем, что self is x, в нём сразу есть value, равный 1.

>>> tree.sum()

Node.sum()
self = tree
result = 0 + node.sum()
node = x

return 1

Получаем результат:

>>> tree.sum()

Node.sum()
self = tree
result = 0 + 1
node = x

Следующая итерация цикла, снова вторая строка: for node in self.subtrees

>>> tree.sum()

Node.sum()
self = tree
result = 1
node = y

Снова третья строка: result += node.sum()

>>> tree.sum()

Node.sum()
self = tree
result = 1 + node.sum()
node = y

Leaf.sum

Снова входим в функцию:

>>> tree.sum()

Node.sum()
self = tree
result = 1 + node.sum()
node = y

Leaf.sum
self = y

И сразу возвращаем результат:

>>> tree.sum()

Node.sum()
self = tree
result = 1 + node.sum()
node = y

return 2

И получем его:

>>> tree.sum()

Node.sum()
self = tree
result = 1 + 2
node = y

На этом цикл завершается, четвёртая строка: 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

Ещё один пример:

   1 class X1(object):
   2     x = 1
   3 
   4 class X2(object):
   5     def __init__(self):
   6         self.x = 1

Казалось бы, конструкции получаются вроде как бы эквивалентные:

   1 >>> x1 = X1()
   2 >>> x1.x
   3 1
   4 >>> x2 = X2()
   5 >>> x2.x
   6 1

Но что происходит на самом деле? На самом деле, в классе X1 мы завели поле x, а в классе X2 у нас пусто, и только метод __init___. Когда мы создаём объект класса X1, мы создаём пустой объект. Когда мы создаём объект класса X2, питон вызывает на нём метод __init___, а посредством его мы в объект x2 (непосредственно в сам объект) записываем поле x.

Картинка оказывается такая (я не нарисовал __init__ внутри X2 из лени, потому, что его сущестование там нас не очень сейчас волнует):

 +-------+   +-------+
 | X1    |   | X2    |
 +-------+   +-------+
 | x = 1 |   +-------+
 +-------+     ^
   ^           |
   |         +-------+
 +-------+   |x2     |
 |x1     |   +-------+
 +-------+   | x = 1 |
 +-------+   +-------+

О преобразовании типов

Теперь мы можем вспомнить о том, какие в питоне есть встроенные типы – все они на самом деле классы.

И теперь мы чуть-чуть больше понимаем, что значит конструкция int("1") – мы вызваем конструктор класса int и передаём ему в качестве аргумента строку "1". У встроенных типов в питоне умные конструкторы, которые пытаются свои аргументы всякими осмысленными для человеков способами использовать чтобы создать объект своего типа.

  1. К сожалению, в этом месте есть очень много разных способов говорить об одном и том же. В каноническом описании питона и то, и другое называется "атрибутами", в smalltalk оно называется "слотами" и "сообщениями", в eiffel -- "фичами", в c++ иногда их называют "полями" и "методами", а иногда "членами"... Вероятно, ещё несколько школ именования я забыл или даже не сталкивался с ними. (1)

  2. На самом деле, такой код не сработает. В объекты, созданные функцией object, просто так дописывать новые поля нельзя. Только ТШШ! Я этого не говорил! Притворимся, что так сделать можно! (2)

  3. Так уж сложилось, что на заре возникновения объектно-ориентированного программирования (ООП) людям показалось совершенно необходимым выделить понятие класса; спустя двадцать лет после возникнования ООП в нём случился прорыв и люди сказали: "вау, чтобы программировать объекты достаточно иметь объекты". Это течение начали называть "прототипным" программированием. В очень слабой мере в питоне воплощена и эта идея. (3)