Kodomo

Пользователь

Объектная модель питона: наследование; 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. Заполните пропущенные места:
    •    1   >>> @once
         2   ... def hello(who):
         3   ...     print "Hello, %s!" % who
         4   >>> hello("cruel world")
         5 
         6   >>> hello("wonderful world")