본문 바로가기

프로그래머/Python

[Effective Python 복습] Chapter 7. 협력

파이썬 코딩의 기술

Chapter 7. 협력

49. 모든 함수, 클래스, 모듈에 docstring을 작성하자

  • 모든 모듈, 클래스, 함수를 docstring으로 문서화하자. 코드를 업데이트할 때마다 관련 문서도 업데이트하자
  • 모듈 : 모듈의 내용과 모든 사용자가 알아둬야 할 중요한 클래스와 함수를 설명한다
  • 클래스 : class문 다음의 docstring에서 클래스의 동작, 중요한 속성, 서브클래스의 동작을 설명한다
  • 함수와 메서드 : def문 다음의 docstring에서 모든 인수, 반환 값, 일어나는 예외, 다른 동작들을 문서화한다
def palindrome(word):
    """Return True if the given word is a palindrome."""
    return worn == word[::-1]

print(repr(palindrome.__doc__))

모듈 docstring

# words.py
#!usr/bin/env python3
"""
Library for testing words for ...

Testing how words ..
...

Available functions:
- palindrome: Determine if a word is ...
- check_anagram: Determine if two words ...
...
"""
# ...

클래스 docstring

class Player(object):
    """Represents a player of the game:

    Subclasses may override the 'tick' method to provide
    custome ...

    Public attributes: 
    - power : Unused power-ups ...
    - coins : Coins found during the level (integer)
    """

    # ...

함수 docstring

def find_anagrams(word, dictionary):
    """Find all anagrams for a word

    This function only runs as fast as the test for
    ...

    Args:
        word: String of the target word.
        dictionary: Container with all strings that
                are known to be actual words.

    Returns:
        List of anagrams that wer found. Empty if
        none were found
    """

    # ...

50. 모듈을 구성하고 안정적인 api를 제공하려면 패키지를 사용하자

  • 파이썬의 패키지는 다른 모듈을 포함하는 모듈이다
  • 패키지를 이용하면 고유한 절대 모듈 이름으로 코드를 분리하고, 충돌하지 않는 네임스페이스를 구성할 수 있다
  • 간단한 패키지는 다른 소스 파일을 포함하는 디렉터리에 __ init __ .py를 추가하는 방법으로 정의한다
  • __ init __ .py를 제외한 파일들은 디렉터리 패키지의 자식 모듈이 된다.
  • 패키지 디렉터리는 다른 패키지를 포함할 수도 있다
  • __ all __이라는 특별한 속성에 공개하려는 이름을 나열하여 모듈의 명시적인 api를 제공할 수 있다
  • 공개할 이름만 패키지의 __ init __.py 파일에서 임포트하거나 내부 전용 멤버의 이름을 밑줄로 시작하게 만들면 패키지의 내부 구현을 숨길 수 있다
  • 단일 팀이나 단일 코드베이스로 협업할 때는 외부 api용으로 __ all __을 사용할 필요가 없을 것이다
main.py
mypackage/__init__.py
mypackage/models.py
mypackage/utils.py

# main.py
from mypackage import utils
  • 해당 디렉터리에 있는 다른 파이썬 파일은 디렉터리에 상대적인 경로로 임포트할 수 있다

네임스페이스

# main.py
from analysis.utils import log_base2_bucket
from frontend.utils import stringify

bucket = stringify(log_base2_bucket(33))

# main2.py
from analysis.utils import inspect
from frontend.utils import inspect    # 덮어씀!

# main3.p6
from analysis.utils import inspect as analysis_inspect
from frontend.utils import inspect as frontend_inspect

value = 33
if analysis_inspect(value) == frontend_inspect(value):
    print('Inspection equal!')
  • as 절을 이용하면 네임스페이스가 붙은 코드에 접근하기 쉽고 대상을 사용할 때 실체를 명확하게 인식할 수 있다
  • 임포트한 이름이 충돌하는 문제를 피하는 다르 방법은 항상 가장 상위의 고유한 모듈 이름으로 접근하는 것이다
    • import analysis.utils로 임포트 후, analysis.utils.inspect처럼 전체 경로로 inspect 함수에 접근한다
    • 이 방법을 사용하면 as 절을 아예 사용하지 않아도 되고, 각 함수가 어디에 정의되어 있는지 더 명확하게 이해할 수 있다

안정적인 api

# models.py
__all__ = ['Projectile']

class Projectile(object):
    def __init__(self, mass, velocity):
        self.mass = mass
        self.velocity = velocity

# utils.py
from . models import Projectile

__all__ = ['simulate_collision']

def _dot_product(a, b):
    # ...

def simulate_collision((a, b):
    # ...
  • 패키지를 사용하는 코드에서 from foo import * 를 실행하면 foo.all에 있는 속성만 임포트된다
  • foo에 all이 없으면 속성 이름이 밑줄로 시작하지 않는 공개 속성만 임포트된다
# __init__.py
__all__ = []
from . models import *
__all__ += models.__all__
from . utils import *
__all__ += utils.__all__

# api_consumer.py
from mypackage import *
a = Projectile(1.5, 3)
b = Projectile(4, 1.7)
after_a, after_b = simulate_collision(a, b)
  • 내부 모듈에 접근하지 않고 mypackage로부터 직접 임포트할 수 있다

import* 를 주의하자

  • from x import y 같은 임포트 문은 y의 출처가 명시적으로 x 패키지나 몯류이므로 명확하다
  • from foo import * 는 코드를 새로 접하는 사용자들에게서 이름의 출처를 숨긴다
  • import * 문으로 가져온 이름은 이 문자를 포함하는 몯류 내에서 충돌하는 이름을 덮어쓴다.
    • 이상한 버그가 생길 수 있다
  • 가장 안전한 방법은 코드에서 import *를 피하고 명시적으로 from x import y 스타일로 이름을 임포트하는 것이다

51. 루트 Exception을 정의해서 api로부터 호출자를 보호하자

  • 작성중인 모듈에 루트 예외를 정의하면 api로부터 api 사용자를 보호할 수 있다
  • 루트 예외를 잡으면 api를 사용하는 코드에 숨은 버그를 찾는 데 도움이 될 수 있다
  • 파이썬 Exception 기반 클래스를 잡으면 api 구현에 있는 버그를 찾는 데 도움이 될 수 있다
  • 중간 루트 예외를 이용하면 api를 사용하는 코드에 영향을 주지 않고 나중에 더 구체적인 예외를 추가할 수 있다

라이브러리용 내장 에외 계층

def determine_wight(volume, density):
    if density <= 0:
        raise ValueError('Density must be positive')
    # ...

자신만의 예외 계층 정의

# my_mdoule.py
class Error(Exception):
    """Base- class for all exceptions raised by this module"""

class InvalidDensityError(Error):
    """There was a problem with a provided density value."""


try:
    weight = my_module.determine_weight(1, -1)
except my_module.InvalidDensityError:
    weight = 0
except my_module.Error as e:
    logging.error('Bug in the calling code: %s', e)
except Exception as e:
    logging.error('Bug in the api code: %s', e)
    raise

Exception 서브클래스를 추가할 수 있다

# my_mdoule.py
class NegativeDensityError(InvalidDensityError):
    """A provided densit value was negative."""

def determine_weight(volume, density):
    if density < 0:
        raise NegativeDensityError

try:
    weight = my_module.determine_weight(1, -1)
except my_module.NegativeDensityError as e:
    raise ValueError('Must supply non-negative density') from e
except my_module.InvalidDensityError:
    weight = 0
except my_module.Error as e: 
    logging.error('Bug in the calling code: %s', e)
except Exception as e:
    logging.error('Bug in the api code: %s', e)
    raise

52. 순환 의존성을 없애는 방법을 알자

  • 순환 의존성은 두 모듈이 임포트 시점에 서로 호출할 때 일어난다
  • 이런 의존성은 프로그램이 시작할 때 동작을 멈추는 원인이 된다
  • 순환 의존성을 없애는 가장 좋은 방법은 순환 의존성을 의존성 트리의 아래에 있는 분리된 모듈로 리팩토링하는 것이다
  • 동적 임포트는 리팩토링과 복잡도를 최소화해서 모듈 간의 순환 의존성을 없애는 가장 간단한 해결책이다

순환의존성 발생

# dialog.py
import app

class Dialog(object):
    def __init__(self, save_dir):
        self.save_dir = save_dir
    # ...

save_dialog = Dialog(app.prefs.get('save_dir'))

def show():
    # ...

# app.py
import dialog

class Prefs(object):
    # ...
    def get(self, name):
        # ...

prefs = Prefs()
dialog.show()

임포트 재정렬

# app.py
class Prefs(object):
    # ...
    def get(self, name):
        # ...

prefs = Prefs()

import dialog    # 재정렬
dialog.show()
  • 불안정, 코드의 순서를 약간만 바꿔도 모듈이 동작하지 않는 원인이 된다
  • 피하는 게 좋다

임포트, 설정, 실행

# dialog.py
import app

class Dialog(object):
    # ...

save_dialog = Dialog()

def configure():
    save_dialog.save_dir = app.prefs.get('save_dir')

# app.py
import dialog

class Prefs(object):
    # ...

prefs = Prefs()

def configure():
    # ...

# main.py
import app
import dialog

app.configure()
dialog.configure()

dialog.show()
  • 모듈에 별개의 두 단계를 두면 설정에서 객체의 정의가 분리되기 때문에 코드를 더 이해하기 어렵다

동적 임포트

# dialog.py
class Dialog(object):
    # ...

save_dialog = Dialog()

def show():
    import app    # 동적 임포트
    save_dialog.save_dir = app.prefs.get('save_dir')
    # ...

# app.py
import dialog

class Prefs(object):
    # ...

prefs = Prefs()
dialog.show()
  • 모듈을 정의하고 임포트하는 방식을 구조적으로 변경할 필요가 없다
  • import문의 비용은 무시하지 못할 정도이고, 복잡한 루프에서는 좋지 않다
  • 실행을 미루는 동작으로서, 런타임에 상당히 심각한 피해를 야기한다
  • 하지만 전체 프로그램을 대체하거나 재구성하는 것보다는 낫다

53. 의존성을 분리하고 재현하려면 가상 환경을 사용하자

  • 가상 환경은 pip를 사용하여 같은 머신에서 같은 패키지의 여러 버전을 충돌 없이 설치할 수 있게 해준다
  • 가상 환경은 pyvenv로 생성하며, source bin/active로 활성화하고 deactivate로 비활성화한다
  • pip freeze로 환경에 대한 모든 요구 사항을 덤프할 수 있다
  • requirements.txt 파일을 pip install -r 명령의 인수로 전달하여 환경을 재현할 수 있다
  • 파이썬 3.4 이전 버전에서는 pyvenv 도구를 별도로 다운로드해서 설치해야 한다
  • 명령줄 도구의 이름은 pyvenv가 아닌 virtualenv 이다

pip freeze > requirements.txt

pip3 install -r /tmp/myproject/requirements.txt