Python/파이썬클린코드

[파이썬 클린코드] Chapter3 : 좋은 코드의 일반적인 특징

공부하는 sum 2023. 3. 12. 23:30
728x90

http://www.yes24.com/Product/Goods/114667254

파이썬 클린코드 2nd edition 을 읽고 정리한 내용입니다.


계약에 의한 디자인

 

계약이란, 통신 중에 지켜져야 할 규칙을 문서화 하는 것을 의미한다. 계약에서는 주로 사전조건과 사후 조건을 명시하며, 때로는 불변식이나 부작용을 기술하기도 한다. 즉, 계약을 통해서 각 컴포넌트들이 지켜야 할 부분을 명시하고 그 것을 기반으로 개발하는 것을 의미한다. 
이렇게 하면 여러가지 장점들이 있는데 

  • 검증에 실패한 경우 오류를 쉽게 찾아서 수정 가능
  • 사전조건의 경우 클라이언트, 사후조건은 컴포넌트 등 서로 연관되어 있는 부분이 다르기 때문에 책임소재를 신속하게 파악 가능함
  • 사전조건의 경우 런타임 중에 조건이 맞는지 확인할 수 있기 때문에 조건이 맞지 않는다면 실행하지 않아야 함

등을 들 수 있다.
사전조건
함수나 메서드가 제대로 동작하기 위해서 보장해야 하는 모든 것, 예를 들어 데이터가 적절한 형태여야 한다는 것 등을 말한다. 이는 타입 체킹과는 다른 관점으로, 필요로 하는 값이 정확한지에 대한 확인을 의미한다. 
사후조건

함수 반환값의 유효성 검사. 만약 함수나 메서드가 적절하게 호출되었다면, 사후 조건은 특정 조건이 보존되어야 한다. 
 

방어적 프로그래밍

문제가 생길 것으로 예상되는 부분을 미리 예상하고 관련 오류가 발생하지 않도록 프로그래밍 하는 것을 말한다. 
주요 주제

  • 시나리오 상의 오류를 처리 → 에러 핸들링 프로시저
  • 발생하지 않아야 하는 오류에 대한 처리  → 어설션 assertion

에러 핸들링

에러 핸들링은 예상되는 에러에 대해 계속 실행할지 또는 프로그램을 중단할지를 결정하는 것을 주요 목적으로 한다. 에러 핸들링의 방법으로는 값 대체, 에러 로깅, 예외 처리 등을 들 수 있다. 


값 대체
잘못된 결과를 정합성(모순 없이 일치된 상태)을 깨지 않는 다른 값으로 대체하는 것을 의미한다. 항상 가능한 방법은 아니며, 프로그램의 견고함과 정확성을 트레이드 오프하는 방법이다. 그렇기 때문에 오류가 있는 값을 유사한 값으로 대체하는 것 등을 하는 경우 일부 오류를 숨겨버릴 수 있기 때문에 이러한 부분을 고려해야 한다. 
에러 로깅
따로 수행할 만한 다른 작업이 없을 경우에 에러 로그를 남기게 된다. 
예외 처리
예외가 생길 수 있는 상황을 명확하게 정의하고, 원래의 로직에 따라서 흐름을 유지하는 것이 예외 매커니즘에서 중요한 부분이다. 단순히 go-to문(특정 위치로 건너뛰도록 함) 처럼 예외를 처리해서는 안되고, 함수 내에서 너무 많은 예외가 발생하는 경우 하위 기능으로 나누는 것 등을 고려해봐야 한다. 

  • 올바른 수준의 추상화 단계에서 예외 처리
    • 예외는 오직 한 가지 일을 한느 함수의 한 부분이어야 함
  • 엔드 유저에게 Traceback 노출 금지
    • 보안을 위한 고려사항 중 하나
    • 예외가 발생한 경우 로그를 남기는 것은 중요하나 사용자에게 보여서는 안됨 
  • 비어있는 except 블록 지양
    • except - pass로 하는 경우 예외가 발생해도 그냥 지나가지만 그렇기 때문에 아무 정보도 얻을 수 없어 지양해야 함
    • Exception과 같은 광범위한 예외 대신 조금 더 세분화된 구체적인 예외를 지정하거나
      except에서 실제 오류에 대한 처리를 하는 등의 방식을 이용해야 함
    • 굳이 오류를 무시해야 한다면 밑과 같이 contextlb.suppress 함수를 사용해서 해당 오류를 무시하는 것이 올바른 방법
import contextlib

with contextlib.suppress(KeyError):
	process_data()
  • 원본 예외 포함
    • 처리 과정에서 기존 오류와 다른 오류를 발생 시키고 오류 메세지를 변경할 수도 있는데, 원래 오류가 뭔지에 대한 정보를 포함하는 것이 좋다

파이썬에서 어설션(Assertion) 사용하기

어설션이 있는 지점에서 반드시 참이어야 하는 사항을 표현한 논리식이다. 잘못된 시나리오에 의해서 프로그램이 더 큰 피해를 입지 않도록 중단시키는 행동을 한다. 그렇기 때문에 assertion문의 결과는 항상 참이어야 하고, 만약 거짓이 되어 AssertionError가 발생한 경우에는 프로그램에 문제가 있다는 것을 의미한다. 

# 어설션에서 함수를 직접 호출하지 말고, 로컬 변수에 저장하여 비교하는 방식을 사용
result = condition.holds()
assert result > 0, f"Error with {result}"
  • Assertion과 Exception의 차이
    • Exception : 예상하지 못한 상황의 처리
    • Assertion : 정확성의 보장을 위한 자가 체크

 

관심사의 분리

파이썬 모듈, 패키지를 포함한 모든 소프트웨어 컴포넌트에 대해 적용되는 내용 

응집력(cohesion)과 결합력(coupling)

  • 응집력
    • 객체가 작고 잘 정의된 목적을 가지면서, 가능하면 작아야 한다는 것을 의미
    • 객체의 응집력이 높을수록 더 유용하고 재사용성이 높아짐
  • 결합력
    • 두개 이상의 객체가 어떻게 의존하는지에 대한 내용.
    • 로 너무 의존하는 경우 재사용성이 낮아지고, 파급효과가 생기고, 낮은 수준의 추상화 문제가 있음.

잘 정의된 소프트웨어의 경우에는 높은 응집력과 낮은 결합력을 가진다. 

개발지침 약어

DRY / OAOO
Do not Repeat Yourself / Once and Only Once

코드에 있는 지식은 단 한 번, 단 한 곳에 정의되어야 한다. 그렇지 않으면 다음과 같은 문제가 있을 수 있다. 

  • 오류가 발생하기 쉬움 : 하나를 수정하려고 할 때 여러 군데를 수정해야 하므로 오류의 발생 확률이 높아짐
  • 비용이 비쌈 : 변경하는데 시간이 훨씬 더 많이 걸리게 됨
  • 신뢰성이 떨어짐 : 모든 인스턴스의 위치를 기억해야 하므로 데이터의 완결성이 떨어짐

해결 방법

# 함수 생성 기법
def score_for_student(student) : 
	return student.passed *11 - student.failed*5 -student.years*2
    

def process_students_list(students) : 
	# ~중간 과정~
    students_ranking = sorted(students, key = score_for_student)
    #학생별 순위 출력
    for student in students_ranking:
    	# f-string 임의 추가
    	print(f"이름: {student.name}, 점수: {score_for_student(student)}")

 
YANGNI
You Ain't Gonna Need It

과잉 엔지니어링을 하지 않기 위해 계속 염두에 두어야 할 원칙으로, 개발자는 미래의 요구 사항을 예측해서 복잡한 코드를 만들기보다 현재의 요구사항을 잘 반영하여 유지보수가 가능한 개발을 해야한다. 이는 상세 코드 수준이 아니라 전체적인 소프트웨어 수준에서도 적용되는 아이디어이다. 

KIS
Keep It Simple

과잉 엔지니어링을 조심해야 하고, 선택한 솔루션이 문제에 적합한 최소한의 솔루션인지를 살펴봐야 한다. 디자인이 단순할수록 유지관리는 쉬워진다. 

EAFP / LBYL
Easier to Ask Forgiveness than Permission / Look Before You Leap

  • Easier to Ask Forgiveness than Permission 
    • 일단 코드를 실행하고, 실제로 동작하지 않을 경우에 대응 
    • 실행 시 발행한 예외를 catch, except블록에서 바로잡는 코드 실행
    • 예외 처리가 필요한 부분으로 바로 이동하기 때문에 가독성이 높음
  • Look Before You Leap
    • 실행하기 전에 무엇을 하려고 하는지 확인하는 방법
    • C언어 같은 경우 사용. 파이썬에서는 적용하지 않음

상속

상속을 해서 부모 클래스를 확장하는 경우 부모 클래스와 강하게 결합된 클래스가 생기는데, 결합력은 약할수록 좋기 때문에 상속을 사용할 때에는 이런 위험이 존재한다. 

상속이 좋은 경우

특정 클래스와 같은 기능을 하지만 일부의 기능을 수정하거나 새로운 것을 추가하고 싶은 경우

  • http.server패키지 내의 클래스들
  • 인터페이스 정의 
    • 어떤 객체에 인터페이스 방식을 강제하고자 할 때, 
      세부 구현을 하지 않은 추상 객체를 만들고 상속하는 하위 클래스에서 적절한 구현을 하는 방식 
  • 예외 
    • 모든 예외들은 Exception에서 상속받은 클래스들

파이썬의 다중상속

믹스인(mixin)
코드를 재사용하기 위해서 일반적인 행동을 캡슐화 해 놓은 부모 클래스. 다른 클래스와 믹스인 클래스를 다중상속하고, 믹스인 클래스의 메서드와 속성을 다른 클래스에서 활용한다. 믹스인을 사용하는 경우 새로운 코드가 필요 없게 되며, 일종의 데코레이터 역할을 하는 것이다. 

class BasicTokenizer : 
	def __init__(self,str_token):
    	self.str_token = str_token
    
    def __iter__(self):
    	yield from self.str_token.split("_")
        
        
# 기본 클래스의 값을 변경하지 않고 값을 대문자로 바꾸는 경우
# 변환 작업을 처리하는 계층 구조에 새로운 클래스를 만들어 mix
class UpperIterableMixin:
	def __iter__(self):
    	return map(str.upper,super().__iter__())
        
class Tokenizer(UpperIterableMixin, BaseTokenizer):
	pass
  • Tokenizer 작동 순서
    1. 믹스인에서 __iter__호출 
    2. __iter__는 super()를 호출하여 변환 진행
    3. BaseTokenizer에 전달

함수와 메서드의 인자

파이썬의 함수 인자 동작 방식

인자는 함수에 어떻게 복사되는가

  • 불변형 객체의 경우 : arg += <exp>인 경우, arg = arg + <exp>인데, 앞의 arg는 뒤의 arg와는 상관없는 함수 내의 로컬 변수가 된다. 
  • 변형 객체의 경우 : 동일한 작업을 수행하더라도 원래 리스트 객체에 대한 값을 수정하기 때문에 함수의 외부에서도 값을 수정할 수 있게 된다. 값 자체가 참조인 셈 (ex. 리스트의 append)

가변인자

  • 해당 인자를 패킹할 변수의 이름 앞에 별표(*)를 사용하기
  • 부분적인 언패킹 또한 가능
  • 이중별표(**)를 사용 시 사전의 키를 파라미터 이름으로 사용하고, 사전의 값을 파라미터 값으로 사용
    이 경우 임의의 키워드 인자를 허용한다는 것이고, 이 때 kwargs 인자라는 접근 가능한 사전을 만들게 됨.
    이렇게 사용하는 경우, 인자 값을 조회하는 것이 아니라 함수 정의 시에 언팩하여 기본 값을 설정하도록 하는 것이 좋음
  •  중요 : kwargs 를 직접 조작하지 말고, 함수의 서명에서 적절하게 언패킹하자
# 부분적인 언패킹
def show(e, rest):
	print(f'요소 : {e} - 나머지 : {rest}')
    
first, *rest = [1,2,3,4,5]
show(first,rest)
>>> 요소 : 1 - 나머지 : [2,3,4,5]

# 이중 별표 사용 예
frunction(**{'key':'value'}) # function(key = "value")와 동일


# kwargs 예시
# 안 좋은 예시
def function(**kwargs) : 
	timeout = kwargs.get("timeout", DEFAULT_TIMEOUT)
# 좋은 예시
def function(timeout = DEFAULT_TIMEOUT, **kwargs):

 

위치전용 인자

  • 위치 인자 : 함수의 앞 쪽에 제공되는 인자
  • func(x,y)인 함수에서 func(1,2)로 호출하면 자동으로 x = 1, y = 2로 전달되며, 
    func(y=2,x=1)로 호출해도 이는 마찬가지이지만 func(y=2,1)로 호출하면 오류가 발생한다. 
    하나의 인수를 키워드로 전달 시 나머지들도 모두 키워드로 전달해야 한다.
  • 값을 전달할 때 키워드를 사용하지 않고 위치 인자만 사용하게 하기 위해서는 
    func(x, y, /) 처럼 마지막에 /를 추가하면 된다. 
  • 이름을 사용하려고 하는 것이 비효율적인 경우에 사용하며, 그렇지 않은 경우에는 되도록 사용을 피해야 함.

키워드 전용 인자

  • 키워드 전용 인자는 *를 사용하면서 시작한다.
  • 함수 서명에서 * 뒤에 오는 것들은 키워드 전용 인자가 된다.
  • *args뒤에 오는 kw1,kw2가 키워드 전용 인자이다.
  • *args를 사용하면 임의의 개수만큼 위치 인자를 사용할 수 있고, *만 사용했을 경우에는 2개의 위치 인자만 사용할 수 있다.
  • 이러한 키워드 전용인자는 하위 호환을 유지하면서 기존 함수를 확장할 수 있기 때문에 유용하다.
    추가하는 파라미터를 키워드 전용 인자로 만들어서 명시적으로 새로운 정의를 사용함을 알려주는 것이 좋다.
    또한, 이렇게 사용하는 경우 문맥을 제공할 수 있어 무슨 일이 벌어지는지 알 수 있게된다.
def my_function(x, y, *args, kw1, kw2 = 0):
	print(f"{x=}, {y=}, {kw1=}, {kw2=}")

함수 인자의 개수

너무 많은 인자를 사용하는 함수나 메서드는 왜 나쁜 디자인일까?

함수 서명의 인수가 많을 수록 호출자 함수와 밀접하게 결합될 가능성이 커지게 된다

해결 방법

  • 구체화
    전달하는 모든 인자를 포함하는 새로운 객체를 생성
  • 동적 서명을 가진 함수를 만들기
    파이썬의 특정 기능을 사용하면 되긴하지만, 유지보수하기가 힘들기 때문에 함수의 본문을 잘 살펴 활용해야 한다. 
728x90