Настройка доступа к атрибутам

PythonPythonBeginner
Практиковаться сейчас

This tutorial is from open-source community. Access the source code

💡 Этот учебник переведен с английского с помощью ИИ. Чтобы просмотреть оригинал, вы можете перейти на английский оригинал

Введение

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

Кроме того, вы научитесь настраивать доступ к атрибутам в классах Python, поймете разницу между делегированием и наследованием и потренируетесь в реализации пользовательского управления атрибутами в объектах Python.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL python(("Python")) -.-> python/ObjectOrientedProgrammingGroup(["Object-Oriented Programming"]) python/ObjectOrientedProgrammingGroup -.-> python/classes_objects("Classes and Objects") python/ObjectOrientedProgrammingGroup -.-> python/inheritance("Inheritance") python/ObjectOrientedProgrammingGroup -.-> python/encapsulation("Encapsulation") subgraph Lab Skills python/classes_objects -.-> lab-132502{{"Настройка доступа к атрибутам"}} python/inheritance -.-> lab-132502{{"Настройка доступа к атрибутам"}} python/encapsulation -.-> lab-132502{{"Настройка доступа к атрибутам"}} end

Понимание метода __setattr__ для управления атрибутами

В Python есть специальные методы, которые позволяют настраивать, как к атрибутам объекта осуществляется доступ и как они изменяются. Одним из таких важных методов является __setattr__(). Этот метод вызывается каждый раз, когда вы пытаетесь присвоить значение атрибуту объекта. Он позволяет вам тонко управлять процессом присвоения атрибутов.

Что такое __setattr__?

Метод __setattr__(self, name, value) действует как перехватчик для всех операций присвоения атрибутов. Когда вы пишете простой оператор присваивания, например obj.attr = value, Python не просто напрямую присваивает значение. Вместо этого он внутренне вызывает obj.__setattr__("attr", value). Эта механика позволяет вам решить, что должно происходить во время присвоения атрибута.

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

Шаг 1: Создание нового файла

Сначала откройте новый файл в WebIDE. Вы можете сделать это, кликнув на меню "File" и выбрав "New File". Назовите этот файл restricted_stock.py и сохраните его в директории /home/labex/project. В этом файле будет определен класс, в котором мы будем использовать __setattr__ для управления присвоением атрибутов.

Шаг 2: Добавление кода в restricted_stock.py

Добавьте следующий код в файл restricted_stock.py. Этот код определяет класс RestrictedStock.

class RestrictedStock:
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

    def __setattr__(self, name, value):
        ## Only allow specific attributes
        if name not in {'name', 'shares', 'price'}:
            raise AttributeError(f'Cannot set attribute {name}')

        ## If attribute is allowed, set it using the parent method
        super().__setattr__(name, value)

В методе __init__ мы инициализируем объект атрибутами name, shares и price. Метод __setattr__ проверяет, находится ли имя атрибута, которое мы пытаемся присвоить, в наборе разрешенных атрибутов (name, shares, price). Если оно не находится, он вызывает исключение AttributeError. Если атрибут разрешен, он использует метод __setattr__ родительского класса для фактического присвоения атрибута.

Шаг 3: Создание тестового файла

Создайте новый файл с именем test_restricted.py и добавьте в него следующий код. Этот код будет тестировать функциональность класса RestrictedStock.

from restricted_stock import RestrictedStock

## Create a new stock
stock = RestrictedStock('GOOG', 100, 490.1)

## Test accessing existing attributes
print(f"Name: {stock.name}")
print(f"Shares: {stock.shares}")
print(f"Price: {stock.price}")

## Test modifying an existing attribute
print("\nChanging shares to 75...")
stock.shares = 75
print(f"New shares value: {stock.shares}")

## Test setting an invalid attribute
try:
    print("\nTrying to set an invalid attribute 'share'...")
    stock.share = 50
except AttributeError as e:
    print(f"Error: {e}")

В этом коде мы сначала импортируем класс RestrictedStock. Затем создаем экземпляр этого класса. Мы тестируем доступ к существующим атрибутам, изменение существующего атрибута и, наконец, пытаемся установить недопустимый атрибут, чтобы проверить, работает ли метод __setattr__ как ожидается.

Шаг 4: Запуск тестового файла

Откройте терминал в WebIDE и выполните следующие команды для запуска файла test_restricted.py:

cd /home/labex/project
python3 test_restricted.py

После выполнения этих команд вы должны увидеть вывод, похожий на следующий:

Name: GOOG
Shares: 100
Price: 490.1

Changing shares to 75...
New shares value: 75

Trying to set an invalid attribute 'share'...
Error: Cannot set attribute share

Как это работает

Метод __setattr__ в нашем классе RestrictedStock работает в следующих шагах:

  1. Сначала он проверяет, находится ли имя атрибута в наборе разрешенных (name, shares, price).
  2. Если имя атрибута не находится в наборе разрешенных, он вызывает исключение AttributeError. Это предотвращает присвоение нежелательных атрибутов.
  3. Если атрибут разрешен, он использует super().__setattr__() для фактического присвоения атрибута. Это гарантирует, что для разрешенных атрибутов происходит обычный процесс присвоения атрибутов.

Этот метод более гибок, чем использование __slots__, которое мы видели в предыдущих примерах. В то время как __slots__ может оптимизировать использование памяти и ограничить атрибуты, у него есть ограничения при работе с наследованием и он может конфликтовать с другими функциями Python. Наш подход с использованием __setattr__ дает нам аналогичный контроль над присвоением атрибутов без некоторых из этих ограничений.

Создание объектов только для чтения с использованием прокси

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

Что такое прокси?

Прокси - это объект, который находится между вами и другим объектом. Он имеет ту же набор функций и свойств, что и исходный объект, но может выполнять дополнительные действия. Например, он может контролировать, кто может получить доступ к объекту, вести запись действий (логирование) или добавлять другие полезные функции.

Давайте создадим прокси только для чтения. Такой прокси не позволит вам изменять атрибуты объекта.

Шаг 1: Создание класса прокси только для чтения

Сначала нам нужно создать файл Python, в котором будет определен наш прокси только для чтения.

  1. Перейдите в директорию /home/labex/project.
  2. Создайте новый файл с именем readonly_proxy.py в этой директории.
  3. Откройте файл readonly_proxy.py и добавьте следующий код:
class ReadonlyProxy:
    def __init__(self, obj):
        ## Store the wrapped object directly in __dict__ to avoid triggering __setattr__
        self.__dict__['_obj'] = obj

    def __getattr__(self, name):
        ## Forward attribute access to the wrapped object
        return getattr(self._obj, name)

    def __setattr__(self, name, value):
        ## Block all attribute assignments
        raise AttributeError("Cannot modify a read-only object")

В этом коде определен класс ReadonlyProxy. Метод __init__ сохраняет объект, который мы хотим обернуть. Мы используем self.__dict__ для прямого сохранения объекта, чтобы избежать вызова метода __setattr__. Метод __getattr__ вызывается, когда мы пытаемся получить доступ к атрибуту прокси. Он просто перенаправляет запрос к обернутому объекту. Метод __setattr__ вызывается, когда мы пытаемся изменить атрибут. Он вызывает ошибку, чтобы предотвратить любые изменения.

Шаг 2: Создание тестового файла

Теперь мы создадим тестовый файл, чтобы посмотреть, как работает наш прокси только для чтения.

  1. Создайте новый файл с именем test_readonly.py в той же директории /home/labex/project.
  2. Добавьте следующий код в файл test_readonly.py:
from stock import Stock
from readonly_proxy import ReadonlyProxy

## Create a normal Stock object
stock = Stock('AAPL', 100, 150.75)
print("Original stock object:")
print(f"Name: {stock.name}")
print(f"Shares: {stock.shares}")
print(f"Price: {stock.price}")
print(f"Cost: {stock.cost}")

## Modify the original stock object
stock.shares = 200
print(f"\nAfter modification - Shares: {stock.shares}")
print(f"After modification - Cost: {stock.cost}")

## Create a read-only proxy around the stock
readonly_stock = ReadonlyProxy(stock)
print("\nRead-only proxy object:")
print(f"Name: {readonly_stock.name}")
print(f"Shares: {readonly_stock.shares}")
print(f"Price: {readonly_stock.price}")
print(f"Cost: {readonly_stock.cost}")

## Try to modify the read-only proxy
try:
    print("\nAttempting to modify the read-only proxy...")
    readonly_stock.shares = 300
except AttributeError as e:
    print(f"Error: {e}")

## Show that the original object is unchanged
print(f"\nOriginal stock shares are still: {stock.shares}")

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

Шаг 3: Запуск тестового скрипта

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

  1. Откройте терминал и перейдите в директорию /home/labex/project с помощью следующей команды:
cd /home/labex/project
  1. Запустите тестовый скрипт с помощью следующей команды:
python3 test_readonly.py

Вы должны увидеть вывод, похожий на следующий:

Original stock object:
Name: AAPL
Shares: 100
Price: 150.75
Cost: 15075.0

After modification - Shares: 200
After modification - Cost: 30150.0

Read-only proxy object:
Name: AAPL
Shares: 200
Price: 150.75
Cost: 30150.0

Attempting to modify the read-only proxy...
Error: Cannot modify a read-only object

Original stock shares are still: 200

Как работает прокси

Класс ReadonlyProxy использует два специальных метода, чтобы обеспечить функциональность только для чтения:

  1. __getattr__(self, name): Этот метод вызывается, когда Python не может найти атрибут обычным способом. В нашем классе ReadonlyProxy мы используем функцию getattr(), чтобы перенаправить запрос на доступ к атрибуту к обернутому объекту. Таким образом, когда вы пытаетесь получить доступ к атрибуту прокси, он на самом деле получит атрибут из обернутого объекта.

  2. __setattr__(self, name, value): Этот метод вызывается, когда вы пытаетесь присвоить значение атрибуту. В нашей реализации мы вызываем исключение AttributeError, чтобы предотвратить любые изменения атрибутов прокси.

  3. В методе __init__ мы напрямую изменяем self.__dict__, чтобы сохранить обернутый объект. Это важно, потому что если бы мы использовали обычный способ присвоения объекта, он бы вызвал метод __setattr__, который бы вызвал ошибку.

Этот паттерн прокси позволяет нам добавить слой только для чтения вокруг любого существующего объекта без изменения его исходного класса. Прокси - объект ведет себя так же, как и обернутый объект, но не позволит вам внести какие - либо изменения.

Делегирование как альтернатива наследованию

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

Наследование - это механизм, при котором подкласс наследует методы и атрибуты от родительского класса. Подкласс может переопределить некоторые из этих унаследованных методов, чтобы предоставить свою собственную реализацию.

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

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

Создание примера делегирования

Сначала нам нужно создать базовый класс, с которым будет взаимодействовать наш делегирующий класс.

  1. Создайте новый файл с именем base_class.py в директории /home/labex/project. В этом файле будет определен класс с именем Spam с тремя методами: method_a, method_b и method_c. Каждый метод выводит сообщение и возвращает результат. Вот код, который нужно поместить в base_class.py:
class Spam:
    def method_a(self):
        print('Spam.method_a executed')
        return "Result from Spam.method_a"

    def method_b(self):
        print('Spam.method_b executed')
        return "Result from Spam.method_b"

    def method_c(self):
        print('Spam.method_c executed')
        return "Result from Spam.method_c"

Далее мы создадим делегирующий класс.

  1. Создайте новый файл с именем delegator.py. В этом файле мы определим класс с именем DelegatingSpam, который делегирует часть своего поведения экземпляру класса Spam.
from base_class import Spam

class DelegatingSpam:
    def __init__(self):
        ## Create an instance of Spam that we'll delegate to
        self._spam = Spam()

    def method_a(self):
        ## Override method_a but also call the original
        print('DelegatingSpam.method_a executed')
        result = self._spam.method_a()
        return f"Modified {result}"

    def method_c(self):
        ## Completely override method_c
        print('DelegatingSpam.method_c executed')
        return "Result from DelegatingSpam.method_c"

    def __getattr__(self, name):
        ## For any other attribute/method, delegate to self._spam
        print(f"Delegating {name} to the wrapped Spam object")
        return getattr(self._spam, name)

В методе __init__ мы создаем экземпляр класса Spam. Метод method_a переопределяет исходный метод, но также вызывает метод method_a класса Spam. Метод method_c полностью переопределяет исходный метод. Метод __getattr__ - это специальный метод в Python, который вызывается, когда обращаются к атрибуту или методу, который не существует в классе DelegatingSpam. Затем он делегирует вызов экземпляру Spam.

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

  1. Создайте тестовый файл с именем test_delegation.py. В этом файле будет создан экземпляр класса DelegatingSpam и будут вызваны его методы.
from delegator import DelegatingSpam

## Create a delegating object
spam = DelegatingSpam()

print("Calling method_a (overridden with delegation):")
result_a = spam.method_a()
print(f"Result: {result_a}\n")

print("Calling method_b (not defined in DelegatingSpam, delegated):")
result_b = spam.method_b()
print(f"Result: {result_b}\n")

print("Calling method_c (completely overridden):")
result_c = spam.method_c()
print(f"Result: {result_c}\n")

## Try accessing a non-existent method
try:
    print("Calling non-existent method_d:")
    spam.method_d()
except AttributeError as e:
    print(f"Error: {e}")

Наконец, мы запустим тестовый скрипт.

  1. Запустите тестовый скрипт, используя следующие команды в терминале:
cd /home/labex/project
python3 test_delegation.py

Вы должны увидеть вывод, похожий на следующий:

Calling method_a (overridden with delegation):
DelegatingSpam.method_a executed
Spam.method_a executed
Result: Modified Result from Spam.method_a

Calling method_b (not defined in DelegatingSpam, delegated):
Delegating method_b to the wrapped Spam object
Spam.method_b executed
Result: Result from Spam.method_b

Calling method_c (completely overridden):
DelegatingSpam.method_c executed
Result: Result from DelegatingSpam.method_c

Calling non-existent method_d:
Delegating method_d to the wrapped Spam object
Error: 'Spam' object has no attribute 'method_d'

Делегирование против наследования

Теперь давайте сравним делегирование с традиционным наследованием.

  1. Создайте файл с именем inheritance_example.py. В этом файле мы определим класс с именем InheritingSpam, который наследует от класса Spam.
from base_class import Spam

class InheritingSpam(Spam):
    def method_a(self):
        ## Override method_a but also call the parent method
        print('InheritingSpam.method_a executed')
        result = super().method_a()
        return f"Modified {result}"

    def method_c(self):
        ## Completely override method_c
        print('InheritingSpam.method_c executed')
        return "Result from InheritingSpam.method_c"

Класс InheritingSpam переопределяет методы method_a и method_c. В методе method_a мы используем super(), чтобы вызвать метод method_a родительского класса.

Далее мы создадим тестовый файл для примера наследования.

  1. Создайте тестовый файл с именем test_inheritance.py. В этом файле будет создан экземпляр класса InheritingSpam и будут вызваны его методы.
from inheritance_example import InheritingSpam

## Create an inheriting object
spam = InheritingSpam()

print("Calling method_a (overridden with super call):")
result_a = spam.method_a()
print(f"Result: {result_a}\n")

print("Calling method_b (inherited from parent):")
result_b = spam.method_b()
print(f"Result: {result_b}\n")

print("Calling method_c (completely overridden):")
result_c = spam.method_c()
print(f"Result: {result_c}\n")

## Try accessing a non-existent method
try:
    print("Calling non-existent method_d:")
    spam.method_d()
except AttributeError as e:
    print(f"Error: {e}")

Наконец, мы запустим тест наследования.

  1. Запустите тест наследования, используя следующие команды в терминале:
cd /home/labex/project
python3 test_inheritance.py

Вы должны увидеть вывод, похожий на следующий:

Calling method_a (overridden with super call):
InheritingSpam.method_a executed
Spam.method_a executed
Result: Modified Result from Spam.method_a

Calling method_b (inherited from parent):
Spam.method_b executed
Result: Result from Spam.method_b

Calling method_c (completely overridden):
InheritingSpam.method_c executed
Result: Result from InheritingSpam.method_c

Calling non-existent method_d:
Error: 'InheritingSpam' object has no attribute 'method_d'

Основные различия и соображения

Давайте рассмотрим сходства и различия между делегированием и наследованием.

  1. Переопределение методов: И делегирование, и наследование позволяют переопределять методы, но синтаксис различается.

    • В делегировании вы определяете собственный метод и решаете, вызывать ли метод обернутого объекта.
    • В наследовании вы определяете собственный метод и используете super(), чтобы вызвать метод родительского класса.
  2. Доступ к методам:

    • В делегировании неопределенные методы перенаправляются через метод __getattr__.
    • В наследовании неопределенные методы наследуются автоматически.
  3. Типовые отношения:

    • При делегировании isinstance(delegating_spam, Spam) возвращает False, потому что объект DelegatingSpam не является экземпляром класса Spam.
    • При наследовании isinstance(inheriting_spam, Spam) возвращает True, потому что класс InheritingSpam наследует от класса Spam.
  4. Ограничения: Делегирование через __getattr__ не работает с специальными методами, такими как __getitem__, __len__ и т.д. Эти методы должны быть явно определены в делегирующем классе.

Делегирование особенно полезно в следующих ситуациях:

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

Наследование обычно предпочтительнее, когда:

  • Отношение "является - подтипом" ясно (например, автомобиль является транспортным средством).
  • Вам нужно сохранить совместимость типов в вашем коде.
  • Необходимо наследовать специальные методы.

Резюме

В этом практическом занятии вы узнали о мощных механизмах Python для настройки доступа к атрибутам и поведения объектов. Вы изучили, как использовать __setattr__ для контроля над тем, какие атрибуты можно устанавливать для объекта, обеспечивая контролируемый доступ к свойствам объекта. Кроме того, вы реализовали прокси только для чтения, чтобы обернуть существующие объекты, предотвращая их изменение, но сохраняя их функциональность.

Вы также рассмотрели разницу между делегированием и наследованием для повторного использования и настройки кода. Используя __getattr__, вы научились перенаправлять вызовы методов к обернутому объекту. Эти техники предоставляют гибкие способы контроля поведения объектов, выходящие за рамки стандартного наследования, и полезны для создания контролируемых интерфейсов, реализации ограничений доступа, добавления кросс - функциональных поведений и комбинирования поведения из нескольких источников. Понимание этих паттернов поможет вам писать более поддерживаемый и гибкий код на Python.