Наследование и дескрипторы

Идея наследования состоит в том, что бы использовать описание предыдущего объекта для написания нового.

class C:
    var = 10

    def __init__(self,val):
        self.var = val

    def __str__(self):
        return "<{}>".format(self.var)

class D(C):
    pass

c = C(123)
d = D(456)
print(c,type(c),issubclass(C,D))
print(d,type(d),issubclass(D,C))

Написали два разных класса. Один является подклассом другого класса. Issubclass - возвращает флаг, указывающий на то, является ли указанный класс подклассом указанного класса (классов).

<123> <class '__main__.C'> False
<456> <class '__main__.D'> True
>>>

В процессе наследования Python отслеживает, кто от кого порождается. Изменим немного нашу программу. Добавим класс S.

class C:
    var = 10

    def __init__(self,val):
        self.var = val

    def __str__(self):
        return "<{}>".format(self.var)

class S(C):
    def __str__(self):
        s = C.__str__(self)
        return "{}:{}".format(s,type(self))
class D(S):
    pass

c = C(123)
s = S(100500)
print(s)
d = D(456)
print(c,type(c),issubclass(C,D))
print(d,type(d),issubclass(D,C))

Получилось, что у элемента типа s перебит str, у элемента типа c такого еще нет, а у элемента d наследовано от s.

<100500>:<class '__main__.S'>
<123> <class '__main__.C'> False
<456>:<class '__main__.D'> <class '__main__.D'> True

Класс D принял все поля, которые были у S. Очевидно, что в класс можно добавить свои методы и поля.

class C:
    var = 10

    def __init__(self,val):
        self.var = val

    def __str__(self):
        return "<{}>".format(self.var)

class S(C):
    def __str__(self):
        s = C.__str__(self)
        return "{}:{}".format(s,type(self))
class D(S):
    def newm(self):
        return self.var%2

c = C(123)
s = S(100500)
print(s)
d = D(456)
print(c,type(c),issubclass(C,D))
print(d,type(d),issubclass(D,C))

В наследовании есть хитрости. Напишем в предыдущей программе метод __add__(self).

class C:
    var = 10

    def __init__(self,val):
        self.var = val

    def __str__(self):
        return "<{}>".format(self.var)
    def __add__(self,other):
        return C(self.var+other.var)
    __repr__=__str__

class S(C):
    def __str__(self):
        s = C.__str__(self)
        return "{}:{}".format(s,type(self))
class D(S):
    def newm(self):
        return self.var%2

c = C(123)
s = S(100500)
print(s)
d = D(456)
print(c,type(c),issubclass(C,D))
print(d,type(d),issubclass(D,C))
Этот объект типа C и так не должно быть.
<100500>:<class '__main__.S'>
<123> <class '__main__.C'> False
<456>:<class '__main__.D'> <class '__main__.D'> True
>>> c+s
<100623>
>>> type(c+s)
<class '__main__.C'>
>>> type(s+c)
<class '__main__.C'>
>>> s+d
<100956>
>>>

Применим следующую конструкцию

    def __add__(self,other):
        return type(self)(self.var+other.var)

Получим

<100500>:<class '__main__.S'>
<123> <class '__main__.C'> False
<456>:<class '__main__.D'> <class '__main__.D'> True
>>> c+s
<__main__.C object at 0x02F3FA50>
>>> s+c
<__main__.S object at 0x031A6F10>
>>>

Помимо issubclass есть isinstance(). Эта функция поддерживает наследование. Для isinstance() экземпляр производного класса есть экземпляр его базового класса.

Замечание: объект D является экземпляром класса С.

>>> isinstance(c,C)
True
>>> isinstance(d,D)
True
>>> isinstance(d,C)
True
>>>

Поговорим о защите полей. В Python нет такого, что поля доступны только внутри методов объекта, а снаружи доступ закрыт. Перебить или не перебить поле становится элементом договоренности до определённого момента. Проблема: если я унаследовал класс, не читая, что есть в этом классе, а потом перебил метод, который не начинается на “__” – значит, я так хотел. А если он начинается на “__” – то внутри “кишки” родителя начинают ходить к другому полю.

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

class C:
    __var = 10
    def __str__(self):
        return "<{}>".format(self.__var)

Но если мы наследуем один класс от другого, то появляются легкие извращения.

class C:
    __var = 10
    def __str__(self):
        return "<{}>".format(self.__var)
class D(C):
    pass
c = C()
print(c)
d = D()
print(d)
Вывод
<10>
<10>
>>>

Определим метод следующим образом.

class C:
    __var = 10
    def __str__(self):
        return "<{}>".format(self.__var)
class D(C):
    def add(self):
        self.__var += 1
c = C()
print(c)
d = D()
print(d)
Получим, что
<10>
<10>
>>> d.add()
Traceback (most recent call last):
  File "<pyshell#0>", line 1, in <module>
    d.add()
  File "E:/Документы (не удалять)!!!/Alya/Рабочий стол/1.py", line 7, in add
    self.__var += 1
AttributeError: 'D' object has no attribute '_D__var'
>>>
>>> c.__var
Traceback (most recent call last):
  File "<pyshell#1>", line 1, in <module>
    c.__var
AttributeError: 'C' object has no attribute '__var'
>>> c._C__var
10
>>>

Т.е. при определении класса С ко всем поля, которые начинаются на “__” , дописывается имя класса.

Замечание: имена не должны заканчиваться на “__”.

Множественное наследование

Это ситуация, когда класс наследует свойства нескольких других классов.

class A:
    var = 1

class B:
    def fun(self):
        return 100500

class C(A,B):
    Field = None
Вывод:
>>> b = B()
>>> b.fun()
100500
>>> dir(b)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'fun']
>>> c = C()
>>> dir(c)
['Field', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'fun', 'var']
>>>

Класс С является подклассом A и B. Плохая ситуация возникает, когда у родителей есть одинаковые поля(еще хуже – одинаковые методы). Идея, в которой выживает то поле, которое вызывается последним, не работает в замкнутом наследовании. Тогда используем метод обхода графа. Зададим объект, у которого будут методы get, set, del.

class Desc:
    var = 0
    def __set__(self,obj,val):
        print("Set {} of {}".format(val,obj))
        self.var = val

    def __get__(self,obj,cls):
        print("Get from {} (class {})".format(obj,cls))

    def __delete__(self,obj):
        print("Del from",obj)

Объект типа дескриптор должен быть полем нашего класса.

class Desc:
    var = 0
    def __set__(self,obj,val):
        print("Set {} of {}".format(val,obj))
        self.var = val

    def __get__(self,obj,cls):
        print("Get from {} (class {})".format(obj,cls))

    def __delete__(self,obj):
        print("Del from",obj)

class C:
    fld = Desc()

    def __init__(self,name):
        self.name = name

    def __str__(self):
        return self.name

Проверяем

>>> c = C("BEBE")
>>> print(c)
BEBE
>>> dir(c)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'fld', 'name']
>>> c.__dict__
{'name': 'BEBE'}
>>> c.var = 100500
>>> c = C("BEBE")
>>> c.fld = 100500
Set 100500 of BEBE
>>>

В этот момент возникает альтернативный способ работы с дескрипторами. Доступ к соответствующим полям, которые мы определяем как дескриптор, обеспечивается с помощью методов get, set и del.

Бонусы:

  1. Присваивание и отдачу значения поля можно контролировать.
  2. Не плодятся namespace внутри объекта.

Если вставить исключение

class Desc:
    var = 0
    def __set__(self,obj,val):
        #print("Set {} of {}".format(val,obj))
        #self.var = val
        raise ValueError

    def __get__(self,obj,cls):
        print("Get from {} (class {})".format(obj,cls))
        return self.var

    def __delete__(self,obj):
        print("Del from",obj)

class C:
    fld = Desc()

    def __init__(self,name):
        self.name = name

    def __str__(self):
        return self.name

Смотрим

>>> c=C()
Traceback (most recent call last):
  File "<pyshell#0>", line 1, in <module>
    c=C()
TypeError: __init__() missing 1 required positional argument: 'name'
>>> c=C("RO")
>>> c.var
Traceback (most recent call last):
  File "<pyshell#2>", line 1, in <module>
    c.var
AttributeError: 'C' object has no attribute 'var'
>>> c.fld
Get from RO (class <class '__main__.C'>)
0
>>> c.fld=0
Traceback (most recent call last):
  File "<pyshell#4>", line 1, in <module>
    c.fld=0
  File "E:/Документы (не удалять)!!!/Alya/Рабочий стол/1.py", line 6, in __set__
    raise ValueError
ValueError
>>>

Получается объект, у которого есть fld поля.

Исключения

Исключения (exceptions) - тип данных в python. Исключения необходимы для того, чтобы сообщать программисту об ошибках, так как компилятор не способен выловить все ошибки.

Продолжение следует

LecturesCMC/PythonIntro2017/10_InheritanceDescriptors/Conspect (последним исправлял пользователь FrBrGeorge 2017-12-04 12:56:39)