Effective Python Item 22. 변수 위치 인자를 사용해 시각적인 잡음을 줄여라

    요약

    • def 문에서 *args를 사용하면 함수가 가변 위치 기반 인자를 받을 수 있다.
    • * 연산자를 사용하면 가변 인자를 받는 함수에게 시퀀스(리스트) 내의 원소를 전달할 수 있음. (언패킹 한 후 튜플로 만들어 전달함.)
    • 제네레이터에 * 연산자를 사용해 가변인수에 넘기면, 메모리를 소진하고 중단할 수 있음.
    • 가변인수 메서드에 위치 기반 인자를 추가하면 찾기 어려운 버그가 발생할 수 있음. 인자를 추가해야 하는 경우 키워드 기반 인자를 추가하자. 

     

     


    가변 인수 사용의 장점. 

    def log(message, values):
        if not values:
            print(message)
        else:
            value_str = ', '.join(str(x) for x in values)
            print(f'{message}: {value_str}')
    
    # 가변 변수를 사용하지 않는 경우, 사용하지 않더라도 리스트를 넣어야 한다.
    log('안녕', [])
    log('내 숫자는', [1,2])

    log 함수를 구현할 때, 필요한 경우에만 values에 값을 전달해서 로그에 포함하고 싶다고 가정해보자. 이 때, 가변변수를 사용하지 않으면 values에는 최소한 빈 리스트라도 전달해야한다. 이런 것은 불필요한 시각적 잡음을 가져온다. 

    def log(message, *values):
        if not values:
            print(message)
        else:
            value_str = ', '.join(str(x) for x in values)
            print(f'{message}: {value_str}')
    
    # 가변 변수를 사용하는 경우, 빈 리스트를 제공하지 않아도 됨.
    log('안녕')
    log('내 숫자는', [1,2])

    위의 코드에서 *를 이용해 values를 가변인수로 사용하게 한다면, log 함수를 호출할 때 전달할 values 값이 없다면 빈 리스트를 전달하지 않아도 된다. 즉, 가변 변수를 사용하면 함수 호출 시 가독성을 조금 개선해 줄 수 있다. 


    가변인수 사용의 단점.

    가변인수 사용 시 주의해야 할 부분은 다음과 같다.

    1. 가변인수에 제네레이터가 전달되면, 제네레이터를 끝까지 호출해서 튜플을 만들어 전달함.
    2. 잡기 힘든 버그가 생길 수 있음. 

     


    가변인수에 제너레이터 사용하면 메모리 문제 발생

    def my_generator():
        for i in range(10):
            print('generator called.')
            yield i
    
    
    def my_func(*args):
        print('here')
    
    # generator를 가변 인수로 전달할 때는 *를 이용해 언패킹 해줘야 함.
    my_func(*my_generator())

    가변인수는 항상 튜플로 변환된 다음에 함수에 전달된다. 만약 제네레이터가 생성하는 변수를 가변인수로 전달하고 싶어서 *(언패킹)을 이용해서 전달하면 어떻게 될까? 함수가 호출되기 전, 제네레이터의 StopIteration이 나타날 때까지 한 후 튜플을 생성해서 전달한다. 의도치 않게 제네레이터를 많이 호출하면서, 메모리 초과 에러를 만들 수도 있게 된다. 

    generator called.
    generator called.
    generator called.
    generator called.
    generator called.
    generator called.
    generator called.
    generator called.
    generator called.
    generator called.
    here

    실제로 위 코드를 호출하면 args를 호출하지도 않았는데, 제네레이터가 계속 호출되었다는 메세지가 뜬다. 

     


    위치 인자가 추가되는 경우, 잡기 어려운 버그가 생성

    # def log(message, *values): -> 기존 함수 시그니처. 
    def log(seq, message, *values):
        if not values:
            print(message)
        else:
            value_str = ', '.join(str(x) for x in values)
            print(f'{message}: {value_str}')
     
    # 런타임에 잘 동작하지만, 전혀 다른 결과가 나옴. 
    log('내 숫자는', [1,2])
    
    >>>> 출력결과
    내 숫자는 - [1, 2]

    가변인수를 사용하는 기존 함수 시그니처에 seq라는 위치 인수가 추가된 경우를 살펴보자. 기존 함수를 호출하는 코드를 그대로 사용하는 경우라면 어떻게 될까?

    1. 새로운 함수는 seq라는 값이 추가되기를 기대하고 있다. 그러나 함수가 바뀐지 모르고 함수 사용이 그대로 설정되어있다.
    2. seq에 새로운 값이 들어가야하는데, 그걸 까먹었다. 따라서 seq에는 '내 숫자는'이 들어간다.
    3. message에는 [1,2]가 들어간다.
    4. values에는 빈 튜플이 전달될 것이다. 

    여기서 message, values에 있어야 할 값이 각각 seq, message로 전달되었다. 그리고 함수 호출 시, 어떠한 에러도 발생하지 않았기 때문에 버그가 있었는지도 알 수 없게 된다. 이처럼 가변인수를 사용하는 경우에 '새로운 위치 인자'가 추가되면 발견하기 어려운 에러가 생성될 수 있다. 

    # 가변인수를 사용하지 않는 경우, 런타임 에러로 잡을 수 있음. 
    def log(seq, message, values):
        if not values:
            print(f'{seq} - {message}')
        else:
            value_str = ', '.join(str(x) for x in values)
            print(f'{seq} - {message}: {value_str}')
    
    # 이 경우, TypeError: log() missing 1 required positional argument: 'values' 에러 발생.
    log('내 숫자는', [1,2])

    만약 가변인수를 사용하지 않았는 경우, 메서드는 3개의 인자를 요구하는데 전달된 인자는 2개이기 때문에 런타임 에러가 발생할 것이다. 가변인수를 사용하면 잡을 수 없는 에러가, 가변인수를 사용하지 않았을 때는 런타임 에러로 발생하면서 최소한 인지할 수 있게 된다는 점이다. 

    이 부분을 해결하기 위해 가변인수를 사용하는 함수에 새로운 인자를 추가해야 할 때는 키워드 인자를 추가하도록 해야한다. 

    댓글

    Designed by JB FACTORY