이번 포스트 부터 파이썬의 클래스에 대해 알아 보도록 한다.
문법도 잘 모르는데 클래스란 무엇인지 어떻게 쓰는지 장황하게 설명해봐야 처음에는 잘 이해되지 않을 것이다. 우선은 문법 위주로 강의를 진행하고 개론적인 접근은 차차 예제들과 함께 깊이 파고 들어가도록 하겠다. 당장 이해 되지 않아도 멈추지 말고 계속 앞으로 진행해 나가도록 하자. 어차피 앞으로 다뤄질 포스팅에서 지겹도록 계속 언급 된다.
목차
- >> 파이썬 클래스 소개
- instance vs static
- dict vs slots
- property의 활용
- special method
- callable object
- 클래스 데코레이터(class decorator)
- 상속
- 추상 클래스와 추상 메소드
클래스(class) 문법
클래스가 뭔지, 어떤 개념이 있는지 앞뒤 다 자르고 클래스 선언 문법 부터 보자. 클래스 정의의 가장 간단한 문법은 아래와 같다.
class 클래스이름 :
...
클래스를 정의하기 위해서는 'class' 키워드 뒤에 '클래스 이름'을 정의한다. 그리고 콜론으로 이 다음 부터는 클래스의 코드 블록이라는 것을 알려주면 된다. 아주 간단하다.
클래스 상속과 관련하여 추가적인 문법이 있지만, 이는 뒤에서 클래스 상속 항목에서 계속 다루도록 하고 지금은 기본에만 집중하자.
"class 클래스이름 :"
클래스 객체 생성
class ClassName :
pass
c = ClassName()
클래스 객체 생성은 위와 같이 클래스 이름과 괄호만 적어주면 클래스의 객체가 생성 된다. 바로 다음 섹션에 설명 되는 "생성자 메소드"의 인자에 따라 클래스 객체 생성 시 괄호에 필요한 인자가 다를 수도 있지만, 지금은 기본 모양만 보고 넘어 가도록 하고 뒤에 보다 자세한 예제가 나올 때 다시 살펴 보도록 하자.
"객체 = 클래스이름()"
Tip. pass
※ pass는 함수나 클래스를 만들었는데 아무런 구현 내용이 없을 경우 사용한다. 자세한 사항은 [여기]를 참조.
클래스의 생성자와 소멸자
클래스의 생성자와 소멸자는 일종의 특수 메소드로써 클래스 객체가 생성 되거나 메모리에서 사라질 때 호출 된다.
class ClassName1 :
def __init__(self) : # 생성자, 객제 생성 시 호출, self 파라미터는 필수
print('constructor')
def __del__(self) : # 소멸자, 객체 소멸 시 호출, self 파라미터는 필수
print('destructor')
c1 = ClassName1()
class ClassName2 :
def __init__(self, a, b, c) : # 생성자는 인자를 가질 수 있다
print(f'constructor({a},{b},{c})')
def __del__(self) :
print('destructor')
c2 = ClassName2(1, 2, 3)
- 생성자(constructor) : 클래스 객체가 생성 될 때 호출 된다.
생성자는 __init__이라는 미리 지정된 이름을 사용해야 하며 첫번째 인자로 반드시 self를 지정해 주어야 한다.
필요하다면 def __init__(self, a, b, c)와 같이 self 뒤에 필요한 인자들을 더 추가 할 수 있다 - 소멸자(destructor) : 클래스 객체가 소멸 될 때 호출 된다.
소멸자는 __del__이라는 미리 지정된 이름을 사용해야 하며, 첫번째 인자로 반드시 self를 지정해 주어야 한다.
메소드(클래스 내에서 선언 되는 함수)들은 첫번째 인자로 무조건 self를 인자로 선언해야만 한다. 이는 잠시 뒤에 다시 자세히 설명하도록 하겠다.
※ 생성자와 소멸자의 첫번째 인자의 이름은 무엇이든 관계 없지만 , 관례상 self라고 한다.
Tip. 파이썬의 생성자 오버로딩
C++, Java와 같은 다른 프로그래밍 언어의 경험이 있다면 생성자 오버로딩에 대해 알고 있을 것이다. 하지만 파이썬에서는 생성자 오버로딩을 지원하지 않는다. 즉, 파이썬에서는 생성자를 여러개 만들 수 없다는 말이다. 여러개의 생성자를 정의는 가능하지만 가장 나중에 정의된 생성자만 인식한다.
자세한 사항은 [여기]를 참고 하도록 하자.
클래스 인스턴스 필드
파이썬에서 '인스턴스 필드'는 각 객체 마다 따로 보관 되는 데이터를 말한다. 만일 여러분 중에 C++이나 Java와 같은 언어에 대한 경험이 있는 분이라면 멤버 데이터 또는 멤버 변수라고 용어가 더 익숙할 것이다.
class ClassName :
def __init__(self) :
self.x = 0 # 인스턴스 필드 x
self.y = 0 # 인스턴스 필드 y
def __del__(self) :
pass
c1 = ClassName()
c1.x = 10
c2 = ClassName()
c2.x = 20
print(c1.x, ' ', c2.x) # 10 20, c1객체와 c2객체의 각 인스턴스 필드는 다른 값을 가진다
위 예제를 살펴 보면 ClassName 클래스의 생성자 __init__()메소드에서 self를 이용해 x와 y 필드를 만들었다. 그리고 아래에서 ClassName클래스의 인스턴스 c1, c2를 각각 만들어 다른 값을 저장하고 출력한다. 이를 통해 각 객체의 x들은 다른 값을 가질 수 있다는 것을 알 수 있다. 그래서 위의 x와 y같이 선언되는 변수들을 인스턴스 필드라고 한다. 여기 까지가 인스턴스 멤버에 대한 기본이다.
관례상 클래스의 인스턴스 필드를 선언 할 때는 위의 방법 처럼 생성자에서 일괄적으로 진행한다. 여기서 '관례상'이라는 단어를 사용한 것은, 기술적으로는 다른 방법으로도 가능하다는 말이다(하지만 일반적인 상황에서 관례를 따르는 것은 문제를 만드는 것을 미리 방지해 준다).
파이썬은 인스턴스 필드를 위해 따로 특별한 자료 구조가 있는 것이 아니라 __dict__ 어트리뷰트(여기 참고)에 저장한다. 그래서 아래의 예제와 같이 사용것도 가능하다(아래 예는 원리를 설명하기 위한 것이지 이렇게 쓰라는 것은 절대 아니니 오해하지 말자).
class ClassName :
def __init__(self) :
pass
def __del__(self) :
pass
c1 = ClassName()
c1.x = 10
c1.__dict__['y'] = 10
c2 = ClassName()
c2.x = 20
print(c1.y) # 10
# print(c2.y) # Error
클래스 생성자에서 인스턴스 데이터의 초기화를 빼고, 대신 c1 인스턴스 생성 후 __dict__어트리뷰트나 직접 호출을 통해 인스턴스 데이터를 생성하고 있다. 하지만 c2에는 아무런 인스턴스 멤버를 생성하지 않아 같은 클래스의 객체임에도 불구하고 동일한 필드를 참조하려면 어떤 객체는 성공하고 어떤 객체는 에러를 발생 시키게 된다.
클래스 인스턴스 필드는 __dict__어트리뷰트에 저장 된다.
이런 상황을 방지하기 위해 '관례상', 클래스 생성자에서 필요한 모든 인스턴스 필드들을 생성하라고 말한 것이다.
접근 권한 한정자
C++, C#, Java와 같은 프로그래밍 언어는 멤버 변수(파이썬의 인스턴스 필드와 동일. 이름만 다르게 부름)와 멤버 함수(메소드, 역시 이름만 다름)는 클래스 외부에서 접근 정도를 제한 할 수 있는 public 과 private 한정자들을 언어 차원에서 지원하지만 파이썬에서는 접근 권한 한정자를 직접 지원하진 않는다.
여기서 직접 지원하지 않는다고 이야기하는 이유는 메소드 이름 앞에 __ 와 같이 언더바 두개를 붙여 주면 name mangling을 통해 외부에서 접근할 수 있는 이름을 변경하여 마치 클래스 외부에서는 해당 필드에 접근할 방법이 없는 것 처럼 보이지만 실제로 맹글링 규칙을 알면 접근이 가능하다.
Tip. name mangling
프로그래밍 언어에서 컴파일러나 인터프리터가 일정한 규칙에 의해 원래의 이름을 다른 이름으로 변경해 버리는 것. 파이썬에서는 __ 가 붙은 인스턴스 필드를 '_클래스이름__변수이름'과 같이 바꾼다.
class ClassName :
def __init__(self) :
self.x = 1
self.y = 2
self.__z = 3
def print(self) : # 클래스 메소드. 뒤에서 설명함
print(self.x, self.y, self.__z) # 1 2 3
c = ClassName()
c.print()
print(c.__dict__) # {'x': 1, 'y': 2, '_ClassName__z': 3}
print(c.__z) # Error. 외부에서 접근 할 때는 __z라는 이름을 찾지 못한다
print(c._ClassName__z) # 3
위 예제에서 __z는 클래스의 메소드에서는 '__z'라는 이름으로 접근 가능하지만 13라인과 같이 클래스 외부에서 해당 필드에 접근하려고하면 ClassName 객체는 __z라는 어트리뷰트가 없다라는 에러를 발생 킨다. 하지만 14라인과 같이 맹글링된 이름으로 접근하면 정상적으로 접근이 되는 것을 볼 수 있다.
이 부분은 뒤에 나올 클래스 상속에서 좀 더 자세히 설명 하도록 하겠다.
클래스 메소드
클래스 메소드란 클래스에 종속적인 함수를 일컫는다. 일반 함수 처럼 def 키워드를 이용해 정의 되며, 인자를 받을 수 있고 리턴도 할 수 있다.
차이점은 클래스 메소드는 클래스 내부에서 정의 되어야하며, 클래스 내부에 선언된 인스턴스 필드들을 사용 할 수 있다. 그리고 클래스 메소드는 클래스 객체에 종속적이다. 이것은 아래 예제를 살펴 보면서 자세히 설명하도록 하겠다.
class ClassName :
def Method() :
print('This is method')
c = ClassName()
c.Method() # TypeError: Method() takes 0 positional arguments but 1 was given
클래스 내부에 def 키워드와 함께 일반 함수와 똑같은 형식을 가진 Method()를 선언했다. 클래스 내부에 선언 된것 외에는 일반 함수와 동일하다
앞에서 메소드는 객체에 종속적이라고 이야기 했었다. 메소드를 호출하기 위해서는 6라인 처럼 객체를 생성 후 해당 객체를 통해 호출해야만 한다. 하지만 위 예제를 실행하면 오류가 발생한다. 현재 해당 메소드는 아무런 인자도 받지 않게 정의 되어있지만, 실제 클래스의 메소드를 객체를 통해 호출하면 인자가 넘어 온다고 오류를 발생 시킨다.
이유는 클래스 메소드는 객체에 종속적이며 객체를 통해 호출 될 때 인터프리터에 의해 메소드의 객체가 첫번째 인자로 넘겨진다. 위에서 우리가 배운 클래스의 생성자와 소멸자 클래스 메소드의 첫번째 인자로 self(클래스 인스턴스를 가리키는 변수)를 지정해 주었던 이유가 바로 이것이다.
class ClassName :
#def Method() :
def Method(self) :
print('This is method')
c = ClassName()
c.Method() # ok
위와 같이 Method 메소드의 첫번째 인자로 self를 지정해 주고 난뒤 실행하면 이번에는 정상적으로 실행 되는 것을 볼 수 있다. 클래스의 메소드를 호출 할 때, 우리는 아무런 인자를 넘기지 않는다고 하더라도 인터프리터는 내부적으로 메소드 호출 시 첫번째 인자로 객체를 가리키는 변수를 넘긴다. 이것이 일반 함수와 클래스 메소드와의 가장 큰 차이다.
클래스 메소드는 가장 첫번째 인자로 self를 선언해 주어야 한다.
class ClassName :
def __init__(self, x, y) :
self.x = x
self.y = y
def Method(self) :
print(f'This is method. x:{self.x}, y:{self.y}')
c = ClassName(1, 2)
c.Method() # This is method. x:1, y:2
인터프리터로 부터 넘겨 받은 self 인자는 위와 같이 클래스 메소드에서 클래스 내부의 필드를 접근하기 위해 사용 된다.
마치며
이상 클래스의 기본 문법과 메소드, 인스턴스 필드에 대해서 간략하게 알아 보았다. 클래스에 대한 첫 시간이라 가볍게 대략적인 문법만을 살펴 보고 자세한 설명은 대부분 뒤로 미루었다. 너무 모든 것을 처음 부터 이해하려고 스스로를 채찍질 할필요는 없다. 강의를 따라오다 보면 반복되는 설명에 이해하고 싶지 않더라도 자연스럽게 이해가 될 것이다.
다음 장 클래스(class) #2 instance vs static에서는 클래스 인스턴스에 의존적이지 않고 클래스 타입을 이용해 호출 하는 static 필드와 static 메소드에 대해 살펴 보도록 하겠다.