본문 바로가기

진리는어디에/Python

[Python] 클래스(class) #2 instance vs static

이전 포스트에서는 파이썬 클래스의 기본 문법과 인스턴스 필드에 대해 살펴 보았다. 이번 시간에는 이전 포스트에 이어 클래스의 스태틱(static) 필드와 함수에 대해 알아 보도록 하겠다.

목차

  1. 파이썬 클래스 소개
  2. >> instance vs static
  3. dict vs slots
  4. property의 활용
  5. special method
  6. callable object
  7. 클래스 데코레이터(class decorator)
  8. 상속
  9. 추상 클래스와 추상 메소드

instance vs static

객체 지향 프로그래밍을 하다 보면 인스턴스 필드와 스태틱 필드라는 용어를 들을 수 있다. 이전 포스트에서 "필드"라는 것은 클래스 내부의 데이터를 의미한다는 것을 배웠다. 그럼 이번에 언급되는 instance와 static의 차이는 무엇일까? 간단하게는 아래와 같이 정의 할 수 있다.

  • 인스턴스 필드(instance field) : 객체 별로 "따로" 보관되는 데이터
  • 스태틱 필드(static field) : 모든 객체가 "공유"하는 데이터

이해를 돕기 위해 아래 코드를 보자.

class Point :
    def __init__(self, x = 0, y = 0) :
        self.x = x;
        self.y = y;
        
n1 = Point(1, 2)
n2 = Point(3, 4)

인스턴스 필드 x, y를 가진 Point 클래스 객체를 두 개 생성했다. 각 객체의 필드들은 객체의 __dict__어트리뷰트에 각각 저장된다.

그럼 스태틱 필드들은 어디에 저장되는 것일까? 기억을 더듬어 올라가 파이썬 변수의 타입에 대해 설명 할 때, 모든 객체들은 타입을 관리하는 "타입 객체"를 가리킨다고 했었다. 그림으로 표현하면 아래와 같다.

위의 그림과 같이 같은 타입의 객체들은 동일한 "타입 객체"를 가리키고 있다. 그리고 타입 객체 역시 다른 객체와 기본은 동일하므로 __dict__어트리뷰트를 가지고 있다. 이미 짐작하신 분도 있겠지만, 위의 "타입 객체"가 바로 스태틱 필드가 저장되는 공간이다.

스태틱 필드가 저장 되는 메모리 구조에 대해 지금 당장 이해할 필요는 없다. 위의 설명은 그냥 이런식으로 스태틱 필드가 모든 객체에 의해 공유 되는구나 정도로 알고 넘어가면 된다. 여기서 중요한 것은 스태틱 필드는 같은 클래스의 객체들에게 공유 된다는 것을 이해하는 것이다.

스태틱 필드의 문법

스태틱 필드를 코드로 만드는 방법은 간단하다. 아래 예제의 9라인 처럼 클래스 이름을 통해 필드를 생성하면 모든 동일 클래스의 객체가 공유 하는 스태틱 필드가 - 타입 객체에 - 만들어 진다.

class Point :
    def __init__(self, x = 0, y = 0) :
        self.x = x;
        self.y = y;
        
n1 = Point(1, 2)
n2 = Point(3, 4)

Point.count = 0 # Point.__dict__['count'] = 0

하지만 위와 같이 스태틱 필드를 생성하면 최초 사용 시 스태틱 필드가 생성 된다. 이전 포스트에서 우리는 클래스의 필드들은 객체의 생성 시점에 일괄적으로 만든는 것이 좋다고 했다. 스태틱 필드 역시 마찬가지다. 다만 차이점은 스태틱 필드는 클래스 생성자에서 만드는 것이 아니라 아래와 같이 클래스 몸체에 선언 된다.

class Point :
    
    count = 0 # Point.count = 0 과 동일
    
    def __init__(self, x = 0, y = 0) :
        self.x = x;
        self.y = y;
        
n1 = Point(1, 2)
n2 = Point(3, 4)

# Point.count = 0

스태틱 필드의 접근

스태틱 필드의 접근은 기본적으로 클래스 이름을 통해 접근하는 방법과 특수하게 객체의 이름을 통해 접근하는 방법이 있다. 하지만 객체의 이름을 이용해 스태틱 필드에 접근하는 경우 읽기와 쓰기에 따라 동작이 다르므로 실수를 유발 할 수 있다. 뒤에 자세히 설명하겠지만 결론을 먼저 말하자면 스태틱 필드를 사용할 때는 언제나 클래스 이름을 통해 접근 하도록 한다.

class Point :
    
    count = 0 # Point.count = 0 과 동일
    
    def __init__(self, x = 0, y = 0) :
        self.x = x;
        self.y = y;
        
n1 = Point(1, 2)
n2 = Point(3, 4)

# 1. 클래스 이름으로 접근. 문제 없음
Point.count = 100

# 2. 객체 이름으로 접근. 읽기
print(n1.count)    # 100

n1.count = 200
print(Point.count) # 100
print(n1.count)    # 200
print(n2.count)    # 100

위 코드의 16라인을 보면 객체의 이름을 통해 count 스태틱 필드에 접근한다. 파이썬에서 객체를 통해 필드를 읽으려고 시도하면 가장 먼저 n1객체의 __dict__에서 count를 검색한다. 만일 n1에서 해당 필드를 찾지 못하는 경우는 타입 객체의 __dict__, 즉, 위에서 스태틱 필드가 저장되는 곳을 뒤진다.

하지만 쓰기 과정은 위와는 다소 다르다. 객체를 이용해 필드를 쓰고려고 할 때, 만일 객체의 __dict__어트리뷰트에 필드가 없는 경우 필드를 생성 해버린다. 그래서 위 예제의 20라인과 21라인의 결과가 다른 이유는 바로 이 때문이다.

스태틱 필드에 접근 할 땐, 클래스 이름을 이용하자

스태틱 메소드

일반 클래스의 메소드는 객체를 통해서만 호출 할 수 있지만 스태틱 메소드는 객체가 없어도 호출 가능하다.

  • 인스턴스 메소드(instance method) : 객체가 있어야만 호출 가능
  • 스태틱 메소드(static method) : 객체가 없어도 호출 가능

객체가 없어도 호출 가능하다는 뜻이 무엇인지 알아 보기 위해 아래 예제에서 9라인의 일반 클래스 메소드와 12라인의 스태틱 메소드를 비교하면서 살펴 보도록 하자.

class Point :
    
    count = 0 # Point.count = 0 과 동일
    
    def __init__(self, x = 0, y = 0) :
        self.x = x;
        self.y = y;
        
    def foo(self) : # instance method
        print('foo function')
        
    def bar() :     # static method. self가 없는 것에 주목
        print('bar function')
        
p = Point()
p.foo()             # 일반 메소드 호출. 내부적으로 인터프리터가 객체를 첫번째 인자로 넘겨 줌 
Point.foo(t)        # 일반 메소드 호출. 위와 동일한 작업, 함수의 첫번째 인자를 직접 넘겨 줌
Point.bar()         # 스태틱 메소드 호출. 클래스 이름으로 호출

위 예제의 foo함수는 일반 인스턴스 메소드, bar 함수는 스태틱 메소드다. 일반 메소드의 경우는 객체를 통해 호출해야 하며(이렇게 하면 내부적으로 객체가 함수의 첫번째 인자로 넘겨 진다), 클래스 이름으로 호출한다고 하더라도 객체를 첫번째 인자로 넘겨 줘야 한다(인터프리터가 내부적으로 하는 일을 프로그래머가 직접 한다).

하지만 스태틱 메소드의 경우에는 self 파라미터를 받는 인자가 없다. 그리고 함수에 접근 할 때도 클래스 이름을 통해 접근해야만 하지, 객체를 통해 접근하는 경우 에러를 발생 시킨다.

참고로 @staticmethod 데코레이터를 이용해 스태틱 메소드도 객체를 통해 접근 할 수 있는 방법이 있긴하다. 하지만 필자는 스태틱 메소드를 객체로 접근하여 호출하는 방법을 사용하는것을 권하지 않느다.

class Point :
    
    @staticmethod
    def bar() :     # static method. self가 없는 것에 주목
        print('bar function')
        
p = Point()
p.bar()             # 객체를 통해 스태틱 함수 호출

실제 내부적으로도 복잡한 과정이 있는 것이 아니라, 객체를 통해 호출 시, 첫번째 인자로 넘겨지는 self를 사용하지 않고 버리는 것 뿐이다. 결국 객체의 인스턴스 데이터 필드를 사용하게 되므로 예상치 못한 사이드 이펙트가 발생 할 수 있다.

마치며

이상으로 파이썬 클래스의 기본 적인 사항들에 대해 알아 보는 시간을 가졌다. 다음 포스트 #3 'dict' vs 'slots'에서는 클래스의 __slots__어트리뷰트에 대해 알아 보도록 하겠다.

부록 1. 같이 보면 좋은 글

유익한 글이었다면 공감(❤) 버튼 꾹!! 추가 문의 사항은 댓글로!!