🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

Python 기초부터 심화까지 완벽 가이드 - 슬라이드 1/13
A

AI Generated

2025. 11. 1. · 13 Views

Python 기초부터 심화까지 완벽 가이드

Python 프로그래밍의 기초 문법부터 고급 기능까지 체계적으로 학습할 수 있는 완벽한 가이드입니다. 실무에서 바로 활용 가능한 예제와 함께 Python의 핵심 개념들을 깊이 있게 다룹니다.


목차

  1. 변수와 데이터 타입 - 파이썬 프로그래밍의 기초
  2. 함수 정의와 활용 - 재사용 가능한 코드 작성하기
  3. 리스트 컴프리헨션 - 간결하고 효율적인 리스트 생성
  4. 딕셔너리와 활용법 - 구조화된 데이터 관리하기
  5. 예외 처리 - 오류를 우아하게 다루기
  6. 클래스와 객체지향 - 재사용 가능한 코드 구조 만들기
  7. 데코레이터 - 함수를 꾸미는 강력한 패턴
  8. 제너레이터 - 메모리 효율적인 이터레이션

1. 변수와 데이터 타입 - 파이썬 프로그래밍의 기초

시작하며

여러분이 처음 프로그래밍을 시작할 때 가장 먼저 만나게 되는 것이 바로 변수입니다. 데이터를 담는 상자라고 생각하면 쉽습니다.

하지만 많은 초보자들이 "왜 이 변수에는 숫자를 넣었다가 문자를 넣어도 되는데, 다른 언어에서는 안 되는 거지?"라는 의문을 가집니다. Python은 동적 타이핑(Dynamic Typing) 언어로, 변수의 타입을 실행 시간에 결정합니다.

이는 초보자에게는 편리하지만, 대규모 프로젝트에서는 예상치 못한 버그를 일으킬 수 있습니다. 바로 이럴 때 필요한 것이 데이터 타입에 대한 정확한 이해입니다.

타입을 제대로 이해하면 코드의 안정성을 높이고, 디버깅 시간을 크게 줄일 수 있습니다.

개요

간단히 말해서, 변수는 데이터를 저장하는 메모리 공간의 이름이고, 데이터 타입은 그 데이터의 종류를 나타냅니다. Python에서는 숫자형(int, float, complex), 문자열(str), 불린(bool), 리스트(list), 튜플(tuple), 딕셔너리(dict), 집합(set) 등 다양한 기본 타입을 제공합니다.

예를 들어, 사용자 정보를 다루는 웹 애플리케이션을 만든다면 문자열로 이름을, 숫자로 나이를, 딕셔너리로 전체 프로필을 관리할 수 있습니다. 기존 C나 Java에서는 변수를 선언할 때 타입을 명시해야 했다면, Python에서는 값을 할당하는 순간 자동으로 타입이 결정됩니다.

이를 통해 코드 작성이 훨씬 빠르고 유연해집니다. Python의 모든 것은 객체입니다.

심지어 숫자 1도 객체입니다. 이러한 특징 덕분에 메서드 체이닝이 가능하고, 모든 데이터를 일관된 방식으로 다룰 수 있습니다.

type() 함수로 언제든 타입을 확인할 수 있고, isinstance()로 타입을 검증할 수 있어 실무에서 매우 유용합니다.

코드 예제

# 기본 데이터 타입 예제
name = "Alice"  # 문자열 타입
age = 28  # 정수 타입
height = 165.5  # 실수 타입
is_student = False  # 불린 타입

# 타입 확인 및 변환
print(f"이름: {name}, 타입: {type(name)}")  # <class 'str'>
print(f"나이: {age}, 타입: {type(age)}")  # <class 'int'>

# 타입 변환 (Type Casting)
age_str = str(age)  # 숫자를 문자열로
price = int("1000")  # 문자열을 숫자로

# 복합 데이터 타입
user_info = {"name": name, "age": age, "height": height}
scores = [95, 87, 92, 88]  # 리스트

설명

이것이 하는 일: 위 코드는 Python의 기본 데이터 타입들을 선언하고, 타입을 확인하며, 필요시 타입을 변환하는 방법을 보여줍니다. 첫 번째로, 변수 선언 부분에서는 각각 다른 타입의 데이터를 변수에 할당합니다.

Python 인터프리터가 값을 보고 자동으로 적절한 타입을 지정하기 때문에, 따로 타입을 명시할 필요가 없습니다. 예를 들어 name = "Alice"를 실행하는 순간, Python은 큰따옴표로 감싸져 있다는 것을 보고 이를 문자열 객체로 만듭니다.

그 다음으로, type() 함수를 사용하여 각 변수의 실제 타입을 확인합니다. f-string을 활용하면 변수의 값과 타입을 동시에 출력할 수 있어 디버깅할 때 매우 편리합니다.

실무에서는 예상한 타입이 맞는지 확인하는 용도로 자주 사용됩니다. 타입 변환 부분에서는 명시적으로 한 타입을 다른 타입으로 바꿉니다.

str(), int(), float() 같은 내장 함수를 사용하면 됩니다. 웹에서 받은 문자열 형태의 숫자를 실제 계산에 사용하려면 int()로 변환해야 하는데, 이는 웹 개발에서 매우 흔한 패턴입니다.

마지막으로, 딕셔너리와 리스트 같은 복합 타입을 생성합니다. 딕셔너리는 키-값 쌍으로 구조화된 데이터를 저장할 때, 리스트는 순서가 있는 여러 데이터를 담을 때 사용합니다.

실무에서는 JSON 데이터를 다룰 때 딕셔너리를, 반복 작업이 필요한 데이터는 리스트를 주로 사용합니다. 여러분이 이 코드를 사용하면 Python의 기본 타입 시스템을 완벽히 이해하고, API 응답 처리, 데이터 검증, 타입 변환 등 실무에서 매일 마주치는 작업을 자신있게 처리할 수 있습니다.

실전 팁

💡 타입 힌트(Type Hints)를 사용하면 코드 가독성이 높아집니다: def greet(name: str) -> str: 형식으로 작성하면 IDE의 자동완성과 타입 체크가 가능해집니다.

💡 문자열을 숫자로 변환할 때는 try-except로 감싸세요. int("abc")는 ValueError를 일으키므로 사용자 입력 처리 시 반드시 예외 처리가 필요합니다.

💡 is와 ==의 차이를 이해하세요. ==는 값의 동등성을, is는 객체의 동일성을 확인합니다. None 체크는 if x is None:이 올바른 방법입니다.

💡 가변(mutable) vs 불변(immutable) 타입을 구분하세요. 리스트와 딕셔너리는 가변이라 함수에 전달하면 원본이 변경될 수 있지만, 문자열과 튜플은 불변이라 안전합니다.


2. 함수 정의와 활용 - 재사용 가능한 코드 작성하기

시작하며

여러분이 같은 코드를 여러 번 복사-붙여넣기 하고 있다면, 그건 함수로 만들어야 한다는 신호입니다. 예를 들어, 여러 곳에서 사용자 입력을 검증하거나, 데이터를 특정 형식으로 변환하는 작업을 반복한다면 코드가 길어지고 유지보수가 어려워집니다.

이런 문제는 프로젝트가 커질수록 심각해집니다. 버그를 고칠 때 10군데를 모두 수정해야 하고, 하나라도 놓치면 새로운 버그가 생깁니다.

또한 코드 리뷰를 받을 때도 중복 코드가 많으면 좋은 평가를 받기 어렵습니다. 바로 이럴 때 필요한 것이 함수입니다.

함수는 특정 작업을 수행하는 코드 블록을 하나로 묶어서 이름을 붙인 것으로, 필요할 때마다 호출하여 재사용할 수 있게 해줍니다.

개요

간단히 말해서, 함수는 입력(매개변수)을 받아서 처리한 후 출력(반환값)을 돌려주는 코드의 재사용 단위입니다. Python에서 함수는 def 키워드로 정의하며, 매개변수를 받고, 내부 로직을 실행하고, return으로 결과를 반환합니다.

실무에서는 데이터 변환, 검증, API 호출, 파일 처리 등 거의 모든 작업을 함수로 구조화합니다. 예를 들어, 이메일 유효성 검증, 가격 계산, 데이터베이스 쿼리 실행 같은 작업들이 모두 함수로 만들어집니다.

기존에는 같은 코드를 여러 번 작성했다면, 이제는 한 번만 정의하고 여러 곳에서 호출할 수 있습니다. 이는 DRY(Don't Repeat Yourself) 원칙의 핵심입니다.

Python 함수의 핵심 특징은 일급 객체(First-class Object)라는 점입니다. 함수를 변수에 할당하거나, 다른 함수의 인자로 전달하거나, 함수에서 함수를 반환할 수 있습니다.

또한 기본 매개변수, 키워드 인자, 가변 인자(*args, **kwargs) 등 유연한 인자 처리가 가능합니다. 이러한 특징들이 Python을 강력하고 표현력 있는 언어로 만들어줍니다.

코드 예제

# 기본 함수 정의
def calculate_bmi(weight, height):
    """BMI(체질량지수)를 계산하는 함수"""
    bmi = weight / (height ** 2)
    return round(bmi, 2)

# 기본 매개변수 사용
def greet(name, greeting="안녕하세요"):
    """사용자를 인사하는 함수 (기본 인사말 제공)"""
    return f"{greeting}, {name}님!"

# 가변 인자 활용
def calculate_average(*numbers):
    """여러 숫자의 평균을 계산"""
    if not numbers:
        return 0
    return sum(numbers) / len(numbers)

# 함수 사용 예제
bmi = calculate_bmi(70, 1.75)  # 22.86
message = greet("철수")  # "안녕하세요, 철수님!"
avg = calculate_average(90, 85, 95, 88)  # 89.5

설명

이것이 하는 일: 위 코드는 Python에서 함수를 정의하고 사용하는 다양한 패턴을 보여주며, 실무에서 자주 사용하는 함수 작성 기법을 담고 있습니다. 첫 번째로, calculate_bmi 함수는 가장 기본적인 형태의 함수입니다.

두 개의 매개변수를 받아서 계산을 수행하고 결과를 반환합니다. 독스트링("""로 감싼 문자열)은 함수의 목적을 설명하며, help() 함수나 IDE에서 자동으로 표시됩니다.

실무에서는 반드시 독스트링을 작성하여 다른 개발자가 함수를 이해할 수 있게 해야 합니다. 그 다음으로, greet 함수는 기본 매개변수를 사용합니다.

greeting 매개변수에 기본값을 지정해두면, 호출할 때 이 인자를 생략할 수 있습니다. 이는 API 함수를 설계할 때 매우 유용한데, 대부분의 경우 기본값으로 충분하지만 필요시 커스터마이징할 수 있는 유연성을 제공합니다.

세 번째로, calculate_average 함수는 *args를 사용하여 개수가 정해지지 않은 인자를 받습니다. 함수 내부에서 numbers는 튜플로 처리되며, 몇 개의 숫자를 전달하든 상관없이 작동합니다.

이는 유틸리티 함수를 만들 때 매우 강력한 기능입니다. 마지막으로, 실제 함수 호출 예제를 보여줍니다.

함수를 정의한 후에는 함수 이름과 괄호를 사용하여 호출하며, 필요한 인자를 전달합니다. 반환값은 변수에 저장하거나 직접 사용할 수 있습니다.

여러분이 이 코드를 사용하면 중복 코드를 제거하고, 코드의 가독성과 유지보수성을 크게 향상시킬 수 있습니다. 또한 테스트 작성이 쉬워지고, 팀원들과의 협업도 훨씬 수월해집니다.

실전 팁

💡 함수는 한 가지 일만 하도록 설계하세요(Single Responsibility Principle). 함수가 너무 많은 일을 하면 재사용성이 떨어지고 테스트가 어려워집니다.

💡 독스트링을 항상 작성하세요. 함수의 목적, 매개변수, 반환값을 명확히 기술하면 6개월 후에 코드를 다시 볼 때 큰 도움이 됩니다.

💡 타입 힌트를 추가하면 더 좋습니다: def calculate_bmi(weight: float, height: float) -> float: 형식으로 작성하면 버그를 미리 잡을 수 있습니다.

💡 함수명은 동사로 시작하세요. calculate_average, get_user_info, validate_email처럼 동작을 나타내는 이름이 이해하기 쉽습니다.

💡 기본 매개변수로 가변 객체(리스트, 딕셔너리)를 사용하지 마세요. def func(items=[]): 대신 def func(items=None): items = items or []를 사용해야 예상치 못한 버그를 방지할 수 있습니다.


3. 리스트 컴프리헨션 - 간결하고 효율적인 리스트 생성

시작하며

여러분이 빈 리스트를 만들고 for 루프를 돌면서 append()로 요소를 하나씩 추가하는 코드를 작성하고 있다면, 더 좋은 방법이 있습니다. 예를 들어, 1부터 100까지 숫자 중 짝수만 골라내거나, 문자열 리스트에서 대문자로 변환된 새 리스트를 만드는 작업을 할 때 말입니다.

이런 패턴은 Python 코드에서 굉장히 자주 나타나는데, 전통적인 방법으로 작성하면 코드가 4-5줄이 되고 가독성도 떨어집니다. 또한 초보자처럼 보이는 코드 스타일이라 코드 리뷰에서 지적받을 수 있습니다.

바로 이럴 때 필요한 것이 리스트 컴프리헨션입니다. 한 줄로 리스트를 생성하고 변환하며 필터링까지 할 수 있는 Python의 강력한 기능입니다.

개요

간단히 말해서, 리스트 컴프리헨션은 기존 시퀀스로부터 새로운 리스트를 생성하는 간결한 문법입니다. [표현식 for 항목 in 시퀀스 if 조건] 형태로 작성합니다.

Python 개발자들이 가장 사랑하는 기능 중 하나로, 코드를 pythonic하게 만들어줍니다. 실무에서는 데이터 전처리, API 응답 가공, 파일 내용 필터링, 리스트 변환 등 수많은 곳에서 사용됩니다.

예를 들어, 사용자 목록에서 활성 사용자만 추출하거나, 상품 가격에 할인율을 적용하거나, 파일명 리스트에서 특정 확장자만 필터링하는 작업들이 한 줄로 처리됩니다. 기존 방법에서는 빈 리스트를 만들고, for 루프를 작성하고, if 조건을 확인하고, append()를 호출하는 4단계가 필요했다면, 이제는 한 줄로 모든 것을 표현할 수 있습니다.

리스트 컴프리헨션의 핵심 특징은 가독성과 성능입니다. 코드가 짧아질 뿐만 아니라 실제로 실행 속도도 빠릅니다.

Python 인터프리터가 내부적으로 최적화를 수행하기 때문입니다. 또한 중첩된 컴프리헨션도 가능하고, 조건부 표현식을 사용하여 if-else 로직도 넣을 수 있습니다.

이러한 특징들이 리스트 컴프리헨션을 Python의 시그니처 기능으로 만들어줍니다.

코드 예제

# 기본 리스트 컴프리헨션
numbers = [1, 2, 3, 4, 5]
squared = [n ** 2 for n in numbers]  # [1, 4, 9, 16, 25]

# 조건부 필터링
even_numbers = [n for n in range(20) if n % 2 == 0]  # [0, 2, 4, ..., 18]

# 문자열 변환
names = ["alice", "bob", "charlie"]
upper_names = [name.upper() for name in names]  # ["ALICE", "BOB", "CHARLIE"]

# 조건부 표현식 (if-else)
numbers = [1, 2, 3, 4, 5]
labels = ["짝수" if n % 2 == 0 else "홀수" for n in numbers]

# 중첩 리스트 평탄화
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flat = [num for row in matrix for num in row]  # [1, 2, 3, 4, 5, 6, 7, 8, 9]

# 딕셔너리에서 특정 값 추출
users = [{"name": "Alice", "age": 25}, {"name": "Bob", "age": 30}]
ages = [user["age"] for user in users]  # [25, 30]

설명

이것이 하는 일: 위 코드는 리스트 컴프리헨션의 다양한 활용 패턴을 보여주며, 전통적인 for 루프를 간결한 한 줄 표현식으로 대체하는 방법을 담고 있습니다. 첫 번째로, 기본 형태인 squared는 각 숫자를 제곱합니다.

[n ** 2 for n in numbers]는 "numbers의 각 요소 n에 대해 n의 제곱을 계산하여 새 리스트를 만들어라"는 의미입니다. 이는 map() 함수와 람다를 사용하는 것보다 훨씬 읽기 쉽습니다.

그 다음으로, even_numbers는 if 조건을 추가하여 짝수만 필터링합니다. 조건은 컴프리헨션의 끝에 위치하며, True인 항목만 결과 리스트에 포함됩니다.

이는 filter() 함수를 대체하는 pythonic한 방법입니다. 세 번째로, upper_names는 메서드 체이닝을 보여줍니다.

표현식 부분에 어떤 연산이나 메서드 호출도 가능하므로, 문자열 처리, 타입 변환, 계산 등을 자유롭게 수행할 수 있습니다. labels 예제는 삼항 연산자(조건부 표현식)를 사용합니다.

if-else가 필요한 경우, 표현식1 if 조건 else 표현식2 형태로 작성하며, 이때 else는 필수입니다. 이는 각 요소를 다르게 변환해야 할 때 매우 유용합니다.

중첩 리스트 평탄화는 2차원 리스트를 1차원으로 만듭니다. 중첩된 for를 사용하면 for row in matrix for num in row 순서로 작성하며, 왼쪽에서 오른쪽으로 읽으면 됩니다.

마지막으로, 딕셔너리 리스트에서 특정 필드만 추출하는 패턴입니다. API 응답에서 필요한 데이터만 뽑아낼 때 실무에서 매일 사용하는 패턴입니다.

여러분이 이 코드를 사용하면 코드 길이를 50% 이상 줄이고, 가독성을 높이며, 성능도 향상시킬 수 있습니다. 또한 Python다운 코드를 작성하여 동료 개발자들에게 좋은 인상을 줄 수 있습니다.

실전 팁

💡 너무 복잡한 로직은 컴프리헨션에 넣지 마세요. 표현식이 두 줄 이상 필요하다면 일반 for 루프를 사용하는 것이 더 읽기 쉽습니다.

💡 딕셔너리 컴프리헨션과 집합 컴프리헨션도 있습니다: {k: v for k, v in items}, {x for x in list} 형태로 사용할 수 있습니다.

💡 대용량 데이터는 제너레이터 표현식을 고려하세요. (x for x in range(1000000))처럼 괄호를 사용하면 메모리를 절약할 수 있습니다.

💡 리스트 컴프리헨션은 새 리스트를 만듭니다. 원본을 수정하려면 일반 for 루프를 사용해야 합니다.

💡 가독성이 중요합니다. result = [process(x) for x in items if is_valid(x)]처럼 함수로 로직을 분리하면 컴프리헨션이 깔끔해집니다.


4. 딕셔너리와 활용법 - 구조화된 데이터 관리하기

시작하며

여러분이 사용자 정보, 설정 값, API 응답, 데이터베이스 결과 등을 다룰 때 어떻게 저장하시나요? 여러 개의 변수로 흩어놓으면 관리가 어렵고, 리스트만 쓰면 인덱스 번호로 접근해야 해서 코드를 이해하기 힘듭니다.

user_info[0]이 이름인지 나이인지 매번 기억해야 하는 건 너무 불편합니다. 이런 문제는 데이터가 복잡해질수록 심각해집니다.

특히 JSON API를 다루거나, 설정 파일을 읽거나, 데이터베이스 쿼리 결과를 처리할 때 구조화된 데이터 저장 방법이 필수입니다. 바로 이럴 때 필요한 것이 딕셔너리입니다.

키-값 쌍으로 데이터를 저장하여 의미 있는 이름으로 접근할 수 있게 해주는 Python의 핵심 자료구조입니다.

개요

간단히 말해서, 딕셔너리는 키(key)를 통해 값(value)에 접근하는 해시 테이블 기반의 자료구조입니다. 중괄호 {}를 사용하거나 dict() 함수로 생성합니다.

Python에서 딕셔너리는 거의 모든 곳에 사용됩니다. JSON 데이터가 딕셔너리로 파싱되고, 함수의 키워드 인자가 내부적으로 딕셔너리로 처리되며, 객체의 속성도 __dict__라는 딕셔너리에 저장됩니다.

실무에서는 사용자 프로필, 상품 정보, API 응답, 캐시 데이터, 카운터 등 다양한 용도로 활용됩니다. 예를 들어, 전자상거래 사이트에서 장바구니는 {product_id: quantity} 형태의 딕셔너리로 구현할 수 있습니다.

기존 배열이나 리스트에서는 숫자 인덱스로만 접근했다면, 딕셔너리는 문자열이나 숫자 등 다양한 타입을 키로 사용할 수 있습니다. 이를 통해 코드의 의미가 명확해집니다.

딕셔너리의 핵심 특징은 O(1) 시간 복잡도로 데이터에 접근할 수 있다는 점입니다. 내부적으로 해시 테이블을 사용하기 때문에 아무리 데이터가 많아도 키로 값을 찾는 속도는 거의 일정합니다.

또한 Python 3.7 이후로는 삽입 순서를 보장하며, get(), setdefault(), update() 같은 유용한 메서드들을 제공합니다. 이러한 특징들이 딕셔너리를 Python에서 가장 많이 사용되는 자료구조로 만들어줍니다.

코드 예제

# 딕셔너리 생성 방법들
user = {"name": "Alice", "age": 25, "email": "alice@example.com"}
product = dict(id=1001, name="노트북", price=1200000)

# 값 접근 및 수정
name = user["name"]  # "Alice"
user["age"] = 26  # 나이 수정
user["city"] = "서울"  # 새 키-값 추가

# 안전한 접근 (KeyError 방지)
phone = user.get("phone", "정보 없음")  # 키가 없으면 기본값 반환

# 딕셔너리 순회
for key, value in user.items():
    print(f"{key}: {value}")

# 딕셔너리 병합 (Python 3.9+)
defaults = {"theme": "dark", "notifications": True}
settings = {"theme": "light"} | defaults  # {"theme": "light", "notifications": True}

# 키 존재 확인 및 안전한 업데이트
if "cart" not in user:
    user["cart"] = []
user["cart"].append({"product_id": 1001, "quantity": 2})

# 딕셔너리 컴프리헨션
prices = {"사과": 1000, "바나나": 1500, "포도": 2000}
discounted = {item: price * 0.9 for item, price in prices.items()}

설명

이것이 하는 일: 위 코드는 딕셔너리를 생성하고, 접근하고, 수정하며, 순회하는 다양한 실전 패턴을 보여줍니다. 첫 번째로, 딕셔너리 생성 방법 두 가지를 보여줍니다.

리터럴 문법 {}이 가장 일반적이지만, dict() 생성자를 사용하면 키워드 인자로 간편하게 만들 수 있습니다. 단, dict()를 사용할 때는 키가 유효한 식별자여야 합니다(숫자나 특수문자로 시작할 수 없음).

그 다음으로, 대괄호 []로 값에 접근하고 수정합니다. 존재하지 않는 키를 읽으려 하면 KeyError가 발생하므로 주의해야 합니다.

하지만 값을 할당할 때는 키가 없으면 자동으로 추가되므로, 새 키-값 쌍을 만드는 것이 매우 쉽습니다. 안전한 접근을 위해 get() 메서드를 사용합니다.

이 메서드는 키가 없을 때 None이나 지정한 기본값을 반환하므로, 예외 처리 없이 안전하게 코드를 작성할 수 있습니다. 실무에서는 API 응답이나 설정 파일에서 선택적 필드를 읽을 때 필수적으로 사용합니다.

items() 메서드는 키-값 쌍을 튜플로 반환하므로, 언패킹을 사용하여 for 루프에서 편리하게 순회할 수 있습니다. keys()는 키만, values()는 값만 순회할 때 사용합니다.

딕셔너리 병합은 Python 3.9에서 추가된 | 연산자를 사용합니다. 오른쪽 딕셔너리의 값으로 왼쪽을 업데이트하며, 이전 버전에서는 {**dict1, **dict2} 문법이나 update() 메서드를 사용해야 했습니다.

마지막으로, 딕셔너리 컴프리헨션으로 새 딕셔너리를 생성합니다. 가격에 할인율을 적용하는 예제처럼, 기존 딕셔너리를 변환하거나 필터링할 때 매우 유용합니다.

여러분이 이 코드를 사용하면 JSON API 작업, 설정 관리, 데이터 변환, 캐싱 등 실무의 거의 모든 데이터 처리 작업을 효율적으로 수행할 수 있습니다. 딕셔너리는 Python 프로그래밍의 심장부라고 할 수 있습니다.

실전 팁

💡 딕셔너리 키는 불변 객체여야 합니다. 문자열, 숫자, 튜플은 가능하지만 리스트는 불가능합니다. 해시 가능해야 하기 때문입니다.

💡 setdefault()는 키가 없을 때만 값을 설정합니다: user.setdefault("cart", []).append(item) 형태로 초기화와 사용을 한 줄에 처리할 수 있습니다.

💡 defaultdict를 사용하면 더 편리합니다. from collections import defaultdict; counter = defaultdict(int)로 KeyError 걱정 없이 카운터를 만들 수 있습니다.

💡 딕셔너리를 JSON으로 변환: import json; json.dumps(user)로 API 응답을 만들거나, json.loads(json_string)으로 파싱할 수 있습니다.

💡 키 삭제는 del user["key"]user.pop("key", default)를 사용하세요. pop()은 값을 반환하면서 삭제하고 기본값도 지정할 수 있어 더 안전합니다.


5. 예외 처리 - 오류를 우아하게 다루기

시작하며

여러분이 파일을 읽거나, API를 호출하거나, 사용자 입력을 받을 때 뭔가 잘못될 수 있다는 걸 항상 생각하시나요? 파일이 존재하지 않거나, 네트워크가 끊기거나, 사용자가 숫자 대신 문자를 입력할 수 있습니다.

이런 상황을 대비하지 않으면 프로그램이 갑자기 멈추고 사용자는 당황스러운 에러 메시지를 보게 됩니다. 이런 문제는 프로덕션 환경에서 치명적입니다.

예상치 못한 예외로 서버가 다운되거나, 사용자 데이터가 손실되거나, 보안 취약점이 노출될 수 있습니다. 또한 에러 로그가 없으면 문제의 원인을 파악하기도 어렵습니다.

바로 이럴 때 필요한 것이 예외 처리입니다. try-except 구문으로 예상 가능한 오류를 잡아내고, 적절히 처리하며, 프로그램이 계속 실행될 수 있게 해줍니다.

개요

간단히 말해서, 예외 처리는 프로그램 실행 중 발생하는 오류를 감지하고 대응하는 메커니즘입니다. try 블록에 위험한 코드를, except 블록에 오류 처리 로직을 작성합니다.

Python에서는 "허락보다 용서를 구하는 것이 쉽다(EAFP: Easier to Ask for Forgiveness than Permission)" 철학을 따릅니다. 조건문으로 미리 검사하기보다는 일단 시도해보고 예외가 발생하면 처리하는 방식입니다.

실무에서는 파일 I/O, 네트워크 통신, 데이터베이스 작업, 타입 변환, 외부 라이브러리 호출 등 거의 모든 외부 상호작용에서 예외 처리가 필수입니다. 예를 들어, API 서버는 잘못된 요청에 대해 400 에러를 반환하고, 파일 처리 시스템은 권한 오류를 사용자에게 알려야 합니다.

기존 C 스타일 언어에서는 반환값으로 에러를 체크했다면, Python에서는 예외 객체를 던지고 잡는 방식으로 에러를 전파합니다. 이는 정상 로직과 에러 처리를 분리하여 코드를 더 깔끔하게 만듭니다.

예외 처리의 핵심 특징은 예외의 계층 구조입니다. BaseException이 최상위이고, Exception이 대부분의 일반 예외의 부모 클래스입니다.

구체적인 예외(ValueError, FileNotFoundError 등)를 먼저 잡고, 일반적인 예외를 나중에 잡는 것이 원칙입니다. finally 블록은 예외 발생 여부와 관계없이 항상 실행되어 리소스 정리에 사용됩니다.

raise로 예외를 다시 던지거나 커스텀 예외를 만들 수도 있습니다. 이러한 특징들이 견고한 프로그램 작성을 가능하게 합니다.

코드 예제

# 기본 예외 처리
def divide(a, b):
    """안전한 나눗셈 함수"""
    try:
        result = a / b
        return result
    except ZeroDivisionError:
        print("0으로 나눌 수 없습니다.")
        return None
    except TypeError:
        print("숫자 타입만 지원합니다.")
        return None

# 파일 처리 with 예외 처리
def read_config(filename):
    """설정 파일을 읽어 딕셔너리로 반환"""
    try:
        with open(filename, 'r', encoding='utf-8') as f:
            import json
            return json.load(f)
    except FileNotFoundError:
        print(f"{filename} 파일을 찾을 수 없습니다.")
        return {}
    except json.JSONDecodeError:
        print("JSON 형식이 올바르지 않습니다.")
        return {}
    finally:
        print("파일 처리 완료")  # 항상 실행됨

# 여러 예외를 한 번에 처리
def process_user_input(value):
    try:
        number = int(value)
        result = 100 / number
        return result
    except (ValueError, ZeroDivisionError) as e:
        print(f"입력 오류: {e}")
        return None

# 예외 정보 활용
def fetch_data(url):
    try:
        # 실제로는 requests 라이브러리 사용
        response = api_call(url)
        return response
    except Exception as e:
        print(f"오류 발생: {type(e).__name__} - {str(e)}")
        raise  # 예외를 다시 던져서 상위 호출자가 처리하게 함

설명

이것이 하는 일: 위 코드는 Python에서 예외를 안전하게 처리하는 다양한 패턴을 보여주며, 실무에서 견고한 프로그램을 작성하는 방법을 담고 있습니다. 첫 번째로, divide 함수는 가장 기본적인 예외 처리입니다.

try 블록 안에서 나눗셈을 시도하고, ZeroDivisionError가 발생하면 첫 번째 except에서, TypeError가 발생하면 두 번째 except에서 잡습니다. 각 예외마다 다른 메시지를 출력하고 None을 반환하여 호출자가 에러를 알 수 있게 합니다.

그 다음으로, read_config 함수는 파일과 JSON 처리의 실전 패턴입니다. with 문으로 파일을 열면 자동으로 닫히지만, 파일이 없거나 JSON 파싱에 실패할 수 있습니다.

각 상황에 맞는 예외를 잡아 빈 딕셔너리를 반환하여 프로그램이 계속 실행되게 합니다. finally 블록은 성공하든 실패하든 항상 실행되어 로깅이나 리소스 정리에 사용됩니다.

세 번째로, process_user_input은 여러 예외를 하나의 except에서 처리합니다. 튜플로 묶으면 ValueError와 ZeroDivisionError 중 어느 것이 발생해도 같은 방식으로 처리할 수 있습니다.

as e를 사용하면 예외 객체를 변수에 담아 에러 메시지를 확인할 수 있습니다. 마지막으로, fetch_data는 예외를 다시 던지는(re-raise) 패턴입니다.

로그를 남긴 후 raise로 예외를 상위로 전파하면, 호출자가 추가 처리를 할 수 있습니다. 이는 라이브러리 함수를 작성할 때 유용한 패턴으로, 에러를 기록하면서도 최종 처리는 사용자에게 맡깁니다.

여러분이 이 코드를 사용하면 예상치 못한 크래시를 방지하고, 사용자에게 친절한 에러 메시지를 제공하며, 디버깅을 위한 로그를 남길 수 있습니다. 프로덕션 환경에서 안정적인 시스템을 만드는 핵심입니다.

실전 팁

💡 bare except를 피하세요. except: 없이 사용하면 KeyboardInterrupt까지 잡아서 Ctrl+C가 안 먹힙니다. 최소한 except Exception:을 사용하세요.

💡 예외 체이닝을 활용하세요. raise NewError("...") from e로 원래 예외를 보존하면 디버깅이 쉬워집니다.

💡 로깅 라이브러리를 사용하세요. import logging; logging.exception("오류 발생")은 스택 트레이스를 자동으로 기록합니다.

💡 예외는 예외적인 상황에만 사용하세요. 정상적인 제어 흐름에 사용하면 성능이 떨어지고 코드가 이해하기 어려워집니다.

💡 커스텀 예외를 만들면 더 명확합니다: class InvalidUserError(Exception): pass로 도메인 특화 예외를 정의할 수 있습니다.


6. 클래스와 객체지향 - 재사용 가능한 코드 구조 만들기

시작하며

여러분이 비슷한 속성과 동작을 가진 여러 개체를 다룰 때, 딕셔너리와 함수로만 관리하면 금방 복잡해집니다. 예를 들어, 은행 계좌 시스템을 만든다고 할 때 각 계좌마다 잔액, 소유자, 거래 내역을 관리하고, 입금/출금 로직을 구현해야 합니다.

함수와 딕셔너리를 따로 관리하면 어떤 함수가 어떤 데이터와 관련있는지 추적하기 어렵습니다. 이런 문제는 코드가 커질수록 유지보수가 불가능해집니다.

데이터와 로직이 흩어져 있으면 버그 수정이 어렵고, 코드 재사용도 힘들며, 팀원들과 협업할 때도 혼란스럽습니다. 바로 이럴 때 필요한 것이 클래스입니다.

관련된 데이터(속성)와 기능(메서드)을 하나로 묶어서 재사용 가능한 객체를 만드는 객체지향 프로그래밍의 핵심입니다.

개요

간단히 말해서, 클래스는 객체를 만들기 위한 청사진(blueprint)이고, 객체는 클래스로부터 생성된 실제 인스턴스입니다. class 키워드로 정의하며, __init__ 메서드로 초기화합니다.

Python의 모든 것은 객체입니다. 숫자, 문자열, 리스트, 함수까지도 객체이며, 각각 해당하는 클래스의 인스턴스입니다.

실무에서는 도메인 모델(User, Product, Order 등), 유틸리티(Logger, Cache, Database Connection), 데이터 구조(Stack, Queue, Tree) 등을 클래스로 구현합니다. 예를 들어, 전자상거래 시스템에서 ShoppingCart 클래스는 상품 목록, 총 금액 계산, 할인 적용 등의 기능을 하나로 묶어 관리합니다.

기존 절차적 프로그래밍에서는 함수와 데이터가 분리되어 있었다면, 객체지향에서는 관련된 것들을 캡슐화하여 하나의 단위로 만듭니다. 이를 통해 코드의 구조가 명확해지고 재사용성이 높아집니다.

클래스의 핵심 특징은 캡슐화, 상속, 다형성입니다. 캡슐화는 데이터와 메서드를 하나로 묶고 외부 접근을 제한하는 것입니다(언더스코어 _와 __로 프라이빗 표시).

상속은 기존 클래스를 확장하여 새 클래스를 만드는 것으로, 코드 재사용을 극대화합니다. 다형성은 같은 인터페이스로 다른 동작을 하게 하는 것입니다.

또한 __str__, __repr__, __len__ 같은 매직 메서드로 Python의 내장 기능과 통합할 수 있습니다. 이러한 특징들이 대규모 프로젝트에서 코드를 체계적으로 관리하게 해줍니다.

코드 예제

# 기본 클래스 정의
class BankAccount:
    """은행 계좌를 나타내는 클래스"""

    def __init__(self, owner, balance=0):
        """계좌 생성자: 소유자와 초기 잔액을 설정"""
        self.owner = owner
        self.balance = balance
        self.transactions = []  # 거래 내역

    def deposit(self, amount):
        """입금 메서드"""
        if amount > 0:
            self.balance += amount
            self.transactions.append(f"입금: {amount}원")
            return True
        return False

    def withdraw(self, amount):
        """출금 메서드"""
        if 0 < amount <= self.balance:
            self.balance -= amount
            self.transactions.append(f"출금: {amount}원")
            return True
        return False

    def __str__(self):
        """객체를 문자열로 표현 (print용)"""
        return f"{self.owner}님의 계좌 (잔액: {self.balance:,}원)"

# 클래스 사용
account = BankAccount("김철수", 100000)
account.deposit(50000)
account.withdraw(30000)
print(account)  # "김철수님의 계좌 (잔액: 120,000원)"
print(account.transactions)  # ["입금: 50000원", "출금: 30000원"]

# 상속 예제
class SavingsAccount(BankAccount):
    """저축 계좌 (이자 기능 추가)"""

    def __init__(self, owner, balance=0, interest_rate=0.03):
        super().__init__(owner, balance)
        self.interest_rate = interest_rate

    def add_interest(self):
        """이자 적용"""
        interest = self.balance * self.interest_rate
        self.deposit(interest)
        return interest

설명

이것이 하는 일: 위 코드는 클래스를 정의하고 객체를 생성하며, 상속을 통해 기능을 확장하는 객체지향 프로그래밍의 핵심 패턴을 보여줍니다. 첫 번째로, BankAccount 클래스 정의입니다.

class 키워드로 클래스를 선언하고, 독스트링으로 목적을 설명합니다. __init__ 메서드는 생성자로, 객체가 만들어질 때 자동으로 호출됩니다.

self는 인스턴스 자신을 가리키며, 모든 인스턴스 메서드의 첫 번째 매개변수로 필수입니다. owner, balance, transactions는 인스턴스 속성으로, 각 객체마다 독립적인 값을 가집니다.

그 다음으로, deposit과 withdraw 메서드는 계좌의 동작을 정의합니다. self를 통해 인스턴스의 속성에 접근하고 수정합니다.

유효성 검사를 포함하여 잘못된 작업을 방지하며, 불린 값을 반환하여 성공 여부를 알립니다. transactions 리스트에 기록을 남겨 추적 가능하게 합니다.

__str__ 메서드는 매직 메서드의 예입니다. 이를 정의하면 print()나 str()로 객체를 변환할 때 호출되어 사람이 읽기 좋은 형태로 표시됩니다.

f-string과 천 단위 구분자(,)를 사용하여 가독성을 높입니다. 클래스 사용 부분에서는 BankAccount("김철수", 100000)로 객체를 생성합니다.

이때 __init__이 호출되고, 반환된 객체는 account 변수에 저장됩니다. 이후 점(.) 표기법으로 메서드를 호출하고 속성에 접근할 수 있습니다.

마지막으로, SavingsAccount는 BankAccount를 상속받아 기능을 확장합니다. 괄호 안에 부모 클래스를 명시하고, super()로 부모의 초기화를 호출합니다.

새로운 속성(interest_rate)과 메서드(add_interest)를 추가하면서도 부모의 deposit, withdraw는 그대로 사용할 수 있습니다. 이것이 코드 재사용의 핵심입니다.

여러분이 이 코드를 사용하면 복잡한 시스템을 체계적으로 설계하고, 코드 중복을 줄이며, 유지보수가 쉬운 구조를 만들 수 있습니다. 객체지향은 대규모 프로젝트의 필수 스킬입니다.

실전 팁

💡 클래스명은 파스칼 케이스(PascalCase)를 사용하세요: BankAccount, UserProfile처럼 각 단어의 첫 글자를 대문자로 씁니다.

💡 @property 데코레이터로 getter를 만들면 속성처럼 접근할 수 있습니다: @property def total(self): return sum(self.items) 형태로 계산된 속성을 제공할 수 있습니다.

💡 __repr__ 메서드도 정의하세요. 이는 개발자용 표현으로, def __repr__(self): return f"BankAccount('{self.owner}', {self.balance})" 형태로 객체 재생성 코드를 반환합니다.

💡 너무 많은 기능을 하나의 클래스에 넣지 마세요(단일 책임 원칙). 클래스가 비대해지면 여러 클래스로 분리하는 것이 좋습니다.

💡 dataclass를 고려하세요. 주로 데이터를 담는 클래스라면 @dataclass 데코레이터로 __init__, __repr__ 등을 자동 생성할 수 있습니다.


7. 데코레이터 - 함수를 꾸미는 강력한 패턴

시작하며

여러분이 여러 함수에 로깅, 권한 체크, 실행 시간 측정, 캐싱 같은 공통 기능을 추가하고 싶을 때가 있습니다. 각 함수마다 똑같은 코드를 복사-붙여넣기 하면 중복이 심하고, 나중에 로깅 형식을 바꾸려면 모든 함수를 수정해야 합니다.

이런 문제는 횡단 관심사(cross-cutting concerns)라고 불리며, 전통적인 방법으로는 해결하기 어렵습니다. 함수의 본질적인 로직과 부가적인 기능이 뒤섞여서 코드가 지저분해지고 유지보수가 어려워집니다.

바로 이럴 때 필요한 것이 데코레이터입니다. 기존 함수를 수정하지 않고 새로운 기능을 추가할 수 있는 Python의 강력한 메타프로그래밍 도구입니다.

개요

간단히 말해서, 데코레이터는 함수를 받아서 기능을 추가한 새 함수를 반환하는 함수입니다. @decorator_name 문법으로 함수 위에 적용합니다.

Python에서 함수는 일급 객체이므로 다른 함수의 인자로 전달하거나 반환할 수 있습니다. 이 특성을 활용한 것이 데코레이터입니다.

실무에서는 웹 프레임워크의 라우팅(@app.route), 권한 검사(@login_required), 성능 측정, API 재시도 로직, 캐싱(@lru_cache) 등 수많은 곳에서 사용됩니다. 예를 들어, Flask나 FastAPI 같은 웹 프레임워크는 데코레이터를 핵심 기능으로 사용합니다.

기존에는 함수 안에 부가 기능 코드를 직접 작성했다면, 이제는 데코레이터로 깔끔하게 분리할 수 있습니다. 이는 관심사의 분리(Separation of Concerns) 원칙을 실현합니다.

데코레이터의 핵심 특징은 클로저(closure)를 활용한다는 점입니다. 데코레이터 함수 내부에 중첩 함수를 정의하고, 외부 함수의 변수를 캡처하여 사용합니다.

functools.wraps를 사용하면 원본 함수의 메타데이터를 보존할 수 있습니다. 매개변수를 받는 데코레이터는 한 단계 더 감싸서 구현합니다.

클래스를 데코레이터로 사용할 수도 있습니다. 이러한 특징들이 데코레이터를 Python의 가장 우아한 기능 중 하나로 만듭니다.

코드 예제

import time
from functools import wraps

# 기본 데코레이터: 실행 시간 측정
def timer(func):
    """함수 실행 시간을 측정하는 데코레이터"""
    @wraps(func)  # 원본 함수의 메타데이터 보존
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)  # 원본 함수 실행
        end = time.time()
        print(f"{func.__name__} 실행 시간: {end - start:.4f}초")
        return result
    return wrapper

# 매개변수를 받는 데코레이터
def repeat(times):
    """함수를 여러 번 실행하는 데코레이터"""
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for _ in range(times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

# 데코레이터 사용
@timer
def calculate_sum(n):
    """1부터 n까지 합 계산"""
    return sum(range(n + 1))

@repeat(times=3)
def greet(name):
    print(f"안녕하세요, {name}님!")

# 실행
result = calculate_sum(1000000)  # "calculate_sum 실행 시간: 0.0234초" 출력
greet("철수")  # "안녕하세요, 철수님!" 3번 출력

# 여러 데코레이터 중첩
@timer
@repeat(times=2)
def process_data():
    print("데이터 처리 중...")
    time.sleep(0.5)

설명

이것이 하는 일: 위 코드는 데코레이터를 정의하고 사용하는 방법을 보여주며, 함수에 부가 기능을 투명하게 추가하는 강력한 패턴을 담고 있습니다. 첫 번째로, timer 데코레이터는 가장 기본적인 형태입니다.

함수를 매개변수로 받아서 wrapper라는 새 함수를 만들고 반환합니다. wrapper 내부에서는 시작 시간을 기록하고, 원본 함수를 실행하고, 종료 시간을 기록하여 차이를 계산합니다.

*args, **kwargs를 사용하면 어떤 인자를 받는 함수든 데코레이트할 수 있습니다. @wraps(func)는 wrapper가 func의 이름, 독스트링 등을 그대로 유지하게 해줍니다.

그 다음으로, repeat 데코레이터는 매개변수를 받습니다. 이를 위해 한 단계를 더 감싸서 3중 중첩 함수 구조를 만듭니다.

가장 바깥쪽 함수(repeat)가 매개변수를 받고, 중간 함수(decorator)가 원본 함수를 받고, 안쪽 함수(wrapper)가 실제 실행됩니다. times 변수는 클로저로 캡처되어 wrapper 안에서 사용할 수 있습니다.

데코레이터 사용 부분에서는 @timer를 calculate_sum 위에 적용합니다. 이는 calculate_sum = timer(calculate_sum)과 동일한 의미입니다.

함수를 정의하는 순간 데코레이터가 적용되므로, 이후 calculate_sum을 호출할 때마다 자동으로 시간 측정이 됩니다. @repeat(times=3)는 매개변수를 전달하므로 괄호가 필요합니다.

repeat(times=3)가 먼저 실행되어 decorator를 반환하고, 그 decorator가 greet 함수를 감쌉니다. 마지막으로, 여러 데코레이터를 중첩할 수 있습니다.

위에서 아래로 순서대로 적용되므로, process_data는 먼저 repeat으로 감싸지고 그 결과가 timer로 감싸집니다. 이는 timer(repeat(times=2)(process_data))와 동일합니다.

여러분이 이 코드를 사용하면 로깅, 권한 검사, 캐싱, 재시도 로직 같은 공통 기능을 재사용 가능하고 깔끔한 방식으로 구현할 수 있습니다. 데코레이터는 Python의 가장 pythonic한 기능 중 하나입니다.

실전 팁

💡 functools.wraps를 항상 사용하세요. 이를 빼먹으면 help()나 디버깅 도구에서 원본 함수 정보가 사라집니다.

💡 functools.lru_cache는 내장 캐싱 데코레이터입니다. @lru_cache(maxsize=128)로 함수 결과를 캐싱하여 반복 계산을 방지할 수 있습니다.

💡 클래스도 데코레이터가 될 수 있습니다. __call__ 메서드를 정의하면 인스턴스를 함수처럼 호출할 수 있습니다.

💡 데코레이터를 너무 많이 중첩하면 디버깅이 어려워집니다. 3개 이상 중첩되면 코드를 재구성하는 것이 좋습니다.

💡 비동기 함수는 async def로 wrapper를 정의해야 합니다. async def wrapper(*args, **kwargs): return await func(*args, **kwargs) 형태로 작성하세요.


8. 제너레이터 - 메모리 효율적인 이터레이션

시작하며

여러분이 대용량 파일을 읽거나, 무한 시퀀스를 생성하거나, 수백만 개의 데이터를 처리할 때 리스트로 모두 메모리에 올리면 어떻게 될까요? 메모리가 부족해서 프로그램이 느려지거나 죽을 수 있습니다.

예를 들어, 1GB 크기의 로그 파일을 한 번에 읽어서 리스트에 담으면 1GB 이상의 메모리가 필요합니다. 이런 문제는 빅데이터 처리, 스트리밍, 실시간 데이터 분석에서 심각합니다.

모든 데이터를 메모리에 올릴 수 없는 상황에서는 전통적인 리스트 방식이 통하지 않습니다. 바로 이럴 때 필요한 것이 제너레이터입니다.

값을 필요할 때마다 하나씩 생성하여 메모리를 효율적으로 사용하고, 무한 시퀀스도 표현할 수 있게 해주는 Python의 핵심 기능입니다.

개요

간단히 말해서, 제너레이터는 이터레이터를 만드는 간편한 방법으로, return 대신 yield를 사용하여 값을 하나씩 반환합니다. 함수 실행이 중단되었다가 다음 호출 시 이어집니다.

Python의 제너레이터는 게으른 평가(lazy evaluation)를 구현합니다. 값이 실제로 필요할 때까지 계산을 미루기 때문에 메모리와 CPU를 절약할 수 있습니다.

실무에서는 대용량 파일 처리, 데이터베이스 커서, API 페이지네이션, 무한 스트림, ETL 파이프라인 등에서 필수적으로 사용됩니다. 예를 들어, 수백만 줄의 CSV 파일을 처리할 때 제너레이터를 사용하면 한 번에 한 줄만 메모리에 올려서 처리할 수 있습니다.

기존 리스트나 튜플은 모든 요소를 메모리에 저장했다면, 제너레이터는 다음 값을 생성하는 방법만 기억합니다. 이를 통해 공간 복잡도를 O(n)에서 O(1)로 줄일 수 있습니다.

제너레이터의 핵심 특징은 상태 보존입니다. yield에서 함수 실행이 일시 중지되고, 다음 next() 호출 시 중단된 지점부터 재개됩니다.

로컬 변수와 실행 컨텍스트가 유지되므로 복잡한 상태 머신을 간단히 구현할 수 있습니다. 제너레이터 표현식 (x for x in range(n))도 가능하며, send()로 값을 제너레이터 안으로 보낼 수도 있습니다.

itertools 모듈은 제너레이터 기반의 강력한 도구들을 제공합니다. 이러한 특징들이 제너레이터를 효율적인 Python 프로그래밍의 핵심 도구로 만듭니다.

코드 예제

# 기본 제너레이터 함수
def count_up(max):
    """max까지 숫자를 생성하는 제너레이터"""
    count = 1
    while count <= max:
        yield count  # 값을 반환하고 실행 중단
        count += 1

# 사용: for 루프로 자동 반복
for num in count_up(5):
    print(num)  # 1, 2, 3, 4, 5

# 대용량 파일 읽기 제너레이터
def read_large_file(file_path):
    """파일을 한 줄씩 읽어 반환 (메모리 효율적)"""
    with open(file_path, 'r', encoding='utf-8') as file:
        for line in file:
            yield line.strip()

# 무한 시퀀스 제너레이터
def fibonacci():
    """피보나치 수열을 무한히 생성"""
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

# 필요한 만큼만 가져오기
fib = fibonacci()
first_10 = [next(fib) for _ in range(10)]  # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

# 제너레이터 표현식 (리스트 컴프리헨션과 유사)
squares = (x**2 for x in range(1000000))  # 메모리를 거의 안 씀
print(sum(squares))  # 한 번에 하나씩 계산하여 합산

# 데이터 파이프라인
def read_numbers(filename):
    """파일에서 숫자를 읽음"""
    for line in read_large_file(filename):
        yield int(line)

def filter_even(numbers):
    """짝수만 필터링"""
    for num in numbers:
        if num % 2 == 0:
            yield num

def square_numbers(numbers):
    """제곱 계산"""
    for num in numbers:
        yield num ** 2

# 파이프라인 연결 (메모리 효율적)
# pipeline = square_numbers(filter_even(read_numbers("numbers.txt")))
# total = sum(pipeline)

설명

이것이 하는 일: 위 코드는 제너레이터를 정의하고 사용하는 다양한 패턴을 보여주며, 메모리 효율적인 데이터 처리 방법을 담고 있습니다. 첫 번째로, count_up은 가장 기본적인 제너레이터입니다.

return 대신 yield를 사용하면 함수가 제너레이터가 됩니다. yield count를 만나면 count 값을 반환하고 함수 실행이 중단됩니다.

다음 반복에서 next()가 호출되면 중단된 지점(count += 1) 바로 다음부터 재개됩니다. for 루프는 자동으로 next()를 호출하고 StopIteration 예외를 처리합니다.

그 다음으로, read_large_file은 실전에서 가장 많이 쓰이는 패턴입니다. 파일 전체를 메모리에 올리지 않고 한 줄씩 읽어서 yield합니다.

1GB 파일도 메모리는 한 줄 크기만 사용합니다. with 문이 제너레이터 안에 있어도 안전하게 파일이 닫힙니다.

세 번째로, fibonacci는 무한 시퀀스를 생성합니다. while True 루프가 있지만 yield 덕분에 실제로 무한 루프에 빠지지 않습니다.

필요한 만큼만 next()로 가져오면 되므로, 피보나치의 첫 10개든 백만 개든 자유롭게 제어할 수 있습니다. 리스트로는 불가능한 일입니다.

제너레이터 표현식은 리스트 컴프리헨션과 비슷하지만 괄호 ()를 사용합니다. 백만 개의 제곱수를 리스트로 만들면 수백 MB가 필요하지만, 제너레이터는 몇 바이트만 사용합니다.

sum()은 한 번에 하나씩 값을 받아서 더하므로 메모리 걱정이 없습니다. 마지막으로, 데이터 파이프라인 예제는 제너레이터의 진가를 보여줍니다.

세 개의 제너레이터를 연결하여 파일 읽기 → 필터링 → 변환을 수행하는데, 모든 단계가 게으르게(lazily) 실행됩니다. 첫 번째 숫자가 읽히면 필터링되고 제곱되어 sum()에 전달되고, 그 다음 숫자가 처리되는 식입니다.

전체 데이터를 메모리에 올리지 않으므로 테라바이트 데이터도 처리할 수 있습니다. 여러분이 이 코드를 사용하면 메모리 사용을 극적으로 줄이고, 대용량 데이터를 처리하며, 스트리밍 파이프라인을 구축할 수 있습니다.

제너레이터는 Python의 가장 강력하고 우아한 기능 중 하나입니다.

실전 팁

💡 제너레이터는 한 번만 순회할 수 있습니다. 두 번 순회하려면 제너레이터 함수를 다시 호출해야 합니다.

💡 itertools 모듈을 활용하세요. itertools.islice(generator, 100)로 처음 100개만 가져오거나, itertools.chain(gen1, gen2)로 여러 제너레이터를 연결할 수 있습니다.

💡 제너레이터는 디버깅이 어려울 수 있습니다. 복잡한 파이프라인은 중간 결과를 리스트로 변환하여 확인하세요: list(filter_even(read_numbers("test.txt"))).

💡 send()로 제너레이터와 양방향 통신할 수 있습니다: value = yield로 받아서 동적으로 동작을 바꿀 수 있습니다.

💡 yield from을 사용하면 제너레이터를 위임할 수 있습니다: yield from other_generator()는 다른 제너레이터의 모든 값을 자동으로 전달합니다.


#Python#Functions#Classes#Decorators#ContextManager

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.