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
데코레이터까지 써가며 클래스 내부에 메서드를 정의할까? 몇가지 이유가 있다.
- 클래스의 그 어떤 객체를 사용하지 않지만, 클래스가 사용될 때만 사용되는 함수이기 때문이다.
- Bound Method 를 구체화 할 필요 없이 코드를 실행하기 때문에 메모리가 적게 든다.
- 인스턴스가 정의되지 않은 상황에도 메서드를 호출할 수 있다.
첫 예시를 보면 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()
라는 명령을 이해하면 왜 이런 인자 오류가 뜨는지 이해할 수 있다. tester
는 Static
을 통해 만들어진 클래스 객체이기 때문에 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()
가 훨씬 직관적이다.