본문 바로가기
TIL _Today I Learned/2024.10

[DAY 70] SQLAlchemy ORM

by gamdong2 2024. 10. 29.
[천재교육] 프로젝트 기반 빅데이터 서비스 개발자 양성 과정 9기
학습일 : 2024.10.29

📕 학습 목록

  • 기능별 파일로 모듈을 분리하여 저장 → 필요 시 해당 모듈을 호출
  • DB에 가상 데이터 저장
  • 시각화
  • 파이썬 심화

 

📗 기억할 내용

[데이터 파이프라인]

  • 데이터의 생성에서 가공, 데이터베이스 전송, 시각화까지
    • 가상 데이터 유입: 처음 단계에서 가상 데이터(Fake Data)가 생성되어 데이터베이스로 유입됨
    • PostgreSQL에 데이터 저장: 생성된 가상 데이터는 PostgreSQL 데이터베이스에 저장
    • 데이터 가공: Python을 사용하여 데이터 가공 작업을 수행합니다. 이 단계에서 필요한 데이터 변환이나 전처리가 이루어짐
    • MySQL로 데이터 이행: 가공된 데이터는 MySQL 데이터베이스로 전송되어 저장됨
    • 데이터 읽기: MySQL에 저장된 데이터는 Streamlit을 통해 읽어옴
    • 시각화: 마지막으로 Streamlit에서 데이터를 시각화하여 사용자에게 데이터를 이해하기 쉽게 제공함

 
 

📘코드 실습

* 실습 내용
1. postgresql DB에 Fake 유저 정보 저장 (Batch Data)
2. Pipeline 모듈을 통한 데이터 이행 및 데이터 가공
3. POSTGRESQL DB에 가공된 datamart 테이블 저장
4. Streamlit 라이브러리를 통한 POSTGRESQL 테이블 데이터 시각화
5. n초 마다 Batch성 이행을 통해 시각화 테이블 업데이트 내용 확인

[모듈 생성 & 호출]  

1. 모듈을 파일 형태로 나누어 관리

1) 디렉토리 구조 및 역할

[각 파일의 역할]
1. 최상위 파일 및 폴더
  - .env: 데이터베이스나 API 키와 같은 민감한 환경 변수를 저장
  - app.py: 애플리케이션의 진입점 파일로, 서버를 실행하거나 특정 기능을 실행
  - controller.py: ETL 파이프라인을 관리하는 컨트롤러 파일로, 데이터를 추출, 변환, 로드하고 임시 파일을 정리
  - .ipynb: Jupyter Notebook 파일로, 분석, 테스트 또는 실험 코드가 포함되어 있음
  - main.py: 애플리케이션의 메인 실행 파일로, 초기 설정과 함께 전체 워크플로우를 관리
  - settings.py: 애플리케이션의 설정 파일로, 데이터베이스 연결 정보, 파일 경로와 같은 주요 설정 저장
2. dataset: 데이터셋 파일을 저장하는 폴더로, 분석이나 모델 학습을 위한 데이터를 보관
3. db: 데이터베이스 관련 파일을 모아놓은 폴더
  - connector.py: 데이터베이스 연결 객체를 생성. DB에 연결하는 기능을 제공
  - pgsql_query.py: PostgreSQL 데이터베이스에 사용되는 쿼리들을 저장
4. fakedata: 가상 데이터를 생성하고 삽입하는 기능을 제공하는 폴더
  - create.py: 가상 데이터를 생성하는 로직을 포함
  - insert.py: 생성된 가상 데이터를 데이터베이스에 삽입
  - process.py: 가상 데이터를 처리하는 데 필요한 추가적인 로직을 포함
5. pipeline: ETL 파이프라인을 구성하는 각 단계의 파일을 포함
  - extract.py: 데이터베이스로부터 데이터를 추출
  - load.py: 변환된 데이터를 데이터베이스에 로드
  - remove.py: ETL 작업 후 임시 파일이나 불필요한 데이터를 삭제하는 기능을 수행
  - transform.py: 데이터를 변환하는 로직을 포함
6. temp_storage: 임시 파일을 저장하는 폴더로, ETL 과정에서 사용되는 임시 데이터를 보관

 
2) 파일 생성

 controller.py

[tip] controller : 아래 기능들을 순차적으로 실행
1. DBconnector >> DB Connector 생성
2. postgresql_query >> queries 에서 테이블 이름 목록(table_list) 받아오기       
  ex) for tbl in table_list:
3. extract >> DB 조회 후 DataFrame 형태로 변환
4. transform >> 저장 경로 생성 후 임시 저장 디렉토리 아래에 dataframe 저장
5. load >> 저장소에 dataframe 파일 저장
6. remove >> 저장이 끝난 후 임시 저장 디렉토리 삭제
# controller.py
from db.connector import DBconnector        
from db.pgsql_query import queries            
from settings import DB_SETTINGS, TEMP_PATH  
from pipeline.extract import extractor      
from pipeline.transform import transformer   
from pipeline.load import loader             
from pipeline.remove import remover
from datetime import datetime                 

def controller(batch_date):
    """
    ETL (Extract, Transform, Load) 파이프라인의 컨트롤러 함수.
    주어진 날짜를 기준으로 데이터베이스의 데이터를 추출, 변환, 로드하고
    임시 파일을 정리함
    """
    # queries에 있는 키 값(테이블 이름)을 리스트로 변환하여 반복 작업에 사용
    table_list = list(queries.keys())

    # DB 연결 객체를 한 번만 생성 (반복적으로 연결을 생성하지 않음으로써 효율성 향상)
    db_obj = DBconnector(**DB_SETTINGS["POSTGRES"])

    # 테이블 목록을 순회하면서 각 테이블에 대해 데이터를 처리
    for table_name in table_list:
        # extractor 함수를 통해 해당 테이블의 데이터를 pandas DataFrame으로 추출
        pandas_df = extractor(db_obj, table_name)

        # 데이터가 존재하는 경우에만 변환 및 로드 작업을 수행
        if len(pandas_df) > 0:
            # transformer 함수를 통해 데이터를 변환하고 임시 저장 경로에 파일을 생성
            res = transformer(TEMP_PATH, batch_date, pandas_df, table_name)

            # 변환 결과가 존재하고 빈 데이터가 아닌 경우에만 데이터 로드 진행
            if res is not None and not res.empty:
                loader(db_obj, res, table_name)  # pandas_df 대신 변환된 res를 로드합니다.

    # 모든 작업이 완료되면 임시 파일을 삭제하여 디스크 공간 정리
    remover(TEMP_PATH)

# 메인 스크립트로 실행될 때의 처리
if __name__ == "__main__":
    # 현재 날짜와 시간을 가져와 "YYYYMMDD" 형식으로 변환하여 batch_date에 저장
    _date = datetime.now()
    batch_date = _date.strftime("%Y%m%d")
    
    # controller 함수를 호출하여 ETL 작업을 실행
    controller(batch_date)

 
② fakedata>create.py

# create.py
from faker import Faker                  # Faker 라이브러리 - 가상 데이터를 생성하기 위해 사용
import shortuuid                         # shortuuid 라이브러리 - 짧은 UUID를 생성하기 위해 사용
import pandas as pd                      # Pandas 라이브러리 - 데이터프레임을 사용하여 데이터 조작

def create_fakedataframe(count: int) -> pd.DataFrame:  # 가상 사용자 데이터를 생성하여 Pandas DataFrame으로 반환하는 함수
    # count 수만큼 가상 사용자 데이터를 생성하고 리스트에 저장
    fake_data_list = [create_fakeuser() for _ in range(count)] 
    # 리스트를 데이터프레임으로 변환하여 반환
    return pd.DataFrame(fake_data_list)  

def create_fakeuser() -> dict:  # 단일 가상 사용자를 생성하여 딕셔너리 형태로 반환하는 함수
    fake = Faker("ko_KR")                 # 한국어 기반의 가상 데이터 생성을 위해 Faker 인스턴스 생성
    fake_profile = fake.profile()         # Faker 라이브러리를 사용해 가상 프로필 생성
    key_list = ["name", "ssn", "job", "residence", "blood_group", "sex", "birthdate"]  # 필요한 필드 목록

    fake_dict = dict()                    # 가상 사용자 정보를 저장할 빈 딕셔너리 생성

    # 필요한 필드를 key_list에서 가져와 fake_dict에 추가
    for key in key_list:
        fake_dict[key] = fake_profile[key]

    fake_dict["uuid"] = shortuuid.uuid()  # 사용자에게 고유한 UUID 생성하여 추가
    fake_dict["birthdate"] = fake_dict["birthdate"].strftime("%Y%m%d")  # birthdate를 'YYYYMMDD' 형식으로 변환

    return fake_dict                      # 가상 사용자 정보를 담은 딕셔너리 반환

 
③ fakedata>insert.py

# insert.py
import pandas as pd                      # Pandas 라이브러리 - 데이터프레임을 사용하여 데이터 조작
from db.connector import DBconnector      # DBconnector 클래스 - 데이터베이스 연결 객체를 생성하는 클래스
from settings import DB_SETTINGS          # DB 설정 정보가 담긴 파일

def insert_fakedataframe(df: pd.DataFrame) -> bool:  # 가상의 데이터를 담은 Pandas DataFrame을 데이터베이스에 삽입하는 함수  # 성공 시 True를 반환하고, 실패 시 False를 반환   
    db_connector = DBconnector(**DB_SETTINGS["POSTGRES"])  # PostgreSQL 설정을 사용하여 DB 연결 객체 생성

    # with 문을 사용해 DB 연결을 관리 (연결이 자동으로 닫힘)
    with db_connector as connected:
        try:
            orm_conn = connected.orm_connect()  # ORM 방식으로 데이터베이스에 연결
            # 데이터프레임을 'fake' 테이블에 삽입, 기존 데이터가 있으면 추가 (if_exists="append")
            df.to_sql(name="fake", con=orm_conn, if_exists="append", index=False)
            return True  # 성공적으로 삽입된 경우 True 반환

        except Exception as e:
            print(e)  # 에러 발생 시 에러 메시지를 출력
            return False  # 실패한 경우 False 반환

 
④ fakedata>process.py

# process.py
import pandas as pd 

def categorize_age(age: int):  # 나이를 나이대 문자열로 변환하는 함수  # 예: 25 -> "20대", 95 -> "90대 이상"
    if age >= 90:
        return "90대 이상"
    else:
        return str(age // 10 * 10) + "대"  # 나이대를 10 단위로 구분하여 문자열로 반환

def create_fakedatamart(df: pd.DataFrame) -> pd.DataFrame:  # 가상 데이터 프레임을 가공하여 새로운 데이터 마트 데이터프레임을 생성하는 함수
    today = pd.to_datetime("today")  # 현재 날짜를 가져옴
    df["birthdate"] = pd.to_datetime(df["birthdate"], format="%Y%m%d")  # birthdate를 날짜 형식으로 변환

    # 도시 컬럼 생성 - 거주지(residence)에서 첫 번째 단어를 추출하여 city로 지정
    df["city"] = df["residence"].str.split().str[0]
    # 출생 연도 컬럼 생성 - birthdate에서 연도(year)만 추출하여 birth_year로 지정
    df["birth_year"] = df["birthdate"].dt.year
    # 나이 컬럼 생성 - 현재 연도와 출생 연도를 비교하여 나이를 계산
    df["age"] = (today.year - df["birthdate"].dt.year - (
        (today.month < df["birthdate"].dt.month) |
        ((today.month == df["birthdate"].dt.month) & (today.day < df["birthdate"].dt.day))
    )).astype(int)
    # 혈액형 컬럼 생성 - 혈액형(blood_group)에서 마지막 문자를 제외한 부분을 추출하여 blood로 지정
    df["blood"] = df["blood_group"].str.slice(0, -1)
    # 나이대 컬럼 생성 - categorize_age 함수를 사용하여 나이대를 age_category로 지정
    df["age_category"] = df["age"].apply(categorize_age)
    
    # 필요한 컬럼만 선택하여 최종 데이터 마트 데이터프레임을 생성
    column_list = ["uuid", "name", "job", "sex", "blood", "city", "birthdate", "age", "age_category"]
    df_datamart = df[column_list]

    return df_datamart  # 가공된 데이터프레임 반환

 

3) 이미 존재하는 컬럼(level_0) 삭제

from db.connector import DBconnector  # DBconnector 클래스를 통해 데이터베이스에 연결
from settings import DB_SETTINGS      # DB 설정 정보를 가져옴

# 삭제할 테이블 및 컬럼 정보
table_name = "lecture"                # 컬럼을 삭제할 테이블 이름
column_name = "level_0"               # 삭제할 컬럼 이름

# 컬럼 삭제 작업
try:
    # DBconnector 인스턴스를 통해 데이터베이스에 연결
    db_connector = DBconnector(**DB_SETTINGS["POSTGRES"])
    
    # `postgres_connect()` 메서드를 통해 연결 설정
    with db_connector.postgres_connect() as connector:
        cursor = connector.conn.cursor()  # 데이터베이스 커서를 생성

        # 컬럼 삭제 쿼리 실행
        alter_query = f"ALTER TABLE {table_name} DROP COLUMN IF EXISTS {column_name}"  # 컬럼 삭제 SQL 쿼리
        cursor.execute(alter_query)  # 쿼리 실행
        connector.conn.commit()      # 변경 사항을 커밋하여 반영

        print(f"'{table_name}' 테이블에서 '{column_name}' 컬럼이 삭제되었습니다.")  # 성공 메시지 출력

except Exception as e:
    print(f"오류 발생: {e}")  # 예외 발생 시 에러 메시지 출력

finally:
    # 커넥션 및 커서 닫기
    if 'cursor' in locals() and cursor is not None:  # 커서가 존재하는 경우 닫기
        cursor.close()
    if 'connector' in locals() and connector.conn is not None:  # 연결 객체가 존재하는 경우 닫기
        connector.conn.close()

 
4) PostgreSQL DB에 Fake 데이터 저장

① create.py에서 함수(create_fakeuser) 호출 → Fake user 데이터 생성

from create import create_fakeuser  
    
create_fakeuser()  # 정상적으로 데이터가 생성된 것을 확인

"""10명의 Fake user 데이터 생성 및 출력
for _ in range(10):
    fake_user = create_fakeuser()  # create.py의 create_fakeuser 함수 호출
    print(fake_user)               # 생성된 가짜 사용자 데이터 출력
"""

 
② create.py에서 함수(create_fakedataframe) 호출: Fake 데이터 → Fake 데이터프레임 저장

  • create_fakedataframe() ⊃ create_fakeuser()
import pandas as pd
from random import randint
from create import create_fakedataframe  # create.py에서 create_fakedataframe 함수 가져오기

count = randint(5, 15)                  # 5에서 15 사이의 랜덤한 개수 생성
df = create_fakedataframe(count)        # create_fakedataframe 함수 호출로 DataFrame 생성

 

③ PostgreSQL 데이터베이스에 연결

from db.connector import DBconnector
from settings import DB_SETTINGS

pgsql_obj = DBconnector(**DB_SETTINGS["POSTGRES"])  # PostgreSQL 데이터베이스 연결 객체 생성
pgsql_obj.__dict__  # 데이터베이스 정보를 딕셔너리로 확인


④ 연결된 데이터베이스에 데이터 넣기(테이블 명: fake)

with pgsql_obj as connected:
    sqlalchemy_conn = connected.orm_connect()   # SQLAlchemy 연결 객체 생성
    df = create_fakedataframe(count)            # 랜덤한 개수의 가짜 데이터프레임 생성
    df.to_sql(name="fake", con=sqlalchemy_conn, if_exists="replace", index=False)
    # if_exists="replace" -> 기존 테이블을 대체하여 새 데이터로 저장
    # if_exists="append" -> 기존 데이터에 새 데이터를 추가하여 누적 저장

print(df)  # 생성된 Fake 데이터프레임 출력

 

⑤ 데이터가 잘 들어갔는지 유효성 검사

# 데이터가 비어 있지 않은지 확인
assert not df.empty, "DataFrame이 비어있습니다."

# 필수 컬럼이 모두 포함되어 있는지 확인
assert set(["name", "ssn", "job", "residence", "blood_group", "sex", "birthdate"]).issubset(df.columns), "필드가 누락되었습니다."

# birthdate 필드가 "YYYYMMDD" 형식인지 확인
assert df["birthdate"].str.match(r"\d{8}").all(), "birthdate 필드가 YYYYMMDD 형식이 아닙니다."

# uuid 필드가 문자열 형식인지 확인
assert df["uuid"].apply(lambda x: isinstance(x, str)).all(), "uuid 필드가 문자열이 아닙니다."

# 모든 유효성 검사가 통과된 경우 출력
print("모든 필드가 올바르게 생성되었습니다.")

 
5) 데이터 가공

[pandas를 통해 Data Mart  형태의 테이블로 가공]
  1. 거주하는 도시 통계 -> 'residence' 전처리 및 칼럼 생성   
  2. 혈액형 통계 -> 'blood_group' 전처리 및 컬럼 생성   
  3. 남녀 통계 -> 'sex' 칼럼 사용   
  4. 나이테 통계 -> 'birthdate' 전처리 및 칼럼 생성

 

① Fake 데이터 프레임 생성(create_fakedataframe) → 데이터 마트 생성(create_fakedatamart)

from fakedata.process import create_fakedatamart       # 데이터 가공 함수 가져오기
from fakedata.create import create_fakedataframe       # 가짜 데이터 생성 함수 가져오기

# 10개의 가짜 데이터를 포함한 데이터프레임 생성
_df = create_fakedataframe(10)

# 생성된 데이터를 데이터마트 형식으로 가공
df = create_fakedatamart(_df)
# df 데이터프레임에서 sex 컬럼을 기준으로 그룹화하여 각 성별의 데이터 개수를 세고, 이를 새로운 데이터프레임으로 반환
# df.groupby("sex").size().reset_index(name="count")

 
6) 데이터 이행 모듈에 통합 

① main.py

[tip] main.py 의 Batch Process
  1. Click 라이브러리를 사용하여 명령줄에서 실행 가능한 스크립트를 정의
  2. 가짜 데이터를 생성하여 PostgreSQL 데이터베이스에 저장
  3. ETL 컨트롤러를 실행
# main.py
import click                                    # Click 라이브러리 - 명령줄 인터페이스 구성
import random, time
from datetime import datetime, timedelta        # 날짜와 시간 처리
from controller import controller               # ETL 컨트롤러 함수 가져오기
from fakedata.create import create_fakedataframe # 가짜 데이터 생성 함수
from fakedata.insert import insert_fakedataframe # 가짜 데이터 삽입 함수

@click.command()
@click.option("-s", "--batch_date", default="")  # batch_date 옵션, 기본값은 빈 문자열

def start(batch_date: str) -> None:
    # 배치 프로세스를 시작하는 함수

    if not batch_date:  # batch_date 값이 주어지지 않은 경우 어제 날짜를 사용
        _date = datetime.now() - timedelta(days=1)
        batch_date = _date.strftime("%Y%m%d")  # YYYYMMDD 형식으로 변환

    for _ in range(3):  # 배치 작업을 3번 반복
        count = random.randint(5, 15)  # 5~15 사이의 랜덤한 데이터 개수 생성
        print(f"======{count}개의 row가 삽입됩니다.======")  # 디버깅 출력

        # 가짜 데이터 생성
        fake_df = create_fakedataframe(count)
        print("<<< fake_df 생성 >>>")  # 가짜 데이터프레임 생성 디버깅 출력

        # 생성한 가짜 데이터를 PostgreSQL에 삽입
        insert_fakedataframe(fake_df)

        # ETL 컨트롤러 실행
        controller(batch_date)

        time.sleep(3)  # 각 배치 사이에 3초 대기

if __name__ == "__main__":
    start()  # start 함수 실행
    
    
"""
[파이썬 파일 실행 명령어]
터미널 창에, python main.py
"""

 
7) 시각화

① app.py

# app.py
import streamlit as st           # Streamlit 라이브러리 - 웹 대시보드 개발을 위한 라이브러리
import pandas as pd              # Pandas 라이브러리 - 데이터프레임을 사용하여 데이터 조작
import altair as alt             # Altair 라이브러리 - 데이터 시각화를 위한 라이브러리

# Streamlit 웹 설정
st.title("사용자 데이터 시각화 대시보드")  # 대시보드의 제목을 설정
st.sidebar.title("필터")                 # 사이드바 제목 설정

# 예시 데이터 생성
data = {
    "age_category": ["10대", "20대", "30대", "10대", "10대", "30대", "30대", "40대", "50대", "10대"],
    "sex": ["M", "F", "M", "F", "F", "F", "M", "M", "F", "F"],
    "city": ["서울", "부산", "대구", "대구", "대구", "대구", "부산", "서울", "서울", "대구"]    
}
df = pd.DataFrame(data)  # 딕셔너리 데이터를 Pandas DataFrame으로 변환

# 나이대 필터
# 사용자가 사이드바에서 선택한 나이대를 기반으로 데이터를 필터링
age_category = st.sidebar.multiselect(
    "나이대 선택",                    # 멀티셀렉트 위젯의 제목
    df["age_category"].unique(),       # 선택할 수 있는 나이대 목록
    default=df["age_category"].unique() # 기본 선택값으로 모든 나이대를 선택
)
filtered_data = df[df["age_category"].isin(age_category)]  # 선택된 나이대로 데이터 필터링


# 1. 나이대별 인원수 시각화
age_chart = (
    alt.Chart(filtered_data)            # 필터링된 데이터를 사용하여 Altair 차트 생성
    .mark_bar()                         # 막대(bar) 차트 유형 설정
    .encode(
        x = alt.X("age_category", sort="-y", title="나이대"),  # x축에 나이대, 내림차순 정렬
        y = alt.Y("count()", title="인원수"),                  # y축에 인원수(카운트)
        color = "age_category"                                # 나이대별 색상 구분
    )
    .properties(title="나이대별 인원 분포")                   # 차트 제목 설정
)


# 2. 성별별 인원수 시각화
gender_chart = (
    alt.Chart(filtered_data)            # 필터링된 데이터를 사용하여 Altair 차트 생성
    .mark_bar()                         # 막대(bar) 차트 유형 설정
    .encode(
        x = alt.X("sex", title="성별"),                    # x축에 성별
        y = alt.Y("count()", title="인원수"),              # y축에 인원수(카운트)
        color = "sex"                                     # 성별별 색상 구분
    )
    .properties(title="성별별 인원 분포")                  # 차트 제목 설정
)


# 3. 지역별 인원수 시각화
city_chart = (
    alt.Chart(filtered_data)            # 필터링된 데이터를 사용하여 Altair 차트 생성
    .mark_bar()                         # 막대(bar) 차트 유형 설정
    .encode(
        x = alt.X("city", sort="-y", title="지역"),         # x축에 지역, 내림차순 정렬
        y = alt.Y("count()", title="인원수"),              # y축에 인원수(카운트)
        color = "city"                                    # 지역별 색상 구분
    )
    .properties(title="지역별 인원 분포")                   # 차트 제목 설정
)


# 시각화 결과 출력
st.altair_chart(age_chart, use_container_width=True)     # 나이대별 인원 분포 차트를 Streamlit에 표시
st.altair_chart(gender_chart, use_container_width=True)  # 성별별 인원 분포 차트를 Streamlit에 표시
st.altair_chart(city_chart, use_container_width=True)    # 지역별 인원 분포 차트를 Streamlit에 표시


"""
[시각화 명령어]
터미널 창에, streamlit run app.py -> 이메일 입력
"""

 

② 시각화 결과
 

 

 

 

[파이썬 심화]

1. 기본 함수와 데코레이터

1) 기본 함수 정의 및 실행

  • time.sleep(): 입력된 시간(초) 동안 코드 실행을 중지
  • time.time(): 현재 시간을 초 단위로 측정
import time  # 시간을 측정하거나 지연을 줄 수 있는 모듈

def say_hello():
    # 이 함수는 "Hi!"를 출력하고, 3초간 대기한 후 "Goodbye"를 출력함
    print("Hi!")
    time.sleep(3)  # 3초간 대기. time.sleep() 함수는 입력된 시간(초)만큼 일시적으로 코드 실행을 중지시킴
    print("Goodbye")

# 실행 시작 시간을 기록하여 총 소요 시간 계산에 사용
start = time.time()  # 현재 시간을 초 단위로 측정해 변수 start에 저장
say_hello()  # say_hello 함수 호출

 

2)  데코레이터

① 정의 및 사용

  • 데코레이터: 다른 함수의 동작을 감싸서 추가적인 기능을 부여할 수 있는 구조
  • "he = say_hello": say_hello 함수를 he라는 변수에 참조시킨 것
    • he()를 호출하면 say_hello()가 실행됨
    • he 자체는 함수 객체를 가리키는 변수
def time_checker(func):
    # time_checker 데코레이터는 전달된 함수(func)의 실행 시간을 측정하여 출력하는 함수
    def wrapper():
        start = time.time()  # 시작 시간 기록
        func()  # 전달된 함수 실행
        end = time.time()  # 종료 시간 기록
        print("total time: ", end - start)  # 실행에 걸린 총 시간 출력
    return wrapper  # wrapper 함수를 반환하여 데코레이터 역할 수행

# 함수 객체 호출
he = say_hello  # say_hello 함수 자체를 he 변수에 저장
he()  # he()로 say_hello() 함수 호출 (실제 함수 실행)
he  # 함수 객체 자체를 출력, 실행되지 않음

 

② 데코레이터 적용 방식

  • @데코레이터 표기법: @time_checker 데코레이터가 say_hello와 say_bye에 적용됨
    • 이 경우, 각 함수가 호출될 때 time_checker의 wrapper가 실행되어 해당 함수의 실행 시간을 측정하고 출력
  • 함수 데코레이터: 코드의 가독성을 높여주며, 동일한 기능(여기서는 실행 시간 측정)을 여러 함수에 쉽게 적용할 수 있도록 함
@time_checker  # time_checker 데코레이터를 say_hello 함수에 적용
def say_hello():
    time.sleep(3)  # 3초간 대기
    print("Hello!")

@time_checker  # time_checker 데코레이터를 say_bye 함수에 적용
def say_bye():
    time.sleep(3)  # 3초간 대기
    print("Bye!")

say_hello()  # 데코레이터가 적용된 say_hello 함수 실행
say_bye()  # 데코레이터가 적용된 say_bye 함수 실행

 

2. 클래스와 메서드

1) 기본 클래스 정의 및 __init__ 메서드

 

  • __init__ 메서드: 객체가 생성될 때 초기화를 담당. var1과 var2 값을 받아 인스턴스 변수를 설정
  • 메서드 호출: upper_letter와 lower_letter 메서드는 var1의 대문자와 소문자 변환 버전을 각각 생성해 인스턴스 속성으로 저장하며, 출력함
  • __dict__: 객체의 속성을 딕셔너리 형태로 보여주어 속성 정보를 확인할 수 있음
class TempClass:
    # 클래스 초기화 메서드 (__init__) 정의. 인스턴스가 생성될 때 자동으로 호출됨
    def __init__(self, var1: str, var2: int):
        self.var1 = var1  # 인스턴스 변수 var1에 입력값을 저장
        self.var2 = var2  # 인스턴스 변수 var2에 입력값을 저장

    def upper_letter(self):
        # var1을 대문자로 변환하여 upper_var1 속성에 저장하고, 변환된 값 출력
        self.upper_var1 = self.var1.upper()
        print(self.var1.upper())

    def lower_letter(self):
        # var1을 소문자로 변환하여 lower_var1 속성에 저장하고, 변환된 값 출력
        self.lower_var1 = self.var1.lower()
        print(self.var1.lower())

# 객체 생성 및 메서드 호출
tc = TempClass("BANANA", 50)  # TempClass 인스턴스 생성
tc.lower_letter()  # lower_letter 메서드 호출, "banana" 출력
print(tc.__dict__)  # 객체의 모든 속성을 딕셔너리로 확인

 

2) dataclass 활용

 

  • @dataclass: 클래스의 기본적인 메서드(__init__, __repr__, __eq__ 등)를 자동으로 생성해주는 데코레이터
    • frozen=True로 설정하면 불변 객체가 되어 인스턴스를 생성한 후 필드를 수정할 수 없음
  • __post_init__ 메서드: 데이터 클래스의 초기화 이후 추가적인 설정이 필요할 때 사용
    • object.__setattr__을 통해 불변 객체의 속성을 설정할 수 있습니다.
from dataclasses import dataclass, field

# 데이터 클래스를 frozen=True로 설정하여 불변 객체로 만듦
@dataclass(frozen=True)
class TempClass:
    var1: str  # 필드 var1 정의
    var2: int  # 필드 var2 정의
    upper_var1: str = field(init=False)  # 초기화에서 제외할 필드 upper_var1 정의

    def __post_init__(self):
        # 불변 객체의 필드를 설정하기 위해 object.__setattr__ 사용
        object.__setattr__(self, "upper_var1", self.var1.upper())

 

3) staticmethod 활용

  • @staticmethod: 인스턴스가 아닌 클래스 자체에서 호출할 수 있는 메서드를 정의
    • 여기서 add_int는 클래스의 인스턴스 없이도 호출할 수 있음
  • staticmethod의 용도: 클래스와 독립적인 기능을 정의할 때 사용
    • 예를 들어, 인스턴스가 필요 없는 단순한 계산 작업 등을 처리할 때 유용
class TempClass:
    # 클래스 초기화 메서드
    def __init__(self, var1: str, var2: str):
        self.a = var1  # 인스턴스 변수 a 설정
        self.b = var2  # 인스턴스 변수 b 설정
        self.upper_letter()  # upper_letter 메서드 호출하여 대문자 설정

    def upper_letter(self):
        # var1을 대문자로 변환하여 upper_var1 속성에 저장
        self.upper_var1 = self.a.upper()

    @staticmethod
    def add_int(age: int):
        # 전달된 age 값을 두 배로 출력
        print(age * 2)

# 클래스 메서드 호출
TempClass.add_int(5)  # 정적 메서드 add_int 호출, 10 출력

 

3. 복잡한 데이터 구조와 dataclass

1) DataPipeline 클래스와 __post_init__ 메서드 활용

  • DataPipeline 클래스: 복잡한 데이터를 다룰 때 사용
    • catalog_name 딕셔너리와 schema_name 문자열을 매개변수로 받아 초기화 됨
  • __post_init__ 메서드: dataclass의 __init__ 실행 후 추가 속성을 설정할 때 사용
    • 여기서는 catalog_name의 "index" 값을 사용해 index 속성을 리스트로 초기화함
from dataclasses import dataclass

# DataPipeline 클래스 정의. catalog_name과 schema_name 필드를 받음
@dataclass
class DataPipeline:
    catalog_name: dict  # 딕셔너리 필드
    schema_name: str  # 문자열 필드

    def __post_init__(self):
        # catalog_name의 "index" 값을 리스트 형태로 새로운 index 속성에 추가
        self.index: list[int] = [self.catalog_name["index"]]

    def sample(self):
        # sample 메서드 실행시 "sample" 출력
        print("sample")

 

2) getattr 활용

  • getattr 함수: 객체에서 속성이나 메서드를 동적으로 접근할 수 있게 해주는 함수
    • 여기서는 DataPipeline 객체의 sample 메서드를 getattr을 통해 동적으로 호출함
  • 딕셔너리 언패킹: pp = DataPipeline(**night)는 night 딕셔너리의 키-값 쌍을 DataPipeline 클래스의 매개변수로 전달해 객체를 생성하는 방식
    • ** 표기법: 키-값 쌍을 해체하여 전달하는 역할
# DataPipeline 클래스 객체 생성 및 sample 메서드 호출
pipeline = DataPipeline(
    catalog_name=night["catalog_name"], schema_name=night["schema_name"]
)
print("index 속성: ", pipeline.index)  # index 속성 확인
pipeline.sample()  # sample 메서드 호출

pp = DataPipeline(**night)  # 딕셔너리 언패킹으로 객체 생성

# getattr로 DataPipeline 객체의 메서드 동적 호출
getattr(DataPipeline(**night), "sample")()  # sample 메서드 동적으로 호출

 

 

 

📙 내일 일정

  • PySpark 실습 및 문제 풀이


 
 
 
 
 

'TIL _Today I Learned > 2024.10' 카테고리의 다른 글

[DAY 72] Data Pipeline 및 PySpark 시험  (0) 2024.10.31
[DAY 71] PySpark  (0) 2024.10.30
[DAY 69] SQLAlchemy ORM  (1) 2024.10.28
[DAY 68] 데이터 엔지니어링  (1) 2024.10.25
[DAY 67] AWS 아키텍처 그리기  (0) 2024.10.24