잠시 옛날로 돌아가 과거를 회상해 보는 시간을 가지자..
1학년 컴퓨터 공학부 대학생들은 모두 c로 프로그래밍을 접하였다. 배열과 연산 챕터까지는 즐겁게 과제도 하면서 모두들 재능이 있다고 생각했었지만 메모리를 직접 다루는 포인터를 배우게 되면서 많은 학생들이 좌절했었던 것 같다.
이제는 대부분의 대학에서 1학년 프로그래밍 수업에서 파이썬을 많이 가르치고 있다.
다시 말해 파이썬은 명실상부한 '프로그래밍 1도 모르는데, 뭘로 입문하는게 좋을까요?' 라는 질문에 많은 사람들이 추천할 수 있는 언어가 되었다고 본다.
내가 생각하는 파이썬의 장점은 몇 가지가 있는데,
그런데 내가 파이썬을 배우고, 이걸 개인 / 팀 프로젝트에서 쓸 때에는 어떤 방식으로 프로젝트를 만들어야 좋은 것일지에 대한 가이드라인을 많이 참고하지 못했던 것 같다.
그 당시에도 물론 찾아보면 밑에서 소개할 내용들이 있었겠지만 왜 이런 내용을 진작에 알지 못했을까..! 하는 한탄이 들어 조금 정리해보았다.
내 파이썬 프로젝트를 어떻게 만들어야 잘 만들었다고 소문이 날까?
https://docs.python-guide.org/writing/structure/
프로젝트의 구조는 프로젝트의 목표를 가장 잘 달성할 수 있는 방법에 대한 결정이다.
어떤 프로그래밍 언어로 작성된 프로젝트라면 깔끔하고 효과적인 코드를 작성하기 위해서 해당 언어의 기능을 어떻게 하면 가장 잘 활용할 수 있을지를 고려해야 한다는 뜻이다.
즉 '프로젝트의 구조'는 아래 전부를 통틀어 말하는 것이다.
레포지토리 구조는 프로젝트의 설계적인 측면에서 중요한 부분이다.
잠재적인 사용자, 프로젝트에 기여할 컨트리뷰터, 새롭게 팀에 합류하게 되는 동료가 레포지토리 페이지에 랜딩했을 때 볼 수 있는 것들은 아래와 같다.
레포지토리 페이지 구조 상 파일들의 목록을 조회한 후 readme를 읽도록 되어 있다. 때문에 만약 레포지토리가 정리되지 않은, 그저 엄청나게 많은 파일 또는 디렉토리로만 구성되어 있다면 문서화를 얼마나 잘했던간에 마음이 떠날 수도 있다.
https://kennethreitz.org/essays/2013/01/27/repository-structure-and-python
README.rst
LICENSE
setup.py
requirements.txt
sample/__init__.py
sample/core.py
sample/helpers.py
docs/conf.py
docs/index.rst
tests/test_basic.py
tests/test_advanced.py
각 목록을 하나씩 뜯어보자.
./sample/
)src
또는 python
서브 디렉토리에 속해서는 안된다.LICENSE
)setup.py
)requirements.txt
)./docs/
)./test_sample.py
또는 ./tests
)./test_sample.py
)tests/test_basic.py
, test/test_advanced.py
)./Makefile
)manage.py
또는 fabfile.py
와 같은 일반적인 관리 스크립트 또한 레포지토리의 root에 위치해야 한다.파이썬에서 import 하고 module을 제공하는 방식 때문에 파이썬 프로젝트를 구성하기는 상대적으로 쉽다. (쉽다는 건 모듈과 모델을 가져오는데 있어서 큰 제약 사항이 없다는 의미이다)
따라서 개발자는 프로젝트의 여러 부분과 그 상호작용을 만들기 위한 순수한 아키텍쳐 작업에 신경써야 한다.
쉽게 프로젝트를 구성한다는 의미는 쉽게 프로젝트 구조를 망치는 것과 같다.
잘 구성되지 않은 프로젝트들의 특징으로는
Table
의 구현을 변경할 때마다 관련 없는 테스트 케이스에서 다수의 테스트가 깨지는 경우이다. Carpenter
의 코드에서 Table
에 대한 가정이 너무 많거나 Table
의 코드에서 Carpenter
에 대한 가정이 너무 많은 경우Table
의 구현이 전역 변수의 상태에 의존하여 이뤄졌다고 가정해보자. 직사각형의 Table
이 정사각형이 되는 버그의 원인을 찾아야 할 때..FurnitureTable
, AssetTable
, Table
, TableNew
를 사용해야 하는 경우이다.파이썬 모듈은 가장 자연스러우면서도 주로 사용하게 되는 추상화 방법이다.
추상화 계층은 관련된 데이터와 기능을 가지는 코드의 부분으로 분리할 수 있게 해준다.
import
선언문을 사용했다는 건 모듈을 사용한다는 뜻이다. 빌트인 모듈인 os
나 sys
서부터, 개별 환경에 설치된 서드 파틸 모듈이나 프로젝트의 내부 모듈이 될 수 있다.
스타일 가이드를 따라가기 위해서는 모듈의 이름을 짧게, lowercase로, 특수문자 (.
, ?
)를 포함하지 않게 지어야 한다.
my.spam.py
파일은 my
폴더 안에 들어 있는 spam.py
파일이 존재할 것이라고 예상하게 해준다. my_spam.py
으로 이름을 지을 수도 있지만 모듈 이름 내에서는 자주 등장하지 않는 것이 좋다.-
)을 쓰라는 이름이 아니다!정리하자면 모듈 이름을 최대한 짧게 짓되, 어쩔 수 없이 두 단어 이상으로 분리되는 경우라면 서브 모듈을 쓰는 것이 좋다.
네이밍 컨벤션만 잘 지킨다면 파이썬 파일 내에서는 모듈에 대해 딱히 필요한 것이 없다. 하지만 모듈의 컨셉을 잘 쓰고 특정 이슈를 피하기 위해서는 import를 조금 더 알아볼 필요가 있다.
import modu
선언문은 호출한 파일의 동일한 디렉토리 내에서 알맞은 파일인 modu.py
를 찾는다.
modu.py
파일을 'path'에서 재귀적으로 찾는다.ImportError
예외를 던진다.modu.py
파일을 찾았다면
modu.py
파일의 모든 최상위 선언문이 실행된다.import
문도 실행된다. import
선언문을 호출한 호출자에서 모듈의 네임스페이스 하에 이용 가능해진다.많은 프로그래밍 언어에서 include file
디렉티브는 전처리기에 의해 찾아진 파일의 코드를 전부 복사하여 호출자의 코드로 옮겨 담는다. 파이썬은 그렇지 않다. 불러와진 코드는 독립된 모듈 네임스페이스에 분리되기 때문에 해당 코드가 원치 않은 효과를 내는 것을 방지한다.
때문에
from modu import *
는 좋지 않다.import *
를 사용하는 것은 코드를 읽기 어렵게 만들고 종속성을 구분하기 어려워진다.
그 대신 from modu import func
는 가져올 함수를 특정하여 로컬 네임스페이스에 넣는 방식이다. 로컬 네임스페이스로 가져올 대상을 명시적으로 보여주기 때문에 import *
보다 훨씬 나은 방식이다.
파이썬은 모듈이 동작하는 방식을 디렉토리로 확장한, 굉장히 직접적인 패키징 시스템을 제공한다.
__init__.py
파일이 포함된 모든 디렉토리는 파이썬 패키지로 간주된다.
패키지 내의 서로 다른 모듈들은 모듈이 불러와지는 방식과 유사하게 import
되지만 __init__.py
파일은 모든 패키지 정의를 가져온다는 점이 특별한다.
pack/
디렉토리 안에 들어 있는 modu.py
파일은 import pack.modu
선언문을 통해 import
된다.
pack
디렉토리 안에 있는 __init__.py
파일을 찾고 해당 파일의 최상위 선언문을 모두 실행한다.pack/modu.py
파일을 찾고, 해당 파일의 모든 최상위 선언문을 모두 실행한다.modu.py
파일 내에 정의되어 있는 명령어, 변수, 함수, 클래스가 pack.modu
네임스페이스 안에서 사용 가능하다.프로젝트의 복잡성이 증가함에 따라 서브 패키지와 서브에 서브 패키지가 생기게 되기도 한다. 이런 경우 디렉토리 트리 구조를 순회하면서 모든 __init__.py
를 실행한다.
때문에
__init__.py
파일을 빈 상태로 유지하는 것이 정상적인 방법이며 권장된다. 패키지의 모듈과 서브 패키지는__init__.py
파일을 통해 어떠한 코드를 공유할 필요도 없기 때문이다.
드디어 나왔다. '그 주제'
파이썬은 가끔 객체 지향 프로그래밍 언어로 언급되기도 한다. 다소 오해의 소지가 있을 수 있기 때문에 추가 설명이 필요한 부분이 아니라고 할 수 없겠다.
파이썬에서 모든 것은 객체이며, 그렇게 처리할 수 있다.
파이썬에서의 함수가 일급 객체라고 말할 수 있는 것처럼, 함수, 클래스, 문자열, 심지어 타입 또한 파이썬에서는 객체이다. 때문에 파이썬에서 모든 것들은
이런 면에서 본다면 파이썬은 객체 지향 언어가 맞다.
하지만 자바와는 다르게 파이썬은 객체 지향 프로그래밍을 메인 프로그래밍 패러다임으로써 강요하지 않는다.
모듈 섹션에서 보았던 것처럼 파이썬은 모듈과 네임스페이스를 써서 개발자로 하여금 캡슐화와 추상화 계층을 분리할 수 있게 해준다. 그리고 이는 객체 지향을 사용하는 주된 이유이다. 그렇기 떄문에 파이썬 개발자들은 비즈니스 모델에 의해 필요한 것이 아니라면 객체 지향에 집착할 필요가 없다.
클래스를 정의하는 것은 어떤 상태를 제공하고 기능들을 묶는데 유용하지만 이 '상태'라는 것이 얼마나 문제가 될 수 있는지 객체 지향에 익숙한 개발자라면 알 것이다.
웹 애플리케이션을 작성한다고 가정해보자.
때문에 함수를 컨텍스트와 분리하고 stateless 하게 만들어야 한다. (순수 함수)
컨텍스트와 무관한, 순수한 함수를 만드는 것은 다음의 이점이 있다.
1. 고정된 입력을 넣으면 항상 동일한 출력이 '결정'된다 (deterministic)
2. 순수 함수는 리팩토링 또는 최적화를 위해 변경하기 용이하다
3. 순수 함수는 유닛 테스트로 테스트하기 쉬워진다. 복잡한 컨텍스트 생성이나 이후 데이터 정리 작업이 필요하지 않기 때문이다.
4. 순수 함수는 조작하거나, 꾸미거나, 전달하기 더 쉽다.
하지만 그럼에도 불구하고 객체 지향은 유용하고, 많은 경우에 필요하다. 유즈케이스에 따라 알맞은 패러다임을 가져가는 것은 개발자의 역량에 달려 있다.
여러 개의 창과 버튼이 있는 gui 또는 게임을 만든다고 가정한다면 기기 메모리에 오랫동안 유지되는 상태가 필요하고, 이런 경우에는 객체 지향 접근이 유용하게 동작할 수 있다.
파이썬은 데코레이터라는 간단하면서도 강력한 문법을 지원한다.
데코레이터는 함수 / 메서드를 감싸는 함수 또는 클래스이다.
데코레이터의 대상이되는 함수 / 메서드는 기존의 데코레이트되지 않은 함수를 교체한다. 함수는 파이썬에서 일급 객체이기 때문에 이 과정은 매뉴얼하게 이뤄질 수도 있다. 하지만 @decorator
문법을 쓰는 것이 더 깔끔하기 때문에 권장된다.
def foo():
# 어떤 작업
def decorator(func):
# func를 조정한다.
return func
foo = decorator(func) # 매뉴얼하게 데코레이터를 적용시킨다.
@decorator
def bar():
# 어떤 작업
# bar()는 데코레이트되었다.
이런 동작 방식은 관심사를 분리하고 외부의 관련 없는 로직이 함수 또는 메서드의 핵심 로직을 오염시키는 것을 방지할 수 있다.
데코레이터를 잘 활용할 수 있는 좋은 예시로는 메모이제이션과 캐싱이 있다.
실행 비용이 비싼 함수의 결과를 테이블에 저장하고 이미 계산한 결과를 다시 계산하는 대신 저장된 결과를 사용하는 경우 데코레이터를 활용한다면 함수의 로직과 관련 없는 부분을 잘 감쌀 수 있다.
컨텍스트 매니저는 어떤 행위에 대한 추가적인 맥락 정보를 제공하는 파이썬 객체이다.
'추가 맥락 정보' 라 함은 with
절을 사용하여 컨텍스트를 시작할 때 callable을 실행하고, with
블록 내 모든 코드의 실행이 완료되면 callable를 실행한다. 컨텍스트 매니저를 사용하는 가장 유명한 예시로는 파일을 여는 아래의 짧은 예제가 될 수 있겠다.
with open('file.txt') as f:
contents = f.read()
이런 패턴에 익숙한 사람이라면 open
을 이런 방식으로 사용하는 것은 f
의 close
메서드를 어떤 지점에서 실행할 것이라는 것이 보장됨을 알고 있을 것이다. 이는 개발자의 인지 부하를 줄이고 코드를 읽기 쉽게 만든다. (내가 close
를 깜빡하고 제대로 안 쓴다면 큰 사고가 발생할 수 있다!)
컨텍스트 매니저를 사용하기 위해서는 클래스를 사용하거나 제네레이터를 사용하는 방법이 있다.
class CustomOpen(object):
def __init__(self, filename):
self.file = open(filename)
def __enter__(self):
return self.file
def __exit__(self, ctx_type, ctx_value, ctx_traceback):
self.file.close()
with CustomOpen('file') as f:
contents = f.read()
그렇다. CustomOpen
이 인스턴스화된 다음 __enter__
메서드가 호출되고, __enter__
가 반환하는 값이 f
에 할당된다. with
블록의 실행이 완료되면 __exit__
메서드가 호출된다.
from contextlib import contextmanager
@contextmanager
def custom_open(filename):
f = open(filename)
try:
yield f
finally:
f.close()
with custom_open('file') as f:
contents = f.read()
위와 같이 제네레이터를 사용한 방식은 클래스를 사용한 예시와 동일하게 동작하지만 좀 더 간결하다.
custom_open
함수는 yield
문에 도달할 때까지 실행된다.with
문에 다시 제어권을 넘겨준다.yield
된 것을 f
에 할당한다.finally
절에서는 with
문 안에 예외가 있었는지에 관계 없이 close()
가 호출되도록 한다.파이썬은 동적 타입 언어이다. 즉 변수는 고정된 타입을 가지지 않는다.
사실 파이썬에서 변수는 다른 언어에서 취급되는 것과 상당히 다르다. 특히 정적 타입 언어와는 완전히 다르다고 볼 수 있다.
파이썬에서 변수는 어떤 값이 기록되는 컴퓨터 메모리의 세그먼트가 아니라, 객체를 가르키는 '태그' 또는 '이름'이다.
때문에 변수 'a'를 1로 값을 설정한 다음 'string' 이라는 값을, 또는 함수로 설정할 수 있다.
파이썬의 동적 타입은 개발 복잡성 증대 및 디버깅 하기 어려운 코드를 만드는, 약점으로도 지적된다. 'a'라고 불릴 수 있는 것은 너무나도 많은 것으로 할당될 수 있으며, 개발자는 코드 내에서 해당 이름을 추적하여 완전히 관련 없는 객체로 설정되지 않도록 확실히 해줘야 한다.
a = 1
a = 'a string'
def a():
pass # 어떤 동작을 하는 함수 할당
items = 'a b c d' # 문자열 할당
items = items.split(' ') # list로 바꿔서 할당하기
items = set(items) # 다시 set으로 할당하기
위와 같이 이름을 재사용해도 어차피 새로운 객체를 생성해야 하기 때문에 전혀 효율적이지 않다. 하지만 복잡성이 커지고 각 할당이 if
분기와 반복문 등 다른 코드로 분리되면 주어진 변수의 유형을 확인하기 더 어렵다.
함수형 프로그래밍과 같은 일부 코딩 방식에서는 변수를 재할당하지 않는 것을 권장한다.
final
키워드를 사용하여 재할당을 방지할 수 있다.final
키워드가 없고, 어쨌든 재할당을 억지로 막는 것은 파이썬의 철학에 위배되는 행위이다. https://docs.python.org/3/library/typing.html
3.5 버전부터는 타입 힌트를 지원하기 위한 내장 라이브러리인 typing
이 도입되었다.
def moon_weight(earth_weight: float) -> str:
return f'On the moon, you would weigh {earth_weight * 0.166} kilograms.'
위와 같이 사용할 수 있으며, moon_weight
함수는 float
타입의 인스턴스를 인수로 받아 str
타입의 인스턴스를 리턴할 것을 예상가능하게 해준다.
마치 타입스크립트와 같다고 생각할 수 있지만, 파이썬 런타임은 함수나 변수에 타입 어노테이션을 강제하지 않는다. 즉 타입 체커, ide, 린터와 같은 서드 파티 툴에서 활용될 수 있을 뿐이다.
심지어 파이썬은 인터프리터 언어라는 점 때문에 트랜스파일 과정을 거치는 타입스크립트보다 어쩌면 타입에 대해 '유연한' 언어이다.
def myfun(num1: int, num2: int) -> int:
return str(num1) + num2
a = myfun(1, 'abc')
print(a)
# output -> 1abc
위와 같은 방식의 코드는 타입스크립트에서 에러를 냈겠지만 파이썬에서는 에러를 발생시키지 않는다.
@classmethod
https://docs.python.org/3/library/functions.html#classmethod
메서드를 클래스 메서드로 변환한다.
C.f()
도 가능하고 C().f()
도 가능하다@staticmethod
https://docs.python.org/3/library/functions.html#staticmethod
해당 데코레이터가 붙은 메서드를 정적 메서드로 변환시킨다.
C.f()
) 인스턴스에서 호출될 수도 있다. (C().f()
)f()
)staticmethod
자체를 일반 함수로 호출하고 그 결과를 써먹을 수도 있다.def regular_function():
...
class C:
method = staticmethod(regular_function)
파이썬 코드를 살펴보다 보면 수 많은 self
키워드를 볼 수 있다. c나 java 와 같은 프로그래밍 언어에 익숙한 (나같은) 사용자에게는 조금 낯설고 이질적인 문법이었다고 기억한다.
함수를 짜는 입장에서는 불필요한 인자를 하나 더 추가하게 되는 것 같아 궁금해졌다.
self의 사용을 제거하려던 proposal도 있었던 것으로 봐서는 나만이 이런 생각을 한 건 아니라고 생각했다.
왜 파이썬에서 객체 함수의 첫번째 인자로 self
키워드를 전달해줘야 할까?
파이썬에서는 인스턴스 자신의 애트리뷰트를 가르키는 특정 문법이 없기 때문이다.
self
문자열이 아니어도 된다. 관행적으로 사용할 뿐이다.self
는 코드 상에 특별한 키워드가 아니라 또 다른 객체를 가르키는 것일 뿐이다.파이썬은 모든 것을 명시적으로 만들고 '무엇이 무엇인지'를 명확하게 하는데 중점을 둔다.
파이썬의 철학에 따라 인스턴스 애트리뷰트에 할당하기 위해서는 '어떤 인스턴스에 할당할지'를 알아야 하고, 그렇기 때문에 self
가 필요한 것이다.
https://www.geeksforgeeks.org/difference-between-function-and-method/
https://docs.python.org/3/reference/executionmodel.html
파이썬 코드는 어떻게 실행될까?
name 은 객체를 참조한다. 그리고 name은 name binding operation에 의해 생성된다. 아래 목록은 name 생성에 관여하는 녀석들이다.
with
문, except
절, except*
절import
선언문type
선언문예를 들어 from ... import *
와 같은 import
문은 참조된 모듈에 존재하는 모든 name들을 바인딩한다. (_
로 시작하는 녀석들은 제외)
import
선언문은 클래스 또는 함수 정의에 의해 묶이는 블록 내 또는 모듈 수준에서 발생한다. nonlocal
또는 global
로 선언되지 않은 블록 내에서 바운드되는 name은 블록 내 로컬 변수가 된다. 스코프는 블록 내 name에 대한 가시성을 정의한다. 만약 로컬 변수가 블록 안에서 정의되었다면 해당 변수의 스코프는 그 블록이 된다.
코드 블록 내에서 name이 사용되는 경우 가장 가까운 둘러싸는 스코프를 사용하여 name을 확인한다. 코드 블록에 표시되는 이런 모든 범위의 집합을 블록의 환경이라고 부른다.
NameError
에러가 발생한다. UnboundLocalError
예외가 발생한다.UnboundLocalError
는 NameError
의 서브 클래스이다.global
문이 블록 내에서 발생하는 경우 global
문에 지정된 이름의 모든 사용은 최상위 네임스페이스에 있는 해당 이름의 바인딩을 참조한다.
__init__.py
파일은 되도록 빈 파일로 두는 것이 좋다.
정리를 엄청 잘 해 주셨네요. 도움 많이 받고갑니다.