object, type(object)

"""
Explanation: Кафедра дискретной математики МФТИ
Курс математической статистики
Никита Волков
На основе http://www.inp.nsk.su/~grozin/python/
Объектно-ориентированное программирование
Питон является развитым объектно-ориентированным языком. Всё, с чем он работает, является объектами - целые числа, строки, словари, функции и т.д. Каждый объект принадлежит определённому типу (или классу, что одно и то же). Класс тоже является объектом. Классы наследуют друг от друга. Класс object является корнем дерева классов - каждый класс наследует от него прямо или через какие-то промежуточные классы.
End of explanation
"""


dir(object)

"""
Explanation: Функция dir возвращает список атрибутов класса.
End of explanation
"""


object.__doc__

help(object)

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


class A:
    pass

A, type(A)

"""
Explanation: Ниже мы рассмотрим цели некоторых других специальных атрибутов.
Вот простейший класс. Поскольку не указано, от чего он наследует, он наследует от object.
End of explanation
"""


o = A()
o, type(o)

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


type(o), o.__class__

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


o.x = 1
o.y = 2
o.x, o.y

o.z

del o.y
o.y

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


o.__dict__

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


class S:
    '''Простой класс'''
    
    x = 1
    
    def f(self):
        print(self)
    
    @staticmethod
    def getx():
        return S.x

"""
Explanation: Класс вводит пространство имён. В описании класса мы определяем его атрибуты (атрибуты, являющиеся функциями, называются методами). Потом эти атрибуты можно использовать как Class.attribute. Принято, чтобы имена классов начинались с заглавной буквы.
Вот более полный пример класса. В нём есть док-строка, метод f, статический атрибут x (атрибут класса, а не конкретного объекта) и статический метод getx (опять же принадлежащий классу, а не конкретному объекту).
End of explanation
"""


set(dir(S)) - set(dir(object))

dict(S.__dict__)

S.x

S.x = 2
S.x

S.f, S.getx

S.getx()

"""
Explanation: Заклинание тёмной магии, начинающееся с @, называется декоратором. Запись
@dec
def fun(x):
    ...
эквивалентна
def fun(x):
    ...
fun=dec(fun)
То есть dec - это функция, параметр которой - функция, и он возвращает эту функцию, преобразованную некоторым образом. Мы не будем обсуждать, как самим сочинять такие заклинания - за этим обращайтесь в Дурмстранг.
Функция dir возвращает список атрибутов класса. Чтобы не смотреть снова на атрибуты, унаследованные от object, мы их вычтем.
End of explanation
"""


o = S()
o, type(o)

"""
Explanation: Теперь создадим объект этого класса.
End of explanation
"""


o.getx()

"""
Explanation: Метод класса можно вызвать и через объект.
End of explanation
"""


o.x = 5
o.x, S.x

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


S.f(o)

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


o.f()

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


o.f

"""
Explanation: То есть текущий объект передаётся методу в качестве первого аргумента. Этот первый аргумент любого метода принято называть self. В принципе, Вы можете назвать его как угодно, но это затруднит понимание Вашего класса читателями, воспитанными в этой традиции.
Отличие метода класса (@staticmethod) от метода объекта состоит в том, что такое автоматическое вставление первого аргумента не производится.
o.f - это связанный метод: S.f связанный с объектом o.
End of explanation
"""


S.__doc__

help(S)

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


S.y = 2
S.y

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


def g(self):
    print(self.y)

S.g = g
o.g()

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


class C:
    
    def __init__(self, x):
        self.x = x
        
    def getx(self):
        return self.x
    
    def setx(self, x):
        self.x = x

"""
Explanation: Менять класс налету таким образом - плохая идея. Когда в каком-то месте программы Вы видете, что используется какой-то объект некоторого класса, первое, что Вы сделаете - это посмотрите определение этого класса. И если текущее его состояние отлично от его определения, это сильно затрудняет понимание программы.
Класс S, который мы рассмотрели в качестве примера - отнюдь не пример для подражания. В нормальном объектно-ориентированном подходе объект класса должен создаваться в допустимом (пригодном к использованию) состоянии, со всеми необходимыми атрибутами. В других языках за это твечает конструктор. В питоне аналогичную роль играет метод инициализации __init__. Вот пример такого класса.
End of explanation
"""


o = C(1)
o.getx()

o.setx(2)
o.getx()

"""
Explanation: Теперь для создания объекта мы должны вызвать C с одним аргументом x (первый аргумент метода __init__, self, это свежесозданный объект, в котором ещё ничего нет и который надо инициализировать).
End of explanation
"""


o.x

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


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()

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


isinstance(o, C2), isinstance(o, C), isinstance(o, object), isinstance(o, S)

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


issubclass(C2, C2), issubclass(C2, C), issubclass(C2, object), issubclass(C2, S)

"""
Explanation: C2 является подклассом (потомком) себя, класса C и object, но не является подклассом S.
End of explanation
"""


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)

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


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

"""
Explanation: В питоне все методы являются, в терминах других языков, виртуальными. Пусть у нас есть класс A; метод get вызывает метод str.
End of explanation
"""


class B(A):
    
    def str(self):
        return 'The value of x is ' + super().str()

"""
Explanation: Класс B наследует от него и переопределяет метод str.
End of explanation
"""


oa = A(1)
oa.get()

"""
Explanation: Создадим объект класса A и вызовем метод get. Он вызывает self.str(); str ищется (и находится) в классе A.
End of explanation
"""


ob = B(1)
ob.get()

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


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

"""
Explanation: Напишем класс 2-мерных векторов, определяющий некоторые специальные методы для того, чтобы к его объектам можно было применять встроенные операции и функции языка питон (в тех случаях, когда это имеет смысл).
End of explanation
"""


u = Vec2(1, 2)
u

"""
Explanation: Создадим вектор. Когда в командной строке питона написано выражение, его значение печатается при помощи метода __repr__. Он старается напечатать объект в таком виде, чтобы эту строку можно было вставить в исходный текст программы и воссоздать этот объект. (Для объектов некоторых классов это невозможно, тогда __repr__ печатает некоторую информацию в угловых скобках <...>).
End of explanation
"""


print(u)

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


u * 2

u.__mul__(2)

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


3 * u, u.__rmul__(3)

"""
Explanation: А это выражение - в следующий.
End of explanation
"""


u *= 2
u

"""
Explanation: Такой оператор преобразуется в вызов u.__imul__(2).
End of explanation
"""


v = Vec2(-1, 2)
2 * u + 3 * v

"""
Explanation: Другие арифметические операторы работают аналогично.
End of explanation
"""


-v, v.__neg__()

"""
Explanation: Унарный минус пеобразуется в __neg__.
End of explanation
"""


abs(u), u.__abs__()

u += v
u

"""
Explanation: Вызов встроенной функции abs - в метод __abs__.
End of explanation
"""


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

"""
Explanation: Питон позволяет переопределять то, что происходит при чтении и записи атрибута (а также при его удалении). Эту тёмную магию мы изучать не будем, за одним исключением. Можно определить пару методов, один из которых будет вызываться при чтении некоторого "атрибута", а другой при его записи. Такой "атрибут", которого на самом деле нет, называется свойством. Пользователь класса будес спокойно читать и писать этот "атрибут", не подозревая, что на самом деле для этого вызываются какие-то методы.
В питоне нет приватных атрибутов (в том числе приватных методов). По традиции, атрибуты (включая методы), имена которых начинаются с _, считаются приватными. Технически ничто не мешает пользователю класса обращаться к таким "приватным" атрибутам. Но автор класса может в любой момент изменить детали реализации, включая "приватные" атрибуты. Использующий их код пользователя при этом сломается. Сам дурак.
В этом классе есть свойство x. Его чтение и запись приводят к вызову пары методов, которые читают и пишут "приватный" атрибут _x, а также выполняют некоторый код. Свойство создаётся при помощи декораторов. В принципе свойство может быть и чисто синтетическим (без соответствующего "приватного" атрибута) - его "чтение" возвращает результат некоторого вычисления, исходящего из реальных атрибутов, а "запись" меняет значения таких реальных атрибутов.
End of explanation
"""


1 / 0

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


try:
    x = 0
    x = 1 / x
except ZeroDivisionError:
    x = 0

x

try:
    s = 'xyzzy'
    f = open(s)
except IOError:
    print('cannot open ' + s)

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


try:
    x = 1 / 0
except Exception as err:
    print(type(err))
    print(err)
    print(repr(err))
    print(err.args)

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


raise NameError('Hi there')

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


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))

"""
Explanation: Вот более полезный пример.
End of explanation
"""


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)

"""
Explanation: Естественно, можно определять свои классы исключений, наследуя от Exception или от какого-нибудь его потомка, подходящего по смыслу. Именно так и нужно делать, чтобы Ваши исключения не путались с системными.
End of explanation
"""


