파이썬 코딩의 기술
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
'프로그래머 > Python' 카테고리의 다른 글
[윤성우의 열혈 파이썬 중급편] 01. reference count와 garbage collection (0) | 2020.12.15 |
---|---|
[Effective Python 복습] Chapter 8. 제품화 (0) | 2020.12.13 |
[Effective Python 복습] Chapter 6. 내장 모듈 (0) | 2020.12.11 |
[널널한 교수의 고급 파이썬] 05-5 파이썬 외부 라이브러리 (0) | 2020.12.11 |
[널널한 교수의 고급 파이썬] 05-3, 05-4 파이썬 표준 라이브러리 (0) | 2020.12.11 |