Объектная модель питона: наследование; staticmethod, classmethod; множественное наследование; super

План

Материалы

Заметки

Наследование

"классический" класс <-> new-style класс = подкласс встроенного типа. т.е. создаваемый класс всегда наследуется от какого-то встроенного класса, или существующего класса(ов).

При этом Old-style классы, это то, что почти не используется (в Python [3:].* - упразднены).

встроенный тип <-> созданный тип

Наследуя класс мы создаем новый тип. Тип класса наследованного от встроенного типа именуется 'type'. Класс и тип new-style классов - это одно и то же.

Переключить отображение номеров строк
   1 >>>class defaultdict(dict):
   2 ......
   3 >>>defaultdict
   4 <class '__main__.defaultdict'>
   5 >>>type(defaultdict)
   6 <type 'type'>
   7 >>> defaultdict.__class__ is type(defaultdict)
   8 True
   9 >>> a = defaultdict()
  10 >>> a.__class__ is type(a)
  11 True

New-style класс наследует функции встроенного типа. Но можно добавить новые. Добавляя новые функции - заменяет встроенные функции типа. Этим всем мы многократно пользовались - например, при создании конструктора класса.

Наглядный пример про работу классов и объектов

Переключить отображение номеров строк
   1 >>> def a():
   2         print 'ill kill your brain'
   3 >>> class A(object):
   4         def __init__(self):
   5                 self.__dict__['show'] = a
   6         def show(self):
   7                 print 'show is going on'
   8 >>> b = A()
   9 >>> b.show()
  10 ill kill your brain
  11 >>> del b.show
  12 >>> b.show()
  13 show is going on
  14 >>> class A(object):
  15         show = a
  16         def show(self):
  17                 print 'show is going on'
  18 >>> b = A()
  19 >>> b.show()
  20 show is going on
  21 >>> class A(object):
  22         def show(self):
  23                 print 'show is going on'
  24         show = a
  25 >>> b = A()
  26 >>> b.show()
  27 Traceback (most recent call last):
  28   File "<pyshell#151>", line 1, in <module>
  29     b.show()
  30 TypeError: a() takes no arguments (1 given)

__new__ , __init__

__new__ создает новый объект наследуя его от класса. Затем запускается __init__ - который как-то модифицирует пространство имен самого объекта.

unbound/bound method

Переключить отображение номеров строк
   1 >>> class A(object):
   2     def b(self):
   3         pass
   4     print b
   5 <function b at 0x2c4f8c0>
   6 >>> print A
   7 <class '__main__.A'>
   8 >>> print A.b
   9 <unbound method A.b>
  10 >>> a = A()
  11 >>> print a.b
  12 <bound method A.b of <__main__.A object at 0x2c51f50>>

Т.е. - есть функция <function b at 0x2c4f8c0> - которая сама по себе, и лежит где-то в своем месте в памяти; есть класс <class '__main__.A'>. A.b - представляет из себя некую обертку <unbound method A.b> - которая связана с <class '__main__.A'> и в нужный момент подсовывает нам функцию <function b at 0x2c4f8c0>. А есть объект a <__main__.A object at 0x2c51f50> - и a.b - это тоже некая обертка - которая в нужный момент нам подсовывает A.b - который в свою очередь...

__slots__

.__slots__ - хранит список возможных атрибутов класса, read-only, т.е. нельзя считерить типа object.__slots__ = [new_list], и дальше веселуха. Тогда при попытке придать объекту какой-либо атрибут, вылезет ошибка.

.__slots__ vs. .__dict__ - (если кто забыл, .__dict__ <=> vars() - словарь пространства имен объекта). Если внутри класса задается .__slots__ - то внутри него не создается атрибут .__dict__, а следовательно и внутри класса не создается этого атрибута.

__getattr__, __getattribute__, __setattr__

a.x = y <=>

Переключить отображение номеров строк
   1 if hasattr(a, "__setattr__"):
   2     a.__setattr__('x', y)
   3 else:
   4     vars(x)['x'] = y

y = a.x <=>

Переключить отображение номеров строк
   1 try:
   2     try:
   3         a.__getattribute__('x')
   4     except KeyError:
   5         for o in [self] + self.__mro__:
   6             if 'x' in o.__dict__:
   7                 y = o.__dict__('x')
   8         raise AttributeError
   9 except AttributeError:
  10     y = a.__getattr__('x')

Переключить отображение номеров строк
   1 >>> class A(object):
   2         def __init__(self):
   3                 self.x = 'x'
   4         def __getattribute__(self, name):
   5                 a = self.__dict__(name)
   6                 return a
   7 
   8 
   9 >>> a = A()
  10 >>> a.x
  11 .
  12 .
  13 .
  14   File "<pyshell#30>", line 3, in __getattribute__
  15     return self.__dict__(name)
  16 RuntimeError: maximum recursion depth exceeded while calling a Python object

staticmethod, classmethod

Переключить отображение номеров строк
   1 class C(object):
   2 
   3     def foo(x, y):
   4         print "staticmethod", x, y
   5     foo = staticmethod(foo)

<=> (вспоминая, что вообще то, как раз так выглядит декоратор, и понимая, что это он и есть)

Переключить отображение номеров строк
   1 class C(object):
   2 
   3     @staticmethod
   4     def foo(x, y):
   5         print "staticmethod", x, y

staticmethod возвращает фукцию неизмененной, т.е. не добавляет туда self в качестве аргумента.

Переключить отображение номеров строк
   1 class C(object):
   2 
   3     def foo(cls, y):
   4         print "class", cls,"return", y
   5     foo = classmethod(foo)

classmethod - возвращает в качестве порвого аргумента - класс. По традиции эта переменная именуется cls.

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

Переключить отображение номеров строк
   1 class C(object):
   2   @staticmethod
   3    def from_foo():
   4      result = C()
   5      return result

Но, что же делать - если мы захотим от нашего объекта С - 100500 раз последовательно наследоваться. Ну а создавать то новый объект мы хотим каждый раз текущего класса:

Переключить отображение номеров строк
   1 class C(object):
   2     @staticmethod
   3     def from_foo(*args):
   4         result = C()
   5         for arg in args:
   6             result.__dict__[str(arg)] = arg
   7         return result

property

Переключить отображение номеров строк
   1 class C(object):
   2 
   3     def __init__(self):
   4         self.__x = 0
   5 
   6     def getx(self):
   7         return self.__x
   8 
   9     def setx(self, x):
  10         if x < 0: x = 0
  11         self.__x = x
  12 
  13     x = property(getx, setx)

Следовательно x = property(getx, setx) - обозначает, что атрибут x - будет использовать функцию getx, когда атрибут будут пытаться "прочитать", и setx - когда пытаться присвоить.

Вообще, полный систаксис - property(fget=None, fset=None, fdel=None, doc=None).

Зачем, собственно нужно: Если изменить __setattr__- то у нас он начнет медленнее работать, притом для всех атрибудтов(а нам может надо только над одним так глумиться). А если менять __getattr__ - то придется еще и голову поломать, как добиться того, чтобы все работало.

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

Ну хорошо, хорошо. Если мы можем наследоваться от одного типа/класса, почему бы нам не наследоваться от двух...нет...трех классов?!

Переключить отображение номеров строк
   1 class A(B,C):
   2     pass

Так же, как и в случае "простого" наследования - потомок получит все функции.

Вроде как неплохо: отсюда мы получаем применение - создавая классы-блоки, потом мы можем наследовать новый класс от них всех (или только некого набора) - получая класс, обладающий неким набором свойств. Т.е. - множественное наследование - при достаточном мастерстве может стать очень удобным подходом.

Проблемы множественного наследования

Одноимённые методы родителей:

Решить, какой из методов лучше, очевидно, нельзя. Вызывается тот, кто стоит первый. т.е.:

Переключить отображение номеров строк
   1 class C(A,B):
   2     pass

и

Переключить отображение номеров строк
   1 class C(B,A):
   2     pass

могут иметь одинаковый набор имен функций, и разный набор самих функций.

Чуть более сложная система:

              class A:
                ^ ^  def save(self): ...
               /   \
              /     \
             /       \
            /         \
        class B     class C:
            ^         ^  def save(self): ...
             \       /
              \     /
               \   /
                \ /
              class D

super

super(type[, object-or-type])

что делает метод super - он направляет выполнение функции к одноименной функции предка класса объекта.

При этом он может быть использован и как просто переход к функции предка, без дополнительных усложнений.

Также в нем реализован обход по mro, что позволяет разрешать проблему diamond diagram и пр.

Альтернативы

Данный способ реализации наследования основан на том, что каждый класс будет загружаться ровно один раз:

- вот такие беды с разрешением того, чью функцию использовать

+ нет рассинхронизации классов

+ ну и меньше памяти занимает само собой

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

Крышеснос

Переключить отображение номеров строк
   1 >>> class defaultdict(dict):
   2     a = 1
   3     __slots__ = ['a', 'default']
   4     def __init__(self, default=None):
   5         dict.__init__(self)
   6         self.default = default
   7 >>> dict(vars(defaultdict))
   8 {'a': 1, '__module__': '__main__', 'default': <member 'default' of 'defaultdict' objects>, '__slots__': ['a', 'default'], '__doc__': None, '__init__': <function __init__ at 0x1fa4758>}
   9 >>> defaultdict.__dict__ is defaultdict.__dict__
  10 False

Контрольная работа

  1. Напишите декоратор once, который при первом обращении к задекорированной функции вызывает её с теми аргументами, которые ей передали, и запоминает результат. При последующих обращениях к задекорированной функции он возвращает сохранённое значение вне зависимости от аргументов.

  2. Заполните пропущенные места: