먹고 기도하고 코딩하라

[인프런 리프 2기] 2주 - 파이썬 스페셜 메소드 본문

일상

[인프런 리프 2기] 2주 - 파이썬 스페셜 메소드

2G Dev 2021. 3. 21. 01:33
728x90
728x90

이전 글 보기

 

[인프런 리프 2기] 2주 - 파이썬 클래스 심화

이전 글 보기 [인프런 리프 2기] 1주 - 환경 설정 오늘은 인프런 리프 1주차 미션인 섹션 0과 1을 정리해 보겠습니다. 제가 수강하는 강의는 [우리를 위한 프로그래밍], 파이썬 중급 과정 인프런 오

dev-dain.tistory.com

 

 

오늘은 인프런 리프 2주차 미션인 섹션 3을 정리해 보겠습니다.

 

 

제가 수강하는 강의는 [우리를 위한 프로그래밍], 파이썬 중급 과정 인프런 오리지널 강의입니다.

 

 


섹션 3 : 파이썬 스페셜 메소드

저번 섹션 2에서 클래스 내에서 쓸 수 있는 빌트인 메소드 몇 가지를 간단하게 살펴봤는데요. 섹션 3은 섹션 2의 연장격으로, 생성, 삭제, 문자열로 출력 외에도 유용하게 사용할 수 있는 매직 메소드들을 배우고 직접 사용해 봤습니다.

섹션 3은 다음과 같은 내용들을 다룹니다:

 

(1) 클래스 인스턴스 간의 사칙연산(덧셈, 뺄셈, 곱셈 등)과 대소비교 메소드
(2) namedtuple 개념과 사용법

 

 

3-1. 인스턴스 간의 사칙연산과 대소비교 메소드

보통 int나 float, str 같은 primitive 타입의 값들은 같은 타입의 값들끼리 서로 사칙 연산 및 비교 연산을 할 수 있습니다. int나 float는 혼용해서 사칙, 비교 연산도 가능하고요.

 

a = 3 + 5	# 8
b = 7.3 - 3.7	# 3.5999999999999996
c = 'this is ' + 'python'	# this is python

단, 다른 타입, 예를 들어 int와 str 간에는 사칙 연산이 불가능합니다. 이 때 흥미로운 점은 연산자의 왼쪽의 것이 str이냐 int냐에 따라서 에러 메시지가 조금 다르게 나온다는 것입니다. 요지는 str과 다른 타입은 그냥 사칙 연산을 할 수는 없다는 것입니다. 어찌 생각하면 당연한 얘기죠?

 

d = 'this is ' + 3
# 결과 : TypeError: can only concatenate str (not "int") to str
e = 3 + 'three'
# 결과 : TypeError: unsupported operand type(s) for +: 'int' and 'str'

 

비교 연산의 경우도 마찬가지입니다. 다만, str의 경우에는 앞글자부터 아스키 코드 값을 비교해서 아스키 코드가 더 높은 쪽이 크다고 판별합니다. str 간의 대소 비교에서 문자열의 길이는 아스키 코드 값보다는 덜 중요합니다.

 

print(3 > 5)	# False
print(3.14 < 5)	# True
print('algebra' > 'zebra')	# False
print(7 < 'cereal')
# 결과 : TypeError: '<' not supported between instances of 'int' and 'str' 

그럼 같은 클래스 인스턴스끼리 어떻게 사칙 연산과 대소비교 연산을 할 수 있을까요? 저번에 썼던 Student 클래스를 다시 소환해서 인스턴스를 만들어 보고, 연산을 해 보겠습니다.

 

# 클래스 선언
class Student:
  student_num = 0

  def __init__(self, name, major, year):
    self._name = name
    self._major = major
    self._year = year
    Student.student_num += 1

  def __str__(self):
    print("{}'s major: {}, year: {}".format(self._name, self._major, self._year))

  def sayHi(self):
    print('%s, Hi!' % self._name)

  @classmethod
  def transfer_student(cls, num):
    cls.student_num = num

  @staticmethod
  def isSenior(s):
    if s._year == 4:
      return '{} is senior.'.format(s._name)
s1 = Student('Alice', 'Computer Science', 3)
s2 = Student('Bob', 'English Literature', 4)

print(s1 + s2)
# 결과 : TypeError: unsupported operand type(s) for +: 'Student' and 'Student' 
print(s1 > s2)
# 결과 : TypeError: '>' not supported between instances of 'Student' and 'Student'

보시는 것처럼 덧셈 연산과 대소비교 연산이 제대로 안 되고 있네요. 지원되지 않는 연산자 타입이라 그렇다는 건데요. 이렇듯 클래스 인스턴스끼리 덧셈이나 대소비교 연산을 해야 할 때는 내장된 매직 메소드(스페셜 메소드)를 클래스 안에 정의해 줘야 합니다.

 

사칙 연산에 필요한 매직 메소드들은 다음과 같습니다. 

# 덧셈
def __add__(self, other):
  # 코드

# 뺄셈
def __sub__(self, other):
  # 코드

# 곱셈
def __mul__(self, other):
  # 코드

# 나눗셈
def __div__(self, other):
  # 코드   

섹션 2에서 배운 것을 토대로 우리는 이제 self가 첫 번째 인자로 들어온 걸 보고 이것들이 인스턴스 메소드임을 알 수 있습니다. 2번째 인자는 other은 다른 객체가 될 수도 있고, 아니면 다른 값이 될 수도 있습니다. other을 같은 클래스의 인스턴스로 넣을 생각이라면 메소드 안을 인스턴스 변수 속성을 이용한 연산을 하는 것으로 꾸미면 되겠죠.

예를 들어, s1 + s2를 했을 때 name이 합쳐져서 나오는 연산을 하고 싶다면 이렇게 하면 되는 겁니다.

 

def __add__(self, other):
  return self._name + " & " + other._name
  
print(s1 + s2)	# Alice & Bob

마찬가지로, 다른 사칙 연산들도 비슷하게 하면 됩니다.

대소비교 연산도 이와 비슷하게 하시면 됩니다. 대소비교에 쓰이는 매직 메소드들은 다음과 같습니다.

 

def __lt__(self, other):
  # 코드

def __le__(self, other):
  # 코드

def __gt__(self, other):
  # 코드

def __ge__(self, other):
  # 코드

def __eq__(self, other):
  # 코드

def __ne__(self, other):
  # 코드

순서대로 <, <=, >, >=, ==, != 입니다. lt랑 gt가 헷갈리실 수 있는데 왼쪽이 오른쪽보다 적은 게 lt, 오른쪽보다 큰 게 gt입니다. 

lt와 le, gt와 ge의 차이점은 초과/미만인지, 이상/이하인지의 차이입니다. lt는 less than, le는 less than or equal to이니까 풀네임으로 기억하시면 더 쉬울 것 같습니다.

예를 들어, s1 < s2를 했을 때 학년을 비교하고 싶다면 다음과 같이 쓰면 됩니다.

 

def __lt__(self, other):
  return self._year < other._year	# True

 

 

3-2. namedtuple 개념과 사용법

namedtuple(이하 네임드튜플)을 살펴보기 전에 일반적 튜플을 살펴 보겠습니다. 소괄호 안에 값을 감싸는 튜플은 리스트와 여러모로 비슷하지만, 내부의 값을 재할당할 수 없다는 특징을 가지고 있습니다.

x값 3.0, y값 2.5를 갖는 2차원상의 점 좌표를 나타내는 변수 p1을 튜플로 만들어 봅니다.

 

p1 = (3.0, 2.5)
p1[1] = 4.5
# 결과 : TypeError: 'tuple' object does not support item assignment

콜렉션의 네임드튜플 자료구조를 사용하면 튜플인데 마치 딕셔너리처럼 쓸 수 있게 됩니다. 차이가 있다면, 딕셔너리는 키로만 값 접근이 가능하지만 네임드튜플은 키와 인데스 모두로 접근이 가능하다는 점입니다. 그러면서도 값 재할당이 불가능하다는 튜플 고유의 특징은 가져갑니다.

그럼 인덱스로도 접근이 가능하고 변경이 불가능한 딕셔너리인가.. 하면 단순히 그렇게 설명하기에는 뭔가 부족합니다. 코드를 보면서 자세히 얘기해 보죠. 

 

일단 import는 다음과 같이 해 주면 됩니다.

 

from collections import namedtuple

이건 강의에서 다룬 예제를 한 번 써 보겠습니다. 위에서 만든 (3.0, 2.5) 좌표를 네임드튜플을 이용해서 한 번 만들어 보겠습니다.

 

Point = namedtuple('Point', 'x y')
print(Point)	# <class '__main__.Point'>
pt3 = Point(3.0, 2.5)
pt4 = Point(2.5, 1.5)

print(pt3)	# Point(x=3.0, y=2.5)
pt3.y = 7.0
# 결과 : AttributeError: can't set attribute

namedtuple은 2가지 인자를 받는데요. 첫 번째는 새로 만들어질 타입 이름, 두 번째는 필드 이름들입니다.

위의 경우, Point 타입을 만드는데 x와 y라는 속성을 갖는 타입을 새로 만든다는 것입니다. 이렇게 되면 클래스 형식으로 튜플이 추상화되는데요. 데이터 관리 측면에서는 위의 튜플보다는 훨씬 낫겠죠? x, y라는 이름이 있어서 [0], [1] 이런 식으로 접근하지 않아도 개별 값에 접근이 가능하니까요. 

그래서 Point 자체를 print해 보면 마치 클래스처럼 main의 Point라는 타입으로 나오는 걸 확인할 수 있습니다. 일종의 틀이 된 거죠. pt3, pt4 같은 경우에는 클래스의 새로운 인스턴스 만들듯이 값을 넣어서 쓸 수 있고요.

하지만 네임드'튜플'이기 때문에 값을 변경하는 것은 불가능합니다. 값을 변경하려고 하면 AttributeError가 뜹니다.

 

파이썬 문서에서 네임드튜플을 좀 더 자세하게 설명하고 있는데요.

namedtuple(typename, fields_name, *, rename=False, defaults=None, module=None)은 네임드튜플은 typename(여기선 Point)으로 된 새로운 튜플 서브클래스를 반환합니다. 이 서브클래스는 튜플 비슷한 객체를 만드는 데 쓰입니다. 

자세한 설명은 문서에서...

 

이 네임드 튜플을 선언하는 방법은 여러 가지가 있는데요.

 

Point1 = namedtuple('Point', ['x', 'y'])  # 리스트 안에 넣어 구분
Point2 = namedtuple('Point', 'x, y')  # 쉼표 구분
Point3 = namedtuple('Point', 'x y') # 띄어쓰기로 구분
# rename=False일 때는 class가 예약어라서 쓸 수 없음. 4개를 입력받음
Point4 = namedtuple('Point', 'x y x class', rename=True)  # Default = False

이렇게 4가지 방법으로 쓸 수 있습니다.

첫 번째는 리스트 안에 넣어서 구분하는 것이고, 두 번째는 str 하나로 쓰되 필드 이름을 쉼표로 구분하기, 세 번째는 띄어쓰기로 구분하기입니다.

네 번째는 한눈에 봐도 좀 특이한데요. 일단 띄어쓰기로 구분은 되어 있는데 x가 중복이 되어 있고, class라는 키워드를 필드 이름으로 쓰기까지 했습니다. rename이라는 옵션에도 값을 줬고요. 과연 네 번째는 어떻게 쓰는 걸까요? 일단 4개의 튜플 서브클래스를 만들었으니 한 번 만들어 봅시다.

 

p1 = Point1(x=10, y=35) # 명시적으로 쓰기
p2 = Point2(20, 40)
p3 = Point3(45, y=20)
p4 = Point4(10, 20, 30, 40) # 3개만 쓰면 안 됨

각 서브클래스마다 인스턴스를 하나씩 만들어 봤는데요. p1이나 p3처럼 명시적으로 어떤 필드에 어떤 값을 줄지 결정할 수도 있고, p2처럼 그냥 쓰면 필드 순서대로 값이 들어가기도 합니다.
p4는 4개 값이 들어갔는데요. 과연 어떻게 처리된 걸까요? 한 번 보겠습니다.

 

print(p1)	# Point(x=10, y=35)
print(p2)	# Point(x=20, y=40)
print(p3)	# Point(x=45, y=20)
# x는 중복되고 class는 예약어인 상태로 지정하면 _2, _3 이런 식으로 할당됨
print(p4)	# Point(x=10, y=20, _2=30, _3=40)

p1부터 p3까지는 그렇다 쳐도 p4 결과가 좀 특이한데요. 중복이 된 x 자리와 예약어 키워드인 class 자리는 이름이 나오지 않고 _2, _3처럼 필드가 할당된 것을 확인할 수 있습니다. rename을 쓴 이유가 바로 여기에 있는데요. rename이 False일 때는 ValueError가 뜹니다.

 

Point4 = namedtuple('Point', 'x y x class', rename=False)
# 결과 : ValueError: Type names and field names cannot be a keyword: 'class'  

rename을 True로 하면서 겹치는 필드, 예약어 이름의 필드까지 쓸 수는 있게 해 주지만 의도대로 되지는 않고 좀 바꿔서 보여주는 걸 볼 수 있습니다. 제 생각엔 특별한 사유가 있지 않다면 저렇게 쓰는 건 지양해야 할 것 같네요.

 

참고로 딕셔너리 형식으로 되어 있는 것도 딕셔너리의 키-값 쌍 개수가 네임드튜플 필드 개수와 맞고, 딕셔너리 키가 필드 이름 목록과 완전히 같다면 튜플 서브클래스 형식으로 변환이 가능합니다. 딕셔너리를 이용해서 새로운 서브클래스 인스턴스를 만든다는 말이 더 정확하겠네요.

 

temp_dict = {'z': 75, 'y': 55}
p5 = Point3(**temp_dict)	# 언패킹을 할 때 **을 쓸 것
print(p5)	# Point(x=75, y=55)

 

네임드튜플의 간단한 변수와 메소드를 살펴 보겠습니다. _fields, _make(), _asdict()를 살펴볼 건데요. 

먼저 _fields네임드튜플의 필드 이름만 확인하는 것입니다. 다음과 같이 쓸 수 있습니다.

 

print(p1._fields)	# ('x', 'y')
print(p4._fields)	# ('x', 'y', '_2', '_3')

다음은 _make()입니다. _make()는 새로운 네임드튜플 서브클래스 인스턴스를 생성하는 건데요. 위에서 살펴본 네임드튜플 생성법과는 조금 다르게 사용합니다. 

 

temp = [52, 38] # Point1의 키 개수와 맞아야 함
p3 = Point1._make(temp) # _make로 개수를 맞춤
print(p3)	# Point(x=52, y=38)

위에서 Point1(52, 38)과 같이 썼다면 여기서는 Point1._make([52, 38])과 같이 사용합니다. 결과적으로는 별 차이가 없는 것 같지만, 만약 값이 배열이나 튜플로 되어 있다면 굳이 따로 빼내서 쓰기보다는 _make()를 쓰는 게 더 편리할 듯합니다.

다음은 _asdict() 입니다. _asdict()는 서브클래스 인스턴스를 OrderedDict로 변환해서 반환해 주는데요. 방금 딕셔너리를 네임드튜플로 바꾸는 방법은 다뤘지만 네임드튜플을 딕셔너리로 바꾸는 방법은 다루지 않았죠? 여기서 이렇게 바꾸면 됩니다.

 

print(p1._asdict())	# {'x': 10, 'y': 35}
print(p4._asdict())	# {'x': 10, 'y': 20, '_2': 30, '_3': 40}

OrderedDict는 기존 딕셔너리와 달리 리스트나 튜플처럼 요소 간 순서가 정해져 있습니다. 만약 딕셔너리인데 키 간 순서가 변하지 않아야 한다면(json 파일로 데이터를 내보낼 때 등), OrderedDict를 쓰는 게 더 좋겠습니다.

 

 


 

 

이번 시간에는 파이썬 클래스의 스페셜 메소드, 그리고 namedtuple이라는 새로운 자료구조를 살펴 봤습니다.

클래스 인스턴스 간의 사칙 연산과 대소비교 연산은 예전에 해 봐서 약간 수월한 부분이었는데 네임드튜플이 생각보다 약간 까다로운 부분이었습니다. 솔직히 네임드튜플을 이 강의에서 처음 접해 봅니다. 딕셔너리와 비슷하지만 미묘하게(사실 많이) 다른 새로운 자료구조는 마치 클래스와 비슷하게 사용할 수 있다는 점에서 흥미로운 자료구조입니다.

그러고 보니 네임드튜플에 커스텀 메소드를 만들 수 있는지 모르겠네요. 만들 수 있다면 클래스와 별반 다를 바가 없을 것 같다는 생각이 듭니다.

 

나머지 자세한 내용은 강의에서 확인해 보시기 바랍니다. ㅎㅎ
다음에는 3주 미션으로 새롭게 포스팅하겠습니다.

 

 

이 포스팅은 '인프런 리프' 활동의 일환으로, <우리를 위한 프로그래밍 : 파이썬 중급> 강의를 무료로 제공받고 수강한 후기를 담고 있습니다.

 

 

 

 

References

 

 

다음 글 보기

 

[인프런 리프 2기] 3주 - 파이썬 시퀀스

오늘은 인프런 리프 3주차 미션인 섹션 4를 정리해 보겠습니다. 제가 수강하는 강의는 [우리를 위한 프로그래밍], 파이썬 중급 과정 인프런 오리지널 강의입니다. 섹션 4 : 파이썬 시퀀스 파이썬

dev-dain.tistory.com

 

728x90
반응형
0 Comments
댓글쓰기 폼