Gradio Agents 和 MCP 黑客马拉松

获奖者
Gradio logo
  1. 其他教程
  2. 封装布局

封装布局

引言

Gradio 具有 功能,可轻松布局应用程序。要使用此功能,您需要堆叠或嵌套布局组件并创建它们的层次结构。对于小型项目来说,这不难实现和维护,但项目变得更复杂后,这种组件层次结构将变得难以维护和重用。

在本指南中,我们将探讨如何封装布局类,以创建更易于维护和阅读的应用程序,同时不牺牲灵活性。

示例

我们将参考这个 Huggingface Space 示例中的实现。

实现

封装实用程序包含两个重要的类。第一个是 `LayoutBase` 类,另一个是 `Application` 类。

为简洁起见,我们将查看它们的 `render` 和 `attach_event` 函数。您可以从示例代码中查看完整实现。

那么,我们从 `LayoutBase` 类开始。

LayoutBase 类

  1. 渲染函数

    我们来看看 `LayoutBase` 类中的 `render` 函数

# other LayoutBase implementations

def render(self) -> None:
    with self.main_layout:
        for renderable in self.renderables:
            renderable.render()

    self.main_layout.render()

这最初可能有点令人困惑,但如果您考虑默认实现,就会很容易理解。让我们来看一个示例

在默认实现中,我们正在做的是:

with Row():
    left_textbox = Textbox(value="left_textbox")
    right_textbox = Textbox(value="right_textbox")

现在,请注意 Textbox 变量。这些变量的 `render` 参数默认为 true。因此,当我们使用 `with` 语法创建这些变量时,它们会在 `with` 语法下调用 `render` 函数。

我们知道 `render` 函数是在构造函数中通过 `gradio.blocks.Block` 类的实现调用的

class Block:
    # constructor parameters are omitted for brevity
    def __init__(self, ...):
        # other assign functions 

        if render:
            self.render()

所以我们的实现是这样的

# self.main_layout -> Row()
with self.main_layout:
    left_textbox.render()
    right_textbox.render()

这意味着通过在 `with` 语法下调用组件的渲染函数,我们实际上是在模拟默认实现。

现在我们考虑两个嵌套的 `with` 语句,看看外部的 `with` 是如何工作的。为此,我们用 `Tab` 组件来扩展我们的示例

with Tab():
    with Row():
        first_textbox = Textbox(value="first_textbox")
        second_textbox = Textbox(value="second_textbox")

这次请注意 Row 和 Tab 组件。我们已经在上面创建了 Textbox 变量,并使用 `with` 语法将它们添加到 Row 中。现在我们需要将 Row 组件添加到 Tab 组件中。您可以看到 Row 组件是使用默认参数创建的,因此其渲染参数为 true,这就是为什么渲染函数将在 Tab 组件的 `with` 语法下执行的原因。

为了模仿这种实现,我们需要在 `main_layout` 变量的 `with` 语法之后调用 `main_layout` 变量的 `render` 函数。

所以实现看起来像这样

with tab_main_layout:
    with row_main_layout:
        first_textbox.render()
        second_textbox.render()

    row_main_layout.render()

tab_main_layout.render()

默认实现和我们的实现是相同的,但是我们是自己调用 `render` 函数。所以这需要一些额外的工作。

现在,我们来看看 `attach_event` 函数。

  1. 附加事件函数

    该函数未实现,因为它特定于类,因此每个类都必须实现其 `attach_event` 函数。

    # other LayoutBase implementations

    def attach_event(self, block_dict: Dict[str, Block]) -> None:
        raise NotImplementedError

查看 `Application` 类的 `attach_event` 函数中的 `block_dict` 变量。

Application 类

  1. 渲染函数
    # other Application implementations

    def _render(self):
        with self.app:
            for child in self.children:
                child.render()

        self.app.render()

从 `LayoutBase` 类的 `render` 函数的解释中,我们可以理解 `child.render` 部分。

那么我们来看看底部,为什么我们要调用 `app` 变量的 `render` 函数呢?调用这个函数很重要,因为如果我们查看 `gradio.blocks.Blocks` 类中的实现,可以看到它正在将组件和事件函数添加到根组件中。换句话说,它正在创建和构建 Gradio 应用程序。

  1. 附加事件函数

    让我们看看如何将事件附加到组件

    # other Application implementations

    def _attach_event(self):
        block_dict: Dict[str, Block] = {}

        for child in self.children:
            block_dict.update(child.global_children_dict)

        with self.app:
            for child in self.children:
                try:
                    child.attach_event(block_dict=block_dict)
                except NotImplementedError:
                    print(f"{child.name}'s attach_event is not implemented")

您可以从示例代码中看到为什么 `LayoutBase` 类中使用了 `global_children_list`。有了它,应用程序中的所有组件都被收集到一个字典中,因此组件可以通过其名称访问所有组件。

这里再次使用 `with` 语法来将事件附加到组件。如果我们查看 `gradio.blocks.Blocks` 类中的 `__exit__` 函数,可以看到它正在调用 `attach_load_events` 函数,该函数用于为组件设置事件触发器。因此,我们必须使用 `with` 语法来触发 `__exit__` 函数。

当然,我们可以不使用 `with` 语法来调用 `attach_load_events`,但该函数需要一个 `Context.root_block`,它是在 `__enter__` 函数中设置的。因此,我们在这里使用了 `with` 语法,而不是自己调用该函数。

总结

在本指南中,我们探讨了:

  • 如何封装布局
  • 组件如何渲染
  • 如何使用封装的布局类构建应用程序

由于本指南中使用的类仅用于演示目的,它们可能尚未完全优化或模块化。但那样会使指南篇幅过长!

我希望本指南能帮助您对布局类有新的认识,并为您提供如何根据自身需求使用它们的想法。在此查看我们示例的完整实现