대학생 때 알았어야 할 '좋은 파이썬 프로젝트' 만드는 법

redjen·2024년 4월 29일
14

월간 딥다이브

목록 보기
4/11
post-thumbnail

시작은 미약하였으나

잠시 옛날로 돌아가 과거를 회상해 보는 시간을 가지자..

1학년 컴퓨터 공학부 대학생들은 모두 c로 프로그래밍을 접하였다. 배열과 연산 챕터까지는 즐겁게 과제도 하면서 모두들 재능이 있다고 생각했었지만 메모리를 직접 다루는 포인터를 배우게 되면서 많은 학생들이 좌절했었던 것 같다.

이제는 대부분의 대학에서 1학년 프로그래밍 수업에서 파이썬을 많이 가르치고 있다.

다시 말해 파이썬은 명실상부한 '프로그래밍 1도 모르는데, 뭘로 입문하는게 좋을까요?' 라는 질문에 많은 사람들이 추천할 수 있는 언어가 되었다고 본다.

내가 생각하는 파이썬의 장점은 몇 가지가 있는데,

  • 간결하고 직관적인 문법 (초보자도 탈주하지 않을..)
  • 다양한 라이브러리와 프레임워크, 그리고 활발한 커뮤니티와 문서화
  • 빌드 / 컴파일이 필요없음
  • 흥미를 위한 개발이 가장 쉽다 (AI 찍먹)

그런데 내가 파이썬을 배우고, 이걸 개인 / 팀 프로젝트에서 쓸 때에는 어떤 방식으로 프로젝트를 만들어야 좋은 것일지에 대한 가이드라인을 많이 참고하지 못했던 것 같다.

그 당시에도 물론 찾아보면 밑에서 소개할 내용들이 있었겠지만 왜 이런 내용을 진작에 알지 못했을까..! 하는 한탄이 들어 조금 정리해보았다.

내 파이썬 프로젝트를 어떻게 만들어야 잘 만들었다고 소문이 날까?

더 견고하게

프로젝트 구조

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/)
    • 모듈 패키지는 레포지토리의 주된 관심사이다.
    • 모듈이 만약 하나의 파일로만 구성된다면, 레포지토리 구조의 root에 위치시킬 수 있다.
    • 라이브러리는 모호한 src 또는 python 서브 디렉토리에 속해서는 안된다.
  • 라이선스 (LICENSE)
    • 소스 코드를 제외한다면 레포지토리의 가장 중요한 부분이다.
    • 라이선스의 전체 텍스트와 저작권에 대한 내용이 포함되어야 한다.
  • 설치 파일 (setup.py)
    • 패키지와 배포 관리에 관한 파일
    • 모듈 패키지가 레포지토리의 root에 존재하지 않는다면 명백하게 이 파일은 root에 존재해야 한다.
  • 의존성 관리 파일 (requirements.txt)
    • pip로 의존성 관리를 한다면 레포지토리의 root에 해당 파일이 위치해야 한다.
    • 테스트, 빌드, 문서화 등 프로젝트에 기여하는 모든 의존성을 구체적으로 명시한다.
    • 프로젝트가 어떠한 라이브러리에도 의존하지 않는다면 이 파일은 필요 없을 수도 있다.
  • 문서 (./docs/)
    • 패키지 참조 문서
  • 테스트 스위트 (./test_sample.py 또는 ./tests)
    • 패키지 통합과 단위 테스트를 위한 파일
    • 작은 테스트 스위트는 단일 파일로 구성될 때가 많다. (./test_sample.py)
    • 테스트 스위트가 커짐에 따라 테스트들을 디렉토리로 옮길 수 있다. (tests/test_basic.py, test/test_advanced.py)
    • 이런 테스트 모듈들은 당신의 패키지된 모듈을 import 하여 테스트해야 한다.
  • Makefile (./Makefile)
    • 엥? 그거 C 프로젝트를 진행할 때만 필요한거 아닌가? 할수도 있지만..
    • 프로젝트가 플랫폼에 종속적이지 않도록 하게 하는 아주 유용한 툴이다.
    • manage.py 또는 fabfile.py 와 같은 일반적인 관리 스크립트 또한 레포지토리의 root에 위치해야 한다.

좋은 코드 구조

파이썬에서 import 하고 module을 제공하는 방식 때문에 파이썬 프로젝트를 구성하기는 상대적으로 쉽다. (쉽다는 건 모듈과 모델을 가져오는데 있어서 큰 제약 사항이 없다는 의미이다)

따라서 개발자는 프로젝트의 여러 부분과 그 상호작용을 만들기 위한 순수한 아키텍쳐 작업에 신경써야 한다.

쉽게 프로젝트를 구성한다는 의미는 쉽게 프로젝트 구조를 망치는 것과 같다.

잘 구성되지 않은 프로젝트들의 특징으로는

  • 다수의 관리되지 않은 순환 종속: 순환 종속이 발생하게 되는 경우 함수 내에서 import 문을 사용하는 등 권장되지 않는 편법을 써야만 한다.
  • 숨겨진 결합: Table 의 구현을 변경할 때마다 관련 없는 테스트 케이스에서 다수의 테스트가 깨지는 경우이다.
    • 가령 Carpenter의 코드에서 Table에 대한 가정이 너무 많거나 Table의 코드에서 Carpenter 에 대한 가정이 너무 많은 경우
  • 전역 상태 또는 컨텍스트를 남용: Table의 구현이 전역 변수의 상태에 의존하여 이뤄졌다고 가정해보자. 직사각형의 Table이 정사각형이 되는 버그의 원인을 찾아야 할 때..
    • 전역 변수를 사용하는 모든 코드를 살펴봐야 한다.
    • 특정 코드가 특정 컨텍스트를 수정하고 있다는 사실을 발견하기 전까지는 이 버그를 해결할 수 없을 것이다.
  • 스파게티 코드: 파이썬에서의 스파게티 코드는 다수의 if 문과 for 반복문, 그리고 단순히 복붙한 절차지향적 코드와 세분화 없는 코드로 이뤄져있다.
    • 파이썬은 특히 들여쓰기를 통해 코드의 의미를 부여하기 때문에 이런 코드를 유지보수하기 더욱 까다롭다.
  • 라비올리 코드: 적절한 구조 없이 클래스 또는 객체 등 수백 개의 유사한 작은 로직 조각으로 구성된 코드를 말한다. 파이썬에서는 더 발생하기 쉽다.
    • 어떠한 문제를 해결하기 위해서 FurnitureTable, AssetTable, Table, TableNew를 사용해야 하는 경우이다.

모듈

파이썬 모듈은 가장 자연스러우면서도 주로 사용하게 되는 추상화 방법이다.

추상화 계층은 관련된 데이터와 기능을 가지는 코드의 부분으로 분리할 수 있게 해준다.

import 선언문을 사용했다는 건 모듈을 사용한다는 뜻이다. 빌트인 모듈인 ossys 서부터, 개별 환경에 설치된 서드 파틸 모듈이나 프로젝트의 내부 모듈이 될 수 있다.

스타일 가이드를 따라가기 위해서는 모듈의 이름을 짧게, lowercase로, 특수문자 (., ?)를 포함하지 않게 지어야 한다.

  • my.spam.py 파일은 my 폴더 안에 들어 있는 spam.py 파일이 존재할 것이라고 예상하게 해준다.
  • my_spam.py 으로 이름을 지을 수도 있지만 모듈 이름 내에서는 자주 등장하지 않는 것이 좋다.
    • 그렇다고 공백이나 하이픈 (-)을 쓰라는 이름이 아니다!

정리하자면 모듈 이름을 최대한 짧게 짓되, 어쩔 수 없이 두 단어 이상으로 분리되는 경우라면 서브 모듈을 쓰는 것이 좋다.

네이밍 컨벤션만 잘 지킨다면 파이썬 파일 내에서는 모듈에 대해 딱히 필요한 것이 없다. 하지만 모듈의 컨셉을 잘 쓰고 특정 이슈를 피하기 위해서는 import를 조금 더 알아볼 필요가 있다.

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을 이런 방식으로 사용하는 것은 fclose 메서드를 어떤 지점에서 실행할 것이라는 것이 보장됨을 알고 있을 것이다. 이는 개발자의 인지 부하를 줄이고 코드를 읽기 쉽게 만든다. (내가 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'라고 불릴 수 있는 것은 너무나도 많은 것으로 할당될 수 있으며, 개발자는 코드 내에서 해당 이름을 추적하여 완전히 관련 없는 객체로 설정되지 않도록 확실히 해줘야 한다.

1. 서로 다른 것에 대해 동일한 변수명을 사용하지 말 것

a = 1
a = 'a string'
def a():
    pass # 어떤 동작을 하는 함수 할당
  • 짧은 함수를 사용한다면 서로 관련 없는 두 가지에 같은 이름을 사용할 위험을 줄이는데 도움이 된다.
  • 관련성이 있는 항목이라도 타입이 다른 경우라면 다른 이름을 사용하는 것이 좋다.

2. 한 변수에 서로 다른 타입을 재할당하지 말 것

items = 'a b c d'  # 문자열 할당
items = items.split(' ')  # list로 바꿔서 할당하기
items = set(items)  # 다시 set으로 할당하기

위와 같이 이름을 재사용해도 어차피 새로운 객체를 생성해야 하기 때문에 전혀 효율적이지 않다. 하지만 복잡성이 커지고 각 할당이 if 분기와 반복문 등 다른 코드로 분리되면 주어진 변수의 유형을 확인하기 더 어렵다.

함수형 프로그래밍과 같은 일부 코딩 방식에서는 변수를 재할당하지 않는 것을 권장한다.

  • java에서는 final 키워드를 사용하여 재할당을 방지할 수 있다.
  • 하지만 파이썬에는 final 키워드가 없고, 어쨌든 재할당을 억지로 막는 것은 파이썬의 철학에 위배되는 행위이다.
  • 그러나 변수에 두 번 이상 할당하지 않는 것은 좋은 규칙이고, 변경 가능한 유형과 변경 불가능한 유형의 개념을 파악하는데 도움이 된다.

typing 라이브러리

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, 린터와 같은 서드 파티 툴에서 활용될 수 있을 뿐이다.

심지어 파이썬은 인터프리터 언어라는 점 때문에 트랜스파일 과정을 거치는 타입스크립트보다 어쩌면 타입에 대해 '유연한' 언어이다.

https://stackoverflow.com/questions/67412233/python-type-hinting-not-generating-error-for-wrong-type-when-running-the-code

def myfun(num1: int, num2: int) -> int:
    return str(num1) + num2


a = myfun(1, 'abc')
print(a)
# output -> 1abc

위와 같은 방식의 코드는 타입스크립트에서 에러를 냈겠지만 파이썬에서는 에러를 발생시키지 않는다.

  • 즉 해당 라이브러리는 어디까지나 타입에 대한 힌트를 제공해준다.
  • 런타임에는 해당 타입 힌트는 무의미해진다.
  • 하지만 IDE 또는 문서에 동작에 대해 알려줌으로써 함수를 제대로 사용하지 않고 있다는 것만 알려준다고 해도 의미가 없지는 않다.

빌트인 함수 데코레이터

@classmethod

https://docs.python.org/3/library/functions.html#classmethod

메서드를 클래스 메서드로 변환한다.

  • 클래스 메서드는 클래스를 암묵적으로 첫 번째 인수로 받는다.
    • 이는 인스턴스의 메서드가 인스턴스를 인수로 받는 것과 같다.
  • 클래스 메서드는 클래스에서 호출되거나 클래스의 인스턴스에서 호출될 수 있다. C.f() 도 가능하고 C().f() 도 가능하다
    • 인스턴스에서 호출되는 경우 인스턴스는 무시되고 클래스만 사용된다.
    • 클래스 메서드가 파생 클래스에서 호출된 경우 파생 클래스의 객체가 암묵적인 첫번째 인수로써 전달된다.
  • 바로 밑에서 설명하겠지만 클래스 메서드는 C++이나 자바의 정적 메서드와는 다르다.

@staticmethod

https://docs.python.org/3/library/functions.html#staticmethod

해당 데코레이터가 붙은 메서드를 정적 메서드로 변환시킨다.

  • 정적 메서드는 클래스에서 호출되거나 (C.f()) 인스턴스에서 호출될 수도 있다. (C().f())
    - 또는 일반 함수로써 호출될 수도 있다. (f())
  • 파이썬에서의 정적 메서드는 자바 또는 C++의 정적 메서드와 유사하다.
  • 다른 데코레이터처럼, staticmethod 자체를 일반 함수로 호출하고 그 결과를 써먹을 수도 있다.
    - 이는 클래스 본문에서 함수에 대한 참조가 필요하고 인스턴스 메서드로의 자동 변환을 피하고 싶은 경우에 유용하다.
def regular_function():
    ...

class C:
    method = staticmethod(regular_function)

과거의 나에게

클래스에서의 self

https://stackoverflow.com/questions/2709821/what-is-the-purpose-of-the-self-parameter-why-is-it-needed

파이썬 코드를 살펴보다 보면 수 많은 self 키워드를 볼 수 있다. c나 java 와 같은 프로그래밍 언어에 익숙한 (나같은) 사용자에게는 조금 낯설고 이질적인 문법이었다고 기억한다.

함수를 짜는 입장에서는 불필요한 인자를 하나 더 추가하게 되는 것 같아 궁금해졌다.

self의 사용을 제거하려던 proposal도 있었던 것으로 봐서는 나만이 이런 생각을 한 건 아니라고 생각했다.

왜 파이썬에서 객체 함수의 첫번째 인자로 self 키워드를 전달해줘야 할까?

파이썬에서는 인스턴스 자신의 애트리뷰트를 가르키는 특정 문법이 없기 때문이다.

  • 파이썬에서 메서드가 속한 인스턴스는 자동으로 전달은 되지만 자동으로 받지는 못하는 방식으로 동작한다.
  • 메서드의 첫 번째 매개변수는 메서드가 호출되는 인스턴스이다.
    - 이렇게 한다면 메서드는 함수와 완전히 동일해진다.
    - 실제 매개변수는 사실 self 문자열이 아니어도 된다. 관행적으로 사용할 뿐이다.
  • self는 코드 상에 특별한 키워드가 아니라 또 다른 객체를 가르키는 것일 뿐이다.

파이썬은 모든 것을 명시적으로 만들고 '무엇이 무엇인지'를 명확하게 하는데 중점을 둔다.
파이썬의 철학에 따라 인스턴스 애트리뷰트에 할당하기 위해서는 '어떤 인스턴스에 할당할지'를 알아야 하고, 그렇기 때문에 self 가 필요한 것이다.

ps. 메서드와 함수의 차이점

https://www.geeksforgeeks.org/difference-between-function-and-method/

  • 함수는 재사용 가능한 코드 조각이다. 인수를 가질 수 있으며 어떤 데이터를 반환할 수 있다.
  • 메서드는 인수와 반환 데이터를 가질 수 있다는 점에서 함수와 비슷하다. 하지만
    - 메서드는 객체의 인스턴스에 연관되어 있다.
    - 메서드는 메서드를 가지는 클래스 안의 데이터를 조작하는데 한정되어 있다.
    - 때문에 함수보다는 객체 지향적 성격이 있다.

Execution Frames

https://docs.python.org/3/reference/executionmodel.html

파이썬 코드는 어떻게 실행될까?

  • 파이썬 프로그램은 코드 블록으로부터 만들어진다.
  • 블록은 파이썬 프로그램의 한 조각으로써, 하나의 유닛으로 실행된다.
    • 모듈, 함수 바디, 클래스 정의도 블록이다.
    • 인터프리터에 전달되는 스크립트 파일도 블록이다.
    • 커맨드 라인을 통해 상위 레벨에서 실행되는 모듈도 블록이다.
    • 빌트인 함수에 전달되는 문자열 인수도 코드 블록이다.
  • 코드 블록은 실행 프레임에서 실행된다.
    • 프레임은 디버깅을 위한 관리 성격의 정보와, 하나의 코드 블록이 실행 완료되었을 때 다음 실행이 어떻게, 언제 재개되어야 하는지를 결정한다.

Naming and Binding

name 은 객체를 참조한다. 그리고 name은 name binding operation에 의해 생성된다. 아래 목록은 name 생성에 관여하는 녀석들이다.

  • 함수의 매개변수
  • 클래스 정의
  • 함수 정의
  • 할당 표현식
  • 할당을 발생시키는 식별자인 target
    • for 반복문의 헤더
    • with 문, except 절, except*
  • import 선언문
  • type 선언문
  • 타입 인수 목록

예를 들어 from ... import * 와 같은 import 문은 참조된 모듈에 존재하는 모든 name들을 바인딩한다. (_로 시작하는 녀석들은 제외)

  • 할당문이나 import 선언문은 클래스 또는 함수 정의에 의해 묶이는 블록 내 또는 모듈 수준에서 발생한다.
  • nonlocal 또는 global로 선언되지 않은 블록 내에서 바운드되는 name은 블록 내 로컬 변수가 된다.

Resolution of names

스코프는 블록 내 name에 대한 가시성을 정의한다. 만약 로컬 변수가 블록 안에서 정의되었다면 해당 변수의 스코프는 그 블록이 된다.

코드 블록 내에서 name이 사용되는 경우 가장 가까운 둘러싸는 스코프를 사용하여 name을 확인한다. 코드 블록에 표시되는 이런 모든 범위의 집합을 블록의 환경이라고 부른다.

  • name을 전혀 찾을 수 없는 경우 NameError 에러가 발생한다.
  • 현재 스코프가 함수 스코프이고 name이 사용된 지점에서 아직 값에 바인딩되지 않은 지역 변수를 참조하는 경우 UnboundLocalError 예외가 발생한다.
    - UnboundLocalErrorNameError의 서브 클래스이다.

global 문이 블록 내에서 발생하는 경우 global 문에 지정된 이름의 모든 사용은 최상위 네임스페이스에 있는 해당 이름의 바인딩을 참조한다.

  • 최상위 네임스페이스에서 name은
    - 코드 블록을 포함하는 모듈의 네임스페이스인 전역 네임스페이스와 (우선적으로 검색한다)
    - 모듈 내장 네임스페이스인 빌트인 네임스페이스를 모두 검색해서 확인한다.
  • 여기서도 이름을 찾지 못하는 경우에는 전역 네임스페이스에 새 변수가 생성된다.

끝은 창대하리라

TL; DR

  • 파이썬 프로젝트를 맨 처음 만들 때에 고려해야 할 여러 사항들이 있다.
  • 모듈 이름은 되도록 짧게, 가능한 한 단어로 짓는 것이 좋다.
  • 패키지를 사용하는 방식은 모듈을 사용하는 방식과 매우 유사하게 동작한다.
    • 패키지 내 __init__.py 파일은 되도록 빈 파일로 두는 것이 좋다.
  • 파이썬은 객체 지향 프로그래밍을 강요하지 않는다. 프로젝트의 목적성에 알맞게 패러다임을 잘 구성해야 한다.
  • 데코레이터, 컨텍스트 매니저, 타입을 잘 활용한다면 읽기 쉬우면서도 생산성 높은 개발을 하는데 도움이 된다.
profile
make maketh install

1개의 댓글

comment-user-thumbnail
2024년 6월 3일

정리를 엄청 잘 해 주셨네요. 도움 많이 받고갑니다.

답글 달기

관련 채용 정보