Введение
В этом практическом занятии (лабораторной работе) вы узнаете о фундаментальном аспекте объектно-ориентированного программирования на Python: доступе к атрибутам. Python позволяет разработчикам настраивать, как к атрибутам классов осуществляется доступ, как они устанавливаются и управляются, с помощью специальных методов. Это предоставляет мощные возможности для контроля поведения объектов.
Кроме того, вы научитесь настраивать доступ к атрибутам в классах Python, поймете разницу между делегированием и наследованием и потренируетесь в реализации пользовательского управления атрибутами в объектах Python.
Понимание метода __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 работает в следующих шагах:
- Сначала он проверяет, находится ли имя атрибута в наборе разрешенных (
name,shares,price). - Если имя атрибута не находится в наборе разрешенных, он вызывает исключение
AttributeError. Это предотвращает присвоение нежелательных атрибутов. - Если атрибут разрешен, он использует
super().__setattr__()для фактического присвоения атрибута. Это гарантирует, что для разрешенных атрибутов происходит обычный процесс присвоения атрибутов.
Этот метод более гибок, чем использование __slots__, которое мы видели в предыдущих примерах. В то время как __slots__ может оптимизировать использование памяти и ограничить атрибуты, у него есть ограничения при работе с наследованием и он может конфликтовать с другими функциями Python. Наш подход с использованием __setattr__ дает нам аналогичный контроль над присвоением атрибутов без некоторых из этих ограничений.
Создание объектов только для чтения с использованием прокси
В этом шаге мы рассмотрим прокси - классы, очень полезный паттерн в Python. Прокси - классы позволяют взять существующий объект и изменить его поведение без изменения его исходного кода. Это похоже на то, что вы оборачиваете объект специальной оболочкой, чтобы добавить новые функции или ограничения.
Что такое прокси?
Прокси - это объект, который находится между вами и другим объектом. Он имеет ту же набор функций и свойств, что и исходный объект, но может выполнять дополнительные действия. Например, он может контролировать, кто может получить доступ к объекту, вести запись действий (логирование) или добавлять другие полезные функции.
Давайте создадим прокси только для чтения. Такой прокси не позволит вам изменять атрибуты объекта.
Шаг 1: Создание класса прокси только для чтения
Сначала нам нужно создать файл Python, в котором будет определен наш прокси только для чтения.
- Перейдите в директорию
/home/labex/project. - Создайте новый файл с именем
readonly_proxy.pyв этой директории. - Откройте файл
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: Создание тестового файла
Теперь мы создадим тестовый файл, чтобы посмотреть, как работает наш прокси только для чтения.
- Создайте новый файл с именем
test_readonly.pyв той же директории/home/labex/project. - Добавьте следующий код в файл
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: Запуск тестового скрипта
После создания класса прокси и тестового файла нам нужно запустить тестовый скрипт, чтобы увидеть результаты.
- Откройте терминал и перейдите в директорию
/home/labex/projectс помощью следующей команды:
cd /home/labex/project
- Запустите тестовый скрипт с помощью следующей команды:
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 использует два специальных метода, чтобы обеспечить функциональность только для чтения:
__getattr__(self, name): Этот метод вызывается, когда Python не может найти атрибут обычным способом. В нашем классеReadonlyProxyмы используем функциюgetattr(), чтобы перенаправить запрос на доступ к атрибуту к обернутому объекту. Таким образом, когда вы пытаетесь получить доступ к атрибуту прокси, он на самом деле получит атрибут из обернутого объекта.__setattr__(self, name, value): Этот метод вызывается, когда вы пытаетесь присвоить значение атрибуту. В нашей реализации мы вызываем исключениеAttributeError, чтобы предотвратить любые изменения атрибутов прокси.В методе
__init__мы напрямую изменяемself.__dict__, чтобы сохранить обернутый объект. Это важно, потому что если бы мы использовали обычный способ присвоения объекта, он бы вызвал метод__setattr__, который бы вызвал ошибку.
Этот паттерн прокси позволяет нам добавить слой только для чтения вокруг любого существующего объекта без изменения его исходного класса. Прокси - объект ведет себя так же, как и обернутый объект, но не позволит вам внести какие - либо изменения.
Делегирование как альтернатива наследованию
В объектно-ориентированном программировании повторное использование и расширение кода - обычная задача. Существует два основных способа достичь этого: наследование и делегирование.
Наследование - это механизм, при котором подкласс наследует методы и атрибуты от родительского класса. Подкласс может переопределить некоторые из этих унаследованных методов, чтобы предоставить свою собственную реализацию.
Делегирование, с другой стороны, предполагает, что объект содержит другой объект и перенаправляет определенные вызовы методов ему.
В этом шаге мы рассмотрим делегирование как альтернативу наследованию. Мы реализуем класс, который делегирует часть своего поведения другому объекту.
Создание примера делегирования
Сначала нам нужно создать базовый класс, с которым будет взаимодействовать наш делегирующий класс.
- Создайте новый файл с именем
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"
Далее мы создадим делегирующий класс.
- Создайте новый файл с именем
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.
Теперь давайте создадим тестовый файл, чтобы проверить нашу реализацию.
- Создайте тестовый файл с именем
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}")
Наконец, мы запустим тестовый скрипт.
- Запустите тестовый скрипт, используя следующие команды в терминале:
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'
Делегирование против наследования
Теперь давайте сравним делегирование с традиционным наследованием.
- Создайте файл с именем
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 родительского класса.
Далее мы создадим тестовый файл для примера наследования.
- Создайте тестовый файл с именем
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}")
Наконец, мы запустим тест наследования.
- Запустите тест наследования, используя следующие команды в терминале:
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'
Основные различия и соображения
Давайте рассмотрим сходства и различия между делегированием и наследованием.
Переопределение методов: И делегирование, и наследование позволяют переопределять методы, но синтаксис различается.
- В делегировании вы определяете собственный метод и решаете, вызывать ли метод обернутого объекта.
- В наследовании вы определяете собственный метод и используете
super(), чтобы вызвать метод родительского класса.
Доступ к методам:
- В делегировании неопределенные методы перенаправляются через метод
__getattr__. - В наследовании неопределенные методы наследуются автоматически.
- В делегировании неопределенные методы перенаправляются через метод
Типовые отношения:
- При делегировании
isinstance(delegating_spam, Spam)возвращаетFalse, потому что объектDelegatingSpamне является экземпляром классаSpam. - При наследовании
isinstance(inheriting_spam, Spam)возвращаетTrue, потому что классInheritingSpamнаследует от классаSpam.
- При делегировании
Ограничения: Делегирование через
__getattr__не работает с специальными методами, такими как__getitem__,__len__и т.д. Эти методы должны быть явно определены в делегирующем классе.
Делегирование особенно полезно в следующих ситуациях:
- Вы хотите настроить поведение объекта без влияния на его иерархию.
- Вы хотите объединить поведения из нескольких объектов, которые не имеют общего родителя.
- Вам нужна больше гибкости, чем предоставляет наследование.
Наследование обычно предпочтительнее, когда:
- Отношение "является - подтипом" ясно (например, автомобиль является транспортным средством).
- Вам нужно сохранить совместимость типов в вашем коде.
- Необходимо наследовать специальные методы.
Резюме
В этом практическом занятии вы узнали о мощных механизмах Python для настройки доступа к атрибутам и поведения объектов. Вы изучили, как использовать __setattr__ для контроля над тем, какие атрибуты можно устанавливать для объекта, обеспечивая контролируемый доступ к свойствам объекта. Кроме того, вы реализовали прокси только для чтения, чтобы обернуть существующие объекты, предотвращая их изменение, но сохраняя их функциональность.
Вы также рассмотрели разницу между делегированием и наследованием для повторного использования и настройки кода. Используя __getattr__, вы научились перенаправлять вызовы методов к обернутому объекту. Эти техники предоставляют гибкие способы контроля поведения объектов, выходящие за рамки стандартного наследования, и полезны для создания контролируемых интерфейсов, реализации ограничений доступа, добавления кросс - функциональных поведений и комбинирования поведения из нескольких источников. Понимание этих паттернов поможет вам писать более поддерживаемый и гибкий код на Python.