Питон является развитым объектно-ориентированным языком. Всё, с чем он работает, является объектами - целые числа, строки, словари, функции и т.д. Каждый объект принадлежит определённому типу (или классу, что одно и то же). Класс тоже является объектом. Классы наследуют друг от друга. Класс 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,o.getx()
Как мы уже обсуждали, можно вызвать метод класса S.f с каким-нибудь аргументом, например, o.
S.f(o)
Следующий вызов означает в точности то же самое. Интерпретатор питон фактически преобразует его в предыдущий.
o.f()
То есть текущий объект передаётся методу в качестве первого аргумента. Этот первый аргумент любого метода принято называть self. В принципе, Вы можете назвать его как угодно, но это затруднит понимание Вашего класса читателями, воспитанными в этой традиции.
Отличие метода класса (@staticmethod) от метода объекта состоит в том, что такое автоматическое вставление первого аргумента не производится.
o.f - это связанный метод: S.f связанный с объектом o.
o.f
g=o.f
g()
Док-строка доступна как атрибут __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 f'Vec2({self.x},{self.y})'
def __str__(self):
return f'({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='Моё окно'
Рассмотрим ещё один пример - класс рациональных чисел. В стандартной библиотеке уже есть такой класс, он называется Fraction, его и надо использовать в своих программах. Мы сейчас напишем несколько упрощённый его вариант.
from math import gcd
class Rat:
def __init__(self,n,d=1):
if d==0:
raise ValueError('Zero denominator')
elif d<0:
n,d=-n,-d
g=gcd(n,d)
self.n,self.d=n//g,d//g
# String representations
def __repr__(self):
return f'Rat({self.n},{self.d})'
def __str__(self):
return f'{self.n}/{self.d}'
# Unary operations
def __pos__(self):
return Rat(self.n,self.d)
def __neg__(self):
return Rat(-self.n,self.d)
def __abs__(self):
return Rat(abs(self.n),self.d)
# + -
def __add__(self,x):
if isinstance(x,Rat):
return Rat(self.n*x.d+self.d*x.n,self.d*x.d)
else:
return Rat(self.n+self.d*x,self.d)
def __radd__(self,x):
return self+x
def __iadd__(self,x):
if isinstance(x,Rat):
n,d=self.n*x.d+self.d*x.n,self.d*x.d
g=gcd(n,d)
self.n,self.d=n//g,d//g
else:
self.n=self.n+self.d*x
return self
def __sub__(self,x):
return self+(-x)
def __rsub__(self,x):
return x+(-self)
def __isub__(self,x):
self+=-x
return self
# *
def __mul__(self,x):
if isinstance(x,Rat):
return Rat(self.n*x.n,self.d*x.d)
else:
return Rat(self.n*x,self.d)
def __rmul__(self,x):
return self*x
def __imul__(self,x):
if isinstance(x,Rat):
n,d=self.n*x.n,self.d*x.d
g=gcd(n,d)
self.n,self.d=n//g,d//g
else:
g=gcd(x,self.d)
x,self.d=x//g,self.d//g
self.n=self.n*x
return self
# /
def inv(self):
return Rat(self.d,self.n)
def __truediv__(self,x):
return self*x.inv()
def __rtruediv__(self,x):
return (self/x).inv()
def __itruediv__(self,x):
if isinstance(x,Rat):
self*=x.inv()
else:
g=gcd(x,self.n)
x,self.n=x//g,self.n//g
self.n,self.d=x*self.d,self.n
return self
# **
def __pow__(self,x):
if x<0:
return self.inv()**(-x)
else:
return Rat(self.n**x,self.d**x)
def __ipow__(self,x):
if x<0:
self=self.inv()
self**=-x
else:
self.n**=self.n**x
self.d**=self.d**x
return self
# Comparisons
def __eq__(self,x):
if isinstance(x,Rat):
return self.n==x.n and self.d==x.d
else:
return self.d==1 and self.n==x
def __ne__(self,x):
return not self==x
def __lt__(self,x):
if isinstance(x,Rat):
return self.n*x.d<self.d*x.n
else:
return self.n<self.d*x
def __le__(self,x):
return self<x or self==x
def __gt__(self,x):
return not self<=x
def __ge__(self,x):
return not self<x
# float
def __float__(self):
return self.n/self.d
При инициализации рациональноо числа нужно сократить числитель и знаменатель на наибольший общий делитель (gcd), а также обеспечить, чтобы знаменатель был положительным. Если знаменатель нулевой, мы возбуждаем исключение (то подробно описано в следующем параграфе).
r=Rat(10,-6)
print(r)
r
Арифметические операции аналогичны классу Vec2. Методы, реализующие деление, называются __truediv__ и т.д., потому что есть ещё деление с остатком (__floordiv__; __mod__ - остаток).
Rat(1,2)+Rat(1,3)
x=Rat(1,2)
x+=1
x
x-=Rat(1,6)
x
3*x
Rat(3,2)/x
x**2
Выражение x>y преобразуется в x.__gt__(y) и т.д.
Rat(1,2)>Rat(1,3)
1<Rat(3,2)
Вызов встроенной функции float(x) преобразуется в вызов метода x.__float__().
float(x)