> 백엔드 개발 > 파이썬 튜토리얼 > Python 사전 표현식에 대한 심층 분석

Python 사전 표현식에 대한 심층 분석

零到壹度
풀어 주다: 2018-04-02 11:30:08
원래의
4115명이 탐색했습니다.

이 기사에서는 Python의 사전 표현식 작동에 대해 자세히 설명합니다. 내용이 꽤 좋아서 도움이 필요한 친구들에게 도움이 되기를 바랍니다.

Python 사전 표현식 퍼즐

다음과 같은 모호한 Python 사전 표현식을 탐색하여 Python 인터프리터 내부에서 실제로 무슨 일이 일어나고 있는지 알아봅시다.

<code class="hljs language-python" style="font-size:.85em;font-family:Consolas, Inconsolata, Courier, monospace;margin:0px .15em;white-space:pre;overflow:auto;border-width:1px;border-style:solid;border-color:rgb(204,204,204);background:rgb(29,31,33);color:rgb(197,200,198);padding:.5em;display:block !important;"><span class="hljs-comment" style="color:rgb(150,152,150);"># 一个python谜题:</span><br><span class="hljs-comment" style="color:rgb(150,152,150);"># 这个表达式计算以后会得到什么结果?</span><br><br><span class="hljs-prompt">>>> </span>{<span class="hljs-keyword" style="color:rgb(178,148,187);">True</span>: <span class="hljs-string" style="color:rgb(181,189,104);">'yes'</span>, <span class="hljs-number" style="color:rgb(222,147,95);">1</span>: <span class="hljs-string" style="color:rgb(181,189,104);">'no'</span>, <span class="hljs-number" style="color:rgb(222,147,95);">1.0</span>: <span class="hljs-string" style="color:rgb(181,189,104);">'maybe'</span>}</code>
로그인 후 복사

때로는 코드 한 줄이라도 아주 깊이 있는 코드 예제를 접하게 되지만, 충분히 생각한다면 프로그래밍 언어에 대해 많은 것을 배울 수 있습니다.

이러한 코드 조각은 Zen kōan과 같습니다. 영적 수행 중 학생의 진행 상황에 대해 질문하고 테스트하는 데 사용되는 질문 또는 설명입니다. (번역자 주: Zen kōan 아마도 영적인 수행의 한 방법일 것입니다. 자세한 내용은 위키피디아를 참조하세요.)

우리가 논의할 코드 조각은 그러한 예 중 하나입니다. 언뜻 보면 단순한 사전적 표현처럼 보일 수도 있지만, 곰곰이 생각해보면, cpython 인터프리터를 통해 정신을 확장하는 연습을 하게 됩니다.

저는 이 짧은 코드 줄에서 영감을 얻었고, 한 번 참석한 Python 컨퍼런스에서 이를 연설의 내용으로 사용하여 연설을 시작했습니다.

이것은 또한 내 Python 메일링 리스트 회원들 사이에 긍정적인 의사소통에 영감을 주었습니다.

더 이상 고민하지 말고 여기에 이 ​​코드 조각이 있습니다. 아래 사전적 표현과 그 표현이 무엇으로 평가될지 잠시 생각해 보세요:

>>> {True: 'yes', 1: 'no', 1.0: 'maybe'}
로그인 후 복사

여기서, 여러분이 생각하는 동안 잠시 기다리겠습니다…

5…
4…
3…
2…
1 ...
좋아요, 준비됐나요?

이것은 cpython 인터프리터 대화형 인터페이스에서 위의 사전 표현식을 평가할 때의 결과입니다.

<code class="hljs language-python" style="font-size:.85em;font-family:Consolas, Inconsolata, Courier, monospace;margin:0px .15em;white-space:pre;overflow:auto;border-width:1px;border-style:solid;border-color:rgb(204,204,204);background:rgb(29,31,33);color:rgb(197,200,198);padding:.5em;display:block !important;"><span class="hljs-prompt">>>> </span>{<span class="hljs-keyword" style="color:rgb(178,148,187);">True</span>: <span class="hljs-string" style="color:rgb(181,189,104);">'yes'</span>, <span class="hljs-number" style="color:rgb(222,147,95);">1</span>: <span class="hljs-string" style="color:rgb(181,189,104);">'no'</span>, <span class="hljs-number" style="color:rgb(222,147,95);">1.0</span>: <span class="hljs-string" style="color:rgb(181,189,104);">'maybe'</span>}<br>{<span class="hljs-keyword" style="color:rgb(178,148,187);">True</span>: <span class="hljs-string" style="color:rgb(181,189,104);">'maybe'</span>}</code>
로그인 후 복사
로그인 후 복사
로그인 후 복사

인정합니다. 이 결과를 처음 봤을 때 놀랐습니다. 그러나 일어나는 과정을 단계별로 살펴보면 모든 것이 이해가 됩니다.

그러면 우리가 왜 이런 – 예상치 못한 – 결과를 얻었는지 생각해 봅시다.

이 하위 사전은 어디에서 왔나요?

파이썬은 사전 표현식을 처리할 때 먼저 새로운 빈 사전 객체를 구성한 다음 사전 표현식에 지정된 순서대로 키와 값을 할당합니다.

그래서 우리가 그것을 분해하면 사전 표현은 다음 명령문 순서와 동일합니다:

<code class="hljs language-python" style="font-size:.85em;font-family:Consolas, Inconsolata, Courier, monospace;margin:0px .15em;white-space:pre;overflow:auto;border-width:1px;border-style:solid;border-color:rgb(204,204,204);background:rgb(29,31,33);color:rgb(197,200,198);padding:.5em;display:block !important;"><span class="hljs-prompt">>>> </span>xs = dict()<br><span class="hljs-prompt">>>> </span>xs[<span class="hljs-keyword" style="color:rgb(178,148,187);">True</span>] = <span class="hljs-string" style="color:rgb(181,189,104);">'yes'</span><br><span class="hljs-prompt">>>> </span>xs[<span class="hljs-number" style="color:rgb(222,147,95);">1</span>] = <span class="hljs-string" style="color:rgb(181,189,104);">'no'</span><br><span class="hljs-prompt">>>> </span>xs[<span class="hljs-number" style="color:rgb(222,147,95);">1.0</span>] = <span class="hljs-string" style="color:rgb(181,189,104);">'maybe'</span></code>
로그인 후 복사

이상하게도 Python은 이 예제에 사용된 모든 사전 키를 동일한 것으로 간주합니다

<code class="hljs language-python" style="font-size:.85em;font-family:Consolas, Inconsolata, Courier, monospace;margin:0px .15em;white-space:pre;overflow:auto;border-width:1px;border-style:solid;border-color:rgb(204,204,204);background:rgb(29,31,33);color:rgb(197,200,198);padding:.5em;display:block !important;"><span class="hljs-prompt">>>> </span><span class="hljs-keyword" style="color:rgb(178,148,187);">True</span> == <span class="hljs-number" style="color:rgb(222,147,95);">1</span> == <span class="hljs-number" style="color:rgb(222,147,95);">1.0</span><br><span class="hljs-keyword" style="color:rgb(178,148,187);">True</span></code>
로그인 후 복사

좋습니다. 하지만 여기서 기다리세요. 1.0 == 1을 받아들일 수 있다고 확신합니다. 그런데 왜 True가 1과 같다고 간주됩니까? 이 사전적 표현은 처음 봤을 때 정말 당황스러웠습니다.

파이썬 문서를 좀 살펴본 후, 파이썬이 bool을 int 유형의 하위 클래스로 취급한다는 것을 발견했습니다. 이것은 Python 2 및 Python 3의 스니펫입니다.

“부울 유형은 정수 유형의 하위 유형이며 부울 값은 거의 모든 상황에서 각각 0 및 1 값처럼 동작합니다. 즉, 문자열로 변환하면 'False' 또는 'True'라는 문자열이 각각 반환됩니다."

"Boolean 유형은 정수 유형의 하위 유형이며 거의 모든 항목에서 값 0과 1처럼 동작합니다. contexts에서 예외는 문자열로 변환될 때 문자열 "False" 또는 "True"가 각각 반환된다는 것입니다.

예, 이는 프로그래밍할 때 Python에서 부울 값을 목록이나 튜플로 사용할 수 있다는 의미입니다. :

<code class="hljs language-python" style="font-size:.85em;font-family:Consolas, Inconsolata, Courier, monospace;margin:0px .15em;white-space:pre;overflow:auto;border-width:1px;border-style:solid;border-color:rgb(204,204,204);background:rgb(29,31,33);color:rgb(197,200,198);padding:.5em;display:block !important;"><span class="hljs-prompt">>>> </span>[<span class="hljs-string" style="color:rgb(181,189,104);">'no'</span>, <span class="hljs-string" style="color:rgb(181,189,104);">'yes'</span>][<span class="hljs-keyword" style="color:rgb(178,148,187);">True</span>]<br><span class="hljs-string" style="color:rgb(181,189,104);">'yes'</span></code>
로그인 후 복사

但为了代码的可读性起见,您不应该类似这样的来使用布尔变量。(也请建议你的同事别这样做)

Anyway,让我们回过来看我们的字典表达式。

就python而言,True,1和1.0都表示相同的字典键。当解释器计算字典表达式时,它会重复覆盖键True的值。这就解释了为什么最终产生的字典只包含一个键。

在我们继续之前,让我们再回顾一下原始字典表达式:

<code class="hljs language-python" style="font-size:.85em;font-family:Consolas, Inconsolata, Courier, monospace;margin:0px .15em;white-space:pre;overflow:auto;border-width:1px;border-style:solid;border-color:rgb(204,204,204);background:rgb(29,31,33);color:rgb(197,200,198);padding:.5em;display:block !important;"><span class="hljs-prompt">>>> </span>{<span class="hljs-keyword" style="color:rgb(178,148,187);">True</span>: <span class="hljs-string" style="color:rgb(181,189,104);">'yes'</span>, <span class="hljs-number" style="color:rgb(222,147,95);">1</span>: <span class="hljs-string" style="color:rgb(181,189,104);">'no'</span>, <span class="hljs-number" style="color:rgb(222,147,95);">1.0</span>: <span class="hljs-string" style="color:rgb(181,189,104);">'maybe'</span>}<br>{<span class="hljs-keyword" style="color:rgb(178,148,187);">True</span>: <span class="hljs-string" style="color:rgb(181,189,104);">'maybe'</span>}</code>
로그인 후 복사
로그인 후 복사
로그인 후 복사

这里为什么最终得到的结果是以True作为键呢?由于重复的赋值,最后不应该是把键也改为1.0了?

经过对cpython解释器源代码的一些模式研究,我知道了,当一个新的值与字典的键关联的时候,python的字典不会更新键对象本身:

<code class="hljs language-python" style="font-size:.85em;font-family:Consolas, Inconsolata, Courier, monospace;margin:0px .15em;white-space:pre;overflow:auto;border-width:1px;border-style:solid;border-color:rgb(204,204,204);background:rgb(29,31,33);color:rgb(197,200,198);padding:.5em;display:block !important;"><span class="hljs-prompt">>>> </span>ys = {<span class="hljs-number" style="color:rgb(222,147,95);">1.0</span>: <span class="hljs-string" style="color:rgb(181,189,104);">'no'</span>}<br><span class="hljs-prompt">>>> </span>ys[<span class="hljs-keyword" style="color:rgb(178,148,187);">True</span>] = <span class="hljs-string" style="color:rgb(181,189,104);">'yes'</span><br><span class="hljs-prompt">>>> </span>ys<br>{<span class="hljs-number" style="color:rgb(222,147,95);">1.0</span>: <span class="hljs-string" style="color:rgb(181,189,104);">'yes'</span>}</code>
로그인 후 복사

当然这个作为性能优化来说是有意义的 —- 如果键被认为是相同的,那么为什么要花时间更新原来的?

在最开始的例子中,你也可以看到最初的True对象一直都没有被替换。因此,字典的字符串表示仍然打印为以True为键(而不是1或1.0)。

就目前我们所知而言,似乎看起来像是,结果中字典的值一直被覆盖,只是因为他们的键比较后相等。然而,事实上,这个结果也不单单是由__eq__比较后相等就得出的。

等等,那哈希值呢?

python字典类型是由一个哈希表数据结构存储的。当我第一次看到这个令人惊讶的字典表达式时,我的直觉是这个结果与散列冲突有关。

哈希表中键的存储是根据每个键的哈希值的不同,包含在不同的“buckets”中。哈希值是指根据每个字典的键生成的一个固定长度的数字串,用来标识每个不同的键。

这可以实现快速查找。在哈希表中搜索键对应的哈希数字串会快很多,而不是将完整的键对象与所有其他键进行比较,来检查互异性。

然而,通常计算哈希值的方式并不完美。并且,实际上会出现不同的两个或更多个键会生成相同的哈希值,并且它们最后会出现在相同的哈希表中。

如果两个键具有相同的哈希值,那就称为哈希冲突(hash collision),这是在哈希表插入和查找元素时需要处理的特殊情况。

基于这个结论,哈希值与我们从字典表达中得到的令人意外的结果有很大关系。所以让我们来看看键的哈希值是否也在这里起作用。

我定义了这样一个类来作为我们的测试工具:

<code class="hljs language-python" style="font-size:.85em;font-family:Consolas, Inconsolata, Courier, monospace;margin:0px .15em;white-space:pre;overflow:auto;border-width:1px;border-style:solid;border-color:rgb(204,204,204);background:rgb(29,31,33);color:rgb(197,200,198);padding:.5em;display:block !important;"><span class="hljs-class"><span class="hljs-keyword" style="color:rgb(178,148,187);">class</span> <span class="hljs-title" style="color:rgb(150,152,150);">AlwaysEquals</span>:</span><br>     <span class="hljs-function" style="color:rgb(129,162,190);"><span class="hljs-keyword" style="color:rgb(178,148,187);">def</span> <span class="hljs-title" style="color:rgb(150,152,150);">__eq__</span><span class="hljs-params" style="color:rgb(222,147,95);">(self, other)</span>:</span><br>         <span class="hljs-keyword" style="color:rgb(178,148,187);">return</span> <span class="hljs-keyword" style="color:rgb(178,148,187);">True</span><br><br>     <span class="hljs-function" style="color:rgb(129,162,190);"><span class="hljs-keyword" style="color:rgb(178,148,187);">def</span> <span class="hljs-title" style="color:rgb(150,152,150);">__hash__</span><span class="hljs-params" style="color:rgb(222,147,95);">(self)</span>:</span><br>         <span class="hljs-keyword" style="color:rgb(178,148,187);">return</span> id(self)</code>
로그인 후 복사

这个类有两个特别之处。

第一,因为它的__eq__魔术方法(译者注:双下划线开头双下划线结尾的是一些Python的“魔术”对象)总是返回true,所以这个类的所有实例和其他任何对象都会恒等:

<code class="hljs language-python" style="font-size:.85em;font-family:Consolas, Inconsolata, Courier, monospace;margin:0px .15em;white-space:pre;overflow:auto;border-width:1px;border-style:solid;border-color:rgb(204,204,204);background:rgb(29,31,33);color:rgb(197,200,198);padding:.5em;display:block !important;"><span class="hljs-prompt">>>> </span>AlwaysEquals() == AlwaysEquals()<br><span class="hljs-keyword" style="color:rgb(178,148,187);">True</span><br><span class="hljs-prompt">>>> </span>AlwaysEquals() == <span class="hljs-number" style="color:rgb(222,147,95);">42</span><br><span class="hljs-keyword" style="color:rgb(178,148,187);">True</span><br><span class="hljs-prompt">>>> </span>AlwaysEquals() == <span class="hljs-string" style="color:rgb(181,189,104);">'waaat?'</span><br><span class="hljs-keyword" style="color:rgb(178,148,187);">True</span><br>第二,每个Alwaysequals实例也将返回由内置函数id()生成的唯一哈希值值:<br><br><span class="hljs-prompt">>>> </span>objects = [AlwaysEquals(),<br>               AlwaysEquals(),<br>               AlwaysEquals()]<br><span class="hljs-prompt">>>> </span>[hash(obj) <span class="hljs-keyword" style="color:rgb(178,148,187);">for</span> obj <span class="hljs-keyword" style="color:rgb(178,148,187);">in</span> objects]<br>[<span class="hljs-number" style="color:rgb(222,147,95);">4574298968</span>, <span class="hljs-number" style="color:rgb(222,147,95);">4574287912</span>, <span class="hljs-number" style="color:rgb(222,147,95);">4574287072</span>]</code>
로그인 후 복사

在CPython中,id()函数返回的是一个对象在内存中的地址,并且是确定唯一的。

通过这个类,我们现在可以创建看上去与其他任何对象相同的对象,但它们都具有不同的哈希值。我们就可以通过这个来测试字典的键是否是基于它们的相等性比较结果来覆盖。

正如你所看到的,下面的一个例子中的键不会被覆盖,即使它们总是相等的:

<code class="hljs language-python" style="font-size:.85em;font-family:Consolas, Inconsolata, Courier, monospace;margin:0px .15em;white-space:pre;overflow:auto;border-width:1px;border-style:solid;border-color:rgb(204,204,204);background:rgb(29,31,33);color:rgb(197,200,198);padding:.5em;display:block !important;"><span class="hljs-prompt">>>> </span>{AlwaysEquals(): <span class="hljs-string" style="color:rgb(181,189,104);">'yes'</span>, AlwaysEquals(): <span class="hljs-string" style="color:rgb(181,189,104);">'no'</span>}<br>{ <AlwaysEquals object at <span class="hljs-number" style="color:rgb(222,147,95);">0x110a3c588</span>>: <span class="hljs-string" style="color:rgb(181,189,104);">'yes'</span>,<br>  <AlwaysEquals object at <span class="hljs-number" style="color:rgb(222,147,95);">0x110a3cf98</span>>: <span class="hljs-string" style="color:rgb(181,189,104);">'no'</span> }</code>
로그인 후 복사

下面,我们可以换个思路,如果返回相同的哈希值是不是就会让键被覆盖呢?

<code class="hljs language-python" style="font-size:.85em;font-family:Consolas, Inconsolata, Courier, monospace;margin:0px .15em;white-space:pre;overflow:auto;border-width:1px;border-style:solid;border-color:rgb(204,204,204);background:rgb(29,31,33);color:rgb(197,200,198);padding:.5em;display:block !important;"><span class="hljs-class"><span class="hljs-keyword" style="color:rgb(178,148,187);">class</span> <span class="hljs-title" style="color:rgb(150,152,150);">SameHash</span>:</span><br>    <span class="hljs-function" style="color:rgb(129,162,190);"><span class="hljs-keyword" style="color:rgb(178,148,187);">def</span> <span class="hljs-title" style="color:rgb(150,152,150);">__hash__</span><span class="hljs-params" style="color:rgb(222,147,95);">(self)</span>:</span><br>        <span class="hljs-keyword" style="color:rgb(178,148,187);">return</span> <span class="hljs-number" style="color:rgb(222,147,95);">1</span></code>
로그인 후 복사

这个SameHash类的实例将相互比较一定不相等,但它们会拥有相同的哈希值1:

<code class="hljs language-python" style="font-size:.85em;font-family:Consolas, Inconsolata, Courier, monospace;margin:0px .15em;white-space:pre;overflow:auto;border-width:1px;border-style:solid;border-color:rgb(204,204,204);background:rgb(29,31,33);color:rgb(197,200,198);padding:.5em;display:block !important;"><span class="hljs-prompt">>>> </span>a = SameHash()<br><span class="hljs-prompt">>>> </span>b = SameHash()<br><span class="hljs-prompt">>>> </span>a == b<br><span class="hljs-keyword" style="color:rgb(178,148,187);">False</span><br><span class="hljs-prompt">>>> </span>hash(a), hash(b)<br>(<span class="hljs-number" style="color:rgb(222,147,95);">1</span>, <span class="hljs-number" style="color:rgb(222,147,95);">1</span>)</code>
로그인 후 복사

一起来看看python的字典在我们试图使用SameHash类的实例作为字典键时的结果:

<code class="hljs language-python" style="font-size:.85em;font-family:Consolas, Inconsolata, Courier, monospace;margin:0px .15em;white-space:pre;overflow:auto;border-width:1px;border-style:solid;border-color:rgb(204,204,204);background:rgb(29,31,33);color:rgb(197,200,198);padding:.5em;display:block !important;"><span class="hljs-prompt">>>> </span>{a: <span class="hljs-string" style="color:rgb(181,189,104);">'a'</span>, b: <span class="hljs-string" style="color:rgb(181,189,104);">'b'</span>}<br>{ <SameHash instance at <span class="hljs-number" style="color:rgb(222,147,95);">0x7f7159020cb0</span>>: <span class="hljs-string" style="color:rgb(181,189,104);">'a'</span>,<br>  <SameHash instance at <span class="hljs-number" style="color:rgb(222,147,95);">0x7f7159020cf8</span>>: <span class="hljs-string" style="color:rgb(181,189,104);">'b'</span> }</code>
로그인 후 복사

如本例所示,“键被覆盖”的结果也并不是单独由哈希冲突引起的。

Umm..好吧,可以得到什么结论呢?

Python字典中的键 是否相同(只有相同才会覆盖)取决于两个条件:

1、两者的值是否相等(比较__eq__方法)。
2、比较两者的哈希值是否相同(比较__hash__方法)。

让我们试着总结一下我们研究的结果:

{true:'yes',1:'no',1.0:'maybe'}
로그인 후 복사

字典表达式计算结果为 {true:'maybe'},是因为键true,1和1.0都是相等的,并且它们都有相同的哈希值:

<code class="hljs language-python" style="font-size:.85em;font-family:Consolas, Inconsolata, Courier, monospace;margin:0px .15em;white-space:pre;overflow:auto;border-width:1px;border-style:solid;border-color:rgb(204,204,204);background:rgb(29,31,33);color:rgb(197,200,198);padding:.5em;display:block !important;"><span class="hljs-prompt">>>> </span><span class="hljs-keyword" style="color:rgb(178,148,187);">True</span> == <span class="hljs-number" style="color:rgb(222,147,95);">1</span> == <span class="hljs-number" style="color:rgb(222,147,95);">1.0</span><br><span class="hljs-keyword" style="color:rgb(178,148,187);">True</span><br><span class="hljs-prompt">>>> </span>(hash(<span class="hljs-keyword" style="color:rgb(178,148,187);">True</span>), hash(<span class="hljs-number" style="color:rgb(222,147,95);">1</span>), hash(<span class="hljs-number" style="color:rgb(222,147,95);">1.0</span>))<br>(<span class="hljs-number" style="color:rgb(222,147,95);">1</span>, <span class="hljs-number" style="color:rgb(222,147,95);">1</span>, <span class="hljs-number" style="color:rgb(222,147,95);">1</span>)<br>`</code>
로그인 후 복사

也许并不那么令人惊讶,这就是我们为何得到这个结果作为字典的最终结果的原因:

<code class="hljs language-python" style="font-size:.85em;font-family:Consolas, Inconsolata, Courier, monospace;margin:0px .15em;white-space:pre;overflow:auto;border-width:1px;border-style:solid;border-color:rgb(204,204,204);background:rgb(29,31,33);color:rgb(197,200,198);padding:.5em;display:block !important;"><span class="hljs-prompt">>>> </span>{<span class="hljs-keyword" style="color:rgb(178,148,187);">True</span>: <span class="hljs-string" style="color:rgb(181,189,104);">'yes'</span>, <span class="hljs-number" style="color:rgb(222,147,95);">1</span>: <span class="hljs-string" style="color:rgb(181,189,104);">'no'</span>, <span class="hljs-number" style="color:rgb(222,147,95);">1.0</span>: <span class="hljs-string" style="color:rgb(181,189,104);">'maybe'</span>}<br>{<span class="hljs-keyword" style="color:rgb(178,148,187);">True</span>: <span class="hljs-string" style="color:rgb(181,189,104);">'maybe'</span>}</code>
로그인 후 복사
로그인 후 복사
로그인 후 복사

我们在这里涉及了很多方面内容,而这个特殊的python技巧起初可能有点令人难以置信 —- 所以我一开始就把它比作是Zen kōan。

如果很难理解本文中的内容,请尝试在Python交互环境中逐个去检验一下代码示例。你会收获一些关于python深处知识。

推荐阅读:

위 내용은 Python 사전 표현식에 대한 심층 분석의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

관련 라벨:
원천:php.cn
본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
인기 튜토리얼
더>
최신 다운로드
더>
웹 효과
웹사이트 소스 코드
웹사이트 자료
프론트엔드 템플릿