Наследование и дескрипторы
Идея наследования состоит в том, что бы использовать описание предыдущего объекта для написания нового.
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.
Бонусы:
- Присваивание и отдачу значения поля можно контролировать.
- Не плодятся 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. Исключения необходимы для того, чтобы сообщать программисту об ошибках, так как компилятор не способен выловить все ошибки.
Продолжение следует