Object-Oriented Programming - Classmethods and Staticmethods

Contents

  • Class Methods
    • Usecase 1: Modifying Class Variables
    • Usecase 2: Alternative Constructors
  • Static Methods

Class Methods

class Student:

    reduce_health = 0.9

    def __init__(self, first, last, health):
        self.first = first
        self.last = last
        self.health = health
        self.email = first + '.' + last + '@korea.ac.kr'

    def fullname(self):
        return f'{self.first} {self.last}'

    def allnighter(self):
        self.health = self.health * Student.reduce_health
        return self.health

stu_1 = Student('Tom', 'Lee', 100)
stu_2 = Student('Ian', 'Goodfellow', 100)

Student 라는 클래스를 통해 두명의 학생 인스턴스 stu_1, stu_2 를 생성했다. fullname() 라는 메서드로 학생들의 이름을 출력할 수 있고, allnighter() 라는 메서드는 학생의 체력인 instance variable self.health 와 class variable reduce_health 를 곱해 밤샘 후 학생의 체력을 리턴해준다.

객체지향적 코드를 짜는 궁극적 이유는 reusable 한 청사진을 사용자에게 제공해서 언제든지 사용자가 조작할 수 있는 모듈을 짜는 것이다. 그렇기에 객체지향적 패턴들을 공부 할 때 ‘이 라이브러리를 배포한 후 사용자가 직접 모듈을 뜯어낼 필요 없이 사용자가 자유롭게 객체를 조작할 수 있을까?’ 를 항상 염두에 두어야 한다. 그렇다면 위 코드에 한가지 문제가 생긴다.

Class variable 인 reduce_health 는 밤샘 후 학생의 남은 체력을 계산할 때 사용되는 변수다. 하지만 만약 사용자가 정의된 reduce_health 의 값이 마음에 안든다면 어떡할까? 위 코드에선 class variable 을 마음대로 바꾸기 위한 entry point 가 없다. 이런 경우엔 @classmethod 를 사용하면 된다.

Usecase 1: Modifying Class Variables

class Student:

    reduce_health = 0.9

    def __init__(self, first, last, health):
        self.first = first
        self.last = last
        self.health = health
        self.email = first + '.' + last + '@korea.ac.kr'

    @classmethod
    def set_reduce_health(cls, amount):
        cls.reduce_health = amount

    def allnighter(self):
        self.health = self.health * Student.reduce_health
        return self.health

stu_1 = Student('Tom', 'Lee', 100)
stu_2 = Student('Ian', 'Goodfellow', 100)

>>> print(stu_1.health)
100
>>> print(stu_1.allnighter())
90

Student.set_reduce_health(0.8)
>>> print(stu_2.health)
100
>>> print(stu_2.allnighter())
80

allnighter(self) 와 같은 일반적인 메서드는 self, 즉 인스턴스를 인자로 받는다. 일반적인 메서드는 인스턴스를 조작하기 위한 함수인 것이다. 반면에 클래스 메서드는 클래스 자체를 조작하기 위한 메서드이기 때문에, 메서드 위에 @classmethod 라는 데코레이터를 추가해서 사용한다. set_reduce_health(cls, amount) 는 클래스 메서드이며, 첫번째 인자로 self 대신 cls 를 받는다. cls 는 클래스의 줄임말로, 해당 메서드는 클래스 자체를 조작하는 것을 알 수 있다.

사실 self, cls 도 결국 사용자 지정 인자이기 때문에 마음대로 정할 수 있다. 하지만 numpy 를 np 로 import 하는 것 처럼 통용되는 표현이다.

reduce_health = 0.9

@classmethod
def set_reduce_health(cls, amount):
  cls.reduce_health = amount

>>> print(stu_1.health)
100
>>> print(stu_1.allnighter())
90

Student.set_reduce_health(0.8)
>>> print(stu_2.health)
100
>>> print(stu_2.allnighter())
80

stu_1 인스턴스를 정의할 때 health 를 100 으로 설정했다. 이때 allnighter() 라는 메서드를 통해 stu_1 의 체력이 class variable 인 reduce_health = 0.9 만큼 곱해지며 체력이 90으로 내려간다.

그런데 사용자가 ‘밤샘을 하는데 체력이 그정도밖에 안깎여?’ 라는 의문을 품고 reduce_health 값을 0.8 로 줄이려 한다. 이때 set_reduce_health(cls) 를 통해 reduce_health 를 업데이트 한다. 클래스 메서드는 클래스 자체, 즉 cls 를 첫 인자로 받기 때문에 Student.set_reduce_health(0.8) 로 invoke 해야 한다.

Student.set_reduce_health(0.8) 말고도 stu_1.set_reduce_health(0.8) 로 사용할 수 있다.

왜일까? 클래스 메서드는 첫 인자로 cls 를 받는다고 했는데 stu_1.set 은 인스턴스를 첫 인자로 pass 하는 것임에도 코드는 작동한다. 이는 인스턴스가 애초에 클래스를 통해 생성되었기 때문에 이런 메서드를 invoke 할때 해당 클래스 역할을 하는 것이다. 하지만 pythonic 한 타이핑이 아니기 때문에 이를 지양하는것이 좋다.

Student.set_reduce_health(0.8) 을 통해 reduce_health 값이 0.8 로 바뀌었다. 그렇기에 stu_2.allnighter() 를 출력하면 체력이 100에서 0.8 을 곱한 만큼인 80으로 준 것을 볼 수 있다.

Key: 일반 메서드는 인스턴스를 조작할때 사용, 클래스 메서드는 클래스 자체를 조작할때 사용

Usecase 2: Alternative Constructors

위 예시에서 클래스 메서드로 class variable 을 조작할 수 있다는 것을 배웠다. 이외에도 대표적으로 클래스 메서드가 사용되는 경우는 Alternative Constructors 이다.

한가지 예시를 들어보자. 사용자는 웹에서 회원가입 하는 학생들의 정보를 관리하고 있다. 이 Student 클래스를 사용해 학생들을 DB 에 인스턴스로 저장하려고 하는데, 웹페이지에서는 학생들이 정보를 first-last-100 식으로 입력하도록 설정이 되어있다. 현재 코드에선 인스턴스를 생성하는 방식은 stu = Student(first,last,health) 인데 first-last-100 의 문자열을 그대로 넘길 수 없다. 하지만 클래스 메서드는 본질적으로 클래스를 조작하는 메서드이기 때문에, 앞선 예시처럼 class variable 이외에도 __init__() 으로 넘길 인자를 설정해 줄 수 있다.

class Student:

    def __init__(self, first, last, health):
        self.first = first
        self.last = last
        self.health = health
        self.email = first + '.' + last + '@korea.ac.kr'

    @classmethod
    def from_string(cls, stu_str):
        first, last, health = stu_str.split('-')
        return cls(first, last, health)

stu_str = 'Yosuha-Bengio-200'
stu_3 = Student.from_string(stu_str)

>>> print(stu_3.email)
Yoshua.Bengio@korea.ac.kr

stu_str = 'Yosuha-Bengio-200' 라는 문자열로 새로운 인스턴스를 만들기 위해서, 먼저 from_string(cls, stu_str) 이라는 클래스 메서드를 정의했다. 해당 메서드는 Student 클래스와 문자열을 인자로 받고, - 을 기준으로 문자열을 Student 인스턴스를 만들기 위한 Attribute 들인 first, last, health 로 파싱해준다. 그리고 cls(first, last, health) 를 리턴해주는데, 이렇게 클래스 자체를 invoke 한다.

클래스를 invoke 하면 자동적으로 __init__ 메서드가 실행되기 때문에, 해당 클래스 메서드로부터 받은 값들인 first, last, health__init__ 에 넘긴다. 이때 Student.from_string(stu_str) 을 실행하면 stu_3 이라는 새로운 인스턴스가 생성되는 것이다.

이렇게 클래스 메서드를 통해 새로운 인스턴스를 만드는 과정을 Alternative Constructor Method 라고도 한다. __init__ 메서드를 통해 인스턴스를 생성하는 것이 아니라 다른 방식으로 인스턴스를 생성하기 때문에 Alternative 라고 불리는 것이다.

이런 Alternative Constructor 로 사용되는 클래스 메서드명은 from 을 붙히는게 일반적이다.

Static Methods

클래스 메서드는 클래스를 직접 조작할 때 쓰이는 메서드라면, 정적 메소드 (Static method) 는 반대로 클래스의 그 어떤 것도 사용하지 않는 메서드다.

import datetime
class Student:

    def __init__(self, first, last, health):
        self.first = first
        self.last = last
        self.health = health
        self.email = first + '.' + last + '@korea.ac.kr'

    @staticmethod
    def is_schoolday(day):
        if day.weekday() == 5 or day.weekday() == 6:
            return False
        return True

my_date = datetime.date(2022, 8, 7)

>>> print(Student.is_schoolday(my_date))
False

클래스 메서드와 마찬가지로 @staticmethod 를 통해 메서드를 정적 메서드로 정의할 수 있다. 정적 메서드는 일반 메서드와 클래스 메서드와 달리, self, cls 등 클래스와 관련된 그 어떤 Attribute 도 인자로 받지 않는다.

위 예시에서 is_schoolday(day) 라는 정적 메서드를 정의했다. 이 메서드는 datetime.date 모듈의 날짜 object 를 인자로 받아, 해당 날이 주중이면 True, 주말이면 False 를 반환해준다. 클래스 내부에 있기 때문에 Student.is_schoolday() 를 통해 선언해야 하지만, 클래스의 그 어떤 객체도 사용하지 않기에 왜 필요한지 의아할 수 있다.

import datetime
def is_schoolday(day):
    if day.weekday() == 5 or day.weekday() == 6:
        return False
    return True

my_date = datetime.date(2022, 8, 7)

>>> print(is_schoolday(my_date))
False

위 함수는 정적 메서드 예시와 완전히 같은 결과물을 리턴한다. 그렇다면 왜 굳이 @staticmethod 데코레이터까지 써가며 클래스 내부에 메서드를 정의할까? 몇가지 이유가 있다.

  1. 클래스의 그 어떤 객체를 사용하지 않지만, 클래스가 사용될 때만 사용되는 함수이기 때문이다.
  2. Bound Method 를 구체화 할 필요 없이 코드를 실행하기 때문에 메모리가 적게 든다.
  3. 인스턴스가 정의되지 않은 상황에도 메서드를 호출할 수 있다.

첫 예시를 보면 3번 이유처럼 인스턴스를 정의하지 않았음에도 불구하고 메서드가 호출되는 것을 볼 수 있다.

조금 더 직관적인 예시를 들어보자.

class Static:
    def method_one(self):
        return 'method one called'

    def method_two():
        return 'method two called'

tester = Static()

>>> print(tester.method_one())
method one called
>>> print(tester.method_two())
TypeError: method_two() takes 0 positional arguments but 1 was given

위 코드를 실행시키면 두번째 출력문에서 TypeError: method_two() takes 0 positional arguments but 1 was given 에러가 뜨는 것을 볼 수 있다. tester.method_two() 라는 명령을 이해하면 왜 이런 인자 오류가 뜨는지 이해할 수 있다. testerStatic 을 통해 만들어진 클래스 객체이기 때문에 method_two() 라는 메서드를 호출 시 자기 자신을 인자로 넣기 때문이다.

tester.method_two() == Static.method_two(tester) 이 둘은 동일한 코드이기 때문에 에러가 뜨는 것이다.

class Static:
    def method_one(self):
        return 'method one called'

    @staticmethod
    def method_two():
        return 'method two called'

tester = Static()
>>> print(tester.method_one())
method one called
>>> print(tester.method_two())
method two called

이렇게 @staticmethod 데코레이터를 추가하면 인스턴스를 통해 메서드를 호출할 수 있다.

결국 @staticmethod 는 큰 의미에서 볼 때 사용자가 메서드를 호출하는 방식의 통일화를 위해 존재한다. 사용자는 두개의 메서드가 있다면, 자연스럽게 각 메서드를 인스턴스를 통해 호출하려고 할 것이다. 하지만 굳이 인스턴스 자체를 인자로 쓸 필요 없는 메서드를 인스턴스를 통해 호출하고 싶으면 반드시 @staticmethod 가 필요하다.

먄약 @staticmethod 데코레이터가 없었다면 Static.method_two() 를 통해 메서드를 호출 할 수 있겠지만, 사용자 입장에선 tester.method_two() 가 훨씬 직관적이다.