코린이의 소소한 공부노트

클래스, 오브젝트 본문

Python

클래스, 오브젝트

무지맘 2021. 6. 25. 22:17

그동안 미래의 나에게 설명을 미뤄왔는데, 이제 때가 된 듯하다.

우리가 일상에서 들어본 class는 수업, 학교 반, 게임에서 장비나 유저들의 티어를 나타내는 단어 등이었는데, 파이썬에서의 클래스는 무엇을 뜻하는 것일까?

이번 글에서는

1. 클래스, 오브젝트란 무엇인지

2. 클래스 선언은 어떻게 하는지

3. 클래스 상속은 무엇인지

4. 상속받은 다음 어떤 방식으로 메소드를 정의하는지

5. 특수 함수에는 어떤 것이 있는지 알아보도록 하겠다.


1. 클래스 Class와 오브젝트 Object


파이썬에서 말하는 클래스는

1) 실제 있는 것들을 모델링해서 그것의 속성(attribute 어트리뷰트)과 기능(method 메소드)을 만들어 놓은 데이터 타입으로

2) string, int, list 등등 모두가 클래스다.

3) 다루고자 하는 데이터(변수) 와 데이터를 다루는 연산(함수)을 하나로 캡슐화(encapsulation)하여 클래스로 표현한다.

간단한 예시로, 이름, 나이 같은 속성이 있고, 인사하기, 밥먹기 같은 메소드가 있는 것으로 사람이라는 클래스를 표현할 수 있겠다.

 

클래스는 추상적인 개념이고 실제 구현을 해서 쓸 수 있는건 다음에 나오는 객체(오브젝트)라는 것이다.

오브젝트란

1) 클래스로 생성되어 구체화된 객체(=인스턴스 instance)를 말하는 것으로

2) 파이썬 코딩에 사용하는 str, int, list 등은 전부 다 객체다.

3) 클래스가 인스턴스화 되어 메모리에 상주하는 상태를 의미한다.

클래스가 붕어빵틀(추상적)이라면 오브젝트는 틀로 찍어낸 붕어빵(실체적)이라고 할 수 있다. 어찌 됐든 객체를 만들려면 클래스부터 선언해야둬야한다.


2. 클래스 선언하기


클래스를 선언할 때는 class 클래스이름: 형태로 선언한 뒤 들여 쓰기를 해서 클래스에 표현할 속성, 메소드 등을 적어주면 된다.

클래스를 선언할 때 제일 먼저 해야 할 것은 init 함수에서 속성을 정해주는 것이다. 그리고 필요하다면 다른 변수나 메소드를 정해주면 된다.

1) init: 생성자

- 클래스 인스턴스가 생성될 때 호출되는 함수

- self 인자는 항상 첫 번째에 오며 자기 자신을 가리킨다.

- 생성자에서 해당 클래스가 다루는 데이터를 정의한다. 이 데이터를 멤버 변수(member variable) 또는 속성(attribute)라고 한다.

class Person:
    def __init__(self): # _(언더바) 2개씩 앞뒤로
        pass # 문법적으로 오류 없이 빈 것을 구현하게 해주는 키워드
             # 속성을 정해주려면 pass를 지우고 구현하면 됨

p1 = Person() # Person 객체 하나 생성
    

2) self: 파이썬 메소드의 첫 번째 인자

- 해당 객체의 주소를 나타내는 인자

- 이름이 꼭 self일 필요는 없지만, 통상적으로 self라고 사용한다.

- C++, C#, Java의 this에 해당한다.

- 메소드를 호출할 때 self인자를 사용하지 않는다.

class Person:
    def __init__(self, a, b=1): ### 여기서는 인자가 3개지만
        self.name = a
        self.age = b # 입력 안 할 경우 1살

p1 = Person('펭수', 10) ### 객체 생성시에는 인자가 2개!
print(p1.name, p1.age)
>> 펭수 10             # 그러고보니 펭수는 펭귄인데..ㅎ

3) method: 메소드, 멤버함수

- 해당 클래스의 객체에서만 호출이 가능한 함수

- 메소드는 객체 레벨에서 호출되며, 해당 객체의 속성에 대한 연산을 한다.

- 객체이름.메소드이름(인자) 형태로 호출된다.

class Person:
    def __init__(self, a, b=1):
        self.name = a
        self.age = b
    def introduce(self): # Person클래스의 객체가 사용할 수 있는 메소드
        print('{}(이)라고 합니다. {}살이에요.'.format(self.name, self.age))
        print('주소:',self)

p1 = Person('펭수', 10)
p1.introduce()  # 객체이름.메소드이름(인자)
>> 펭수(이)라고 합니다. 10살이에요.
>> 주소: <__main__.Person object at 0x000001AFB24BA820>

- 메소드의 타입에는 2가지가 있다. 인스턴스 메소드와 클래스 메소드

ㄱ. 인스턴스 메소드 instance method

 - 위 예시의 introduce 메소드처럼 우리가 지금 쓰고 있는 것이 인스턴스 메소드이다.

 - 객체 레벨로 호출되기 때문에 해당 메소드를 호출한 객체에만 영향을 미친다.

ㄴ. 정적 메소드 static method / class method

 - 객체를 만들지 않고도 사용할 수 있는 메소드이다.

 - 임포트한 모듈의 함수를 쓰는 것처럼 래스이름.메소드이름(인자) 형식으로 사용하면 된다.

class Cal:
    n = 10 # 클래스 변수. 해당 클래스로 생성된 객체는 다 이 변수에 접근 가능
	
    # 스태틱 메소드 표시는
    @staticmethod      
    def add_s(a, b):    # self 인자를 쓰지 않기 때문에
        return a+b+Cal.n # Cal.n으로 클래스 변수를 불러온다.
    
    # 클래스 메소드 표시는
    @classmethod
    def add_c(cls, a, b): # self 대신 cls를 쓰기 때문에
        return a+b+cls.n  # cls.n으로 클래스 변수를 불러온다.
    
print(Cal.add_s(3, 4)) # 객체 생성 없이
>> 17
print(Cal.add_c(2, 4)) # 클래스의 메소드를 사용 가능
>> 16

c = Cal()   # 당연하겠지만, 객체 생성 후 인스턴스 메소드 사용 가능
print(c.add_s(1, 2))
>> 13
print(c.add_c(5, 3))
>> 18

스태틱 메소드와 클래스 메소드의 차이는 이어지는 글인 3. 클래스 상속 가장 마지막 부분에서 설명하겠다.


3. 클래스 상속(class inheritance 클래스 인헤리턴스)


클래스 상속은

1) 우리가 알고 있는 상속의 뜻처럼, 클래스의 기능을 그대로 물려받는 것을 말한다.

2) 기능을 추가 또는 변경해서 새로운 클래스를 만들 수 있다.

3) 코드를 재사용할 수 있기 때문에 메모리가 절약된다.

이때 상속해주는 클래스의 명칭은 parent / super / base class이고, 상속받는 클래스의 명칭은 child / sub / derived class이다.

클래스를 상속한다는 것의 의미를 따져본다면 child is-a parent 관계이다. (child가 parent의 부분집합이라고 보면 편하다. child이면 parent라서 child가 parent의 기능을 다 쓸 수 있지만, parent라고 해서 다 child는 아니기 때문에 parent가 child의 기능을 쓸 수 없다.)

class Person: # 부모 클래스
    def __init__(self, a, b=1):
        self.name = a
        self.age = b
    def introduce(self):
        print('{}(이)라고 합니다. {}살이에요.'.format(self.name, self.age))
        print('주소:',self)
        
class Student(Person): # 자식 클래스. ()안에 부모 클래스의 이름을 넣어서 상속받는다.
    pass

s1 = Student('무지맘', 100) # 자식 클래스의 객체
s1.introduce() # 자식 클래스는 부모 클래스의 메소드를 그대로 쓸 수 있다.
>> 무지맘(이)라고 합니다. 100살이에요.
>> 주소: <__main__.Student object at 0x000001AFB2773790>

아주 간단한 예시지만 상속이 뭔지 조금은 감이 왔으리라 생각한다. 그럼 이제 좀 전에 봤던 스태틱 메소드와 클래스 메소드의 차이에 대해서 설명하겠다. 이 둘의 차이는 그냥 봤을 때는 별반 차이가 없으나, 상속을 받아서 사용해보면 금방 그 차이가 나타난다.

class Cal:
    n = 10 # 부모의 클래스 변수

    @staticmethod
    def add_s(a, b):    # self 인자를 쓰지 않기 때문에
        return a+b+Cal.n # Cal.n으로 클래스 변수를 불러온다.
    
    @classmethod
    def add_c(cls, a, b): # self 대신 cls를 쓰기 때문에
        return a+b+cls.n  # cls.n으로 클래스 변수를 불러온다.
    
class Cal2(Cal):
    n = 100 # 자식의 클래스 변수
    
# Cal을 상속받았으므로 객체 생성 없이 메소드 사용 가능
print(Cal2.add_s(1, 2)) # Cal.n = 10
>> 13
print(Cal2.add_c(5, 3)) # cls.n = 100
>> 108

Cal2는 Cal의 자식 클래스이므로 메소드 2가지 모두 사용 가능하다. 그렇게 해서 각각의 메소드를 다 사용해보면, 스태틱 메소드는 두 수의 합에 10을 더했고 클래스 메소드는 두 수의 합에 100을 더한 것을 알 수 있다. 스태틱 메소드에서는 self나 cls 인자를 사용하지 않기 때문에 Cal의 클래스 변수인 10을 더하게 된 것이고, 클래스 메소드에서는 cls인자가 있기 때문에 상속을 받으면서 cls가 Cal에서 Cal2로 업데이트가 일어나서 Cal2의 클래스 변수인 100을 더하게 된 것이다.

이렇게 보면 클래스 메소드가 뭔가 더 유용해보이지만, 이는 클래스의 성격에 따라 다르니 유용하게 스태틱과 클래스 메소드를 이용하는 것이 좋겠다.

이제 다시 조금 전으로 돌아가서, Person 클래스를 상속받은 Student에서 어떤 일을 더 할 수 있는지 살펴보겠다.


4. 메소드 오버라이드 method override


메소드 오버라이드란 부모 클래스의 메소드를 자식 클래스에서 재정의하는 것이다. 자식 클래스의 객체가 메소드를 호출하면 자식 클래스에서 재정의한 메소드가 호출된다.

class Person: # 부모 클래스
    def __init__(self, a, b=1):
        self.name = a
        self.age = b
    def introduce(self):
        print('{}(이)라고 합니다. {}살이에요.'.format(self.name, self.age))
        print('주소:',self)
        
class Student(Person): # 자식 클래스
     def introduce(self): # 재정의한 메소드
        print('{}(이)라고 합니다. {}살이에요.'.format(self.name, self.age)) # 부모와 겹치는 부분
        print('열심히 공부하고 있습니다!')

s1 = Student('무지맘', 100)
s1.introduce()
>> 무지맘(이)라고 합니다. 100살이에요.
>> 열심히 공부하고 있습니다!

만약 자식 클래스에서 메소드를 재정의할 때 원 메소드(부모 클래스의 것)를 호출하고 싶다면 super를 이용하면 된다.

class Person: # 부모 클래스
    def __init__(self, a, b=1):
        self.name = a
        self.age = b
    def introduce(self):
        print('{}(이)라고 합니다. {}살이에요.'.format(self.name, self.age))
        print('주소:',self)
        
class Student(Person): # 자식 클래스
     def introduce(self, what): # 재정의하면서 인자 what 추가
        super().introduce()                        # 부모 클래스의 메소드를 먼저 실행한 다음
        print('열심히 {} 공부하고 있습니다!'.format(what)) # 자식 클래스 메소드의 나머지 부분 실행

s1 = Student('무지맘', 100)
s1.introduce('파이썬')
>> 무지맘(이)라고 합니다. 100살이에요.
>> 주소: <__main__.Student object at 0x000001AFB2721CD0>
>> 열심히 파이썬 공부하고 있습니다!

5. 특수 함수 special method


특수 함수는

1) __(언더바 2개)로 시작해서 __로 끝나는 함수다.

2) 더하기 연산은 __add__, 빼기 연산은 __sub__ 등으로 되어있는데, 이 함수를 오버라이딩하면 우리가 만든 객체에 +, - 연산자를 이용할 수 있다.

3) 오버라이딩할 때 위에서 봤던 클래스 상속이나 super같은 것은 필요 없다!

예시를 들겠다. 점의 좌표를 나타내는 Point라는 클래스를 생성하려고 한다. 이 클래스는 x좌표와 y좌표를 속성으로 가지며, 두 점의 좌표의 덧셈 연산을 하고, 점의 좌표를 (x, y) 형태로 출력하는 메소드를 만드려고 한다.

특수 함수를 오버라이딩하지 않고 이 클래스를 만들어서 써보면 아래와 같다.

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y
        
    def print_pt(self): # 함수를 호출한 객체의 좌표 출력
        print('({}, {})'.format(self.x, self.y))
        
    def add(self, pt): # 함수를 호출한 객체의 좌표 + pt의 좌표
        new_x = self.x + pt.x
        new_y = self.y + pt.y
        return Point(new_x, new_y)
    
p1 = Point(4, 6)
p2 = Point(1, 1)

p1.print_pt() # p1의 좌표 출력
>> (4, 6)

print(p1.add(p2))                    # add함수는 point 객체를 반환하기 때문에
>> <__main__.Point object at 0x000001AFB2773040> #  해당 객체의 주소가 출력됨

p3 = p1.add(p2) # p1+p2 결과가 보고싶다면
p3.print_pt()   # 이렇게 해야한다...하
>> (5, 7)

보기만 해도 답답한 상황이다. 이 상황에서 특수함수에 오버라이딩을 하면 도대체 어떻게 바뀐다는 걸까??

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y
        
    def __str__(self): # print_pt, 함수를 호출한 객체의 좌표 출력
        return '({}, {})'.format(self.x, self.y)
        
    def __add__(self, pt): # add, 함수를 호출한 객체의 좌표 + pt의 좌표
        new_x = self.x + pt.x
        new_y = self.y + pt.y
        return Point(new_x, new_y)
    
p1 = Point(4, 6)
p2 = Point(1, 1)

print(p1) # p1의 좌표 출력
>> (4, 6)

print(p1+p2) # add함수를 호출하는게 아닌 + 연산자로 바뀜
>> (5, 7)    # 더해서 만든 객체가 print함수에 들어가서 __str__함수를 만남

p3 = p1+p2
print(p3)   # 위와 같은 결과가 나올 수 밖에 없음
>> (5, 7)

오버라이딩 하나로 우리가 직관적으로 생각할 수 있는 결과가 나오는 함수로 바뀌었다.

더하기, 빼기 외에 오버라이딩 가능한 함수 목록은 여기>>https://docs.python.org/3/reference/datamodel.html


<요약>

 

1. 클래스: 속성+기능이 있는 데이터 타입

- init 생성자: 속성 정의

- 메소드: 인스턴스 / 스태틱 / 클래스 메소드

- 클래스 변수: 해당 클래스의 객체가 모두 접근 가능

- 인스턴스 변수: 객체만 접근 가능한 변수. init에서 정의

2. 오브젝트(=객체): 클래스를 실제 구현한 것

- 실체가 있어서 메모리에 상주하고 있는 상태

- 파이썬 코딩으로 만들어진 모든 것은 객체로 존재함

3. 클래스 상속: 자식이 부모의 것을 물려받는 것

- 부모와 자식은 is-a 관계

4. 메소드 오버라이드: 자식이 부모의 메소드를 재정의하는 것

- 자식 클래스에 맞는 것으로 재정의 가능

- 부모 클래스의 것을 그대로 가져오려면 super().메소드이름(인자)

5. 특수 함수: __함수이름__ 형태의 함수

- 오버라이딩해서 커스터마이징이 가능