Шаг 144.
Python: сборник рецептов.
Классы и объекты. Делегирование доступа к атрибуту

    На этом шаге мы рассмотрим реализацию этой задачи.

Задача

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

Решение

    Если не усложнять, делегирование - это паттерн программирования, который подразумевает передачу ответственности за реализацию конкретной операции другому объекту. В простейшей форме это часто выглядит как-то так:

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
>>> 

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




Предыдущий шаг Содержание Следующий шаг