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 없이 - 예외 발생 시 close() 가 실행 안 될 수 있음 💥
f = open("file.txt", "r")
data = f.read() # 여기서 예외 발생하면?
f.close() # 실행 안 됨 → 파일이 열린 채로 남음!
# with 사용 - 예외 발생해도 반드시 close() 실행 ✅
with open("file.txt", "r") as f:
data = f.read()
내부적으로는 __enter__, __exit__ 라는 특수 메서드로 동작합니다.
# with 는 사실 이것과 동일
f = open("file.txt", "r")
f.__enter__()
try:
data = f.read()
finally:
f.__exit__() # 항상 실행 → close()
__init__ 처럼 __ 로 감싸진 Python의 특수 메서드(dunder method) 중 하나입니다.
매개변수에 기본값을 지정하면, 호출 시 해당 인자를 생략할 수 있습니다.
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!
함수 호출 시 매개변수명=값 형태로 전달하면, 순서와 무관하게 인자를 넘길 수 있습니다.
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)는 반드시 키워드 인자보다 앞에 와야 합니다.
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은 자연스럽게 처리됩니다.
# 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: 위치 인자를 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)로 자주 쓰입니다.
어떤 함수든 인자를 그대로 받아 전달할 수 있기 때문입니다.
이름 없이 한 줄로 정의하는 익명 함수입니다.
Java의 람다식과 유사하며, 주로 sorted() 의 key 인자처럼 간단한 함수가 필요할 때 사용합니다.
# 기본 문법
lambda 매개변수: 표현식
# 예시
add = lambda a, b: a + b
add(3, 4) # 7
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를 기준으로 오름차순 정렬합니다.
큰 수에-가 붙어 작아지므로 앞으로 오게 됩니다.
반환값은 원본 값 그대로입니다.
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 # 할당 시도 → 지역변수로 간주 → 에러 💥
count = 0
def increment():
global count # 전역 변수 count를 사용하겠다고 선언
count += 1
increment()
increment()
print(count) # 2
global 이 전역 변수라면, nonlocal 은 바로 바깥 함수의 변수에 접근할 때 사용합니다.
def outer():
count = 0
def inner():
nonlocal count # 바깥 함수의 count를 사용
count += 1
inner()
inner()
print(count) # 2
outer()
| 키워드 | 접근 범위 | Java 유사 개념 |
|---|---|---|
| 없음 | 지역 변수 | 메서드 내 지역 변수 |
global | 전역 변수 | static 변수 |
nonlocal | 바깥 함수 변수 | 없음 (Java는 중첩 함수 없음) |
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() # 빈 객체 생성
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 없이 - 예외와 무관한 코드가 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}")
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 를 단독으로 쓰면 현재 예외를 그대로 다시 던집니다.
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) 와 동일한 개념입니다.
Java의 import 와 유사하지만, Python은 모듈(파일) 단위로 import합니다.
# Java - 클래스 단위
import java.util.ArrayList;
# Python - 모듈 단위
import math
math.sqrt(16) # 4.0 - 모듈명.함수명 으로 접근
math.pi # 3.14159...
from math import sqrt, pi
sqrt(16) # 4.0 - 모듈명 없이 바로 사용
pi # 3.14159...
# 전부 가져오기 - 권장하지 않음 ⚠️
from math import *
# 어디서 온 함수인지 불분명해져 가독성이 떨어짐
import numpy as np # 긴 이름을 줄여서 사용
from math import sqrt as sq # 이름 충돌 방지
np.array([1, 2, 3])
sq(16) # 4.0
# 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 - 프로젝트 루트 기준 (권장 ✅)
from utils.math_utils import multiply
# 상대 import - 현재 파일 위치 기준
from .math_utils import multiply # 같은 패키지 내
from ..main import something # 상위 패키지
| 절대 import | 상대 import | |
|---|---|---|
| 기준 | 프로젝트 루트 | 현재 파일 위치 |
| 가독성 | 어디서 오는지 명확 | 경로가 짧음 |
| 권장 | ✅ 일반적으로 권장 | 패키지 내부에서만 사용 |
# 내장 모듈 - 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 |
json | JSON 파싱/직렬화 | 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() 메서드와 비슷한 역할로,
"이 파일이 직접 실행될 때만 이 코드를 실행해라" 는 진입점 역할을 합니다.
함수를 인자로 받아 기능을 추가한 새로운 함수를 반환하는 문법입니다.
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)입니다.
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
decorator는 어떤 함수에도 붙을 수 있어야 합니다.
*args, **kwargs 로 인자를 그대로 받아서 그대로 전달하면,
인자 형태가 어떻든 상관없이 범용적으로 동작합니다.
| Spring AOP | Python Decorator | |
|---|---|---|
| 방식 | 어노테이션 + 프레임워크 처리 | 언어 자체에서 지원 |
| 구현 | 프레임워크 의존 | 직접 구현 가능 |
일반 함수는 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 - 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 핵심 개념 정리 완료!