hobokai 님의 블로그

Python 완전 정복 가이드 - 2편: 중급편 본문

Language

Python 완전 정복 가이드 - 2편: 중급편

hobokai 2025. 8. 4. 10:34

Python 완전 정복 가이드 - 2편: 중급편

목차

  1. 객체지향 프로그래밍
  2. 예외 처리
  3. 파일 입출력
  4. 정규표현식
  5. 컴프리헨션
  6. 제너레이터와 이터레이터
  7. 데코레이터

객체지향 프로그래밍

클래스와 객체

# 기본 클래스 정의
class Person:
    # 클래스 변수 (모든 인스턴스가 공유)
    species = "Homo sapiens"

    # 생성자 메서드
    def __init__(self, name, age):
        # 인스턴스 변수
        self.name = name
        self.age = age

    # 인스턴스 메서드
    def introduce(self):
        return f"안녕하세요, 저는 {self.name}이고 {self.age}세입니다."

    # 인스턴스 메서드
    def have_birthday(self):
        self.age += 1
        print(f"{self.name}님이 {self.age}세가 되었습니다!")

# 객체 생성과 사용
person1 = Person("Alice", 25)
person2 = Person("Bob", 30)

print(person1.introduce())  # 안녕하세요, 저는 Alice이고 25세입니다.
person1.have_birthday()     # Alice님이 26세가 되었습니다!

# 클래스 변수 접근
print(Person.species)       # Homo sapiens
print(person1.species)      # Homo sapiens

메서드의 종류

class MathUtils:
    pi = 3.14159

    def __init__(self, name):
        self.name = name

    # 인스턴스 메서드
    def instance_method(self):
        return f"인스턴스 메서드: {self.name}"

    # 클래스 메서드
    @classmethod
    def class_method(cls):
        return f"클래스 메서드: {cls.pi}"

    # 정적 메서드
    @staticmethod
    def static_method(x, y):
        return x + y

# 사용 예시
math_util = MathUtils("계산기")
print(math_util.instance_method())      # 인스턴스 메서드: 계산기
print(MathUtils.class_method())         # 클래스 메서드: 3.14159
print(MathUtils.static_method(5, 3))    # 8

상속 (Inheritance)

# 부모 클래스
class Animal:
    def __init__(self, name, species):
        self.name = name
        self.species = species

    def make_sound(self):
        return "동물이 소리를 냅니다"

    def info(self):
        return f"{self.name}은(는) {self.species}입니다"

# 자식 클래스
class Dog(Animal):
    def __init__(self, name, breed):
        super().__init__(name, "개")  # 부모 클래스 생성자 호출
        self.breed = breed

    # 메서드 오버라이딩
    def make_sound(self):
        return "멍멍!"

    # 새로운 메서드 추가
    def fetch(self):
        return f"{self.name}이(가) 공을 가져옵니다"

class Cat(Animal):
    def __init__(self, name, color):
        super().__init__(name, "고양이")
        self.color = color

    def make_sound(self):
        return "야옹!"

    def climb_tree(self):
        return f"{self.name}이(가) 나무에 올라갑니다"

# 사용 예시
dog = Dog("멍멍이", "골든 리트리버")
cat = Cat("나비", "검은색")

print(dog.info())        # 멍멍이은(는) 개입니다
print(dog.make_sound())  # 멍멍!
print(dog.fetch())       # 멍멍이이(가) 공을 가져옵니다

print(cat.info())        # 나비은(는) 고양이입니다
print(cat.make_sound())  # 야옹!
print(cat.climb_tree())  # 나비이(가) 나무에 올라갑니다

다중 상속

class Flyable:
    def fly(self):
        return "날고 있습니다"

class Swimmable:
    def swim(self):
        return "수영하고 있습니다"

class Duck(Animal, Flyable, Swimmable):
    def __init__(self, name):
        super().__init__(name, "오리")

    def make_sound(self):
        return "꽥꽥!"

# 사용 예시
duck = Duck("도날드")
print(duck.info())       # 도날드은(는) 오리입니다
print(duck.make_sound()) # 꽥꽥!
print(duck.fly())        # 날고 있습니다
print(duck.swim())       # 수영하고 있습니다

특수 메서드 (Magic Methods)

class Book:
    def __init__(self, title, author, pages):
        self.title = title
        self.author = author
        self.pages = pages

    # 문자열 표현
    def __str__(self):
        return f"'{self.title}' by {self.author}"

    def __repr__(self):
        return f"Book('{self.title}', '{self.author}', {self.pages})"

    # 길이
    def __len__(self):
        return self.pages

    # 비교 연산자
    def __eq__(self, other):
        return self.pages == other.pages

    def __lt__(self, other):
        return self.pages < other.pages

    # 덧셈 연산자
    def __add__(self, other):
        total_pages = self.pages + other.pages
        return f"합쳐진 책 ({total_pages} 페이지)"

# 사용 예시
book1 = Book("Python Guide", "Alice", 300)
book2 = Book("Data Science", "Bob", 250)

print(str(book1))           # 'Python Guide' by Alice
print(repr(book1))          # Book('Python Guide', 'Alice', 300)
print(len(book1))           # 300
print(book1 == book2)       # False
print(book1 > book2)        # True
print(book1 + book2)        # 합쳐진 책 (550 페이지)

프로퍼티 (Property)

class Circle:
    def __init__(self, radius):
        self._radius = radius  # 보호된 변수 (관례)

    @property
    def radius(self):
        return self._radius

    @radius.setter
    def radius(self, value):
        if value < 0:
            raise ValueError("반지름은 음수가 될 수 없습니다")
        self._radius = value

    @property
    def area(self):
        return 3.14159 * self._radius ** 2

    @property
    def circumference(self):
        return 2 * 3.14159 * self._radius

# 사용 예시
circle = Circle(5)
print(circle.radius)        # 5
print(circle.area)          # 78.53975
print(circle.circumference) # 31.4159

circle.radius = 3
print(circle.area)          # 28.27431

# circle.radius = -1  # ValueError 발생

예외 처리

기본 예외 처리

# try-except 기본 구조
try:
    number = int(input("숫자를 입력하세요: "))
    result = 10 / number
    print(f"결과: {result}")
except ValueError:
    print("잘못된 숫자 형식입니다")
except ZeroDivisionError:
    print("0으로 나눌 수 없습니다")
except Exception as e:
    print(f"예상치 못한 오류: {e}")

다양한 예외 처리 패턴

# 여러 예외를 한 번에 처리
try:
    # 위험한 코드
    pass
except (ValueError, TypeError) as e:
    print(f"값 또는 타입 오류: {e}")

# else와 finally
try:
    file = open("data.txt", "r")
    data = file.read()
except FileNotFoundError:
    print("파일을 찾을 수 없습니다")
else:
    print("파일을 성공적으로 읽었습니다")
    print(f"데이터 길이: {len(data)}")
finally:
    print("정리 작업을 수행합니다")
    if 'file' in locals():
        file.close()

사용자 정의 예외

class CustomError(Exception):
    """사용자 정의 예외"""
    pass

class AgeError(Exception):
    """나이 관련 오류"""
    def __init__(self, age, message="나이가 유효하지 않습니다"):
        self.age = age
        self.message = message
        super().__init__(self.message)

def check_age(age):
    if age < 0:
        raise AgeError(age, "나이는 음수가 될 수 없습니다")
    elif age > 150:
        raise AgeError(age, "나이가 너무 많습니다")
    else:
        print(f"유효한 나이: {age}")

# 사용 예시
try:
    check_age(-5)
except AgeError as e:
    print(f"오류: {e.message}, 입력된 나이: {e.age}")

예외 발생시키기

def divide(a, b):
    if b == 0:
        raise ValueError("두 번째 인수는 0이 될 수 없습니다")
    return a / b

# assert 문 사용
def factorial(n):
    assert n >= 0, "n은 0 이상이어야 합니다"
    assert isinstance(n, int), "n은 정수여야 합니다"

    if n <= 1:
        return 1
    return n * factorial(n - 1)

파일 입출력

기본 파일 읽기/쓰기

# 파일 쓰기
with open("example.txt", "w", encoding="utf-8") as file:
    file.write("안녕하세요, Python!\n")
    file.write("파일 입출력 연습입니다.\n")

# 파일 읽기
with open("example.txt", "r", encoding="utf-8") as file:
    content = file.read()
    print(content)

# 줄 단위로 읽기
with open("example.txt", "r", encoding="utf-8") as file:
    lines = file.readlines()
    for i, line in enumerate(lines, 1):
        print(f"줄 {i}: {line.strip()}")

# 한 줄씩 처리 (메모리 효율적)
with open("example.txt", "r", encoding="utf-8") as file:
    for line in file:
        print(line.strip())

다양한 파일 모드

# 파일 모드
# "r" - 읽기 (기본값)
# "w" - 쓰기 (기존 내용 삭제)
# "a" - 추가 (파일 끝에 추가)
# "x" - 배타적 생성 (파일이 이미 존재하면 실패)
# "b" - 바이너리 모드
# "t" - 텍스트 모드 (기본값)

# 추가 모드
with open("log.txt", "a", encoding="utf-8") as file:
    file.write("새로운 로그 항목\n")

# 바이너리 파일 처리
with open("image.jpg", "rb") as file:
    binary_data = file.read()
    print(f"파일 크기: {len(binary_data)} 바이트")

CSV 파일 처리

import csv

# CSV 파일 쓰기
data = [
    ["이름", "나이", "도시"],
    ["Alice", 25, "서울"],
    ["Bob", 30, "부산"],
    ["Charlie", 35, "대구"]
]

with open("people.csv", "w", newline="", encoding="utf-8") as file:
    writer = csv.writer(file)
    writer.writerows(data)

# CSV 파일 읽기
with open("people.csv", "r", encoding="utf-8") as file:
    reader = csv.reader(file)
    for row in reader:
        print(row)

# 딕셔너리 형태로 CSV 처리
with open("people.csv", "r", encoding="utf-8") as file:
    reader = csv.DictReader(file)
    for row in reader:
        print(f"이름: {row['이름']}, 나이: {row['나이']}, 도시: {row['도시']}")

JSON 파일 처리

import json

# JSON 데이터
data = {
    "name": "Alice",
    "age": 30,
    "hobbies": ["독서", "영화감상", "등산"],
    "address": {
        "city": "서울",
        "zipcode": "12345"
    }
}

# JSON 파일 쓰기
with open("data.json", "w", encoding="utf-8") as file:
    json.dump(data, file, ensure_ascii=False, indent=2)

# JSON 파일 읽기
with open("data.json", "r", encoding="utf-8") as file:
    loaded_data = json.load(file)
    print(loaded_data)
    print(f"이름: {loaded_data['name']}")
    print(f"취미: {', '.join(loaded_data['hobbies'])}")

정규표현식

기본 정규표현식

import re

# 기본 패턴 매칭
text = "전화번호: 010-1234-5678"
pattern = r"010-\d{4}-\d{4}"

if re.search(pattern, text):
    print("전화번호 패턴을 찾았습니다")

# 모든 매치 찾기
text = "이메일: alice@example.com, bob@test.org"
pattern = r"\w+@\w+\.\w+"
emails = re.findall(pattern, text)
print(emails)  # ['alice@example.com', 'bob@test.org']

정규표현식 메서드들

import re

text = "Python 3.9.0 was released on 2020-10-05"

# search: 첫 번째 매치
match = re.search(r"\d+\.\d+\.\d+", text)
if match:
    print(f"버전: {match.group()}")  # 3.9.0

# findall: 모든 매치
numbers = re.findall(r"\d+", text)
print(numbers)  # ['3', '9', '0', '2020', '10', '05']

# split: 패턴으로 분할
text = "apple,banana;cherry:grape"
fruits = re.split(r"[,;:]", text)
print(fruits)  # ['apple', 'banana', 'cherry', 'grape']

# sub: 치환
text = "전화번호: 010-1234-5678"
masked = re.sub(r"\d{4}-\d{4}", "****-****", text)
print(masked)  # 전화번호: 010-****-****

그룹과 캡처

import re

# 그룹 사용
text = "생년월일: 1990-05-15"
pattern = r"(\d{4})-(\d{2})-(\d{2})"
match = re.search(pattern, text)

if match:
    print(f"전체: {match.group(0)}")  # 1990-05-15
    print(f"년도: {match.group(1)}")  # 1990
    print(f"월: {match.group(2)}")    # 05
    print(f"일: {match.group(3)}")    # 15

# 이름이 있는 그룹
pattern = r"(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})"
match = re.search(pattern, text)

if match:
    print(f"년도: {match.group('year')}")
    print(f"월: {match.group('month')}")
    print(f"일: {match.group('day')}")

유용한 정규표현식 패턴

import re

# 이메일 검증
def validate_email(email):
    pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
    return bool(re.match(pattern, email))

# 전화번호 추출
def extract_phone_numbers(text):
    pattern = r"01[0-9]-\d{3,4}-\d{4}"
    return re.findall(pattern, text)

# URL 추출
def extract_urls(text):
    pattern = r"https?://[^\s]+"
    return re.findall(pattern, text)

# 한글만 추출
def extract_korean(text):
    pattern = r"[가-힣]+"
    return re.findall(pattern, text)

# 사용 예시
print(validate_email("user@example.com"))  # True
print(extract_phone_numbers("연락처: 010-1234-5678, 011-9876-5432"))
print(extract_urls("방문하세요: https://www.python.org"))
print(extract_korean("Hello 안녕하세요 World 세계"))

컴프리헨션

리스트 컴프리헨션

# 기본 리스트 컴프리헨션
numbers = [1, 2, 3, 4, 5]
squares = [x**2 for x in numbers]
print(squares)  # [1, 4, 9, 16, 25]

# 조건부 리스트 컴프리헨션
even_squares = [x**2 for x in numbers if x % 2 == 0]
print(even_squares)  # [4, 16]

# 중첩 반복문
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flattened = [num for row in matrix for num in row]
print(flattened)  # [1, 2, 3, 4, 5, 6, 7, 8, 9]

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

딕셔너리 컴프리헨션

# 기본 딕셔너리 컴프리헨션
numbers = [1, 2, 3, 4, 5]
square_dict = {x: x**2 for x in numbers}
print(square_dict)  # {1: 1, 2: 4, 3: 9, 4: 16, 5: 25}

# 조건부 딕셔너리 컴프리헨션
even_squares = {x: x**2 for x in numbers if x % 2 == 0}
print(even_squares)  # {2: 4, 4: 16}

# 키-값 변환
original = {"a": 1, "b": 2, "c": 3}
swapped = {v: k for k, v in original.items()}
print(swapped)  # {1: 'a', 2: 'b', 3: 'c'}

집합 컴프리헨션

# 집합 컴프리헨션
numbers = [1, 2, 2, 3, 3, 4, 5]
unique_squares = {x**2 for x in numbers}
print(unique_squares)  # {1, 4, 9, 16, 25}

# 조건부 집합 컴프리헨션
text = "Hello World"
unique_chars = {char.lower() for char in text if char.isalpha()}
print(unique_chars)  # {'h', 'e', 'l', 'o', 'w', 'r', 'd'}

제너레이터 표현식

# 제너레이터 표현식 (메모리 효율적)
numbers = (x**2 for x in range(10))
print(type(numbers))  # <class 'generator'>

# 지연 평가 - 필요할 때만 계산
for square in numbers:
    print(square)

# 큰 데이터셋에 유용
sum_of_squares = sum(x**2 for x in range(1000000))
print(sum_of_squares)

제너레이터와 이터레이터

제너레이터 함수

# 기본 제너레이터 함수
def count_up_to(n):
    count = 1
    while count <= n:
        yield count
        count += 1

# 사용법
counter = count_up_to(5)
for num in counter:
    print(num)  # 1, 2, 3, 4, 5

# 제너레이터는 한 번만 사용 가능
counter = count_up_to(3)
print(list(counter))  # [1, 2, 3]
print(list(counter))  # [] (빈 리스트)

피보나치 수열 제너레이터

def fibonacci():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

# 무한 제너레이터 사용
fib = fibonacci()
fibonacci_numbers = []
for _ in range(10):
    fibonacci_numbers.append(next(fib))

print(fibonacci_numbers)  # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

이터레이터 프로토콜

class CountDown:
    def __init__(self, start):
        self.start = start

    def __iter__(self):
        return self

    def __next__(self):
        if self.start <= 0:
            raise StopIteration
        self.start -= 1
        return self.start + 1

# 사용법
countdown = CountDown(5)
for num in countdown:
    print(num)  # 5, 4, 3, 2, 1

itertools 모듈

import itertools

# 무한 이터레이터
# count
counter = itertools.count(10, 2)  # 10부터 2씩 증가
first_five = [next(counter) for _ in range(5)]
print(first_five)  # [10, 12, 14, 16, 18]

# cycle
colors = itertools.cycle(['red', 'green', 'blue'])
color_list = [next(colors) for _ in range(7)]
print(color_list)  # ['red', 'green', 'blue', 'red', 'green', 'blue', 'red']

# repeat
repeated = list(itertools.repeat('hello', 3))
print(repeated)  # ['hello', 'hello', 'hello']

# 조합 이터레이터
# permutations (순열)
letters = ['A', 'B', 'C']
perms = list(itertools.permutations(letters, 2))
print(perms)  # [('A', 'B'), ('A', 'C'), ('B', 'A'), ('B', 'C'), ('C', 'A'), ('C', 'B')]

# combinations (조합)
combs = list(itertools.combinations(letters, 2))
print(combs)  # [('A', 'B'), ('A', 'C'), ('B', 'C')]

# product (데카르트 곱)
colors = ['red', 'blue']
sizes = ['S', 'M', 'L']
products = list(itertools.product(colors, sizes))
print(products)  # [('red', 'S'), ('red', 'M'), ('red', 'L'), ('blue', 'S'), ('blue', 'M'), ('blue', 'L')]

데코레이터

기본 데코레이터

# 함수 데코레이터
def my_decorator(func):
    def wrapper():
        print("함수 실행 전")
        func()
        print("함수 실행 후")
    return wrapper

@my_decorator
def say_hello():
    print("안녕하세요!")

# 사용
say_hello()
# 출력:
# 함수 실행 전
# 안녕하세요!
# 함수 실행 후

인수가 있는 함수를 위한 데코레이터

import functools

def my_decorator(func):
    @functools.wraps(func)  # 원본 함수의 메타데이터 보존
    def wrapper(*args, **kwargs):
        print(f"함수 {func.__name__} 실행 전")
        result = func(*args, **kwargs)
        print(f"함수 {func.__name__} 실행 후")
        return result
    return wrapper

@my_decorator
def add(a, b):
    """두 수를 더합니다"""
    return a + b

result = add(3, 5)
print(f"결과: {result}")
print(f"함수 이름: {add.__name__}")
print(f"함수 문서: {add.__doc__}")

실용적인 데코레이터 예시

import time
import functools

# 실행 시간 측정 데코레이터
def timer(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        end_time = time.time()
        print(f"{func.__name__} 실행 시간: {end_time - start_time:.4f}초")
        return result
    return wrapper

# 재시도 데코레이터
def retry(max_attempts=3):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(max_attempts):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    print(f"시도 {attempt + 1} 실패: {e}")
                    if attempt == max_attempts - 1:
                        raise
            return None
        return wrapper
    return decorator

# 로깅 데코레이터
def log_calls(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print(f"함수 {func.__name__} 호출됨")
        print(f"인수: args={args}, kwargs={kwargs}")
        result = func(*args, **kwargs)
        print(f"반환값: {result}")
        return result
    return wrapper

# 사용 예시
@timer
@log_calls
def calculate_sum(n):
    return sum(range(n))

@retry(max_attempts=3)
def unreliable_function():
    import random
    if random.random() < 0.7:
        raise Exception("임의 오류 발생")
    return "성공!"

# 테스트
result = calculate_sum(1000000)
# try:
#     result = unreliable_function()
#     print(result)
# except Exception as e:
#     print(f"최종 실패: {e}")

클래스 데코레이터

# 싱글톤 패턴 데코레이터
def singleton(cls):
    instances = {}
    def get_instance(*args, **kwargs):
        if cls not in instances:
            instances[cls] = cls(*args, **kwargs)
        return instances[cls]
    return get_instance

@singleton
class Database:
    def __init__(self):
        print("데이터베이스 연결 생성")
        self.connection = "DB 연결"

# 사용
db1 = Database()  # 데이터베이스 연결 생성
db2 = Database()  # 새로운 연결 생성되지 않음
print(db1 is db2)  # True (같은 인스턴스)

마무리

이번 2편에서는 Python의 중급 개념들을 살펴봤습니다:

주요 내용 요약

  • 객체지향 프로그래밍 (클래스, 상속, 특수 메서드)
  • 예외 처리와 사용자 정의 예외
  • 파일 입출력 (텍스트, CSV, JSON)
  • 정규표현식을 이용한 패턴 매칭
  • 컴프리헨션을 통한 간결한 코드 작성
  • 제너레이터와 이터레이터
  • 데코레이터를 이용한 코드 확장

다음 편 예고

3편 - 고급편에서는 다음 내용을 다룰 예정입니다:

  • 메타클래스와 동적 클래스 생성
  • 컨텍스트 매니저
  • 멀티스레딩과 멀티프로세싱
  • 비동기 프로그래밍 (async/await)
  • 패키지 생성과 배포
  • 성능 최적화 기법

연습 과제

  1. 도서 관리 시스템 (클래스, 상속, 예외 처리)
  2. 로그 파일 분석기 (파일 입출력, 정규표현식)
  3. 데이터 처리 파이프라인 (제너레이터, 데코레이터)

Python의 중급 기능들을 마스터하여 더욱 강력한 프로그램을 만들어보세요!