본문 바로가기

Python

Python OOP - Part 1 ( Object Oriented Programming)

안녕하세요. 이번에는 객체지향프로그래밍에 대한 간단한 이야기와 함께 파이썬에서의 OOP에 대해 이야기해보려고 합니다. 꽤나 긴 글이 될 것 같네요! 시작하겠습니다 :)

OOP란?


먼저 OOP란 즉 객체지향 프로그래밍이란 뭔지에 대해 알아야한다. 기술면접 등에서 쉽게 접할 수 있는 듯한 질문이면서도, 쉽게 대답하기는 어려운 질문이라고 생각한다. 우선 정의를 짚고 가자면 "데이터를 추상화시켜 속성과 행위를 가진 객체로 만들고, 그 객체들 간의 유기적인 상호작용을 통해 로직을 구성하는 프로그래밍 방법" 이다. 객체지향에 대한 개념을 조금 배워본 사람이라면 쉽게 와닿을 수도 있을 것이다. 하지만, 그렇지 않다면 추상적인 말처럼 느껴질 수 있다. 이에 대해 조금 더 이야기해보고자 한다.

사실 프로그래밍에서의 많은 개념들은 실제 세상 즉, 현실세계에 근간을 두고 있다. OOP또한, 근간에 프로그램을 실제 세상에 가깝게 모델링하고자한다는 개념이 깔려있다. 몇가지 예시를 들어보자. 프로그래밍을 공부해본 사람들이라면 Stack, Queue등에 대해서 많이 들어봤을 것이다. 효율적으로 데이터를 처리하기 위한 중요한 자료구조 중에 하나라는 것도 알고 있을 것이다. 그렇다면 이러한 자료구조는 갑자기 생긴걸까? 아니다. 현실세계에서부터 생각해보자. 편의점 카운터에 있는 현금들을 쌓아두는 곳을 본 적이 있을 것이다. 손님에게 돈을받아 돈을 넣는다고 생각해보자. 돈을 어디에 놓을까? 기존에 있던 지폐 혹은 동전이 쌓여있는 곳 위에 올려놓는다. 이것이 stack의 개념이다. 이번에는 돈을 거슬러 주기위해서 꺼내야하는 상황이라고 생각해보자. 갑자기 지폐더미의 맨 아래에서 지폐를 꺼내서 줄까? 아니다. 가장 위에 있는 지폐부터 차근차근 꺼낼 것이다. 현실세계의 이러한 개념을 컴퓨터 세상으로 가져가 사용하는 것이 stack인 것이다. 이렇게 컴퓨터 세상은 현실 세계에 근간을 두는 경우가 많다. 그렇다면, OOP는 어떨까? 마찬가지다. OOP에서 항상 이야기하는 객체는 무엇일까? 현실세계의 존재하는 모든 사물, 생명체들이 다 객체인 것이다. 앞서 OOP를 정의하며 속성과 행위를 가진 객체로 만든다고 했다. 이건 자동차를 떠올려보자. 자동차는 '간다'라는 행위나 '멈춘다'라는 행위등을 할 수 있다. 즉, 가지고 있다. 또한 이러한 객체들간의 유기적인 상호작용을 통해 현실세계를 구성하고 있다. 이를 컴퓨터세상으로 가져온 것이 OOP인 것이다.

https://www.edaily.co.kr/news/read?newsId=01400566615965656&mediaCodeNo=0

그렇다면 이제 본격적으로 OOP에 대해서 알아보자. OOP는 크게 어떠한 개념들로 이루어지고 있을지 먼저 살펴보자. OOP에서 꼭 알아야할 기본 개념들 중 무척 중요한 클래스, 객체, 인스턴스에 대해서 먼저 알아보자.

Class, Object, Instance


우선 본격적으로 들어가기 전에 파이썬의 모든 것은 객체다라는 말을 다시 한번 떠올리고 가기를 바란다. 파이썬에서는 함수도 클래스도 등등 모든 것이 다 객체이다.

클래스의 개념부터 정리해보자. 정의를 하자면, 어떤 문제를 해결하기 위한 데이터를 만들기 위해 OOP원칙에 따라 집단에 속하는 속성과 행위를 변수와 메소드로 정의한 것이다. ( 속성이나 행위를 현실세계의 개념, 이에 대응하는 컴퓨터세상의 개념으로 데이터(속성값)과 메소드(속성메소드)라고 생각하면 좋겠다.) 복잡하지만, 간단히 OOP원칙에 따라 만든 설계도라고 이해하면 좋을 것 같다. 포인트는 설계도에 있다. 이 설계도를 통해서 객체를 찍어낼 수 있는 것이다. 그렇다면 객체는 무엇일까? OOP적인 접근으로 객체를 정의하자면 Class에서 선언된 모양 그대로 생성된 실체이다. 그리고 이러한 객체가 소프트웨어에서 실체화될 때 즉, 메모리에 할당되어 사용될 때 해당 객체를 인스턴스라고 하게 된다. 사실 객체와 인스턴스 같은 경우 정확히는 객체가 인스턴스의 상위개념이지만, 일반적으로 인스턴스와 같은 의미로 사용된다. ( 대부분 프로그래밍 언어에서 사용자 정의 클래스를 통해 찍어낸 객체가 바로 메모리에 올라가기 때문이다.)

지금 말한 개념들이 무척 추상적이게 느껴질 수 있다. 보다 편한 이해를 위해 마찬가지로 현실세계에 대응해보자. 사실 이러한 개념을 위해 많이 드는 예시는 쿠키와 쿠키틀이다. 우리가 실제로 현실에서 쿠키를 먹기위해서는 반죽을 쿠키틀로 찍어낸다. 이렇게 쿠키를 만들어내기 위해 사용하는 쿠키틀을 ( 쿠키틀 모양대로 쿠키를 찍어냄 ) 설계도인 클래스로 대응시킬 수 있고, 이렇게 찍어진 반죽을 객체라고 볼 수 있다. 이 쿠키를 실제로 먹기 위해서는 쿠키를 구워야한다. 이렇게 구워진 쿠키를 인스턴스라고 대응하면 좋을 것 같다. ( 사실 객체를 concept으로 접근하는 관점도 있는데, 그러한 관점에서 이 예시는 틀린 예시다. 하지만 OOP적으로는 메모리에 올라간 객체를 인스턴스로 정의하기에 이렇게 예시를 구성했다.) 여기서 주의해야할 점이 있다. 그러면 하나의 클래스로 만들어진 여러 인스턴스드은 같은 것일까? 전혀아니다. 매우 중요한 말인데 "하나의 클래스로 만들어진 여러 인스턴스들은 각각 독립적이다." 우리가 같은 쿠키틀을 이용해서 여러 쿠키를 만들었다면 이 쿠키들이 본질적으로 같은 것일까? 전혀 아닌 것이다. 만약 독립적이지 않다면 내가 한 쿠키를 베어물었을 때 다른 쿠키들도 한입씩 다 먹어져야한다. 즉, 객체 하나의 데이터를 수정하거나 변경하더라도 나머지 메모리에 존재하는 다른 데이터들(인스턴스들)은 아무런 영향을 받지 않는 것이다. ( 애초에 다른 메모리에 올라간다는 것으로 독립을 이해해도 좋다.) 좀 더 본격적으로 클래스에 대해서 공부해보자.

Class

클래스의 간단한 형태는 다음과 같다. 클래스를 통해 어떤 효과가 생기게 하기 위해서는 함수정의문 ( def )처럼 먼저 실행되어야 한다. 실질적으로 클래스를 정의할 때 대부분의 구문은 함수 정의들이지만, ( 물론 일부 규약이 있는) 다른 것들도 사용할 수 있는 것이다. 이렇게 클래스를 정의할 때 ClassName이라는 새로운 namespace가 만들어지게 된다. 따라서 이 scope에 해당하는 모든 local name들의 경우 이 ClassName이라는 namespace로 가게 된다. 즉 dir을 통해 현재 존재하는 네임스페이스들을 확인해보면 ClassName이라는 네임스페이스가 새롭게 생긴 것을 확인할 수 있다. 결국 ClassName이라는 클래스를 통해서 인스턴스를 하나 찍어내게 되면, 찍힌 인스턴스안에 namespace가 생기고, 이 공간에는 속성을 포함한 다양한 행위가 있는 것이다.

class ClassName:
    <statement-1>
    .
    .
    .
    <statement-N>


print(dir())

>>>
['ClassName', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__']

이 클래스는 크게 두가지로 구성된다. 앞서 이야기했던 속성과 메소드이다. 아주 좋은 예시가 있어서 가져왔다. 우리의 클래스가 어떤 캐릭터를 만들어내는 클래스라면, 해당 클래스로 만들어진 캐릭터 인스턴스에는 즉 캐릭터에는 체력도 있어야하고 마나도 있어야할 것이며, 칼로 베기, 칼로 찌르기 등의 행위도 할 수 있어야 한다. 이를 위해서 설계할 때 이러한 속성과 메소드를 가진 객체로 태어날 수 있도록 설정해주는 것이다.

https://dojang.io/mod/page/view.php?id=2372

class MyClass:
    """A simple example class"""
    i = 12345

    def f(self):
        return 'hello world'

이러한 클래스를 만들었다고 합시다. 클래스로 할 수 있는 것은 크게 두가지이다. 바로 attribute 참조와 인스턴스 생성이다. 먼저 attribure reference에 대해서 보자. 이는 우리가 지금까지 해왔던 attributte reference들과 다르지 않다. list라는 객체에는 appned와 같은 attribute가 존재하기에 우리가 list.append()와 같이 쓸 수 있는 것이다. 우리는 이런식으로 object.name을 통해서 attribute를 참조한다. 즉 attribute란 . 뒤에 나오는 모든 name인 것이다. 따라서 위의 예시에서도 마찬가지로 MyClass.iMyClass.f 를 통해서 attribute를 reference할 수 있는 것이다. ( 각각 int object와 function object를 돌려줄 것이다.) 또한, __doc__ 역시 attribute이기에 MyClass.__doc__ 을 통해서 "A simple example class"를 돌려받을 수 있다. ( 매직메소드는 특별한 행위를 통해 실행되는 메소드라는 것을 이전에 살펴봤다. )

MyClass.i
>>> 12345

MyClass.f
>>> <function __main__.MyClass.f>

MyClass.__doc__
>>> 'A simple example class'

여기서 또 중요한 포인트는 클래스의 attribute는 할당을 통해 변경도 가능하다. 즉, MyClass.i = 234 와 같이 변경해줄 수 있다는 것이다. 물론 함수객체 또한, 사전에 정의한 다른 함수를 이용해서 변경할 수 있는 것이다.

MyClass.i = 1
MyClass.i
>>> 1

def func(a):
    print(a)

MyClass.f = func
MyClass.f( a=1 )
>>> 1

Instance

이번에는 인스턴스 생성하는 것에 대해 알아보자. 앞서 클래스와 객체 그리고 인스턴스에 대해 이야기하면서 인스턴스가 클래스를 통해 생성된다는 것을 알고 있을 거다. 클래스로 인스턴스를 생성할 때는 일반적으로 함수표기법을 따른다. 이 때도 한가지 주의해야할 점이 있다. 파이썬에서 클래스에는 생성자와 소멸자가 있다. 생성자(__init__)은 클래스의 인스턴스가 생성될 때 자동으로 호출되는 함수이고, 소멸자(__del__)은 클래스의 인스턴스가 소멸될 때 자동으로 호출되는 함수이다. 이 때 클래스에 __init__()이라는 메소드가 클래스가 인스턴스를 만들 때 자동으로 호출된다는 것은 이런식으로 해당 클래스의 인스턴스를 만들 때 무조건 초기화되어야하는 attribute들을 __init__ 안에 넣어주어 인스턴스가 생성될 때마다 초기화해줄 수 있게 해준다는 것이다. 좀 더 보자면, 이렇게 인스턴스 생성을 하게 되면 해당 클래스에서 __new__()라는 매직메소드가 호출되어 인스턴스가 새롭게 할당되고 그 뒤에 __init__() 매직메소드가 인스턴스를 초기화하는 것이다. 즉, init이란 추상적인 클래스가 객체로 구체화되어서 메모리에 올릴 때 즉 인스턴스가 새롭게 할당된 직후 실행되는 녀석인 것이다.

class Complex:
    def __init__(self, realpart, imagpart):
        self.r = realpart
        self.i = imagpart

x = Complex(3.0, -4.5)
x.r, x.i
>>> (3.0, -4.5)

그렇다면 이렇게 생성된 인스턴스 객체로는 뭘 할 수 있을까? 오직 attribute reference 뿐이다. 클래스에는 두가지 종류의 attribute가 존재한다. 바로 속성( data attribute )과 메소드( method attribute )다. 우리가 주목해야할 부분은 메소드 부분이다. 보통 메소드는 함수이기에 아래와 같이 객체에서 메소드를 호출할 경우 바로 실행된다. ( 보통 Method 라는 용어는 여기저기서 사용되지만, 헷갈리지 않기 위해서는 클래스 인스턴스 객체의 함수를 지칭할 때 사용되는 것이라고 하는 것이 좋다. 정의라고 하면 "객체에 속하는 함수"정도일 것이다. )

class MyClass:
    """A simple example class"""
    i = 12345

    def f(self):
        return 'hello world'
        
        
 x = MyClass()
 x.f()
 >>> 'hello world'

그러면 이렇게 메소드가 호출될 때 실제로는 어떤 일이 일어날까? 메소드의 특별함은 메소드를 정의할 때 확인할 수 있다. 일반적인 함수와 다르게 클래스 내에서 메소드를 정의할 때는 항상 self를 인자로 제일 먼저 넣어주는 것을 볼 수 있다. 이 self는 뭘 의미하는 것일까? self는 생성된 하나의 인스턴스를 지칭한다. 즉, x.f()가 호출되는 것은 실제로 MyClass.f(x)가 호출되는 것과 동일한 것이라는 것이다. 즉, 인스턴스의 데이터 속성이 아닌 속성이 reference되면 해당 인스턴스의 클래스가 검색되고, 거기서 만약 그 속성이름이 함수객체인 attribute라면, 메소드 객체는 인스턴스 객체와 함수 객체가 packing되면서 생성된다. 그래서 이러한 메소드 객체가 argument와 함께 호출되면, 인스턴스 객체와 그 argument에서 새로운 argument list가 생성되고, 함수 객체가 이 argument list와 함께 호출되는 것이다. 결국은 이 인스턴스가 클래스의 속성이나 행위의 주체이기 때문에 반드시 클래스에 속성이나 행위를 정의할 때 가장 첫 번째 인자로 넣어주어야하는 것이다. self에 대해서는 아래서 더 자세히 알아보도록 하겠다.

추상화

클래스에 대해서 보다 깊게 설명하기 위해서 추상화에 대한 이야기를 간단히 하고 계속 설명해보도록 하겠다. 사실 추상화의 개념은 간단하다. 불필요한 정보는 숨기고 필요한 정보만 표현함으로써 공통의 속성 값이나 행위를 하나로 묶어 이름을 붙이는 것이다. 즉, 클래스에 대해서만 적용되는게 아닌 변수, 함수, 클래스 등에 대해서 모두 적용되는 것으로 우리는 어떤 변수의 값을 알지 못하더라도 해당 변수를 사용할 수 있고, 어떤 기능을 하는 함수의 코드를 알지 못하더라도 그 함수를 사용할 수 있다. 클래스 또한 마찬가지다. 이러한 개념으로 만들어진 리스트 클래스 객체를 살펴봐도 동일하다. 우리는 리스트라는 자료구조가 어떤식으로 구현되어있는지 모르지만, 해당 리스트를 잘 사용하고 있다. 그렇다면 예시를 들어서 클래스에서 추상화가 왜 중요한지 어떻게 되는 것인지 살펴보도록 하자.

💡 사실 클래스를 사용해야할 순간을 알기 위해서는 먼저 절차지향적으로 생각한 후 반복되는 구성과 하나의 집단에서 행위와 속성을 나눌 수 있으면 좋겠다는 생각이 들 때 사용하게 된다.

예시를 하나 들어보자. 우리는 어떤 음성로봇을 하나 만들려고 한다. 이름은 Siri로 하자. 물론 이것 말고도 빅스비, 자비스 등도 만들어보려고 한다. 우선 아래와 같이 음성데이터를 읽어서 분석한 후 답변을 해주는 siri를 만들어보려고 한다. ( 이러한 복잡한 분석, 읽기 등등의 코드를 생략했다고 해보자.)

siri_name = 'siri'
siri_code = 21039788127

def siri_say_hi():
    print("say hello!! my name is siri")

def siri_add_cal():
    return 2+3 

def siri_die():
    # code..
    print('siri die')

즉, 이렇게 절차지향적으로 먼저 생각을 한 후에 이러한 siri와 유사하게 자비스나 빅스비를 만들려고 할 때 생각해보면 결국 음성데이터를 읽어오고, say hi라는 기능을 넣을 것이고, 일련변호와 이름도 필요하고 등등의 이렇게 행위와 속성이 반복되고, 유사할 경우 설계도인 클래스를 작성한 후 독립적인 인스턴스를 찍어내는 것이 훨씬 효율적이라는 생각이 들기에 클래스를 사용하게 되는 것이다. ( 공통의 속성과 행위)  이 때 우리는 아래와 같이 Robot class를 만들어야하는 것이다.

class Robot:
    """
    [Robot class]
    Author : 이효석
    Role: test
    """
    # 클래스 변수
    population = 0                 

    def __init__(self,name,code):
        # 인스턴스 변수
        self.name = name           
        self.code =code 
        
        Robot.population +=1
    
    #인스턴스 메소드
    def say_hi(self):            
        # code..
        print(f"Greetings, my masters call me{self.name}.")
        
    #인스턴스 메소드    
    def cal_add(self,a,b):         
        return a+b
        
    #인스턴스 메소드
    def die(self):                 
        
        print(f"{self.name}is being destroyed!")
        Robot.population-=1
        if Robot.population ==0:
            print(f"{self.name}was the last one.")
        else:
            print(f"There are still {Robot.population}robots working")
    
    
    # 클래스 메소드
    @classmethod                   
    def how_many(cls): 
        print(f"We have {cls.population} robots.")
        
        
        

        
print(Robot.population)
>>> 0
siri = Robot("siri", 210397881237)
print(Robot.population)
>>> 1
javis = Robot("javis", 123456781290)
print(Robot.population)
>>> 2
bixby = Robot("bixby", 12341234123)
print(Robot.population)
>>> 3

siri.say_hi()
>>> Greetings, my masters call mesiri.
javis.say_hi()
>>> Greetings, my masters call mejavis.

Robot.how_many()
>>> We have 3 robots.

이렇게 실제로 구체화한 인스턴스로 만들어내야한다 즉, 우리가 원하는 데이터로 찍어내야한다. 이렇게 데이터, 인스턴스 안에 여러가지 메소드, 그리고 속성값이 있는데, 이 속성값과 메소드를 공통된 클래스 하나로 묶어서 개별 인스턴스 들로 찍어내는 것으로 이것이 이렇게 묶는 것이 추상화다. 즉, 이렇게 추상화된 클래스를 사용하면 우리는 say_hi 라는 복잡한 빅데이터 분석 기반의 어떤 기술에 대한 복잡한 코드를 전혀 모르지만, 이 메소드를 사용을 할 수 있다는 것이고, 결국 이렇게 추상화라는 특성덕에 우리는 불필요한 정보(복잡한 코드) 없이 필요한 정보만 볼 수 있는 것이다. 따라서 함수도 추상화라는 특성을 가진 것이고, 이러한 클래스도 추상화인 것이다.

위의 예시에서는 지금까지 보지 못했던 것들이 보인다. 우리가 Class를 사용할 때 알아야할 개념들을 지금부터 하나하나 설명하겠다. 

Class Method, Class Variable, Instance Method, Instance Variable

우리는 Class를 통해 각각의 Instance들을 찍어낸다. 이 때, 모든 인스턴스들이 공유해야하만 하는 경우 가 있을 때 어떻게 해야할까? 이를 위해 사용하는 것들이 클래스 변수와 클래스 메소드이다. 예를 들자면, 인스턴스를 찍어낼 때 마다 변하는 행위나 속성이 있을 수 있고, 이러한 속성은 인스턴스 각각의 namespace에서 컨트롤할 수 없다. 따라서, 클래스라는 namespace에서 인스턴스들이 공유하는 변수와 메소드를 정의하는 것이다.

그런데 모든 인스턴스들이 생성될 때마다 독같이 초기화가 되는데 굳이 이러한 인스턴스 변수들과 구분하는 이유가 뭐냐는 착각이 들 수 있다. 포인트는 공유라고 생각한다. 위의 예시처럼 로봇클래스가 있고, 인스턴스가 생성될 때마다 현재까지 몇개의 로봇이 생성되었는지를 확인하고자 한다고 해보자. 그렇다면 이 로봇의 개수를 담은 변수는 로봇 인스턴스가 생성될 때마다 초기화되는 것이 아닌 그대로 가지고 있다가 추가되어야할 것이다. 그리고 이 변수는 각 로봇 인스턴스가 모두 공유하고 있어야할 것이다. ( 변할 때 모두 변한다 ) 이 때 사용하는 것이 바로 클래스 변수이다. 즉, 클래스 변수란 인스턴스들이 공유하는 공유변수인 것이다. 이러한 클래스 변수는 클래스 내에서 정의되는 변수로 인스턴스의 네임스페이스에 속한 attribute가 아니기에, self를 붙이지 않는다.

동일한 맥락에서 클래스 메소드 또한 정의된다. 인스턴스들이 공유하는 어떤 행위를 정의할 필요가 있을 때 사용되는 것이다. 하지만, 클래스 메소드의 경우 인스턴스와 다르게 파라미터로 자기자신의 class를 전달해줘야한다. ( 인스턴스 메소드가 자기자신 인스턴스 객체인 self를 전달했던 것과 상반된다.) 이러한 클래스 메소드는 파이썬 내장 데코레이터인 @classmethod를 통해서 데코레이팅을 해줘야한다. 추가적으로 이러한 클래스 메소드는 주로 인스턴스들이 공유하는 클래스 변수에 접근하거나 변경을 하고 싶을 때 주로 사용한다.

class Robot:
    """
    [Robot class]
    Author : 이효석
    Role: test
    """
    # 클래스 변수
    population = 0                 

	# code ...
    
    # 클래스 메소드
    @classmethod                   
    def how_many(cls): 
        print(f"We have {cls.population} robots.")

Class & Instance Namespace

그렇다면 이쯤에서 기존의 인스턴스들의 namespace와 클래스 namespace를 적절히 구분하며 인스턴스 변수, 인스턴스메소드, 클래스 변수 그리고 클래스 메소드를 더 명확히 알아보자.

우선 namespace란 앞서 파이썬에서의 Scoping Rule을 다루면서 "파이썬에서 모든 것은 객체로 되어있어, 각각의 객체의 특정 이름과 그 객체가 매칭관계를 가지게 되는데 이러한 mapping을 포함하고 있는 전체 공간을 namespace라고 한다."고 했다. 이를 좀 더 간단히 표현하자면 객체를 구분할 수 있는 범위, 공간 정도이겠다. 

파이썬에서 어떤 객체의 namespace에 접근하는 방법으로는 __dict__를 사용하는 방법과 dir()을 사용하는 방법이 있다. __dict__ 매직 메소드를 통해 확인하는 경우, 우리는 객체의 namespace를 확인가능한 dict형식으로 반환받을 수 있다. 따라서, 해당 namespace의 각 name과 그 객체에 해당하는 것을 같이 반환해준다. 반면 dir() 함수의 경우 파이썬의 built-in function으로 객체의 namespace를 좀 더 사용자가 보기에 편하게 반환해주는 형태로 해당 namespace에 어떤 name들이 있는지를 보여주게 된다.

print(siri.__dict__)
>>> {'name': 'siri', 'code': 210397881237 }

print(dir(siri))
>>> ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'cal_add', 'code', 'die', 'how_many', 'name', 'population', 'say_hi', 'this_is_robot_class']

그런데 결과를 보면 이상하다는 생각이 든다. 동일한 인스턴스에 대해서 namespace를 다르게 확인했을 뿐인데, 가지고 있는 name들이 다른 것이다. 왜 이럴까? 사실 __dict__ 매직메소드의 경우 실제로 physical하게 메모리 단계에서의 namespace를 보여주는 것이다. 반면 dir의 경우 실제로 사용가능한 모든 name들의 리스트를 보여주는 것이기에 위와 같은 인스턴스 객체에 대해서 dir을 통해 확인하면 메모리에 올라가 있지 않은 인스턴스 객체 namepace내의 name들에 대해서도 확인가능해 더 많아 보이게 되는 것이다.

이를 통해 class 객체와 instance 객체의 namespace를 한번 확인 비교 해보자.

siri.__dict__
>>> {'code': 210397881237, 'name': 'siri'}

Robot.__dict__
>>> mappingproxy({'__dict__': <attribute '__dict__' of 'Robot' objects>,
              '__doc__': '\n    [Robot class]\n    Author : 이효석\n    Role: test\n    ',
              '__init__': <function __main__.Robot.__init__>,
              '__module__': '__main__',
              '__weakref__': <attribute '__weakref__' of 'Robot' objects>,
              'cal_add': <function __main__.Robot.cal_add>,
              'die': <function __main__.Robot.die>,
              'how_many': <classmethod at 0x7f1630a23050>,
              'population': 1,
              'say_hi': <function __main__.Robot.say_hi>,
              'this_is_robot_class': <staticmethod at 0x7f1630a23c10>})

우리는 여기서 이상한 점을 느낄 수 있다. 우선, instance namespace안에 인스턴스 변수는 있는데, 인스턴스 메소드는 없다. 왜 이런걸까? 파이썬은 메모리의 효율성을 위해 클래스 namespace안에 인스턴스 메소드를 저장해두기 때문이다. 그렇다면 인스턴스를 통해 인스턴스 메소드에 접근이 어떻게 가능한 것일까? 여기서 앞서 살펴봤던 scoping rule을 떠올릴 수 있다. 파이썬은 내부적으로 해당 인스턴스의 namespace에서 탐색 후 없다면 해당 인스턴스를 찍어낸 클래스의 namespace로 이동하여 해당하는 변수나 메소드를 찾게 된다. 이러한 원리로 우리는 인스턴스를 통해 클래스 변수와 메소드 그리고 인스턴스 메소드에 접근할 수 있는 것이다. 물론 한가지 주의할 점은 class namespace에 인스턴스 메소드가 있다고 해서 클래스 객체를 통해서 인스턴스 메소드에 접근하고자 한다면 오류가 난다. 그 이유는 인스턴스 메소드가 동작하기 위해 필요한 인스턴스 객체 self에 해당하는 인자가 없기 때문이다. 다르게 말하자면 클래스 자체에서 인스턴스 메소드나 변수에 접근하려고 한다면 해당하는 인스턴스가 어떤 인스턴스인지를 확인할 수 없기 때문에 접근할 수 없는 것이다.

또한 class namespace에 있는 클래스 변수와 클래스 메소드들은 당연히 class의 namespace를 찍었을 때 보여야한다. 그런데, __module__ 혹은 __doc__ 같은 것들이 namespace안에 있는 것처럼 보인다. 우리는 정의한 적도 없는데! 이를 이해하기 위해서는 매직 메소드에 대해서 더 살펴봐야 한다. 해당 내용은 이 글을 참고하자.

Self

그럼 우리는 이제 class와 instatnce의 namespace를 보면서 각 attribute에 대해 더 알 수 있었다. 이를 바탕으로 앞서 배웠던 self에 대해서 보다 자세히 이해해보도록 하자. 우리는 앞서 self는 해당 클래스로 찍어지는 인스턴스 객체를 의미한다고 했다. 이 말이 조금 모호하게 느껴질 수 있을 것 같아 자세히 뜯어보며 self가 인스턴스 객체 그 자체임을 확인해보려 한다. 아래와 같은 예시를 살펴보자.

SelfTest라는 클래스에 대해서 우리는 Class를 통해 self에 접근해 해당 self의 주소를 반환하는 함수와 ( 클래스 메소드 ) Instance를 통해 self에 접근해 해당 self의 주소를 반환하는 함수( 인스턴스 메소드 ) 를 통해 self가 인스턴스 그 자체라는 것을 강조해서 확인해보려고 한다. 결국 인스턴스 객체의 주소와 Class 안에 있는 self의 주소가 실제로 동일하다는 것을 확임함으로써 둘은 완전히 동일한 것으로 self가 인스턴스 객체 그 자체라는 것을 보려는 것이다. 또한, cls가 클래스 그 자체를 가리킨다는 것 또한 func1이라는 클래스 메소드를 통해 확인해보려고 한다. 

class SelfTest:
    
    name="anamony"
    
    def __init__(self,x):
        self.x = x
        
    @classmethod
    def func1(cls):
        print(f"cls : {cls}")
        print("class만의 cls 주소", id(cls))
        print("func1")
        
    def func2(self):
        print(f"self:{self}")
        print("class만의 self주소:",id(self))
        print("func2")

test_obj = SelfTest(17)
test_obj.func2() 
SelfTest.func1()
print("인스턴스의 주소 : ", id(test_obj))
print("클래스의 주소 : ", id(SelfTest))

print('*****************************************************')

t2 = SelfTest(18)
t2.func2()
SelfTest.func1()
print("인스턴스의 주소 : ", id(t2))
print("클래스의 주소 : ", id(SelfTest))
>>> self:<__main__.SelfTest object at 0x0000024C5B75CD00>
>>> class만의 Self주소: 2526975216896
>>> func2
>>> cls : <class '__main__.SelfTest'>
>>> class만의 cls 주소 93871411808608
>>> func1
>>> 인스턴스의 주소 :  2526975216896
>>> 클래스의 주소 :  93871411808608
>>> *****************************************************
>>> self:<__main__.SelfTest object at 0x0000024C5B751B50>
>>> class만의 Self주소: 2526975171408
>>> func2
>>> cls : <class '__main__.SelfTest'>
>>> class만의 cls 주소 93871411808608
>>> func1
>>> 인스턴스의 주소 :  2526975171408
>>> 클래스의 주소 :  93871411808608

이렇게 우리는 클래스 객체의 주소와 cls의 주소가 동일해 cls와 클래스 객체는 완전히 동일한 것이고, 인스턴스 객체의 주소와 self의 주소가 동일해 self와 인스턴스 객체는 완전히 동일하다는 것을 확인할 수 있다.

이렇게 우리는 OOP의 첫 단계로 클래스에 대해서 충분히 이해해봤다. 하지만, 이 때 우리는 추가적으로 알아야할 것들이 있다. 바로 OOP 원칙이다. 지금껏 클래스와 그를 통해 만들어지는 객체와 인스턴스 개념에 대해 배웠는데, 그렇다면 이게 OOP의 전부일까? 전혀아니다. 이렇게 클래스를 사용하는 프로그래밍은 한다고 해서, 무조건 객체지향프로그래밍인 것이 아니다. OOP의 4원칙을 골로루 사용해서 프로그래밍하는 것을 OOP라고하는 것이다. 이런 객체지향적 특징없이 클래스만을 이용하는 코딩방식은 OOP가 아니다. 여기서 강조하고 싶은 것은 정말 OOP를 제대로 사용하고자 한다면 클래스만 사용하는 것에 그쳐서는 안된다는 것이다.

이어지는 내용은 OOP 2번째 글에서 이어서 하도록 하겠습니다! :)

 

 

Reference

https://webclub.tistory.com/155

 

객체지향 프로그래밍이란?

객체지향 프로그래밍 정의 객체지향 프로그래밍(Object Oriented Programming)은 문제를 여러 개의 객체 단위로 나눠 작업하는 방식을 말합니다. 이 방식은 오늘날 가장 많이 사용하는 대표적인 프로그

webclub.tistory.com

윤상석 지식제공자 님의 타입 파이썬! 강의내용 : https://www.inflearn.com/course/%ED%83%80%EC%9E%85-%ED%8C%8C%EC%9D%B4%EC%8D%AC

 

타입 파이썬! 올바른 class 사용법과 객체지향 프로그래밍 - 인프런 | 강의

Python으로 생산성있는 개발만 아니라 견고하고 안전하게, 그리고 확장성있는 개발을 하세요! 🔥, - 강의 소개 | 인프런...

www.inflearn.com

https://docs.python.org/3/tutorial/classes.html

 

9. Classes — Python 3.10.2 documentation

9. Classes Classes provide a means of bundling data and functionality together. Creating a new class creates a new type of object, allowing new instances of that type to be made. Each class instance can have attributes attached to it for maintaining its st

docs.python.org

 

'Python' 카테고리의 다른 글

Python OOP - Part2  (0) 2022.02.13
Python - Magic Method  (0) 2022.02.13
Python - Decorator  (0) 2022.02.02
Python - Variable Length Arguments  (0) 2022.02.02
Python - Iterable / Iterator / Generator  (0) 2022.02.01