питон

Объектно-ориентированное программирование

Питон является развитым объектно-ориентированным языком. Всё, с чем он работает, является объектами - целые числа, строки, словари, функции и т.д. Каждый объект принадлежит определённому типу (или классу, что одно и то же). Класс тоже является объектом. Классы наследуют друг от друга. Класс object является корнем дерева классов - каждый класс наследует от него прямо или через какие-то промежуточные классы.

In [1]:
object,type(object)
Out[1]:
(object, type)

Функция dir возвращает список атрибутов класса.

In [2]:
dir(object)
Out[2]:
['__class__',
 '__delattr__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__gt__',
 '__hash__',
 '__init__',
 '__le__',
 '__lt__',
 '__ne__',
 '__new__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__subclasshook__']

Атрибуты, имена которых начинаются и кончаются двойным подчерком, используются интерпретатором для особых целей. Например, атрибут __doc__ содержит док-строку.

In [3]:
object.__doc__
Out[3]:
'The most base type'
In [4]:
help(object)
Help on class object in module builtins:

class object
 |  The most base type

Ниже мы рассмотрим цели некоторых других специальных атрибутов.

Вот простейший класс. Поскольку не указано, от чего он наследует, он наследует от object.

In [5]:
class A:
    pass
In [6]:
A,type(A)
Out[6]:
(__main__.A, type)

Создать объект какого-то класса можно, вызвав имя класса как функцию (возможно, с какими-нибудь аргументами). Мы уже это видели: имена классов int, str, list и т.д. создают объекты этих классов.

In [7]:
o=A()
o,type(o)
Out[7]:
(<__main__.A at 0x7f01f8124278>, __main__.A)

Узнать, какому классу принадлежит объект, можно при помощи функции type или атрибута __class__.

In [8]:
type(o),o.__class__
Out[8]:
(__main__.A, __main__.A)

У только что созданного объекта o нет атрибутов. Их можно создавать (и удалять) налету.

In [9]:
o.x=1
o.y=2
o.x,o.y
Out[9]:
(1, 2)
In [10]:
o.z
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-10-c8c0d478b237> in <module>()
----> 1 o.z

AttributeError: 'A' object has no attribute 'z'
In [11]:
del o.y
o.y
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-11-68acd6859c06> in <module>()
      1 del o.y
----> 2 o.y

AttributeError: 'A' object has no attribute 'y'

Такой объект похож на словарь, ключами которого являются имена атрибутов: можно узнать значение атрибута, изменить его, добавить новый или удалить старый. Это и неудивительно: для реализации атрибутов объекта используется именно словарь.

In [12]:
o.__dict__
Out[12]:
{'x': 1}

Класс вводит пространство имён. В описании класса мы определяем его атрибуты (атрибуты, являющиеся функциями, называются методами). Потом эти атрибуты можно использовать как 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]:
{'__dict__', '__module__', '__weakref__', 'f', 'getx', 'x'}
In [15]:
dict(S.__dict__)
Out[15]:
{'__dict__': <attribute '__dict__' of 'S' objects>,
 '__doc__': 'Простой класс',
 '__module__': '__main__',
 '__weakref__': <attribute '__weakref__' of 'S' objects>,
 'f': <function __main__.S.f>,
 'getx': <staticmethod at 0x7f01f80ddf28>,
 'x': 1}
In [16]:
S.x
Out[16]:
1
In [17]:
S.x=2
S.x
Out[17]:
2
In [18]:
S.f,S.getx
Out[18]:
(<function __main__.S.f>, <function __main__.S.getx>)
In [19]:
S.getx()
Out[19]:
2

Теперь создадим объект этого класса.

In [20]:
o=S()
o,type(o)
Out[20]:
(<__main__.S at 0x7f01f8090748>, __main__.S)

Метод класса можно вызвать и через объект.

In [21]:
o.getx()
Out[21]:
2

Следующее присваивание создаёт атрибут объекта o с именем x. Когда мы запрашиваем o.x, атрибут x ищется сначала в объекте o, а если он там не найден - в его классе. В данном случае он найдётся в объекте o. На атрибут класса S.x это присваивание не влияет.

In [22]:
o.x=5
o.x,S.x
Out[22]:
(5, 2)

Как мы уже обсуждали, можно вызвать метод класса S.f с каким-нибудь аргументом, например, o.

In [23]:
S.f(o)
<__main__.S object at 0x7f01f8090748>

Следующий вызов означает в точности то же самое. Интерпретатор питон фактически преобразует его в предыдущий.

In [24]:
o.f()
<__main__.S object at 0x7f01f8090748>

То есть текущий объект передаётся методу в качестве первого аргумента. Этот первый аргумент любого метода принято называть self. В принципе, Вы можете назвать его как угодно, но это затруднит понимание Вашего класса читателями, воспитанными в этой традиции.

Отличие метода класса (@staticmethod) от метода объекта состоит в том, что такое автоматическое вставление первого аргумента не производится.

o.f - это связанный метод: S.f связанный с объектом o.

In [25]:
o.f
Out[25]:
<bound method S.f of <__main__.S object at 0x7f01f8090748>>

Док-строка доступна как атрибут __doc__ и используется функцией help.

In [26]:
S.__doc__
Out[26]:
'Простой класс'
In [27]:
help(S)
Help on class S in module __main__:

class S(builtins.object)
 |  Простой класс
 |  
 |  Methods defined here:
 |  
 |  f(self)
 |  
 |  ----------------------------------------------------------------------
 |  Static methods defined here:
 |  
 |  getx()
 |  
 |  ----------------------------------------------------------------------
 |  Data descriptors defined here:
 |  
 |  __dict__
 |      dictionary for instance variables (if defined)
 |  
 |  __weakref__
 |      list of weak references to the object (if defined)
 |  
 |  ----------------------------------------------------------------------
 |  Data and other attributes defined here:
 |  
 |  x = 2

Классу можно добавить новый атрибут налету (равно как и удалить имеющийся).

In [28]:
S.y=2
S.y
Out[28]:
2

Можно добавить и атрибут, являющийся функцией, т.е. метод. Сначала опишем (вне тела класса!) какую-нибудь функцию, а потом добавим её к классу в качестве нового метода.

In [29]:
def g(self):
    print(self.y)
S.g=g
o.g()
2

Менять класс налету таким образом - плохая идея. Когда в каком-то месте программы Вы видете, что используется какой-то объект некоторого класса, первое, что Вы сделаете - это посмотрите определение этого класса. И если текущее его состояние отлично от его определения, это сильно затрудняет понимание программы.

Класс 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]:
1
In [32]:
o.setx(2)
o.getx()
Out[32]:
2

Этот класс - тоже не пример для подражания. В некоторых объектно-ориентированных языках считается некошерным напрямую читать и писать атрибуты; считается, что вся работа должна производиться через вызов методов. В питоне этот предрассудок не разделяют. Так что писать методы типа getx и setx абсолютно излишне. Они не добавляют никакой полезной функциональности - всё можно сделать, просто используя атрибут x.

In [33]:
o.x
Out[33]:
2

Любой объектно-ориентированный язык, заслуживающий такого названия, поддерживает наследование. Класс 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]:
(1, 2)

o является объектом класса C2, а также его родительского класса C (и, конечно, класса object), но не является объектом класса S.

In [36]:
isinstance(o,C2),isinstance(o,C),isinstance(o,object),isinstance(o,S)
Out[36]:
(True, True, True, False)

C2 является подклассом (потомком) себя, класса C и object, но не является подклассом S.

In [37]:
issubclass(C2,C2),issubclass(C2,C),issubclass(C2,object),issubclass(C2,S)
Out[37]:
(True, True, True, False)

Эти функции используются редко. В питоне придерживаются принципа утиной типизации: если объект ходит, как утка, плавает, как утка, и крякает, как утка, значит, он утка. Пусть у нас есть класс Утка с методами иди, плыви и крякни. Конечно, можно создать подкласс Кряква, наследующий эти методы и что-то в них переопределяющий. Но можно написать класс Кряква с нуля, без всякой генетической связи с классом Утка, и реализовать эти методы. Тогда в любую программу, ожидающую получить объект класса Утка (и общающуюся с ним при помощи методов иди, плыви и крякни), можно вместо этого подставить объект класса Кряква, и программа будет по-прежнему работать. А функции isinstance и issubclass нарушают принцип утиной типизации.

Класс может наследовать от нескольких классов. Мы не будем обсуждать множественное наследование, оно используется редко. Атрибут __bases__ даёт кортеж родительских классов.

In [38]:
C2.__bases__
Out[38]:
(__main__.C,)
In [39]:
C.__bases__
Out[39]:
(object,)
In [40]:
object.__bases__
Out[40]:
()
In [41]:
set(dir(C))-set(dir(object))
Out[41]:
{'__dict__', '__module__', '__weakref__', 'getx', 'setx'}
In [42]:
set(dir(C2))-set(dir(object))
Out[42]:
{'__dict__', '__module__', '__weakref__', 'getx', 'gety', 'setx', 'sety'}
In [43]:
set(dir(C2))-set(dir(C))
Out[43]:
{'gety', 'sety'}
In [44]:
help(C2)
Help on class C2 in module __main__:

class C2(C)
 |  Method resolution order:
 |      C2
 |      C
 |      builtins.object
 |  
 |  Methods defined here:
 |  
 |  __init__(self, x, y)
 |      Initialize self.  See help(type(self)) for accurate signature.
 |  
 |  gety(self)
 |  
 |  sety(self, y)
 |  
 |  ----------------------------------------------------------------------
 |  Methods inherited from C:
 |  
 |  getx(self)
 |  
 |  setx(self, x)
 |  
 |  ----------------------------------------------------------------------
 |  Data descriptors inherited from C:
 |  
 |  __dict__
 |      dictionary for instance variables (if defined)
 |  
 |  __weakref__
 |      list of weak references to the object (if defined)

В питоне все методы являются, в терминах других языков, виртуальными. Пусть у нас есть класс 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()
1
Out[47]:
1

Теперь создадим объект класса B и вызовем метод get. Он ищется в B, не находится, потом ищется и находится в A. Этот метод A.get(ob) вызывает self.str(), где self - это ob. Поэтому метод str ищется в классе B, находится и вызывается. То есть метод родительского класса вызывает переопределённый метод дочернего класса.

In [48]:
ob=B(1)
ob.get()
The value of x is 1
Out[48]:
1

Напишем класс 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]:
Vec2(1,2)

Метод __str__ печатает объект в виде, наиболее простом для восприятия человека (не обязательно машинно-читаемом). Функция print использует этот метод.

In [52]:
print(u)
(1,2)

Это выражение автоматически преобразуется в следующий вызов.

In [53]:
u*2
Out[53]:
Vec2(2,4)
In [54]:
u.__mul__(2)
Out[54]:
Vec2(2,4)

А это выражение - в следующий.

In [55]:
3*u,u.__rmul__(3)
Out[55]:
(Vec2(3,6), Vec2(3,6))

Такой оператор преобразуется в вызов u.__imul__(2).

In [56]:
u*=2
u
Out[56]:
Vec2(2,4)

Другие арифметические операторы работают аналогично.

In [57]:
v=Vec2(-1,2)
2*u+3*v
Out[57]:
Vec2(1,14)

Унарный минус пеобразуется в __neg__.

In [58]:
-v,v.__neg__()
Out[58]:
(Vec2(1,-2), Vec2(1,-2))

Вызов встроенной функции abs - в метод __abs__.

In [59]:
abs(u),u.__abs__()
Out[59]:
(4.47213595499958, 4.47213595499958)
In [60]:
u+=v
u
Out[60]:
Vec2(1,6)

Питон позволяет переопределять то, что происходит при чтении и записи атрибута (а также при его удалении). Эту тёмную магию мы изучать не будем, за одним исключением. Можно определить пару методов, один из которых будет вызываться при чтении некоторого "атрибута", а другой при его записи. Такой "атрибут", которого на самом деле нет, называется свойством. Пользователь класса будес спокойно читать и писать этот "атрибут", не подозревая, что на самом деле для этого вызываются какие-то методы.

В питоне нет приватных атрибутов (в том числе приватных методов). По традиции, атрибуты (включая методы), имена которых начинаются с _, считаются приватными. Технически ничто не мешает пользователю класса обращаться к таким "приватным" атрибутам. Но автор класса может в любой момент изменить детали реализации, включая "приватные" атрибуты. Использующий их код пользователя при этом сломается. Сам дурак.

В этом классе есть свойство 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
getting x
Out[62]:
'a'
In [63]:
o.x='b'
setting x
In [64]:
o.x
getting x
Out[64]:
'b'

Я использовал свойство один раз, когда писал Монте-Карловское моделирование модели Изинга. У изинговской решётки было свойство - температура, которую можно было читать и писать. Но соответствующего атрибута не было. Был атрибут $x=\exp(-J/T)$, где $J$ - энергия взаимодействия.

Свойства полезны также для обёртки GUI библиотек. Например, окно имеет свойство - заголовок. Чтение или изменение заголовка требует вызова соответствующих функций из низкоуровневой библиотеки (на C или C++). Но на питоне гораздо приятнее написать

w.title='Моё окно'

Исключения

Всякие недопустимые операции типа деления на 0 или открытия несуществующего файла приводят к возбуждению исключений. Интерпретатор питон печатает подробную и понятную информацию об исключении. Если это интерактивный интерпретатор, то сессия продолжается; исли это программа, то её выполнение прекращается. В питоне отладчик приходится использовать гораздо реже, чем в более низкоуровневых языках, потому что эти сообщения интерпретатора позволяют сразу понять, где и что неверно. Впрочем, иногда приходится использовать и отладчик. Допустим, из сообщения об ошибке Вы поняли, что некоторая функция вызвана со строковым аргументом, а Вы про него думали, что он число. Тогда приходится искать - какая сволочь испортила мою переменную?

In [65]:
1/0
---------------------------------------------------------------------------
ZeroDivisionError                         Traceback (most recent call last)
<ipython-input-65-05c9758a9c21> in <module>()
----> 1 1/0

ZeroDivisionError: division by zero

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

In [66]:
try:
    x=0
    x=1/x
except ZeroDivisionError:
    x=0
In [67]:
x
Out[67]:
0
In [68]:
try:
    s='xyzzy'
    f=open(s)
except IOError:
    print('cannot open '+s)
cannot open xyzzy

Исключения - это объекты. Класс Exception являестя корнем дерева классов исключений. Этот объект можно поймать и исследовать.

In [69]:
try:
    x=1/0
except Exception as err:
    print(type(err))
    print(err)
    print(repr(err))
    print(err.args)
<class 'ZeroDivisionError'>
division by zero
ZeroDivisionError('division by zero',)
('division by zero',)

Если в Вашем коде возникла недопустимая ситуация, нужно возбудить исключение оператором raise.

In [70]:
raise NameError('Hi there')
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-70-d36a3cf2a944> in <module>()
----> 1 raise NameError('Hi there')

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))
ValueError('x should not be 0',)

Естественно, можно определять свои классы исключений, наследуя от 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)
-2