데이터 분석 코드를 안정적으로 작성하려면 문법만 아는 것보다 다음 내용을 함께 이해해야 한다.
Python 코드가 실행되는 과정
→ 데이터를 담을 자료구조 선택
→ 반복 작업을 간결하게 표현
→ 기능을 함수로 분리
→ 파일에서 데이터 읽기
→ 예외와 로그로 실행 과정 관리
이번 1편에서는 Python 실행 구조부터 함수, 파일 처리, 데코레이터, 예외 처리와 로깅까지 살펴본다. 다음 2편에서는 타입 힌트와 데이터 모델링, 코드 품질과 테스트, 비동기·병렬 처리로 이어갈 예정이다.

1. Python의 실행 구조와 개발 환경
Python이 데이터 분석에 많이 사용되는 이유
Python은 문법이 간결하고 대화형 실행 환경을 제공한다. 코드를 조금씩 실행하며 데이터를 확인할 수 있어 탐색적인 분석에 적합하다.
또한 데이터 분석과 AI에 필요한 라이브러리가 풍부하다.
NumPy
→ 수치 계산과 배열
Pandas
→ 표 형태의 데이터 처리
scikit-learn
→ 머신러닝
PyTorch·TensorFlow
→ 딥러닝
Polars·DuckDB
→ 대용량 데이터 처리
코드 블록을 들여쓰기로 구분하기 때문에 비교적 읽기 쉽고, Windows·macOS·Linux에서 동일한 코드를 실행할 수 있다는 장점도 있다.
Python은 코드를 바로 실행하는 것처럼 보인다
Python은 흔히 인터프리터 언어라고 설명한다. 그러나 소스 코드를 그대로 한 줄씩 읽어 실행하는 단순한 구조는 아니다.
CPython의 기본 실행 흐름은 다음과 같다.
Python 소스 코드
→ Parser
→ AST
→ Bytecode
→ Python Virtual Machine
→ 실행 결과
Parser
작성한 코드가 Python 문법에 맞는지 해석한다.
result = a + b
AST
AST는 Abstract Syntax Tree의 약자로, 코드를 문법적인 의미 단위로 표현한 트리 구조다.
위 코드는 대략 다음 요소로 나누어 볼 수 있다.
변수 대입
├─ 대상: result
└─ 연산
├─ 왼쪽: a
├─ 연산자: +
└─ 오른쪽: b
린터, 포매터와 코드 분석 도구가 AST를 이용해 문법 구조를 파악한다.
Bytecode
AST는 Python Virtual Machine이 실행할 수 있는 중간 코드인 Bytecode로 변환된다.
import dis
def add(x, y):
return x + y
dis.dis(add)
간단히 보면 다음과 같은 흐름이다.
x 불러오기
→ y 불러오기
→ 덧셈 수행
→ 결과 반환
PVM
PVM은 Bytecode 명령을 읽고 실제 연산을 수행하는 실행 엔진이다.
따라서 Python은 다음과 같은 혼합 구조로 이해할 수 있다.
소스 코드
→ Bytecode로 컴파일
→ PVM에서 해석하며 실행
__pycache__ 디렉터리의 .pyc 파일은 이미 생성한 Bytecode를 재사용하기 위해 만들어진다.
Python의 메모리 관리
Python에서는 정수, 문자열, 리스트와 클래스 인스턴스까지 모두 객체로 다룬다.
각 객체에는 다음 정보가 포함된다.
type
→ 객체의 자료형
value
→ 실제 값
reference count
→ 해당 객체를 참조하는 수
a = [1, 2, 3]
b = a
a와 b는 서로 다른 리스트를 가진 것이 아니라 같은 객체를 참조한다.
print(a is b) # True
CPython은 기본적으로 참조 횟수가 0이 된 객체를 정리하고, 순환 참조는 Garbage Collector가 추가로 처리한다.
가상환경이 필요한 이유
프로젝트마다 필요한 라이브러리 버전이 다를 수 있다.
프로젝트 A
→ pandas 2.x
프로젝트 B
→ pandas 1.x
하나의 전역 환경에 모든 패키지를 설치하면 버전 충돌이 발생할 수 있다. 이를 방지하기 위해 프로젝트별 가상환경을 만든다.
python3 -m venv .venv
source .venv/bin/activate
필요한 패키지를 설치하고 목록을 저장한다.
pip install pandas numpy
pip freeze > requirements.txt
.venv 디렉터리는 Git에 올리지 않고, requirements.txt를 통해 필요한 의존성을 공유하는 것이 일반적이다.
2. 제어문과 자료구조
조건문과 Truthy·Falsy
Python의 조건식에는 반드시 True나 False만 들어가는 것은 아니다.
다음 값은 거짓으로 평가된다.
None
0
""
[]
{}
set()
items = []
if items:
print("데이터가 있습니다.")
else:
print("데이터가 없습니다.")
Short-circuit Evaluation
and와 or는 왼쪽 값만으로 결과가 결정되면 오른쪽을 평가하지 않는다.
user = None
if user and user.is_active:
print("활성 사용자입니다.")
user가 None이면 첫 번째 조건에서 거짓이 결정되므로 user.is_active는 실행되지 않는다. 덕분에 AttributeError를 피할 수 있다.
A and B
→ A가 거짓이면 B를 실행하지 않음
A or B
→ A가 참이면 B를 실행하지 않음
또한 and, or는 항상 Boolean을 반환하는 것이 아니라 실제 피연산자 값을 반환한다.
name = ""
display_name = name or "익명"
print(display_name) # 익명
반복문
for는 반복 가능한 객체를 순회할 때 사용한다.
fruits = ["사과", "바나나", "체리"]
for fruit in fruits:
print(fruit)
인덱스도 필요하다면 enumerate()를 사용한다.
for index, fruit in enumerate(fruits):
print(index, fruit)
while은 조건이 참인 동안 반복한다.
count = 0
while count < 3:
print(count)
count += 1
반복 흐름은 break와 continue로 제어할 수 있다.
for number in range(10):
if number == 5:
break
if number % 2 == 0:
continue
print(number)
기본 자료구조 비교
| 자료구조 | 순서 | 중복 | 변경 | 주된 용도 |
| list | 있음 | 허용 | 가능 | 순서가 있는 데이터 |
| tuple | 있음 | 허용 | 불가능 | 변경되면 안 되는 값 |
| set | 없음 | 불가 | 가능 | 중복 제거와 집합 연산 |
| dict | 삽입 순서 유지 | Key 중복 불가 | 가능 | Key–Value 데이터 |
List
numbers = [10, 20, 30]
numbers.append(40)
리스트는 동적 배열 구조다. 인덱스로 접근하거나 뒤에 값을 추가하는 연산은 빠르지만, 앞이나 중간에 값을 삽입하면 이후 요소들을 이동해야 한다.
numbers.insert(0, 5) # 요소 이동 필요
Tuple
point = (10, 20)
생성 후 요소를 변경할 수 없다. 고정된 좌표나 여러 반환값처럼 변경되지 않아야 하는 데이터에 적합하다.
Set
tags = {"python", "data", "python"}
print(tags)
# {'python', 'data'}
중복을 자동으로 제거하고 합집합·교집합·차집합을 처리할 수 있다.
Dictionary
user = {
"name": "Alice",
"age": 25
}
print(user["name"])
Key로 값을 빠르게 조회할 수 있어 JSON 데이터나 분석 레코드를 표현할 때 많이 사용한다. List는 동적 배열, Set과 Dictionary는 해시 구조를 중심으로 동작하며 자료구조에 따라 접근과 삽입·삭제 비용이 달라진다.
목적에 맞는 고급 자료구조
deque
양쪽 끝에서 데이터를 추가하거나 삭제할 때 적합하다.
from collections import deque
queue = deque([1, 2, 3])
queue.append(4)
queue.appendleft(0)
queue.pop()
queue.popleft()
리스트의 pop(0)은 뒤의 요소를 이동해야 하지만, deque.popleft()는 왼쪽 요소를 효율적으로 제거할 수 있다.
Counter
요소별 빈도를 계산한다.
from collections import Counter
categories = ["식품", "전자", "식품", "도서", "식품"]
counts = Counter(categories)
print(counts)
print(counts.most_common(2))
defaultdict
존재하지 않는 Key에 기본값을 자동으로 만든다.
from collections import defaultdict
scores_by_team = defaultdict(list)
scores_by_team["A"].append(90)
scores_by_team["A"].append(85)
scores_by_team["B"].append(70)
heapq
최솟값이나 우선순위가 높은 작업을 빠르게 꺼낼 때 사용한다.
import heapq
scores = []
heapq.heappush(scores, 80)
heapq.heappush(scores, 50)
heapq.heappush(scores, 90)
print(heapq.heappop(scores)) # 50
bisect
정렬된 리스트에서 값이 들어갈 위치를 이진 탐색으로 찾는다.
import bisect
numbers = [1, 3, 5, 7]
position = bisect.bisect_left(numbers, 6)
bisect.insort(numbers, 6)
print(position) # 3
print(numbers) # [1, 3, 5, 6, 7]
3. 컴프리헨션과 제너레이터
List Comprehension
반복문으로 리스트를 만드는 코드는 컴프리헨션으로 간결하게 표현할 수 있다.
squares = []
for number in range(1, 6):
squares.append(number ** 2)
squares = [
number ** 2
for number in range(1, 6)
]
조건도 함께 작성할 수 있다.
even_squares = [
number ** 2
for number in range(1, 11)
if number % 2 == 0
]
너무 많은 조건과 중첩 반복을 한 줄에 넣으면 오히려 읽기 어려워질 수 있다. 컴프리헨션은 변환 규칙이 단순할 때 사용하는 것이 좋다.
직원 데이터 필터링 예제
리스트 안에 여러 직원 정보가 Dictionary로 저장되어 있다고 하자.
employees = [
{
"name": "Alice",
"department": "Engineering",
"age": 30,
"salary": 85_000
},
{
"name": "Bob",
"department": "Marketing",
"age": 25,
"salary": 60_000
},
{
"name": "Charlie",
"department": "Engineering",
"age": 35,
"salary": 95_000
}
]
Engineering 부서이면서 급여가 80,000 이상인 직원의 이름만 추출할 수 있다.
high_salary_engineers = [
employee["name"]
for employee in employees
if employee["department"] == "Engineering"
and employee["salary"] >= 80_000
]
print(high_salary_engineers)
# ['Alice', 'Charlie']
급여가 높은 순서로 정렬할 수도 있다.
top_employees = sorted(
employees,
key=lambda employee: employee["salary"],
reverse=True
)
result = [
(employee["name"], employee["salary"])
for employee in top_employees[:2]
]
print(result)
# [('Charlie', 95000), ('Alice', 85000)]
이처럼 List와 Dictionary, 조건문, 컴프리헨션을 조합하면 작은 데이터셋을 Pandas 없이도 필터링하고 정렬할 수 있다.
Dictionary Comprehension
prices = {
"apple": 1000,
"banana": 500,
"cherry": 2000
}
discounted = {
fruit: price * 0.9
for fruit, price in prices.items()
}
print(discounted)
Dictionary의 Key와 Value를 변환하거나, 집계 결과를 새로운 Dictionary로 만들 때 유용하다.
zip()과 zip_longest()
zip()은 여러 Iterable의 같은 위치에 있는 값을 묶는다.
names = ["Alice", "Bob", "Charlie"]
scores = [90, 80, 95]
for name, score in zip(names, scores):
print(name, score)
두 리스트를 Dictionary로 만들 수도 있다.
keys = ["name", "age", "city"]
values = ["Alice", 25, "Seoul"]
person = dict(zip(keys, values))
길이가 다르면 zip()은 짧은 데이터에 맞춰 종료한다.
print(list(zip([1, 2, 3], ["one", "two"])))
# [(1, 'one'), (2, 'two')]
부족한 값을 채워야 한다면 zip_longest()를 사용한다.
from itertools import zip_longest
result = list(
zip_longest(
[1, 2, 3],
["one", "two"],
fillvalue="없음"
)
)
print(result)
# [(1, 'one'), (2, 'two'), (3, '없음')]
제너레이터
List Comprehension은 결과 전체를 메모리에 생성한다.
squares = [
number ** 2
for number in range(1_000_000)
]
제너레이터 표현식은 값을 한꺼번에 만들지 않고 필요할 때 하나씩 생성한다.
squares = (
number ** 2
for number in range(1_000_000)
)
함수 안에서 yield를 사용하면 제너레이터 함수를 만들 수 있다.
def read_rows(path: str):
with open(path, encoding="utf-8") as file:
next(file)
for line in file:
yield line.strip().split(",")
for row in read_rows("large.csv"):
print(row)
전체 파일을 메모리에 올리지 않고 한 행씩 처리하므로 대용량 CSV나 로그를 다루는 데 적합하다.
4. 함수와 고급 함수 패턴
함수의 기본 구조
함수는 반복되는 동작을 이름이 있는 코드 단위로 분리한다.
def calculate_average(
values: list[float]
) -> float:
if not values:
return 0.0
return sum(values) / len(values)
average = calculate_average(
[80, 90, 100]
)
print(average)
함수를 사용하면 같은 로직을 재사용할 수 있고, 입력과 출력을 기준으로 테스트하기도 쉬워진다.
기본값이 있는 매개변수
def introduce(
name: str,
age: int = 20
) -> str:
return f"이름: {name}, 나이: {age}"
print(introduce("영희"))
print(introduce("철수", 25))
기본값이 있는 매개변수는 기본값이 없는 매개변수 뒤에 배치해야 한다.
*args와 **kwargs
전달받을 인자의 개수가 정해지지 않았다면 가변 인자를 사용할 수 있다.
def calculate_sum(*numbers: float) -> float:
return sum(numbers)
*args는 여러 위치 인자를 Tuple로 받는다.
print(calculate_sum(10, 20, 30))
**kwargs는 여러 Keyword 인자를 Dictionary로 받는다.
def print_info(**info):
for key, value in info.items():
print(f"{key}: {value}")
print_info(
name="Alice",
age=25,
city="Seoul"
)
함께 사용할 때의 기본 순서는 다음과 같다.
일반 매개변수
→ *args
→ **kwargs
Lambda와 Partial
짧은 변환 함수는 Lambda로 표현할 수 있다.
products = [
{"name": "A", "price": 3000},
{"name": "B", "price": 1000}
]
sorted_products = sorted(
products,
key=lambda product: product["price"]
)
functools.partial()은 일부 인자를 고정한 새로운 함수를 만든다.
from functools import partial
def scale(value, factor):
return value * factor
double = partial(scale, factor=2)
print(double(10)) # 20
클로저
클로저는 내부 함수가 외부 함수의 변수를 기억하는 구조다.
def make_multiplier(factor):
def multiply(value):
return value * factor
return multiply
double = make_multiplier(2)
triple = make_multiplier(3)
print(double(10))
print(triple(10))
외부 함수 실행이 끝난 뒤에도 내부 함수는 factor 값을 기억한다.
5. 파일과 데이터 포맷 다루기
with를 사용하는 이유
파일은 사용 후 반드시 닫아야 한다.
file = open("example.txt", "r")
content = file.read()
file.close()
with를 사용하면 블록을 벗어날 때 자동으로 파일이 닫힌다.
with open(
"example.txt",
"r",
encoding="utf-8"
) as file:
content = file.read()
파일 처리 중 예외가 발생하더라도 자원이 정리되기 때문에 더 안전하다.
파일 모드
| 모드 | 의미 |
| r | 읽기 |
| w | 새로 쓰기, 기존 내용 삭제 |
| a | 기존 내용 뒤에 추가 |
| r+ | 읽기와 쓰기 |
| w+ | 새로 생성한 뒤 읽기와 쓰기 |
with open(
"log.txt",
"a",
encoding="utf-8"
) as file:
file.write("새로운 로그\n")
CSV
CSV는 행과 열 형태의 데이터를 텍스트로 저장한다.
import csv
with open(
"employees.csv",
"r",
encoding="utf-8"
) as file:
reader = csv.DictReader(file)
for row in reader:
print(row["name"])
DictReader를 사용하면 첫 번째 행의 Header가 Dictionary의 Key가 된다.
name,department,salary
Alice,Engineering,85000
{
"name": "Alice",
"department": "Engineering",
"salary": "85000"
}
JSON
JSON은 중첩된 Dictionary와 List 구조를 저장하거나 API와 데이터를 주고받을 때 유용하다.
import json
user = {
"name": "Alice",
"skills": [
"Python",
"SQL"
]
}
with open(
"user.json",
"w",
encoding="utf-8"
) as file:
json.dump(
user,
file,
ensure_ascii=False,
indent=2
)
with open(
"user.json",
"r",
encoding="utf-8"
) as file:
loaded_user = json.load(file)
ensure_ascii=False를 사용하면 한글이 Unicode Escape 형태로 바뀌는 것을 방지할 수 있다.
Pathlib과 Parquet
문자열로 경로를 조합하기보다 Path를 사용하면 읽기 쉽다.
from pathlib import Path
data_dir = Path("data")
file_path = data_dir / "sales.csv"
Parquet은 열 중심으로 데이터를 저장하는 형식이다.
import pandas as pd
frame = pd.read_parquet(
data_dir / "sales.parquet"
)
frame.to_parquet(
data_dir / "result.parquet"
)
CSV는 사람이 쉽게 확인할 수 있다는 장점이 있고, Parquet은 데이터 타입과 컬럼 구조를 유지하며 분석용 데이터 저장에 활용할 수 있다. CSV·JSON·Parquet과 pathlib.Path를 조합하면 파일 기반 데이터 처리 흐름을 구성할 수 있다.
6. 데코레이터, 예외 처리와 로깅
데코레이터
데코레이터는 기존 함수를 직접 수정하지 않고 실행 전후에 공통 기능을 추가한다.
from functools import wraps
from time import perf_counter
def timer(func):
@wraps(func)
def wrapper(*args, **kwargs):
started_at = perf_counter()
result = func(*args, **kwargs)
elapsed = perf_counter() - started_at
print(
f"{func.__name__}: "
f"{elapsed:.4f}초"
)
return result
return wrapper
@timer
def calculate_total(values):
return sum(values)
calculate_total([1, 2, 3])
실행 흐름은 다음과 같다.
calculate_total 호출
→ 실제로 wrapper 실행
→ 시작 시간 기록
→ 원래 함수 실행
→ 종료 시간 계산
→ 결과 반환
@wraps(func)를 사용하면 데코레이터를 적용한 뒤에도 원래 함수의 이름과 Docstring을 유지할 수 있다.
데코레이터는 다음과 같은 공통 기능에 활용할 수 있다.
실행 시간 측정
재시도
로깅
인증 확인
입력값 전처리
결과 캐싱
오류와 예외의 차이
문법이 잘못되어 코드를 실행할 수 없는 상태는 일반적으로 SyntaxError로 나타난다.
print("Hello"
예외는 문법적으로는 올바르지만 실행 중 문제가 발생한 상황이다.
result = 10 / 0
예외는 try-except로 처리할 수 있다.
try:
value = int(input("숫자: "))
result = 100 / value
except ValueError:
print("숫자를 입력해야 합니다.")
except ZeroDivisionError:
print("0으로 나눌 수 없습니다.")
else:
print(f"결과: {result}")
finally:
print("처리가 끝났습니다.")
try
→ 예외가 발생할 수 있는 코드
except
→ 특정 예외 처리
else
→ 예외가 없을 때 실행
finally
→ 성공·실패와 관계없이 실행
가능하면 except Exception으로 모든 예외를 한꺼번에 숨기기보다 예상 가능한 예외를 구체적으로 처리하는 것이 좋다.
사용자 정의 예외
업무에 맞는 오류를 직접 정의할 수도 있다.
class DataValidationError(ValueError):
def __init__(
self,
column: str,
value: object
):
message = (
f"{column}={value!r} "
"검증에 실패했습니다."
)
super().__init__(message)
def validate_salary(salary: int) -> None:
if salary < 0:
raise DataValidationError(
"salary",
salary
)
try:
validate_salary(-1000)
except DataValidationError as error:
print(error)
일반적인 ValueError보다 어떤 업무 규칙이 실패했는지를 명확하게 표현할 수 있다.
print()보다 Logging을 사용하는 이유
print()는 간단한 확인에는 편리하지만 운영 중 실행 이력을 관리하기 어렵다.
Logging을 사용하면 다음 정보를 함께 기록할 수 있다.
발생 시간
로그 수준
파일과 줄 번호
처리 내용
예외 정보
로그 수준은 다섯 단계로 구분할 수 있다.
| 레벨 | 용도 |
| DEBUG | 상세한 디버깅 정보 |
| INFO | 정상적인 실행 흐름 |
| WARNING | 문제가 될 수 있는 상황 |
| ERROR | 기능 처리 실패 |
| CRITICAL | 서비스 중단 가능성이 있는 오류 |
import logging
logging.basicConfig(
level=logging.INFO,
format=(
"%(asctime)s | "
"%(levelname)s | "
"%(message)s"
)
)
logger = logging.getLogger("pipeline")
logger.info("데이터 로딩 시작")
logger.warning("일부 값이 누락되었습니다.")
logger.error("파일을 읽지 못했습니다.")
규모가 커지면 Console과 파일에 서로 다른 수준의 로그를 남길 수도 있다.
ConsoleHandler
→ INFO 이상 출력
FileHandler
→ DEBUG 이상 저장
Error FileHandler
→ ERROR 이상 별도 저장
실행 흐름을 기록하고 오류를 재현해야 하는 데이터 파이프라인에서는 print()보다 구조화된 Logging이 적합하다.
간단한 데이터 처리 예제
지금까지의 내용을 연결해 CSV를 읽고, 유효한 행만 집계하고, 처리 결과를 로그로 남기는 간단한 예제를 만들 수 있다.
import csv
import logging
from collections import defaultdict
from pathlib import Path
logging.basicConfig(
level=logging.INFO,
format="%(levelname)s | %(message)s"
)
logger = logging.getLogger("sales")
class InvalidAmountError(ValueError):
pass
def parse_amount(value: str) -> float:
amount = float(value)
if amount < 0:
raise InvalidAmountError(
"매출은 0 이상이어야 합니다."
)
return amount
def summarize_sales(
file_path: Path
) -> dict[str, float]:
totals = defaultdict(float)
try:
with file_path.open(
encoding="utf-8"
) as file:
reader = csv.DictReader(file)
for row in reader:
try:
amount = parse_amount(
row["amount"]
)
totals[row["region"]] += amount
except (
ValueError,
InvalidAmountError
) as error:
logger.warning(
"행 제외: %s",
error
)
except FileNotFoundError:
logger.error(
"파일이 없습니다: %s",
file_path
)
return {}
logger.info(
"집계 완료: %d개 지역",
len(totals)
)
return dict(totals)
result = summarize_sales(
Path("sales.csv")
)
print(result)
이 예제에는 다음 개념이 함께 적용되어 있다.
Path
→ 파일 경로 관리
csv.DictReader
→ CSV를 Dictionary 형태로 읽기
defaultdict
→ 지역별 합계의 기본값 자동 생성
함수
→ 읽기·변환·집계 책임 분리
사용자 정의 예외
→ 잘못된 매출값 표현
try-except
→ 행 단위 오류와 파일 오류 처리
logging
→ 처리 흐름과 오류 기록
전체 흐름 다시 보기
Python 소스 코드
→ AST
→ Bytecode
→ PVM 실행
조건문·반복문
→ 처리 흐름 제어
List·Tuple·Set·Dictionary
→ 데이터 구조 표현
Comprehension
→ 필터링과 변환
Generator
→ 대용량 데이터 스트리밍
Function
→ 기능과 책임 분리
CSV·JSON·Parquet
→ 데이터 저장과 교환
Decorator
→ 공통 기능 분리
Exception
→ 실패 상황 제어
Logging
→ 실행 과정 기록
핵심 정리
Python
→ 소스 코드를 Bytecode로 변환한 뒤 PVM에서 실행
List
→ 순서 있는 가변 데이터
Tuple
→ 순서 있는 불변 데이터
Set
→ 중복 없는 데이터
Dictionary
→ Key–Value 데이터
Comprehension
→ 간단한 필터링과 변환
Generator
→ 전체 데이터를 메모리에 올리지 않고 순차 처리
Function
→ 로직을 재사용 가능한 단위로 분리
Decorator
→ 원래 함수를 수정하지 않고 기능 확장
Exception
→ 예상 가능한 실패 처리
Logging
→ 운영 중 실행 흐름과 오류 기록
복습 질문
- Python 소스 코드는 어떤 단계를 거쳐 실행되는가?
- AST와 Bytecode는 각각 어떤 역할을 하는가?
- List의 앞쪽 삽입이 뒤쪽 추가보다 느린 이유는 무엇인가?
- List와 deque는 어떤 상황에서 구분해 사용해야 하는가?
- Counter와 defaultdict는 어떤 집계에 유용한가?
- zip()과 zip_longest()는 길이가 다른 데이터를 어떻게 처리하는가?
- List Comprehension과 Generator Expression의 메모리 사용 방식은 어떻게 다른가?
- 클로저가 외부 함수 실행 후에도 값을 기억할 수 있는 이유는 무엇인가?
- 데코레이터에서 functools.wraps()를 사용하는 이유는 무엇인가?
- CSV, JSON과 Parquet은 각각 어떤 데이터에 적합한가?
- 구체적인 예외를 나누어 처리하는 것이 좋은 이유는 무엇인가?
- 운영 환경에서 print()보다 Logging이 적합한 이유는 무엇인가?
'개발 > SKALA 4기' 카테고리의 다른 글
| [SKALA][Feature Engineering] EDA와 기본 전처리 정리 (0) | 2026.08.06 |
|---|---|
| [SKALA] 데이터 분석 및 Python 기초 ③: Pandas, 분석 자동화 (0) | 2026.08.06 |
| [SKALA] 입력값 검증부터 Spring JPA와 API 문서화 (0) | 2026.07.31 |
| [SKALA] 컴포넌트 스캔과 Spring 컨테이너, HTTP 요청 바인딩 정리 (0) | 2026.07.29 |
| [SKALA] Spring Boot 설정 관리와 MVC·Actuator 정리 (0) | 2026.07.29 |