Низкоуровневые аспекты создания классов

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

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

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

Введение

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

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

Ручное создание класса

В программировании на Python классы являются фундаментальным понятием, которое позволяет группировать данные и функции вместе. Обычно мы определяем классы с использованием стандартного синтаксиса Python. Например, вот простой класс Stock. Этот класс представляет акцию с атрибутами, такими как name, shares и price, и имеет методы для расчета стоимости и продажи акций.

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

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

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

Но вы когда-нибудь задумывались, как Python на самом деле создает класс "за кулисами"? Что, если мы хотим создать этот класс, не используя стандартный синтаксис классов? В этом разделе мы рассмотрим, как классы Python создаются на более низком уровне.

Запуск интерактивной оболочки Python

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

Откройте терминал в WebIDE и запустите интерактивную оболочку Python, введя следующие команды. Первая команда cd ~/project изменяет текущую директорию на директорию проекта, а вторая команда python3 запускает интерактивную оболочку Python 3.

cd ~/project
python3

Определение методов как обычных функций

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

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

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

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

Здесь функция __init__ является специальным методом в классах Python. Она называется конструктором и используется для инициализации атрибутов объекта при создании экземпляра класса. Метод cost вычисляет общую стоимость акций, а метод sell уменьшает количество акций.

Создание словаря методов

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

methods = {
    '__init__': __init__,
    'cost': cost,
    'sell': sell
}

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

Использование конструктора type() для создания класса

В Python функция type() является встроенной функцией, которая может быть использована для создания классов на более низком уровне. Функция type() принимает три аргумента:

  1. Имя класса: Это строка, которая представляет имя класса, который мы хотим создать.
  2. Кортеж базовых классов: В Python классы могут наследоваться от других классов. Здесь мы используем (object,), что означает, что наш класс наследуется от базового класса object, который является базовым классом для всех классов в Python.
  3. Словарь, содержащий методы и атрибуты: Это словарь, который мы создали ранее и который содержит все методы нашего класса.
Stock = type('Stock', (object,), methods)

Эта строка кода создает новый класс с именем Stock с использованием функции type(). Класс наследуется от класса object и имеет методы, определенные в словаре methods.

Тестирование нашего вручную созданного класса

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

s = Stock('GOOG', 100, 490.10)
print(s.name)
print(s.cost())
s.sell(25)
print(s.shares)

В первой строке мы создаем экземпляр класса Stock с именем GOOG, 100 акциями и ценой 490.10. Затем мы выводим имя акции, вычисляем и выводим стоимость, продаем 25 акций и, наконец, выводим оставшееся количество акций.

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

GOOG
49010.0
75

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

Выйдите из оболочки Python, когда закончите:

exit()

Создание вспомогательного класса с типизацией структуры

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

Понимание класса Structure

Сначала нам нужно открыть файл structure.py в редакторе WebIDE. Этот файл содержит базовый класс Structure. Этот класс предоставляет основную функциональность для инициализации и представления структурированных объектов. Инициализация означает настройку объекта с предоставленными данными, а представление - это то, как объект отображается при его выводе.

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

cd ~/project

После выполнения этой команды вы окажетесь в правильной директории, где находится файл structure.py. При открытии файла вы увидите базовый класс Structure. Наша цель - расширить этот класс для поддержки валидации типов.

Реализация функции typed_structure

Теперь добавим функцию typed_structure в файл structure.py. Эта функция создаст новый класс, который наследуется от класса Structure и включает указанные валидаторы. Наследование означает, что новый класс будет иметь всю функциональность класса Structure и также может добавить свои собственные особенности. Валидаторы используются для проверки, являются ли значения, присваиваемые атрибутам класса, допустимыми.

Вот код функции typed_structure:

def typed_structure(clsname, **validators):
    """
    Create a Structure class with type validation.

    Parameters:
    - clsname: Name of the class to create
    - validators: Keyword arguments mapping attribute names to validator objects

    Returns:
    - A new class with the specified name and validators
    """
    cls = type(clsname, (Structure,), validators)
    return cls

Параметр clsname - это имя, которое мы хотим дать новому классу. Параметр validators - это словарь, где ключи - это имена атрибутов, а значения - это объекты валидаторов. Функция type() используется для динамического создания нового класса. Она принимает три аргумента: имя класса, кортеж базовых классов (в данном случае только класс Structure) и словарь атрибутов класса (валидаторы).

После добавления этой функции ваш файл structure.py должен выглядеть следующим образом:

## Structure class definition

class Structure:
    _fields = ()

    def __init__(self, *args, **kwargs):
        if len(args) > len(self._fields):
            raise TypeError(f'Expected {len(self._fields)} arguments')

        ## Set all of the positional arguments
        for name, value in zip(self._fields, args):
            setattr(self, name, value)

        ## Set the remaining keyword arguments
        for name, value in kwargs.items():
            setattr(self, name, value)

    def __repr__(self):
        attrs = ', '.join(f'{name}={getattr(self, name)!r}' for name in self._fields)
        return f'{type(self).__name__}({attrs})'

def typed_structure(clsname, **validators):
    """
    Create a Structure class with type validation.

    Parameters:
    - clsname: Name of the class to create
    - validators: Keyword arguments mapping attribute names to validator objects

    Returns:
    - A new class with the specified name and validators
    """
    cls = type(clsname, (Structure,), validators)
    return cls

Тестирование функции typed_structure

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

Сначала откройте интерактивную оболочку Python. Используем следующие команды в терминале:

cd ~/project
python3

Первая команда переводит нас в правильную директорию, а вторая команда запускает интерактивную оболочку Python.

Теперь импортируем необходимые компоненты и создаем структуру с типизацией:

from validate import String, PositiveInteger, PositiveFloat
from structure import typed_structure

## Create a Stock class with type validation
Stock = typed_structure('Stock', name=String(), shares=PositiveInteger(), price=PositiveFloat())

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

## Test the instance
print(s.name)
print(s)

## Test validation
try:
    invalid_stock = Stock('AAPL', -10, 150.25)  ## Should raise an error
except ValueError as e:
    print(f"Validation error: {e}")

Мы импортируем валидаторы String, PositiveInteger и PositiveFloat из файла validate.py. Затем используем функцию typed_structure для создания класса Stock с валидацией типов. Создаем экземпляр класса Stock и тестируем его, выводя его атрибуты. Наконец, пытаемся создать недопустимый экземпляр акции, чтобы протестировать валидацию.

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

GOOG
Stock('GOOG', 100, 490.1)
Validation error: Expected a positive value

После завершения тестирования выйдите из оболочки Python:

exit()

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

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

Эффективное создание классов

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

Понимание существующих классов валидаторов

Сначала нам нужно открыть файл validate.py в WebIDE. Этот файл уже содержит несколько классов валидаторов, которые используются для проверки, удовлетворяют ли значения определенным условиям. Эти классы включают Validator, Positive, PositiveInteger и PositiveFloat. Мы добавим базовый класс Typed и несколько валидаторов, специфичных для определенных типов, в этот файл.

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

cd ~/project

Добавление класса валидатора Typed

Начнем с добавления класса валидатора Typed. Этот класс будет использоваться для проверки, является ли значение ожидаемого типа.

class Typed(Validator):
    expected_type = object  ## Default, will be overridden in subclasses

    @classmethod
    def check(cls, value):
        if not isinstance(value, cls.expected_type):
            raise TypeError(f'Expected {cls.expected_type}')
        super().check(value)

В этом коде expected_type по умолчанию установлен на object. Подклассы переопределят это значением конкретного типа, который они проверяют. Метод check использует функцию isinstance для проверки, является ли значение ожидаемого типа. Если нет, он вызывает исключение TypeError.

Традиционно мы бы создали валидаторы, специфичные для определенных типов, следующим образом:

class Integer(Typed):
    expected_type = int

class Float(Typed):
    expected_type = float

class String(Typed):
    expected_type = str

Однако этот подход является повторяющимся. Мы можем сделать лучше, используя конструктор type() для динамического создания этих классов.

Динамическое создание валидаторов типов

Мы заменим отдельные определения классов более эффективным подходом.

_typed_classes = [
    ('Integer', int),
    ('Float', float),
    ('String', str)
]

globals().update((name, type(name, (Typed,), {'expected_type': ty}))
                 for name, ty in _typed_classes)

Вот что делает этот код:

  1. Он определяет список кортежей. Каждый кортеж содержит имя класса и соответствующий тип Python.
  2. Он использует генераторное выражение с функцией type() для создания каждого класса. Функция type() принимает три аргумента: имя класса, кортеж базовых классов и словарь атрибутов класса.
  3. Он использует globals().update() для добавления только что созданных классов в глобальное пространство имен. Это делает классы доступными в рамках всего модуля.

Ваш завершенный файл validate.py должен выглядеть приблизительно так:

## Basic validator classes

class Validator:
    def __set_name__(self, owner, name):
        self.name = name

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return instance.__dict__[self.name]

    def __set__(self, instance, value):
        self.check(value)
        instance.__dict__[self.name] = value

    @classmethod
    def check(cls, value):
        pass

class Positive(Validator):
    @classmethod
    def check(cls, value):
        if value <= 0:
            raise ValueError('Expected a positive value')
        super().check(value)

class PositiveInteger(Positive):
    @classmethod
    def check(cls, value):
        if not isinstance(value, int):
            raise TypeError('Expected an integer')
        super().check(value)

class PositiveFloat(Positive):
    @classmethod
    def check(cls, value):
        if not isinstance(value, float):
            raise TypeError('Expected a float')
        super().check(value)

class Typed(Validator):
    expected_type = object  ## Default, will be overridden in subclasses

    @classmethod
    def check(cls, value):
        if not isinstance(value, cls.expected_type):
            raise TypeError(f'Expected {cls.expected_type}')
        super().check(value)

## Generate type validators dynamically
_typed_classes = [
    ('Integer', int),
    ('Float', float),
    ('String', str)
]

globals().update((name, type(name, (Typed,), {'expected_type': ty}))
                 for name, ty in _typed_classes)

Тестирование динамически созданных классов

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

cd ~/project
python3

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

from validate import Integer, Float, String

## Test the Integer validator
i = Integer()
i.__set_name__(None, 'test_int')
try:
    i.check("not an integer")
    print("Error: Check passed when it should have failed")
except TypeError as e:
    print(f"Integer validation: {e}")

## Test the String validator
s = String()
s.__set_name__(None, 'test_str')
try:
    s.check(123)
    print("Error: Check passed when it should have failed")
except TypeError as e:
    print(f"String validation: {e}")

## Add a new validator class to the list
import sys
print("Current validator classes:", [cls for cls in dir() if cls in ['Integer', 'Float', 'String']])

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

После завершения тестирования выйдите из оболочки Python:

exit()

Расширение динамического создания классов

Если вы хотите добавить больше валидаторов типов, вы можете просто обновить список _typed_classes в файле validate.py.

_typed_classes = [
    ('Integer', int),
    ('Float', float),
    ('String', str),
    ('List', list),
    ('Dict', dict),
    ('Bool', bool)
]

Этот подход предоставляет мощный и эффективный способ создания нескольких похожих классов без написания повторяющегося кода. Он позволяет легко масштабировать ваше приложение по мере роста требований.

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

Резюме

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

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