Python 의 고차 함수

Beginner

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

소개

이 랩에서는 Python 의 고차 함수에 대해 배우게 됩니다. 고차 함수는 다른 함수를 인수로 받거나 함수를 결과로 반환할 수 있습니다. 이 개념은 함수형 프로그래밍에서 매우 중요하며, 더 모듈화되고 재사용 가능한 코드를 작성할 수 있게 해줍니다.

고차 함수가 무엇인지 이해하고, 함수를 인수로 받는 고차 함수를 만들고, 기존 함수를 고차 함수를 사용하도록 리팩토링하며, Python 의 내장 map() 함수를 활용할 것입니다. 랩 동안 reader.py 파일이 수정될 것입니다.

코드 중복 이해하기

reader.py 파일의 현재 코드를 살펴보는 것으로 시작해 봅시다. 프로그래밍에서 기존 코드를 검토하는 것은 작동 방식을 이해하고 개선할 부분을 식별하는 중요한 단계입니다. WebIDE 에서 reader.py 파일을 열 수 있습니다. 이렇게 하는 두 가지 방법이 있습니다. 파일 탐색기에서 파일을 클릭하거나 터미널에서 다음 명령을 실행할 수 있습니다. 이 명령은 먼저 프로젝트 디렉토리로 이동한 다음 reader.py 파일의 내용을 표시합니다.

cd ~/project
cat reader.py

코드를 보면 두 개의 함수가 있다는 것을 알 수 있습니다. Python 의 함수는 특정 작업을 수행하는 코드 블록입니다. 다음은 두 함수와 그 기능입니다.

  1. csv_as_dicts(): 이 함수는 CSV 데이터를 받아 딕셔너리 목록으로 변환합니다. Python 에서 딕셔너리는 키 - 값 쌍의 모음이며, 데이터를 구조화된 방식으로 저장하는 데 유용합니다.
  2. csv_as_instances(): 이 함수는 CSV 데이터를 받아 인스턴스 목록으로 변환합니다. 인스턴스는 객체를 생성하기 위한 청사진인 클래스에서 생성된 객체입니다.

이제 이 두 함수를 자세히 살펴보겠습니다. 두 함수가 매우 유사하다는 것을 알 수 있습니다. 두 함수 모두 다음 단계를 따릅니다.

  • 먼저, 빈 records 리스트를 초기화합니다. Python 에서 리스트는 서로 다른 유형의 항목 모음입니다. 빈 리스트를 초기화하는 것은 항목이 없는 리스트를 생성하는 것을 의미하며, 처리된 데이터를 저장하는 데 사용됩니다.
  • 그런 다음 csv.reader()를 사용하여 입력을 파싱합니다. 파싱은 의미 있는 정보를 추출하기 위해 입력 데이터를 분석하는 것을 의미합니다. 이 경우 csv.reader()는 CSV 데이터를 행별로 읽는 데 도움이 됩니다.
  • 헤더를 동일한 방식으로 처리합니다. CSV 파일의 헤더는 일반적으로 열의 이름이 포함된 첫 번째 행입니다.
  • 그 후, CSV 데이터의 각 행을 반복합니다. 루프는 코드 블록을 여러 번 실행할 수 있게 해주는 프로그래밍 구성 요소입니다.
  • 각 행에 대해 레코드를 생성하기 위해 처리합니다. 이 레코드는 함수에 따라 딕셔너리 또는 인스턴스가 될 수 있습니다.
  • 레코드를 records 리스트에 추가합니다. 추가는 리스트의 끝에 항목을 추가하는 것을 의미합니다.
  • 마지막으로, 처리된 모든 데이터가 포함된 records 리스트를 반환합니다.

이러한 코드 중복은 여러 가지 이유로 문제가 됩니다. 코드가 중복되면 다음과 같은 문제가 발생합니다.

  • 유지 관리가 더 어려워집니다. 코드를 변경해야 하는 경우 여러 위치에서 동일한 변경을 수행해야 합니다. 이는 더 많은 시간과 노력이 필요합니다.
  • 모든 변경 사항은 여러 위치에서 구현되어야 합니다. 이는 변경 사항을 한 곳에서 잊어버려 일관성 없는 동작을 초래할 가능성을 높입니다.
  • 또한 버그가 발생할 가능성이 높아집니다. 버그는 예상치 못한 동작을 유발할 수 있는 코드의 오류입니다.

이 두 함수의 유일한 실제 차이점은 행을 레코드로 변환하는 방식입니다. 이것은 고차 함수가 매우 유용할 수 있는 전형적인 상황입니다. 고차 함수는 다른 함수를 인수로 받거나 함수를 결과로 반환할 수 있는 함수입니다.

이 함수들이 어떻게 작동하는지 더 잘 이해하기 위해 이러한 함수의 몇 가지 사용 예시를 살펴보겠습니다. 다음 코드는 csv_as_dicts()csv_as_instances()를 사용하는 방법을 보여줍니다.

## Example of using csv_as_dicts
with open('portfolio.csv') as f:
    portfolio = csv_as_dicts(f, [str, int, float])
print(portfolio[0])  ## {'name': 'AA', 'shares': 100, 'price': 32.2}

## Example of using csv_as_instances
class Stock:
    @classmethod
    def from_row(cls, row):
        return cls(row[0], int(row[1]), float(row[2]))

    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

with open('portfolio.csv') as f:
    portfolio = csv_as_instances(f, Stock)
print(portfolio[0].name, portfolio[0].shares, portfolio[0].price)  ## AA 100 32.2

다음 단계에서는 이 코드 중복을 제거하기 위해 고차 함수를 만들 것입니다. 이렇게 하면 코드를 더 유지 관리하기 쉽고 오류가 발생하기 쉬워집니다.

고차 함수 만들기

Python 에서 고차 함수는 다른 함수를 인수로 받을 수 있는 함수입니다. 이를 통해 더 큰 유연성과 코드 재사용이 가능합니다. 이제 convert_csv()라는 고차 함수를 만들어 보겠습니다. 이 함수는 CSV 데이터를 처리하는 일반적인 작업을 처리하는 동시에 CSV 의 각 행을 레코드로 변환하는 방식을 사용자 정의할 수 있도록 합니다.

WebIDE 에서 reader.py 파일을 엽니다. CSV 데이터의 반복 가능한 객체, 변환 함수, 그리고 선택적으로 열 헤더를 사용할 함수를 추가할 것입니다. 변환 함수는 CSV 의 각 행을 레코드로 변환하는 데 사용됩니다.

다음은 convert_csv() 함수의 코드입니다. reader.py 파일에 복사하여 붙여넣으세요.

def convert_csv(lines, conversion_func, *, headers=None):
    '''
    Convert lines of CSV data using the provided conversion function

    Args:
        lines: An iterable containing CSV data
        conversion_func: A function that takes headers and a row and returns a record
        headers: Column headers (optional). If None, the first row is used as headers

    Returns:
        A list of records as processed by conversion_func
    '''
    records = []
    rows = csv.reader(lines)
    if headers is None:
        headers = next(rows)
    for row in rows:
        record = conversion_func(headers, row)
        records.append(record)
    return records

이 함수가 하는 일을 자세히 살펴보겠습니다. 먼저, 변환된 레코드를 저장하기 위해 records라는 빈 리스트를 초기화합니다. 그런 다음 csv.reader() 함수를 사용하여 CSV 데이터의 행을 읽습니다. 헤더가 제공되지 않으면 첫 번째 행을 헤더로 사용합니다. 각 후속 행에 대해 conversion_func을 적용하여 행을 레코드로 변환하고 records 리스트에 추가합니다. 마지막으로, 레코드 목록을 반환합니다.

이제 convert_csv() 함수를 테스트하기 위한 간단한 변환 함수가 필요합니다. 이 함수는 헤더와 행을 받아 헤더를 키로 사용하여 행을 딕셔너리로 변환합니다.

다음은 make_dict() 함수의 코드입니다. 이 함수도 reader.py 파일에 추가하세요.

def make_dict(headers, row):
    '''
    Convert a row to a dictionary using the provided headers
    '''
    return dict(zip(headers, row))

make_dict() 함수는 zip() 함수를 사용하여 각 헤더를 행의 해당 값과 쌍으로 연결한 다음 이러한 쌍에서 딕셔너리를 생성합니다.

이 함수들을 테스트해 보겠습니다. 터미널에서 다음 명령을 실행하여 Python 셸을 엽니다.

cd ~/project
python3 -i reader.py

python3 명령의 -i 옵션은 대화형 모드에서 Python 인터프리터를 시작하고 reader.py 파일을 가져오므로 방금 정의한 함수를 사용할 수 있습니다.

Python 셸에서 다음 코드를 실행하여 함수를 테스트합니다.

## Open the CSV file
lines = open('portfolio.csv')

## Convert to a list of dictionaries using our new function
result = convert_csv(lines, make_dict)

## Print the result
print(result)

이 코드는 portfolio.csv 파일을 열고, make_dict() 변환 함수와 함께 convert_csv() 함수를 사용하여 CSV 데이터를 딕셔너리 목록으로 변환한 다음 결과를 출력합니다.

다음과 유사한 출력이 표시되어야 합니다.

[{'name': 'AA', 'shares': '100', 'price': '32.20'}, {'name': 'IBM', 'shares': '50', 'price': '91.10'}, {'name': 'CAT', 'shares': '150', 'price': '83.44'}, {'name': 'MSFT', 'shares': '200', 'price': '51.23'}, {'name': 'GE', 'shares': '95', 'price': '40.37'}, {'name': 'MSFT', 'shares': '50', 'price': '65.10'}, {'name': 'IBM', 'shares': '100', 'price': '70.44'}]

이 출력은 고차 함수 convert_csv()가 올바르게 작동함을 보여줍니다. 다른 함수를 인수로 사용하는 함수를 성공적으로 만들었으며, 이를 통해 CSV 데이터의 변환 방식을 쉽게 변경할 수 있습니다.

Python 셸을 종료하려면 exit()를 입력하거나 Ctrl+D 를 누르세요.

기존 함수 리팩터링

이제 convert_csv()라는 고차 함수를 만들었습니다. 고차 함수는 다른 함수를 인수로 받거나 함수를 결과로 반환할 수 있는 함수입니다. 이는 Python 에서 더 모듈화되고 재사용 가능한 코드를 작성하는 데 도움이 되는 강력한 개념입니다. 이 섹션에서는 이 고차 함수를 사용하여 원래 함수인 csv_as_dicts()csv_as_instances()를 리팩터링합니다. 리팩터링은 외부 동작을 변경하지 않고 기존 코드를 재구성하여 코드 중복 제거와 같은 내부 구조를 개선하는 프로세스입니다.

WebIDE 에서 reader.py 파일을 열어 시작해 보겠습니다. 다음과 같이 함수를 업데이트합니다.

  1. 먼저, csv_as_dicts() 함수를 대체합니다. 이 함수는 CSV 데이터의 행을 딕셔너리 목록으로 변환하는 데 사용됩니다. 다음은 새로운 코드입니다.
def csv_as_dicts(lines, types, *, headers=None):
    '''
    Convert lines of CSV data into a list of dictionaries
    '''
    def dict_converter(headers, row):
        return {name: func(val) for name, func, val in zip(headers, types, row)}

    return convert_csv(lines, dict_converter, headers=headers)

이 코드에서 headersrow를 인수로 사용하는 내부 함수 dict_converter를 정의합니다. 딕셔너리 컴프리헨션 (dictionary comprehension) 을 사용하여 키가 헤더 이름이고 값이 해당 유형 변환 함수를 행의 값에 적용한 결과인 딕셔너리를 만듭니다. 그런 다음 dict_converter 함수를 인수로 사용하여 convert_csv() 함수를 호출합니다.

  1. 다음으로, csv_as_instances() 함수를 대체합니다. 이 함수는 CSV 데이터의 행을 주어진 클래스의 인스턴스 목록으로 변환하는 데 사용됩니다. 다음은 새로운 코드입니다.
def csv_as_instances(lines, cls, *, headers=None):
    '''
    Convert lines of CSV data into a list of instances
    '''
    def instance_converter(headers, row):
        return cls.from_row(row)

    return convert_csv(lines, instance_converter, headers=headers)

이 코드에서 headersrow를 인수로 사용하는 내부 함수 instance_converter를 정의합니다. 주어진 클래스 clsfrom_row 클래스 메서드를 호출하여 행에서 인스턴스를 생성합니다. 그런 다음 instance_converter 함수를 인수로 사용하여 convert_csv() 함수를 호출합니다.

이러한 함수를 리팩터링한 후에는 예상대로 작동하는지 확인하기 위해 테스트해야 합니다. 이를 위해 Python 셸에서 다음 명령을 실행합니다.

cd ~/project
python3 -i reader.py

cd ~/project 명령은 현재 작업 디렉토리를 project 디렉토리로 변경합니다. python3 -i reader.py 명령은 reader.py 파일을 대화형 모드로 실행합니다. 즉, 파일 실행이 완료된 후에도 Python 코드를 계속 실행할 수 있습니다.

Python 셸이 열리면 다음 코드를 실행하여 리팩터링된 함수를 테스트합니다.

## Define a simple Stock class for testing
class Stock:
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

    @classmethod
    def from_row(cls, row):
        return cls(row[0], int(row[1]), float(row[2]))

    def __repr__(self):
        return f'Stock({self.name}, {self.shares}, {self.price})'

## Test csv_as_dicts
with open('portfolio.csv') as f:
    portfolio_dicts = csv_as_dicts(f, [str, int, float])
print("First dictionary:", portfolio_dicts[0])

## Test csv_as_instances
with open('portfolio.csv') as f:
    portfolio_instances = csv_as_instances(f, Stock)
print("First instance:", portfolio_instances[0])

이 코드에서는 먼저 테스트를 위해 간단한 Stock 클래스를 정의합니다. __init__ 메서드는 Stock 인스턴스의 속성을 초기화합니다. from_row 클래스 메서드는 CSV 데이터의 행에서 Stock 인스턴스를 생성합니다. __repr__ 메서드는 Stock 인스턴스의 문자열 표현을 제공합니다.

그런 다음 portfolio.csv 파일을 열고 유형 변환 함수 목록과 함께 함수에 전달하여 csv_as_dicts() 함수를 테스트합니다. 결과 목록의 첫 번째 딕셔너리를 출력합니다.

마지막으로, portfolio.csv 파일을 열고 Stock 클래스와 함께 함수에 전달하여 csv_as_instances() 함수를 테스트합니다. 결과 목록의 첫 번째 인스턴스를 출력합니다.

모든 것이 제대로 작동하면 다음과 유사한 출력이 표시되어야 합니다.

First dictionary: {'name': 'AA', 'shares': 100, 'price': 32.2}
First instance: Stock(AA, 100, 32.2)

이 출력은 리팩터링된 함수가 올바르게 작동함을 나타냅니다. 동일한 기능을 유지하면서 코드 중복을 성공적으로 제거했습니다.

Python 셸을 종료하려면 exit()를 입력하거나 Ctrl+D 를 누르세요.

map() 함수 사용하기

Python 에서 고차 함수는 다른 함수를 인수로 받거나 함수를 결과로 반환할 수 있는 함수입니다. Python 의 map() 함수는 고차 함수의 훌륭한 예입니다. 이는 주어진 함수를 리스트나 튜플과 같은 반복 가능한 객체의 각 항목에 적용할 수 있는 강력한 도구입니다. 각 항목에 함수를 적용한 후 결과의 이터레이터 (iterator) 를 반환합니다. 이러한 기능으로 인해 map()은 CSV 파일의 행과 같이 데이터 시퀀스를 처리하는 데 완벽합니다.

map() 함수의 기본 구문은 다음과 같습니다.

map(function, iterable, ...)

여기서 functioniterable의 각 항목에 대해 수행하려는 작업입니다. iterable은 리스트 또는 튜플과 같은 항목의 시퀀스입니다.

간단한 예제를 살펴보겠습니다. 숫자 목록이 있고 해당 목록의 각 숫자를 제곱하고 싶다고 가정해 보겠습니다. map() 함수를 사용하여 이를 수행할 수 있습니다. 방법은 다음과 같습니다.

numbers = [1, 2, 3, 4, 5]
squared = list(map(lambda x: x * x, numbers))
print(squared)  ## Output: [1, 4, 9, 16, 25]

이 예제에서는 먼저 numbers라는 리스트를 정의합니다. 그런 다음 map() 함수를 사용합니다. lambda 함수 lambda x: x * xnumbers 리스트의 각 항목에 대해 수행하려는 작업입니다. map() 함수는 이 lambda 함수를 리스트의 각 숫자에 적용합니다. map()은 이터레이터를 반환하므로 list() 함수를 사용하여 리스트로 변환합니다. 마지막으로, 원래 숫자의 제곱 값을 포함하는 squared 리스트를 출력합니다.

이제 map() 함수를 사용하여 convert_csv() 함수를 수정하는 방법을 살펴보겠습니다. 이전에는 for 루프를 사용하여 CSV 데이터의 행을 반복했습니다. 이제 해당 for 루프를 map() 함수로 대체합니다.

def convert_csv(lines, conversion_func, *, headers=None):
    '''
    Convert lines of CSV data using the provided conversion function
    '''
    rows = csv.reader(lines)
    if headers is None:
        headers = next(rows)

    ## Use map to apply conversion_func to each row
    records = list(map(lambda row: conversion_func(headers, row), rows))
    return records

이 업데이트된 버전의 convert_csv() 함수는 이전 버전과 정확히 동일한 작업을 수행하지만 for 루프 대신 map() 함수를 사용합니다. map() 내부의 lambda 함수는 CSV 데이터에서 각 행을 가져와 헤더와 함께 conversion_func을 적용합니다.

이 업데이트된 함수가 제대로 작동하는지 테스트해 보겠습니다. 먼저 터미널을 열고 프로젝트 디렉토리로 이동합니다. 그런 다음 reader.py 파일로 Python 대화형 셸을 시작합니다.

cd ~/project
python3 -i reader.py

Python 셸에 들어가면 다음 코드를 실행하여 업데이트된 convert_csv() 함수를 테스트합니다.

## Test the updated convert_csv function
with open('portfolio.csv') as f:
    result = convert_csv(f, make_dict)
print(result[0])  ## Should print the first dictionary

## Test that csv_as_dicts still works
with open('portfolio.csv') as f:
    portfolio = csv_as_dicts(f, [str, int, float])
print(portfolio[0])  ## Should print the first dictionary with converted types

이 코드를 실행한 후 다음과 유사한 출력이 표시되어야 합니다.

{'name': 'AA', 'shares': '100', 'price': '32.20'}
{'name': 'AA', 'shares': 100, 'price': 32.2}

이 출력은 map() 함수를 사용하는 업데이트된 convert_csv() 함수가 올바르게 작동하고, 이에 의존하는 함수도 예상대로 계속 작동함을 보여줍니다.

map() 함수를 사용하면 다음과 같은 몇 가지 장점이 있습니다.

  1. for 루프보다 더 간결할 수 있습니다. for 루프에 대해 여러 줄의 코드를 작성하는 대신 map()을 사용하여 동일한 결과를 한 줄로 얻을 수 있습니다.
  2. 시퀀스의 각 항목을 변환하려는 의도를 명확하게 전달합니다. map()을 보면 즉시 함수를 반복 가능한 객체의 각 항목에 적용하고 있음을 알 수 있습니다.
  3. 이터레이터를 반환하므로 메모리 효율성이 더 높을 수 있습니다. 이터레이터는 값을 즉시 생성하므로 모든 결과를 한 번에 메모리에 저장하지 않습니다. 이 예제에서는 map()에서 반환된 이터레이터를 리스트로 변환했지만, 경우에 따라 메모리를 절약하기 위해 이터레이터를 직접 사용할 수 있습니다.

Python 셸을 종료하려면 exit()를 입력하거나 Ctrl+D 를 누르세요.

요약

이 랩에서는 Python 의 고차 함수와 고차 함수가 더 모듈화되고 유지 관리 가능한 코드를 작성하는 데 어떻게 기여하는지 배웠습니다. 먼저, 두 개의 유사한 함수에서 코드 중복을 식별했습니다. 그런 다음 변환 함수를 인수로 받는 고차 함수 convert_csv()를 만들고 이를 사용하도록 원래 함수를 리팩터링했습니다. 마지막으로, Python 의 내장 map() 함수를 활용하도록 고차 함수를 업데이트했습니다.

이러한 기술은 Python 프로그래머의 도구 상자에서 강력한 자산입니다. 고차 함수는 코드 재사용과 관심사 분리를 촉진하는 반면, 함수를 인수로 전달하면 더 유연하고 사용자 정의 가능한 동작이 가능합니다. map()과 같은 함수는 데이터를 변환하는 간결한 방법을 제공합니다. 이러한 개념을 마스터하면 더 간결하고, 유지 관리 가능하며, 오류 발생 가능성이 적은 Python 코드를 작성할 수 있습니다.