Питон является развитым объектно-ориентированным языком. Всё, с чем он работает, является объектами - целые числа, строки, словари, функции и т.д. Каждый объект принадлежит определённому типу (или классу, что одно и то же). Класс тоже является объектом. Классы наследуют друг от друга. Класс object
является корнем дерева классов - каждый класс наследует от него прямо или через какие-то промежуточные классы.
object,type(object)
Функция dir
возвращает список атрибутов класса.
dir(object)
Атрибуты, имена которых начинаются и кончаются двойным подчерком, используются интерпретатором для особых целей. Например, атрибут __doc__
содержит док-строку.
object.__doc__
help(object)
Ниже мы рассмотрим цели некоторых других специальных атрибутов.
Вот простейший класс. Поскольку не указано, от чего он наследует, он наследует от object
.
class A:
pass
A,type(A)
Создать объект какого-то класса можно, вызвав имя класса как функцию (возможно, с какими-нибудь аргументами). Мы уже это видели: имена классов int
, str
, list
и т.д. создают объекты этих классов.
o=A()
o,type(o)
Узнать, какому классу принадлежит объект, можно при помощи функции type
или атрибута __class__
.
type(o),o.__class__
У только что созданного объекта o
нет атрибутов. Их можно создавать (и удалять) налету.
o.x=1
o.y=2
o.x,o.y
o.z
del o.y
o.y
Такой объект похож на словарь, ключами которого являются имена атрибутов: можно узнать значение атрибута, изменить его, добавить новый или удалить старый. Это и неудивительно: для реализации атрибутов объекта используется именно словарь.
o.__dict__
Класс вводит пространство имён. В описании класса мы определяем его атрибуты (атрибуты, являющиеся функциями, называются методами). Потом эти атрибуты можно использовать как Class.attribute
. Принято, чтобы имена классов начинались с заглавной буквы.
Вот более полный пример класса. В нём есть док-строка, метод f
, статический атрибут x
(атрибут класса, а не конкретного объекта) и статический метод getx
(опять же принадлежащий классу, а не конкретному объекту).
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
, мы их вычтем.
set(dir(S))-set(dir(object))
dict(S.__dict__)
S.x
S.x=2
S.x
S.f,S.getx
S.getx()
Теперь создадим объект этого класса.
o=S()
o,type(o)
Метод класса можно вызвать и через объект.
o.getx()
Следующее присваивание создаёт атрибут объекта o
с именем x
. Когда мы запрашиваем o.x
, атрибут x
ищется сначала в объекте o
, а если он там не найден - в его классе. В данном случае он найдётся в объекте o
. На атрибут класса S.x
это присваивание не влияет.
o.x=5
o.x,S.x
Как мы уже обсуждали, можно вызвать метод класса S.f
с каким-нибудь аргументом, например, o
.
S.f(o)
Следующий вызов означает в точности то же самое. Интерпретатор питон фактически преобразует его в предыдущий.
o.f()
То есть текущий объект передаётся методу в качестве первого аргумента. Этот первый аргумент любого метода принято называть self
. В принципе, Вы можете назвать его как угодно, но это затруднит понимание Вашего класса читателями, воспитанными в этой традиции.
Отличие метода класса (@staticmethod
) от метода объекта состоит в том, что такое автоматическое вставление первого аргумента не производится.
o.f
- это связанный метод: S.f
связанный с объектом o
.
o.f
Док-строка доступна как атрибут __doc__
и используется функцией help
.
S.__doc__
help(S)
Классу можно добавить новый атрибут налету (равно как и удалить имеющийся).
S.y=2
S.y
Можно добавить и атрибут, являющийся функцией, т.е. метод. Сначала опишем (вне тела класса!) какую-нибудь функцию, а потом добавим её к классу в качестве нового метода.
def g(self):
print(self.y)
S.g=g
o.g()
Менять класс налету таким образом - плохая идея. Когда в каком-то месте программы Вы видете, что используется какой-то объект некоторого класса, первое, что Вы сделаете - это посмотрите определение этого класса. И если текущее его состояние отлично от его определения, это сильно затрудняет понимание программы.
Класс S
, который мы рассмотрели в качестве примера - отнюдь не пример для подражания. В нормальном объектно-ориентированном подходе объект класса должен создаваться в допустимом (пригодном к использованию) состоянии, со всеми необходимыми атрибутами. В других языках за это твечает конструктор. В питоне аналогичную роль играет метод инициализации __init__
. Вот пример такого класса.
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
, это свежесозданный объект, в котором ещё ничего нет и который надо инициализировать).
o=C(1)
o.getx()
o.setx(2)
o.getx()
Этот класс - тоже не пример для подражания. В некоторых объектно-ориентированных языках считается некошерным напрямую читать и писать атрибуты; считается, что вся работа должна производиться через вызов методов. В питоне этот предрассудок не разделяют. Так что писать методы типа getx
и setx
абсолютно излишне. Они не добавляют никакой полезной функциональности - всё можно сделать, просто используя атрибут x
.
o.x
Любой объектно-ориентированный язык, заслуживающий такого названия, поддерживает наследование. Класс 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()
.
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
o=C2(1,2)
o.getx(),o.gety()
o
является объектом класса C2
, а также его родительского класса C
(и, конечно, класса object
), но не является объектом класса S
.
isinstance(o,C2),isinstance(o,C),isinstance(o,object),isinstance(o,S)
C2
является подклассом (потомком) себя, класса C
и object
, но не является подклассом S
.
issubclass(C2,C2),issubclass(C2,C),issubclass(C2,object),issubclass(C2,S)
Эти функции используются редко. В питоне придерживаются принципа утиной типизации: если объект ходит, как утка, плавает, как утка, и крякает, как утка, значит, он утка. Пусть у нас есть класс Утка
с методами иди
, плыви
и крякни
. Конечно, можно создать подкласс Кряква
, наследующий эти методы и что-то в них переопределяющий. Но можно написать класс Кряква
с нуля, без всякой генетической связи с классом Утка
, и реализовать эти методы. Тогда в любую программу, ожидающую получить объект класса Утка
(и общающуюся с ним при помощи методов иди
, плыви
и крякни
),
можно вместо этого подставить объект класса Кряква
, и программа будет по-прежнему работать. А функции isinstance
и issubclass
нарушают принцип утиной типизации.
Класс может наследовать от нескольких классов. Мы не будем обсуждать множественное наследование, оно используется редко. Атрибут __bases__
даёт кортеж родительских классов.
C2.__bases__
C.__bases__
object.__bases__
set(dir(C))-set(dir(object))
set(dir(C2))-set(dir(object))
set(dir(C2))-set(dir(C))
help(C2)
В питоне все методы являются, в терминах других языков, виртуальными. Пусть у нас есть класс A
; метод get
вызывает метод str
.
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
.
class B(A):
def str(self):
return 'The value of x is '+super().str()
Создадим объект класса A
и вызовем метод get
. Он вызывает self.str()
; str
ищется (и находится) в классе A
.
oa=A(1)
oa.get()
Теперь создадим объект класса B
и вызовем метод get
. Он ищется в B
, не находится, потом ищется и находится в A
. Этот метод A.get(ob)
вызывает self.str()
, где self
- это ob
. Поэтому метод str
ищется в классе B
, находится и вызывается. То есть метод родительского класса вызывает переопределённый метод дочернего класса.
ob=B(1)
ob.get()
Напишем класс 2-мерных векторов, определяющий некоторые специальные методы для того, чтобы к его объектам можно было применять встроенные операции и функции языка питон (в тех случаях, когда это имеет смысл).
from math import sqrt
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__
печатает некоторую информацию в угловых скобках <...>).
u=Vec2(1,2)
u
Метод __str__
печатает объект в виде, наиболее простом для восприятия человека (не обязательно машинно-читаемом). Функция print
использует этот метод.
print(u)
Это выражение автоматически преобразуется в следующий вызов.
u*2
u.__mul__(2)
А это выражение - в следующий.
3*u,u.__rmul__(3)
Такой оператор преобразуется в вызов u.__imul__(2)
.
u*=2
u
Другие арифметические операторы работают аналогично.
v=Vec2(-1,2)
2*u+3*v
Унарный минус пеобразуется в __neg__
.
-v,v.__neg__()
Вызов встроенной функции abs
- в метод __abs__
.
abs(u),u.__abs__()
u+=v
u
Питон позволяет переопределять то, что происходит при чтении и записи атрибута (а также при его удалении). Эту тёмную магию мы изучать не будем, за одним исключением. Можно определить пару методов, один из которых будет вызываться при чтении некоторого "атрибута", а другой при его записи. Такой "атрибут", которого на самом деле нет, называется свойством. Пользователь класса будес спокойно читать и писать этот "атрибут", не подозревая, что на самом деле для этого вызываются какие-то методы.
В питоне нет приватных атрибутов (в том числе приватных методов). По традиции, атрибуты (включая методы), имена которых начинаются с _
, считаются приватными. Технически ничто не мешает пользователю класса обращаться к таким "приватным" атрибутам. Но автор класса может в любой момент изменить детали реализации, включая "приватные" атрибуты. Использующий их код пользователя при этом сломается. Сам дурак.
В этом классе есть свойство x
. Его чтение и запись приводят к вызову пары методов, которые читают и пишут "приватный" атрибут _x
, а также выполняют некоторый код. Свойство создаётся при помощи декораторов. В принципе свойство может быть и чисто синтетическим (без соответствующего "приватного" атрибута) - его "чтение" возвращает результат некоторого вычисления, исходящего из реальных атрибутов, а "запись" меняет значения таких реальных атрибутов.
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
o=D('a')
o.x
o.x='b'
o.x
Я использовал свойство один раз, когда писал Монте-Карловское моделирование модели Изинга. У изинговской решётки было свойство - температура, которую можно было читать и писать. Но соответствующего атрибута не было. Был атрибут $x=\exp(-J/T)$, где $J$ - энергия взаимодействия.
Свойства полезны также для обёртки GUI библиотек. Например, окно имеет свойство - заголовок. Чтение или изменение заголовка требует вызова соответствующих функций из низкоуровневой библиотеки (на C
или C++
). Но на питоне гораздо приятнее написать
w.title='Моё окно'
Всякие недопустимые операции типа деления на 0 или открытия несуществующего файла приводят к возбуждению исключений. Интерпретатор питон печатает подробную и понятную информацию об исключении. Если это интерактивный интерпретатор, то сессия продолжается; исли это программа, то её выполнение прекращается. В питоне отладчик приходится использовать гораздо реже, чем в более низкоуровневых языках, потому что эти сообщения интерпретатора позволяют сразу понять, где и что неверно. Впрочем, иногда приходится использовать и отладчик. Допустим, из сообщения об ошибке Вы поняли, что некоторая функция вызвана со строковым аргументом, а Вы про него думали, что он число. Тогда приходится искать - какая сволочь испортила мою переменную?
1/0
Исключения можно отлавливать, и в случае, если они произошли, выполнять какой-нибудь корректирующий код.
try:
x=0
x=1/x
except ZeroDivisionError:
x=0
x
try:
s='xyzzy'
f=open(s)
except IOError:
print('cannot open '+s)
Исключения - это объекты. Класс Exception
являестя корнем дерева классов исключений. Этот объект можно поймать и исследовать.
try:
x=1/0
except Exception as err:
print(type(err))
print(err)
print(repr(err))
print(err.args)
Если в Вашем коде возникла недопустимая ситуация, нужно возбудить исключение оператором raise
.
raise NameError('Hi there')
Вот более полезный пример.
def f(x):
if x==0:
raise ValueError('x should not be 0')
return x
try:
x=f(1)
x=f(0)
except ValueError as err:
print(repr(err))
Естественно, можно определять свои классы исключений, наследуя от Exception
или от какого-нибудь его потомка, подходящего по смыслу. Именно так и нужно делать, чтобы Ваши исключения не путались с системными.
class MyError(Exception):
def __init__(self,value):
self.value=value
def __str__(self):
return str(self.value)
def f(x):
if x<0:
raise MyError(x)
else:
return x
try:
x=f(2)
x=f(-2)
except MyError as err:
print(err)