메서드 종류
Python 클래스에는 세 가지 종류의 메서드가 있습니다: 인스턴스 메서드, 클래스 메서드(@classmethod), 정적 메서드(@staticmethod). 각각의 목적과 사용 시기를 명확히 이해하면 더 깔끔하고 유지보수하기 쉬운 코드를 작성할 수 있습니다.
인스턴스 메서드 (Instance Method)
인스턴스 메서드는 가장 일반적인 메서드입니다. 첫 번째 매개변수로 self를 받아 인스턴스의 속성에 접근하거나 수정할 수 있습니다.
class Circle:
PI = 3.14159265358979
def __init__(self, radius: float):
self.radius = radius
# 인스턴스 메서드: self를 통해 인스턴스 속성에 접근
def area(self) -> float:
return self.PI * self.radius ** 2
def circumference(self) -> float:
return 2 * self.PI * self.radius
def scale(self, factor: float) -> None:
"""인스턴스 상태를 수정하는 메서드"""
self.radius *= factor
def __repr__(self) -> str:
return f"Circle(radius={self.radius})"
c = Circle(5)
print(f"넓이: {c.area():.2f}") # 넓이: 78.54
print(f"둘레: {c.circumference():.2f}") # 둘레: 31.42
c.scale(2)
print(c) # Circle(radius=10)
@classmethod: 클래스 메서드
클래스 메서드는 @classmethod 데코레이터를 사용하고, 첫 번째 매개변수로 self 대신 cls(클래스 자체)를 받습니다. 인스턴스가 아닌 클래스 레벨의 작업을 수행할 때 사용합니다.
class Date:
def __init__(self, year: int, month: int, day: int):
self.year = year
self.month = month
self.day = day
@classmethod
def from_string(cls, date_string: str) -> "Date":
"""문자열 'YYYY-MM-DD'에서 Date 객체 생성 — 대안 생성자"""
year, month, day = map(int, date_string.split("-"))
return cls(year, month, day)
@classmethod
def from_timestamp(cls, timestamp: float) -> "Date":
"""Unix 타임스탬프에서 Date 객체 생성"""
import time
t = time.gmtime(timestamp)
return cls(t.tm_year, t.tm_mon, t.tm_mday)
@classmethod
def today(cls) -> "Date":
"""오늘 날짜로 Date 객체 생성"""
from datetime import date
d = date.today()
return cls(d.year, d.month, d.day)
def is_leap_year(self) -> bool:
return (self.year % 4 == 0 and self.year % 100 != 0) or (self.year % 400 == 0)
def __repr__(self) -> str:
return f"Date({self.year}-{self.month:02d}-{self.day:02d})"
# 다양한 방식으로 객체 생성
d1 = Date(2024, 3, 15)
d2 = Date.from_string("2024-06-20")
d3 = Date.today()
print(d1) # Date(2024-03-15)
print(d2) # Date(2024-06-20)
print(d3) # Date(2026-03-16) (오늘 날짜)
print(d1.is_leap_year()) # True
클래스 메서드와 상속
클래스 메서드는 상속 시 cls가 자동으로 서브클래스를 가리켜 올바른 타입의 객체를 생성합니다.
class Animal:
def __init__(self, name: str, sound: str):
self.name = name
self.sound = sound
@classmethod
def create_silent(cls, name: str) -> "Animal":
return cls(name, "...") # cls는 호출된 클래스
def speak(self) -> str:
return f"{self.name}: {self.sound}"
class Dog(Animal):
def fetch(self) -> str:
return f"{self.name}이(가) 공을 가져옵니다!"
# Dog.create_silent()는 Dog 인스턴스를 반환
silent_dog = Dog.create_silent("벙어리 개")
print(type(silent_dog)) # <class '__main__.Dog'>
print(silent_dog.speak()) # 벙어리 개: ...
print(silent_dog.fetch()) # 벙어리 개이(가) 공을 가져옵니다!
@staticmethod: 정적 메서드
정적 메서드는 @staticmethod 데코레이터를 사용하며, self나 cls를 받지 않습니다. 클래스나 인스턴스의 상태와 무관하지만 논리적으로 클래스에 속하는 유틸리티 함수에 사용합니다.
class MathUtils:
@staticmethod
def is_prime(n: int) -> bool:
"""소수 여부 확인 — 클래스/인스턴스 상태 불필요"""
if n < 2:
return False
for i in range(2, int(n ** 0.5) + 1):
if n % i == 0:
return False
return True
@staticmethod
def factorial(n: int) -> int:
"""팩토리얼 계산"""
if n < 0:
raise ValueError("음수의 팩토리얼은 정의되지 않습니다.")
result = 1
for i in range(2, n + 1):
result *= i
return result
@staticmethod
def gcd(a: int, b: int) -> int:
"""최대 공약수 계산 (유클리드 알고리즘)"""
while b:
a, b = b, a % b
return a
# 인스턴스 없이 직접 호출 가능
print(MathUtils.is_prime(17)) # True
print(MathUtils.factorial(5)) # 120
print(MathUtils.gcd(48, 18)) # 6
# 인스턴스를 통해서도 호출 가능 (권장하지 않음)
mu = MathUtils()
print(mu.is_prime(13)) # True
세 가지 메서드 선택 기준
| 메서드 종류 | 첫 번째 매개변수 | 인스턴스 상태 | 클래스 상태 | 사용 시기 |
|---|---|---|---|---|
| 인스턴스 메서드 | self | 접근/수정 가능 | 접근/수정 가능 | 인스턴스 데이터를 다룰 때 |
| 클래스 메서드 | cls | 불가 | 접근/수정 가능 | 대안 생성자, 클래스 상태 관리 |
| 정적 메서드 | 없음 | 불가 | 불가 | 유틸리티 함수, 순수 계산 |
class Temperature:
"""온도 변환 클래스 — 세 가지 메서드 타입 모두 활용"""
_conversion_count = 0 # 변환 횟수 추적
def __init__(self, celsius: float):
self._celsius = celsius
# 인스턴스 메서드: 인스턴스 상태(self._celsius)에 의존
def to_fahrenheit(self) -> float:
Temperature._conversion_count += 1
return self._celsius * 9/5 + 32
def to_kelvin(self) -> float:
Temperature._conversion_count += 1
return self._celsius + 273.15
# 클래스 메서드: 클래스 상태에 접근
@classmethod
def get_conversion_count(cls) -> int:
return cls._conversion_count
@classmethod
def from_fahrenheit(cls, fahrenheit: float) -> "Temperature":
"""대안 생성자: 화씨에서 온도 객체 생성"""
celsius = (fahrenheit - 32) * 5/9
return cls(celsius)
@classmethod
def from_kelvin(cls, kelvin: float) -> "Temperature":
"""대안 생성자: 켈빈에서 온도 객체 생성"""
return cls(kelvin - 273.15)
# 정적 메서드: 클래스/인스턴스와 무관한 순수 계산
@staticmethod
def celsius_to_fahrenheit(celsius: float) -> float:
"""순수 변환 함수 — 상태 불필요"""
return celsius * 9/5 + 32
@staticmethod
def is_valid_celsius(celsius: float) -> bool:
"""절대 영도 이상인지 확인"""
return celsius >= -273.15
def __repr__(self) -> str:
return f"Temperature({self._celsius}°C)"
# 다양한 사용 패턴
t1 = Temperature(100)
print(t1.to_fahrenheit()) # 212.0
print(t1.to_kelvin()) # 373.15
t2 = Temperature.from_fahrenheit(98.6)
print(t2) # Temperature(37.0°C)
t3 = Temperature.from_kelvin(0)
print(t3) # Temperature(-273.15°C)
# 정적 메서드: 인스턴스 없이 직접 사용
print(Temperature.celsius_to_fahrenheit(25)) # 77.0
print(Temperature.is_valid_celsius(-300)) # False
# 클래스 메서드: 변환 횟수 확인
print(Temperature.get_conversion_count()) # 2
실전 예제: 날짜 파서
from datetime import datetime
from typing import Optional
import re
class DateParser:
"""다양한 형식의 날짜 문자열을 파싱하는 클래스"""
SUPPORTED_FORMATS = [
"%Y-%m-%d",
"%Y/%m/%d",
"%d-%m-%Y",
"%d/%m/%Y",
"%Y%m%d",
"%B %d, %Y", # January 15, 2024
"%b %d, %Y", # Jan 15, 2024
]
def __init__(self, date: datetime):
self._date = date
@classmethod
def parse(cls, date_str: str) -> "DateParser":
"""다양한 형식의 날짜 문자열 파싱"""
date_str = date_str.strip()
for fmt in cls.SUPPORTED_FORMATS:
try:
dt = datetime.strptime(date_str, fmt)
return cls(dt)
except ValueError:
continue
raise ValueError(f"지원하지 않는 날짜 형식: {date_str!r}")
@classmethod
def from_iso(cls, iso_str: str) -> "DateParser":
"""ISO 8601 형식 파싱"""
dt = datetime.fromisoformat(iso_str)
return cls(dt)
@classmethod
def now(cls) -> "DateParser":
"""현재 시각으로 생성"""
return cls(datetime.now())
# 정적 메서드: 순수 유틸리티
@staticmethod
def is_valid_date(year: int, month: int, day: int) -> bool:
"""주어진 날짜가 유효한지 확인"""
try:
datetime(year, month, day)
return True
except ValueError:
return False
@staticmethod
def guess_format(date_str: str) -> Optional[str]:
"""날짜 문자열의 형식 추측"""
patterns = {
r"\d{4}-\d{2}-\d{2}$": "YYYY-MM-DD",
r"\d{4}/\d{2}/\d{2}$": "YYYY/MM/DD",
r"\d{8}$": "YYYYMMDD",
}
for pattern, fmt_name in patterns.items():
if re.match(pattern, date_str.strip()):
return fmt_name
return None
# 인스턴스 메서드: 인스턴스 상태에 의존
def format(self, fmt: str = "%Y년 %m월 %d일") -> str:
return self._date.strftime(fmt)
def days_until(self, other: "DateParser") -> int:
return (other._date - self._date).days
def is_weekend(self) -> bool:
return self._date.weekday() >= 5
def __repr__(self) -> str:
return f"DateParser({self._date.isoformat()})"
# 다양한 형식 파싱
d1 = DateParser.parse("2024-03-15")
d2 = DateParser.parse("15/03/2024")
d3 = DateParser.parse("March 15, 2024")
d4 = DateParser.now()
print(d1.format()) # 2024년 03월 15일
print(d2.format("%Y/%m/%d")) # 2024/03/15
print(d3.is_weekend()) # False (금요일)
print(DateParser.is_valid_date(2024, 2, 29)) # True (2024년은 윤년)
print(DateParser.is_valid_date(2023, 2, 29)) # False
print(DateParser.guess_format("2024-03-15")) # YYYY-MM-DD
실전 예제: 단위 변환 클래스
class UnitConverter:
"""단위 변환 유틸리티 클래스"""
# 클래스 변수: 변환 비율 테이블
_length_to_meter = {
"mm": 0.001,
"cm": 0.01,
"m": 1.0,
"km": 1000.0,
"inch": 0.0254,
"foot": 0.3048,
"yard": 0.9144,
"mile": 1609.344,
}
_weight_to_kg = {
"mg": 0.000001,
"g": 0.001,
"kg": 1.0,
"t": 1000.0,
"oz": 0.028350,
"lb": 0.453592,
}
def __init__(self, value: float, unit: str):
self._value = value
self._unit = unit.lower()
@classmethod
def from_length(cls, value: float, unit: str) -> "UnitConverter":
if unit.lower() not in cls._length_to_meter:
raise ValueError(f"알 수 없는 길이 단위: {unit}")
return cls(value, unit)
@classmethod
def from_weight(cls, value: float, unit: str) -> "UnitConverter":
if unit.lower() not in cls._weight_to_kg:
raise ValueError(f"알 수 없는 무게 단위: {unit}")
return cls(value, unit)
@staticmethod
def list_length_units() -> list[str]:
return list(UnitConverter._length_to_meter.keys())
@staticmethod
def list_weight_units() -> list[str]:
return list(UnitConverter._weight_to_kg.keys())
def to_length(self, target_unit: str) -> float:
if self._unit not in self._length_to_meter:
raise ValueError(f"{self._unit}은 길이 단위가 아닙니다.")
target_unit = target_unit.lower()
if target_unit not in self._length_to_meter:
raise ValueError(f"알 수 없는 목표 단위: {target_unit}")
# 현재 단위 → 미터 → 목표 단위
in_meters = self._value * self._length_to_meter[self._unit]
return in_meters / self._length_to_meter[target_unit]
def to_weight(self, target_unit: str) -> float:
if self._unit not in self._weight_to_kg:
raise ValueError(f"{self._unit}은 무게 단위가 아닙니다.")
target_unit = target_unit.lower()
if target_unit not in self._weight_to_kg:
raise ValueError(f"알 수 없는 목표 단위: {target_unit}")
in_kg = self._value * self._weight_to_kg[self._unit]
return in_kg / self._weight_to_kg[target_unit]
def __repr__(self) -> str:
return f"UnitConverter({self._value} {self._unit})"
# 사용 예시
length = UnitConverter.from_length(100, "cm")
print(f"100 cm = {length.to_length('m')} m") # 1.0 m
print(f"100 cm = {length.to_length('inch'):.4f} in") # 39.3701 in
print(f"100 cm = {length.to_length('foot'):.4f} ft") # 3.2808 ft
weight = UnitConverter.from_weight(70, "kg")
print(f"70 kg = {weight.to_weight('lb'):.2f} lb") # 154.32 lb
print("지원 길이 단위:", UnitConverter.list_length_units())
고수 팁
1. 클래스 메서드를 활용한 레지스트리 패턴
class Serializer:
_registry: dict[str, type] = {}
def __init_subclass__(cls, format_name: str = "", **kwargs):
super().__init_subclass__(**kwargs)
if format_name:
Serializer._registry[format_name] = cls
@classmethod
def get(cls, format_name: str) -> type:
if format_name not in cls._registry:
raise KeyError(f"등록되지 않은 포맷: {format_name}")
return cls._registry[format_name]
def serialize(self, data: dict) -> str:
raise NotImplementedError
class JSONSerializer(Serializer, format_name="json"):
def serialize(self, data: dict) -> str:
import json
return json.dumps(data, ensure_ascii=False)
class CSVSerializer(Serializer, format_name="csv"):
def serialize(self, data: dict) -> str:
return ",".join(f"{k}={v}" for k, v in data.items())
data = {"name": "파이썬", "version": "3.12"}
for fmt in ["json", "csv"]:
s = Serializer.get(fmt)()
print(f"{fmt}: {s.serialize(data)}")
2. @staticmethod는 클래스에서 독립시킬 수도 있지만 응집도를 위해 유지
# 나쁜 예: 모듈 레벨 함수를 무분별하게 정적 메서드로
class StringUtils:
@staticmethod
def reverse(s: str) -> str:
return s[::-1]
# 좋은 예: 클래스와 논리적 연관성이 있을 때만 정적 메서드 사용
class EmailValidator:
DOMAIN_BLACKLIST = {"example.com", "test.com"}
@staticmethod
def is_valid_format(email: str) -> bool:
import re
return bool(re.match(r"[^@]+@[^@]+\.[^@]+", email))
@classmethod
def is_allowed_domain(cls, email: str) -> bool:
domain = email.split("@")[-1].lower()
return domain not in cls.DOMAIN_BLACKLIST
def validate(self, email: str) -> tuple[bool, str]:
if not self.is_valid_format(email):
return False, "잘못된 이메일 형식"
if not self.is_allowed_domain(email):
return False, "허용되지 않는 도메인"
return True, "유효한 이메일"
정리
- 인스턴스 메서드:
self로 인스턴스 상태 접근 — 대부분의 경우 - 클래스 메서드:
cls로 클래스 레벨 작업 — 대안 생성자, 팩토리 패턴 - 정적 메서드: 클래스/인스턴스와 무관한 유틸리티 — 순수 계산, 헬퍼 함수
메서드 선택의 핵심: "이 메서드가 인스턴스 상태에 의존하는가?" → Yes면 인스턴스 메서드, "클래스 상태에 의존하거나 클래스를 반환하는가?" → Yes면 클래스 메서드, "둘 다 아닌가?" → 정적 메서드.