На этом шаге мы рассмотрим реализацию этой задачи.
Вы хотите, чтобы экземпляр делегировал содержащемуся внутри экземпляру доступ к атрибуту - возможно, в качестве альтернативы наследованию (или чтобы реализовать прокси).
Если не усложнять, делегирование - это паттерн программирования, который подразумевает передачу ответственности за реализацию конкретной операции другому объекту. В простейшей форме это часто выглядит как-то так:
class A: def spam(self, x): pass def foo(self): pass class B: def __init__(self): self._a = A() def spam(self, x): # Делегирование внутреннему экземпляру self._a return self._a.spam(x) def foo(self): # Делегирование внутреннему экземпляру self._a return self._a.foo() def bar(self): pass
Если нужно делегировать только пару методов, написать код типа вышеприведенного будет несложно. Однако если нужно делегировать много методов, существует альтернативный подход с определением метода __getattr__():
class A: def spam(self, x): pass def foo(self): pass class B: def __init__(self): self._a = A() def bar(self): pass # Показывает все методы, определенные на классе A def __getattr__(self, name): return getattr(self._a, name)
Метод __getattr__() - это "общая ловушка" для обращений к атрибутам. Это метод, который вызывается, когда программа пытается обратиться к несуществующему атрибуту. В приведенном выше коде она перехватит операции доступа к неопределенным методам в классе B и просто делегирует их A. Например:
b = B() b.bar() # Вызывает B.bar() (существует на B) b.spam(42) # Вызывает B.__getattr__('spam') и делегирует A.spam
Еще один пример делегирования - это реализация прокси. Например:
# Класс-прокси, который оборачивается вокруг другого объекта, # но показывает его публичные атрибуты >>> class Proxy: def __init__(self, obj): self._obj = obj # Делегирует поиск атрибутов внутреннему obj def __getattr__(self, name): print('getattr:', name) return getattr(self._obj, name) # Делегирует присвоение атрибутов def __setattr__(self, name, value): if name.startswith('_'): super().__setattr__(name, value) else: print('setattr:', name, value) setattr(self._obj, name, value) # Делегирует удаление атрибутов def __delattr__(self, name): if name.startswith('_'): super().__delattr__(name) else: print('delattr:', name) delattr(self._obj, name) >>>
Чтобы использовать этот прокси-класс, просто оберните им другой экземпляр. Например:
>>> class Spam: def __init__(self, x): self.x = x def bar(self, y): print('Spam.bar:', self.x, y) >>> # Создаем экземпляр >>> s = Spam(2) >>> # Создаем прокси вокруг него >>> p = Proxy(s) >>> # Доступ через прокси >>> p.x getattr: x 2 >>> p.bar(3) getattr: bar Spam.bar: 2 3 >>> p.x = 37 setattr: x 37 >>>
Путем кастомизации реализации методов доступа к атрибутам вы можете настроить поведение прокси (заставить его логировать доступ, разрешить доступ
только на чтение и т. д.).
Делегирование иногда используется в качестве альтернативы наследованию. Например, вместо написания такого кода:
class A: def spam(self, x): print('A.spam', x) def foo(self): print('A.foo') class B(A): def spam(self, x): print('B.spam') super().spam(x) def bar(self): print('B.bar')
class A: def spam(self, x): print('A.spam', x) def foo(self): print('A.foo') class B: def __init__(self): self._a = A() def spam(self, x): print('B.spam', x) self._a.spam(x) def bar(self): print('B.bar') def __getattr__(self, name): return getattr(self._a, name)
Подобное использование делегирования часто полезно в ситуациях, когда прямое наследование может не иметь смысла или когда вы хотите лучше контролировать отношения между объектами (например, показывать наружу некоторые методы, реализовывать интерфейсы и т. п.).
При использовании делегирования для реализации прокси нужно держать в голове несколько важных деталей. Во-первых, метод __getattr__() - это на самом деле "запасной" метод, который вызывается, только если атрибут не найден. Поэтому когда запрашивается доступ к атрибутам самого экземпляра прокси (например, атрибут _obj), этот метод вызываться не будет. Во-вторых, методы __setattr__() и __getattr__() требуют добавления дополнительной логики, чтобы отделить атрибуты самого экземпляра прокси от атрибутов внутреннего объекта _obj. Общепринятое условие состоит в том, что прокси делегируются только атрибутам, которые не начинаются с нижнего подчеркивания (то есть прокси показывают только "публичные" атрибуты содержащегося внутри объекта).
Важно подчеркнуть, что метод __getattr__() обычно не применяется к специальным методам, которые начинаются и заканчиваются двойным нижним подчеркиванием. Например, рассмотрите такой класс:
>>> class ListLike: def __init__(self): self._items = [] def __getattr__(self, name): return getattr(self._items, name) >>>
Если вы попытаетесь создать объект ListLike, то обнаружите, что он поддерживает обычные методы списков, такие как append() и insert(). Однако он не поддерживает операторы типа len(), поиска элемента и т. д. Например:
>>> a = ListLike() >>> a.append(2) >>> a.insert(0, 1) >>> a.sort() >>> len(a) Traceback (most recent call last): File "<pyshell#14>", line 1, in <module> len(a) TypeError: object of type 'ListLike' has no len() >>> a[0] Traceback (most recent call last): File "<pyshell#15>", line 1, in <module> a[0] TypeError: 'ListLike' object is not subscriptable >>>
Чтобы реализовать поддержку различных операторов, вы должны вручную делегировать специальные ассоциированные методы. Например:
class ListLike: def __init__(self): self._items = [] def __getattr__(self, name): return getattr(self._items, name) # Добавляет специальные методы для поддержки некоторых списковых операций def __len__(self): return len(self._items) def __getitem__(self, index): return self._items[index] def __setitem__(self, index, value): self._items[index] = value def __delitem__(self, index): del self._items[index]
>>> a = ListLike() >>> a.append(2) >>> a.insert(0, 1) >>> a.sort() >>> len(a) 2 >>> list(a) [1, 2] >>> a[0] 1 >>>
На следующем шаге мы рассмотрим определение более одного конструктора в классе.