프로처럼 반복하기

Beginner

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

소개

이 랩에서는 Python 프로그래밍의 기본적인 반복 (iteration) 개념에 대해 배우게 됩니다. 반복은 리스트, 튜플, 딕셔너리와 같은 시퀀스 내의 요소를 효율적으로 처리할 수 있게 해줍니다. 반복 기술을 마스터하면 Python 코딩 능력을 크게 향상시킬 수 있습니다.

기본 for 루프 반복, 시퀀스 언패킹 (sequence unpacking), enumerate()zip()과 같은 내장 함수 사용, 그리고 더 나은 메모리 효율성을 위한 제너레이터 표현식 (generator expressions) 활용 등, 몇 가지 강력한 Python 반복 기술을 탐구할 것입니다.

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

기본 반복 및 시퀀스 언패킹 (Sequence Unpacking)

이 단계에서는 for 루프를 사용한 기본 반복과 Python 에서의 시퀀스 언패킹을 살펴보겠습니다. 반복은 프로그래밍의 기본적인 개념으로, 시퀀스 내의 각 항목을 하나씩 처리할 수 있게 해줍니다. 반면에 시퀀스 언패킹은 시퀀스의 개별 요소를 변수에 편리하게 할당할 수 있도록 해줍니다.

CSV 파일에서 데이터 로드하기

CSV 파일에서 데이터를 로드하는 것으로 시작해 보겠습니다. CSV (Comma-Separated Values, 쉼표로 구분된 값) 는 표 형식 데이터를 저장하는 데 사용되는 일반적인 파일 형식입니다. 시작하려면 WebIDE 에서 터미널을 열고 Python 인터프리터를 시작해야 합니다. 이렇게 하면 Python 코드를 대화식으로 실행할 수 있습니다.

cd ~/project
python3

이제 Python 인터프리터에 들어왔으므로, portfolio.csv 파일에서 데이터를 읽기 위해 다음 Python 코드를 실행할 수 있습니다. 먼저, CSV 파일 작업을 위한 기능을 제공하는 csv 모듈을 가져옵니다. 그런 다음 파일을 열고 데이터를 읽기 위해 csv.reader 객체를 생성합니다. next 함수를 사용하여 열 머리글을 가져오고, 나머지 데이터를 리스트로 변환합니다. 마지막으로, pprint 모듈의 pprint 함수를 사용하여 행을 더 읽기 쉬운 형식으로 출력합니다.

import csv

f = open('portfolio.csv')
f_csv = csv.reader(f)
headers = next(f_csv)    ## Get the column headers
rows = list(f_csv)       ## Convert the remaining data to a list
from pprint import pprint
pprint(rows)             ## Pretty print the rows

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

[['AA', '100', '32.20'],
 ['IBM', '50', '91.10'],
 ['CAT', '150', '83.44'],
 ['MSFT', '200', '51.23'],
 ['GE', '95', '40.37'],
 ['MSFT', '50', '65.10'],
 ['IBM', '100', '70.44']]

for 루프를 사용한 기본 반복

Python 의 for 문은 리스트, 튜플 또는 문자열과 같은 모든 데이터 시퀀스를 반복하는 데 사용됩니다. 이 경우, CSV 파일에서 로드한 데이터의 행을 반복하는 데 사용할 것입니다.

for row in rows:
    print(row)

이 코드는 rows 리스트의 각 행을 순회하며 출력합니다. CSV 파일의 각 행이 하나씩 출력되는 것을 볼 수 있습니다.

['AA', '100', '32.20']
['IBM', '50', '91.10']
['CAT', '150', '83.44']
['MSFT', '200', '51.23']
['GE', '95', '40.37']
['MSFT', '50', '65.10']
['IBM', '100', '70.44']

루프에서 시퀀스 언패킹

Python 에서는 for 루프에서 시퀀스를 직접 언패킹할 수 있습니다. 이는 시퀀스의 각 항목의 구조를 알고 있을 때 매우 유용합니다. 이 경우, rows 리스트의 각 행에는 이름, 주식 수, 가격의 세 가지 요소가 포함되어 있습니다. 이러한 요소를 for 루프에서 직접 언패킹할 수 있습니다.

for name, shares, price in rows:
    print(name, shares, price)

이 코드는 각 행을 name, shares, price 변수로 언패킹한 다음 출력합니다. 데이터를 더 읽기 쉬운 형식으로 출력하는 것을 볼 수 있습니다.

AA 100 32.20
IBM 50 91.10
CAT 150 83.44
MSFT 200 51.23
GE 95 40.37
MSFT 50 65.10
IBM 100 70.44

일부 값이 필요하지 않은 경우, 해당 값을 신경 쓰지 않음을 나타내기 위해 _를 자리 표시자로 사용할 수 있습니다. 예를 들어, 이름과 가격만 출력하려면 다음 코드를 사용할 수 있습니다.

for name, _, price in rows:
    print(name, price)

이 코드는 각 행의 두 번째 요소를 무시하고 이름과 가격만 출력합니다.

AA 32.20
IBM 91.10
CAT 83.44
MSFT 51.23
GE 40.37
MSFT 65.10
IBM 70.44

* 연산자를 사용한 확장 언패킹

더 고급 언패킹을 위해 * 연산자를 와일드카드로 사용할 수 있습니다. 이를 통해 여러 요소를 리스트로 수집할 수 있습니다. 이 기술을 사용하여 데이터를 이름별로 그룹화해 보겠습니다.

from collections import defaultdict

byname = defaultdict(list)
for name, *data in rows:
    byname[name].append(data)

## Print the data for IBM
print(byname['IBM'])

## Iterate through IBM's data
for shares, price in byname['IBM']:
    print(shares, price)

이 코드에서는 먼저 collections 모듈에서 defaultdict 클래스를 가져옵니다. defaultdict는 키가 존재하지 않는 경우 자동으로 새 값 (이 경우 빈 리스트) 을 생성하는 딕셔너리입니다. 그런 다음 * 연산자를 사용하여 첫 번째 요소를 제외한 모든 요소를 data라는 리스트로 수집합니다. 이 리스트를 이름별로 그룹화하여 byname 딕셔너리에 저장합니다. 마지막으로, IBM 에 대한 데이터를 출력하고 이를 반복하여 주식 수와 가격을 출력합니다.

출력:

[['50', '91.10'], ['100', '70.44']]
50 91.10
100 70.44

이 예제에서 *data는 첫 번째 항목을 제외한 모든 항목을 리스트로 수집한 다음, 이름별로 그룹화된 딕셔너리에 저장합니다. 이는 가변 길이 시퀀스를 처리하기 위한 강력한 기술입니다.

enumerate()zip() 함수 사용하기

이 단계에서는 반복에 필수적인 Python 의 두 가지 매우 유용한 내장 함수인 enumerate()zip()을 살펴보겠습니다. 이러한 함수는 시퀀스로 작업할 때 코드를 크게 단순화할 수 있습니다.

enumerate()로 카운팅하기

시퀀스를 반복할 때 각 항목의 인덱스 또는 위치를 추적해야 하는 경우가 많습니다. 이때 enumerate() 함수가 유용합니다. enumerate() 함수는 시퀀스를 입력으로 받아 해당 시퀀스의 각 항목에 대해 (인덱스, 값) 쌍을 반환합니다.

이전 단계에서 Python 인터프리터를 따라 진행했다면 계속 사용할 수 있습니다. 그렇지 않은 경우 새 세션을 시작하십시오. 처음부터 시작하는 경우 데이터를 설정하는 방법은 다음과 같습니다.

## If you're starting a new session, reload the data first:
## import csv
## f = open('portfolio.csv')
## f_csv = csv.reader(f)
## headers = next(f_csv)
## rows = list(f_csv)

## Use enumerate to get row numbers
for rowno, row in enumerate(rows):
    print(rowno, row)

위 코드를 실행하면 enumerate(rows) 함수는 인덱스 (0 부터 시작) 와 rows 시퀀스에서 해당 행의 쌍을 생성합니다. 그런 다음 for 루프는 이러한 쌍을 rownorow 변수로 언패킹하고 이를 출력합니다.

출력:

0 ['AA', '100', '32.20']
1 ['IBM', '50', '91.10']
2 ['CAT', '150', '83.44']
3 ['MSFT', '200', '51.23']
4 ['GE', '95', '40.37']
5 ['MSFT', '50', '65.10']
6 ['IBM', '100', '70.44']

enumerate()를 언패킹과 결합하여 코드를 더욱 읽기 쉽게 만들 수 있습니다. 언패킹을 사용하면 시퀀스의 요소를 개별 변수에 직접 할당할 수 있습니다.

for rowno, (name, shares, price) in enumerate(rows):
    print(rowno, name, shares, price)

이 코드에서는 (name, shares, price) 주위에 추가 괄호 쌍을 사용하여 행 데이터를 올바르게 언패킹하고 있습니다. enumerate(rows)는 여전히 인덱스와 행을 제공하지만, 이제 행을 name, shares, price 변수로 언패킹하고 있습니다.

출력:

0 AA 100 32.20
1 IBM 50 91.10
2 CAT 150 83.44
3 MSFT 200 51.23
4 GE 95 40.37
5 MSFT 50 65.10
6 IBM 100 70.44

zip()으로 데이터 페어링하기

zip() 함수는 Python 의 또 다른 강력한 도구입니다. 여러 시퀀스의 해당 요소를 결합하는 데 사용됩니다. 여러 시퀀스를 zip()에 전달하면 각 튜플에 동일한 위치의 각 입력 시퀀스에서 가져온 요소가 포함된 튜플을 생성하는 반복자가 생성됩니다.

headersrow 데이터와 함께 zip()을 사용하는 방법을 살펴보겠습니다.

## Recall the headers variable from earlier
print(headers)  ## Should show ['name', 'shares', 'price']

## Get the first row
row = rows[0]
print(row)      ## Should show ['AA', '100', '32.20']

## Use zip to pair column names with values
for col, val in zip(headers, row):
    print(col, val)

이 코드에서 zip(headers, row)headers 시퀀스와 row 시퀀스를 가져와 해당 요소를 페어링합니다. 그런 다음 for 루프는 이러한 쌍을 col(headers 의 열 이름) 과 val(row 의 값) 로 언패킹하고 이를 출력합니다.

출력:

['name', 'shares', 'price']
['AA', '100', '32.20']
name AA
shares 100
price 32.20

zip()의 매우 일반적인 사용 사례 중 하나는 키 - 값 쌍에서 딕셔너리를 만드는 것입니다. Python 에서 딕셔너리는 키 - 값 쌍의 모음입니다.

## Create a dictionary from headers and row values
record = dict(zip(headers, row))
print(record)

여기서 zip(headers, row)는 열 이름과 값의 쌍을 생성하고 dict() 함수는 이러한 쌍을 가져와 딕셔너리로 변환합니다.

출력:

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

이 아이디어를 확장하여 rows 시퀀스의 모든 행을 딕셔너리로 변환할 수 있습니다.

## Convert all rows to dictionaries
for row in rows:
    record = dict(zip(headers, row))
    print(record)

이 루프에서 rows의 각 행에 대해 zip(headers, row)를 사용하여 키 - 값 쌍을 생성한 다음 dict()를 사용하여 해당 쌍을 딕셔너리로 변환합니다. 이 기술은 데이터 처리 애플리케이션, 특히 첫 번째 행에 헤더가 포함된 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'}

제너레이터 표현식과 메모리 효율성

이 단계에서는 제너레이터 표현식을 살펴보겠습니다. 이는 Python 에서 대용량 데이터 세트를 처리할 때 매우 유용합니다. 코드를 훨씬 더 메모리 효율적으로 만들 수 있으며, 이는 많은 양의 데이터를 처리할 때 중요합니다.

제너레이터 표현식 이해하기

제너레이터 표현식은 리스트 컴프리헨션과 유사하지만 중요한 차이점이 있습니다. 리스트 컴프리헨션을 사용하면 Python 은 모든 결과를 한 번에 포함하는 리스트를 생성합니다. 이는 특히 대용량 데이터 세트로 작업하는 경우 많은 메모리를 차지할 수 있습니다. 반면에 제너레이터 표현식은 필요에 따라 한 번에 하나씩 결과를 생성합니다. 즉, 모든 결과를 한 번에 메모리에 저장할 필요가 없으므로 상당한 양의 메모리를 절약할 수 있습니다.

이것이 어떻게 작동하는지 간단한 예제를 살펴보겠습니다.

## Start a new Python session if needed
## python3

## List comprehension (creates a list in memory)
nums = [1, 2, 3, 4, 5]
squares_list = [x*x for x in nums]
print(squares_list)

## Generator expression (creates a generator object)
squares_gen = (x*x for x in nums)
print(squares_gen)  ## This doesn't print the values, just the generator object

## Iterate through the generator to get values
for n in squares_gen:
    print(n)

이 코드를 실행하면 다음과 같은 출력을 볼 수 있습니다.

[1, 4, 9, 16, 25]
<generator object <genexpr> at 0x7f...>
1
4
9
16
25

제너레이터에 대해 주목해야 할 중요한 점은 한 번만 반복할 수 있다는 것입니다. 제너레이터의 모든 값을 처리하고 나면 소진되어 값을 다시 가져올 수 없습니다.

## Try to iterate again over the same generator
for n in squares_gen:
    print(n)  ## Nothing will be printed, as the generator is already exhausted

next() 함수를 사용하여 제너레이터에서 한 번에 하나씩 값을 수동으로 가져올 수도 있습니다.

## Create a fresh generator
squares_gen = (x*x for x in nums)

## Get values one by one
print(next(squares_gen))  ## 1
print(next(squares_gen))  ## 4
print(next(squares_gen))  ## 9

제너레이터에 더 이상 값이 없으면 next()를 호출하면 StopIteration 예외가 발생합니다.

yield 를 사용한 제너레이터 함수

더 복잡한 제너레이터 로직의 경우 yield 문을 사용하여 제너레이터 함수를 작성할 수 있습니다. 제너레이터 함수는 한 번에 하나의 결과를 반환하는 대신 yield를 사용하여 한 번에 하나씩 값을 반환하는 특수한 유형의 함수입니다.

def squares(nums):
    for x in nums:
        yield x*x

## Use the generator function
for n in squares(nums):
    print(n)

이 코드를 실행하면 다음과 같은 출력을 볼 수 있습니다.

1
4
9
16
25

제너레이터 표현식으로 메모리 사용량 줄이기

이제 제너레이터 표현식이 대용량 데이터 세트로 작업할 때 메모리를 절약하는 방법을 살펴보겠습니다. 매우 큰 CTA 버스 데이터 파일을 사용합니다.

cd /home/labex/project
unzip ctabus.csv.zip && rm ctabus.csv.zip

먼저, 메모리 집약적인 방식을 시도해 보겠습니다.

import tracemalloc
tracemalloc.start()

import readrides
rows = readrides.read_rides_as_dicts('ctabus.csv')
rt22 = [row for row in rows if row['route'] == '22']
max_row = max(rt22, key=lambda row: int(row['rides']))
print(max_row)

## Check memory usage
current, peak = tracemalloc.get_traced_memory()
print(f"Current memory usage: {current / 1024 / 1024:.2f} MB")
print(f"Peak memory usage: {peak / 1024 / 1024:.2f} MB")

이제 Python 을 종료하고 제너레이터 기반 방식과 비교하기 위해 다시 시작합니다.

exit() python3
import tracemalloc
tracemalloc.start()

import csv
f = open('ctabus.csv')
f_csv = csv.reader(f)
headers = next(f_csv)

## Use generator expressions for all operations
rows = (dict(zip(headers, row)) for row in f_csv)
rt22 = (row for row in rows if row['route'] == '22')
max_row = max(rt22, key=lambda row: int(row['rides']))
print(max_row)

## Check memory usage
current, peak = tracemalloc.get_traced_memory()
print(f"Current memory usage: {current / 1024 / 1024:.2f} MB")
print(f"Peak memory usage: {peak / 1024 / 1024:.2f} MB")

이 두 가지 방식 간의 메모리 사용량에 상당한 차이가 있음을 알 수 있습니다. 제너레이터 기반 방식은 모든 것을 한 번에 메모리에 로드하지 않고 데이터를 점진적으로 처리하므로 메모리 효율성이 훨씬 높습니다.

축소 함수를 사용한 제너레이터 표현식

제너레이터 표현식은 전체 시퀀스를 처리하고 단일 결과를 생성하는 sum(), min(), max(), any(), all()과 같은 함수와 결합할 때 특히 유용합니다.

몇 가지 예를 살펴보겠습니다.

from readport import read_portfolio
portfolio = read_portfolio('portfolio.csv')

## Calculate the total value of the portfolio
total_value = sum(s['shares']*s['price'] for s in portfolio)
print(f"Total portfolio value: {total_value}")

## Find the minimum number of shares in any holding
min_shares = min(s['shares'] for s in portfolio)
print(f"Minimum shares in any holding: {min_shares}")

## Check if any stock is IBM
has_ibm = any(s['name'] == 'IBM' for s in portfolio)
print(f"Portfolio contains IBM: {has_ibm}")

## Check if all stocks are IBM
all_ibm = all(s['name'] == 'IBM' for s in portfolio)
print(f"All stocks are IBM: {all_ibm}")

## Count IBM shares
ibm_shares = sum(s['shares'] for s in portfolio if s['name'] == 'IBM')
print(f"Total IBM shares: {ibm_shares}")

제너레이터 표현식은 문자열 연산에도 유용합니다. 튜플의 값을 결합하는 방법은 다음과 같습니다.

s = ('GOOG', 100, 490.10)
## This would fail: ','.join(s)
## Use a generator expression to convert all items to strings
joined = ','.join(str(x) for x in s)
print(joined)  ## Output: 'GOOG,100,490.1'

이러한 예에서 제너레이터 표현식을 사용하는 주요 장점은 중간 리스트가 생성되지 않아 메모리 효율적인 코드가 된다는 것입니다.

요약

이 랩에서는 몇 가지 강력한 Python 반복 기술을 배웠습니다. 첫째, for 루프를 사용하여 시퀀스를 반복하고 개별 변수로 언패킹하여 기본 반복 및 시퀀스 언패킹을 숙달했습니다. 둘째, 반복 중에 인덱스를 추적하기 위한 enumerate() 및 서로 다른 시퀀스의 요소를 페어링하기 위한 zip()과 같은 내장 함수를 탐구했습니다.

이러한 기술은 효율적인 Python 프로그래밍의 기본입니다. 이를 통해 더 간결하고, 읽기 쉽고, 메모리 효율적인 코드를 작성할 수 있습니다. 이러한 반복 패턴을 숙달함으로써 Python 프로젝트에서 데이터 처리 작업을 보다 효과적으로 처리할 수 있습니다.