Понимание декораторов в Python

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

Введение

В этой лабораторной работе вы получите всестороннее представление о декораторах (decorators) в Python — мощной функции для изменения или расширения возможностей функций и методов. Мы начнем с введения в основную концепцию декораторов и рассмотрим их базовое использование на практических примерах.

Основываясь на этом фундаменте, вы узнаете, как эффективно использовать functools.wraps для сохранения важной метаинформации декорируемой функции. Затем мы углубимся в специфические декораторы, такие как property, поняв его роль в управлении доступом к атрибутам. Наконец, в лабораторной работе будут разъяснены различия между методами экземпляра (instance methods), методами класса (class methods) и статическими методами (static methods), демонстрируя, как декораторы используются в этих контекстах для управления поведением методов внутри классов.

Это Guided Lab, который предоставляет пошаговые инструкции, чтобы помочь вам учиться и практиковаться. Внимательно следуйте инструкциям, чтобы выполнить каждый шаг и получить практический опыт. Исторические данные показывают, что это лабораторная работа уровня начальный с процентом завершения 93%. Он получил 100% положительных отзывов от учащихся.

Понимание базовых декораторов

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

Сначала найдите файл 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__}")

Единственные изменения заключаются в следующем:

  1. Мы импортировали wraps из модуля functools.
  2. Мы добавили @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), понимая, как каждый из них служит своей цели в структуре класса в зависимости от его доступа к состоянию экземпляра и класса.