새소식

인기 검색어

개발일기

파이썬 동적 타입 언어와 몽키패칭, 덕타이핑

  • -

들어가면서

파이썬을 사용하다 보면 가끔 몽키패칭과 덕타이핑이란 키워드를 듣게 되는데, 이번 기회에 조사해 보면서 최근 읽는 러닝 파이썬이란 책에서 공부한 내용을 바탕으로 생각을 정리해 보았다

 

단순히 보았을 때 몽키패칭은 테스트 작성시 혹은 외부 모듈 수정시 유용하고, 덕타이핑은 타입 관계 없이 행위로 받아서 사용할 때 유용해 보인다.

몽키패칭

Java만 사용하던 사람들은 깜짝 놀라 기절할 수도 있지만 파이썬에선 이러한 행위가 가능하다.

class BananaClient:
    def request_banana(self):
        return "banana"

banana_client = BananaClient()

class Monkey:
    def banana(self):
        return banana_client.request_banana()

if __name__ == "__main__":
    monkey = Monkey()
    print(monkey.banana()) # banana
    monkey.banana = lambda: "fake banana"
    print(monkey.banana()) # fake banana
    monkey.cry = lambda: "kiki"
    print(monkey.cry()) # kiki

 

1. 인스턴스의 메서드를 동적으로 변경한다는 점

2. 인스턴스의 멤버 변수가 동적으로 추가된다는 점

 

이러한 특징은 테스트할 때 빛을 발한다. 위의 코드에 Monkey.banana()는 BananaClinet로 외부호출(이라 치자)하는 코드를 가진다. 만약 테스트 환경이라면 테스트할 때마다 매번 외부호출을 할 수 없는 노릇이다. 이럴때 몽키패칭하여 호출하는 부분 자체를 덮어씌우면 간단하게 해결 가능하다. 만약 자바였으면? BananaClient를 interface로 뽑고 stub을 만들어 해결했을 것이다.

 

또 몽키패칭을 활용하면 다음과 같은 행위가 가능해진다.

 

import some_library

original_function = some_library.some_function

def patched_function(*args, **kwargs):
    print("함수 호출 전 로깅")
    result = original_function(*args, **kwargs)
    print("함수 호출 후 로깅")
    return result

some_library.some_function = patched_function

 

이런 행위는 외부 모듈에 대한 제어를 직접 가져갈 수 있게 해준다. 이때의 장점은 미처 interface로 뽑지 않았더라도, 부가기능을 달 수 있게 된다. 

 

내 경우에는 최근에 celery로 넘어가는 요청들의 크기를 모니터링해야 할 일이 있었는데, 그때 몽키패칭 활용을 고려했었다.

 

덕타이핑

덕타이핑은 행동시(속성 참조 / 메서드 호출) 타입을 보고 행동하겠다는 것이 아닌, 행동을 가지고 있는지만 보고 판단하겠다는 특성이다.  다음은 덕타이핑의 대표적인 예시이다

 

class Duck:
    def swim(self):
        print("Duck swimming")

    def fly(self):
        print("Duck flying")

class Whale:
    def swim(self):
        print("Whale swimming")

for animal in [Duck(), Whale()]:
    animal.swim()
    animal.fly()

 

결과는 다음과 같다

Duck swimming
Duck flying
Whale swimming
AttributeError: 'Whale' object has no attribute 'fly'

 

코드에서 animal은 타입을 보지 않고, swim이 있는지만 보고 있다면 실행시킨다. 이것이 행위 혹은 속성만 보고 접근한다는 의미이다

 

동적 타입 언어

어떻게 이런 일들이 가능할까? 파이썬이 동적 타입 언어이기 때문이다. 동적 타입언어는 식별자가 타입을 알고 있는 것이 아니라, 값을 보고 그때 타입을 알게 된다. 따라서 타입이 언제든 바뀔 수 있다.

 

내 생각에는 이러한 이유로 파이썬에서 메서드 오버로딩을 지원하지 않는 듯 하다. 메서드 오버로딩을 지원하려면 넘어가는 인자들에 대해 타입을 알 필요가 있다. 그래야만 인자들에 대한 "구분"이 가능하기 때문이다. 식별자에 타입이 명시될 수 없는 파이썬 특성상 시그니처에 args가 들어가지 않는 것은 어찌보면 당연할지도...? 라는 생각이 든다.

 

또한 파이썬의 namespace tree에는 언제든 node가 추가될 수 있고, 덮어씌워질 수도 있는 특성이 있다.

출처: https://www.oreilly.com/library/view/learning-python/1565924649/ch06s05.html

 

파이썬에서 모든 것은 "객체"이고, 이는 상속 구조를 가진다. 특이한 점은 다중상속이 가능하다. 그렇다면 파이썬은 시그니처를 이름으로만 검사하는데 시그니처가 겹치는 현상에 대해선 어떻게 회피할까? 바로 덮어씌우기다.

 

파이썬에서는 호출한 곳에서부터 상속 트리를 타고 따라 올라가며 가장 먼저 내가 호출한 식별자와 같은 식별자가 네임스페이스에 존재하는지 검색한다.

 

 

단례로 위와 같은 코드를 작성하면, cry가 중복되기 때문에 에러가 발생해야 할 것 같지만, 그렇지 않는다. 바로 cry를 덮어쓰기 때문이다. 그렇다면 여기서 덮어쓴다는 의미는 cry가 2개 존재하고, 가장 최근 노드에 kyao를 호출하는 함수 노드가 있어서 그런 것일까?

 

 

dir는 인스턴스가 가지는 속성과 메서드들을 알려주는 함수이다. 이를 찍어봤을 때는 cry가 하나만 나온다. 내가 정확히 아는 것은 아니지만, 내 생각에는 bomi가 가지는 cry에 대한 식별자가 참조하는 게 kyao를 가지는 걸로 대체되고, wow는 참조 해제되는 것 같다. 그리고 gc에 의해 def cry(self): print("wow")는 해제되지 않을까 생각하고 있다.

 

파이썬에서는 모든 것이 객체기 때문에 가능한 방법인 것 같다.

Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.