[Python] __str__와 __repr__의 차이 살펴보기

[Python] __str__와 __repr__의 차이 살펴보기

2019, Mar 25    

0. 목차

  1. 들어가며
  2. 이해
  3. __str__, __repr__의 공통점
  4. 차이점
  5. 마치며
  6. 자료 출처


1. 들어가며


오늘은 파이썬에서 사소해보이지만 생각보다 중요하고, 또 은근히 헷갈리는 두 메소드에 대해 다룬다. 바로 __str__와 __repr__로서 이 둘은 객체를 사용자가 이해할 수 있는 문자열로 반환하는 함수다. 이 둘에 대해 어렴풋이 알고 있었는데 그 차이를 정확하게 알지는 못했다. 방금 생각나는 김에 다시 알아봤다.

그래서 오늘 포스트는 이 두 메소드에 대해 각각 알아보고 둘의 공통점과 차이점에 대해 살펴보겠다. 구분이 매우 까다롭지는 않지만 파이썬 OOP에 대한 기본적인 이해가 필요한 부분이기도 하다. 들어가보자.


2. 이해


이 장에서는 __str__, __repr__ 두 메소드에 대해 각각 알아본다.


2.1. str, __str__

‘__str__‘를 본 적이 없는 사용자라도, str 함수는 많이 써봤을 것이다. 어떤 작업 후 정수나 소수 등의 자료형을 출력하거나, 접합(concatenating) 등의 작업을 할 때 str을 통해 가공하기 때문이다.

def add_expr(a, b):
    return str(a) + ' + ' + str(b)

>>> add_expr(3, 5)

'3 + 5'

str은 입력 받은 객체의 문자열 버전을 반환하는 함수다. 위의 함수는 숫자를 받아 str 함수를 통해 정수를 문자열로 변환했다.

이때 기억할 것은 str은 사실 내장 함수가 아니고, 파이썬의 기본 내장 클래스라는 것이다. 우리가 ‘str(3)’ 처럼 입력하는 것은 내장 함수 str을 실행하는 것이 아니고 사실 내장 str 클래스의 생성자 메소드를 실행하고, 그 인자로 3을 주는 것과 같다. str이 클래스라는 것은 help(str) 만 입력해보면 바로 확인할 수 있다.


그러면 이 절의 제목에서 보듯 str과 __str__는 무슨 상관이 있을까? 이 부분은 파이썬의 OOP와 관련이 있다. 파이썬에는 내장된 많은 자료형들에, 해당하는 자료형에 대한 연산을 정의하는 메소드들이 있다. 그 메소드들은 메소드의 이름 앞뒤에 ‘__‘(double underscore)를 지니고 있다.

일례를 들어보자. 파이썬에서는 모든 변수들이 클래스이고 인스턴스다. 정수 ‘3’에 대해서 우리는 이 값이 내장 int 클래스의 인스턴스임을 안다.

>>> isinstance(3, int)

True

isinstance 내장 함수는 첫 인자가 두 번째 인자의 인스턴스인지의 여부를 반환하는 함수다. 위를 통해 3이 ‘int’ 클래스의 인스턴스임을 확인할 수 있다.

이때 우리는 ‘3 + 5’ 등의 식을 어디서든 실행시킬 수 있음을 안다. 여기에 적을 필요도 없을 정도로 자명하다. 근데 이것이 어떻게 실행되는지가 중요한데, int 클래스에서는 ‘+’ 연산을 처리하는 __add__ 메소드를 정의하고 있고, ‘+’ 기호가 들어왔을 때 이 메소드가 실행되는 구조다.

다시 말해 어떤 값에 대해 ‘+’, ‘-‘, ‘*=’, ‘>>’ 등의 연산자를 취하는 것은 내부적으로 ‘__add__’, ‘__sub__’, ‘__imul__’, ‘__rshift__’ 메소드를 실행하는 것과 동일하다. 예를 통해 확인하자.

# 1.
>>> 3 + 5  # 내부적으로 밑 문장을 실행!
>>> (3).__add__(5)  # '(3)'처럼 ()로 감싸야 한다. 소수와 구별해야 하기 때문이다.

8

# 2.
>>> [1, 2, 3] + [4, 5, 6]  # 내부적으로 밑 문장을 실행!
>>> [1, 2, 3].__add__([4,5,6])

[1, 2, 3, 4, 5, 6]

첫 번째 예에서 ‘3 + 5’는 ‘3’이라는 정수 인스턴스에 대해 __add__ 메소드를 호출한다. 그 값은 ‘5’를 받아 새로운 정수 8을 반환하게 된다. 1번에서 두 문장은 완전히 동일하다.

두 번째 예는 같은 ‘+’ 연산자에 대해 클래스마다 다른 구현이 되어 있음을 보여주고 있다. list 자료형은 ‘+’ 연산에 대해 값을 더하는 것이 아닌 접합(concatenate)을 하고 새로 생성된 list를 반환한다. 각종 파이썬의 연산들에는 이런 마법들이 많이 있다. 실제로 클래스들에서 구현하는 위와 같은 메소드들을 ‘Magic method’라고 하며 매우 많은 목록이 존재한다.(밑의 자료 출처에서 확인하면 된다.)


str와 __str__, repr과 __repr__의 관계도 이와 동일하다. 어떤 객체 object에 str, repr 함수를 씌우면 해당 객체의 클래스에 정의되어 있는 __str__, __repr__ 메소드가 해당 객체에 실행되고, 두 메소드에 있는 코드를 실행한다.

다시 말해 str, repr 함수가 인스턴스의 __str__, __repr__ 메소드를 각각 호출한다고 이해할 수 있다. 앞서 살펴본 __add__ 등도 똑같다.


2.2. repr, __repr__

repr은 ‘Representation’의 약자로 이 단어는 ‘표현하다’라는 뜻을 가지고 있다. 이 단어는 내 블로그의 ‘REST에 대한 고찰’ 포스트를 정독한 사람들이라면 쉽게 이해할 수 있다. 표현은 어떤 객체의 ‘본질’보다는 외부에 노출되는, 사용자가 이해할 수 있는 객체의 모습을 표현한다. REST에서 다룬 내용과 똑같다.(어떻게 이렇게 겹치는지! 모든 것은 연결되어 있다.)

repr 함수는 어떤 객체의 ‘출력될 수 있는 표현’(printable representation)을 문자열의 형태로 반환한다. 다시 말해 해당 객체를 설명해줄 수 있는, 그리고 화면에 출력될 수 있는 문자열 표현을 반환하는 것이다. repr 함수를 적용해보자.

import math

>>> repr(3)
>>> repr([1, 2, 3])
>>> repr(math)


'3'
'[1, 2, 3']
"<module 'math' from ...>"


repr, __repr__의 관계도 앞서 설명한 예와 동일하다. 어떤 객체를 인자로 해서 repr 함수를 실행하면 해당 객체의 클래스에 정의된 __repr__를 실행해 그 결과를 반환한다.


3. __str__, __repr__의 공통점


다음 두 장에서는 이 둘의 공통점과 차이점에 대해 다룬다. 이 둘은 차이점이 더 아리송한데 공통점을 다루는 것도 의미가 있다.

  • 두 메소드는 객체의 문자열 표현을 반환한다.
    • 확인했듯이 두 메소드는 객체가 어떤 데이터 타입이든지간에 객체의 문자열 표현을 반환한다. 이는 중요한 질문을 수반한다. ‘왜 문자열 표현인가?’ 그 이유는 일반적인 문자열 평문(plain text)은 파이썬을 사용하는 모든 인간들이 이해할 수 있는 Universal interface이기 때문이다. 유닉스의 철학 교리 중 프로그램이나 설정파일을 평문으로 작성하라는 설명은 이를 대변한다. 명심하자. 프로그램을 사용하는 것은 인간이고 인간에게 유익해야 한다.


4. 차이점



4.1. 둘의 주요 차이점

사실 이 포스트를 보는 분들이라면 둘의 공통점보다는 차이점에 관심이 있을 것이다. 2장 ‘이해’를 마치고 나면 둘을 적용한 예시들의 결과가 너무나도 유사함을 느낄 수 있다. 사실 그렇기 때문에 둘의 차이점을 느끼지 못하는 것이기도 하다. 이 둘의 차이는 본질적으로 의도된 사용처가 다르다는 데서 기인한다.

  • __str__는 태생적인 목적 자체가 인자를 ‘문자열화’해 반환하라는 것이다.평문 문자는 Universal Interface이기 때문에, 서로 다른 데이터 타입이 상호작용하는 좋은 인터페이스가 된다.(인터페이스라는 데 주목하라.)

이를 극적으로 보여주는 사례가 바로 그 누구나 아는 print라는 함수이다. 아시다시피 이 함수는 인자를 제한없이(0개 이상) 받을 수 있는데 그 사례를 보자.

>>> a = 1
>>> b = '가'
>>> c = [1, 2, 3, 4, 5]

>>> print(a, b, c)

1  [1, 2, 3, 4, 5]

a, b, c는 서로 다른 테이터 타입의 변수이다. 그런데 print 함수는 인자들의 타입을 묻지도 따지지도 않고 문제없이 연결해서 출력했다. ‘[1] + 1’ 같은 문장을 실행시키면 TypeError가 발생하는 것과는 대조적이다. 이것이 가능한 것은 a, b, c에 해당하는 int, str, list 자료형이 각 객체를 ‘문자열’로 반환하는 __str__ 메소드를 내부적으로 구현하고 있고, 문자열은 Universal Interface이기 때문에 출처가 서로 완전히 다른 자료형임에도 문자열화된 인자들을 매끄럽게 이을 수 있었기 때문이다.

따라서 __str__의 본질적인 목적은 객체를 ‘표현’하는 것(representation)에 있다기보다는 추가적인 가공이나 다른 데이터와 호환될 수 있도록 문자열화하는 데 있다고 하겠다.


위의 예에서 추론할 수 있는 재미있는 부분은 print가 인자들을 str화해서 출력한다는 것이다. 이게 사실일까? 간단한 실험을 해보자.

class A:
    def __str__(self):
        return 'str method is called'

    def __repr__(self):
        return 'repr method is called'

>>> a = A()
 
>>> str(a)  # 1.
>>> a  # 2.

# Look at here!
>>> print(a)  # 3.

'str method is called' # 1.
repr method is called  # 2.

# Look at here!
str method is called   # 3.

더 설명이 필요한가?

다음은 repr에 대해 살펴보자.


  • __repr__은 본 목적이 객체를 인간이 이해할 수 있는 평문으로 ‘표현’하라는 것이다.

__str__가 서로 다른 자료형 간에 인터페이스를 제공하기 위해서 존재한다면, __repr__은 해당 객체를 인간이 이해할 수 있는 표현으로 나타내기 위한 용도이다.

둘은 비슷해보이지만 본질적으로는 다른 것이다. 위의 예시들에서 기본 내장 데이터 타입에 대해 두 함수의 반환값이 매우 비슷했던 것은 기본 내장 클래스에서 객체를 ‘표현’하는 것과 다른 데이터 타입과의 상호작용을 위한 ‘인터페이스’가 되는 것이 비슷했기 때문이다. 하지만 꼭 비슷할 필요는 없다. 우리는 두 함수의 결과가 다른 클래스를 만들 수 있다.



4.2. __str__, __repr__가 다른 예 만들어보기

말이 나온 김에 두 함수가 서로 다른 결과를 가질 수 있다는 것을 예시를 통해 확인하자.

내가 다음과 같은 문제 상황을 느꼈다고 가정하자.

내가 기존의 내장 int 클래스에 큰 불만을 느꼈다고 하자. 그 이유는 기존의 int 클래스는 인스턴스를 표현할 때(repr를 실행할 때) 그 인스턴스의 메모리에 저장된 값을 반환했다. 하지만 ‘10 ** 100’라는 값을 같는 두 변수가 있을 때 이 둘을 정말로 같다고 할 수가 있을까? 서로 다른 메모리 위치에 우연히 같은 값이 위치할 수도 있는 거잖아?

철학의 달인인 나는 뭔가 실존이라는 위대한 이유로 이는 잘못됐다고 생각했고 정수를 나타내는 자료형의 표현(repr)을 값이 아닌 고유한 메모리 주소값으로 만들기로 했다.

이에 대한 해결책으로 아까 만든 A 클래스와 int를 동시에 상속받는 NewInt 클래스를 만들자. 대신 A 클래스의 __str__, __repr__는 조금 변형할 생각이다.

class A:
    # __str__ 은 구현하지 않는다.(다시 말해 overriding하지 않는다.)
    
    def __repr__(self):
        return str(id(self))

>>> a = A()
>>> a

140249540632704

###

# 1. A와 int를 다중상속하는 NewInt를 만든다.
class NewInt(A, int):
    pass


# 2. 인스턴스 생성
>>> n = NewInt(5)

# 3. __add__ 메소드 호출
>>> n + 5

10

# 4. __str__ 메소드 호출
>>> str(n)

'5'

# 5. __repr__ 메소드 호출
>>> repr(n)

'140249540506184'


객체의 표현을 값이 아닌 고유한 주소값으로 나타내는 클래스 A를 구현했다. 이 클래스는 __repr__만을 구현하고 있는데 그 결과 인스턴스를 표현할 때 고유한 메모리 주소값이 출력되게 됐다.

이제 나만의 int 클래스인 NewInt를 정의하고 사용하는 예시를 살펴보자.

  1. NewInt 클래스를 정의한다. 이 클래스는 방금 작성한 A와 내장 int 자료형을 다중상속 받아 각각의 특징을 유지할 계획이다.
  2. ‘5’라는 값을 갖는 n 이라는 변수를 할당한다. 파이썬의 내장 int를 밀어낼 대안의 자랑스러운 첫 인스턴스다.
  3. int에서 상속받은 __add__ 메소드를 호출한다. n 의 값은 5이기 때문에 ‘n + 5’는 10을 반환한다.
  4. int에서 상속받은 __str__ 메소드를 호출한다. int에서와 마찬가지로 ‘5’라는 문자열이 반환됐다.
  5. A에서 상속받은 __repr__ 메소드를 호출한다. n의 실제 값이 아닌 고유한 메모리 주소값을 반환한다.

이와 같이 우리가 작성하는 클래스에서 str과 repr이 다른 결과를 반환하도록 하는 것이 얼마든지 가능하다. 둘의 차이를 이해하고 필요할 때 다르게 구현하는 것도 필요할 수 있겠다.


5. 마치며


오늘은 파이썬에서 객체를 표현하는 메소드인 __str__, __repr__를 살펴보았다. 이 두 메소드는 객체를 Universal interface인 평문으로 변환해 반환한다는 공통점이 있었다. 반면 __str__이 객체를 평문화하는 데 방점이 찍혀 있는 데 비해, __repr__는 객체를 표현하는 데 방점이 찍혀 있다는 차이점도 있었다.

포스트를 마치면서 아쉬움이 많이 남는다. 시작할 때는 ‘정말 별 것 아니겠지’라는 마음가짐으로 시작했는데 쓰다보니 조금 길어졌다. 완벽해보이지 않아서 걱정인데 잘 모르겠다. 이렇게 품질이 애매한 포스트를 작성할 때마다 공정하게 누군가에게 평가받고 싶다고 느끼는데, 포스트를 보시는 분이 있다면 의견을 남겨주시면 감사하겠다.

이상 포스트를 마칩니다.


6. 자료 출처