블로그 이미지
Peter Note
Web & LLM FullStacker, Application Architecter, KnowHow Dispenser and Bike Rider

Publication

Category

Recent Post

LangChain 의 Document Loaders 에 대한 개념 및 패키지, API를 들여다 보자. 

 

개념

LLM은 대규모 언어모델로 언어가 텍스트 또는 음성으로 인풋을 받다. 이때 텍스트는 사용자가 직접 입력한 내용일 수도 있고, 첨부한 문서일 수도 있고, 텍스트가 있는 소스에 접근할 수도 있다.

 

- 다양한 문서 포멧 로딩

- 내용을 축출하기

- 내용을 원하는 단위로 잘 쪼개기

 

문서 종류 와 위치 (170개 가량)

- 파일: text, csv, pdf, markdown, docx, excel, json, ...

- 위치: folder, database, url, ...

- 서비스: youtube, slack, gitbook, github, git, discord, docusaurus, figma, ...

출처: deeplearning.ai

- LangChain의 document loaders 개념 설명

- LangChain의 document loaders 가이드

 

패키지

document_loaders 구현체들 https://python.langchain.com/v0.2/docs/integrations/document_loaders/

 

Document loaders | 🦜️🔗 LangChain

If you'd like to write your own document loader, see this how-to.

python.langchain.com

 

langchain 패키지 소스를 보면 document_loaders 패키지가 존재하고, 대부분 langchain_community 패키지 소스를 re-exporting 하고 있다. 

langchain 패키지

 

패키지 및 로더 호출 방법

- 기본 langchain.document_loaders 에서 xxxLoader 를 임포트하여 사용한다. 

//----------
// 예-1)
from langchain.document_loaders import PyPDFLoader
// 또는 from langchain_community.document_loaders import PyPDFLoader

loader = PyPDFLoader(FILE_PATH)
pages = loader.load()

//----------
// 예-2)
from langchain.document_loaders import WebBaseLoader

loader = WebBaseLoader("https://github.com/langchain-ai/langchain/blob/master/README.md")
docs = loader.load()
print(docs[0].page_content[:500])

 

API 

API 설명을 살펴보자.

 

- langchain_community 패키지밑으로 구현체가 존재하기에 필요한 경우 다양한 Custom Loader를 만들 수 있다. 

- 추상 클래스 (Abstract Class)은 langchain_core 추상클래스인 BaseLoader를 상속받아 구현한다. 

class BaseLoader(ABC):
  def load(self) -> List[Document]:
      """Load data into Document objects."""
      return list(self.lazy_load())
  def lazy_load(self) -> Iterator[Document]:
      """Custom Loader에서 구현해야 함."""

 

- BaseLoader <- BasePDFLoader <- PyPDFLoader 를 구현한 클래스를 보면 lazy_load 메소드를 구현하고 있다. 

class PyPDFLoader(BasePDFLoader):
    def lazy_load(
        self,
    ) -> Iterator[Document]:
        """Lazy load given path as pages."""
        if self.web_path:
            blob = Blob.from_data(open(self.file_path, "rb").read(), path=self.web_path)  # type: ignore[attr-defined]
        else:
            blob = Blob.from_path(self.file_path)


 - 반환값은 langchain_core의 Document 리스트이고, BaseMedia <- Document 를 통해 항시 기본적으로 접근 가능한 애트리뷰트가 있다. 

    - metadata : 문서 속성 정보

    - page_content : 문서의 내용

class BaseMedia(Serializable):
  id: Optional[str] = None
  metadata: dict = Field(default_factory=dict)
  
class Document(BaseMedia):
  page_content: str
  type: Literal["Document"] = "Document"

 

- GenericLoader 는 다양한 문서를 로드할 수 있다. API 설명의 예제를 참조한다.

 

 

Custom Document Loader 만들기

- LangChain custom loader 가이드를 우선 참조하자.

- 구현 순서

   - BaseLoader를 상속받는다.

   - lazy_load 메서드를 구현한다.

   - alazy_load는 옵션으로 lazy_load로 위임해도 된다. 

- HWPLoader를 구현한 TeddyNote 소스를 참조하자.

 

 

<참조>

API: https://api.python.langchain.com/en/latest/langchain_api_reference.html

 

Source: https://github.com/langchain-ai/langchain/tree/master

 

GitHub - langchain-ai/langchain: 🦜🔗 Build context-aware reasoning applications

🦜🔗 Build context-aware reasoning applications. Contribute to langchain-ai/langchain development by creating an account on GitHub.

github.com

 

Guide: https://python.langchain.com/v0.2/docs/concepts/

 

Conceptual guide | 🦜️🔗 LangChain

This section contains introductions to key parts of LangChain.

python.langchain.com

HWPLoader: https://github.com/teddylee777/langchain-teddynote/blob/main/langchain_teddynote/document_loaders/hwp.py

 

langchain-teddynote/langchain_teddynote/document_loaders/hwp.py at main · teddylee777/langchain-teddynote

LangChain 을 더 쉽게 구현하기 위한 유틸 함수, 클래스를 만들어서 패키지로 배포하였습니다. - teddylee777/langchain-teddynote

github.com

- LangChain KR: https://wikidocs.net/253706

 

01. 도큐먼트(Document) 의 구조

.custom { background-color: #008d8d; color: white; padding: 0.25em 0.…

wikidocs.net

 

posted by Peter Note
2024. 8. 4. 15:16 [LLM FullStacker]/Python

1) 클래스내에서 사용하는 경우

__getattr__ 메서드는 Python에서 객체의 속성에 접근할 때 호출되는 특별 메서드입니다. 객체의 속성을 조회할 때 해당 속성이 존재하지 않으면 __getattr__ 메서드가 호출됩니다. 이는 동적 속성 접근을 가능하게 하며, 클래스에서 존재하지 않는 속성에 대한 요청을 처리할 수 있습니다.

__getattr__의 의미와 사용법

  • 의미: __getattr__은 객체에서 속성에 접근할 때 해당 속성이 존재하지 않는 경우 호출됩니다. 이는 객체의 속성을 동적으로 생성하거나 계산된 속성을 반환하는 데 유용합니다.
  • 사용법: __getattr__ 메서드는 클래스 내부에 정의되며, 단 하나의 매개변수를 가집니다. 이 매개변수는 존재하지 않는 속성의 이름을 나타내는 문자열입니다. 이 메서드는 존재하지 않는 속성에 대해 반환할 값을 반환해야 합니다.

예시 코드

아래는 __getattr__ 메서드를 사용하여 동적으로 속성을 생성하는 예시입니다:

from typing import Any

class DynamicAttributes:
    def __init__(self):
        self.existing_attribute = "This attribute exists"

    def __getattr__(self, name: str) -> Any:
        """
        This method is called when an attribute is not found.

        Args:
            name (str): The name of the attribute that is being accessed.

        Returns:
            Any: The value to return for the non-existing attribute.
        """
        return f"The attribute '{name}' does not exist, but here is a default value."

# Example usage:
obj = DynamicAttributes()
print(obj.existing_attribute)  # Output: This attribute exists
print(obj.non_existing_attribute)  # Output: The attribute 'non_existing_attribute' does not exist, but here is a default value.

주요 포인트

  1. 속성이 존재하는 경우: __getattr__는 호출되지 않습니다. 예를 들어, obj.existing_attribute에 접근하면 __getattr__가 호출되지 않고, existing_attribute의 실제 값이 반환됩니다.
  2. 속성이 존재하지 않는 경우: __getattr__가 호출됩니다. 예를 들어, obj.non_existing_attribute에 접근하면, __getattr__가 호출되어 해당 속성의 이름을 인수로 받아 반환값을 제공합니다.
  3. 반환값: __getattr__ 메서드는 호출된 속성에 대해 반환할 값을 반환해야 합니다. 이 값은 문자열, 숫자, 객체 등 어떤 유형도 될 수 있습니다.

요약

__getattr__ 메서드는 Python 객체에서 존재하지 않는 속성에 대한 접근을 처리하기 위한 메서드입니다. 이 메서드를 사용하면 동적 속성 접근, 계산된 속성 반환, 기본값 제공 등의 작업을 수행할 수 있습니다. 이를 통해 객체의 유연성을 높이고, 동적으로 속성을 관리할 수 있습니다.


2) init.py 파일 내에서 사용

__init__.py 파일에 __getattr__ 메서드를 정의하는 것은 패키지 수준에서의 동적 속성 접근을 가능하게 합니다. 이 메서드를 사용하면 모듈 또는 패키지에서 존재하지 않는 속성에 접근할 때 동적 동작을 정의할 수 있습니다.

패키지 수준에서의 __getattr__ 사용 예시

Python 3.7부터 패키지의 __init__.py 파일에 __getattr__를 정의할 수 있습니다. 이를 통해 패키지에서 존재하지 않는 속성에 접근할 때 원하는 동작을 수행할 수 있습니다.

예시: 동적 속성 접근

다음은 mypackage라는 패키지에서 __getattr__를 정의하는 예시입니다:

mypackage/
    __init__.py
    module1.py
    module2.py

__init__.py 파일에서 __getattr__를 정의합니다:

# mypackage/__init__.py

def __getattr__(name):
    if name == "special_attribute":
        return "This is a special attribute"
    raise AttributeError(f"module {__name__} has no attribute {name}")

이제 mypackage 패키지에서 special_attribute에 접근할 수 있습니다:

import mypackage

print(mypackage.special_attribute)  # Output: This is a special attribute
print(mypackage.non_existing_attribute)  # Raises AttributeError

주요 포인트

  1. 동적 속성 접근: 패키지 수준에서 존재하지 않는 속성에 접근할 때 동적 동작을 정의할 수 있습니다. 예를 들어, 특정 속성 이름에 대해 동적으로 값을 반환하거나, 필요한 경우 예외를 발생시킬 수 있습니다.
  2. 패키지 초기화: 패키지를 초기화할 때 __getattr__를 사용하면, 패키지에 포함되지 않은 속성에 대한 접근을 처리할 수 있습니다. 이는 패키지의 유연성을 높이고, 사용자에게 특정 속성 접근을 허용할 수 있습니다.
  3. Python 3.7 이상: 패키지의 __init__.py 파일에 __getattr__를 정의하는 기능은 Python 3.7에서 도입되었습니다. 따라서 이 기능을 사용하려면 Python 3.7 이상 버전이 필요합니다.

예제 코드 설명

  • __getattr__ 메서드는 name 매개변수를 받아, 접근하려는 속성의 이름을 나타냅니다.
  • special_attribute에 접근할 때는 "This is a special attribute"를 반환합니다.
  • special_attribute가 아닌 다른 속성에 접근하려고 하면 AttributeError를 발생시킵니다.

요약

  • __init__.py 파일에 __getattr__를 정의하면 패키지 수준에서 동적 속성 접근을 처리할 수 있습니다.
  • 이는 패키지의 유연성을 높이고, 사용자에게 특정 속성에 대한 동적 접근을 허용하는 데 유용합니다.
  • Python 3.7 이상에서만 사용 가능합니다.
posted by Peter Note
2024. 8. 4. 15:12 [LLM FullStacker]/Python

Google 스타일 docstring의 전체 포맷을 설명하기 위해, 모든 주요 섹션을 포함한 예시를 제공합니다. 이 예시는 함수와 클래스에 대한 포맷을 모두 다룹니다.

함수의 예시

def embed_query(text, model='default', verbose=False):
    """
    Embed query text using a specified model.

    This function takes a text string and converts it into an embedding
    using the specified model. It supports different models for embedding
    and can provide verbose output.

    Args:
        text (str): The text to embed. This should be a plain string without any special formatting.
        model (str, optional): The model to use for embedding. Defaults to 'default'.
        verbose (bool, optional): If True, the function will print detailed information during execution. Defaults to False.

    Returns:
        list: A list representing the text embedding.

    Raises:
        ValueError: If the text is empty or the model is not supported.
        RuntimeError: If the embedding process fails.

    Examples:
        >>> embed_query("Hello, world!")
        [0.1, 0.3, 0.5, ...]
        >>> embed_query("Hello, world!", model='advanced')
        [0.2, 0.4, 0.6, ...]

    Notes:
        The embedding process can take a significant amount of time
        depending on the length of the text and the complexity of the model.
    """
    if not text:
        raise ValueError("Text cannot be empty.")
    if model not in ['default', 'advanced']:
        raise ValueError("Unsupported model.")
    # Implementation of embedding process...
    embedding = [0.1, 0.3, 0.5]  # Dummy embedding
    if verbose:
        print(f"Embedding for '{text}' generated using model '{model}'.")
    return embedding

클래스의 예시

class EmbeddingModel:
    """
    A model for generating embeddings from text.

    This class provides methods to embed text using various models. It can
    handle different types of text inputs and supports multiple embedding
    techniques.

    Attributes:
        model_name (str): The name of the model.
        version (str): The version of the model.
        is_trained (bool): Indicates whether the model has been trained.

    Methods:
        train(data):
            Trains the model using the provided data.
        embed(text):
            Embeds the given text and returns the embedding.
        save(path):
            Saves the model to the specified path.
    """

    def __init__(self, model_name, version):
        """
        Initializes the EmbeddingModel with a name and version.

        Args:
            model_name (str): The name of the model.
            version (str): The version of the model.
        """
        self.model_name = model_name
        self.version = version
        self.is_trained = False

    def train(self, data):
        """
        Trains the model using the provided data.

        This method takes a dataset and trains the embedding model.
        It updates the is_trained attribute to True upon successful training.

        Args:
            data (list): A list of training data samples.

        Returns:
            None

        Raises:
            ValueError: If the data is empty or not in the expected format.

        Examples:
            >>> model = EmbeddingModel('text_model', '1.0')
            >>> model.train(['sample1', 'sample2'])
        """
        if not data:
            raise ValueError("Training data cannot be empty.")
        # Implementation of training process...
        self.is_trained = True

    def embed(self, text):
        """
        Embeds the given text and returns the embedding.

        Args:
            text (str): The text to embed.

        Returns:
            list: A list representing the text embedding.

        Raises:
            RuntimeError: If the model has not been trained.

        Examples:
            >>> model = EmbeddingModel('text_model', '1.0')
            >>> model.train(['sample1', 'sample2'])
            >>> model.embed('Hello, world!')
            [0.1, 0.3, 0.5, ...]
        """
        if not self.is_trained:
            raise RuntimeError("Model must be trained before embedding.")
        # Implementation of embedding process...
        return [0.1, 0.3, 0.5]  # Dummy embedding

    def save(self, path):
        """
        Saves the model to the specified path.

        Args:
            path (str): The file path to save the model to.

        Returns:
            None

        Examples:
            >>> model = EmbeddingModel('text_model', '1.0')
            >>> model.save('/path/to/save/model')
        """
        # Implementation of save process...
        pass

주요 구성 요소

  1. 요약 설명(Summary):
    • 클래스나 함수의 첫 번째 줄에서 기능을 간략하게 설명합니다.
    • 간결하고 명확하게 작성하며, 마침표로 끝냅니다.
  2. 확장 설명(Extended Description):
    • 요약 설명 이후 빈 줄을 두고, 기능에 대한 상세 설명을 작성합니다.
    • 설명이 길어질 경우 여러 문단으로 나눌 수 있습니다.
  3. Args (인수):
    • Args: 섹션에서 함수나 메소드의 매개변수를 설명합니다.
    • 각 매개변수에 대해 이름, 유형, 설명을 포함합니다.
    • 선택적 매개변수는 (optional)로 표시합니다.
  4. Attributes (속성):
    • 클래스의 속성을 설명합니다.
    • 각 속성의 이름과 설명을 포함합니다.
  5. Methods (메소드):
    • 클래스의 메소드를 설명합니다.
    • 각 메소드의 이름과 기능을 간략하게 설명합니다.
  6. Returns (반환값):
    • 함수나 메소드의 반환값을 설명합니다.
    • 반환값의 유형과 설명을 포함합니다.
    • 반환값이 없으면 생략할 수 있습니다.
  7. Raises (예외):
    • 함수나 메소드가 발생시킬 수 있는 예외를 설명합니다.
    • 예외의 유형과 설명을 포함합니다.
  8. Examples (예제):
    • 함수나 메소드의 사용 예제를 포함합니다.
    • 코드 블록을 사용하여 예제를 보여줍니다.
  9. Notes (참고):
    • 함수나 메소드에 대한 추가적인 참고 사항을 작성합니다.
    • 필요한 경우에만 포함합니다.

이러한 구성 요소들을 사용하면, Google 스타일의 docstring을 통해 코드의 문서화를 일관성 있게 작성할 수 있습니다.

posted by Peter Note

LangChain은 LLM 애플리케이션을 구축하기 위한 개발 프레임워크로써 다양한 컴포넌트를 제공한다. 프러덕션 애플리케이션 개발시 RAG(Retrieval Augmented Generation)를 기반으로 할 때 LangChain 컴포넌트를 통해 일관된 코드 인터페이스를 유지할 수 있다.

LangChain Components

Prompts

  • Prompt Templates
  • Output Parsers: 5+ implementations
    • Retry/fixing logic
  • Example Selectors: 5+ implementations

Models

  • LLM's: 20+ integrations
  • Chat Models
  • Text Embedding Models: 10+ integrations

Indexes

  • Document Loaders: 50+ implementations
  • Text Splitters: 10+ implementations
  • Vector stores: 10+ integrations
  • Retrievers: 5+ integrations/implementations

Chains

  • Can be used as building blocks for other chains
  • More application specific cahins: 20+ different types

Agents

  • Agent Types: 5+ types
    • Algorithms for getting LLMs to use tools
  • Agent Tookkits: 10+ implementations
    • Agents armed with specific tools for a specific application

LangChain API

각 컴포넌트는 구현된 패키지와 API랑 함께 볼 필요가 있다.

langchain

  • document_loaders
  • text_splitter
  • embeddings
  • retrievers
  • prompts
  • chat_models
  • output_parsers
  • chains
  • agents

등의 패키지에서 langchain_community, langchain_core 패키지의 모듈을 re-export 하고 있다. 중요 패키지는 langchain.* 으로 import 할 수 있다.

  • text_splitter.py : text-splitters를 re-export하고 있다.
  • text_splitter 별도 패키지

langchain-core

langchain-community


RAG Step

RAG는 크게 2단계로 볼 수 있다. 사전에 프라이빗 정보를 로딩->쪼개기->임베딩벡터->저장하기 단계를 거쳐서 준비를 한다. 이때 LangChain의 Indexes 영역의 컴포넌트를 사용한다. 다음으로 사용자가 질의를 하게되면 프라이빗 정보를 기반으로 증강검색->프롬프트생성->LLM응답->응답처리 등의 과정을 거쳐, 유의미한 응답을 생성한다. 이때 LangChain의 Prompts, Models, Chains, Agents 영역의 컴포넌트를 사용한다.

 

출처: DeepLearning.ai

1 단계

  • Document Loader
    • Structured/Unstructured 미디어를 로딩한다.
      • 문서, 이미지, 동영상, 음성
  • Splitting
  • Embedding
  • Vector Storing

2단계

  • Retrieval
  • Prompting
  • LLM
  • Output Parsing
posted by Peter Note
2024. 7. 22. 22:16 [LLM FullStacker]/Python

튜플 (Tuple)

튜플(tuple)은 파이썬의 내장 데이터 타입 중 하나로, 여러 값을 하나의 순서 있는 집합으로 저장할 수 있는 자료형입니다. 리스트(list)와 유사하지만, 몇 가지 중요한 차이점이 있습니다.

주요 특징

  1. 변경 불가능(Immutable): 한 번 생성된 튜플의 원소는 변경할 수 없습니다. 따라서 튜플의 내용을 수정, 추가, 삭제하는 작업은 불가능합니다.
  2. 순서가 있음(Ordered): 튜플은 순서가 있는 데이터 타입으로, 각 원소는 인덱스를 통해 접근할 수 있습니다.
  3. 중복 허용: 튜플 내에 동일한 값을 여러 번 포함할 수 있습니다.

생성 방법

튜플은 소괄호 ()를 사용하여 생성하며, 각 요소는 쉼표(,)로 구분합니다.

# 튜플 생성 예시
empty_tuple = ()
single_element_tuple = (42,)  # 하나의 요소를 가진 튜플 생성 시 쉼표 필요
multiple_elements_tuple = (1, 2, 3)

튜플은 괄호 없이도 생성할 수 있습니다. 예를 들어:

another_tuple = 1, 2, 3

접근 방법

튜플의 각 요소에 접근하려면 리스트와 마찬가지로 인덱스를 사용합니다.

example_tuple = (10, 20, 30)
print(example_tuple[0])  # 출력: 10
print(example_tuple[1])  # 출력: 20
print(example_tuple[-1])  # 출력: 30 (마지막 요소)

활용 예

튜플은 주로 다음과 같은 상황에서 사용됩니다:

  • 변경 불가능한 데이터 구조가 필요할 때
  • 여러 값을 하나로 묶어 함수의 인수나 반환값으로 사용하고자 할 때
  • 딕셔너리의 키로 사용하고자 할 때 (리스트는 키로 사용할 수 없음)

기타 메서드

튜플은 몇 가지 유용한 메서드를 제공합니다:

example_tuple = (1, 2, 3, 2, 1)

# 특정 값의 개수 세기
print(example_tuple.count(2))  # 출력: 2

# 특정 값의 인덱스 찾기
print(example_tuple.index(3))  # 출력: 2

이와 같이, 튜플은 파이썬에서 매우 유용한 데이터 타입으로, 특정 상황에서 매우 효율적으로 사용될 수 있습니다.


리스트 (List)

리스트는 순서가 있는 변경 가능한(mutable) 데이터 타입으로, 다양한 유형의 값을 포함할 수 있습니다. 리스트는 대괄호 []를 사용하여 생성하며, 각 요소는 쉼표로 구분합니다.

주요 특징

  1. 변경 가능(Mutable): 리스트의 요소를 추가, 수정, 삭제할 수 있습니다.
  2. 순서가 있음(Ordered): 리스트는 순서를 유지하며, 인덱스를 통해 각 요소에 접근할 수 있습니다.
  3. 중복 허용: 리스트는 동일한 값을 여러 번 포함할 수 있습니다.

생성 방법 및 사용 예

# 리스트 생성
empty_list = []
numbers = [1, 2, 3, 4, 5]
mixed_list = [1, "hello", 3.14, True]

# 리스트 요소 접근
print(numbers[0])  # 출력: 1
print(numbers[-1])  # 출력: 5

# 리스트 수정
numbers[0] = 10
print(numbers)  # 출력: [10, 2, 3, 4, 5]

# 리스트 요소 추가
numbers.append(6)
print(numbers)  # 출력: [10, 2, 3, 4, 5, 6]

# 리스트 요소 삭제
numbers.remove(10)
print(numbers)  # 출력: [2, 3, 4, 5, 6]

 

딕셔너리 (Dictionary)

딕셔너리는 키(key)와 값(value) 쌍을 저장하는 변경 가능한(mutable) 데이터 타입입니다. 딕셔너리는 중괄호 {}를 사용하여 생성하며, 각 키-값 쌍은 콜론 :으로 구분합니다.

주요 특징

  1. 변경 가능(Mutable): 딕셔너리의 키-값 쌍을 추가, 수정, 삭제할 수 있습니다.
  2. 순서가 없음(Unordered) (파이썬 3.7 이후로는 삽입 순서 유지)
  3. 고유 키(Unique Key): 딕셔너리의 각 키는 고유해야 하며, 중복된 키를 가질 수 없습니다.

생성 방법 및 사용 예

# 딕셔너리 생성
empty_dict = {}
person = {"name": "Alice", "age": 25, "city": "New York"}

# 딕셔너리 값 접근
print(person["name"])  # 출력: Alice
print(person.get("age"))  # 출력: 25

# 딕셔너리 값 수정
person["age"] = 30
print(person)  # 출력: {'name': 'Alice', 'age': 30, 'city': 'New York'}

# 딕셔너리 키-값 쌍 추가
person["job"] = "Engineer"
print(person)  # 출력: {'name': 'Alice', 'age': 30, 'city': 'New York', 'job': 'Engineer'}

# 딕셔너리 키-값 쌍 삭제
del person["city"]
print(person)  # 출력: {'name': 'Alice', 'age': 30, 'job': 'Engineer'}

리스트와 딕셔너리 비교

특징 리스트 (List) 딕셔너리 (Dictionary)
변경 가능성 변경 가능 변경 가능
순서 순서가 있음 파이썬 3.7 이후로는 삽입 순서 유지, 그 전에는 순서가 없음
중복 요소 중복 허용 키 중복 불가, 값 중복 허용
접근 방법 인덱스를 통해 요소에 접근 (list[0]) 키를 통해 값에 접근 (dict["key"])

 

리스트와 딕셔너리는 각각의 장점이 있으며, 특정 상황에 맞게 선택하여 사용할 수 있습니다. 데이터를 순차적으로 처리해야 할 때는 리스트를, 키-값 쌍으로 데이터를 관리해야 할 때는 딕셔너리를 사용하면 됩니다.

posted by Peter Note