파이썬스러운 코드

Sawol·2021년 6월 6일
1

I Need Python

목록 보기
11/13
post-thumbnail

❗️ 파이썬 클린코드를 읽고 정리한 글입니다.

파이써닉(Pythonic)

아래에 정리된 파이썬의 기능을 잘 활용하면 파이썬스러운 코드를 작성할 수 있다.

인덱스와 슬라이스

인덱스와 슬라이스를 적절히 잘 사용하면 그 코드를 파이써닉하다 라고 한다.
아래의 예제는 모두 리스트로 작성되었지만 문자열, 튜플, 리스트 모두 가능하다.

마지막 값 선택하기

arr = [1,2,3,4]

# bad
last_item = arr[len(arr)-1]		# 4

# good
last_item = arr[-1]			# 4

복사하기

arr1 = [1,2,3,4]

# bad
arr2 = [1,2,3,4]

# good
arr3 = arr1[::]

특정 간격만큼 값 선택하기

arr = [1,2,3,4,5,6,7]

# bad
arr1 = []
for i in range(0,len(arr),3):
    arr1.append(arr[i])
    
# good
arr1 = arr[::3]

__getitem__ 메서드

앞서 보았던 인덱스, 슬라이스 기능은 __getitem__ 이라는 매직 메서드 덕분에 동작한다. myobject[key] 형태를 사용할 때 호출되는 메서드인데, 대괄호 안의 key 값을 파라미터로 전달한다. 즉, 내가 만든 클래스에 인덱스, 슬라이스 기능을 넣고 싶으면 __getitem____len__ 메서드를 모두 구현하면 된다.

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

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

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

test = Items(1,2,3,4)
print(len(test))		# 4
print(test[0])			# 1

컨텍스트 관리자(context manager)

어떤 코드의 시작과 끝에 특정 동작을 하도록 할 때 사용되는 기능이다. 예를 들면, 파일을 열면(시작) 닫아야하고(끝), 데이터베이스를 백업하기 위해 멈추면(시작) 실행시켜야한다(끝).

파일을 열고 닫음

파일을 열고 닫을 때 컨텍스트 관리자 사용 유무에 따라 차이는 아래와 같다.

# bad
f = open(filename)
"""
파일 관련된 동작 코드
ex- 읽기/쓰기/삭제
"""
f.close()

# good
with open(filename) as f:
    """
    파일 관련된 동작 코드
    ex- 읽기/쓰기/삭제
    """

# bad를 보면 파일을 여닫는 것을 직접 작성해야한다. 그래서 파일을 열기만 하고 닫는 동작의 코드를 작성하는 것을 잊을 수 있다. 이러한 위험성 때문에 # good 과 같이 with 문을 사용한다.
with 문은 컨텍스트 관리자로 진입하게 하는 데, 컨텍스트 관리자는 __enter__, __exit__ 메소드를 가진다.

  • __enter__
    with 문이 실행되면 자동으로 이 메서드가 호출된다.
    특정 동작을 수행하는 함수를 호출한다.
  • __exit__
    __enter__ 의 마지막 코드의 실행이 끝나면 자동으로 이 메서드를 호출한다.
    __enter__ 에서 예외나 오류가 발생해도 __exit__ 는 항상 실행된다.

데이터베이스 백업

데이터베이스를 백업을 하려면 데이터베이스를 멈춘 뒤에 해야한다. 백업이 실패 또는 성공을 해도 항상 데이터베이스는 실행을 시켜야한다. 그렇기때문에 컨텍스트 관리자로 구현을 하는 것이 파이썬스러운 코드이다.

def stop_database():
    """
    데이터베이스 멈추는 코드 
    """
def start_database():
    """
    데이터베이스 실행하는 코드
    """
    
class DBHandler:
    def __enter__(self):
        stop_database()
        return self
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        start_database()
                
def db_backup():
    """
    데이터베이스 백업하는 코드
    """
    
def main():
    with DBHandler():
        db_backup()

좀 더 간결한 컨텍스트 관리자 구현

앞서 살펴본 구현보다 contextlib 이라는 표준 모듈을 사용하면 좀 더 간결한 구현이 가능하다.

  • @contextlib.contextmanager
    함수에 @contextlib.contextmanager 데코레이터를 적용하면 해당 함수의 코드를 컨텍스트 관리자로 변환한다. 여기서 함수는 제너레이터 형태여야한다.
    이렇게 작성하는 이유는 기존 함수를 리팩토링하기 쉽고, 다른 클래스와 독립되어 잇는 컨텍스트 관리자 함수를 만들 수 있다.
import contextlib

@contextlib.contextmanger
def db_handler():
    stop_database()		# __enter__
    yield			# db_backup() 실행
    start_database()		# __enter__
    
with db_handler():
    db_backup()
  • contextlib.ContextDecorator
    컨텍스트 관리자 안에서 실행될 함수에 데코레이터를 적용하기 위한 로직을 제공한다. 컨텍스트 관리자 자체의 로직은 앞서 언급한 매직 메서드를 구현하여 제공하여야 한다.
    with 문 없이 그저 함수를 호출하기만 하면 자동으로 컨텍스트 관리자 안에서 함수가 실행된다. 다만, 컨텍스트 관리자 내부에서 사용하고자 하는 객체를 얻을 수 없다.
class dbhandler_decorator(contextlib.ContextDecorator):
    def __enter__(self):
        stop_database()
        
    def __exit__(self, exc_type, exc_val, exc_tb):
        start_database()
        
@dbhandler_decorator     
def offline_backup():
    """
    데이터베이스 백업하는 코드
    """

프로퍼티

프로퍼티란 객체의 어떤 속성에 대한 접근을 제어하려는 경우 사용된다. 다른 언어에서는 흔히 public, private, protected 라는 프로퍼티를 가지는데, 파이썬은 모든 객체의 프로퍼티는 public이다.
개발을 하다보면 외부의 사용자(호출자)가 내부 객체의 속성을 호출하지 못하도록 해야할 때가 있다. 이를 강제하는 방법은 없지만, "이 속성은 호출 하지 않았으면 좋겠어요"라는 의미를 주는 몇 가지 규칙이 있다.

파이썬에서의 밑줄( _ )

파이썬에서 객체 앞에 _ 이 있으면 호출자는 해당 속성을 호출하면 안 된다. 물론, 호출해도 오류를 반환하거나 호출이 되지않는 등의 문제는 없이 정상적으로 호출이 된다. 하지만 객체가 외부로 공개하는 용도가 아니면 모든 멤버에는 접두사로 하나의 밑줄을 사용하는 것이 좋다.

# bad
class Connector:
    def __init__(self, sourse):
        self.sourse = sourse
        self.timeout = 60
        
conn = Connector("mysql://localhost")
print(conn.source)		# "mysql://localhost"
conn.timeout = 30
print(conn.timeout)		# 30

# good
class Connector:
    def __init__(self, sourse):
        self.sourse = sourse
        self._timeout = 60
        
conn = Connector("mysql://localhost")
print(conn.source)		# "mysql://localhost"
conn._timeout = 30
print(conn._timeout)		# 30

파이썬에서의 이중 밑줄(__)
파이썬에서의 이중 밑줄은 네이밍 맹글링이라고 부르는데 이는 프로퍼티와는 전혀 별개의 기능이다. 이중 밑줄이 private와 같은 효과라고 생각하는 것은 잘못된 생각이다.

private 속성을 프로퍼티로 조작하기

파이썬에서는 _ 은 "이 속성이 private로 사용될 것입니다."라는 뜻이라고 했다. 하지만 이러한 private 속성을 외부 사용자가 사용할 수 있게 해줘야 할 때가 있다. 예를 들면 사용자에게 어떠한 입력 값을 내부의 어떠한 속성에 담는데 이 속성은 내부 함수가 돌아갈 때 이 값을 보호하기 위해서 private로 만들었다고 하자. 이 경우에는 private 속성에 사용자가 값을 입력하기위해 접근해야한다. 이때 사용하는 것이 프로퍼티로 자바에서 게터(getter)와 세터(setter)의 역할과 동일하다. 게터는 현재의 private 속성 값을 가져오는 역할, 세터는 private 속성 값을 변경하는 역할을 한다.

  • 간단한 프로퍼티 구현
# bad
class Person:
    def __init__(self):
        self._age = 0
 
    def get_age(self):           # getter
        return self._age
    
    def set_age(self, value):    # setter
        self._age = value
 
james = Person()
james.set_age(20)
print(james.get_age())		# 20

# good
class Person:
    def __init__(self):
        self._age = 0
 
    @property
    def age(self):           # getter
        return self._age
 
    @age.setter
    def age(self, value):    # setter
        self._age = value
 
james = Person()
james.age = 20      # 인스턴스.속성 형식으로 접근하여 값 저장
print(james.age)    # 20
  • 프로퍼티를 이용한 이메일 확인
    아래는 사용자가 입력한 이메일이 이메일 형식에 맞는 지 확인하는 코드이다. 확인만 하는 코드이니 사용자의 이메일이 담긴 코드는 변경 되어서는 안 되어 private 속성에 값을 담는다.
import re

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

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

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 is_valid_email(new_email):
        print("유효한 이메일이 아닙니다.")
        return
    self._email = new_email

사용자가 이메일을 추가하려고 User.email=사용자의 이메일를 입력하면 @email.setter 가 실행되어 유효성 검사를 진행한다. 만약 사용자가 이메일 값을 확인하고 싶어 print(User.email)를 실행하면 @property가 실행되어 현재 값을 반환해준다.

deleter
게터와 세터 외에도 디리터도 존재하는데 자세한 내용은 공식 문서에 담겨있다.
이름에서 예상할 수 있듯이 디리터는 값을 삭제하는 것이다.

이터러블 객체

반복 가능한 객체를 이터러블 객체라고 한다. 여기서 반복 가능하다는 의미는 for문을 사용해 루프를 돌며 반복적으로 값을 가져올 수 있는 것을 의미한다. 이터러블 객체가 값을 하나씩 들고 올 수 있는 것은 이터레이터 덕분이다. 이터러블 객체는 __iter__라는 메서드를 가져야하고, 이터레이터는 __next__라는 메서드를 가져야한다.
파이썬에서는 리스트, 튜블, 세트, 사전이 이터러블 객체로 for문을 사용 가능한데, 개발자가 만든 객체를 for문에서 사용할 수 있는 이터러블 객체로 만들고 싶다면 매직 메서드를 구현하면 된다.

이터러블 객체 만들기

파이썬은 객체를 반복하려고 하면 가장 먼저 iter() 함수를 호출한다. 이 함수가 처음으로 하는 것은 객체에 __iter__ 라는 메서드가 있는지 확인하는 것이다. 메서드가 존재하면 해당 메서드를 실행한다.

from datetime import timedelta, date

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 day in DateRangeIterable(date(2019, 1, 1), date(2019, 1, 5)):
        print(day)
...        
2019-01-01
2019-01-02
2019-01-03
2019-01-04

DateRangeIterable이 실행되니 __init__ 함수가 실행되고, 반복하기 위해 iter() 함수를 실행시킨다. 그럼 __iter__이 호출되는데 이 메서드는 self를 반환하여 자기 자신이 이터러블 객체라고 알린다. 따라서 next() 함수가 호출되고 __next__ 메서드가 요소를 하나씩 반환한다. 더이상 요소가 없으면 StopIteration 예외를 발생시켜 파이썬에게 알려준다.

시퀸스 만들기

객체에 __iter__ 함수가 없어도 반복할 수 있다. iter() 함수는 객체에 __iter__ 함수가 없으면 __getitem__을 찾고, 이 또한 없으면 그때서야 TypeError를 발생시킨다. 즉, __getitem__가 있으면 반복을 할 수 있다.
이를 시퀸스라고 하는데, 시퀸스는 __getitem__, __len__ 이 구현되어 있어야한다.

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)
        
>>> for day in DateRangeSequence(date(2019, 1, 1), date(2019, 1, 5)):
       print(day)
...
2019-01-01
2019-01-02
2019-01-03
2019-01-04

코드를 보면 알 수 있지만 __init__ 함수가 바로 _create_range를 호출하여 모든 요소를 생성한다. 그 후, iter() 함수가 __getitem__을 실행하여 요소를 하나씩 가져온다.
즉, 시퀸스는 이미 모든 요소 값을 생성한 후 하나씩 가져오고, 앞서 봤던 이터러블은 요소 하나씩 생성 및 반환한다. 이러한 특징때문에 시퀸스는 메모리 공간을 많이 차지하지만 인덱싱에서는 O(1)의 시간 복잡도를 가지고 이터러블은 메모리 공간을 적게 차지하지만 인덱싱에서는 모든 요소를 돌아하므로 O(n) 시간 복잡도를 가진다.

컨테이너 객체

파이썬 문법에서 in 을 사용하면 호출되는 객체를 말한다. __contains__ 메서드를 이용해 구현하는데 보통 불리언 값을 리턴한다. __contains__ 메서드를 잘 사용하면 가독성이 높아지므로 파이써닉한 코드를 작성할 수 있게 된다.

element in container 
# 파이썬은 위 코드를 다음과 같이 해석함
container.__contains__(element)

컨테이너 객체의 예

아래 예시를 보면 컨테이너 객체를 사용하면 왜 파이썬스러운 코드가 되는지 확인할 수 있다.
알고리즘 문제에서도 자주 출제되는 2차원 게임 지도에서 특정 위치에 표시를 하는 코드이다.

# bad  --> 가독성이 떨어짐
def mark_coordinate(grid, coord):
    if 0 <= coord.x < grid.width and 0 <= coord.y < grid.height:
        grid[coord] = MARKED
   
   
# good  --> 가독성이 높아짐
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

이렇게 하면 지도에게 특정 좌표가 포함되어 있는지만 물어보면 된다.

grid = Grid(x,y)
def mark_coordinate(grid, coord):
    if coord in grid:			# 좌표 coord가 지도에 포함되는지
        grid[coord] = MARKED

0개의 댓글