Использование классов

Узнайте, как использовать обертки классов для более чистых API плагинов, основанных на объектно-ориентированном программировании, в Python.

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

Зачем использовать классы?

Работа с API на основе дескрипторов в традиционном стиле C может быть многословной и подверженной ошибкам:

Без классов (стиль C)
from plugify.pps import example_plugin

# Создать дескриптор KeyValues вручную
kv_handle = example_plugin.Kv1Create("MyConfig")

# Установить свойства, используя дескриптор
example_plugin.Kv1SetName(kv_handle, "ServerSettings")

# Найти подключ
subkey_handle = example_plugin.Kv1FindKey(kv_handle, "Players")

# Легко забыть очистку!
example_plugin.Kv1Destroy(kv_handle)

С классами тот же код становится намного чище и безопаснее:

С классами (стиль ООП)
from plugify.pps import example_plugin

# Создать с использованием конструктора класса
with example_plugin.KeyValues("MyConfig") as kv:
    # Использовать интуитивные методы
    kv.SetName("ServerSettings")

    # Найти подключ - возвращает экземпляр KeyValues
    subkey = kv.FindKey("Players")

    # Автоматическая очистка при выходе из контекста!

Преимущества:

  • Более чистый синтаксис - Методы вместо функций с явными дескрипторами
  • Автоматическое управление ресурсами - Нет необходимости вручную вызывать функции уничтожения
  • Безопасность типов - Лучшее автодополнение в IDE и проверка типов
  • Меньше ошибок - Труднее забыть очистку или перепутать дескрипторы
  • Питонический API - Ощущается естественным для разработчиков на Python

Как работают классы

Когда плагин определяет классы в своем манифесте, языковой модуль Python создаёт для них классы-обертки, когда ваш плагин импортирует этот плагин. Обертки:

  1. Оборачивают базовый дескриптор - Хранят необработанный указатель внутри
  2. Связывают методы - Преобразуют вызовы функций в вызовы методов с автоматической передачей дескриптора
  3. Управляют жизненным циклом - Автоматически вызывают деструктор, когда объект больше не нужен
  4. Предоставляют утилитарные методы - get(), release(), valid(), close(), reset() для управления дескриптором
  5. Поддерживают менеджеры контекста - Реализуют __enter__ и __exit__ для операторов with
  6. Предоставляют подсказки типов - plugify-gen -lang python -classes генерирует .pyi stub-файлы для поддержки IDE

Определение классов в вашем манифесте

Чтобы создать классы для вашего плагина, добавьте секцию classes в ваш манифест:

plugin.pplugin
{
  "name": "example_plugin",
  "version": "1.0.0",
  "language": "cpp",
  "methods": [
    { "name": "Kv1Create", "funcName": "Kv1Create", "paramTypes": [{ "name": "setName", "type": "string" }], "retType": { "type": "ptr64" } },
    { "name": "Kv1Destroy", "funcName": "Kv1Destroy", "paramTypes": [{ "name": "kv", "type": "ptr64" }], "retType": { "type": "void" } },
    { "name": "Kv1GetName", "funcName": "Kv1GetName", "paramTypes": [{ "name": "kv", "type": "ptr64" }], "retType": { "type": "string" } },
    { "name": "Kv1SetName", "funcName": "Kv1SetName", "paramTypes": [{ "name": "kv", "type": "ptr64" }, { "name": "name", "type": "string" }], "retType": { "type": "void" } },
    { "name": "Kv1FindKey", "funcName": "Kv1FindKey", "paramTypes": [{ "name": "kv", "type": "ptr64" }, { "name": "keyName", "type": "string" }], "retType": { "type": "ptr64" } },
    { "name": "Kv1GetFirstSubKey", "funcName": "Kv1GetFirstSubKey", "paramTypes": [{ "name": "kv", "type": "ptr64" }], "retType": { "type": "ptr64" } },
    { "name": "Kv1AddSubKey", "funcName": "Kv1AddSubKey", "paramTypes": [{ "name": "kv", "type": "ptr64" }, { "name": "subKey", "type": "ptr64" }], "retType": { "type": "void" } },
    { "name": "Kv1MakeCopy", "funcName": "Kv1MakeCopy", "paramTypes": [{ "name": "kv", "type": "ptr64" }], "retType": { "type": "ptr64" } },
    { "name": "Kv1GetString", "funcName": "Kv1GetString", "paramTypes": [{ "name": "kv", "type": "ptr64" }, { "name": "keyName", "type": "string" }, { "name": "defaultValue", "type": "string" }], "retType": { "type": "string" } },
    { "name": "Kv1SetString", "funcName": "Kv1SetString", "paramTypes": [{ "name": "kv", "type": "ptr64" }, { "name": "keyName", "type": "string" }, { "name": "value", "type": "string" }], "retType": { "type": "void" } }
  ],
  "classes": [
    {
      "name": "KeyValues",
      "description": "RAII wrapper for KeyValues handle",
      "handleType": "ptr64",
      "invalidValue": "0",
      "constructors": ["Kv1Create"],
      "destructor": "Kv1Destroy",
      "bindings": [
        { "name": "GetName", "method": "Kv1GetName", "bindSelf": true },
        { "name": "SetName", "method": "Kv1SetName", "bindSelf": true },
        { "name": "FindKey", "method": "Kv1FindKey", "bindSelf": true, "retAlias": { "name": "KeyValues", "owner": false } },
        { "name": "GetFirstSubKey", "method": "Kv1GetFirstSubKey", "bindSelf": true, "retAlias": { "name": "KeyValues", "owner": false } },
        { "name": "AddSubKey", "method": "Kv1AddSubKey", "bindSelf": true, "paramAliases": [{ "name": "KeyValues", "owner": true }] },
        { "name": "MakeCopy", "method": "Kv1MakeCopy", "bindSelf": true, "retAlias": { "name": "KeyValues", "owner": true } },
        { "name": "GetString", "method": "Kv1GetString", "bindSelf": true },
        { "name": "SetString", "method": "Kv1SetString", "bindSelf": true }
      ]
    }
  ]
}

Объяснение ключевых полей:

  • name: Имя класса Python (рекомендуется PascalCase)
  • handleType: Тип базового дескриптора (обычно ptr64 или ptr32)
  • invalidValue: Какое значение представляет недопустимый дескриптор (обычно "0" или "-1")
  • constructors: Массив имен методов, которые создают экземпляры
  • destructor: Имя метода, который очищает ресурсы (опционально)
  • bindings: Массив методов, доступных в классе
    • bindSelf: Если true, автоматически передает дескриптор в качестве первого параметра

Сгенерированный код Python

Когда вы определяете классы в своем манифесте, языковой модуль Python создаёт классы-обертки с несколькими встроенными методами. Концептуально они выглядят так:

Сгенерированный класс (концептуально)
class KeyValues:
    """RAII wrapper for KeyValues handle"""

    def __init__(self, *args, **kwargs):
        """Создает новый экземпляр KeyValues

        Может быть вызван двумя способами:
        1. Режим конструктора: KeyValues("ConfigName")
        2. Режим прямого дескриптора: KeyValues(handle_value, Ownership.OWNED)
        """
        from plugify.plugin import Ownership

        # Сначала инициализировать в недопустимое состояние
        self._handle = 0  # invalid_value
        self._owned = Ownership.BORROWED

        # Конструкция прямого дескриптора
        if len(args) == 2 and isinstance(args[1], Ownership):
            self._handle = args[0]
            self._owned = args[1]
            return

        # Режим вызова конструктора
        try:
            self._handle = _plugin.Kv1Create(*args, **kwargs)
            self._owned = Ownership.OWNED
        except Exception as e:
            raise e

    def close(self):
        """Закрыть/уничтожить дескриптор, если он принадлежит"""
        if not hasattr(self, '_handle'):
            return

        if self._handle != 0 and self._owned == Ownership.OWNED:
            _plugin.Kv1Destroy(self._handle)
        self._handle = 0
        self._owned = Ownership.BORROWED

    def __del__(self):
        """Автоматически вызывается при уничтожении объекта"""
        self.close()

    def __enter__(self):
        """Вход в менеджер контекста"""
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        """Выход из менеджера контекста - обеспечивает очистку"""
        self.close()
        return False

    # Утилитарные методы
    def get(self) -> int:
        """Возвращает значение базового дескриптора"""
        if not hasattr(self, '_handle'):
            return 0
        return self._handle

    def release(self) -> int:
        """Освобождает владение и возвращает дескриптор"""
        if not hasattr(self, '_handle'):
            return 0
        tmp = self._handle
        self._handle = 0
        self._owned = Ownership.BORROWED
        return tmp

    def reset(self):
        """Сбросить дескриптор, закрыв его"""
        self.close()

    def valid(self) -> bool:
        """Проверяет, является ли дескриптор действительным"""
        if not hasattr(self, '_handle'):
            return False
        return self._handle != 0

    # Связанные методы (с автоматической проверкой дескриптора)
    def GetName(self) -> str:
        """Получает имя секции"""
        if self._handle == 0:
            raise RuntimeError("KeyValues handle is closed")
        return _plugin.Kv1GetName(self._handle)

    def SetName(self, name: str) -> None:
        """Устанавливает имя секции"""
        if self._handle == 0:
            raise RuntimeError("KeyValues handle is closed")
        _plugin.Kv1SetName(self._handle, name)

    def FindKey(self, keyName: str) -> 'KeyValues':
        """Находит ключ по имени, возвращает принадлежащий экземпляр KeyValues"""
        if self._handle == 0:
            raise RuntimeError("KeyValues handle is closed")
        result = _plugin.Kv1FindKey(self._handle, keyName)
        # Автоматически оборачивает возвращаемое значение на основе retAlias
        if result != 0:
            return KeyValues(result, Ownership.OWNED)
        return None

    def AddSubKey(self, subKey: 'KeyValues') -> None:
        """Добавляет подключ, принимает владение параметром"""
        if self._handle == 0:
            raise RuntimeError("KeyValues handle is closed")
        # Автоматически освобождает владение от параметра subKey
        handle = subKey.release() if hasattr(subKey, 'release') else subKey
        _plugin.Kv1AddSubKey(self._handle, handle)

plugify-gen -lang python -classes генерирует соответствующий .pyi stub-файл для поддержки IDE:

plugin.pyi
from typing import Optional
from plugify.plugin import Ownership

class KeyValues:
    """RAII wrapper for KeyValues handle"""

    def __init__(self, setName: str) -> None: ...
    def __enter__(self) -> 'KeyValues': ...
    def __exit__(self, exc_type, exc_val, exc_tb) -> bool: ...

    def close(self) -> None: ...
    def get(self) -> int: ...
    def release(self) -> int: ...
    def reset(self) -> None: ...
    def valid(self) -> bool: ...

    def GetName(self) -> str: ...
    def SetName(self, name: str) -> None: ...
    def FindKey(self, keyName: str) -> Optional['KeyValues']: ...
    def AddSubKey(self, subKey: 'KeyValues') -> None: ...

Встроенные утилитарные методы

Каждый сгенерированный класс включает несколько утилитарных методов для управления дескриптором:

valid() - Проверка действительности дескриптора

Возвращает True, если дескриптор действителен (не равен invalidValue):

Использование valid()
from plugify.pps import example_plugin

kv = example_plugin.KeyValues("Config")

if kv.valid():
    kv.SetName("ServerConfig")
    print("Handle is valid")
else:
    print("Handle is invalid")

# После release дескриптор становится недействительным
handle = kv.release()
print(f"valid: {kv.valid()}")  # valid: False

get() - Доступ к необработанному дескриптору

Возвращает значение базового дескриптора. Используйте это, когда вам нужно передать необработанный дескриптор в функции в стиле C:

Использование get()
from plugify.pps import example_plugin

kv = example_plugin.KeyValues("Config")

# Получить необработанный дескриптор
raw_handle = kv.get()
print(f"Handle value: {raw_handle}")

# Передать в функцию, ожидающую необработанный дескриптор
example_plugin.Kv1GetName(kv.get())

release() - Передача владения

Освобождает владение дескриптором и возвращает его. После вызова release() экземпляр класса становится недействительным и не будет вызывать деструктор:

Использование release()
from plugify.pps import example_plugin

def create_and_release():
    kv = example_plugin.KeyValues("Config")
    kv.SetName("ServerConfig")

    # Передать владение наружу
    handle = kv.release()

    # kv теперь недействителен, не будет очищать
    print(f"valid: {kv.valid()}")  # valid: False

    return handle

# Теперь мы владеем дескриптором и должны очистить его вручную
raw_handle = create_and_release()
# ... использовать raw_handle ...
example_plugin.Kv1Destroy(raw_handle)  # Требуется ручная очистка!

Используйте release(), когда вам нужно:

  • Передать владение другой системе
  • Сохранить дескриптор в долгоживущей структуре данных
  • Взаимодействовать с кодом в стиле C, который принимает владение

close() - Ручная очистка

Явно закрывает дескриптор и вызывает деструктор, если объект владеет им:

Использование close()
from plugify.pps import example_plugin

kv = example_plugin.KeyValues("Config")
kv.SetName("ServerConfig")

# Явно закрыть дескриптор сейчас
kv.close()

# Дескриптор теперь недействителен
print(f"valid: {kv.valid()}")  # valid: False

# Методы вызовут RuntimeError
try:
    kv.SetName("Test")
except RuntimeError as e:
    print(f"Error: {e}")  # "KeyValues handle is closed"

Метод close():

  • Идемпотентный - Безопасно вызывать несколько раз
  • Вызывается автоматически - С помощью __del__ и __exit__
  • Учитывает владение - Вызывает деструктор только если объект владеет дескриптором

reset() - Псевдоним для Close

Удобный метод, который вызывает close():

Использование reset()
from plugify.pps import example_plugin

kv = example_plugin.KeyValues("Config")
kv.SetName("ServerConfig")

# Reset эквивалентен close
kv.reset()

print(f"valid: {kv.valid()}")  # valid: False

Управление ресурсами

Классы Python, сгенерированные Plugify, поддерживают несколько паттернов очистки:

Паттерн менеджера контекста (рекомендуется)

Каждый класс поддерживает оператор with через __enter__ и __exit__:

Менеджер контекста
from plugify.pps import example_plugin

with example_plugin.KeyValues("Config") as kv:
    kv.SetName("ServerConfig")
    kv.SetString("hostname", "My Server")
    # ... использовать kv ...
# Автоматически очищается при выходе из блока 'with'

Это рекомендуемый паттерн, потому что:

  • Гарантированная очистка даже при возникновении исключений
  • Четкая область жизни ресурса
  • Питонический и знакомый разработчикам на Python

Автоматическая очистка

Деструктор (__del__) вызывается автоматически при сборке мусора объекта:

Автоматическая очистка
from plugify.pps import example_plugin

def process_config():
    kv = example_plugin.KeyValues("Config")
    kv.SetName("ServerConfig")
    # ... использовать kv ...
    # kv автоматически уничтожается при возврате из функции

process_config()  # kv очищается здесь

Ручная очистка

Для немедленной очистки ресурсов используйте close() или удалите объект:

Ручная очистка
from plugify.pps import example_plugin

kv = example_plugin.KeyValues("Config")
kv.SetName("ServerConfig")
# ... использовать kv ...

# Вариант 1: Явно закрыть
kv.close()

# Вариант 2: Удалить объект
kv2 = example_plugin.KeyValues("Config2")
del kv2

# Оба приводят к немедленной очистке

Случай без деструктора

Если в манифесте класс не имеет определенного деструктора, он действует как простая обертка без автоматической очистки: close() только сбрасывает дескриптор, а утилитарные методы работают как обычно.

Работа с владением

Некоторые методы передают владение ресурсами. Манифест указывает это с помощью поля owner в paramAliases и retAlias.

Принятие владения (параметры метода)

Когда метод принимает владение ресурсом, вы должны передать его и не использовать после этого:

Передача владения
from plugify.pps import example_plugin

# Создать родителя и ребенка
parent = example_plugin.KeyValues("Parent")
child = example_plugin.KeyValues("Child")

# AddSubKey принимает владение child
parent.AddSubKey(child)

# AddSubKey забрал дескриптор у child: child.valid() теперь False
# parent обработает очистку

В манифесте это определяется как:

Владение в манифесте
{
  "name": "AddSubKey",
  "method": "Kv1AddSubKey",
  "bindSelf": true,
  "paramAliases": [
    {
      "name": "KeyValues",
      "owner": true  // Этот параметр принимает владение
    }
  ]
}

Лучшая практика: После передачи владения обращайтесь к объекту только через нового владельца:

Правильная обработка владения
from plugify.pps import example_plugin

parent = example_plugin.KeyValues("Parent")
child = example_plugin.KeyValues("Child")

# Передать владение
parent.AddSubKey(child)

print(f"valid: {child.valid()}")  # valid: False, обертка отдала дескриптор

# Использовать только через parent
found = parent.FindKey("Child")

Возврат владения (возвращаемые значения)

Когда метод возвращает новый ресурс с владением:

Возврат владения
from plugify.pps import example_plugin

parent = example_plugin.KeyValues("Parent")

# MakeCopy возвращает НОВЫЙ KeyValues, которым мы владеем
copy = parent.MakeCopy()

if copy and copy.valid():
    copy.SetName("ParentCopy")
    # Мы ответственны за жизненный цикл copy
    # Он будет автоматически очищен, когда выйдет из области видимости

В манифесте:

Возврат владения в манифесте
{
  "name": "MakeCopy",
  "method": "Kv1MakeCopy",
  "bindSelf": true,
  "retAlias": {
    "name": "KeyValues",
    "owner": true  // Вызывающий владеет возвращаемым объектом
  }
}

Невладеющие ссылки

Когда owner: false, метод возвращает ссылку без передачи владения:

Невладеющая ссылка
from plugify.pps import example_plugin

parent = example_plugin.KeyValues("Parent")

# GetFirstSubKey возвращает ссылку, parent все еще владеет ею
child_ref = parent.GetFirstSubKey()

if child_ref and child_ref.valid():
    # Используйте ссылку, но НЕ удаляйте ее
    name = child_ref.GetName()
    # Не вызывайте del child_ref или release()
    # child_ref будет очищен parent

Жизненный цикл плагина и глобальные объекты

При использовании классов в плагинах вы должны быть осторожны с очисткой объектов во время выгрузки плагина. CPython использует подсчет ссылок, что означает, что финализаторы (__del__) детерминированы и вызываются немедленно, когда счетчик ссылок достигает нуля. Однако, если объекты все еще живы, когда ваш плагин выгружается, их финализаторы могут быть вызваны после того, как код плагина больше не находится в памяти, что приводит к неопределенному поведению или сбоям.

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

Если вы храните экземпляры классов в глобальных переменных или атрибутах уровня модуля, вы должны явно очистить их в функции plugin_end() вашего плагина:

plugin.py
from plugify.plugin import Plugin
from plugify.pps import example_plugin

# Глобальный объект - опасен, если не очищен!
g_config = None

class MyPlugin(Plugin):
    def plugin_start(self):
        global g_config
        # Создать глобальную конфигурацию
        g_config = example_plugin.KeyValues("GlobalConfig")
        g_config.SetName("ServerSettings")
        print("Plugin started with global config")

    def plugin_end(self):
        global g_config
        # КРИТИЧНО: Очистить глобальные объекты перед выгрузкой плагина
        if g_config:
            g_config.close()  # или del g_config
            g_config = None
        print("Plugin ended, global config cleaned up")

Безопасные паттерны

✅ Безопасно: Локальная область видимости с автоматической очисткой

Безопасный паттерн
class MyPlugin(Plugin):
    def on_command(self, args):
        # Локальный объект - автоматически очищается при возврате из функции
        kv = example_plugin.KeyValues("TempConfig")
        kv.SetName("CommandConfig")
        # ... использовать kv ...
        # Автоматически уничтожается здесь - безопасно!

✅ Безопасно: Менеджер контекста

Безопасный паттерн
class MyPlugin(Plugin):
    def on_command(self, args):
        # Менеджер контекста обеспечивает очистку перед возвратом из функции
        with example_plugin.KeyValues("TempConfig") as kv:
            kv.SetName("CommandConfig")
            # ... использовать kv ...
        # Определенно очищается здесь - безопасно!

✅ Безопасно: Переменная экземпляра с очисткой

Безопасный паттерн
class MyPlugin(Plugin):
    def plugin_start(self):
        self.config = example_plugin.KeyValues("PluginConfig")
        self.config.SetName("Settings")

    def plugin_end(self):
        # Очистить переменные экземпляра
        if hasattr(self, 'config') and self.config:
            self.config.close()
            self.config = None

❌ Небезопасно: Глобальная без очистки

Небезопасный паттерн
from plugify.pps import example_plugin

# ОПАСНО: Глобальный объект
g_config = example_plugin.KeyValues("GlobalConfig")

class MyPlugin(Plugin):
    def plugin_start(self):
        g_config.SetName("ServerSettings")

    def plugin_end(self):
        # ОТСУТСТВУЕТ: Нет очистки!
        # Финализатор g_config запустится после выгрузки плагина - СБОЙ!
        pass

❌ Небезопасно: Кэш уровня модуля без очистки

Небезопасный паттерн
from plugify.pps import example_plugin

# ОПАСНО: Кэш уровня модуля
_kv_cache = {}

class MyPlugin(Plugin):
    def cache_config(self, name):
        _kv_cache[name] = example_plugin.KeyValues(name)

    def plugin_end(self):
        # ОТСУТСТВУЕТ: Нет очистки кэша!
        # Кэшированные объекты завершатся после выгрузки плагина - СБОЙ!
        pass

Контрольный список очистки

Перед выгрузкой вашего плагина (plugin_end()), убедитесь:

  1. ✅ Все глобальные экземпляры классов явно закрыты или удалены
  2. ✅ Все коллекции уровня модуля (списки, словари, множества), содержащие экземпляры классов, очищены
  3. ✅ Все переменные экземпляра класса явно очищены
  4. ✅ Не осталось ссылок на экземпляры классов в долгоживущих структурах данных
  5. ✅ Циклические ссылки разорваны (если есть)

Полный пример: Система конфигурации

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

config_manager.py
from plugify.pps import example_plugin
from typing import Optional

class ConfigManager:
    def __init__(self, config_name: str):
        """Инициализировать менеджер конфигурации"""
        self.root = example_plugin.KeyValues(config_name)

        if not self.root.valid():
            raise RuntimeError(f"Failed to create config: {config_name}")

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.root.close()
        return False

    def create_section(self, section_name: str) -> bool:
        """Создать новую секцию конфигурации"""
        section = example_plugin.KeyValues(section_name)

        if not section.valid():
            return False

        self.root.AddSubKey(section)
        return True

    def get_section(self, section_name: str) -> Optional[example_plugin.KeyValues]:
        """Получить секцию конфигурации"""
        section = self.root.FindKey(section_name)

        if section and section.valid():
            return section
        return None

    def set_value(self, section_name: str, key: str, value: str) -> bool:
        """Установить значение конфигурации"""
        section = self.get_section(section_name)

        if section:
            section.SetString(key, value)
            return True
        return False

    def get_value(self, section_name: str, key: str, default: str = "") -> str:
        """Получить значение конфигурации"""
        section = self.get_section(section_name)

        if section:
            return section.GetString(key, default)
        return default


# Использование с менеджером контекста
with ConfigManager("ServerConfig") as config:
    config.create_section("Server")
    config.set_value("Server", "hostname", "My Server")
    config.set_value("Server", "maxplayers", "32")

    config.create_section("Game")
    config.set_value("Game", "mode", "competitive")

# Ресурсы автоматически очищаются

Расширенный пример: Ручное управление дескриптором

Иногда вам нужно работать с необработанными дескрипторами вместе с экземплярами классов:

mixed_usage.py
from plugify.pps import example_plugin

def process_with_mixed_api():
    # Создать с использованием класса
    kv = example_plugin.KeyValues("Config")

    # Использовать методы класса
    kv.SetName("ProcessedConfig")

    # Получить необработанный дескриптор для функции в стиле C
    raw_handle = kv.get()

    # Вызвать устаревшую функцию в стиле C
    result = example_plugin.Kv1GetName(raw_handle)

    # Продолжить использование методов класса
    if kv.valid():
        kv.SetString("status", "done")

    # Автоматическая очистка все еще работает
    # kv будет уничтожен при возврате из функции

def transfer_ownership_example():
    # Создать и настроить
    kv = example_plugin.KeyValues("Config")
    kv.SetName("TransferTest")

    # Освободить владение
    handle = kv.release()

    # kv теперь недействителен
    assert not kv.valid()

    # Мы должны вручную уничтожить
    example_plugin.Kv1Destroy(handle)

    # Или обернуть снова
    kv2 = example_plugin.KeyValues("NewConfig")
    # ... использовать kv2 ...

Лучшие практики

  1. Всегда используйте менеджеры контекста - Используйте операторы with для гарантированной очистки
  2. Используйте close() для немедленной очистки - Более явно, чем полагаться на del или сборку мусора
  3. Очищайте глобальные объекты в plugin_end() - Критично: Явно очищайте все глобальные или уровня модуля экземпляры классов перед выгрузкой плагина, чтобы избежать сбоев
  4. Предпочитайте локальную область видимости - Держите экземпляры классов в локальной области видимости, когда возможно, для автоматической очистки
  5. Проверяйте valid() для безопасности - Особенно после операций, которые могут завершиться неудачей
  6. Уважайте владение - Не используйте объекты после передачи владения
  7. Используйте release() экономно - Только когда вам нужен ручной контроль
  8. Избегайте смешивания стилей - Предпочитайте API классов необработанным функциям в стиле C
  9. Доверяйте автоматической очистке для локальных объектов - Пусть __del__ обрабатывает очистку для локальных объектов, но всегда очищайте глобальные вручную
  10. Обрабатывайте RuntimeError - Будьте готовы перехватывать исключения от методов, вызванных на закрытых дескрипторах
  11. Проверяйте возвращаемые объекты - Методы могут возвращать None при неудаче

Когда НЕ использовать классы

Классы предназначены для ресурсов, которым требуется управление жизненным циклом. Не определяйте классы для:

  • Функции-утилиты без состояния - Простые функции, которые не управляют ресурсами
  • Функции, возвращающие примитивные значения - Нет необходимости оборачивать простые геттеры
  • Одноразовые операции - Операции, которые не поддерживают состояние
  • Глобальные синглтоны - Ресурсы, которые живут в течение всего времени работы программы

Для этих случаев продолжайте использовать обычный API на основе функций.

Устранение неполадок

Ошибка "No constructor matched"

Если аргументы не подходят ни к одному конструктору класса, конструктор выбрасывает ValueError. Если функция создания вернула недопустимое значение, объект создаётся, но valid() возвращает False:

Сбой конструктора дескриптора
try:
    kv = example_plugin.KeyValues("Config")
except ValueError as e:
    print(f"Failed to create KeyValues: {e}")
else:
    if not kv.valid():
        print("Kv1Create returned an invalid handle")

Использование закрытых или освобожденных объектов

Попытка использовать объект после вызова close(), release() или после уничтожения вызовет RuntimeError:

Использование закрытого дескриптора
from plugify.pps import example_plugin

kv = example_plugin.KeyValues("Config")
kv.close()

# Это вызовет RuntimeError
try:
    kv.SetName("Test")
except RuntimeError as e:
    print(f"{e}")  # "KeyValues handle is closed"

# Сначала проверьте valid(), чтобы избежать исключений
if kv.valid():
    kv.SetName("Test")
else:
    print("Handle is closed!")

Все связанные методы автоматически проверяют дескриптор перед вызовом базовой функции C:

Автоматическая проверка
from plugify.pps import example_plugin

kv = example_plugin.KeyValues("Config")
handle = kv.release()

# Любой вызов метода завершится неудачей
try:
    name = kv.GetName()
except RuntimeError as e:
    print(f"{e}")  # "KeyValues handle is closed"

Висячие ссылки

Будьте осторожны с невладеющими ссылками, когда владелец уничтожается:

Проблема висячей ссылки
def get_child_ref():
    parent = example_plugin.KeyValues("Parent")
    child = parent.GetFirstSubKey()  # Невладеющая ссылка
    return child  # ПЛОХО: parent будет уничтожен!

# Это опасно!
child_ref = get_child_ref()
# child_ref теперь указывает на уничтоженную память

# Лучший подход:
def get_child_owned():
    parent = example_plugin.KeyValues("Parent")
    child = parent.FindKey("Child")  # Невладеющая ссылка
    return child.MakeCopy() if child else None  # Копия, которой мы владеем, переживёт parent

Утечки памяти

Если вы испытываете утечки памяти:

  1. Проверьте циклические ссылки - GC Python обрабатывает большинство случаев, но циклические ссылки могут задержать очистку
  2. Используйте менеджеры контекста - Гарантированная очистка с операторами with
  3. Проверьте использование release() - Убедитесь, что освобожденные дескрипторы в конечном итоге уничтожаются
  4. Проверьте владение - Убедитесь, что вы не держите ссылки на принадлежащие объекты

Сбои при выгрузке плагина

Если ваш плагин падает при выгрузке:

  1. Проверьте глобальные объекты - Убедитесь, что все глобальные экземпляры классов очищены в plugin_end()
  2. Проверьте коллекции уровня модуля - Очистите все списки, словари или множества, содержащие экземпляры классов
  3. Проверьте переменные экземпляра - Очистите переменные экземпляра класса в plugin_end()
  4. Добавьте явную очистку - Используйте close() или del для всех долгоживущих объектов перед выгрузкой плагина
Исправление сбоя при выгрузке
# Глобальные объекты, которые вызывали сбои
g_config = None
g_cache = {}

class MyPlugin(Plugin):
    def plugin_start(self):
        global g_config, g_cache
        g_config = example_plugin.KeyValues("Config")
        g_cache["main"] = example_plugin.KeyValues("Main")

    def plugin_end(self):
        global g_config, g_cache

        # Очистить глобальные объекты
        if g_config:
            g_config.close()
            g_config = None

        # Очистить кэшированные объекты
        for kv in g_cache.values():
            kv.close()
        g_cache.clear()

        print("All resources cleaned up safely")

См. также

  • Узнайте, как экспортировать свои собственные классы
  • Узнайте об импорте функций
  • Справочник по манифесту плагина