협업

About_work·2023년 2월 25일
0

python 기초

목록 보기
25/56

82. 커뮤니티에서 만든 모듈을, 어디서 찾을 수 있는지 알아두라.

  • 요약
    • 파이썬 패키지 인덱스(PyPI)에는 파이썬 커뮤니티가 개발하고 유지하는 풍부한 공통 패키지가 들어있다.
    • pip은 PyPI에 있는 패키지를 설치하고, 사용할 떄 쓸 수 있는 명령줄 도구다.
    • PyPI 모듈 대다수는 자유 소프트웨어이거나, 오픈 소스 소프트웨어다.
  • 본문
    • pip: Pip Installs Packages 의 약자다.
    • python3 -m pip install ~~: 패키지가, 시스템에 설치된 파이썬 버전에 맞게 설치되도록 보장할 수 있다.

83. 가상 환경을 사용해, 의존 관계를 격리하고 반복 생성할 수 있게 하라.

  • 요약

    • 가상 환경을 사용하면, 한 컴퓨터 안에서 pip을 사용해 패키지의 여러 버전을 충돌 없이 설치할 수 있다.
    • 만들기: python -m venv
    • 활성화: source bin/activate
    • 비활성화: deactivate
    • 어떤 환경 안의 모든 의존 관계를 dump: python -m pip freeze
    • 환경 재생성: python3 -m pip install -r requirements.txt
  • 본문

    • 기본 pip: 새로운 패키지를, 모든 파이썬 Interpretor가 볼 수 있는 global 위치에 저장
    • 문제가 되는 경우: 설치한 패키지가 다른 패키지에 의존하는 경우.
    • 특성 패키지의 의존 패키지를 어떻게 볼 수 있는가? python3 -m pip show numpy
    • venv는 가상 환경을 제공한다. python -m venv로 접근 가능
    • venv
      • 특정 파이썬 버전 설치 가능
    • python3 명령어
      • $which python3
        • /usr/local/bin/python3
      • $python3 --version
        • Python 3.8.0
    • CLI 에서 venv 사용하기
      • 전역적으로 설치된 패키지는 무시된다.
      • $python3 -m venv myproject
      • $cd myproject
      • $ls
        • bin
          • activate (스크립트임)
          • python3
        • include
        • lib
        • pyvenv.cfg
      • (myproject)$ which python3
        • /tmp/myproject/bin/python3
      • (myproject)$ ls -l /tmp/myproject/bin/python3
        • ... -> /usr/local/bin/python3.8
    • 환경 복사 생성하기
      • python3 -m pip freeze > requirements.txt 명령을 사용해, requirements.txt에 환경 정보를 저장 가능하다.
      • 이걸 재활용하는 방법
        • 새로운 환경을 activate한 뒤
        • $ python3 -m pip install -r /tmp/myproject/requirements.txt
        • 하지만 이 txt에는 파이썬 버전은 들어가지 않으므로, 파이썬 버전을 별도로 관리해야 한다.

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

85. 패키지를 사용해, 모듈을 체계화하고 안정적인 API를 제공하라.

요약

  • 파이썬 패키지는 다른 모듈을 포함하는 모듈이다.
  • 패키지를 사용하면, 서로 분리돼 충돌이 일어나지 않는, 유일한 절대 모듈 경로를 사용하는 이름공간으로 코드를 나눌 수 있다.
  • 다른 소스 파일이 들어 있는 디렉터리에 __init__.py를 추가하면 간단한 패키지를 만들 수 있다.
  • 소스 파일들은 디렉터리로 인해 생긴 패키지의 자식 모듈이 된다.
  • 패키지 디렉터리에는 다른 패키지가 들어갈 수도 있다.
  • 모듈 외부에서 볼 수 있게 허용할 이름을 __all__ special attribute에 지정해, 공개 API를 제공할 수 있다.
  • 아래와 같은 방법으로 패키지 내부에서만 사용할 수 있는 이름을 감출 수 있다.
    • 패키지의 __init__.py 파일에 외부에 공개할 이름만 import 하거나,
    • 패키지 내부에서만 사용할 이름 앞에 _를 붙임으로써,
  • 단일 코드베이스나, 단일 팀 안에서 협업을 진행한다면, 아마도 __all__로 API를 명시할 필요가 없을 것이다.

본문

  • __init__.py 가 있는 디렉토리에선, 이 디렉터리에 있는 다른 모듈은 __init__.py가 있는 디렉터리를 기준으로 relative 경로를 통해 import해서 사용할 수 있다.
main.py
mypackage/__init__.py
mypackage/models.py
mypackage/utils.py

# main.py
from mypackage import utils
  • 패키지 왜 쓰는데?
    • 이름 공간
      • 모듈을 별도의 namespace로 분리하는 것
      • 다른 패키지에 같은 모듈 이름을 생성할 수 있다.
    • 안정적인 API
      • 엄격하고 안정적인 API를 외부 사용자에게 제공할 수 있다.
      • 외부 사람들에게 안정적인 기능을 제공하려면, 외부 사용자로부터 내부 코드 조직을 감춰야 한다.
      • 그렇게 해야 외부 사용자의 코드를 깨지 않으면서 여러분 패키지의 내부 모듈을 리펙토링하고 개선할 수 있다.
      • 패키지나 모듈의 __all__ special attribute를 통해, API 소비자에게 노출할 표면적 이름을 제한할 수 있다.
      • 대전제: wildcard import를 하지마라.
      • 아래 예제 중요!!
      • from foo import * 을 실행한 소비자 코드는
        • foo.__all__에 있는 attribute만 import 할 수 있고,
        • foo.__all__가 없으면, 공개 attribute(_가 없는)만 import 된다.
mypackage/__init__.py
 
__all__ = []

 
from .utils import *
__all__ += utils.__all__
# mypackage/utils.py

__all__ = ['simulate_collision']

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

86. 배포 환경을 설정하기 위해, 모듈 영역의 코드를 사용하라.

요약

  • 고유한 가정과 설정이 있는 다양한 배포 환경에서 프로그램을 실행해야 하는 경우가 많다.
  • 모듈 영역에서 일반적인 python 문을 사용하면, 각 배포 환경에 맞게 모듈의 내용을 조정할 수 있다.
  • 모듈 내용은 모든 외부 조건에 따라 달라질 수 있는 결과물이다. 외부 조건에는 sysos 모듈을 사용해 알아낸 host introspection 정보가 포함된다

본문

  • 배포 환경
    • production 환경
    • 개발 환경
# dev_main.py
TESTING = True
import db_connection
db = db_connection.Database()
# prod_main.py
TESTING = False
import db_connection
db = db_connection.Database()
# db_connection.py
import  __main__

class TestingDataBase:
	...
class RealDataBase:
	...
if __main__.TESTING:
	Database = TestingDataBase
else:
	Database = RealDataBase
  • if 문을 모듈 수준(함수나 메서드 내부의 깊은 수준이 아니라)에서 사용하면, 모듈 안에서 이름이 정의되는 방식을 결정할 수 있따.

  • 그리고 배포 환경이 복잡해지면,

    • TESTING 같은 설정을 코드와 구분하기 위해, 전용 설정 파일로 옮겨야 한다.
    • 그리고 TESTING 처럼 쓰지 않고, configparser 내장 모듈 같은 도구를 사용하면, production 설정을 코드로부터 분리해 유지 보수할 수 있다.
  • sys를 활용하여, 윈도우와 우분투 등의 환경에 맞게 실행하도록 할 수 있다.

import sys
class Win32DataBase:
	...
class PosixDataBase:
	...
if sys.platform.startswith('win32'):
	Database = Win32DataBase
else:
	Database = PosixDataBase
  • 비슷한 방식으로, os.environ에서 얻은 환경 변수를 모듈 정의에 참조할 수도 있다.

87. 호출자를 API로부터 보호하기 위해, 최상위 Exception을 정의하라.

요약

  • 모듈에서 사용할 최상위 예외를 정의하면, API 사용자들이 자신을 API로부터 보호할 수 있다.
  • 최상위 예외를 잡아내면, API를 소비하는 코드의 버그를 쉽게 찾을 수 있다.
  • 파이썬 내장 Exception 기반 클래스를 잡아내면, API 구현의 버그를 쉽게 찾을 수 있다.
  • 중간 단계의 최상위 예외를 사용하면, 미래에 새로운 타입의 예외를 추가할 때, API를 사용하는 코드가 깨지는 일을 방지할 수 있다.

본문

  • 모듈 API에서는, 모듈 내에 정의한 함수 또는 클래스만큼이나 발생시킬 예외도 중요하다.
  • 파이썬에는 내장 예외 타입이 존재하지만, API의 경우 새로운 예외 계층 구조를 정의하는 편이 훨씬 강력하다.
  • 아래와 같이 예외 계층 구조를 만들 수 있다.
# my_module.py
class Error(Exception):
	"""이 모듈에서 발생할 모든 예외의 상위 클래스."""
class InvalidDensityError(Error):
	"""밀도 값이 잘못된 경우"""
class InvalidVolumeError(Error):
	"""부피 값이 잘못된 경우"""
    
def determine_weight(volume, density):
	if density < 0:
    	raise InvalidDensityError('밀도는 0보다 커야 합니다')
    if volume < 0:
    	raise InvalidVolumeError('부피는 0보다 커야 합니다')
  • 이처럼 모듈 안에 최상위 예외가 있으면, 아래와 같은 방법으로 발생한 모든 오류를 더 쉽게 잡아낼 수 있다.
    • 아래 내용 에 예외의 전체 stack trace를 출력하기 때문에 쉽게 디버깅을 할 수 있다.
    • try/except 문을 사용하면, 우리 모듈에서 발생한 예외가, 모듈을 호출하는 코드로부터 아주 멀리 전달돼 프로그램이 꺠지는 상황을 방지할 수 있다.
try:
	weight = my_module.determine_weight(1, -1)
except my_module.Error:
	logging.exception('예상치 못한 오류')

>>>
ERROR:root:예상치 못한 오류
~~~~~내용~~~~
  • 최상위 예외의 3가지 효과
    • 최상위 예외가 있으면, API를 호출하는 사용자가 API를 잘못 사용한 경우를 더 쉽게 이해할 수 있다.
    • API 모듈 코드의 버그를 발견할 때 도움이 된다.
      • API 코드는 의도적으로 API 내에서 정의한 예외 계층에 속하는 예외만 발생시킬 수 있다.
      • 만약 사용자가 API 사용 중 다른 타입의 예외가 발생한다면, 그 예외는 우리가 의도하지 않은 것이므로, API 코드에 버그가 있다는 뜻이다.
try:
	weight = my_module.determine_weight(1, -1)
except my_module.InvalidDensityError:
	weight = 0
except my_module.Error:
	logging.exception('호출 코드에 버그가 있음')
except Exception:
	logging.exception('API 코드에 버그가 있음.')

>>>
ERROR:root:예상치 못한 오류
~~~~~내용~~~~
    • 미래의 API를 보호해 준다.
      • 시간이 지남에 따라 API를 확장해 특정 상황에서 더 구체적인 예외를 제공하고 싶을 때가 있다.
      • 그에 따라 API가 바뀌더라도, 기존의 API를 호출하는 코드에는 문제가 없다.
      • 이처럼, 최상위 예외 바로 아래에 폭넓은 예외 상황을 표현하는 다양한 오류를 제공하면, 미래의 코드 변경에 대한 보호를 더 강화할 수 있다.
      • 중간 단계의 최상위 예외를 사용하면, 미래에 새로운 타입의 예외를 추가할 때, API를 사용하는 코드가 깨지는 일을 방지할 수 있다.
      • 모든 API 호출 코드가 구체적인 Exception 하위 클래스 예외를 일일이 처리하는 것보다, 이런 식의 예외 계층 구조를 채택하는 편이 훨씬 낫다.
# my_module.py

class NegativeDensityError(InvalidDensityError):
	"""밀도가 음수인 경우."""
def determine_weight(volume, density):
	if density < 0:
    	raise NegativeDensityError('밀도는 0보다 커야 합니다')
# my_module.py

class Error(Exception):
	"""이 모듈에서 발생할 모든 예외의 상위 클래스."""
class DensityError(Error):
	"""밀도 값이 잘못된 경우의 상위 클래스"""
class VolumeError(Error):
	"""부피 값이 잘못된 경우의 상위 클래스"""

88. 순환 의존성을 깨는 방법을 알아두라.

요약

  • 두 모듈이 import 시점에 서로를 호출하면 순환 의존 관계가 생긴다. 순환 의존 관계가 있으면 프로그램이 시작되다가 오류가 발생하면서 중단될 수 있따.
  • 순환 의존 관계를 꺠는 가장 좋은 방법은, 상호 의존 관계를 의존 관계 트리의 맨 아래에 위치한 별도의 모듈로 refactoring 하는 것이다.
  • 동적 import는 refactoring과 복잡도 증가를 최소화하면서, 모듈 간의 순환 의존 관계를 깨는 가장 단순한 해법이다.

본문

  • 문제 상황
# dialog.py

import app

class Dialog:
	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:
	def get(self, name):
    	...
prefs = Prefs()
dialog.show()
  • app.py를 실행하면, 순환 참조 에러가 발생한다.
    • app의 5단계가 끝나지 않았는데, dialog의 5단계를 실행하는 도중에 에러가 터졌다.
  • 파이썬에서, 모듈이 import 되면 파이썬이 실제로 어떤 일을 하는지를 아래에 나타내었다.
    • sys.path에서 모듈 위치를 검색한다.
    • 모듈의 코드를 로딩하고, 컴파일되는지 확인한다.
    • 임포트할 모듈에 상응하는, 빈 모듈 객체를 만든다.
    • 모듈을 sys.modules에 넣는다. (import 문을 이용해 모듈 load 가능)
    • 모듈 객체에 있는 코드를 실행해서, 모듈의 내용을 정의한다. (모듈 attribute 정의)
  • 순환 참조를 해결하는 방법 1: 임포트, 설정, 실행
    • dialog/ app 모두 임포트 시, 동작을 수행하지 않게 정의하자.
    • import 후, 각 모듈을 설정하는 configure() 을 구현하자.
    • 단점
      • 객체를 정의하는 부분과, 객체를 설정하는 부분이 분리되기 때문에, 코드를 더 읽기 어려워진다.
      • 명시적인 configure 단계를 분리할 수 없을 때도 있다.
# dialog.py

import app

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

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

def show():
	...
# app.py
import dialog

class Prefs:
	def get(self, name):
    	...
def configure():
	prefs = Prefs()
# main.py
import app
import dialog
app.configure()
dialog.configure()

dialog.show()
  • 순환 참조를 해결하는 방법 1: 동적 import
    • 하지만 함수 호출시 ,매번 import를 돌 수 있으므로, 주의해야한다.
    • 하지만, 프로그램 전체를 바꿔서 순환 참조를 해결해야 하는 어려운 경우에는, 이 동적 import를 고려해보라.
# dialog.py


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

save_dialog = Dialog()

def show():
	import app # 동적 import
	save_dialog.save_dir = app.prefs.get('save_dir')
	...
# app.py
import dialog

class Prefs:
	def get(self, name):
    	...
prefs = Prefs()
dialog.show()

89. refactoring과 마이그레이션 방법을 알려주기 위해, warning을 사용하라.

요약

  • warnings 모듈을 사용하면, 여러분의 API를 호출하는 사용자들에게, 앞으로 사용 금지될 사용법에 대해 알려줄 수 있다.
    • 경고 메시지는 API 사용자들이 (API 변경으로 인해) 자신의 코드가 꺠지기 전에 코드를 변경하도록 권장한다.
  • -w error 명령줄 인자를 파이썬 interpretor에 넘기면, 경고를 오류로 높일 수 있다.
    • 의존 관계에서 잠재적인 회귀 오류가 있는지 잡아내고 싶은 자동화 테스트에서 이런 기능이 특히 유용하다.
  • 프로덕션 환경에서는 경고를 logging 모듈로 복제해, 실행 시점에 기존 오류 보고 시스템이 경고를 잡아내게 할 수 있다.
  • downstream 의존 관계에서, 알맞은 때 경고가 발동되도록 코드가 생성하는 경고에 대해 테스트를 작성하면 유용하다.

본문

  • 필요할 때 다시 공부하자!

90. typing과 정적 분석을 통해 버그를 없애라.

profile
새로운 것이 들어오면 이미 있는 것과 충돌을 시도하라.

0개의 댓글