10/18/2019

Менеджеры контекста - специальные объекты в Python

Стоимость использования Python всегда ищет более простые способы сделать что-то. Это относится и к Blender. Недавно я обнаружил, что копирую и вставляю из кода тот же самый старый шаблон BMesh и спрашиваю себя, не существует ли способа сделать это менее повторяющимся. Есть конечно!



Всегда пишите код, так как будто программист, который будет поддерживать ваш код, будет жестоким психопатом, который знает, где вы живете. Джон Ф. Вудс

Моей первой идеей было создание функции, которая принимает другую функцию (функция высокого порядка в функциональном жаргоне). Эта функция создаст объект bmesh, запустит заданную ему функцию, а затем отправит данные BMesh в меш и освободит его. Но это было своего рода ограничение. Вы можете запустить только одну вещь, или вам придется сгруппировать их в другую функцию. Другая идея заключалась в том, чтобы создать класс-обертку, но мне все равно пришлось бы вручную его освобождать. Кроме того, я не большой поклонник классов.
Открытие файла дало мне лучшую идею: контекстные менеджеры.
Менеджеры контекста - это специальные объекты в Python, которые позволяют вам определять специальные контексты времени выполнения. Обычно для управления ресурсами. Вы создаете / открываете ресурсы, когда заходите в контекст, используете его, и когда вы покидаете ресурс, он автоматически закрывается / освобождается.
Рассмотрим openфункцию / менеджер контекста, например.

    # This snippet...
    with open('some/file', 'r') as the_file:
        do_something(the_file)

    # ...is doing this behind the scenes
    the_file = open('some/file', 'r')
    do_something(the_file)
    close(the_file)
Немного сахара для мирской задачи открытия файлов. Но он автоматически заботится о закрытии файлов (чтобы мы не пропускали дескрипторы) и аккуратно группирует весь код на новом уровне отступов. А как же БМеш? Объект BMesh не слишком отличается от ресурса Python. Мы создаем его, передаем и, наконец, отправляем в меш и освобождаем (или позволяем, если выпадают из области видимости).
Менеджеры контекста определяются как классы с тремя специфическими методами:
__init____enter__и __exit__Вы можете добавить свой, конечно, но это минимум, необходимый. Давайте посмотрим на первую реализацию.

class Bmesh_from_obj():

    def __init__(self, obj):
        # Register parameters as class properties
        self.obj = obj

    def __enter__(self):
        # Create and return bmesh object
        self.bm = bmesh.new()
        self.bm.from_object(self.obj)

        return self.bm

    def __exit__(self, *args):
        # Clean up: send bmesh to mesh and free()
        self.bm.to_mesh(obj.data)
        self.bm.free()
При этом мы уже можем использовать bmesh_objв качестве менеджера контекста (в объектном режиме). Обратите внимание, что возвращать значение в __enter__необязательно, вы можете создать  with блок, который не привязывает переменную. Также, если мы нажмем исключение __init__ или __enter__никогда не попадем внутрь блока with. С другой стороны, если мы идем внутрь, __exit__метод всегда вызывается, и мы можем использовать его для обработки исключений, подобных этому:

    def __exit__(self, exc_type, exc_value, exc_traceback):
        self.bm.to_mesh(obj.data)
        self.bm.free()

        if exc_type:
            print('Oh no')
            print(f'{exc_value}. Trace: {exc_traceback}')
Я оставлю обработку исключений для вас, хотя. Каждый проект индивидуален, и вы можете захотеть обработать исключения раньше или позже (позволяя им подняться). Кроме исключений, мы можем сделать еще две вещи, чтобы улучшить это: упростить код и поддерживать режим редактирования. Давайте сначала упростим.
Мы можем использовать декоратор для функции вместо написания целого класса. Сначала нам нужно импортировать @contextmanagerиз contextlib. Модуль contextlib полностью посвящен менеджерам контекста, и его документация - это то место, куда можно обратиться, если вы хотите углубиться в них. Как бы тогда выглядела эта функция?

from contextlib import contextmanager


@contextmanager
def bmesh_from_obj(obj):
    # Create bmesh object and yield it

    yield bm
        # Code inside the with block gets executed here

    # Clean up (we're leaving the with block)
Заметьте , что мы получали Bm вместо того , чтобы вернуться , как мы делали в в  __enter__методе раньше. Декоратору нужна функция для возврата одного итератора генератора значений, который затем становится целью оператора with (часть «as»). Вот и все для инициализации. Код внутри withблока выполняется сразу после yield, и как только мы достигаем конца, он выполняет остальную часть функции.
Давайте добавим параметр mode и уточним это:

@contextmanager
def bmesh_from_obj(obj, mode):
    """Context manager to auto-manage BMesh."""

    if mode == 'EDIT_MESH':
        bm = bmesh.from_edit_mesh(obj.data)
    else:
        bm = bmesh.new()
        bm.from_mesh(obj.data)

    yield bm

    # Send to mesh and clean up
    bm.normal_update()

    if mode == 'EDIT_MESH':
        bmesh.update_edit_mesh(obj.data)
    else:
        bm.to_mesh(obj.data)

    bm.free()
Теперь мы можем передать переменную режима из контекста и использовать BMesh в любом режиме. Вот пример из руководства по экструзии.

with bmesh_from_obj(obj, bpy.context.mode) as bm:
    # Get geometry to extrude
    bm.faces.ensure_lookup_table()
    faces = [bm.faces[0]]  # For a plane
    faces = [bm.faces[5]]  # For the top face of the cube# Extrude

    extruded = bmesh.ops.extrude_face_region(bm, geom=faces)

    # Move extruded geometry
    translate_verts = [v for v in extruded['geom'] if isinstance(v, BMVert)]

    up = Vector((0, 0, 1))
    bmesh.ops.translate(bm, vec=up, verts=translate_verts)


    bmesh.ops.delete(bm, geom=faces, context=DEL_FACES)

    # Remove doubles
    bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.001)
Что если мы хотим отправить данные BMesh новому объекту? Мы можем добавить небольшую служебную функцию, чтобы создать новый пустой объект и передать его менеджеру контекста.

def new_obj(obj_name):
    """Add a new object to the scene."""

    # Make new object when leaving context manager
    mesh = bpy.data.meshes.new(obj_name)
    obj = bpy.data.objects.new(obj_name, mesh)
    bpy.context.scene.objects.link(obj)

    return obj


# (Later)
with bmesh_obj(new_obj('myobj'), bpy.context.mode):
    pass

Финальный код

Я сделал пример немного более интересным для окончательного примера. Попробуйте! Этот код создает простую скрученную форму. Он начинается с плоскости, несколько раз выдавливает ее, вращает и масштабирует некоторые края. Как вы можете видеть в  extrudeфункции, мы также можем абстрагировать некоторые общие операции bmesh в функции, чтобы повторно использовать код или сделать его более выразительным. Все может пойти в withблоке.

import bpy
import bmesh
from bpy.types import Object

from bmesh.types import BMFace, BMVert

from mathutils import Vector
from contextlib import contextmanager

from math import radians
from mathutils import Matrix

# ------------------------------------------------------------------------------
# BMesh Context manager
# ------------------------------------------------------------------------------

@contextmanager
def bmesh_from_obj(obj, mode):
    """Context manager to auto-manage bmesh regardless of mode."""

    if mode == 'EDIT_MESH':
        bm = bmesh.from_edit_mesh(obj.data)
    else:
        bm = bmesh.new()
        bm.from_mesh(obj.data)

    yield bm

    bm.normal_update()

    if mode == 'EDIT_MESH':
        bmesh.update_edit_mesh(obj.data)
    else:
        bm.to_mesh(obj.data)

    bm.free()



# ------------------------------------------------------------------------------
# Bmesh / Utils functions
# ------------------------------------------------------------------------------
def new_obj(obj_name):
    """Add a new object to the scene."""

    # Make new object when leaving context manager
    mesh = bpy.data.meshes.new(obj_name)
    obj = bpy.data.objects.new(obj_name, mesh)
    bpy.context.scene.objects.link(obj)

    return obj


def extrude(bm, faces, direction, remove=True):
    """Extrude a set of faces in a direction"""

    # Extrude
    extruded = bmesh.ops.extrude_face_region(bm, geom=faces)
    translate_verts = [v for v in extruded['geom'] if isinstance(v, BMVert)]

    bmesh.ops.translate(bm, vec=Vector(direction), verts=translate_verts)

    if remove:
        bmesh.ops.delete(bm, geom=faces, context=5)

    return [f for f in extruded['geom'] if isinstance(f, BMFace)]


# ------------------------------------------------------------------------------
# Testing
# ------------------------------------------------------------------------------

# Add a new (empty) object
obj = new_obj('Bmesh test')

# We could also pass an existing object
# obj = bpy.context.object

with bmesh_from_obj(obj, bpy.context.mode) as bm:

    # A grid with segments of 1 is a plane
    bmesh.ops.create_grid(bm, x_segments=1, y_segments=1, size=1)

    # We need to call this since we are accesing faces by index
    bm.faces.ensure_lookup_table()

    faces = [bm.faces[0]]
    new_faces = extrude(bm, faces, (0, 0, 1), False)

    # Keep a copy of these verts for later
    middle_verts = new_faces[0].verts[:]

    # Another extrusion because why not
    top_faces = extrude(bm, new_faces, (0, 0, 3))

    # Give it a thin waist
    bmesh.ops.scale(bm, vec=Vector((0.25, 0.25, 1)), verts=middle_verts)

    # Add a small rotation at the top
    bmesh.ops.rotate(bm, verts=top_faces[0].verts, cent=(0, 0, 0),
                     matrix=Matrix.Rotation(radians(15), 3, 'Z'))

    # Unnecesary but for demo purposes...
    bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.001)






Посмотрите мою серию уроков по созданию сетки, если вы ищете больше идей.
Если вы заинтересованы в получении дополнительной информации о менеджерах контекста, я рекомендую обратиться к документации модуля contextlib. Также проверьте оригинальное предложение PEP для этой функции: PEP343 .

Комментариев нет:

Отправить комментарий