함수에서 값 반환하기

Beginner

This tutorial is from open-source community. Access the source code

소개

이 랩에서는 Python 에서 함수로부터 여러 값을 반환하는 방법을 배우게 됩니다. 또한 선택적 반환 값과 오류를 효과적으로 처리하는 방법을 이해하게 됩니다.

더 나아가, 동시 프로그래밍을 위한 Futures (미래) 의 개념을 탐구할 것입니다. 값을 반환하는 것이 간단해 보일 수 있지만, 다양한 프로그래밍 시나리오에서 다양한 패턴과 고려 사항이 존재합니다.

이것은 가이드 실험입니다. 학습과 실습을 돕기 위한 단계별 지침을 제공합니다.각 단계를 완료하고 실무 경험을 쌓기 위해 지침을 주의 깊게 따르세요. 과거 데이터에 따르면, 이것은 초급 레벨의 실험이며 완료율은 91%입니다.학습자들로부터 100%의 긍정적인 리뷰율을 받았습니다.

함수에서 여러 값 반환하기

Python 에서는 함수가 둘 이상의 값을 반환해야 할 때 유용한 해결책이 있습니다: 튜플을 반환하는 것입니다. 튜플은 Python 의 데이터 구조 유형 중 하나입니다. 튜플은 불변 시퀀스 (immutable sequence) 로, 튜플을 생성한 후에는 해당 요소를 변경할 수 없습니다. 튜플은 서로 다른 유형의 여러 값을 한 곳에 보관할 수 있기 때문에 유용합니다.

name=value 형식의 구성 라인을 구문 분석하는 함수를 만들어 보겠습니다. 이 함수의 목표는 이 형식의 라인을 가져와 이름과 값을 별도의 항목으로 반환하는 것입니다.

  1. 먼저, 새로운 Python 파일을 생성해야 합니다. 이 파일에는 함수와 테스트 코드에 대한 코드가 포함됩니다. 프로젝트 디렉토리에서 return_values.py라는 파일을 생성합니다. 터미널에서 다음 명령을 사용하여 이 파일을 생성할 수 있습니다.
touch ~/project/return_values.py
  1. 이제 코드 편집기에서 return_values.py 파일을 엽니다. 이 파일 내부에 parse_line 함수를 작성합니다. 이 함수는 라인을 입력으로 받아 첫 번째 '=' 기호에서 분할하고 이름과 값을 튜플로 반환합니다.
def parse_line(line):
    """
    'name=value' 형식의 라인을 구문 분석하고 이름과 값을 모두 반환합니다.

    Args:
        line (str): 'name=value' 형식으로 구문 분석할 입력 라인

    Returns:
        tuple: (name, value) 를 포함하는 튜플
    """
    parts = line.split('=', 1)  ## 첫 번째 등호에서 분할
    if len(parts) == 2:
        name = parts[0]
        value = parts[1]
        return (name, value)  ## 튜플로 반환

이 함수에서 split 메서드는 입력 라인을 첫 번째 '=' 기호에서 두 부분으로 나눕니다. 라인이 올바른 name=value 형식인 경우 이름과 값을 추출하여 튜플로 반환합니다.

  1. 함수를 정의한 후, 함수가 예상대로 작동하는지 확인하기 위해 몇 가지 테스트 코드를 추가해야 합니다. 테스트 코드는 샘플 입력을 사용하여 parse_line 함수를 호출하고 결과를 출력합니다.
## parse_line 함수 테스트
if __name__ == "__main__":
    result = parse_line('email=guido@python.org')
    print(f"Result as tuple: {result}")

    ## 튜플을 별도의 변수로 언패킹
    name, value = parse_line('email=guido@python.org')
    print(f"Unpacked name: {name}")
    print(f"Unpacked value: {value}")

테스트 코드에서 먼저 parse_line 함수를 호출하고 반환된 튜플을 result 변수에 저장합니다. 그런 다음 이 튜플을 출력합니다. 다음으로, 튜플 언패킹 (tuple unpacking) 을 사용하여 튜플의 요소를 namevalue 변수에 직접 할당하고 별도로 출력합니다.

  1. 함수와 테스트 코드를 작성했으면 return_values.py 파일을 저장합니다. 그런 다음 터미널을 열고 다음 명령을 실행하여 Python 스크립트를 실행합니다.
python ~/project/return_values.py

다음과 유사한 출력을 볼 수 있습니다.

Result as tuple: ('email', 'guido@python.org')
Unpacked name: email
Unpacked value: guido@python.org

설명:

  • parse_line 함수는 split 메서드를 사용하여 입력 문자열을 '=' 문자를 기준으로 분할합니다. 이 메서드는 지정된 구분 기호를 기준으로 문자열을 부분으로 나눕니다.
  • return (name, value) 구문을 사용하여 두 부분을 튜플로 반환합니다. 튜플은 여러 값을 함께 그룹화하는 방법입니다.
  • 함수를 호출할 때 두 가지 옵션이 있습니다. result 변수에서와 같이 전체 튜플을 하나의 변수에 저장할 수 있습니다. 또는 name, value = parse_line(...) 구문을 사용하여 튜플을 별도의 변수로 직접 "언패킹"할 수 있습니다. 이렇게 하면 개별 값으로 작업하기가 더 쉬워집니다.

튜플로 여러 값을 반환하는 이 패턴은 Python 에서 매우 일반적입니다. 함수가 호출하는 코드에 둘 이상의 정보를 제공할 수 있으므로 함수를 더 다재다능하게 만듭니다.

선택적 값 반환

프로그래밍에서 함수가 유효한 결과를 생성할 수 없는 경우가 있습니다. 예를 들어, 함수가 입력에서 특정 정보를 추출하도록 되어 있지만 입력이 예상 형식이 아닌 경우입니다. Python 에서 이러한 상황을 처리하는 일반적인 방법은 None을 반환하는 것입니다. None은 유효한 반환 값이 없음을 나타내는 Python 의 특수한 값입니다.

입력이 예상 기준을 충족하지 않는 경우 함수를 수정하여 처리하는 방법을 살펴보겠습니다. 'name=value' 형식의 라인을 구문 분석하고 이름과 값을 모두 반환하도록 설계된 parse_line 함수를 사용합니다.

  1. return_values.py 파일에서 parse_line 함수를 업데이트합니다.
def parse_line(line):
    """
    'name=value' 형식의 라인을 구문 분석하고 이름과 값을 모두 반환합니다.
    라인이 올바른 형식이 아니면 None 을 반환합니다.

    Args:
        line (str): 'name=value' 형식으로 구문 분석할 입력 라인

    Returns:
        tuple or None: (name, value) 를 포함하는 튜플 또는 구문 분석에 실패하면 None
    """
    parts = line.split('=', 1)  ## 첫 번째 등호에서 분할
    if len(parts) == 2:
        name = parts[0]
        value = parts[1]
        return (name, value)  ## 튜플로 반환
    else:
        return None  ## 잘못된 입력에 대해 None 반환

이 업데이트된 parse_line 함수에서 먼저 split 메서드를 사용하여 입력 라인을 첫 번째 등호에서 분할합니다. 결과 목록에 정확히 두 개의 요소가 있으면 라인이 올바른 'name=value' 형식임을 의미합니다. 그런 다음 이름과 값을 추출하여 튜플로 반환합니다. 목록에 두 개의 요소가 없으면 입력이 잘못된 것이므로 None을 반환합니다.

  1. 업데이트된 함수를 보여주기 위해 테스트 코드를 추가합니다.
## 업데이트된 parse_line 함수 테스트
if __name__ == "__main__":
    ## 유효한 입력
    result1 = parse_line('email=guido@python.org')
    print(f"Valid input result: {result1}")

    ## 잘못된 입력
    result2 = parse_line('invalid_line_without_equals_sign')
    print(f"Invalid input result: {result2}")

    ## 결과를 사용하기 전에 None 확인
    test_line = 'user_info'
    result = parse_line(test_line)
    if result is None:
        print(f"Could not parse the line: '{test_line}'")
    else:
        name, value = result
        print(f"Name: {name}, Value: {value}")

이 테스트 코드는 유효한 입력과 잘못된 입력을 모두 사용하여 parse_line 함수를 호출합니다. 그런 다음 결과를 출력합니다. parse_line 함수의 결과를 사용할 때 먼저 None인지 확인합니다. 튜플인 것처럼 None 값을 언패킹하려고 하면 오류가 발생하므로 이것이 중요합니다.

  1. 파일을 저장하고 실행합니다.
python ~/project/return_values.py

스크립트를 실행하면 다음과 유사한 출력을 볼 수 있습니다.

Valid input result: ('email', 'guido@python.org')
Invalid input result: None
Could not parse the line: 'user_info'

설명:

  • 이제 함수는 라인에 등호가 포함되어 있는지 확인합니다. 이는 등호에서 라인을 분할하고 결과 목록의 길이를 확인하여 수행됩니다.
  • 라인에 등호가 포함되어 있지 않으면 구문 분석에 실패했음을 나타내기 위해 None을 반환합니다.
  • 이러한 함수를 사용할 때는 사용하기 전에 결과가 None인지 확인하는 것이 중요합니다. 그렇지 않으면 None 값의 요소에 액세스하려고 할 때 오류가 발생할 수 있습니다.

설계 토론:
잘못된 입력을 처리하는 또 다른 방법은 예외를 발생시키는 것입니다. 이 접근 방식은 특정 상황에 적합합니다.

  1. 잘못된 입력은 실제로 예외적이며 예상되는 경우가 아닙니다. 예를 들어, 입력이 신뢰할 수 있는 소스에서 제공되어야 하고 항상 올바른 형식이어야 하는 경우입니다.
  2. 호출자가 오류를 처리하도록 강제하려는 경우. 예외를 발생시키면 프로그램의 정상적인 흐름이 중단되고 호출자는 오류를 명시적으로 처리해야 합니다.
  3. 자세한 오류 정보를 제공해야 하는 경우. 예외는 오류에 대한 추가 정보를 전달할 수 있으며, 이는 디버깅에 유용할 수 있습니다.

예외 기반 접근 방식의 예:

def parse_line_with_exception(line):
    """라인을 구문 분석하고 잘못된 입력에 대해 예외를 발생시킵니다."""
    parts = line.split('=', 1)
    if len(parts) != 2:
        raise ValueError(f"Invalid format: '{line}' does not contain '='")
    return (parts[0], parts[1])

None을 반환할지 예외를 발생시킬지는 애플리케이션의 요구 사항에 따라 다릅니다.

  • 결과가 없는 것이 일반적이고 예상되는 경우 None을 반환합니다. 예를 들어, 목록에서 항목을 검색할 때 항목이 없을 수 있습니다.
  • 실패가 예상치 못하고 정상적인 흐름을 중단해야 하는 경우 예외를 발생시킵니다. 예를 들어, 항상 존재해야 하는 파일에 액세스하려고 할 때입니다.

동시 프로그래밍을 위한 Future 사용하기

Python 에서 함수를 동시에, 즉 동시적으로 실행해야 할 필요가 있을 때, 스레드 (thread) 및 프로세스 (process) 와 같은 유용한 도구를 제공합니다. 하지만 여기서 흔히 직면하는 문제가 있습니다: 다른 스레드에서 실행 중인 함수가 반환하는 값을 어떻게 얻을 수 있을까요? 이 때 Future의 개념이 매우 중요해집니다.

Future는 나중에 사용할 수 있는 결과에 대한 자리 표시자와 같습니다. 함수가 실행을 완료하기 전에도 함수가 미래에 생성할 값을 나타내는 방법입니다. 간단한 예제를 통해 이 개념을 더 잘 이해해 보겠습니다.

1 단계: 새 파일 생성

먼저, 새 Python 파일을 생성해야 합니다. 이 파일의 이름을 futures_demo.py라고 하겠습니다. 터미널에서 다음 명령을 사용하여 이 파일을 생성할 수 있습니다.

touch ~/project/futures_demo.py

2 단계: 기본 함수 코드 추가

이제 futures_demo.py 파일을 열고 다음 Python 코드를 추가합니다. 이 코드는 간단한 함수를 정의하고 일반적인 함수 호출이 어떻게 작동하는지 보여줍니다.

import time
import threading
from concurrent.futures import Future, ThreadPoolExecutor

def worker(x, y):
    """A function that takes time to complete"""
    print('Starting work...')
    time.sleep(5)  ## Simulate a time-consuming task
    print('Work completed')
    return x + y

## Part 1: Normal function call
print("--- Part 1: Normal function call ---")
result = worker(2, 3)
print(f"Result: {result}")

이 코드에서 worker 함수는 두 개의 숫자를 받아 더하지만, 먼저 5 초 동안 일시 중지하여 시간이 많이 걸리는 작업을 시뮬레이션합니다. 이 함수를 일반적인 방식으로 호출하면 프로그램은 함수가 완료될 때까지 기다린 다음 반환 값을 가져옵니다.

3 단계: 기본 코드 실행

파일을 저장하고 터미널에서 다음 명령을 사용하여 실행합니다.

python ~/project/futures_demo.py

다음과 같은 출력을 볼 수 있습니다.

--- Part 1: Normal function call ---
Starting work...
Work completed
Result: 5

이것은 일반적인 함수 호출이 함수가 완료될 때까지 기다린 다음 결과를 반환함을 보여줍니다.

4 단계: 별도의 스레드에서 함수 실행

다음으로, worker 함수를 별도의 스레드에서 실행할 때 어떤 일이 발생하는지 살펴보겠습니다. 다음 코드를 futures_demo.py 파일에 추가합니다.

## Part 2: Running in a separate thread (problem: no way to get result)
print("\n--- Part 2: Running in a separate thread ---")
t = threading.Thread(target=worker, args=(2, 3))
t.start()
print("Main thread continues while worker runs...")
t.join()  ## Wait for the thread to complete
print("Worker thread finished, but we don't have its return value!")

여기서는 threading.Thread 클래스를 사용하여 worker 함수를 새 스레드에서 시작합니다. 메인 스레드는 worker 함수가 완료될 때까지 기다리지 않고 실행을 계속합니다. 그러나 worker 스레드가 완료되면 반환 값을 얻을 쉬운 방법이 없습니다.

5 단계: 스레드 코드 실행

파일을 다시 저장하고 동일한 명령을 사용하여 실행합니다.

python ~/project/futures_demo.py

메인 스레드가 계속 실행되고, worker 스레드가 실행되지만, worker 함수의 반환 값에 액세스할 수 없음을 알 수 있습니다.

6 단계: Future 수동 사용

스레드에서 반환 값을 얻는 문제를 해결하기 위해 Future 객체를 사용할 수 있습니다. 다음 코드를 futures_demo.py 파일에 추가합니다.

## Part 3: Using a Future to get the result
print("\n--- Part 3: Using a Future manually ---")

def do_work_with_future(x, y, future):
    """Wrapper that sets the result in the Future"""
    result = worker(x, y)
    future.set_result(result)

## Create a Future object
fut = Future()

## Start a thread that will set the result in the Future
t = threading.Thread(target=do_work_with_future, args=(2, 3, fut))
t.start()

print("Main thread continues...")
print("Waiting for the result...")
## Block until the result is available
result = fut.result()  ## This will wait until set_result is called
print(f"Got the result: {result}")

이 코드에서는 Future 객체를 생성하고 새 함수 do_work_with_future에 전달합니다. 이 함수는 worker 함수를 호출한 다음 Future 객체에 결과를 설정합니다. 그런 다음 메인 스레드는 Future 객체의 result() 메서드를 사용하여 결과가 사용 가능할 때 결과를 얻을 수 있습니다.

7 단계: Future로 코드 실행

파일을 저장하고 다시 실행합니다.

python ~/project/futures_demo.py

이제 스레드에서 실행되는 함수에서 반환 값을 성공적으로 얻을 수 있음을 알 수 있습니다.

8 단계: ThreadPoolExecutor 사용

Python 의 ThreadPoolExecutor 클래스는 동시 작업을 훨씬 쉽게 처리할 수 있도록 해줍니다. 다음 코드를 futures_demo.py 파일에 추가합니다.

## Part 4: Using ThreadPoolExecutor (easier way)
print("\n--- Part 4: Using ThreadPoolExecutor ---")
with ThreadPoolExecutor() as executor:
    ## Submit the work to the executor
    future = executor.submit(worker, 2, 3)

    print("Main thread continues after submitting work...")
    print("Checking if the future is done:", future.done())

    ## Get the result (will wait if not ready)
    result = future.result()
    print("Now the future is done:", future.done())
    print(f"Final result: {result}")

ThreadPoolExecutorFuture 객체를 생성하고 관리하는 것을 처리합니다. 함수와 해당 인수를 제출하기만 하면 결과를 얻는 데 사용할 수 있는 Future 객체를 반환합니다.

9 단계: 전체 코드 실행

파일을 마지막으로 저장하고 실행합니다.

python ~/project/futures_demo.py

설명

  1. 일반 함수 호출: 일반적인 방식으로 함수를 호출하면 프로그램은 함수가 완료될 때까지 기다리고 직접 반환 값을 가져옵니다.
  2. 스레드 문제: 별도의 스레드에서 함수를 실행하는 데는 단점이 있습니다. 해당 스레드에서 실행되는 함수의 반환 값을 얻을 수 있는 내장된 방법이 없습니다.
  3. 수동 Future: Future 객체를 생성하여 스레드에 전달하면 Future에 결과를 설정한 다음 메인 스레드에서 결과를 얻을 수 있습니다.
  4. ThreadPoolExecutor: 이 클래스는 동시 프로그래밍을 단순화합니다. Future 객체의 생성 및 관리를 처리하여 함수를 동시적으로 실행하고 반환 값을 얻는 것을 더 쉽게 만듭니다.

Future 객체에는 몇 가지 유용한 메서드가 있습니다.

  • result(): 이 메서드는 함수의 결과를 얻는 데 사용됩니다. 결과가 아직 준비되지 않은 경우 준비될 때까지 기다립니다.
  • done(): 이 메서드를 사용하여 함수의 계산이 완료되었는지 확인할 수 있습니다.
  • add_done_callback(): 이 메서드를 사용하면 결과가 준비될 때 호출될 함수를 등록할 수 있습니다.

이 패턴은 동시 프로그래밍에서, 특히 병렬로 실행되는 함수에서 결과를 얻어야 할 때 매우 중요합니다.

요약

이 Lab 에서 Python 의 함수에서 값을 반환하기 위한 몇 가지 주요 패턴을 배웠습니다. 첫째, Python 함수는 튜플로 패킹하여 여러 값을 반환할 수 있으며, 이를 통해 깔끔하고 읽기 쉬운 값 반환 및 언패킹이 가능합니다. 둘째, 항상 유효한 결과를 생성하지 못할 수 있는 함수의 경우, None을 반환하는 것은 값의 부재를 나타내는 일반적인 방법이며, 예외를 발생시키는 것도 대안으로 제시되었습니다.

마지막으로, 동시 프로그래밍에서 Future는 미래 결과에 대한 자리 표시자 역할을 하여 별도의 스레드 또는 프로세스에서 실행되는 함수에서 반환 값을 얻을 수 있도록 합니다. 이러한 패턴을 이해하면 Python 코드의 견고성과 유연성이 향상됩니다. 추가 연습을 위해, 다양한 오류 처리 전략을 실험하고, 다른 동시 실행 유형과 함께 Futures 를 사용하고, async/await를 사용한 비동기 프로그래밍에서의 적용을 탐색해 보세요.