플로라도의 data workout

파이썬 클래스 메서드(classmethod)와 정적 메서드(staticmethod) 본문

기초 노트/Python

파이썬 클래스 메서드(classmethod)와 정적 메서드(staticmethod)

플로라도 2024. 4. 22. 21:08

기본적으로 파이썬 클래스의 메서드들은 첫번째 인자로 'self' 매개변수를 포함시키는게 일반적이다.
'self'는 메서드가 호출될 때 해당 인스턴스 자체를 자동으로 참조하는 역할을 한다.
이를 통해 메서드 내에서 해당 인스턴스의 속성(attribute)이나 다른 메서드에 대해 접근할 수 있다.
 
그런데 이와는 달리,  첫번째 인자로 'self'가 아닌 'cls'를 받는 메서드가 존재한다.
바로 클래스 메서드(clasmethod)이다.
클래스 메서드의 경우, 'self'가 아닌 'cls'를 필수적으로 포함해야 한다.
 

1. 클래스 메서드

- 클래스 메서드는 메서드 선언시에 @classmethod의 데코레이터를 달아주어 정의하게 된다.
- 클래스에서 직접 호출하는 점 때문에 인스턴스 객체를 수정할 수 없다.
 
클래스 메서드는 다음과 같은 상황에서 사용할 수 있다.
 
 
 

1-1. 인스턴스 변수가 아닌 클래스 변수를 공통적으로 제어하기 위해

다음의 예제에서는 클래스 메서드를 사용하여 모든 인스턴스에 공통적으로 적용될 클래스 변수를 수정한다.
Product 클래스에는 tax_rate가 클래스 변수로서 0.05로 선언되어 있고
이후 update_tax_rate 클래스 메서드를 통해 모든 인스턴스의 클래스 변수를 공통적으로 제어할 수 있다.

class Product:
    tax_rate = 0.05  # 기본 세율

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

    @classmethod
    def update_tax_rate(cls, new_rate):
        cls.tax_rate = new_rate  # 클래스 변수 업데이트

    def final_price(self):
        return self.price + (self.price * self.tax_rate)

# 사용 예
product1 = Product(1000)
product2 = Product(2000)
print(product1.final_price())  # 초기 세율로 계산: 1050.0

Product.update_tax_rate(0.1)  # 모든 인스턴스에 적용될 새 세율 설정
print(product1.final_price())  # 업데이트된 세율로 계산: 1100.0
print(product2.final_price())  # 업데이트된 세율로 계산: 2200.0

 
이러한 패턴은 특정 속성을 모든 인스턴스가 공유하고, 한꺼번에 적용할 필요가 있을 때 유용하게 쓰일 수 있다.
 
 

1-2. 생성자를 래핑(wrapping)하기 위해

클래스 메서드는 특정 객체를 여러 입력의 형태로 생성하게 될때도 요긴하게 쓰일 수 있다. 클래스 메서드가 아니더라도 별도의 로직을 사용할 수 있지만 클래스 메서드를 통해 간단히 구현할 수 있다.

class Date:
    def __init__(self, day, month, year):
        self.day = day
        self.month = month
        self.year = year

    @classmethod
    def from_string(cls, date_as_string):
        day, month, year = map(int, date_as_string.split('-'))
        return cls(day, month, year)  # 새 인스턴스 생성

    def display(self):
        print(f"{self.day}/{self.month}/{self.year}")

# 사용 예
date1 = Date.from_string('25-12-2021')
date1.display()  # 출력: 25/12/2021

 
'Date' 클래스는 생성시 'day, month, year'를 입력으로 받을 수 있지만, 'from_string'의 클래스 메서드를 통해서 'string'형태의 타입으로 생성할 수 있다.
 
만약 클래스메서드를 사용하지 않는다면 아래의 코드로 동일한 기능을 구현할 수 있다.
 

class Date:
    def __init__(self, day, month, year):
        self.day = day
        self.month = month
        self.year = year

    def display(self):
        print(f"{self.day}/{self.month}/{self.year}")

def create_date_from_string(date_as_string):
    day, month, year = map(int, date_as_string.split('-'))
    return Date(day, month, year)

# 팩토리 함수를 사용하여 Date 객체 생성
date1 = create_date_from_string('25-12-2021')
date1.display()  # 출력: 25/12/2021

 
별도로 정의된 'create_date_from_string' 함수는 입력받는 파라미터를 split하고 이를 Date클래스의 생성자에 input으로 넘기고, 리턴하면서 인스턴스를 생성한다.
앞선 코드와 동일한 기능을 수행한다.
 
그러나 코드의 가독성 측면에서 클래스 메서드가 좀 더 유리할 것이다.
 
 

1- 3. 다양한 형식의 데이터 처리를 위한 팩토리 패턴

팩토리 메서드는 객체지향프로그래밍(Object-Oriented-Programming)에서 객체 생성을 위한 디자인 패턴중 하나이다.
이 패턴의 주된 목적은 객체 생성 과정을 서브 클래스에게 위임함으로써 시스템의 유연성을 향상시키고, 코드의 불필요한 의존성을 없애는 것이다.
 
여기서 '공장(Factory)'이 바로 객체 생성을하는 클래스이고, 서브 공장인 클래스의 메서드에서 '제품'에 해당하는 객체들을 생성을 도맡기는 것이다.
 

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    @classmethod
    def from_csv(cls, csv_string):
        name, age = csv_string.split(',')
        return cls(name, int(age))

    @classmethod
    def from_dict(cls, data_dict):
        return cls(data_dict['name'], data_dict['age'])

    def display(self):
        print(f"Name: {self.name}, Age: {self.age}")

# CSV 형식의 데이터로부터 Person 객체 생성
person_csv = Person.from_csv("John Doe,30")
person_csv.display()  # 출력: Name: John Doe, Age: 30

# 딕셔너리 형식의 데이터로부터 Person 객체 생성
person_dict = Person.from_dict({'name': 'Alice Smith', 'age': 28})
person_dict.display()  # 출력: Name: Alice Smith, Age: 28

 

그러나 이러한 정적인 팩토리 패턴은 조금만 생각해보면,

2번 예시인 생성자를 래핑하는 기능과 크게 다르지 않다.

각 클래스메서드 마다 클래스의 타입이 결정되는 것이 아니라, 다양한 형태의 입력으로부터 클래스가 생성되는 동일한 기능을 제공한다.

 

 

이와 달리, 전통적인 팩토리 패턴은 메서드마다 클래스의 타입이 동적으로 결정되며 각각의 메서드는 캡슐화되어 있는 방식이다.

 

 

 

class Vehicle:
    def __init__(self, vehicle_type):
        self.vehicle_type = vehicle_type

    def display(self):
        print(f"This is a {self.vehicle_type}")

class Car(Vehicle):
    def __init__(self):
        super().__init__('Car')

class Truck(Vehicle):
    def __init__(self):
        super().__init__('Truck')

class VehicleFactory:
    @classmethod
    def create_vehicle(cls, vehicle_type):
        if vehicle_type == 'car':
            return Car()
        elif vehicle_type == 'truck':
            return Truck()
        else:
            raise ValueError("Unknown vehicle type")

# 팩토리 메서드를 사용한 객체 생성
car = VehicleFactory.create_vehicle('car')
truck = VehicleFactory.create_vehicle('truck')

car.display()  # 출력: This is a Car
truck.display()  # 출력: This is a Truck


 
 

4. 클래스 변수에 기반한 특수한 로직 적용

class Server:
    maintenance_mode = False  # 서버 유지보수 상태

    @classmethod
    def set_maintenance_mode(cls, mode):
        cls.maintenance_mode = mode
        print("Maintenance mode set to", "ON" if mode else "OFF")

    @classmethod
    def check_status(cls):
        if cls.maintenance_mode:
            print("Server is currently under maintenance.")
        else:
            print("Server is running normally.")

# 사용 예
Server.check_status()  # 서버 상태 확인
Server.set_maintenance_mode(True)  # 유지보수 모드 활성화
Server.check_status()  # 다시 상태 확인

 
'set_maintenance_mode'를 통해 모드전환을 하는 예시이다. 클래스 변수를 통해 위와 같이 모드 전환을 할 수 있다.
 
이 외에 모드전환은 파이토치에서 흔히 사용하는 코드인 model.eval(), model.train()에서 자주 볼 수 있다.
하지만 model.eval(), model.train()이 직접적인 클래스 메서드는 아니다.
eval()과 train()은 통상 nn.Module을 상속받은 클래스의 인스턴스인 model의 인스턴스 메서드이고, 
모든 서브클래스(NeuralNet)의 인스턴스(model)에 동일하게 적용되기 때문에 위의 예시와 비슷한 형식을 띄게 된다.
그러나 각 model 인스턴스는 통합적인 관리가 아닌, 독립적으로 자신의 training상태를 관리한다.
 
파이토치 모델의 eval과 train같은 경우 각 개별 모델의 eval과 train의 상태를 독립적으로 관리하기 때문에 인스턴스 레벨의 메서드로 관리하지만, 클래스 레벨에서 관리하는 것이 유리할 경우 위와같이 클래스 메서드를 활용하여 모드관리를 할 수 있다.
 
 

2. 정적 메서드

 
정적메서드(static method)는 클래스나 객체의 상태를 수정하지 않는 유틸리티 함수나 도우미(helper)함수로 주로 사용된다.  '@staticmethod' 데코레이터를 선언함으로써 정의되며, 이 메서드는 첫번째 인자로 'self'나 'cls'를 받지 않는다. 이는 즉, 정적 메서드가 어떠한 클래스나 인스턴스의 어떠한 속성에도 접근하지 않음을 의미한다. 정적 메서드는 주로 클래스나 인스턴스의 상태에 의존하지 않는 작업을 수행할때 유용하게 사용된다.
 
 

class Calc:

    count = 10 # 클래스 변수(클래스 속성)

    @staticmethod
    def add(a):
        print(a + Calc.count) # 클래스 속성에는 엑세스가 가능하다.

    @staticmethod
    def mul(a):
        print(a * Calc.count)

Calc.add(15) # 결과값 :25 
Calc.mul(15) # 결과값 : 150

 
 
 


Reference)
https://journeytosth.tistory.com/73
https://whatisand.github.io/python-staticmethod-classmethod/