메뉴 바로가기 검색 및 카테고리 바로가기 본문 바로가기

한빛출판네트워크

한빛랩스 - 지식에 가능성을 머지하다 / 강의 콘텐츠 무료로 수강하시고 피드백을 남겨주세요. ▶︎

IT/모바일

파이썬 데이터 클래스 빌더: 정의, 주요 기능, 코드스멜

한빛미디어

|

2024-12-04

|

by 루시아누 하말류

1,659

데이터 클래스는 어린이 같다. 

처음에는 그럭저럭 봐주지만, 성숙한 객체 노릇을 하려면 어느 정도 책임을 져야 한다.

― 마틴 파울러, 켄트 벡

 

파이썬은 기능은 거의 없고 단지 필드를 모아 놓은 간단한 클래스를 만드는 방법을 몇 가지 제공한다. 이런 패턴을 데이터 클래스라고 하며, dataclasses는 이러한 패턴을 지원하는 패키지 중 하나다. 이번 장에서는 데이터 클래스를 만드는 데 사용하는 다음 세 가지 클래스 빌더를 알아본다.

 

• collections.namedtuple

파이썬 2.6 이후부터 사용할 수 있는 가장 간단한 방법이다.

 

• typing.NamedTuple

파이썬 3.5 이후부터 사용할 수 있고, 필드에 자료형 힌트를 명시해야 한다. 파이썬 3.6에는 class 구문이 추가되었다.

 

@dataclasses.dataclass

앞의 두 방법보다 커스터마이징하는 옵션을 더 많이 제공하며 더 복잡하다. 파이썬 3.7 이후부터 사용할 수 있다.

 

이 클래스 빌더를 설명한 후에는 데이터 클래스가 왜 코드 악취 code smell 의 대명사가 되었는지 설명한다. 코드 악취는 객체지향 설계가 부실할 때 나타나는 증상을 가진 코딩 패턴을 의미한다.

 

✅데이터 클래스 빌더란 무엇인가?

 

아래 예제처럼 지리적 위치 좌표 쌍을 나타내는 간단한 클래스가 있다고 생각해 보자.

 

class Coordinate:
	def __init__(self, lat, lon):
	self.lat = lat
	self.lon = lon

 

Coordinate 클래스는 위도와 경도 속성을 보관한다. 여기서 틀에 박힌 __init__ ( ) 메서드를 작성하는 일은 특히 클래스 안에 속성에 두 개 이상이면 금세 지루해진다. 각 속성을 세번씩 언급해야 하기 때문이다! 그리고 틀에 박힌 이 코드는 파이썬 객체가 제공해야 할 기본적인 기능조차 제공하지 않는다.

 

>>> from coordinates import Coordinate 
>>> moscow = Coordinate(55.76, 37.62) 
>>> moscow
<coordinates.Coordinate object at 0x107142f10> ❶
>>> location = Coordinate(55.76, 37.62) 
>>> location == moscow ❷ 
False 
>>> (location.lat, location.lon) == (moscow.lat, moscow.lon) ❸ 
True

 

❶ object로부터 상속받은 __repr__ ( )은 그다지 도움 되지 않는다.
❷ 동등 비교 연산자(== )도 의미가 없다. object로부터 상속받은 __eq__ ( ) 메서드는 객체의 ID를 비교하기 때문이다.
❸ 두 좌표를 비교하려면 각 속성을 명시적으로 비교해야 한다.


이번 장에서 설명할 데이터 클래스 빌더는 필수적인 __init__ ( ), __repr__ ( ), __eq__ ( ) 메서드를 자동으로 구현해 줄 뿐만 아니라 여러 유용한 기능을 제공한다.

 

namedtuple은 사용자가 지정한 이름과 필드를 가진 tuple의 서브클래스를 만드는 팩토리 함수이다. 

이 함수로 Coordinate 클래스를 만드는 과정은 다음과 같다.

 

>>> from collections import namedtuple 
>>> Coordinate = namedtuple(‘Coordinate’, ‘lat lon’) 
>>> issubclass(Coordinate, tuple) 
True 
>>> moscow = Coordinate(55.756, 37.617) 
>>> moscow
Coordinate(lat=55.756, lon=37.617) ❶ 
>>> moscow == Coordinate(lat=55.756, lon=37.617) ❷ 
True

 

❶ __repr__ ( )이 쓸만하게 출력한다.
❷ __eq__ ( )가 의미 있게 비교한다.


더 새로운 typing.NamedTuple은 똑같은 기능을 제공하지만, 각 필드의 자료형을 지정할 수 있다.

 

>>> import typing 
>>> Coordinate = typing.NamedTuple(‘Coordinate’, ... [(‘lat’, float), (‘lon’, float)]) 
>>> issubclass(Coordinate, tuple) 
True 
>>> typing.get_type_hints(Coordinate) {‘lat’: <class ‘float’>, ‘lon’: <class ‘float’>}

 

파이썬 3.6 이후 PEP 526–변수 어노테이션 구문 Syntax for Variable Annotations 문서에 명시한 대로 class 문 안에 자료형 어노테이션과 함께 typing.NamedTuple도사용할 수 있게 되었다. 이렇게 하면 가독성도 좋아지고 메서드를 오버라이드하거나 추가하기 쉽다.

 

아래 예제는 똑같은 Coordinate 클래스이지만, float 형 속성과 함께 55.8°N, 37.6°E과같은 형식으로 좌표를 출력하는 __str__ 사용자 정의형이 있다.

 

from typing import NamedTuple

class Coordinate(NamedTuple):
	lat: float 
	lon: float
	
	def __str__(self):
		ns = ‘N’ if self.lat >= 0 else ‘S’
		we = ‘E’ if self.lon >= 0 else ‘W’
		
return f’{abs(self.lat):.1f}°{ns}, {abs(self.lon):.1f}°{we}’

 

typing.NamedTuple이 생성한 __init__ ( ) 메서드에는 class 문에 등장하는 매개변수들의 순서대로 필드들이 들어간다.

typing.NamedTuple과 마찬가지로 @dataclass 데커레이터도 PEP 526을 지원해 인스턴스 속성을 선언하게 해 준다. 데커레이터가 변수 어노테이션을 읽고 클래스의 메서드를 자동으로 생성한다. 

 

아래는 @dataclass 데커레이터를 사용해 Coordinate 클래스를 정의한다. 이 두 방법을 비교해 보기 바란다.

 

from dataclasses import dataclass

@dataclass(frozen=True) class Coordinate:
	lat: float
	lon: float

	def __str__(self):
		ns = ‘N’ if self.lat >= 0 else ‘S’
		we = ‘E’ if self.lon >= 0 else ‘W’
		return f’{abs(self.lat):.1f}°{ns}, {abs(self.lon):.1f}°{we}’

 

 앞서 살펴본 __str__ 사용자 정의형을 이용한 예제와 @dataclass 데커레이터를 이용한 예제에서 정의한 클래스 본체는 똑같고 class 문 자체만 다르다는 것에 주의하자. 

 

@dataclass 데커레이터는 상속이나 메타클래스에 의존하지 않으므로 이런 메커니즘의 사용에 영향을 주지 않는다.  @dataclass 데커레이터를 이용한 예제는 Coordinate 클래스는 object의 서브클래스이다.

 

 

데이터 클래스 빌더의 주요 기능

 

서로 다른 데이터 클래스 빌더들 사이에는 공통점도 많은데, 정리하면 아래 표와 같다. 

여기서 x 는 해당 데이터 클래스의 인스턴스를 나타낸다.

 

특징namedtupleNamedTuple@dataclass
가변 인스턴스아니오아니오
예클래스 구문아니오
딕셔너리 생성x._asdict()x._asdict()dataclasses.asdict(x)
필드명 가져오기x._fieldsx._fields[f.name for f in dataclasses.fields(x)]
기본값 가져오기x._field_defaultsx._field_defaults[f.default for f in dataclasses.fields(x)]
필드 자료형 가져오기N/Ax.__annotations__ x.__annotations__
변경 후 새 인스턴스 생성x._replace(...)x._replace(…)dataclasses.replace(x, ...)
실행 시 새 클래스namedtuple(...)NamedTuple(…)dataclasses.make_ dataclass(...)

 

이제부터 앞에서 정리한 주요 기능을 알아보자.


가변 인스턴스
이 클래스 빌더들 간의 가장 큰 차이점은 collections.namedtuple과 typing.NamedTuple 은 tuple의 서브클래스를 만들기 때문에 인스턴스가 불변형이라는 것이다. 기본적으로 @ dataclass는 가변 클래스를 만든다. 그러나 @dataclass 데커레이터 예제에서처럼 데커레이터는 키워드 인수 frozen을 받는다. 따라서 frozen=True 옵션으로 생성된 클래스의 인스턴스를 초기화한 후에 필드에 값을 할당하면 그 클래스가 예외를 발생시킨다.


• 클래스 구문
typing.NamedTuple과 @dataclass만 일반적인 class 구문을 지원하므로, 생성하는 클래스에 메서드와 독스트링 docstring 을 더 쉽게 추가할 수 있다.


• 딕셔너리 생성
명명된 튜플의 두 변형(collections.namedtuple과 typing.NamedTuple )은 _asdict ( ) 인스턴스 메서드를 사용해 데이터 클래스 인스턴스의 필드로 dict 객체를 생성하게 해 준다. dataclasses 모듈은 dict 객체를 생성하는 dataclasses.asdict ( ) 함수를 제공한다.

 

• 필드명 및 기본값 가져오기
클래스 빌더 세 개 모두 필드명과 (설정된 경우) 기본값을 가져올 수 있게 해 준다. 명명된 튜플 클래스에서는 이 메터데이터가 _fields와 _fields_defaults 클래스 속성에 저장된다.


@dataclass로 데커레이트된 클래스에서는 이 메타데이터를 dataclasses 모듈이 제공하는 fields ( ) 함수를 이용해 가져올 수 있다. 이 함수는 name과 default 등 여러 속성이 있는 Fields 객체의 튜플을 반환한다.


• 필드형 가져오기
typing.NamedTuple과 @dataclass를 이용해 정의된 클래스는 __annotations__ 클래스 속성에 필드명과 자료형을 대응시키는 매핑을 가진다. 그러나 앞서 얘기한 대로 __annotations__ 에 직접 접근하지 말고 typing.get_type_hints ( ) 함수를 사용하는 편이 좋다.


• 속성을 변경해 인스턴스 새로 만들기
명명된 튜플 객체 x가 있을 때 x._replace (**kwargs )를 호출하면 주어진 키워드 인수에 따라 변경된 속성을 가진 객체를 새로 만들어 반환한다. 모듈 수준 함수인 dataclasses.replace (x, **kwargs )도 이와 마찬가지로 @dataclass로 데커레이트된 클래스의 객체를 만들어 반환한다.


• 실행 시 새 클래스 생성
class 구문이 읽기는 더 좋지만 하드코딩되었다. 프레임워크에서는 실행 시 데이터 클래스를 만들어야 할 때가 있다. 이럴 때는 collections.namedtuple과 typing.NamedTuple이 제공 하는 기본적인 함수 호출 구문을 사용하는 편이 좋다. 이와 똑같은 용도로 dataclasses 모듈도 make_dataclass ( ) 함수를 제공한다.

 

✅코드 악취로서의 데이터 클래스

 

지금까지 데이터 클래스 빌더가 제공하는 기능과 장점을 살펴보았다. 그러나 데이터 클래스를 사용할 때, 이 클래스가 설계상 문제를 드러내는 신호일 수 있다는 점도 염두에 두어야 한다.


마틴 파울러와 켄트 벡은 리팩터링이 필요함을 나타내는 코드 패턴에 관한 ‘코드 악취’ 카탈로그를 제시했다. 이 책의 ‘데이터 클래스’ 절에서는 다음과 같이 설명한다.

 

데이터 클래스란 데이터 필드와 게터/세터 메서드로만 구성된 클래스를 말한다. 그저 데이터 저장 용도로만 쓰이다 보니 다른 클래스가 너무 깊이까지 함부로 다룰 때가 많다.

 

마틴 파울러의 개인 웹사이트에는 ‘코드 악취’라는 제목의 글이 있다. 이 글에서는 데이터 클래스를 코드 악취 사례의 하나로 사용하고 해결 방법을 제시한다. 다음은 해당 글 전체를 옮겨온 것이다. 

 

코드 악취 (마틴 파울러)


코드 악취는 보통 시스템 깊은 곳에 있는 문제를 표면에 드러내는 일종의 지표다. 이 용어는 필자의 저서 『리팩터링 2판』(한빛미디어, 2020)의 작성을 도와준 켄트 벡이 만들었다.


앞에 나온 간단한 정의는 두 가지 사소한 점을 지적한다. 

첫 번째, 코드 악취는 빨리 감지할수 있는 것(필자의 최근 표현으로는 악취를 맡을 수 있는 것)으로 정의된다. 아주 긴 메서드가 좋은 사례다. 수십 줄이 넘는 자바 코드는 단지 코드를 쳐다보기만 해도 필자의 코를 실룩거 리게 한다.


두 번째, 코드 악취가 늘 문제를 나타내는 것은 아니다. 메서드가 길더라도 나쁘지 않을 수 있다. 그 안에 문제가 있는지 더 깊이 살펴봐야 한다. 본질적으로 코드 악취는 그 자체로 문제라 기보다는 문제가 있음을 가리키는 지표일 때가 많다.


최고의 코드 악취는 감지하기 쉽고 대체로 실제 문제를 찾아내도록 이끌어 준다. 데이터 클래스(처리하는 코드는 없고 단지 데이터만 있는 클래스)가 좋은 사례다. 클래스를 보고 이 클래스가 어떤 작동을 해야 하는지 자문해 보라. 그러면서 처리하는 코드를 넣으며 리팩터링하게 된다. 간단한 질문과 기본적인 리팩터링은 무기력한 객체를 정말 멋진 품위가 있는 무언가로 바꾸는 중요한 시작점이 될 수 있다.


코드 악취의 장점 중 하나는 경험이 없고 진짜 문제가 있는지를 평가할 지식이 충분하지 않은 사람도 문제를 쉽게 찾아낼 수 있다는 점이다. ‘금주의 코드 악취’를 골라 악취를 찾아내고 선임 개발자에게 문제를 제기하라고 하는 수석 개발자들에 관한 이야기를 들었다. 한 번에 하나씩 코드 악취를 찾아내다 보면 팀원들을 더 훌륭한 개발자로 만들 수 있다.

 

객체지향 프로그래밍의 핵심 개념은 데이터와 행위를 클래스라는 하나의 단위에 통합하는 것이다. 클래스가 널리 쓰이지만, 그 자체로 의미 있는 작업을 수행하지 않는다면 그 인스턴스를 다루는 코드가 시스템에 산재한 메서드와 함수에 분산될 수 있다(더 심하면 중복으로 분산된다). 유지보수할 골칫거리를 만드는 비결이다. 

 

그러므로 마틴 파울러의 리팩터링 기법은 행위를 다시 클래스 안에 넣음으로써 데이터 클래스 문제를 처리한다.
이러한 점을 염두에 두더라도, 행위를 거의 하지 않는 데이터 클래스를 만드는 게 타당한 경우가 두 가지 있다.

 

✔️ 스캐폴딩으로서의 데이터 클래스


이 시나리오에서 데이터 클래스는 프로젝트나 모듈을 새로 시작하기 위한 클래스를 초기에 간단히 구현한 것이다. 시간이 지나면서 클래스는 인스턴스에 연산을 수행하려고 다른 클래스의 메서드에 의존하는 대신, 클래스 자체에 메서드를 추가한다. 

 

스캐폴딩 scaffolding 은 일시적인 발판 으로서, 결국에는 초기의 빌더에서 떨어져 나와 완전히 독립적인 사용자 정의 클래스가 된다. 파이썬은 간단한 문제를 해결하거나 실험할 때 사용되기도 하므로, 이럴 때는 스캐폴딩을 그대로 놔둬도 문제가 되지 않는다.


✔️ 중간 표현으로서의 데이터 클래스


데이터 클래스는 JSON이나 기타 교환 포맷으로 익스포트될 레코드를 만들거나 시스템 경계를 넘어 방금 임포트된 데이터를 보관하는 데 도움이 될 수 있다. 파이썬 데이터 클래스 빌더는 모두 인스턴스를 평범한 dict로 변환하는 메서드나 함수를 제공하며, 언제나 생성자를 호출할때 딕셔너리 언패킹 연산자(** )를 이용해 dict 형 데이터를 키워드 인수로 사용할 수 있다. 이러한 dict 형 데이터는 JSON 레코드와 상당히 비슷하다.


이 시나리오에서는 데이터 클래스 인스턴스의 필드가 가변형이더라도 인스턴스를 불변형 객체 처럼 다루고 인스턴스의 값을 변경하지 않아야 한다. 값을 변경하면 데이터와 행위를 통합하는 객체지향 프로그래밍의 장점이 사라진다. 임포트하거나 익스포트하면서 값을 변경해야 할 때는 딕셔너리 메서드나 표준 생성자를 사용하지 않고 빌더 메서드를 직접 구현해야 한다.

 


위 콘텐츠는 『전문가를 위한 파이썬(2판)』에서 내용을 발췌하여 작성하였습니다.

댓글 입력
자료실

최근 본 상품0