Определение правильного вызываемого объекта

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

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

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

Введение

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

Вы также научитесь реализовывать вызываемый объект с использованием метода __call__ и использовать аннотации функций для вызываемых объектов для валидации параметров. Файл validate.py будет изменен в рамках этого практического занятия.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL python(("Python")) -.-> python/BasicConceptsGroup(["Basic Concepts"]) python(("Python")) -.-> python/FunctionsGroup(["Functions"]) python(("Python")) -.-> python/ObjectOrientedProgrammingGroup(["Object-Oriented Programming"]) python/BasicConceptsGroup -.-> python/variables_data_types("Variables and Data Types") python/BasicConceptsGroup -.-> python/type_conversion("Type Conversion") python/FunctionsGroup -.-> python/function_definition("Function Definition") python/ObjectOrientedProgrammingGroup -.-> python/classes_objects("Classes and Objects") python/ObjectOrientedProgrammingGroup -.-> python/constructor("Constructor") subgraph Lab Skills python/variables_data_types -.-> lab-132513{{"Определение правильного вызываемого объекта"}} python/type_conversion -.-> lab-132513{{"Определение правильного вызываемого объекта"}} python/function_definition -.-> lab-132513{{"Определение правильного вызываемого объекта"}} python/classes_objects -.-> lab-132513{{"Определение правильного вызываемого объекта"}} python/constructor -.-> lab-132513{{"Определение правильного вызываемого объекта"}} end

Понимание валидаторных классов

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

Начнем с открытия файла validate.py в WebIDE. Этот файл содержит код валидаторных классов, которые мы будем использовать. Чтобы открыть его, выполните следующую команду в терминале:

code /home/labex/project/validate.py

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

  1. Validator: Это базовый класс. Он имеет метод check, но в настоящее время этот метод ничего не делает. Он служит отправной точкой для других валидаторных классов.
  2. Typed: Это подкласс Validator. Его основная задача - проверить, является ли значение определенного типа.
  3. Integer, Float и String: Это конкретные валидаторы типов, которые наследуются от Typed. Они предназначены для проверки, является ли значение целым числом, числом с плавающей точкой или строкой соответственно.

Теперь давайте посмотрим, как эти валидаторные классы работают на практике. Мы создадим новый файл с именем test.py для их тестирования. Чтобы создать и открыть этот файл, выполните следующую команду:

code /home/labex/project/test.py

После открытия файла test.py добавьте в него следующий код. Этот код будет тестировать валидаторы Integer и String:

from validate import Integer, String, Float

## Test Integer validator
print("Testing Integer validator:")
try:
    Integer.check(42)
    print("✓ Integer check passed for 42")
except TypeError as e:
    print(f"✗ Error: {e}")

try:
    Integer.check("Hello")
    print("✗ Integer check incorrectly passed for 'Hello'")
except TypeError as e:
    print(f"✓ Correctly raised error: {e}")

## Test String validator
print("\nTesting String validator:")
try:
    String.check("Hello")
    print("✓ String check passed for 'Hello'")
except TypeError as e:
    print(f"✗ Error: {e}")

В этом коде мы сначала импортируем валидаторы Integer, String и Float из файла validate.py. Затем мы тестируем валидатор Integer, пытаясь проверить целочисленное значение (42) и строковое значение ("Hello"). Если проверка проходит для целого числа, мы выводим сообщение об успехе. Если проверка проходит ошибочно для строки, мы выводим сообщение об ошибке. Если проверка правильно вызывает исключение TypeError для строки, мы выводим сообщение об успехе. Мы выполняем аналогичный тест для валидатора String.

После добавления кода запустите тестовый файл, используя следующую команду:

python3 /home/labex/project/test.py

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

Testing Integer validator:
✓ Integer check passed for 42
✓ Correctly raised error: Expected <class 'int'>

Testing String validator:
✓ String check passed for 'Hello'

Как вы можете видеть, эти валидаторные классы позволяют нам легко выполнять проверку типов. Например, когда вы вызываете Integer.check(x), он вызовет исключение TypeError, если x не является целым числом.

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

def add(x, y):
    Integer.check(x)  ## Make sure x is an integer
    Integer.check(y)  ## Make sure y is an integer
    return x + y

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

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

Создание базового вызываемого объекта

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

Начнем с модификации файла validate.py. Мы добавим в этот файл новый класс с именем ValidatedFunction, и этот класс станет нашим вызываемым объектом. Чтобы открыть файл в редакторе кода, выполните следующую команду в терминале:

code /home/labex/project/validate.py

После открытия файла прокрутите его до конца и добавьте следующий код:

class ValidatedFunction:
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        print('Calling', self.func)
        result = self.func(*args, **kwargs)
        return result

Разберем, что делает этот код. Класс ValidatedFunction имеет метод __init__, который является конструктором. Когда вы создаете экземпляр этого класса, вы передаете в него функцию. Эта функция затем сохраняется как атрибут экземпляра с именем self.func.

Метод __call__ - это ключевой элемент, который делает этот класс вызываемым. Когда вы вызываете экземпляр класса ValidatedFunction, метод __call__ выполняется. Вот что он делает пошагово:

  1. Он выводит сообщение, которое сообщает, какую функцию вызывают. Это полезно для отладки и понимания происходящего.
  2. Он вызывает функцию, которая была сохранена в self.func, с аргументами, которые вы передали при вызове экземпляра. *args и **kwargs позволяют передавать любое количество позиционных и именованных аргументов.
  3. Он возвращает результат вызова функции.

Теперь давайте протестируем класс ValidatedFunction. Мы создадим новый файл с именем test_callable.py для написания нашего тестового кода. Чтобы открыть этот новый файл в редакторе кода, выполните следующую команду:

code /home/labex/project/test_callable.py

Добавьте следующий код в файл test_callable.py:

from validate import ValidatedFunction

def add(x, y):
    return x + y

## Wrap the add function with ValidatedFunction
validated_add = ValidatedFunction(add)

## Call the wrapped function
result = validated_add(2, 3)
print(f"Result: {result}")

## Try another call
result = validated_add(10, 20)
print(f"Result: {result}")

В этом коде мы сначала импортируем класс ValidatedFunction из файла validate.py. Затем мы определяем простую функцию с именем add, которая принимает два числа и возвращает их сумму.

Мы создаем экземпляр класса ValidatedFunction, передавая в него функцию add. Это "оборачивает" функцию add внутри экземпляра класса ValidatedFunction.

Затем мы вызываем обернутую функцию дважды, один раз с аргументами 2 и 3, а затем с 10 и 20. Каждый раз, когда мы вызываем обернутую функцию, метод __call__ класса ValidatedFunction вызывается, который в свою очередь вызывает исходную функцию add.

Чтобы запустить тестовый код, выполните следующую команду в терминале:

python3 /home/labex/project/test_callable.py

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

Calling <function add at 0x7f2d1c3a9940>
Result: 5
Calling <function add at 0x7f2d1c3a9940>
Result: 30

Этот вывод показывает, что наш вызываемый объект работает как ожидается. Когда мы вызываем validated_add(2, 3), на самом деле вызывается метод __call__ класса ValidatedFunction, который затем вызывает исходную функцию add.

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

✨ Проверить решение и практиковаться

Реализация валидации типов с использованием аннотаций функций

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

Давайте посмотрим на пример:

def add(x: int, y: int) -> int:
    return x + y

В этом коде x: int и y: int сообщают, что параметры x и y должны быть целыми числами. -> int в конце указывает, что функция add возвращает целое число. Эти аннотации типов хранятся в атрибуте __annotations__ функции, который представляет собой словарь, отображающий имена параметров на их аннотированные типы.

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

Сначала нам нужно изменить класс ValidatedFunction в файле validate.py. Вы можете открыть этот файл с помощью следующей команды:

code /home/labex/project/validate.py

Замените существующий класс ValidatedFunction на следующую улучшенную версию:

import inspect

class ValidatedFunction:
    def __init__(self, func):
        self.func = func
        self.signature = inspect.signature(func)

    def __call__(self, *args, **kwargs):
        ## Bind the arguments to the function parameters
        bound = self.signature.bind(*args, **kwargs)

        ## Validate each argument against its annotation
        for name, val in bound.arguments.items():
            if name in self.func.__annotations__:
                ## Get the validator class from the annotation
                validator = self.func.__annotations__[name]
                ## Apply the validation
                validator.check(val)

        ## Call the function with the validated arguments
        return self.func(*args, **kwargs)

Вот что делает эта улучшенная версия:

  1. Она использует inspect.signature(), чтобы получить информацию о параметрах функции, таких как их имена, значения по умолчанию и аннотированные типы.
  2. Метод bind() сигнатуры используется для сопоставления предоставленных аргументов с соответствующими именами параметров. Это помогает нам связать каждый аргумент с правильным параметром в функции.
  3. Она проверяет каждый аргумент на соответствие его аннотации типа (если таковая существует). Если аннотация найдена, она извлекает класс валидатора из аннотации и применяет валидацию с помощью метода check().
  4. Наконец, она вызывает исходную функцию с валидированными аргументами.

Теперь давайте протестируем этот улучшенный класс ValidatedFunction с некоторыми функциями, которые используют наши классы валидаторов в своих аннотациях типов. Откройте файл test_validation.py с помощью следующей команды:

code /home/labex/project/test_validation.py

Добавьте следующий код в файл:

from validate import ValidatedFunction, Integer, String

def greet(name: String, times: Integer):
    return name * times

## Wrap the greet function with ValidatedFunction
validated_greet = ValidatedFunction(greet)

## Valid call
try:
    result = validated_greet("Hello ", 3)
    print(f"Valid call result: {result}")
except TypeError as e:
    print(f"Unexpected error: {e}")

## Invalid call - wrong type for 'name'
try:
    result = validated_greet(123, 3)
    print(f"Invalid call unexpectedly succeeded: {result}")
except TypeError as e:
    print(f"Expected error for name: {e}")

## Invalid call - wrong type for 'times'
try:
    result = validated_greet("Hello ", "3")
    print(f"Invalid call unexpectedly succeeded: {result}")
except TypeError as e:
    print(f"Expected error for times: {e}")

В этом коде мы определяем функцию greet с аннотациями типов name: String и times: Integer. Это означает, что параметр name должен быть валидирован с использованием класса String, а параметр times - с использованием класса Integer. Затем мы оборачиваем функцию greet в наш класс ValidatedFunction, чтобы включить валидацию типов.

Мы выполняем три тестовых случая: правильный вызов, неправильный вызов с неправильным типом для name и неправильный вызов с неправильным типом для times. Каждый вызов обернут в блок try-except, чтобы поймать любые исключения TypeError, которые могут быть возбуждены во время валидации.

Чтобы запустить тестовый файл, используйте следующую команду:

python3 /home/labex/project/test_validation.py

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

Valid call result: Hello Hello Hello
Expected error for name: Expected <class 'str'>
Expected error for times: Expected <class 'int'>

Этот вывод показывает, что наш вызываемый объект ValidatedFunction теперь обеспечивает валидацию типов на основе аннотаций функций. Когда мы передаем аргументы неправильного типа, классы валидаторов обнаруживают ошибку и возбуждают исключение TypeError. Таким образом, мы можем гарантировать, что функции вызываются с правильными типами данных, что помогает предотвратить ошибки и делает наш код более надежным.

✨ Проверить решение и практиковаться

Вызов: Использование вызываемого объекта в качестве метода

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

Давайте исследуем эту проблему, создав класс Stock. Этот класс будет представлять акцию с такими атрибутами, как название, количество акций и цена. Мы также будем использовать валидатор, чтобы убедиться, что данные, с которыми мы работаем, корректны.

Сначала откройте файл stock.py, чтобы начать писать наш класс Stock. Вы можете использовать следующую команду, чтобы открыть файл в редакторе:

code /home/labex/project/stock.py

Теперь добавьте следующий код в файл stock.py. Этот код определяет класс Stock с методом __init__ для инициализации атрибутов акции, свойством cost для вычисления общей стоимости и методом sell для уменьшения количества акций. Мы также попытаемся использовать ValidatedFunction для валидации входных данных для метода sell.

from validate import ValidatedFunction, Integer

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

    @property
    def cost(self):
        return self.shares * self.price

    def sell(self, nshares: Integer):
        self.shares -= nshares

    ## Try to use ValidatedFunction
    sell = ValidatedFunction(sell)

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

code /home/labex/project/test_stock.py

Добавьте следующий код в файл test_stock.py. Этот код создает экземпляр класса Stock, выводит начальное количество акций и стоимость, пытается продать некоторые акции, а затем выводит обновленное количество акций и стоимость.

from stock import Stock

try:
    ## Create a stock
    s = Stock('GOOG', 100, 490.1)

    ## Get the initial cost
    print(f"Initial shares: {s.shares}")
    print(f"Initial cost: ${s.cost}")

    ## Try to sell some shares
    s.sell(10)

    ## Check the updated cost
    print(f"After selling, shares: {s.shares}")
    print(f"After selling, cost: ${s.cost}")

except Exception as e:
    print(f"Error: {e}")

Теперь запустите тестовый файл с помощью следующей команды:

python3 /home/labex/project/test_stock.py

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

Error: missing a required argument: 'nshares'

Эта ошибка возникает потому, что когда Python вызывает метод, такой как s.sell(10), на самом деле он вызывает Stock.sell(s, 10) в фоновом режиме. Параметр self представляет экземпляр класса, и он автоматически передается в качестве первого аргумента. Однако наш ValidatedFunction не обрабатывает этот параметр self правильно, потому что он не знает, что его используют как метод.

Понимание проблемы

Когда вы определяете метод внутри класса и затем заменяете его на ValidatedFunction, вы, по сути, оборачиваете исходный метод. Проблема в том, что обернутый метод не автоматически обрабатывает параметр self правильно. Он ожидает аргументы таким образом, что не учитывает передачу экземпляра в качестве первого аргумента.

Решение проблемы

Чтобы решить эту проблему, нам нужно изменить способ обработки методов. Мы создадим новый класс с именем ValidatedMethod, который может правильно обрабатывать вызовы методов. Добавьте следующий код в конец файла validate.py:

import inspect

class ValidatedMethod:
    def __init__(self, func):
        self.func = func
        self.signature = inspect.signature(func)

    def __get__(self, instance, owner):
        ## This method is called when the attribute is accessed as a method
        if instance is None:
            return self

        ## Return a callable that binds 'self' to the instance
        def method_wrapper(*args, **kwargs):
            return self(instance, *args, **kwargs)

        return method_wrapper

    def __call__(self, instance, *args, **kwargs):
        ## Bind the arguments to the function parameters
        bound = self.signature.bind(instance, *args, **kwargs)

        ## Validate each argument against its annotation
        for name, val in bound.arguments.items():
            if name in self.func.__annotations__:
                ## Get the validator class from the annotation
                validator = self.func.__annotations__[name]
                ## Apply the validation
                validator.check(val)

        ## Call the function with the validated arguments
        return self.func(instance, *args, **kwargs)

Теперь нам нужно изменить класс Stock, чтобы использовать ValidatedMethod вместо ValidatedFunction. Откройте файл stock.py снова:

code /home/labex/project/stock.py

Обновите класс Stock следующим образом:

from validate import ValidatedMethod, Integer

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

    @property
    def cost(self):
        return self.shares * self.price

    @ValidatedMethod
    def sell(self, nshares: Integer):
        self.shares -= nshares

Класс ValidatedMethod является дескриптором, который представляет собой особый тип объекта в Python, который может изменить способ доступа к атрибутам. Метод __get__ вызывается, когда атрибут доступен как метод. Он возвращает вызываемый объект, который правильно передает экземпляр в качестве первого аргумента.

Запустите тестовый файл снова с помощью следующей команды:

python3 /home/labex/project/test_stock.py

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

Initial shares: 100
Initial cost: $49010.0
After selling, shares: 90
After selling, cost: $44109.0

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

Резюме

В этом практическом занятии (лабораторной работе) вы научились создавать правильные вызываемые объекты в Python. Сначала вы изучили базовые классы валидаторов для проверки типов и создали вызываемый объект с использованием метода __call__. Затем вы улучшили этот объект, чтобы он выполнял валидацию на основе аннотаций функций, и решили проблему использования вызываемых объектов в качестве методов класса.

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