python

이펙티브 파이썬 2nd 정리 #3

기기디 2023. 8. 18. 22:54

21. 변수 영역과 클로저의 상호 작용 방식을 이해하라

파이썬은 클로저를 지원한다

  • 클로저 : 자신이 정의된 영역 외부의 변수를 참조하는 함수

파이선에서는 함수가 일급 시민 객체다

  • 일급시민 : 객체를 직접 가리킬 수 있고, 변수에 대입하거나 다른 함수에 인자로 전달할 수 있으며 식이나 if문에서 함수를 비교하거나 함수에서 반환하는 것 등이 가능하다는 것을 의미한다.

파이썬에는 시퀀스(튜플 포함)를 비교하는 규칙이 존재한다

  • 시퀀스를 비교할 때 각 시퀀스의 0번 인덱스를 비교합니다. 시작 부분의 요소가 같다면 다음 인덱스의 요소를 비교하고 요소가 같지 않다면 비교 결과를 반환합니다. 시퀀스의 값을 비교할 수 없는 경우 TypeError를 발생시킵니다.

파이썬에서 클로저를 사용할 때 변수 스코프가 어떻게 작동하는지 이해하는 것이 중요하다. 내부 함수는 외부 함수에서 정의된 변수에 접근할 수 있지만, 해당 변수가 nonlocal로 선언되지 않는 한 수정할 수 없습니다. 지역 또는 nonlocal 스코프에서 변수를 찾을 수 없으면 다음으로 전역 스코프, 내장 스코프의 순서대로 변수를 찾습니다.

클로저는 파라미터를 추가하거나 조건에 따라 특정 값을 반환하는 함수를 만드는 데 유용하게 사용할 수 있다. 또한 다른 함수의 동작을 수정하는 데코레이터로도 사용할 수 있다.

클로저를 사용할 때 의도하지 않은 동작이 발생할 수 있으므로 주의해야 한다.

  • 예를 들어 클로저가 외부 함수에서 nonlocal로 정의된 변수를 수정하는 경우 해당 변수는 외부 함수가 완료된 후에도 수정된 값이 유지되어 나중에 다른 함수에서 클로저를 호출하면 의도하지 않은 결과를 만들 수 있다.

nonlocal은 기본적으로 안티패턴으로 사용하지 않는걸 권장한다

  • 사용시 지정한 변수와 대입 실제 대입이 이루어지는 변수간 거리가 먼 경우 가독성이 떨어진다
  • 유지보수시 nonlocal변수에 대한 변경 사항이 중첩된 함수 안팎에서 모두 고려되어야 하기 때문에 복잡해진다
  • 함수가 호출될 때마다 nonlocal 변수의 상태가 변할 수 있으므로 함수의 동작이 항상 동일하다고 보장하기 어렵다

26. functools.wrap을 사용해 함수 데코레이터를 정의하라

데코레이터 함수는 다른 함수를 변경하거나 확장하는 데 사용한다. 데코레이터는 데코레이트된 함수의 인자를 변경하거나, 새로운 함수를 반환하거나, 데코레이트된 함수를 대체할 수 있습니다.

예시 - 함수 실행 시간을 측정하는 데코레이터

import time

def time_it(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(f'{func.__name__} 실행시간: {end - start} 초')
        return result
    return wrapper

@time_it
def my_function():
    time.sleep(2)
    print('my_function 실행됨')

my_function()

time_it함수는 데코레이트된 함수의 실행 시간을 측정하는 역할을 한다. wrapper함수는 데코레이트된 함수를 실행하고 실행 시간을 측정한다. 실행 시간을 출력하고 데코레이트된 함수의 결과를 반환합니다.

my_function 함수는 @time_it 데코레이터를 사용해 실행 시간을 출력하도록 변경됩니다. 데코레이터는 my_function 함수를 감싸고 실행 시간을 측정하는 새로운 함수를 반환합니다.

여기서 functools.wrap 함수를 사용하면 데코레이터가 데코레이트된 함수의 메타데이터(예: 독스트링, 이름, 인자)를 올바르게 전달할 수 있습니다. 이전에 작성한 time_it 데코레이터를 functools.wrap 함수를 사용해 변경해보겠습니다.

import time
import functools

def time_it(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(f'{func.__name__} 실행시간: {end - start} 초')
        return result
    return wrapper

@time_it
def my_function():
    time.sleep(2)
    print('my_function 실행됨')

my_function()

functools.wraps 함수를 사용하면 time_it 데코레이터가 my_function 함수의 이름, 독스트링 등의 메타데이터를 올바르게 전달합니다. 이를 통해 my_function 함수의 메타데이터가 데코레이트된 함수에도 유지됩니다.

28. 컴프리헨션 내부에 제어 하위 식을 세 개 이상 사용하지 말라

컴프리헨션은 리스트, 셋, 딕셔너리와 같은 컬렉션을 생성하기 위한 간단한 방법입니다. 컴프리헨션은 대부분의 경우에 루프와 조건문을 조합하여 사용합니다. 하지만 컴프리헨션에서 복잡한 조건문을 사용하면 코드가 가독성이 떨어지고 디버깅이 어려워집니다.

컴프리헨션 내부에서 세 개 이상의 제어 하위 식을 사용하는 것은 가독성을 떨어뜨리고, 다른 개발자들이 코드를 이해하기 어렵게 만듭니다. 이 경우에는 일반적인 루프와 조건문을 사용하여 코드를 작성하는 것이 좋습니다.

컴프리헨션 내부에서 제어 하위 식을 사용해야 할 경우에는 이를 함수로 분리하는 것이 좋습니다. 이렇게 하면 코드의 가독성을 높일 수 있고, 함수의 이름을 통해 코드의 의도를 명확하게 전달할 수 있습니다.

# Bad example
result = [(x, y, z) for x in range(5) if x > 1 for y in range(5) if y > 1 for z in range(5) if z > 1]

# Good example
def is_valid(x, y, z):
    return x > 1 and y > 1 and z > 1

result = [(x, y, z) for x in range(5) for y in range(5) for z in range(5) if is_valid(x, y, z)]

컴프리헨션 내부에서 제어 하위 식을 사용할 때는 가독성을 고려하여 코드를 작성해야 합니다. 제어 하위 식이 많을수록 코드의 가독성이 떨어지므로, 필요한 경우에는 함수를 사용하여 코드를 분리하는 것이 좋습니다.

29. 대입식을 사용해 컴프리헨션 안에서 반복 작업을 피하라

컴프리헨션은 컬렉션을 생성하는 간단한 방법이지만, 각 요소의 값을 계산하기 위해 반복적인 작업이 필요한 경우 코드가 복잡해질 수 있습니다. Python 3.8에서는 대입식(:=)을 사용하여 표현식 내에서 변수를 할당하는 기능을 제공합니다. 이 기능은 반복 작업을 피하고 컴프리헨션을 간소화하는 데 사용할 수 있습니다.

예시 - 0부터 9까지의 모든 짝수의 제곱을 계산하는 리스트 컴프리헨션

squares = [x*x for x in range(10) if x % 2 == 0]

이 컴프리헨션은 각 짝수마다 x*x를 두 번 계산합니다. 대입식을 사용하면 x*x 값을 저장할 변수를 정의하고 컴프리헨션에서 사용할 수 있습니다.

squares = [(y:=x*x) for x in range(10) if x % 2 == 0]

이 컴프리헨션은 := 연산자를 사용하여 x*x 값을 변수 y에 할당합니다. 그런 다음 y 값은 제곱의 리스트를 만드는 데 사용됩니다. 이렇게 하면 각 짝수마다 x*x를 반복해서 계산하는 것을 피할 수 있습니다.

대입식은 중첩된 컴프리헨션을 간소화하는 데도 사용할 수 있습니다. 예를 들어, 0부터 4까지의 모든 숫자 쌍의 곱을 계산하는 다음 중첩된 리스트 컴프리헨션을 고려해 보겠습니다.

products = [x*y for x in range(5) for y in range(5)]

이 컴프리헨션은 숫자 쌍마다 x*y 값을 25번 계산합니다. 대입식을 사용하면 x*y 값을 저장할 변수를 정의하고 컴프리헨션에서 사용할 수 있습니다.

products = [(z:=x*y) for x in range(5) for y in range(5) if (z:=x*y)]

이 컴프리헨션은 := 연산자를 사용하여 x*y 값을 변수 z에 할당합니다. 그런 다음 z 값은 곱의 리스트를 만드는 데 사용됩니다. 대입식은 if 절에서도 사용되어 필요하지 않은 숫자 쌍을 걸러내는 데 사용됩니다.

대입식을 사용하면 반복 작업을 피하고 컴프리헨션을 간소화하여 코드를 더 간결하고 가독성 좋게 만들 수 있습니다. 그러나 과도하게 또는 부적절하게 사용하면 코드를 이해하기 어려워질 수 있으므로 주의해서 사용해야 합니다.

31. 인자에 대해 이터레이션할 때는 방어적이 돼라

함수는 종종 이터러블 객체를 인자로 받습니다. 이터러블 객체는 for 루프에서 사용할 수 있는 객체로 list, tuple, str, bytes와 같은 시퀀스 객체나 set, dict와 같은 비시퀀스 객체를 포함합니다. 이터러블 객체는 불변 객체가 아닐 수도 있습니다.

함수에 이터러블 객체를 전달할 때는 인자의 타입을 확인하고, 객체가 예상한 형태인지 확인하는 것이 좋습니다. 이를 통해 함수가 예상치 않은 인자 타입을 전달 받을 때 생기는 문제를 방지할 수 있습니다.

예시 - 리스트의 합을 반환하는 함수

def add_items(items):
    result = 0
    for item in items:
        result += item
    return result

이 함수는 인자로 리스트를 받아 합을 계산합니다. 하지만 이 함수에 str 객체를 전달하면 예기치 않은 결과가 발생합니다.

>>> add_items('123')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in add_items
TypeError: unsupported operand type(s) for +=: 'int' and 'str'

이 경우 str 객체의 각 문자를 순회하며 합을 계산하려고 하기 때문에 TypeError가 발생합니다.

이터러블 객체를 인자로 받는 함수를 작성할 때는 타입을 확인하고, 예상한 형태의 객체인지 확인하는 것이 좋습니다. 예를 들어, add_items 함수는 sum 함수를 사용하여 다음과 같이 작성할 수 있습니다.

def add_items(items):
    if not isinstance(items, (list, tuple)):
        items = [items]
    return sum(items)

이 함수는 인자가 리스트나 튜플이 아닌 경우 인자를 리스트로 변환하여 합을 계산합니다. 이를 통해 함수가 예상치 않은 인자 타입을 전달 받을 때 생기는 문제를 방지할 수 있습니다.

이터러블 객체를 인자로 받는 함수를 작성할 때는 타입을 확인하고 객체가 예상한 형태인지 확인하는 것이 좋습니다. 이를 통해 함수가 예상치 않은 인자 타입을 전달 받을 때 생기는 문제를 방지할 수 있습니다.

32. 긴 리스트 컴프리헨션보다는 제네레이터 식을 사용하라

긴 리스트 컴프리헨션은 가독성을 떨어뜨리고, 메모리를 과도하게 사용할 수 있습니다. 리스트 컴프리헨션을 사용하여 리스트를 생성할 때는 리스트의 길이가 긴 경우에는 제네레이터 식을 사용하는 것이 좋습니다.

제네레이터 식은 리스트 컴프리헨션과 유사하지만, [] 대신 ()를 사용하여 튜플이 아닌 제네레이터 객체를 생성합니다. 제네레이터 객체는 이터러블 객체로, 루프에서 사용할 수 있습니다. 제네레이터 객체는 값을 한 번에 한 개씩 생성하며, 메모리 사용량을 최소화합니다.

예시 - 0부터 999999까지의 숫자 중에서 3으로 나누어 떨어지는 숫자의 제곱의 합을 계산하는 리스트 컴프리헨션과 제네레이터 식

# List comprehension
result = sum([x*x for x in range(1000000) if x % 3 == 0])

# Generator expression
result = sum(x*x for x in range(1000000) if x % 3 == 0)

이 예시에서는 제네레이터 식을 사용하여 리스트 컴프리헨션보다 더 간결하고 가독성 좋은 코드를 작성할 수 있습니다.

긴 리스트 컴프리헨션을 사용하여 리스트를 생성하는 경우에는 제네레이터 식을 사용하여 코드의 가독성을 높이고, 메모리 사용량을 최소화하는 것이 좋습니다.

'python' 카테고리의 다른 글

이펙티브 파이썬 2nd 정리 #5  (0) 2023.09.08
이펙티브 파이썬 2nd 정리 #4  (0) 2023.09.01
이펙티브 파이썬 2nd 정리 #2  (0) 2023.08.05
이펙티브 파이썬 2nd 정리 #1  (1) 2023.07.28