[Python] Python Clean code - 파이썬스러운 코드

Sean park·2021년 5월 5일
0
post-thumbnail

이 글은 책 '파이썬 클린코드'를 읽고 일부를 정리한 내용입니다.
Python 코드를 작성할때 도움이 되기를 바랍니다.

2장 - 파이썬스러운 코드

1. 파이썬스러운 코드?

각 언어별로 지원하는 고유한 관용구를 활용하여 작성하는 관용적 코드

Ex) 파이썬에는 리스트 컴프리헨션등이 대표적인 관용구

arr = [1, 2, 3, 4] 

# 파이썬스럽지 않은 코드 
result = [] 
for i in range(len(arr)): 
	if arr[i] % 2 == 1: 
		result.append(arr[i]) 

# 파이썬스러운 코드
result = [num for num in arr if num % 2]

2. 인덱스와 슬라이스

파이썬은 다른 언어와는 다르게 배열의 인덱스 접근이나 슬라이스에 편리한 기능을 제공

마이너스 인덱스(-1)를 이용해 배열의 조작이 용이함

list = [1, 2, 3, 4, 5]
copy = list[:-1] # [1,2,3,4]
copy2 = list[2:-1] # [3,4]

파이썬은 위의 코드와 같이 마이너스 인덱스를 이용해 배열을 슬라이스 할 수있다.

** 파이썬에서 배열을 슬라이스 할 경우, 마지막 인덱스의 앞자리 인덱스까지 복사되니 유의!

3. 자체 시퀀스 생성

파이썬의 인덱스 접근, 슬라이스 기능은 파이썬에서 제공하는 getitem 이라는 매직 메서드에 의해 동작한다

** 매직 메서드 : 언어에서 내부적으로 제공하는 기본 메서드

사용자가 설계하는 클래스에서 인덱스 혹은 슬라이스와 같은 기능을 넣고싶다면 getitem 메서드를 정의해주면 된다.

class Items:
	def __init__(self, *values):
		self._values = list(values)

	def __len__(self):
		return len(self._values)

	def __getitem__(self, item):
		return self._values.__getitem__(item)

# someItems = Items(1,2,3)
# someItems[2] -> 3

위와 같이 코드를 작성해주면 우리가 일반적으로 알고있는 파이썬의 list와 같은 기능을 손쉽게 사용할 수 있다.

하지만 위와같이 자체 시퀀스를 생성할 경우 두가지 유의사항이 있다.

  1. 범위로 인덱싱 하는 결과는 해당 클래스와 같은 타입의 인스턴스여야 한다.
    • 일정 범위로 슬라이스 했을때, list에서 슬라이스를 했다면 반환값도 list 타입
  2. slice에 의해 제공된 범위는 파이썬이 하는 것처럼 마지막 요소는 제외해야 한다.

4. 컨텍스트 관리자

컨텍스트 관리자는 시작과 끝에 일정한 동작이 실행되어야 할 경우 사용된다.

컨텍스트 관리자는 enter 구문과 exit 구문으로 구성된다.

with 문을 이용해서 컨텍스트 관리자로 진입하게 한다.

with open(filename) as fd:
	process_file(fd)

위의 코드를 살펴보면 with문이 실행되면 enter문이 실행되고, enter 문이 무엇을 반환하든 as 다음에 있는 변수에 할당한다.

해당 블록(with의 마지막 문장이 끝나면 컨텍스트가 종료되며, 처음 호출한 컨텍스트 관리자 객체의 exit 메서드를 호출한다.

def stop_db():
	''' stop db '''

def start_db():
	''' start db '''

class DBHandler:
	def __enter__(self):
		start_db()
		return self

	def __exit__(self, exc_type, ex_value, ex_traceback):
		stop_db()

def db_backup():
	'''db bakcup'''

def main():
	with DBHandler():
		db_backup()

exit문의 파라미터는 블록에서 발생한 예외를 파라미터로 받으며, 예외가 발생하지 않은 경우 모두 None값을 받게된다.

5. 파이썬에서의 밑줄

파이썬에서는 일부 속성이나 메서드를 실제로 private으로 지정하는 기능은 지원하지 않는다.

파이썬은 밑줄(_)을 이용하여 외부에서 접근하여 사용하지 말라는 명시적 표현을 사용한다. 밑줄이 붙은 변수는 실제로 외부에서 접근할 수 있으나, 이는 파이썬의 원칙에 위배된다.

객체는 외부 호출 객체와 관련된 속성과 메서드만 노출해야한다. 객체의 인터페이스로 공개하는 요도가 아니라면 모든 멤버는 하나의 밑줄을 사용하는 것이 좋다.

** 이중 밑줄(__)

이중 밑줄은 '네임 맹글링'이라고 하며, 이름 맹글링은 다음과 같은 속성을 만든다"___"

class Foo: 
	def __init__(self): 
		self.__bar = 42

위와 같은 형식으로 코드를 작성하면 네임 맹글링이 발생하며, __bar를 호출하면 AttributeError가 발생한다.

위와 같은 형식의 속성은 _Foo__bar로 호출할 수 있으나. private속성과는 다른 개념이다.

네임 맹글링은 여러 번 확장되는 클래스의 메서드를 이름 충돌 없이 오버라이드 하기 위해 만들어졌다.

네임 맹글링을 언제 어디서 사용하는지는 아직 잘 모르겠다...

6. 프로퍼티

프로퍼티는 자바에서의 getter-setter와 같이 접근 메서드를 사용하여, 사용자가 등록한 정보에 잘못된 정보가 입력되지 않게 보호한다. 파이썬에서는 프로퍼티를 이용하여 이와 같은 기능을 구현할 수 있다.

import re

EMAIL_FORMAT = re.compile(r"[^@]+@[^@]+[^@]+")

class User:
	def __init__(self, username):
	  self.username = username 
		self._email = None
	
	@property
	def email(self):
		return self._email

	@email.setter
	def email(self, new_email):
		if not self._is_valid_email(new_email):
			print("유효한 이메일이 아닙니다.")
			return
		self._email = new_email

	def _is_valid_email(self, email):
		return re.match(self.EMAIL_FORMAT, email) is not None

email이라는 객체를 프로퍼티를 이용해서 접근을 제어한 코드이다.

email은 밑줄(_)을 이용해 외부에서 접근을 하지 않을것을 명시하였다. 위 코드에서 email의 값을 받을수 있는 email 함수를 만들고 그 위에 @property 데코레이터를 붙여주었다.

그리고 email의 값을 설정하기 위해서는 <property_name>.setter를 데코레이터로 붙여주었다.

위와 프로퍼티를 이용하여 접근을 제어하면 훨씬 파이썬스러운 코드를 작성할 수 있다.

>>> user = User("kim")
>>> user.email = "kim" 유효한 이메일이 아닙니다.
>>> user.email None
>>> user.email = "kim@naver.com"
>>> user.email kim@naver.com

**메서드는 하나의 동작만 실행해야 한다. 값을 할당하거나, 값을 리턴받거나 둘 중 하나의 동작만 실행해야 하며, 두가지 기능이 모두 필요할 경우 각각의 메서드로 분리해야한다.

7. 이터러블 객체

파이썬이 이터러블 객체를 판단할때 두 가지 조건을 차례로 검사한다.

  1. 객체가 nextiter 이터레이터 메서드 중 하나를 포함하는지 여부
  2. 객체가 시퀀스이고 lengetitem를 모두 가졌는지 여부

위 두가지 조건중 한가지를 만족하다면 반복 가능한 객체로 판단되어 for in 문에서 사용이 가능하다.

class DateRangeIterable:
	def __init__(self, start_date, end_date):
		self.start_date = start_date
		self.end_date = end_date
		self._present_day = start_date

	def __iter__(self):
		return self

	def __next__(self):
		if self._present_day >= self.end_date:
				raise StopIteration
		today = self._present_day
		self._present_day += timedelta(days=1)
		return today

for in 문을 호출하면 반복하려는 객체의 iter 메서드를 호출한다. 위의 코드에서 iter메서드는 자기 self를 반환하여 자기 자신이 이터러블 객체임을 알린다. 이후 next를 반복적으로 호출하여 객체를 반복하고 StopIteration 예외를 발생하면 반복을 중단한다.

  • 시퀀스 만들기

객체에 iter메서드가 정의되어 있지 않으면 getitem을 호출한다. 시퀀스는 lengetitem을 구현한다.

class DateRangeSequence:
	def __init__(self, start_date, end_date):
		self.start_date = start_date
		self.end_date = end_date
		self._range = self._create_range() #메모리와 반복될 값을 초기화
	
	def _create_range(self):
		days = []
		current_day = self.start_date
		while current_day < self.end_date:
			days.append(current_day)
			current_day += timedelta(days=1)
		return days

		def __getitem__(self, day_no):
			return self._range[day_no]

		def __len__(self):
			return len(self._range)

시퀀스는 이터러블 객체와 달리 반복할 값을 미리 만들어두고 반복하여 꺼내 사용하므로 메모리는 더 많이 사용하지만, 시간복잡도가 더 낮다는 장단점이 있다

8. 컨테이너 객체

컨테이너 객체는 contains 메서드를 구현한 객체로, 일반적으로 Boolean값을 반환하며 파이썬에서 in 키워드가 발견될 때 호출된다.

element in container
container.__contains__(element)

위 두 줄의 코드는 서로 같은 코드이다.

class Boundaries:
	def __init__(self, width, height):
		self.width = width
		self.height = height

	def __contains__(self,coord):
		x, y = coord
		return 0 <= x < self.width and 0 <= y < self.height

class Grid:
	def __init__(self, width, height):
		self.width = width
		self.height = height
		self.limits = Boundaries(width, height)

	def __contains__(self, coord):
		return coord in self.limits

if coord in grid:...

위 예제에서 coord값은 grid에 포함되는지를 검사하고, grid는 검사의 범위를 Boundaries로 부터 확인한다.

9. 객체의 동적인 속성

.를 호출하면 파이썬은 객체의 사전(dict)에서 를 찾아 getattribute를 호출한다. 객체에 찾고있는 속성이 없다면 의 이름을 파라미터로 getattr 이라는 메서드를 호출한다.

class MyClass:
	def __init__(self, attribute):
		self.attribute = attribute

	def __getattr__(self, attribute):
		if attribute.startswith("fallback_"):
			name = attribute.replace("fallback_", "")
			return f"[fallback resolved] {name}"
		raise AttributeError

>>> myclass = MyClass("value")
>>> myclass.attribute
'value'

>>> myclass.fallback_test
'[fallback resolved] test'

>>> myclass.__dict__["fallback_new"] = "new value"
>>> myclass.fallback_new
'new value'

위의 코드를 보면 myclass에는 없지만 'fallback_'으로 시작하는 를 호출시 AttributeError를 발생시키지 않는것을 확인할 수 있다. 해당 가 없지만 getattr에서 이를 파라미터로 받아 처리해주고 있다.

10. 호출형 객체

호출형 객체는 call 메서드를 구현하고 있는 객체로, 클래스도 함수와 같이 호출형으로 사용할 수 있게된다.

from collections import defaultdict

class CallCount:
	def __init__(self):
		self._counts = defaultdict(int)

	def __call__(self, argument):
		self._counts[argument] += 1 
		return self._counts[argument]

>>> cc = CallCount()
>>> cc(1) 1
>>> cc(2) 1
>>> cc(1) 2
>>> cc(1) 3

위 코드를 보면 CallCount의 인스턴스인 cc가 호출될때 마다 call이 실행되는 것을 알 수 있다. call은 실행될때 받은 파라미터를 그대로 사용 가능하다.

** collections.defaultdict는 dictionary를 초기화 해줄때 사용하며, 객체가 호출될때 해당 키값이 없다면, 초기값으로 defaultdict에 인자로 넣어준 함수의 리턴값을 초기값으로 설정해준다. (int()의 리턴값은 0)

11. 변경 가능한 파라미터의 기본 값

변경 가능한 객체(list, dict)를 기본인자로 사용하면 문제가 발생할 수 있다.

def wrong(user_data: dict = {"name": "John", "age": 30}):
	name = user_data.pop("name")
	age = user_data.pop("age")
	return f'{name} ({age})'

>>> wrong()
'John (30)'
>>> wrong({"name": "Jane", "age": 25})
'Jane (25)'
>>> wrong()
KeyError: 'name'....

기본 값을 사용해 함수를 호출하면 기본 데이터로 사용될 사전을 한번만 생성한다. 즉 최초 호출시에 기본값을 사용했다면 이 key는 지워지기 때문에 다시 사용할 수 없다.

이를 방지하기 위해서는 초기 값으로 None을 설정하고, 함수 본문에서 user_data의 기본값을 할당하면 된다.

내장 타입 확장

파이썬의 내장 list, dict 등을 확장 또는 수정하기 위해서는 해당 타입(list,dict등)을 상속받아 직업 오버라이드 하여구현하는것이 아닌 collections 모듈의 UserList등을 상속받아 구현하여야 한다.

class BadList(list):
	def __getitem__(self, index):
		value = super().__getitem__(index)
		if index % 2 == 0:
			prefix = "짝수"
		else:
			prefix = "홀수"
		return f'[{prefix}] {value}'

위의 코드는 언뜻보면 잘 작동하는것 같지만 결국 list이기 때문에 list의 getitem과 같이 작동하지는 않게된다.


여기까지 책 '파이썬 클린코드' 2장 파이썬스러운 코드의 내용에 대해 정리해 보았습니다.

profile
제 코드가 세상에 보탬이 되면 좋겠습니다.

0개의 댓글