有没有办法将一些变量捆绑在 LaTeX 中类似 OOP 的对象中?

有没有办法将一些变量捆绑在 LaTeX 中类似 OOP 的对象中?

我非常希望一个对象封装一堆数据,然后将该对象传递给一个函数。

例如,一个图形可能有一个文件的 URL、一个标题和一个宽度。在 Python 中,可以这样写:

class Figure():
    url = 'path/to/file.pdf'
    caption = 'This is a figure.'
    width = 0.1  # fraction of \linewidth
    label = 'a_nice_label'
figure = Figure()

现在,我意识到使用 LaTeX 完全实现 OOP 是徒劳的,但至少将一些数据捆绑在一起并将其传递给函数会非常好。例如,一个采用图形对象的简单函数如下所示:

\newcommand{plotfigure}[1]
    figure = (somehow read from #1)
    \begin{\figure}[hbtp]
      \begin{center}
        \includegraphics[width=figure.width\linewidth]{figure.url}
        \caption{
            figure.caption
            }
        \label{figure.label}
      \end{center}
    \end{figure}

(显然,这种访问属性的方式对于 LaTeX 来说意义不大,但你明白我的意思)。

LaTeX 中是否有任何类型的实现允许基本的变量捆绑?(理想情况下它甚至允许方法和继承,但捆绑已经非常有用了。)

答案1

PGF 有一个面向对象的引擎,根据手册,它支持

类、方法、构造函数、属性、对象、对象标识、继承和重载。

它看起来是这样的:

\pgfooclass{stamp}{
    % This is the class stamp
    \method stamp() { % The constructor
    }
    \method apply(#1,#2) { % Causes the stamp to be shown at coordinate (#1,#2)
        % Draw the stamp:
        \node [rotate=20,font=\huge] at (#1,#2) {Passed};
    }
}
\pgfoonew \mystamp=new stamp()
\begin{tikzpicture}
    \mystamp.apply(1,2)
    \mystamp.apply(3,4)
\end{tikzpicture}

答案2

schtandard 解决方案的一个更冒险的替代方案是制作分隔宏。这将*-ish* 遵循语法class.property。我们只需定义一个分隔宏,该宏后面需要一个点:

\documentclass{article}

\usepackage{graphicx}

\makeatletter
\def\newobj#1{%
  \ifcsname #1\endcsname
    \errmessage{Cannot define #1. Control sequence already defined.}
  \fi%
  \expandafter\def\csname obj@#1\endcsname{}%
  \expandafter\def\csname #1\endcsname.##1 {%
    \csname obj@#1@##1\endcsname%
  }%
}
\def\addtoobj#1#2#3{%
  \ifcsname obj@#1\endcsname\else
    \errmessage{Object #1 undefined. Use \string\newobj\{#1\}}
  \fi%
  \expandafter\edef\csname obj@#1@#2\endcsname{#3}%
}
\makeatother

\newcommand{\plotfigure}[1]{%
  \begin{figure}
    \centering
    \edef\Fw{[width=}
    \expandafter\expandafter\expandafter\includegraphics\expandafter\Fw#1.width ]{#1.url }
    \caption{#1.caption }
    \label{#1.label }
  \end{figure}
}

\begin{document}
\pagenumbering{gobble}

\newobj{Figure}
\addtoobj{Figure}{url}{example-image}
\addtoobj{Figure}{caption}{This is a figure.}
\addtoobj{Figure}{width}{0.5\linewidth}
\addtoobj{Figure}{label}{a_nice_label}

\plotfigure{\Figure}

See figure \ref{\Figure.label }

\end{document}

通过这种方法,您可以创建一个“类”,\newobj{<name>}并使用向其添加属性\addtoobj{<name>}{<property>}{<value>}

例如,当您使用 时\newobj{Figure},会创建一个宏\Figure.#1␣是空格)。此宏扩展为\obj@Figure@#1。然后,当您调用 时\addtoobj{Figure}{url}{example-image},宏\obj@Figure@url被定义为example-image。然后,对 的调用\Figure.url␣(注意空格!)将扩展为example-image

请注意,无论何时使用,\Figure宏后面都必须跟一个.,然后是(有效的)属性名称,然后是空格。空格是命令的一部分!


至于您的\plotfigure宏,我们可以使用\Figure.#1␣而不会出现很多问题,除了 的可选参数\includegraphics。在调用 之前必须扩展可选参数\includegraphics,因此\expandafter这里需要一个技巧。除此之外,一切\plotfigure{\Figure}都很有效 :)

答案3

虽然这通常可能不是最好的做事方式,但您可以通过创建适当的宏名来模拟结构的命名空间:

\documentclass{article}

\usepackage{mwe}    % <-- only for example image
\usepackage{xparse} % <-- only for \setfigurestruct

\makeatletter
    \def\setstructfield#1#2#3{\expandafter\def\csname @struct@#1@field@#2\endcsname{#3}}
    \def\getstructfield#1#2{\csname @struct@#1@field@#2\endcsname}
    \def\ifstructhasfield#1#2{\ifcsname @struct@#1@field@#2\endcsname \expandafter\@firstoftwo\else \expandafter\@secondoftwo\fi}
\makeatother

% \setfigurestruct{name}{path}[width as fraction of \linewidth]{caption}[label]
\NewDocumentCommand\setfigurestruct{m O{.8} m m o}{%
    \setstructfield{#1}{width}{#2\linewidth}%
    \setstructfield{#1}{path}{#3}%
    \setstructfield{#1}{caption}{#4}%
    \IfValueT{#5}{%
        \setstructfield{#1}{label}{#5}%
    }%
}

% \plotfigure{figure struct name}
\newcommand*\plotfigure[1]{%
    \begin{figure}%
        \centering
        \includegraphics[width=\getstructfield{#1}{width}]{\getstructfield{#1}{path}}%
        \caption{\getstructfield{#1}{caption}}%
        \ifstructhasfield{#1}{label}{\label{\getstructfield{#1}{label}}}{}%
    \end{figure}%
}

\begin{document}

\setfigurestruct{testfig}{example-image-a}{A test image.}[fig:test]

Look at figure~\ref{fig:test}, please.

\plotfigure{testfig}

\end{document}

上述代码的输出

这些领域不是真的在这里存储为一个结构,但是界面的行为就像它们一样。


更强大的替代方案是使用pgfkeys。如果您愿意,可以编写一些包装函数来模拟结构的“感觉”(类似于我上面所做的),尽管我并不真正明白其中的意义。

答案4

问题是:你从哪里得到这些数据?

准备好的数字

如果你已经准备好了图形,并想在文档中自定义它们的渲染,最好的方法就是复制粘贴(抱歉)代码,然后调整参数。我觉得这样做更好,因为

  • 简单。将“宽度”分离在一个地方并在另一个地方使用该值的目的是什么?这看起来过于复杂了。

  • 灵活性。如果您想要为一个图形指定宽度,还想指定另一个参数,该怎么办?函数适用于重复的事​​情,但不适合独特的事情。

正如我读到pgfmanual的 3.1.5b 版本第 99.6 节所述,属性说:

属性只能在方法内部设置和读取,无法使用对象句柄进行设置和读取。从传统的面向对象编程的角度来看,属性始终是私有的。如果您希望读取或修改属性,则需要定义 getter 和 setter 方法。

读写属性时不使用方法调用所用的“点符号”。这主要是出于效率方面的考虑。相反,我们使用一组特殊的宏,这些宏只能在方法内部使用。

因此它看起来有点复杂(正如手册所说,TeX 不是以 OOP 为理念创建的)。如果我们回到上面 @marczellm 的示例,我会注意到以下几行:

\begin{tikzpicture}
    \mystamp.apply(1,2)
    \mystamp.apply(3,4)
\end{tikzpicture}

对我来说,问题是没有属性的名称(如mystamp.apply(x=1,y=2))。对于两个坐标,人们可能会猜测,但如果你有一个图形的一堆属性,那么记住所有这些参数的顺序将非常困难,如果你决定更改它们(例如添加新参数),将会出现许多错误。我可以从 @schtandard 的解决方案中看到同样的问题

\setfigurestruct{testfig}{example-image-a}{A test image.}[fig:test]

- 尽管您可以猜出不同论点的含义,但乍一看这并不明显。

@Phelype Oleinik 的代码在这方面看起来很棒:

\newobj{Figure}
\addtoobj{Figure}{url}{example-image}
\addtoobj{Figure}{caption}{This is a figure.}
\addtoobj{Figure}{width}{0.5\linewidth}
\addtoobj{Figure}{label}{a_nice_label}

\plotfigure{\Figure}

See figure \ref{\Figure.label }

但问题仍然存在:这 6 行代码是否比 7 行简单(并且如我所说,更灵活)地包含一个图形更好?

\begin{\figure}[hbtp]
  \begin{center}
    \includegraphics[width=0.5\linewidth]{example-image}
    \caption{This is a figure.}
    \label{a_nice_label}
  \end{center}
\end{figure}

See figure \ref{a_nice_label}.

只有当您计划多次插入一个数字时,OOP 代码才会更好。否则我会使用其他解决方案:

  • 如果你想重复使用一个人物的属性(可能你不需要复制它的所有属性?),创建一个变量并在图中和其他地方使用它,
  • 如果你想重复使用所有图形中的属性(比如,宽度),创建一个变量并将其用于所有图形(例如,如果您稍后决定调整所有图形的宽度)。

生成的图形

我所说的生成是指图形,其属性(图上的数据点、轴标签等)是由程序生成的。我进行数据分析并创建了大量图形。由于生成的质量,我使用 TeX 渲染它们。但是,TeX 不是一种用于通用编程语言,因此我必须将计算参数传递给其引擎。

首先,我尝试在 TeX 运行时输入参数,但发现参数数量越多,输入就越复杂。然后我渲染模板,并发现这很棒。

在 Web 编程非常发达的领域模板用于使用程序中的特定数据呈现 HTML 页面(例如Django 教程)。

LaTeX(或任何其他文本数据)也可以实现同样的效果。

我从中复制了一个例子教程关于数据分析框架 Lena(由我开发):

% histogram_1d.tex
\documentclass{standalone}
%%% include tikz, pgfplots, etc.
\begin{tikzpicture}
\begin{axis}[]
\addplot [
    const plot,
]
table [col sep=comma, header=false] {\VAR{ output.filepath }};
\end{axis}
\end{tikzpicture}
%%%

这里要呈现的变量是\VAR{ output.filepath }output.filepath您通过上下文提供的内容,它是一个简单的 Python 字典)。字典是一个对象,在jinja模板中,它的键(和子键)可以作为属性访问。

当然,还有更复杂的例子:

\BLOCK{ set var = variable if variable else '' }
\begin{tikzpicture}
\begin{axis}[
    \BLOCK{ if var.latex_name }
        xlabel = { $\VAR{ var.latex_name }$
        \BLOCK{ if var.unit }
            [$\mathrm{\VAR{ var.unit }}$]
        \BLOCK{ endif }
        },
    \BLOCK{ endif }
]
...

语法\BLOCK\VAR看起来有点冗长。对于 html 模板来说,它更容易阅读,但对于 LaTeX 来说,它必须进行更改,以免与 LaTeX 代码混淆。但是,我很少编辑和查看我的模板:一旦它们足够通用,您就不需要添加其他内容了。

尽管新构造使 LaTeX 代码变得复杂,但它看起来仍然非常清晰。上下文属性读起来很清楚 ( var.unit),并且不会相互混淆(位置参数可能会混淆)。

令人惊讶的是,我不需要为我的一维直方图创建任何其他模板。即使在更复杂的情况下,当我想向图中添加某些内容(例如一条线)时,我也会编辑该单个模板:我可以有条件地仅为特定图启用某些渲染(通过上下文定义,我可以轻松控制)。对于二维图,我使用单独的模板(因为渲染代码有些不同)。

有关模板渲染的更多详细信息,请参阅Jinja 2 文档或者Lena 教程

尽管如此,尽管我在程序中生成了许多图表,但我仍然手动完成所有其他工作。我选择要包含在文档中的图表。我自己写每个标题​​。我还会在需要时在最终文档中调整它们的属性。对于自动生成的报告,情况会有所不同,但我仍然会编写大量代码来自定义它,使其更好,对读者更有趣(即使是偶尔的读者)。

相关内容