NLP Blog

이펙티브 파이썬 - 3. bytes, str, unicode의 차이점을 알자

|

3. bytes, str, unicode의 차이점을 알자

  • 파이썬 3 의 문자타입
    • bytes : raw 8 bit
    • str : unicode 문자
  • 파이썬 2의 문자타입
    • str : raw 8 bit
    • unicode : unicode 문자

  • 유니코드 문자를 바이너리 데이터 (raw 8 bit)로 표현하는 방법은 많음 (ex: UTF-8)
  • 중요한 것은 파이썬 3의 str 인스턴스와 파이썬 2의 unicode 인스턴스는 연관된 바이너리 인코딩이 없음
    • unicode -> binary data : encode method
    • binary data -> unicode : decode method

  • 파이썬 프로그래밍을 할 때 외부에 제공할 인터페이스에서는 유니코드를 인코드하고 디코드 해야함
  • 프로그램의 핵심 부분에서는 유니코드 문자 타입 사용
    • 문자 인코딩에 대해서는 어떤 가정도 하지 말아야 함
  • 출력 텐스트 인코딩 (이상적으로는 UTF-8)을 엄격하게 유지하면서 다른 텍스트 인코딩 수용 가능

  • 문자 타입이 분리되어 있는 탓에 파이썬 코드에서 일반적으로 다음 두 가지 상황에 부딪힘
    • UTF-8(or 다른 인코딩)으로 인코드된 문자인 raw 8 bit 값을 처리하려는 상황
    • 인코딩이 없는 유니코드 문자를 처리하려는 상황
  • 이 두경우 사이에서 변환하고 코드에서 원하는 타입과 입력값의 타입이 정확히 일치하게 하려면 헬퍼 함수 두 개가 필요
    • 파이썬 3
      • 먼저 str이나 bytes를 입력으로 받고 str을 반환하는 메서드 필요
def to_str(bytes_or_str):
    if isinstance(bytes_or_str, bytes):
        value = bytes_or_str.decode('utf-8')
    else:
        value = bytes_or_str
    return value # str 인스턴스
- `str` or `bytes` 를 받아 `bytes` 반환하는 메서드 필요
def to_bytes(bytes_or_str):
    if isinstance(bytes_or_str, str):
        value = bytes_or_str.encode('utf-8')
    else:
        value = bytes_or_str
    return value # bytes 인스턴스
  • 파이썬 2
    • 먼저 str이나 unicode를 입력으로 받고 unicode를 반환하는 메서드가 필요
def to_unicode(unicode_or_str):
    if isinstance(unicode_or_str, str):
        value = unicode_or_str.decode('utf-8')
    else:
        value = unicode_or_str
    return value # unicode 인스턴스
- `str` or `unicode`를 입력으로 받아 `unicode`를 반환하는 메서드 필요
def to_str(unicode_or_str):
    if isinstance(unicode_or_str, unicode):
        value = unicode_or_str.encode('utf-8')
    else:
        value = unicode_or_str
    return value # str 인스턴스

  • 파이썬에서 raw 8bit 와 unicode 문자를 처리할 때는 중대한 이슈 2개가 있다.
    1. 파이썬 2에서 str이 7bit ascii 문자만 포함하고 있다면, unicodestr 인스턴스가 같은 타입으로 보임
      • 이런 strunicode+ 연산자로 묶을 수 있음
      • = or != 로 비교 가능
      • %s 같은 포맷 문자열에 unicode 인스턴스 사용 가능
      • 파이썬 3에서는 bytesstr은 빈 문자열인 경우에도 절대 같이 않으므로, 함수에 넘기는 문자열의 타입을 신중하게 처리해야함
    2. 파이썬 3에서 내장 함수 open()이 반환하는 파일 핸들을 사용하는 연산 : utf-8
      • 파이썬 2에서는 기본으로 바이너리 인코딩 사용
      • 아래 코드는 python 2에서는 작동하지만, python 3에서는 작동하지 않는다
with open('/tmp/random.bin', 'w') as f:
    f.write(os.urandom(10))

$$$
TypeError: must be str, not bytes
- 문제가 일어난 이유는 파이썬3의 `open`에 `encoding` 인수가 추가되었고 기본값은 `utf-8`
- 따라서 파일 핸들을 사용하는 `read`나 `write` 연산은 바이너리 데이터를 담은 `bytes` 인스턴스가 아니라, 유니코드 문자를 담은 `str` 인스턴스를 기대
- 해결법
with open('/tmp/random.bin', 'wb') as f:
    f.write(os.urandom(10))
- 파일 오픈 시 `rb` 를 사용하여 바이너리 모드임을 알리면 된다.

핵심정리

  • 파이썬 3에서 bytes : raw 8bit, str : unicode
    • > or + 같은 연산자에 bytesstr` 인스턴스를 함께 사용할 수 없다.
  • 파이썬 2에서 str : raw 8 bit, unicode : unicode
    • 만약 7 bit ascii 사용시 연산자에 strunicode 인스턴스 동시 사용 가능
  • 헬퍼 함수를 사용해서 처리할 입력값 원하는 문자 시퀀스 타입 (8 bit, utf-8 encoding, unicode)으로 되어있게 한다.
  • 바이너리 데이터를 파일에서 읽거나 쓸 때는 파일을 바이너리 모드 (rb or wb)로 오픈

Comment  Read more

이펙티브 파이썬 - 2. PEP 8 스타일 가이드를 따르자

|

2. PEP 8 스타일 가이드를 따르자

  • Python Enhancement Proposal #8 , PEP8은 파이썬 코드의 스타일 가이드
    • 문법만 지킨다면 어떻게 코딩하던 에러는 뜨지 않음
    • 하지만 일관성 있는 스타일을 사용한다면
      • 유지보수가 쉬워짐
      • 가독성 높아짐
      • 프로젝트에서의 협업도 가능
  • 파이썬의 진화에 따라 PEP 8도 지속적으로 업데이트 되는중임

  • 중요한 몇가지만 살펴보면….

화이트 스페이스

  • 파이썬에서 공백은 문법적으로 의미있음, 그러므로 영향에 민감한 편

  • 이 아닌 스페이스로 들여쓴다. (vim이나 ide에서 tab을 누르면 4 space로 되도록 설정가능)
  • 문법적으로 의미 있는 들여쓰기는 각 수준마다 스페이스 네 개 사용
  • 한 줄의 문자 길이가 79자 이하여야 함
  • 표현식이 길어서 다음 줄로 이어지면 일반적인 들여쓰기 수준에 추가로 스페이스 네 개를 사용
  • 파일에서 함수와 클래스는 빈 줄 두 개로 구분해야 한다
  • 클래스에서 메서드는 빈 줄 하나로 구분
  • 리스트 인덱스, 함수 호출, 키워드 인수 할당에는 스페이스를 사용하지 않는다
    • ex)
a[0:10] # a는 리스트, 리스트 인덱싱시 : 양 옆에 no space
foo = bar(foo=bar) # 키워드 인수 할당, 함수호출
  • 변수 할당 앞뒤에 스페이스를 하나만 사용한다 (a = 10)


naming

  • PEP 8은 언어의 부분별로 독자적인 명명 스타일을 제안, 각 이름에 대응하는 타입 구별이 쉬움

  • 함수, 변수, 속성lowercase_underscore 형식을 따름
  • protected 인스턴스 속성은 _leading_underscore 형식을 따름
    • protected : getter and setter로 함부로 값을 조회하거나 바꿀수 없도록 보호해 놓은 , 같은 클래스, 패키지, 자손 클래스에서만 접근 가능
  • private 인스턴스 속성은 __double_leading_underscore 형식을 따름
    • private : 같은 클래스 내에서만 접근 가능
  • 클래스예외CapitalizedWord 형식을 따름
  • 모듈 수준 상수ALL_CAPS 형식을 따름
  • 클래스의 인스턴스 메서드에서는 첫 번째 파라미터(해당 객체를 참조)의 이름을 self로 지정
  • 클래스의 메서드에서는 첫 번째 파라미터(해당 클래스를 참조)의 이름을 cls로 지정
    • 클래스 메서드, 인스턴스 메서드 : 인스턴스 메서드의 경우는 그 메서드를 실행한 인스턴스에만 영향을 미치지만, 클래스 메서드를 사용하면 그 클래스로 인해 만들어진 모든 인스턴스에 영향이 간다.


표현식과 문장

  • 파이썬의 계명에는 “어떤 일을 하는 확실한 방법이 (될 수 있으면 하나만) 있어야한다” 라는 말이 있음
  • PEP 8은 표현식과 문장의 본보기로 이 스타일을 정리

  • 긍정 표현식의 부정 (if not a is b) 대신에 인라인 부정(if a is not b)를 사용
  • 길이를 확인할 때 == 사용 안함, 빈 값은 암시적으로 False가 된다고 가정
    • if len(list) == 0 대신 if not somelist
    • 값이 비어있지 않으면 True가 됨
  • 한 줄로 된 if 문, for와 while loop, except 복합문 ㄴㄴ
    • 여러 줄로 나누어서 명료하게 작성
  • 항상 파일의 맨 위에 import
  • 모듈을 임포트할 때는 모듈의 절대이름 사용!
    • 현재 모듈 경로를 기준으로 상대 경로 ㄴㄴ
    • bar 패키지의 foo 모듈을 임포트하려면 import foo ㄴㄴ. from bar import foo ㅇㅇ
  • 상대적인 임포트를 해야 한다면 명시적인 구문 사용 from . import foo
  • 임포트는 표준 라이브러리 모듈 > 서드파티 모듈 > 자신이 만든 모듈 순으로 구분
    • 각각의 섹션에서는 알파벳 순으로 임포트



  • 핵심정리
    • 파이썬 코드를 작성할 때 항상 PEP 8 스타일 가이드를 따르자
    • 큰 파이썬 커뮤니티에서 다른 사람과 원활하게 협업하려면 공통된 스타일을 공유해야 한다.
    • 일관성 있는 스타일로 작성하면 나중에 자신의 코드를 더 쉽게 수정할 수 있다.

Comment  Read more

이펙티브 파이썬 - 1. 사용 중인 파이썬의 버전을 알자

|

1. 사용중인 파이썬의 버전을 알자

  • python 명령어는 보통 python2.7을 가리키지만 때때로 2.6, 2.5를 가르킬 수도 있기 때문에, –version 플래그를 사용하여 버전 확인하자
$ python --version
Python 2.7.8
  • 파이썬3은 보통 python3으로 실행
$ python3 --version
Python 3.5.2
  • 파이썬에 내장된 sys 모듈을 이용하여 파이썬의 버전을 알아낼 수도 있음
import sys
print(sys.version_info)
print(sys.version)

>>>
sys.version_info(major=3, minor=4, micro=2, releaselevel='final', serial=0)
3.4.2 (default, ....
  • 파이썬 2와 3 모두 커뮤니티에서 유지보수 되는중
    • 파이썬 2의 개발은 버그 수정, 보안 강화, 2를 3으로 쉽게 포팅하는 기능 이외에는 중지됨
    • 2to3, six 와 같은 module을 사용하면 파이썬 3과 호환이 쉬워짐
  • 파이썬 3은 새 기능과 향상이 지속적으로 이루어짐.
  • 새로 프로젝트를 실행한다면 파이썬 3 사용을 적극 추천

  • 핵심정리
    • 파이썬의 주요 버전인 파이썬2, 3 모두 활발히 사용됨
    • 파이썬에는 CPython, Jython, IronPython, PyPy 같은 다양한 런타임이 존재함
    • 시스템에서 파이썬을 실행하는 명령이 사용하고자 하는 파이썬 버전인지 확인 필요
    • 파이썬 3 추천

추가 정리 - python runtime

  1. CPython
    • C로 짜여진 파이썬
    • 우리가 가장 흔히 쓰는 파이썬 인터프리터 - 파이썬은 C로 짜여져 있다!
    • Global Interpreter Lock로 인한 멀티쓰레딩 이슈가 있음
  2. CPython
    • 싸이썬은 파이썬의 superset
      • 파이썬이 동적으로 결정되는 부분이 있어서 인터프리팅을 해야함, 그래서 느리다면?
        • 그것을 정적으로 결정하고 컴파일하자! -
    • 인터프리팅이 느리다 > 컴파일이 필요하다 > 모두 다 할 필요가…? > 속도에 치명적인 영향을 주는 부분만 컴파일하자!
  3. PyPy
    • 파이썬으로 작성된 파이썬 인터프리터
    • JIT 기술을 활용해 Cpython보다 빠름!
      • JIT?: Just In Time 컴파일러
        • 파이썬 바이트코드를 인터프리팅 하다가, 자주 사용되는 부분은 컴파일함. 즉 처음에는 느리게 돌아가지만, 점점 빨라 짐
    • 현재 파이썬 구현체중 가장 각광받는 중
  4. Stackless python
    • CPython 구현 시, 파이썬의 함수 호출 스택을 C의 스택에 그대로 얹음
      • 파이썬에서 메모리를 얼마나 쓸 수 있는지와 상관없이 C 스택을 꽉 채우면 스택 오버플로 에러가 뜬다
      • 파이썬 프로그램의 호출 스택(프로그램의 실행흐름)을 CPython 스스로 제어 할 수 없어서 코루틴 등 실행 흐름을 제어하는 언어 기능을 쓸 수 없음
        • 코루틴?: caller가 함수를 call하고, 함수가 caller에게 값을 return하면서 종료하는 것에 더해 return 하는 대신 suspend(or yeild)하면 caller가 나중에 resume하여 중단된 지점부터 실행을 이어나갈 수 있음
    • 이러한 단점들 때문에, Cpython 소스를 수정하여 C스택 쓰는 부분을 모두 교체한 것이 Stackless python
      • PyPy에 밀려서 사양길…
  5. Jython
    • java 위에서 돌라가는 파이썬
    • GIL 이슈가 없어서 쓰레드 쓰기 편리함
  6. IPython, IPython Notebook
    • IPython은 추가 기능을 제공하는 interactive shell
      • 파이썬 기본 인터프리터의 업그레이드 버전
    • IPython Notebook은 웹 기반 쉘 환경을 제공
      • 파이썬 코드, 텍스트, 수학식, 그래프, 다양한 미디어들을 하나의 도큐먼트로 만들 수 있음

출처

Comment  Read more

파이썬 디자인 패턴 - 구조디자인패턴 - 데코레이터 패턴 2

|

2-4-2. 클래스 데코레이터

  • 읽기 및 쓰기 프로퍼티가 아주 많은 클래스를 사용할 일이 자주 있다.
    • 그러한 클래스에는 비슷비슷한 getter와 setter 코드가 많이 포함되어 있기 마련
    • ex) 어떤 책에 대해 title, ISBN, price, quantity 값을 저장하는 Book 클래스가 있다고 하자
      • 이러한 경우 @property 데코레이터를 4개 써야할 것이다.
      • 그런데 이것들은 모두 기본적으로 동일한 코드 (ex: @property def title(self): return title)
      • 또한 값 검증 기능이 들어간 세터 메서드도 4개 필요
      • 이때 price와 quantity를 검증하는 코드는 실제로 허용되는 최댓값과 최솟값만 다를 뿐 코드는 완전히 동일
    • 이런 클래스가 많다면 중복에 가까운 코드가 엄청나게 늘어날 수밖에 없음
  • 다행히 파이썬의 클래스 데코레이터를 사용하면 이런 중복을 없앨 수 있다.
    • 2.2에서 클래스 데코레이터를 사용해 매번 열 줄 정도 되는 코드를 반복할 필요 없이 인터페이스 검사용 클래스를 만들 수 있었음
    • 여기서는 다른 예로 네 개의 프로퍼티(완전한 검증 코드가 포함된)가 들어있는 Book클래스를 보겠다
@ensure("title", is_non_empty_str)
@ensure("isbn", is_valid_isbn)
@ensure("price", is_in_range(1, 10000))
@ensure("quantity", is_in_range(0, 1000000))
class Book:

    def __init__(self, title, isbn, price, quantity):
        self.title = title
        self.isbn = isbn
        self.price = price
        self.quantity = quantity

    @property
    def value(self):
        return self.price * self.quantity
  • self.title, self.isbn등은 모두 프로퍼티이다. 따라서 __init()__에서 이러한 프로퍼티를 초기화하면 프로퍼티 세터에 지정된 대로 검증이 이뤄진다.
    • 하지만 직접 이러한 프로퍼티의 getter와 setter를 만드는 코드를 작성하는 대신 클래스 데코레이터를 4번 사용해서 모든 기능을 제공
  • ensure() 함수는 인자를 두 개(프로퍼티 이름과 검증 함수) 받아 클래스 데코레이터를 만든다. 이렇게 만들어진 클래스 데코레이터는 바로 뒤에 오는 클래스에 적용
  • 실제 실행 순서는 class Book... > @ensure('property',...) > @ensure('price',...) > @ensure("isbn", ...) > @ensure("title",...)
def is_non_empty_str(name, value):
    if not isinstance(value, str):
        raise ValueError("{} must be of type srt".format(name))
    if not bool(value):
        raise ValueError("{} may not be empty".format(name))
  • 이 검증함수는 Book의 title 프로퍼티 검증에 사용
    • 제목이 비어있는지 검사
    • ValueError에서 볼 수 있듯이, 프로퍼티의 이름은 오류 메시지를 표시할 때 유용하게 쓸 수 있음
def is_in_range(minimum=None, maximum=None):
    assert minimum in not None or maximum is not None
    def is_in_range(name, value):
        if not isinstance(value, numbers.Number):
            raise ValueError("{} must be a number".format(name))
        if minimum is not None and value < minimum:
            raise ValueError("{} {} is too small".format(name, value))
        if maximum is not None and value > maximum:
            raise ValueError("{} {} is too big".format(name, value))
    return is_in_range
  • 이 함수는 입력 값이 수(이를 위해 numbers.Number를 사용) 인지 여부와 값이 제한 범위 내에 들어있는지 검사하는 검증 함수를 만들고 그 함수를 반환
def ensure(name, validate, doc=None):
    def decorator(Class):
        privateName = "__" + name
        def getter(self):
            return getattr(self, privateName)
        def setter(self, value):
            validate(name, value)
            setattr(self, privateName, value)
        setattr(Class, name, property(getter, setter, doc=doc))
        return Class
    return decorator
  • ensure() 함수는 프로퍼티 이름, 검증 함수, 그리고 선택적으로 docstring(인라인 도움말 문자열)을 받는다.
    • 따라서 특정 클래스를 대상으로 ensure()가 반환하는 클래스 데코레이터를 사용하면, 대상 클래스에는 새로운 프로퍼티가 추가됨
    • decorator()함수의 유일한 인자는 클래스다.
      • decorator() 함수는 먼저 클래스 외부에서 접근할 수 없는 이름을 만듦
      • 프로퍼티의 값은 이 이름으로 된 애트리 뷰트에 저장한다(따라서 Book 예제에서 self.title 프로퍼티의 값은 전용 self.__title 애트리뷰트에 저장)
      • 해당 애트리뷰트에 저장된 값을 반환하는 함수 getter함수를 만든다.
        • 내장 함수 getattr()은 객체와 애트리뷰트 이름을 인자로 받아 애트리뷰트의 값을 반환
        • 값이 없으면 AttributeError를 반환
      • setter함수는 저장된 validate() 함수를 호출하고 애트리뷰트를 새로운 값으로 변경
        • 내장 함수 setattr()은 객체와 애트리뷰트 이름, 그리고 값을 받아 객체에 해당 애트리뷰트를 설정
        • 만약 이름에 해당하는 애트리뷰트가 객체에 없다면 새로 하나 만듦
      • 이렇게 setter()getter() 를 만든 다음, 프로퍼티 이름으로 대상 클래스에 새 애트리뷰트를 만든다.
        • 이때 프로퍼티 이름(외부로 노출된)으로 내장 setattr() 함수를 사용
          • 내장 property() 함수는 getter, setter(optional), docstring을 받아 프로퍼티를 반환
          • 이 프로퍼티는 앞에서 본 것처럼 메서드 데코레이터로 사용할 수 있다.
      • 이렇게 변경된 클래스는 decorator() 함수가 반환
    • decorator() 함수 자체는 ensure() 클래스 데코레이터 팩터리 함수가 반환

2-4-2-1. 데코레이터로 프로퍼티 추가하기

  • 앞의 예에서는 @ensure 클래스 데코레이터를 사용해 검증이 필요한 애트리뷰트를 만들어야 했다.
    • 이런 식으로 데코레이터를 여러 개 겹쳐서 사용하는 것을 좋아하지 않는 사람도 있다.
    • 그런 사람들은 데코레이터는 하나만 사용하고, 클래스의 몸체안에 애트리뷰트를 넣어 가독성을 높이는 쪽을 선호
@do_ensure
class Book:

    title = Ensure(is_non_empty_str)
    isbn = Ensure(is_valid_isbn)
    price = Ensure(is_in_range(1, 10000))
    quantity = Ensure(is_in_range(0, 1000000))

    def __init__(self, title, isbn, price, quantity):
        self.title = title
        self,isbn = isbn
        self.price = price
        self.quantity = quantity

    @property
    def value(self):
        return self.price * self.quantity
  • 위 코드는 @do_ensure 클래스 데코레이터와 Ensure 인스턴스를 함께 사용하는 새로운 Book 클래스다.
    • Ensure는 검증함수를 저장
    • @do_ensure 클래스 데토레이터는 각 Ensure 인스턴스를 같은 이름의 프로퍼티로 바꿔치기
    • 물론 여기서 검증함수(is_non_empty_str() 등)는 앞에서 본 것과 동일
class Ensure:

    def __init__(self, validate, doc=None):
        self.validate = validate
        self.doc = doc
  • 이 클래스는 프로퍼티의 세터에서 사용될 검증 함수와 문서화 문자열을 담아두는데 사용
    • ex) Book 클래스의 title 애트리뷰트는 Ensure 인스턴스로 되어있음
    • 하지만 일단 Book 클래스가 만들어지고 @do_ensure 데코레이터가 모든 Ensure을 프로퍼티로 바꿔치기 함
    • 결국 title 애트리뷰트는 결국 마지막에는 title 프로퍼티가 된다.
def do_ensure(Class):
    def make_property(name, attribute):
        privateName = "__" + name
        def getter(self):
            return getattr(self, privateName)
        def setter(self, value):
            attribute.validate(name, value)
            setattr(self, privateName, value)
        return property(getter, setter, doc=attribute.doc)
    for name, attribute in Class.__dict__.items():
        if isinstance(attribute, Ensure):
            setattr(Class, name, make_property(name, attribute))
    return Class
  • 이 클래스 데코레이터는 세 부분으로 구성
    • 첫 부분에서는 중첩된 함수 (make_property())를 정의
      • 이 함수는 이름(ex:title) 과 Ensure 타입의 애트리뷰트를 받아 전용 애트리뷰트 (ex:__title)에 값을 저장하는 새로운 프로퍼티를 만들고 반환
      • 아울러 해당 프로퍼티의 세터에 접근하는 경우 검증함수를 호출
    • 두 번째 부분에서는 클래스의 모든 애트리뷰트를 순회하면서 모든 Ensure를 새로운 프로퍼티로 바꿔치기
    • 세 번째 부분에서는 이렇게 변경된 클래스를 반환
  • 데코레이터가 호출되고 나면 데코레이션된 클래스의 모든 Ensure 애트리뷰트는 같은 이름의 프로퍼티로 바뀌너다.
    • 또한 각 프로퍼티에는 유효성 검증이 추가
  • 이론적으로는 함수가 중첩되지 않게 하고 대신 그 코드를 if isinstance()다음에 넣을수도 있음
    • 하지만 그렇게 구현하면 바인딩 시점의 문제로 제대로 동작하지 않는다.
    • 따라서 여기서는 중첩 함수를 꼭 사용해야 함
    • 데코레이터나 데코레이터 팩터리를 만드는 경우 보통 이런 식으로 별도의 함수(경우에 따라 중첩된)를 사용

2-4-2-2. 클래스 테코레이터를 상속대신 활용하기

  • 메서드나 데이터만 담긴 기반 클래스를 만들고 이 클래스를 상속해서 재사용할 때가 있다.
    • 물론 이렇게 하면 메서드나 데이터를 여러번 정의하지 않아도 되고 하위 클래스를 더 추가해도 잘 동작한다
    • 하지만 하위 클래스에서 상속받은 데이터나 메서드를 결코 변경(재정의)하지 않는 경우, 같은 목적을 달성하기 위해 클래스 데코레이터를 사용할 수도 있다.

  • 여기서는 self.mediator 데이터 애트리뷰트와 on_change() 메서드를 제공하는 조정자(mediator) 클래스를 활용할 텐데, 이 클래스는 Button과 Text에 상속되며, 이 둘은 데이터와 메서드를 사용하기는 하되 값을 변경하지는 않는다.
class Mediated:

    def __init__(self):
        self.mediator = None

    def on_change(self):
        if self.mediator is not None:
            self.mediator.on_change(self)
  • 이 클래스는 일반적인 클래스와 마찬가지로 class Button(Mediated): ...class Text(Mediated): ...를 사용해 상속된다.
    • 하지만 하위 클래스 중 어느 것도 상속받은 on_change() 메서드를 변경하지 않는다.
    • 따라서 이러한 경우 상속 대신 클래스 데코레이터를 쓸 수 있다.
def mediated(Class):
    setattr(Class, "mediator", None)
    def on_change(self):
        if self.mediator is not None:
            self.mediator.on_change(self)
    setattr(Class, "on_change", on_change)
    return Class
  • 클래스 데코레이터는 여느 때와 같이 사용할 수 있다.
    • @mediated class Button: ... 이나 @mediated class Text: ... 같이 하면 된다.
    • 데코레이션된 클래스는 상속을 사용해 만든 클래스와 동일한 동작 방식을 보여준다.

  • 함수나 클래스 데코레이터는 아주 강력하지만 사용하기 쉬운 파이썬 기능 중 하나
  • 클래스 데코레이터는 경우에 따라 상속을 대신할 수도이 ㅣㅆ음
  • 데코레이터를 만드는 것은 메타 프로그래밍의 일종
  • 메타 클래스처럼 더 복잡한 메타프로그래밍대신 클래스 데코레이터를 활용할 만한 경우가 많을 것임

Comment  Read more

파이썬 디자인 패턴 - 구조디자인패턴 - 데코레이터 패턴 1

|

2-4. 데코레이터 패턴

  • 일반적으로 데코레이터(decorator)는 어떤 함수를 인자로 받아 원래 함수와 이름은 같지만 기능은 더 향상된 함수를 반환하는 함수를 말함
  • 데코레이터는 프레임워크에서 프레임 워크과 개발자가 필요로 하는 기능을 편하게 통합 하는데 사용
  • 파이썬에는 데코레이터 기능을 내장
    • 함수와 메서드에 모두 적용 가능
    • 클래스 데코레이터도 만들 수 있다.
      • 클래스를 유일한 인자로 받아 새로운 기능이 추가된 이름이 같은 새 클래스를 반환
    • 상속 대진 클래스 데코레이터 사용 가능

  • 파이썬의 내장 property() 함수는 데코레이터로 사용 가능
    • 2-3 컴포지트 패턴에서 composite와 price 프로퍼티에서 사용
  • 파이썬의 표준 라이브러리에도 몇몇 데코레이터가 들어있음
    • ex) @functools.total_ordering 클래스 데코레이터는 __eq()____lt()__ 특수메서드를 구현한 클래스에 적용가능
      • total_ordering 데코레이터를 적용하면 클래스의 이름은 같지만 해당 클래스에서 모든 다른 비교 연산자를 활용 가능

  • 데코레이터는 한 함수, 메서드, 클래스만을 인자로 받을 수 있다.
  • 이론적으로는 데코레이터를 매개변수화 할 수 없다.
  • 하지만 데코레이터 함수를 반환하는 데코레이터 팩터리를 만들 수 있고, 이 팩터리는 매개변수를 방을 수 있다.
  • 이 데코레이터 팩터리로 만든 데코레이터를 다시 함수, 메서드, 클래스 등에 적용할 수 있다.

2-4-1. 함수 및 메서드 데코레이터

  • 모든 함수(메서드) 데코레이터는 구조가 같다.
    • 우선 래퍼 함수를 만든다.
    • 래퍼 함수 안에서는 반드시 원래의 함수를 호출
    • 래퍼 안에서는 함수를 호출하기 전에 원하는 전처리를 수행 할 수 있고, 호출 결과를 받아 후처리를 할 수도 있다.
    • 원래 함수가 반환한 값을 그대로 반환하거나 변경된 값을 반환하거나, 원한다면 어떤 값이든 반환할 수 있다.
    • 이렇게 변화된 함수는 원래 함수와 똑같은 이름으로 원래 함수를 대체
  • 데코레이터를 함수, 메서드, 클래스에 적용하려면 @defclass와 같은 들여쓰기 수준으로 입력한 다음, 데코레이터의 이름을 적으면 된다.
  • 데코레이터를 계속 둘러싸는 것도 가능
    • 데코레이터가 적용된 함수에 다시 데코레이터를 적용하고, 그런 과정을 반복 가능

@float_args_and_return
def mean(fisrt, second, *rest):
    numbers = (first, second) + rest
    return sum(numbers) / len(numbers)
  • 위 코드에서 @float_args_and_return 데코레이터를 사용해 mean() 함수를 변경했다.
  • 데코레이션 되기 전의 mean()함수는 둘 이상의 수를 받아 평균을 float로 돌려준다.
  • 데코레이션 된 mena()에서는 어떤 종류의 인자든 두개 이상 받아 float로 변환해 평균을 계산
    • 데코레이션 하기 전이라면 mean(5, '6', '7.5')TypeError를 반환 할 것이다
    • 하지만 데코레이션된 버전에서는 float('6') ,float('7.5') 로 처리해주어서 잘 작동
  • 근데 데코레이션 문법은 단지 편의 문법(syntactic sugar)에 불과하다. 위 코드를 아래와 같이 작성해도 동일하게 동작한다.
def mean(first, second, *rest):
    numbers = (fisrt, second) + rest
    return sum(numbers) / len(numbers)
mean = float_args_and_return(mean)
  • 여기서는 데코레이터 없이 함수를 만든 다음, 데코레이터를 직접 호출하는 방식으로 데코레이션 된 버전
  • 데코레이터를 사용하는 것은 아주 편리하지만 때때로 직접 호출해야 하는 경우도 있음
def float_args_and_return(function):
    def wrapper(*args, **kwargs):
        args = [float(arg) for arg in args]
        return float(function(*args, **kargs))
    return wrapper
  • 함수 데코레이터인 float_args_and_return 함수는 어떤 함수를 유일한 인자로 받음
  • 래퍼함수는 *args*kargs를 받는 것이 일반적임
  • 인자에 대한 제약사항은 원래의(래핑이전의) 함수가 처리한다.
  • 이 예제의 경우
    • 해퍼 함수는 위치에 따른 인자를 부동소수점 수 리스트로 만든다.
    • 그 다음 원래의 함수를 변경된 *args로 호출하고, 결과를 받아 float로 변환해 반환
    • 데코레이터 함수 전레의 결과값으로는 바로 이 래퍼 함수를 돌려주면 된다.
  • 하지만 아쉽게도 반환된 함수의 __name__ 애트리뷰트는 원래 함수의 이름이 아니고 wrapper가 된다.
  • 문서화 문자열(docstring)도 없어진다.
  • 이러한 문제를 해결하기 위해 파이썬 표준 라이브러리에는 @functools.wrap 데코레이터가 들어있다.
    • 이는 데코레이터 안에 있는 래퍼 함수에게 사용 가능
    • 래핑된 함수의 __name____doc__이 원래 함수의 내용을 유지할 수 있게 해줌
def float_args_and_return(function):
    @functools.wraps(function)
    def wrapper(*args, **kwargs):
        args = [float(arg) for arg in args]
        return float(function(*args, **kargs))
    return wrapper
  • 위와 같이 코드를 작성하면 __name____doc__이 유지된 채로 데코레이터 사용가능 하다.
  • 항상 @functools.wraps를 사용하는 것이 좋다.
    • 오류를 추적할 때 래핑된 함수의 이름이 제대로 표시
    • 원본 함수의 문서화 문자열도 볼수 있음

@statically_typed(str, str, return_type=str)
def make_tagged(text, tag):
    return "<{0}>{1}</{0}>".format(tag, escape(text))

@statically_typed(str, int, str) # 어떤 반환 타입이든 받아들일 수 있음
def repeat(what, count, separator):
    return((what + separator) * count)[:-len(separator)]
  • statically_typed() 함수는 make_tagged()repeat() 함수를 데코레이션 할 때 사용함
  • 데코레이터 팩토리, 즉 데코레이터를 만드는 함수다.
  • statically_typed()는 함수나 메서드 또는 클레스를 유일한 인자로 받지 않기 때문에 데코레이터는 아님
    • 하지만 여기서는 데코레이터를 매개변수화 할 필요가 있음
    • 데코레이션 되는 함수가 받아들일 수 있는 위치 기반 인자의 자료형과 개수를 지정하고 싶고, 그러한 특성을 함수마다 다르게 적용하고 싶기 때문
    • 따라서 statically_typed() 함수를 만들어 원하는 매개변수를 받고, 데코레이터를 반환
  • 파이썬이 코드상에서 @statically_typed(...)를 만나면 인자를 statically_typed() 함수에 전달
    • 그런 다음 반환받은 데코레이터를 다시 다음에 오는 함수 (여기서는 make_tagged()repeat())에 적용
  • 데코레이터 팩터리를 만드는 것에도 패턴이 있다.
    • 먼저 데코레이터 함수 생성
    • 그 함수의 내부에 래퍼 함수 생성
    • 보통 래퍼의 마지막 부분에서는 원래 함수의 결과값(필요 시 변경하거나 새 값으로 대체된)이 반환
    • 데코레이터 함수의 끝에서는 래퍼가 반환
    • 마지막으로 데코레이터 팩터리 함수 끝에서 데코레이터 함수를 반환
def statically_typed(*types, return_type=None): #데코레이터 팩토리
    def decorator(function):                    #팩토리가 반환할 테코레이터 함수
        @functools.wraps(function)
        def wrapper(*args, **kwargs):
            if len(args) > len(types):
                raise ValueError("too many arguments")
            elif len(args) < len(types):
                raise ValueError("too few arguments")

            for i, (arg, type_) in enumerate(zip(args, types)):
                if not isinstance(arg, type_):
                    raise ValueError("argument {} must be of type{}"
                          .format(i, type_.__name__))
            result = funtion(*args, **kwargs)
            if (return_type is not None and
                not isinstance(result, return_type)):
                raise ValueError("return value must be of type {}"
                    .format(return_type.__name__))

            return result
        return wrapper
    return decorator
  • 여기서 먼저 데코레이터 함수를 생성
    • 함수의 이름을 decorator()로 지정하긴 했지만 이름은 중요 ㄴㄴ
    • 데코레이터 함수 내부에는 래퍼 생성
      • 래퍼가 조금 복잡 : 위치 기반 인자의 자료형과 개수를 원래의 함수를 호출하기 전에 검사, 특정 반환 자료형이 명시돼 있다면 반환 자료명 검사, 그 다음 결과 반환
    • 래퍼가 만들어지고 나면 데코레이터는 해당 래퍼를 반환
    • 그 다음 마지막에 데코레이터 자체를 반환
  • ex)
    • 소스 코드에 @statically_typed(str, int, str)라는 구문이 있다면 파이썬은 statically_typed()함수를 호출
      • 그러면 만들어진 decorator() 함수가 반환
        • 이 함수에는 statically_typed()에 전달된 인자의 정보가 저장
      • 이제 다시 @로 돌아와서 파이썬은 @statically_typed(str, int, srt) 구문 다음에 오는 함수 (def로 정의했거나 다른 데코레이터에서 반환하는 데코레이션된 함수)를 유일한 인자로 decorator()에 유일한 인자로 전달
      • 여기서는 다음에 오는 함수가 repeat()였다.
        • 따라서 repeatdecorator()에 유일한 인자로 전달
      • 이제 decorator()는 저장된 자료형 정보에 따른 새로운 wrapper() 함수를 만들고 반환
    • 이 래퍼 함수는 원래의 repeat() 함수를 대체

@application.post("/mailinglists/add")
@Web.ensure_logged_in
def person_add_submit(username):
    name = bottle.request.forms.get("name")
    try:
        id = Data.MailingLists.add(name)
        bottle.redirect("/mailinglists/view")
    except Data.Sql.Error as err:
        return bottle.mako_template("error", url="/mailinglists/add",
                text="Add Mailinglist", message=str(err))
  • 이 코드는 메일 목록을 관리하는 웹 애플리케이션에서 가져왔으며, 경량 웹 프레임워크인 bottle(bottlepy.org)을 활용
  • 프레임워크에서 지원하는 @application.post 데코레이터는 함수와 URL을 연관시키는데 사용
    • 위 예제에서는 사용자가 로그인해 있을 때에만 mailinglist/add 페이지에 접근할 수 있고, 로그인한 상태가 아니라면 login 페이지로 이동하게 하고싶다.
    • 웹 페이지를 생성하는 모든 함수마다 사용자가 로그인했는지 검사하는 똑같은 코드를 넣는 대신, 이러한 일을 처리하는 @Web.ensure_logged_in 데코레이터를 생성해서 이러한 기능이 필요한 어떤 함수든 로그인 관련 코드에 신경쓰지 않게 했다.
def ensure_logged_in(function):
    @functools.wraps(function)
    def wrapper(*args, **kwargs):
        username = bottle.request.get_cookie(COOKE,
                      secret=secret(bottle.request))

        if username is not None:
            kwargs["username"] = username
            return function(*args, **kwargs)
        bottle.redirect("/login")
    return wrapper
  • 사용자가 웹 사이트에 로그인할 때, login 페이지에 있는 코드는 사용자 계정명과 암호를 검증하고, 유효하다면 사용자 브로우저에 해당 세션동안 유효한 쿠키를 생성
  • 사용자가 접근하려는 페이지와 연게된 함수 (예: mailinglists/add 페이지의 person_add_submit())에 @ensure_logged_in 데코레이터가 설정돼 있다면
    • 위의 코드에 있는 wrapper()함수가 호출된다.
    • 래퍼는 먼저 쿠키로부터 사용자 계정명을 조회
      • 조회에 실패하면 사용자가 로그인하지 않은 상태이므로 웹어플리케이션의 login 페이지로 이동시킴
      • 사용자가 로그인한 상태라면 키워드 인자에 사용자 계정명을 추가하고 호출 경과를 원래 함수에 반환
    • 즉 원래 함수를 호출한 시점에 사용자가 유효하게 로그인해 있을을 안전하게 가정가능
    • 사용자 계정명에 대한 권한을 얻을 수 있음

Comment  Read more