파이썬 (2)

최준병·6일 전

파이썬

목록 보기
2/2

목차

  1. with
  2. 함수
  3. global / nonlocal
  4. 클래스
  5. 예외처리
  6. raise
  7. import
  8. name
  9. Decorator
  10. Generator & yield

with

Java의 try-with-resources 와 완전히 동일한 개념입니다.
블록이 끝나면 예외가 발생하더라도 자원을 자동으로 해제합니다.

// Java
try (FileReader f = new FileReader("file.txt")) {
    // 블록 끝나면 자동으로 close()
}
# Python
with open("file.txt", "r") as f:
    # 블록 끝나면 자동으로 close()
    data = f.read()

with 없이 쓰면 위험한 이유

# with 없이 - 예외 발생 시 close() 가 실행 안 될 수 있음 💥
f = open("file.txt", "r")
data = f.read()   # 여기서 예외 발생하면?
f.close()         # 실행 안 됨 → 파일이 열린 채로 남음!

# with 사용 - 예외 발생해도 반드시 close() 실행 ✅
with open("file.txt", "r") as f:
    data = f.read()

동작 원리 - dunder method

내부적으로는 __enter__, __exit__ 라는 특수 메서드로 동작합니다.

# with 는 사실 이것과 동일
f = open("file.txt", "r")
f.__enter__()
try:
    data = f.read()
finally:
    f.__exit__()   # 항상 실행 → close()

__init__ 처럼 __ 로 감싸진 Python의 특수 메서드(dunder method) 중 하나입니다.


함수

Default Parameter (기본값 매개변수)

매개변수에 기본값을 지정하면, 호출 시 해당 인자를 생략할 수 있습니다.
Java에서는 오버로딩으로 해결해야 했지만, Python은 기본값으로 한 번에 처리합니다.

# Java - 오버로딩 필요
void greet(String name) { greet(name, "안녕하세요"); }
void greet(String name, String message) { ... }
# Python - 기본값으로 한 번에
def greet(name, message="안녕하세요"):
    print(f"{message}, {name}!")

greet("Alice")               # 안녕하세요, Alice!
greet("Bob", "반갑습니다")    # 반갑습니다, Bob!

Keyword Argument (키워드 인자)

함수 호출 시 매개변수명=값 형태로 전달하면, 순서와 무관하게 인자를 넘길 수 있습니다.
Java에는 없는 개념으로, 매개변수가 많을 때 코드 가독성이 크게 높아집니다.

def introduce(name, age, city):
    print(f"이름: {name}, 나이: {age}, 도시: {city}")

introduce("Alice", 30, "서울")                # 순서대로 전달
introduce(age=30, city="서울", name="Alice")  # 이름으로 전달 - 순서 무관
introduce("Alice", city="서울", age=30)       # 혼합 가능 (positional이 먼저)

혼합 사용 시 주의: 위치 인자(positional)는 반드시 키워드 인자보다 앞에 와야 합니다.


Multiple Return & Unpacking

Python 함수는 여러 값을 동시에 반환할 수 있습니다.
내부적으로는 값들을 tuple로 묶어서 반환하는 것입니다.

def min_max(numbers):
    return min(numbers), max(numbers)  # 내부적으로 (min, max) tuple 반환

result = min_max([3, 1, 4, 1, 5, 9])
print(result)        # (1, 9)
print(type(result))  # <class 'tuple'>

반환된 tuple을 각 변수에 바로 분해하는 것을 Unpacking 이라고 합니다.

a, b = min_max([3, 1, 4, 1, 5, 9])
print(a)  # 1
print(b)  # 9

Java였다면 별도 클래스나 배열로 감싸야 했지만, Python은 자연스럽게 처리됩니다.

Unpacking 활용

# swap - temp 변수 없이 한 줄로
a, b = b, a

# * 로 나머지를 한 번에 받기
first, *rest = [1, 2, 3, 4, 5]
print(first)  # 1
print(rest)   # [2, 3, 4, 5]

*head, last = [1, 2, 3, 4, 5]
print(head)   # [1, 2, 3, 4]
print(last)   # 5

*args, **kwargs

인자의 개수가 정해지지 않을 때 사용합니다.

  • *args: 위치 인자를 tuple 로 받음
  • **kwargs: 키워드 인자를 dict 로 받음
def func(*args, **kwargs):
    print(args)    # tuple
    print(kwargs)  # dict

func(1, 2, 3, name="Alice", age=30)
# (1, 2, 3)
# {'name': 'Alice', 'age': 30}

활용 예시

# 개수에 상관없이 합산
def my_sum(*args):
    return sum(args)

my_sum(1, 2, 3)        # 6
my_sum(1, 2, 3, 4, 5)  # 15
# dict를 ** 로 풀어서 함수 인자로 전달
options <= {"reverse": True, "key": lambda x: x}
sorted(numbers, **options)
# == sorted(numbers, reverse=True, key=lambda x: x)

Decorator를 만들 때 wrapper(*args, **kwargs) 로 자주 쓰입니다.
어떤 함수든 인자를 그대로 받아 전달할 수 있기 때문입니다.


lambda

이름 없이 한 줄로 정의하는 익명 함수입니다.
Java의 람다식과 유사하며, 주로 sorted()key 인자처럼 간단한 함수가 필요할 때 사용합니다.

# 기본 문법
lambda 매개변수: 표현식

# 예시
add = lambda a, b: a + b
add(3, 4)  # 7

sorted()의 key와 함께 사용

key 는 각 요소를 어떤 기준으로 비교할지 변환 함수를 넘기는 것입니다.
실제 값을 변환하는 게 아니라 정렬 기준만 바꿉니다.

words = ["banana", "apple", "kiwi"]

sorted(words)                          # 알파벳 순 → ["apple", "banana", "kiwi"]
sorted(words, key=lambda x: len(x))   # 길이 순 → ["kiwi", "apple", "banana"]
sorted(words, key=lambda x: x[-1])    # 마지막 글자 순 정렬
numbers = [3, 1, 4, 1, 5]

sorted(numbers)                      # 오름차순 → [1, 1, 3, 4, 5]
sorted(numbers, reverse=True)        # 내림차순 → [5, 4, 3, 1, 1]
sorted(numbers, key=lambda x: -x)   # 내림차순 (동일한 결과)

key=lambda x: -x-x 를 기준으로 오름차순 정렬합니다.
큰 수에 - 가 붙어 작아지므로 앞으로 오게 됩니다.
반환값은 원본 값 그대로입니다.


global / nonlocal

Python 함수 안에서 외부 변수에 값을 할당(=)하는 순간, 그 변수를 지역 변수로 간주합니다.
외부 변수를 수정하려면 global 또는 nonlocal 을 명시해야 합니다.

왜 에러가 발생할까?

count = 0

def increment():
    count += 1   # UnboundLocalError 발생 💥

# count += 1 은 count = count + 1 과 동일
# 오른쪽 count를 읽으려는데, 이미 지역변수로 간주해서
# "아직 할당 안 된 지역변수를 읽으려 했다!" → 에러

단순히 읽기만 할 때는 괜찮습니다.

count = 0

def print_count():
    print(count)   # 읽기만 함 → 전역변수 접근 가능 ✅

def increment():
    count += 1     # 할당 시도 → 지역변수로 간주 → 에러 💥

global - 전역 변수 접근

count = 0

def increment():
    global count   # 전역 변수 count를 사용하겠다고 선언
    count += 1

increment()
increment()
print(count)  # 2

nonlocal - 바깥 함수 변수 접근

global 이 전역 변수라면, nonlocal바로 바깥 함수의 변수에 접근할 때 사용합니다.

def outer():
    count = 0

    def inner():
        nonlocal count   # 바깥 함수의 count를 사용
        count += 1

    inner()
    inner()
    print(count)  # 2

outer()
키워드접근 범위Java 유사 개념
없음지역 변수메서드 내 지역 변수
global전역 변수static 변수
nonlocal바깥 함수 변수없음 (Java는 중첩 함수 없음)

클래스

self와 메서드

Python 클래스의 메서드는 반드시 첫 번째 매개변수로 self를 선언해야 합니다.
self는 Java의 this와 동일하게, 인스턴스 자기 자신을 가리킵니다.

class MyClass:
    def my_method(self):       # 클래스 메서드 - self 필요
        print(self)

def my_function():             # 클래스 밖 일반 함수 - self 불필요
    print("일반 함수")

self는 컨벤션일 뿐 다른 이름을 써도 동작하지만, 반드시 첫 번째 매개변수여야 합니다.
Python이 메서드를 호출할 때 인스턴스를 첫 번째 인자로 자동 전달하기 때문입니다.


생성자 __init__

Python의 생성자는 반드시 __init__ 이라는 이름을 사용합니다.
클래스를 ClassName() 형태로 호출하면 자동으로 실행됩니다.

Java와 가장 큰 차이는 별도의 필드 선언부가 없다는 점입니다.
__init__ 안에서 self.변수명 = 값 으로 쓰는 순간, 그 변수가 인스턴스 변수로 생성됩니다.

# Java
class Car {
    private String name;  // 필드 선언
    private String color;

    public Car(String name, String color) {
        this.name = name;
        this.color = color;
    }
}
# Python - 별도 선언 없이 __init__ 에서 바로 생성
class Car:
    def __init__(self, name, color):
        self.name = name    # 이 순간 인스턴스 변수 생성
        self.color = color

# new 없이 클래스 이름 호출 → __init__ 자동 실행
my_car = Car("Sonata", "White")

__init__ 이 없어도 클래스 선언은 가능합니다.

class MyClass:
    pass          # 빈 클래스

obj = MyClass()   # 빈 객체 생성

클래스 변수 vs 인스턴스 변수

class Car:
    count = 0        # 클래스 변수 - 모든 인스턴스가 공유 (Java의 static 변수)

    def __init__(self, name):
        Car.count += 1
        self.name = name   # 인스턴스 변수 - 각 인스턴스마다 독립적

car1 = Car("Sonata")
car2 = Car("Avante")

print(Car.count)    # 2 - 클래스 변수는 클래스명으로 접근
print(car1.name)    # "Sonata"
print(car2.name)    # "Avante"
클래스 변수인스턴스 변수
선언 위치클래스 바로 아래__init__ 안에서 self.변수명
공유 여부모든 인스턴스가 공유각 인스턴스마다 독립적
Java 유사 개념static 변수일반 인스턴스 변수

상속

class 자식클래스(부모클래스) 형태로 상속을 나타냅니다.
Java의 extends 와 동일한 역할입니다.

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

    def speak(self):
        return f"{self.name}이 소리를 냅니다"


class Dog(Animal):              # Animal 상속
    def speak(self):            # 메서드 오버라이딩
        return f"{self.name}이 짖습니다"


dog = Dog("멍멍이")
print(dog.speak())              # 멍멍이이 짖습니다
print(isinstance(dog, Animal))  # True - Java의 instanceof

super() 로 부모 클래스의 메서드를 호출할 수 있습니다. Java와 동일합니다.

class Dog(Animal):
    def speak(self):
        parent_result = super().speak()   # 부모의 speak() 호출
        return f"{self.name}이 짖습니다"

Python은 다중 상속도 지원합니다.

class MyClass:              # 상속 없음
    pass

class MyClass(ParentClass): # 단일 상속
    pass

class MyClass(A, B):        # 다중 상속 - Java에는 없는 기능
    pass

타입 힌트

Python은 동적 타이핑 언어라 타입 선언이 필요 없습니다.
하지만 코드 가독성과 IDE 자동완성 지원을 위해 타입 힌트를 사용할 수 있습니다.

# 변수 타입 힌트
age: int = 10
name: str = "jay"

# 함수 매개변수 + 반환 타입 힌트
def greet(name: str, age: int) -> str:
    return f"{name}{age}살입니다"

# 반환값 없을 때
def print_hello(name: str) -> None:
    print(f"안녕, {name}")

주의: 타입 힌트는 강제성이 없습니다.
age: int = 10 으로 선언해도 age = "열" 로 바꿔도 에러가 발생하지 않습니다.
단지 힌트일 뿐이며, 강제하려면 별도 라이브러리(예: pydantic)를 사용해야 합니다.


예외처리

Java의 try-catch-finally와 유사하지만, Python에는 else 블록이 추가됩니다.

try:
    result = 10 / 2          # 예외 가능성 있는 코드
except ZeroDivisionError:    # 특정 예외 처리
    print("0으로 나눌 수 없어요")
else:
    print(f"결과: {result}") # 예외가 발생하지 않았을 때만 실행
finally:
    print("항상 실행")        # 예외 여부와 관계없이 항상 실행

else 블록의 장점

else 블록 덕분에 "예외 가능성 있는 코드""성공했을 때 실행할 코드" 를 명확히 분리할 수 있습니다.

# else 없이 - 예외와 무관한 코드가 try 안에 섞임 ❌
try:
    result = 10 / 2
    print(f"결과: {result}")   # 이 코드는 예외 위험이 없는데 try 안에 있음
except ZeroDivisionError:
    print("0으로 나눌 수 없어요")

# else 사용 - 역할이 명확하게 분리됨 ✅
try:
    result = 10 / 2            # 예외 가능성 있는 코드만
except ZeroDivisionError:
    print("0으로 나눌 수 없어요")
else:
    print(f"결과: {result}")   # 성공했을 때 실행할 코드
finally:
    print("항상 실행")

여러 예외 처리

try:
    value = int(input())
    result = 10 / value
except ValueError:
    print("숫자를 입력해주세요")
except ZeroDivisionError:
    print("0은 입력할 수 없어요")
except Exception as e:        # 모든 예외를 잡는 catch-all
    print(f"알 수 없는 오류: {e}")

raise

Java의 throw 와 동일한 역할입니다.
예외를 강제로 발생시킬 때 사용합니다.

기본 사용법

# Java
throw new IllegalArgumentException("나이는 0 이상이어야 합니다");

# Python
raise ValueError("나이는 0 이상이어야 합니다")

new 키워드 없이 예외 클래스를 바로 호출하면 됩니다.

커스텀 예외

Java처럼 직접 예외 클래스를 만들 수 있습니다.

# Java
class AgeException extends RuntimeException {
    public AgeException(String message) {
        super(message);
    }
}
# Python
class AgeException(Exception):
    def __init__(self, age):
        self.age = age
        super().__init__(f"{age}는 유효하지 않은 나이입니다")

실전 활용 패턴

def set_age(age):
    if not isinstance(age, int):
        raise TypeError("나이는 정수여야 합니다")
    if age < 0:
        raise ValueError("나이는 0 이상이어야 합니다")
    return age

raise 단독 사용 - 예외 다시 던지기

raise 를 단독으로 쓰면 현재 예외를 그대로 다시 던집니다.
Java의 throw e 와 동일합니다.

try:
    set_age(-1)
except ValueError as e:
    print(f"로그 기록: {e}")
    raise   # 잡은 예외를 그대로 다시 던짐

예외 체이닝

Python에는 Java에 없는 예외 체이닝 문법도 있습니다.
원인 예외를 명시적으로 연결할 수 있습니다.

try:
    int("abc")
except ValueError as e:
    raise RuntimeError("변환 실패") from e

# RuntimeError: 변환 실패
#   caused by
# ValueError: invalid literal for int()...

Java의 new RuntimeException("변환 실패", e) 와 동일한 개념입니다.


import

Java의 import 와 유사하지만, Python은 모듈(파일) 단위로 import합니다.

기본 import

# Java - 클래스 단위
import java.util.ArrayList;

# Python - 모듈 단위
import math

math.sqrt(16)   # 4.0 - 모듈명.함수명 으로 접근
math.pi         # 3.14159...

from import - 특정 대상만 가져오기

from math import sqrt, pi

sqrt(16)   # 4.0 - 모듈명 없이 바로 사용
pi         # 3.14159...
# 전부 가져오기 - 권장하지 않음 ⚠️
from math import *
# 어디서 온 함수인지 불분명해져 가독성이 떨어짐

as - 별칭

import numpy as np              # 긴 이름을 줄여서 사용
from math import sqrt as sq     # 이름 충돌 방지

np.array([1, 2, 3])
sq(16)   # 4.0

모듈 vs 패키지

# Java
패키지 = com.example.project
클래스 = Calculator.java

# Python
모듈   = .py 파일
패키지 = 폴더 (+ __init__.py)
my_project/
├── main.py
├── calculator.py        # 모듈
└── utils/               # 패키지 (폴더)
    ├── __init__.py      # 이 파일이 있어야 패키지로 인식
    ├── string_utils.py
    └── math_utils.py
# 모듈 import
import calculator
from calculator import add

# 패키지 import
from utils.math_utils import multiply
from utils import string_utils

__init__.py

패키지 폴더 안에 있는 특수 파일입니다.
이 파일이 있어야 Python이 해당 폴더를 패키지로 인식합니다.

# utils/__init__.py 에 아래처럼 작성하면
from .string_utils import trim
from .math_utils import multiply

# 외부에서 이렇게 바로 접근 가능
from utils import trim, multiply

Python 3.3 부터는 __init__.py 없어도 패키지로 인식되지만,
명시적으로 두는 것이 관례입니다.

절대 import vs 상대 import

# 절대 import - 프로젝트 루트 기준 (권장 ✅)
from utils.math_utils import multiply

# 상대 import - 현재 파일 위치 기준
from .math_utils import multiply    # 같은 패키지 내
from ..main import something        # 상위 패키지
절대 import상대 import
기준프로젝트 루트현재 파일 위치
가독성어디서 오는지 명확경로가 짧음
권장✅ 일반적으로 권장패키지 내부에서만 사용

내장 모듈 vs 외부 라이브러리

# 내장 모듈 - Python 설치 시 기본 포함 (Java의 java.util.* 과 동일)
import math
import os
import sys
import json
import datetime

# 외부 라이브러리 - pip 로 설치 필요 (Java의 Maven/Gradle 과 동일)
# pip install numpy
import numpy as np

자주 쓰는 내장 모듈

모듈역할Java 유사
os파일/디렉토리 조작java.io.File
sys인터프리터 정보System
jsonJSON 파싱/직렬화ObjectMapper
datetime날짜/시간LocalDateTime
math수학 함수Math
random난수 생성Random
re정규표현식Pattern

__name__

__name__ 은 Python이 자동으로 관리하는 특수 변수(dunder variable) 입니다.
현재 파일이 직접 실행되는지, import 되는지에 따라 값이 달라집니다.

실행 방식__name__
python calculator.py 로 직접 실행"__main__"
import calculator 로 import"calculator" (모듈명)

왜 쓸까?

# calculator.py

def add(a, b):
    return a + b

# if __name__ 없이 - import 시에도 print 가 실행돼버림 💥
print(add(3, 4))
# calculator.py

def add(a, b):
    return a + b

# if __name__ 사용 - 직접 실행할 때만 실행 ✅
if __name__ == "__main__":
    print(add(3, 4))

Java의 main() 메서드와 비슷한 역할로,
"이 파일이 직접 실행될 때만 이 코드를 실행해라" 는 진입점 역할을 합니다.


Decorator

함수를 인자로 받아 기능을 추가한 새로운 함수를 반환하는 문법입니다.
Spring의 AOP(관점 지향 프로그래밍) 와 매우 유사한 개념입니다.

로깅, 실행 시간 측정, 권한 체크 등 핵심 로직과 부가 기능을 분리할 때 사용합니다.

def logger(func):
    def wrapper(*args, **kwargs):
        print(f"{func.__name__} 호출됨")   # 함수 실행 전
        result = func(*args, **kwargs)      # 원래 함수 실행
        print(f"{func.__name__} 종료됨")   # 함수 실행 후
        return result
    return wrapper

@logger
def add(a, b):
    return a + b

print(add(3, 4))
# add 호출됨
# add 종료됨
# 7

동작 원리

@logger 는 아래 코드와 완전히 동일합니다.

add = logger(add)
# add 함수를 logger로 감싸서 재할당
# 이후 add() 를 호출하면 실제로는 wrapper() 가 실행됨

즉, @ 문법은 "이 함수를 decorator로 감싸라" 는 문법적 설탕(syntactic sugar)입니다.

wrapper에서 *args, **kwargs를 쓰는 이유

def wrapper(*args, **kwargs):
    result = func(*args, **kwargs)

decorator는 어떤 함수에도 붙을 수 있어야 합니다.
*args, **kwargs 로 인자를 그대로 받아서 그대로 전달하면,
인자 형태가 어떻든 상관없이 범용적으로 동작합니다.

Spring AOPPython Decorator
방식어노테이션 + 프레임워크 처리언어 자체에서 지원
구현프레임워크 의존직접 구현 가능

Generator & yield

일반 함수는 return 으로 값을 반환하면 함수가 종료됩니다.
반면 yield 는 값을 반환한 뒤 함수 실행을 일시정지하고, 다음 호출 시 그 자리에서 재개합니다.

이런 함수를 Generator 함수라고 하며, 호출하면 Generator 객체를 반환합니다.

def countdown(n):
    while n > 0:
        yield n    # 값 반환 후 "일시정지"
        n -= 1     # next() 호출 시 여기서 재개

gen = countdown(3)   # Generator 객체 생성 (아직 실행 안 됨)

next(gen)   # 3 반환 → yield 지점에서 일시정지
next(gen)   # n -= 1 실행 → n=2 → 2 반환 → 다시 일시정지
next(gen)   # n -= 1 실행 → n=1 → 1 반환 → 다시 일시정지
next(gen)   # n=0 → while 조건 False → StopIteration 예외 발생

for 문으로도 자연스럽게 사용할 수 있습니다.

for num in countdown(3):
    print(num)
# 3
# 2
# 1

Java와 비교

// Java - Iterator 직접 구현
class Countdown implements Iterator<Integer> {
    private int n;
    Countdown(int n) { this.n = n; }
    public boolean hasNext() { return n > 0; }
    public Integer next() { return n--; }
}
# Python - yield 한 줄로 끝
def countdown(n):
    while n > 0:
        yield n
        n -= 1

메모리 효율

Generator의 핵심 장점은 메모리 효율입니다.
리스트는 모든 요소를 한 번에 메모리에 올리지만,
Generator는 next() 호출 시마다 값을 하나씩 계산합니다.

# 리스트 - 100만 개를 한 번에 메모리에 올림 💥
result = [x * 2 for x in range(1000000)]

# Generator - 필요할 때 하나씩 계산 ✅
result = (x * 2 for x in range(1000000))

List Comprehension의 []() 로 바꾸면 Generator가 됩니다.
데이터가 매우 크거나, 전체를 한 번에 쓰지 않을 때 유용합니다.


Java 개발자 관점에서 꼭 알아야 할 Python 핵심 개념 정리 완료!

profile
나의 기록

0개의 댓글