[Python] 클래스 메소드와 static 메소드

3 분 소요

클래스 변수

selif.i = 10 처럼 첫 대입 연산에서 생성되는 변수를 인스턴스 변수라고 한다. 인스턴스 변수는 각 객체별로 존재한다.

>>> class Simple:
        cv = 20
        def __init__(self):
            self.iv = 10

>>> Simple.cv
20
>>> s = Simple()
>>> s.cv
20

위 코드에서 cv는 클래스 변수이다. 클래스 변수는 클래스 이름으로도 접근이 가능하고 인스턴스 변수처럼 객체를 통해서도 접근이 가능하다.

class Simple:
    count = 0
    def __init__(self):
        Simple.count += 1
    def get_count(self):
        return Simple.count


def main():
    s1 = Simple()
    print(s1.get_count())
    s2 = Simple()
    print(s1.get_count())
    s3 = Simple()
    print(s1.get_count())


main()
1
2
3

위 코드는 클래스 변수의 특성 대부분을 보여주고 있다. 클래스 변수 count를 모든 객체가 s1을 통해서 get_count메소드를 호출한다. 어차피 이 메소드는 클래스 변수의 값을 반환하는 메소드이므로 어떤 객체를 대상으로 호출하건 결과는 같다.

static 메소드

바로 위에 코드에서 get_count메소드를 호출하기 위해서는 객체를 생성해야 한다. 하지만 get_count 메소드는 메소드 내에서 하는 일이 객체와 아무 관련이 없기 때문에 객체를 만들어 호출하는건 불필요하다. 이것을 해결하기 위해 static 메소드를 만들면 된다.

static 메소드는 클래스 변수와 상당히 유사하다. 클래스 변수와 마찬가지로 static 메소드는 객체에 속하지 않고 클래스에 속한다.

class Simple:
    # @staticmethod
    def sm():
        print('static method!')

    sm = staticmethod(sm)


def main():
    Simple.sm()
    s = Simple()
    s.sm()

main()
static method!
static method!

static 메소드는 첫번째 인자로 self가 없다. 그리고 sm = staticmethod(sm) 코드로 sm 메소드를 static 메소드로 만들 수 있다. static 메소드는 클래스 이름을 통해 호출이 가능하고 객체를 통해서도 호출이 가능하다.

class Simple:
    count = 0
    
    def __init__(self):
        Simple.count += 1
        
    @staticmethod
    def get_count():
        return Simple.count


def main():
    print(Simple.get_count())
    s = Simple()
    print(Simple.get_count())

main()
0
1

위 코드처럼 데코레이터 기반으로 static 메소드를 만들 수 있다. 이 방법이 권장된다.



class 메소드

class Simple:
    num = 5
    
    @staticmethod
    def sm(i):        
        print('st~ 5 + {0} = {1}'.format(i, Simple.num + i))

    @classmethod
    def cm(cls, i):
        print('cl~ 5 + {0} = {1}'.format(i, Simple.num + i))
        
def main():
    Simple.sm(3)
    Simple.cm(3)
    
    s = Simple()
    s.sm(4)
    s.cm(4)

main()
st~ 5 + 3 = 8
cl~ 5 + 3 = 8
st~ 5 + 4 = 9
cl~ 5 + 4 = 9

class 메소드는 매개변수 선언시 cls가 들어간다. 하지만 클래스 메소드를 호출할 때 cls 부분은 무시하고 넘어간다. 첫번째 매개변수 cls를 빼고 보면 static 메소드와 class 메소드는 동일하다. 여기서 cls에 자동으로 전달되는 무언가가 있다.

class Simple:
    count = 0

    def __init__(self):
        Simple.count += 1

    @classmethod
    def get_count(cls):  
        return cls.count  # -> return cls.count
        
def main():
    print(Simple.get_count())
    s = Simple()
    print(Simple.get_count())
    

main()
0
1

위 코드를 보면 cls에 전달되는 것은 Simple 클래스이다. 즉 클래스 메소드의 첫번째 인자로 전달되는 것은 이 메소드의 클래스이다. 파이썬에서는 클래스도 객체이기 때문에 인자로 전달 및 반환이 가능하다.

class Natural:
    def __init__(self, n):
        self.n = n

    def getn(self):
        return self.n
    

    @classmethod
    def add(cls, n1, n2):
        return cls(n1.getn() + n2.getn()) # return Natural(n1.getn()+n2.getn())
        
def main():
    n1 = Natural(1)
    n2 = Natural(2)
    n3 = Natural.add(n1, n2) # 이 경우 add의 cls에 전달되는 것은 Natural 클래스
    print('{0} + {1} = {2}'.format(n1.getn(), n2.getn(), n3.getn()))

main()
1 + 2 =3

cls에 전달되는 것은 Simple 클래스이므로 위 코드처럼 이를 기반으로 객체를 생성하는 것도 가능하다.



static 메소드보다 class 메소드가 더 어울리는 경우

class Date:
    def __init__(self, y, m, d):
        self.y = y
        self.m = m
        self.d = d

    def show(self):
        print('{0}, {1}, {2}'.format(self.y, self.m, self.d))
    

    @classmethod
    def next_day(cls, today):
        return cls(today.y, today.m, today.d + 1)
        
def main():
    d1 = Date(2025, 4, 5)
    d1.show()
    d2 = Date.next_day(d1)
    d2.show()

main()
2025, 4, 5
2025, 4, 6

위 코드에서 새로운 객체를 생성 및 반환하는 메소드를 팩토리메소드(factory method)라고 한다. class 메소드에는 클래스 정보가 전달되므로 이렇게 팩토리 메소드를 만드는데 매우 적합하다. 물론 위 코드에서 class메소드 대신 static 메소드를 사용해 구현이 가능하다.



static 메소드보다 class 메소드가 완전 더 어울리는 경우

class Date:
    def __init__(self, y, m, d):
        self.y = y
        self.m = m
        self.d = d

    def show(self):
        print('{0}, {1}, {2}'.format(self.y, self.m, self.d))
    

    @classmethod
    def next_day(cls, today):
        return cls(today.y, today.m, today.d + 1)

class KDate(Date):
    def show(self):
        print('KOR: {0}, {1}, {2}'.format(self.y, self.m, self.d))

class JDate(Date):
    def show(self):
        print('JPN: {0}, {1}, {2}'.format(self.y, self.m, self.d))


def main():
    kd1 = KDate(2025, 4, 12)
    kd1.show()
    kd2 = KDate.next_day(kd1) # next_day의 cls에 KDate 전달
    kd2.show()

    jd1 = JDate(2027, 5, 19)
    jd1.show()
    jd2 = JDate.next_day(jd1) # next_day의 cls에 JDate 전달
    jd2.show()
    
 
main()
KOR: 2025, 4, 12
KOR: 2025, 4, 13
JPN: 2027, 5, 19
JPN: 2027, 5, 20

Date 클래스에는 class 메소드인 next_day 메소드가 존재해 상속 관계에 의해 다음과 같이 KDate와 JDate의 이름을 통해서 이 메소드 호출이 가능하다. (자식 클래스에 없는 메소드는 부모 클래스에서 찾아서 호출하는 것이 원칙)

next_day 메소드가 존재하는 것은 Date 클래스이지만 위에서는 KDate와 JDate라는 이름을 통해 이 메소드를 호출했기 때문에 next_day에 전달되는 클래스는 Data가 아니라 KDate와 JDate이다. 때문에 KDate 객체를 대상으로 next_day 메소드를 호출하면 KDate 객체가 생성되고 JDate 객체를 대상으로 next_day 메소드를 호출하면 JDate 객체가 생성되는 것이다. 이러한 수준의 메소드 정의는 static 메소드로는 구현하기 어렵다.