Введение
В этой лабораторной работе вы получите всестороннее представление о декораторах (decorators) в Python — мощной функции для изменения или расширения возможностей функций и методов. Мы начнем с введения в основную концепцию декораторов и рассмотрим их базовое использование на практических примерах.
Основываясь на этом фундаменте, вы узнаете, как эффективно использовать functools.wraps для сохранения важной метаинформации декорируемой функции. Затем мы углубимся в специфические декораторы, такие как property, поняв его роль в управлении доступом к атрибутам. Наконец, в лабораторной работе будут разъяснены различия между методами экземпляра (instance methods), методами класса (class methods) и статическими методами (static methods), демонстрируя, как декораторы используются в этих контекстах для управления поведением методов внутри классов.
Понимание базовых декораторов
На этом шаге мы познакомимся с концепцией декораторов и их базовым использованием. Декоратор — это функция, которая принимает другую функцию в качестве аргумента, добавляет некоторую функциональность и возвращает другую функцию, и все это без изменения исходного кода оригинальной функции.
Сначала найдите файл decorator_basics.py в проводнике файлов слева в WebIDE. Дважды щелкните по нему, чтобы открыть. Мы напишем наш первый декоратор в этом файле.
Скопируйте и вставьте следующий код в decorator_basics.py:
import datetime
def log_activity(func):
"""A simple decorator to log function calls."""
def wrapper(*args, **kwargs):
print(f"Calling function '{func.__name__}' at {datetime.datetime.now()}")
result = func(*args, **kwargs)
print(f"Function '{func.__name__}' finished.")
return result
return wrapper
@log_activity
def greet(name):
"""A simple function to greet someone."""
print(f"Hello, {name}!")
## Call the decorated function
greet("Alice")
## Let's inspect the function's metadata
print(f"\nFunction name: {greet.__name__}")
print(f"Function docstring: {greet.__doc__}")
Давайте разберем этот код:
- Мы определяем функцию-декоратор
log_activity, которая принимает функциюfuncв качестве своего аргумента. - Внутри
log_activityмы определяем вложенную функциюwrapper. Эта функция будет содержать новое поведение. Она выводит сообщение в лог, вызывает оригинальную функциюfunc, а затем выводит еще одно сообщение в лог. - Функция
log_activityвозвращает функциюwrapper. - Синтаксис
@log_activityнад функциейgreetявляется сокращением дляgreet = log_activity(greet). Он применяет наш декоратор к функцииgreet.
Теперь сохраните файл (вы можете использовать Ctrl+S или Cmd+S). Чтобы запустить скрипт, откройте интегрированный терминал в нижней части WebIDE и выполните следующую команду:
python ~/project/decorator_basics.py
Вы увидите следующий вывод. Обратите внимание, что дата и время будут отличаться.
Calling function 'greet' at 2023-10-27 10:30:00.123456
Hello, Alice!
Function 'greet' finished.
Function name: wrapper
Function docstring: None
Обратите внимание на две вещи в выводе. Во-первых, наша функция greet теперь обернута сообщениями логирования. Во-вторых, имя и строка документации (docstring) функции были заменены на соответствующие значения функции wrapper. Это может вызвать проблемы при отладке и интроспекции. На следующем шаге мы узнаем, как это исправить.
Сохранение метаданных функции с помощью functools.wraps
На предыдущем шаге мы заметили, что декорирование функции заменяет ее исходные метаданные (такие как __name__ и __doc__) метаданными оберточной функции (wrapper function). Модуль functools в Python предоставляет для этого решение: декоратор wraps.
Декоратор wraps используется внутри вашего собственного декоратора для копирования метаданных из оригинальной функции в оберточную функцию.
Давайте изменим наш код в decorator_basics.py. Откройте файл в WebIDE и обновите его, чтобы использовать functools.wraps.
import datetime
from functools import wraps
def log_activity(func):
"""A simple decorator to log function calls."""
@wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling function '{func.__name__}' at {datetime.datetime.now()}")
result = func(*args, **kwargs)
print(f"Function '{func.__name__}' finished.")
return result
return wrapper
@log_activity
def greet(name):
"""A simple function to greet someone."""
print(f"Hello, {name}!")
## Call the decorated function
greet("Alice")
## Let's inspect the function's metadata again
print(f"\nFunction name: {greet.__name__}")
print(f"Function docstring: {greet.__doc__}")
Единственные изменения заключаются в следующем:
- Мы импортировали
wrapsиз модуляfunctools. - Мы добавили
@wraps(func)непосредственно над определением нашей функцииwrapper.
Сохраните файл и снова запустите его из терминала:
python ~/project/decorator_basics.py
Теперь вывод будет другим:
Calling function 'greet' at 2023-10-27 10:35:00.543210
Hello, Alice!
Function 'greet' finished.
Function name: greet
Function docstring: A simple function to greet someone.
Как видите, имя функции корректно отображается как greet, и ее исходная строка документации сохранена. Использование functools.wraps является лучшей практикой, которая делает ваши декораторы более надежными и профессиональными.
Реализация управляемых атрибутов с помощью @property
Python предоставляет несколько встроенных декораторов. Одним из наиболее полезных является @property, который позволяет преобразовать метод класса в "управляемый атрибут" (managed attribute). Это идеально подходит для добавления логики, такой как валидация или вычисления, к доступу к атрибуту, не изменяя при этом способ взаимодействия пользователей с вашим классом.
Давайте рассмотрим это на примере создания класса Circle. Откройте файл property_decorator.py в проводнике файлов.
Скопируйте и вставьте следующий код в property_decorator.py:
import math
class Circle:
def __init__(self, radius):
## The actual value is stored in a "private" attribute
self._radius = radius
@property
def radius(self):
"""The radius property."""
print("Getting radius...")
return self._radius
@radius.setter
def radius(self, value):
"""The radius setter with validation."""
print(f"Setting radius to {value}...")
if value < 0:
raise ValueError("Radius cannot be negative")
self._radius = value
@property
def area(self):
"""A read-only computed property for the area."""
print("Calculating area...")
return math.pi * self._radius ** 2
## --- Let's test our Circle class ---
c = Circle(5)
## Access the radius like a normal attribute (triggers the getter)
print(f"Initial radius: {c.radius}\n")
## Change the radius (triggers the setter)
c.radius = 10
print(f"New radius: {c.radius}\n")
## Access the computed area property
print(f"Circle area: {c.area:.2f}\n")
## Try to set an invalid radius (triggers the setter's validation)
try:
c.radius = -2
except ValueError as e:
print(f"Error: {e}")
В этом коде:
@propertyнад методомradiusопределяет "геттер" (getter). Он вызывается при доступе кc.radius.@radius.setterопределяет "сеттер" (setter) для свойстваradius. Он вызывается при присваивании значения, например,c.radius = 10. Мы добавили сюда валидацию, чтобы предотвратить отрицательные значения.- Метод
areaтакже использует@property, но не имеет сеттера, что делает его атрибутом только для чтения (read-only). Его значение вычисляется при каждом обращении к нему.
Сохраните файл и запустите его из терминала:
python ~/project/property_decorator.py
Вы должны увидеть следующий вывод, демонстрирующий, как автоматически вызываются геттер, сеттер и логика валидации:
Getting radius...
Initial radius: 5
Setting radius to 10...
Getting radius...
New radius: 10
Calculating area...
Circle area: 314.16
Setting radius to -2...
Error: Radius cannot be negative
Различие между методами экземпляра, класса и статическими методами
В классах Python методы могут быть привязаны к экземпляру, к классу или не привязаны вовсе. Для определения этих различных типов методов используются декораторы.
- Методы экземпляра (Instance Methods): Тип по умолчанию. Они получают экземпляр в качестве первого аргумента, условно называемого
self. Они оперируют данными, специфичными для экземпляра. - Методы класса (Class Methods): Помечаются с помощью
@classmethod. Они получают класс в качестве первого аргумента, условно называемогоcls. Они оперируют данными на уровне класса и часто используются как альтернативные конструкторы. - Статические методы (Static Methods): Помечаются с помощью
@staticmethod. Они не получают никакого специального первого аргумента. По сути, это обычные функции, пространственно ограниченные классом, и они не могут получить доступ к состоянию экземпляра или класса.
Давайте посмотрим на все три типа в действии. Откройте файл class_methods.py в проводнике файлов.
Скопируйте и вставьте следующий код в class_methods.py:
class MyClass:
class_variable = "I am a class variable"
def __init__(self, instance_variable):
self.instance_variable = instance_variable
## 1. Instance Method
def instance_method(self):
print("\n--- Calling Instance Method ---")
print(f"Can access instance data: self.instance_variable = '{self.instance_variable}'")
print(f"Can access class data: self.class_variable = '{self.class_variable}'")
## 2. Class Method
@classmethod
def class_method(cls):
print("\n--- Calling Class Method ---")
print(f"Can access class data: cls.class_variable = '{cls.class_variable}'")
## Note: Cannot access instance_variable without an instance
print("Cannot access instance data directly.")
## 3. Static Method
@staticmethod
def static_method(a, b):
print("\n--- Calling Static Method ---")
print("Cannot access instance or class data directly.")
print(f"Just a utility function: {a} + {b} = {a + b}")
## --- Let's test the methods ---
## Create an instance of the class
my_instance = MyClass("I am an instance variable")
## Call the instance method (requires an instance)
my_instance.instance_method()
## Call the class method (can be called on the class or an instance)
MyClass.class_method()
my_instance.class_method() ## Also works
## Call the static method (can be called on the class or an instance)
MyClass.static_method(10, 5)
my_instance.static_method(20, 8) ## Also works
Сохраните файл и запустите его из терминала:
python ~/project/class_methods.py
Внимательно изучите вывод. Он наглядно демонстрирует возможности и ограничения каждого типа методов.
--- Calling Instance Method ---
Can access instance data: self.instance_variable = 'I am an instance variable'
Can access class data: self.class_variable = 'I am a class variable'
--- Calling Class Method ---
Can access class data: cls.class_variable = 'I am a class variable'
Cannot access instance data directly.
--- Calling Class Method ---
Can access class data: cls.class_variable = 'I am a class variable'
Cannot access instance data directly.
--- Calling Static Method ---
Cannot access instance or class data directly.
Just a utility function: 10 + 5 = 15
--- Calling Static Method ---
Cannot access instance or class data directly.
Just a utility function: 20 + 8 = 28
Этот пример предоставляет четкую справку о том, когда использовать каждый тип метода, в зависимости от того, нужен ли ему доступ к состоянию экземпляра, состоянию класса или ни к тому, ни к другому.
Резюме
В этой лабораторной работе вы получили практическое понимание декораторов в Python. Вы начали с изучения того, как создавать и применять базовый декоратор для добавления функциональности функции. Затем вы увидели важность использования functools.wraps для сохранения метаданных исходной функции, что является важнейшей передовой практикой для написания чистого и поддерживаемого кода декораторов.
Кроме того, вы изучили мощные встроенные декораторы. Вы научились использовать декоратор @property для создания управляемых атрибутов с настраиваемой логикой геттера и сеттера, что позволяет реализовывать такие функции, как валидация ввода. Наконец, вы научились различать методы экземпляра, методы класса (@classmethod) и статические методы (@staticmethod), понимая, как каждый из них служит своей цели в структуре класса в зависимости от его доступа к состоянию экземпляра и класса.



