Никита Волков
Питон является развитым объектно-ориентированным языком. Всё, с чем он работает, является объектами - целые числа, строки, словари, функции и т.д. Каждый объект принадлежит определённому типу (или классу, что одно и то же). Класс тоже является объектом. Классы наследуют друг от друга. Класс object
является корнем дерева классов - каждый класс наследует от него прямо или через какие-то промежуточные классы.
In [1]:
object, type(object)
Out[1]:
Функция dir
возвращает список атрибутов класса.
In [2]:
dir(object)
Out[2]:
Атрибуты, имена которых начинаются и кончаются двойным подчерком, используются интерпретатором для особых целей. Например, атрибут __doc__
содержит док-строку.
In [3]:
object.__doc__
Out[3]:
In [4]:
help(object)
Ниже мы рассмотрим цели некоторых других специальных атрибутов.
Вот простейший класс. Поскольку не указано, от чего он наследует, он наследует от object
.
In [5]:
class A:
pass
In [6]:
A, type(A)
Out[6]:
Создать объект какого-то класса можно, вызвав имя класса как функцию (возможно, с какими-нибудь аргументами). Мы уже это видели: имена классов int
, str
, list
и т.д. создают объекты этих классов.
In [7]:
o = A()
o, type(o)
Out[7]:
Узнать, какому классу принадлежит объект, можно при помощи функции type
или атрибута __class__
.
In [8]:
type(o), o.__class__
Out[8]:
У только что созданного объекта o
нет атрибутов. Их можно создавать (и удалять) налету.
In [9]:
o.x = 1
o.y = 2
o.x, o.y
Out[9]:
In [10]:
o.z
In [11]:
del o.y
o.y
Такой объект похож на словарь, ключами которого являются имена атрибутов: можно узнать значение атрибута, изменить его, добавить новый или удалить старый. Это и неудивительно: для реализации атрибутов объекта используется именно словарь.
In [12]:
o.__dict__
Out[12]:
Класс вводит пространство имён. В описании класса мы определяем его атрибуты (атрибуты, являющиеся функциями, называются методами). Потом эти атрибуты можно использовать как Class.attribute
. Принято, чтобы имена классов начинались с заглавной буквы.
Вот более полный пример класса. В нём есть док-строка, метод f
, статический атрибут x
(атрибут класса, а не конкретного объекта) и статический метод getx
(опять же принадлежащий классу, а не конкретному объекту).
In [13]:
class S:
'''Простой класс'''
x = 1
def f(self):
print(self)
@staticmethod
def getx():
return S.x
Заклинание тёмной магии, начинающееся с @
, называется декоратором. Запись
@dec
def fun(x):
...
эквивалентна
def fun(x):
...
fun=dec(fun)
То есть dec
- это функция, параметр которой - функция, и он возвращает эту функцию, преобразованную некоторым образом. Мы не будем обсуждать, как самим сочинять такие заклинания - за этим обращайтесь в Дурмстранг.
Функция dir
возвращает список атрибутов класса. Чтобы не смотреть снова на атрибуты, унаследованные от object
, мы их вычтем.
In [14]:
set(dir(S)) - set(dir(object))
Out[14]:
In [15]:
dict(S.__dict__)
Out[15]:
In [16]:
S.x
Out[16]:
In [17]:
S.x = 2
S.x
Out[17]:
In [18]:
S.f, S.getx
Out[18]:
In [19]:
S.getx()
Out[19]:
Теперь создадим объект этого класса.
In [20]:
o = S()
o, type(o)
Out[20]:
Метод класса можно вызвать и через объект.
In [21]:
o.getx()
Out[21]:
Следующее присваивание создаёт атрибут объекта o
с именем x
. Когда мы запрашиваем o.x
, атрибут x
ищется сначала в объекте o
, а если он там не найден - в его классе. В данном случае он найдётся в объекте o
. На атрибут класса S.x
это присваивание не влияет.
In [22]:
o.x = 5
o.x, S.x
Out[22]:
Как мы уже обсуждали, можно вызвать метод класса S.f
с каким-нибудь аргументом, например, o
.
In [23]:
S.f(o)
Следующий вызов означает в точности то же самое. Интерпретатор питон фактически преобразует его в предыдущий.
In [24]:
o.f()
То есть текущий объект передаётся методу в качестве первого аргумента. Этот первый аргумент любого метода принято называть self
. В принципе, Вы можете назвать его как угодно, но это затруднит понимание Вашего класса читателями, воспитанными в этой традиции.
Отличие метода класса (@staticmethod
) от метода объекта состоит в том, что такое автоматическое вставление первого аргумента не производится.
o.f
- это связанный метод: S.f
связанный с объектом o
.
In [25]:
o.f
Out[25]:
Док-строка доступна как атрибут __doc__
и используется функцией help
.
In [26]:
S.__doc__
Out[26]:
In [27]:
help(S)
Классу можно добавить новый атрибут налету (равно как и удалить имеющийся).
In [28]:
S.y = 2
S.y
Out[28]:
Можно добавить и атрибут, являющийся функцией, т.е. метод. Сначала опишем (вне тела класса!) какую-нибудь функцию, а потом добавим её к классу в качестве нового метода.
In [29]:
def g(self):
print(self.y)
S.g = g
o.g()
Менять класс налету таким образом - плохая идея. Когда в каком-то месте программы Вы видете, что используется какой-то объект некоторого класса, первое, что Вы сделаете - это посмотрите определение этого класса. И если текущее его состояние отлично от его определения, это сильно затрудняет понимание программы.
Класс S
, который мы рассмотрели в качестве примера - отнюдь не пример для подражания. В нормальном объектно-ориентированном подходе объект класса должен создаваться в допустимом (пригодном к использованию) состоянии, со всеми необходимыми атрибутами. В других языках за это твечает конструктор. В питоне аналогичную роль играет метод инициализации __init__
. Вот пример такого класса.
In [30]:
class C:
def __init__(self, x):
self.x = x
def getx(self):
return self.x
def setx(self, x):
self.x = x
Теперь для создания объекта мы должны вызвать C
с одним аргументом x
(первый аргумент метода __init__
, self
, это свежесозданный объект, в котором ещё ничего нет и который надо инициализировать).
In [31]:
o = C(1)
o.getx()
Out[31]:
In [32]:
o.setx(2)
o.getx()
Out[32]:
Этот класс - тоже не пример для подражания. В некоторых объектно-ориентированных языках считается некошерным напрямую читать и писать атрибуты; считается, что вся работа должна производиться через вызов методов. В питоне этот предрассудок не разделяют. Так что писать методы типа getx
и setx
абсолютно излишне. Они не добавляют никакой полезной функциональности - всё можно сделать, просто используя атрибут x
.
In [33]:
o.x
Out[33]:
Любой объектно-ориентированный язык, заслуживающий такого названия, поддерживает наследование. Класс C2
наследует от C
. Его объекты являются вполне законными для класса C
(имеют атрибут x
), но в добавок к этому имеют ещё и атрибут y
. Метод __init__
теперь должен иметь 2 параметра x
и y
(не считая обязательного self
). К методам getx
и setx
, унаследованным от C
, добавляются методы gety
и sety
.
Чтобы инициализировать атрибут x
, который был в родительском классе, мы могли бы, конечно, скопировать код из метода __init__
класса C
. В данном случае он столь прост, что это не преступление. Но, вообще говоря, копировать куски кода из одного места в другое категорически не рекомендуется. Допустим, в скопированном куске найден и исправлен баг. А в копии он остался. Поэтому для инициализации нового объекта, рассматриваемого как объект родительского класса C
, нам следует вызвать метод __init__
класса C
, а после этого довавить инициализацию атрибута y
, специфичного для дочернего класса C2
. Первую часть задачи можно выполнить, вызвав C.__init__(self,x)
(мы ведь только что написали строчку class
, в которой указали, что класс-предок называется C
). Но есть более универсальный метод, не требующий второй раз писать имя родительского класса. Функция super() возвращает текущий объект self
, рассматриваемый как объект родительского класса C
. Поэтому мы можем написать super().__init__(x)
.
Конечно, не только __init__
, но и другие методы дочернего класса могут захотеть вызвать методы родительского класса. Для этого используется либо вызов через имя родительского класса, либо super()
.
In [34]:
class C2(C):
def __init__(self, x, y):
super().__init__(x)
self.y = y
def gety(self):
return self.y
def sety(self, y):
self.y = y
In [35]:
o = C2(1, 2)
o.getx(), o.gety()
Out[35]:
o
является объектом класса C2
, а также его родительского класса C
(и, конечно, класса object
), но не является объектом класса S
.
In [36]:
isinstance(o, C2), isinstance(o, C), isinstance(o, object), isinstance(o, S)
Out[36]:
C2
является подклассом (потомком) себя, класса C
и object
, но не является подклассом S
.
In [37]:
issubclass(C2, C2), issubclass(C2, C), issubclass(C2, object), issubclass(C2, S)
Out[37]:
Эти функции используются редко. В питоне придерживаются принципа утиной типизации: если объект ходит, как утка, плавает, как утка, и крякает, как утка, значит, он утка. Пусть у нас есть класс Утка
с методами иди
, плыви
и крякни
. Конечно, можно создать подкласс Кряква
, наследующий эти методы и что-то в них переопределяющий. Но можно написать класс Кряква
с нуля, без всякой генетической связи с классом Утка
, и реализовать эти методы. Тогда в любую программу, ожидающую получить объект класса Утка
(и общающуюся с ним при помощи методов иди
, плыви
и крякни
),
можно вместо этого подставить объект класса Кряква
, и программа будет по-прежнему работать. А функции isinstance
и issubclass
нарушают принцип утиной типизации.
Класс может наследовать от нескольких классов. Мы не будем обсуждать множественное наследование, оно используется редко. Атрибут __bases__
даёт кортеж родительских классов.
In [38]:
C2.__bases__
Out[38]:
In [39]:
C.__bases__
Out[39]:
In [40]:
object.__bases__
Out[40]:
In [41]:
set(dir(C)) - set(dir(object))
Out[41]:
In [42]:
set(dir(C2)) - set(dir(object))
Out[42]:
In [43]:
set(dir(C2)) - set(dir(C))
Out[43]:
In [44]:
help(C2)
В питоне все методы являются, в терминах других языков, виртуальными. Пусть у нас есть класс A
; метод get
вызывает метод str
.
In [45]:
class A:
def __init__(self, x):
self.x = x
def str(self):
return str(self.x)
def get(self):
print(self.str())
return self.x
Класс B
наследует от него и переопределяет метод str
.
In [46]:
class B(A):
def str(self):
return 'The value of x is ' + super().str()
Создадим объект класса A
и вызовем метод get
. Он вызывает self.str()
; str
ищется (и находится) в классе A
.
In [47]:
oa = A(1)
oa.get()
Out[47]:
Теперь создадим объект класса B
и вызовем метод get
. Он ищется в B
, не находится, потом ищется и находится в A
. Этот метод A.get(ob)
вызывает self.str()
, где self
- это ob
. Поэтому метод str
ищется в классе B
, находится и вызывается. То есть метод родительского класса вызывает переопределённый метод дочернего класса.
In [48]:
ob = B(1)
ob.get()
Out[48]:
Напишем класс 2-мерных векторов, определяющий некоторые специальные методы для того, чтобы к его объектам можно было применять встроенные операции и функции языка питон (в тех случаях, когда это имеет смысл).
In [49]:
from math import sqrt
In [50]:
class Vec2:
'2-dimensional vectors'
def __init__(self, x = 0, y = 0):
self.x = x
self.y = y
def __repr__(self):
return 'Vec2(%d, %d)' % (self.x, self.y)
def __str__(self):
return '(%d, %d)' % (self.x, self.y)
def __bool__(self):
return self.x != 0 or self.y != 0
def __eq__(self, other):
return self.x == other.x and self.y == other.y
def __abs__(self):
return sqrt(self.x ** 2 + self.y ** 2)
def __neg__(self):
return Vec2(-self.x, -self.y)
def __add__(self,other):
return Vec2(self.x + other.x, self.y+ other.y)
def __sub__(self, other):
return Vec2(self.x - other.x, self.y - other.y)
def __iadd__(self,other):
self.x += other.x
self.y += other.y
return self
def __isub__(self, other):
self.x -= other.x
self.y -= other.y
return self
def __mul__(self, other):
return Vec2(self.x * other, self.y * other)
def __rmul__(self, other):
return Vec2(self.x * other, self.y * other)
def __imul__(self, other):
self.x *= other
self.y *= other
return self
def __truediv__(self, other):
return Vec2(self.x / other, self.y / other)
def __itruediv__(self, other):
self.x /= other
self.y /= other
return self
Создадим вектор. Когда в командной строке питона написано выражение, его значение печатается при помощи метода __repr__
. Он старается напечатать объект в таком виде, чтобы эту строку можно было вставить в исходный текст программы и воссоздать этот объект. (Для объектов некоторых классов это невозможно, тогда __repr__
печатает некоторую информацию в угловых скобках <...>).
In [51]:
u = Vec2(1, 2)
u
Out[51]:
Метод __str__
печатает объект в виде, наиболее простом для восприятия человека (не обязательно машинно-читаемом). Функция print
использует этот метод.
In [52]:
print(u)
Это выражение автоматически преобразуется в следующий вызов.
In [53]:
u * 2
Out[53]:
In [54]:
u.__mul__(2)
Out[54]:
А это выражение - в следующий.
In [55]:
3 * u, u.__rmul__(3)
Out[55]:
Такой оператор преобразуется в вызов u.__imul__(2)
.
In [56]:
u *= 2
u
Out[56]:
Другие арифметические операторы работают аналогично.
In [57]:
v = Vec2(-1, 2)
2 * u + 3 * v
Out[57]:
Унарный минус пеобразуется в __neg__
.
In [58]:
-v, v.__neg__()
Out[58]:
Вызов встроенной функции abs
- в метод __abs__
.
In [59]:
abs(u), u.__abs__()
Out[59]:
In [60]:
u += v
u
Out[60]:
Питон позволяет переопределять то, что происходит при чтении и записи атрибута (а также при его удалении). Эту тёмную магию мы изучать не будем, за одним исключением. Можно определить пару методов, один из которых будет вызываться при чтении некоторого "атрибута", а другой при его записи. Такой "атрибут", которого на самом деле нет, называется свойством. Пользователь класса будес спокойно читать и писать этот "атрибут", не подозревая, что на самом деле для этого вызываются какие-то методы.
В питоне нет приватных атрибутов (в том числе приватных методов). По традиции, атрибуты (включая методы), имена которых начинаются с _
, считаются приватными. Технически ничто не мешает пользователю класса обращаться к таким "приватным" атрибутам. Но автор класса может в любой момент изменить детали реализации, включая "приватные" атрибуты. Использующий их код пользователя при этом сломается. Сам дурак.
В этом классе есть свойство x
. Его чтение и запись приводят к вызову пары методов, которые читают и пишут "приватный" атрибут _x
, а также выполняют некоторый код. Свойство создаётся при помощи декораторов. В принципе свойство может быть и чисто синтетическим (без соответствующего "приватного" атрибута) - его "чтение" возвращает результат некоторого вычисления, исходящего из реальных атрибутов, а "запись" меняет значения таких реальных атрибутов.
In [61]:
class D:
def __init__(self, x):
self._x = x
@property
def x(self):
print('getting x')
return self._x
@x.setter
def x(self, x):
print('setting x')
self._x = x
In [62]:
o = D('a')
o.x
Out[62]:
In [63]:
o.x = 'b'
In [64]:
o.x
Out[64]:
Всякие недопустимые операции типа деления на 0 или открытия несуществующего файла приводят к возбуждению исключений. Интерпретатор питон печатает подробную и понятную информацию об исключении. Если это интерактивный интерпретатор, то сессия продолжается; если это программа, то её выполнение прекращается. В питоне отладчик приходится использовать гораздо реже, чем в более низкоуровневых языках, потому что эти сообщения интерпретатора позволяют сразу понять, где и что неверно. Впрочем, иногда приходится использовать и отладчик. Допустим, из сообщения об ошибке Вы поняли, что некоторая функция вызвана со строковым аргументом, а Вы про него думали, что он число. Тогда приходится искать - какая сволочь испортила мою переменную?
In [65]:
1 / 0
Исключения можно отлавливать, и в случае, если они произошли, выполнять какой-нибудь корректирующий код.
In [66]:
try:
x = 0
x = 1 / x
except ZeroDivisionError:
x = 0
In [67]:
x
Out[67]:
In [68]:
try:
s = 'xyzzy'
f = open(s)
except IOError:
print('cannot open ' + s)
Исключения - это объекты. Класс Exception
являестя корнем дерева классов исключений. Этот объект можно поймать и исследовать.
In [69]:
try:
x = 1 / 0
except Exception as err:
print(type(err))
print(err)
print(repr(err))
print(err.args)
Если в Вашем коде возникла недопустимая ситуация, нужно возбудить исключение оператором raise
.
In [70]:
raise NameError('Hi there')
Вот более полезный пример.
In [71]:
def f(x):
if x == 0:
raise ValueError('x should not be 0')
return x
In [72]:
try:
x = f(1)
x = f(0)
except ValueError as err:
print(repr(err))
Естественно, можно определять свои классы исключений, наследуя от Exception
или от какого-нибудь его потомка, подходящего по смыслу. Именно так и нужно делать, чтобы Ваши исключения не путались с системными.
In [73]:
class MyError(Exception):
def __init__(self, value):
self.value = value
def __str__(self):
return str(self.value)
In [74]:
def f(x):
if x < 0:
raise MyError(x)
else:
return x
In [75]:
try:
x = f(2)
x = f(-2)
except MyError as err:
print(err)