개발을 하다보면 객체의 상태가 업데이트 될 때 변경 지점을 감지해야 되는 상황 등, 같은 타입 오브젝트 간의 차이(difference)를 감지해야 되는 상황이 자주 발생한다.
해당 타입이 dictioanry
이거나 primitive type
이면 쉽게 코드를 작성 할 수 있으나, 클래스의 인스턴스인 경우 조금 난감 할 수 있다. 객체가 프로퍼티로 다른 객체를 들고 있을 경우에는 정확한 변경 지점 파악이 어렵다.
이번 포스팅에서는 파이썬(Python)의 __dict__
를 통해 이를 간단하게 구현해 볼 예정이다.
__dict__
는 해당 클래스의 프로퍼티 네임을 key로 값을 value로 가지는 dictionary를 리턴하는 매직 메소드이다.
테스트 용도로 사용될 3가지 클래스를 정의해보자. 주문이라는 도메인을 가정하고 상품(product), 주문자(person), 주문(order) 세가지 클래스를 아래와 같이 정의한다. 주문(Order)는 orderer와 product 객체를 프로퍼티로 가진다.
class Product:
def __init__(self, name, price):
self.name = name
self.price = price
def __eq__(self, other):
return self.__dict__ == other.__dict__
class Person:
def __init__(self, name, phone_number):
self.name = name
self.phone_number = phone_number
def __eq__(self, other):
return self.__dict__ == other.__dict__
class Order:
def __init__(self, orderer, product, amount, remark=None):
self.orderer = orderer
self.product = product
self.amount = amount # 수량
self.remark = remark # 특이사항
Person과 Product의 __eq__
가 오버라이딩 되어 있는 모습을 볼 수 있다.
__eq__
는 == 연산자 사용시 호출되는 매직 메소드이다. 가장 상위 객체인 object에게 상속받은 __eq__
는 객체의 id(memory reference 등)를 비교하기 때문에 is
와 같은 결과를 나타낸다. 때문에 값의 동일성을 보장하고자 오버라이딩했다
__eq__
는 구현 편의를 위해 dict를 이용해 구현했다. 클래스는 각자 다른 의미에서 동등성을 가질 수 있다.
다음은 두 객체간의 차이를 리턴하는 find_diff
를 구현해보기 전에 우리가 원하는 결과를 먼저 확인해보자. 원래 두괄식 학습이 효과가 좋은 법이다.
# 김씨가 오브젝트 책을 개당 32500원 3권을 주문했다.
kim = Person("kim", "010-3331-9631")
book = Product("Object", 32500)
book_order = Order(kim, book, 3)
# 이씨는 110000원 가격의 레오폴드 키보드를 한 개 주문했다.
lee = Person("lee", "010-3331-9631")
keyboard = Product("Leopold", 110000)
keyboard_order = Order(lee, keyboard, 1, "please add key cap")
김씨와 이씨는 개발자 부부로 자녀 학원비를 위해 하나의 휴대폰 번호를 공유해서 쓰고 있다.
김씨는 32500원 가격의 오브젝트 책 3권을 주문, 이씨는 110000원 가격의 레오폴드 키보드 하나를 주문했다.
두 주문은 휴대폰 번호를 제외한 모든 프로퍼티들이 다르다. 우리가 구현하고자 하는 find_diff
함수는 아래와 같은 결과를 리턴한다.
print(find_diff(book_order, keyboard_order))
# [{'orderer': [{'name': 'lee'}]}, {'product': [{'name': 'Leopold'}, {'price': 110000}]}, {'amount': 3}, {'remark': None}]
정확히 orderer
의 휴대폰 번호를 제외한 모든 필드들이 출력되었다. find_diff
함수는 source와 dest를 파라미터로 받아서, source와 다른 dest의 프로퍼티들을 array 형태로 리턴한다. 함수 구현은 다음과 같다.
def find_diff(source, dest):
if isinstance(source, (int, float, bool, str, list, set, dict, tuple)):
return dest
diffs = list()
for order_attr, source_value in dest.__dict__.items():
dest_value = source.__dict__[order_attr]
if source_value != dest_value:
diffs.append({order_attr: find_diff(source_value, dest_value)})
return diffs
Order가 Product 객체를 가지는 것처럼, 프로퍼티로 객체를 가지는 경우가 있을 수 있어 재귀로 구현했다.
재귀를 짜면 항상 탈출조건을 생각해야한다. 탈출조건은 primitive type
또는 기본 cotainer type
로 잡았다. 두 타입을 확인하기 위해서 type
과 ==
이 아니라 isinstance
함수를 사용했다.
isinstance
는 부모 클래스 타입에 대해서도 True를 리턴한다. 때문에
list, set, dict, tuple를 상속하거나 덕타이핑으로 구현한 클래스들에 대해서는 해당 함수는 제대로 동작하지 않는다.
사실 find_diff
는 isinstance
보다 타입 검사가 더 적절한 예시다. 저 많은 타입들을 다 비교하기에는 코드량이 많아져 isinstance
를 사용했다.
함수 내부에서는 __dict__
를 통해 내부 프로퍼티들을 가지는 dictioanry를 통해 필드값을 비교한다. 파이썬이 아니라면 모든 객체마다 내부 프로퍼티를 가지는 Map을 만들어야 한다.
마지막으로 추가적인 테스트는 다음과 같다.
keyboard_order.product = Product("Object", 32500)
print(find_diff(book_order, keyboard_order))
# [{'orderer': [{'name': 'lee'}]}, {'amount': 3}, {'remark': None}]
keyboard_order.product = Product("Object", 31500)
keyboard_order.orderer.name = "kim"
keyboard_order.amount = 3
print(find_diff(book_order, keyboard_order))
# [{'product': [{'price': 31500}]}, {'remark': None}]
이번 포스팅에서는 파이썬이 제공하는 매직메소드를 통해 간단하게 오브젝트간의 차이점을 리턴하는 함수를 만들어봤다. __dict__
가 아니라면 객체간의 필드들을 직접 비교하는 식으로 구현이 되거나 __dict__
같은 메소드를 모든 객체에 구현해야 한다.
Java 같은 언어에서 같은 함수를 구현할려면 난이도가 올라간다. 구현하는 방법으로 당장 떠오로는건 리플렉션을 이용하거나 프로퍼티를 가지는 Map을 리턴하면 되는데, 후자는 프로퍼티가 변경될 때마다 함께 변경되어야 한다. 또 사용하고자 하는 객체마다 구현해야 되는데 얼마나 번거로운가...
사실 전문가를 위한 파이썬
이라는 책을 읽을며 매직 메소드가 인상깊어서 관련 내용을 포스팅해야지 하다, 두 객체간의 상태 비교를 할 상황이 생겨 이번 포스팅을 작성하게 되었다.
이번에는__dict__
와 __eq__
만 사용해 매직 메소드를 제대로 활용했다는 느낌이 강하게 들지는 않는다. 매직 메소드를 구현한 덕타이핑에 대해서도 포스팅을 하고 싶은데 마땅히 주제가 생각이 안난다. 생각이 나는대로 포스팅하도록 하겠다.
객체간의 diff를 구해야 될 상황이라면 DeepDiff라는 라이브러리도 있으니 참고 바란다.