이전에 클래스의 기본 구조와 필요성에 대해 정리하면서 파이썬이 객체를 어떻게 다루는지 전보다는 깊게 이해할 수 있었다. 이번에는 여기서 더 나아가서 파이썬 내부에서 객체들이 어떻게 상호작용하는지, 그리고 데이터를 더 효율적으로 다루는 방법에 대해 공부했다.
파이썬을 잘 한다는 것은 결국 시퀀스(sequence), 반복(iterator), 함수(functions), 클래스(class) 이 네가지를 얼마나 잘 이해하고 활용하느냐에 달려있다고 한다. 오늘은 그중에서도 클래스안에 숨겨진 매직 메서드와 튜플의 단점을 보완한 네임드 튜플을 공부했다.
평소에 n+100같은 코드를 너무나 자연스럽게 쓴다. 그런데 생각해보면 컴퓨터가 정수 n과 숫자 100을 어떻게 더해야 하는지 어떻게 아는건지 확인해보면, 매직 메서드를 보면 알 수 있다.
파이썬의 모든 데이터는 객체이고, +, -, *같은 연산자를 만나면 내부적으로 약속된 메서드를 호출한다.
+ 연산자 -> __add__ 호출* 연산자 -> __mul__ 호출bool() 형 변환 -> __bool__ 호출구현되어있는 기능이 매직 메서드로 되어있다는 것을 몰랐을 때는 클래스를 만들고 두 객체를 더하려면 s1._price + s2._price 처럼 일일이 속성을 불러와서 더 했어야했다. 그런데 매직 메서드 오버라이딩을 하면 클래스 객체끼리도 직관적인 연산이 가능해진다.
# 벡터(Vector) 연산 예제
class Vector(object):
def __init__(self, *args):
if len(args) == 0:
self._x, self._y = 0, 0
else:
self._x, self._y = args
def __repr__(self):
"""객체의 정보를 개발자가 보기 편하게 출력"""
return "Vector(%r, %r)" %(self._x, self._y)
def __add__(self, other):
""" + 연산자를 만났을 때 호출 """
return Vector(self._x + other._x, self._y + other._y)
def __mul__(self, y):
""" * 연산자를 만났을 때 호출 """
return Vector(self._x * y, self._y * y)
def __bool__(self):
""" (0,0)이면 False, 아니면 True 반환 """
return bool(max(self._x, self._y))
v1 = Vector(5, 7)
v2 = Vector(23, 35)
print(v1 + v2) # 결과: Vector(28, 42)
print(v1 * 3) # 결과: Vector(15, 21)
이런 방법으로 코드를 작성해보니까 특정 경우에 사용하면 가독성이 확실히 좋아질 것으로 보인다. 단순히 기능 구현을 넘어서 내 클래스를 파이썬의 기본 타입처럼 자연스럽게 동작하게 만들 수 있다는게 신기하다.
여기서 *args 로 매개변수를 넣어줬는데 이거는 매개변수의 수를 유연하게 관리할 수 있다. 안넣어도, 여러개를 넣어도 된다.
AI 모델을 다루거나 데이터를 전처리할 때 (x,y) 좌표 같은 데이터를 튜플로 많이 다룬다. 튜플은 가볍고 빠르지만 치명적인 단점이 있다.
"pt[0]이 뭐고pt[1]이 뭐지?"
인덱스로 접근하다 보면 x좌표인지 y좌표인지 헷갈리기도 하고 데이터가 많아지면 인덱스로 구분한다는 것은 실수를 유발할 수 있다고 한다. 이럴때 사용하는 것이 collections 모듈의 네임드 튜플이다.
from collections import namedtuple
from math import sqrt
# 네임드 튜플 선언 (클래스를 만드는 팩토리 함수 느낌이다)
Point = namedtuple('Point', 'x y')
pt1 = Point(1.0, 5.0)
pt2 = Point(2.5, 1.5)
# 인덱스 접근 (비추천)
# dist = sqrt((pt1[0] - pt2[0]) ** 2 + (pt1[1] - pt2[1]) ** 2)
# 키(속성) 접근 (추천) -> 가독성이 훨씬 좋다!
dist = sqrt((pt1.x - pt2.x) ** 2 + (pt1.y - pt2.y) ** 2)
print(dist)
튜플의 성질(생성후 조회만)에 딕셔너리의 느낌(키:밸류)이 추가된거로 보면 이해하기 쉽다.
마치 클래스처럼 pt1.x로 접근할 수 있는데 내부적으로는 튜플이라서 메모리도 적게 먹는다고 한다. 특히 딕셔너리(dict)나 리스트(list)를 네임드 튜플로 변경하는 멕서드들도 있다.
_make(): 리스트를 네임드 튜플로 변환_asdict(): 네임드 튜플을 딕셔너리로 변환네임드 튜플을 공부하다가 문득 import 방식에 대해 궁금한게 생겼다.
from collections import namedtuple을 하면collections안의 다른 기능(deque)은 못 쓰나?
처음엔 단순히 from이 "클래스명 생략" 만 있는 줄 알았다. 위에 처럼 불러와도 collections.deque 라고쓰면 될 줄 알았는데 에러가 났다. 이유를 찾아보니까 파인썬의 임포트 동작원리 때문이었다.
from ... import 를 써도 모듈 전체가 메모리에 로드되는 것은 똑같다. (메모리 절약이 주 목적이 아니란 것을 알았다.)collections. 을 붙여서라도 쓰고 싶다면 import collections로 모듈 이름 자체를 등록해야한다.메모리 절약도 아니면서 못쓰게 막아둔 것은 비효율적으로 보인다. 찾아보니까 모듈 전체를 다 등록 하면 내 코드 아에서 변수 이름 이 라이브러리와 꼬이지 않게 명확하게 관리할 수 있다는 장점이 있다고 한다.
오늘 공부를 통해 리스트 컴프리헨션으로 네임드 튜플 객체들을 한 번에 생성하는 실습도 해봤다.
Classes = namedtuple('Classes', ['rank', 'number'])
ranks = 'A B C D'.split()
numbers = [str(n) for n in range(1, 21)]
# 이중 반복문을 한 줄로!
students = [Classes(rank, number) for rank in ranks for number in numbers]
# 위 과정보다는 아래를 추천한다고한다. (한눈에 보여서)
# 추천 (같은 코드지만 줄바꿈 한거다)
students2 = [Classes(rank, number)
for rank in "A B C D".split()
for number in [str(n) for n in range(1, 21)]]
이게 Pythonic한 코딩이라고 한다. 그리고 오늘 새롭게 배운 팁 중 하나는 처음 보는 라이브러리나 메서드를 만났을 때 당황하지 않고 사용방법이나 이게 무엇인지 확인하는 방법이다.
특히 클래스를 설계할 때 내부에 작성하는 Docstring 은 단순히 메모용이 아니다. 파이썬에서는 __doc__메직 메서드를 통해 언제든 설계 의도를 확인할 수 있다.
내가 직접 클래스를 작성할 때도 아래와 같이 """ """ 구문 안에 적어두면 나중에 다른 개발자나 내가 __doc__ 을 출력해서 설계 의도를 확인가능하다.
class Vector(object):
# 아래가 Docstring 이다.
"""
이곳에 적는 내용이 클래스의 __doc__이 된다.
작성자: 박태정
"""
def __init__(self, *args):
"""이것처럼 메서드 단위로도 __doc__을 사용하여 설명을 남길 수 있다."""
...
실제로 Vector.__doc__뿐만 아니라 Vector.__init__.__doc__ 처럼 메서드 단위로도 작성할 수 도 있다는 점이 처음 알게된 내용이지만 유용하게 쓰일것 같아서 익숙해져야겠다.
오픈소스 라이브러리를 사용할 때 LLM을 사용하기 보다, 코드 자체에 내장된 __doc__을 먼저 확인하는 습관을 들이면 해당 기능의 의도와 사용법을 가장 정확하게 파악할 수 있을 것 같다. LLM의 도움 없이도 스스로 코드를 분석할 수 있는 힘을 기르기 위해 앞으로는 공식 문서와 이 매직 메서드들을 더 가까이해야겠다.