소개
앞서 함수에 대한 소개가 있었지만, 실제로 함수가 더 깊은 수준에서 어떻게 작동하는지에 대한 세부 사항은 거의 제공되지 않았습니다. 이 섹션에서는 몇 가지 공백을 채우고 호출 규약 (calling conventions), 스코핑 규칙 (scoping rules) 등과 같은 문제들을 논의하는 것을 목표로 합니다.
앞서 함수에 대한 소개가 있었지만, 실제로 함수가 더 깊은 수준에서 어떻게 작동하는지에 대한 세부 사항은 거의 제공되지 않았습니다. 이 섹션에서는 몇 가지 공백을 채우고 호출 규약 (calling conventions), 스코핑 규칙 (scoping rules) 등과 같은 문제들을 논의하는 것을 목표로 합니다.
다음 함수를 고려해 봅시다:
def read_prices(filename, debug):
...
위 함수는 위치 인자 (positional arguments) 를 사용하여 호출할 수 있습니다:
prices = read_prices('prices.csv', True)
또는 키워드 인자 (keyword arguments) 를 사용하여 함수를 호출할 수 있습니다:
prices = read_prices(filename='prices.csv', debug=True)
때로는 인자를 선택적으로 만들고 싶을 수 있습니다. 이 경우, 함수 정의에서 기본값을 할당하십시오.
def read_prices(filename, debug=False):
...
기본값이 할당되면, 해당 인자는 함수 호출 시 선택 사항이 됩니다.
d = read_prices('prices.csv')
e = read_prices('prices.dat', True)
참고: 기본값을 가진 인자는 인자 목록의 끝에 나타나야 합니다 (모든 필수 인자는 먼저 옵니다).
다음 두 가지 호출 스타일을 비교해 봅시다:
parse_data(data, False, True) ## ?????
parse_data(data, ignore_errors=True)
parse_data(data, debug=True)
parse_data(data, debug=True, ignore_errors=True)
대부분의 경우, 키워드 인자는 코드의 명확성을 향상시킵니다. 특히 플래그 (flags) 역할을 하거나 선택적 기능과 관련된 인자의 경우 더욱 그렇습니다.
함수 인자 (function arguments) 에는 항상 짧지만 의미 있는 이름을 부여하십시오.
함수를 사용하는 사람은 키워드 호출 스타일 (keyword calling style) 을 사용하고 싶을 수 있습니다.
d = read_prices('prices.csv', debug=True)
Python 개발 도구는 도움말 기능 및 문서에서 이름을 표시합니다.
return 문은 값을 반환합니다.
def square(x):
return x * x
반환 값이 주어지지 않거나 return이 생략된 경우, None이 반환됩니다.
def bar(x):
statements
return
a = bar(4) ## a = None
## OR
def foo(x):
statements ## No `return`
b = foo(4) ## b = None
함수는 하나의 값만 반환할 수 있습니다. 하지만, 함수는 튜플 (tuple) 로 반환하여 여러 값을 반환할 수 있습니다.
def divide(a,b):
q = a // b ## 몫 (Quotient)
r = a % b ## 나머지 (Remainder)
return q, r ## 튜플 반환 (Return a tuple)
사용 예시:
x, y = divide(37,5) ## x = 7, y = 2
x = divide(37, 5) ## x = (7, 2)
프로그램은 변수에 값을 할당합니다.
x = value ## 전역 변수 (Global variable)
def foo():
y = value ## 지역 변수 (Local variable)
변수 할당은 함수 정의 외부와 내부에서 발생합니다. 외부에서 정의된 변수는 "전역 (global)" 변수입니다. 함수 내의 변수는 "지역 (local)" 변수입니다.
함수 내에서 할당된 변수는 비공개 (private) 변수입니다.
def read_portfolio(filename):
portfolio = []
for line in open(filename):
fields = line.split(',')
s = (fields[0], int(fields[1]), float(fields[2]))
portfolio.append(s)
return portfolio
이 예제에서 filename, portfolio, line, fields 및 s는 지역 변수입니다. 이러한 변수는 함수 호출 후 유지되거나 접근할 수 없습니다.
>>> stocks = read_portfolio('portfolio.csv')
>>> fields
Traceback (most recent call last):
File "<stdin>", line 1, in ?
NameError: name 'fields' is not defined
>>>
지역 변수는 또한 다른 곳에서 발견된 변수와 충돌할 수 없습니다.
함수는 동일한 파일에 정의된 전역 변수의 값에 자유롭게 접근할 수 있습니다.
name = 'Dave'
def greeting():
print('Hello', name) ## Using `name` global variable
하지만, 함수는 전역 변수를 수정할 수 없습니다:
name = 'Dave'
def spam():
name = 'Guido'
spam()
print(name) ## prints 'Dave'
기억하세요: 함수 내의 모든 할당은 지역적입니다.
전역 변수를 수정해야 하는 경우, 해당 변수를 전역 변수로 선언해야 합니다.
name = 'Dave'
def spam():
global name
name = 'Guido' ## Changes the global name above
global 선언은 사용 전에 나타나야 하며, 해당 변수는 함수와 동일한 파일에 존재해야 합니다. 이를 알았으니, 이는 좋지 않은 형식으로 간주된다는 것을 알아두세요. 사실, 가능하다면 global을 완전히 피하도록 노력하세요. 함수가 함수 외부의 어떤 종류의 상태를 수정해야 하는 경우, 대신 클래스를 사용하는 것이 더 좋습니다 (이에 대한 자세한 내용은 나중에 다룹니다).
함수를 호출할 때, 인자 변수는 전달된 값을 참조하는 이름입니다. 이러한 값은 복사본이 아닙니다. 가변 데이터 타입 (예: 리스트, 딕셔너리) 이 전달되면, 제자리에서 수정될 수 있습니다.
def foo(items):
items.append(42) ## Modifies the input object
a = [1, 2, 3]
foo(a)
print(a) ## [1, 2, 3, 42]
핵심 사항: 함수는 입력 인자의 복사본을 받지 않습니다.
값 수정과 변수 이름 재할당의 미묘한 차이점을 확실히 이해해야 합니다.
def foo(items):
items.append(42) ## Modifies the input object
a = [1, 2, 3]
foo(a)
print(a) ## [1, 2, 3, 42]
## VS
def bar(items):
items = [4,5,6] ## Changes local `items` variable to point to a different object
b = [1, 2, 3]
bar(b)
print(b) ## [1, 2, 3]
알림: 변수 할당은 메모리를 절대 덮어쓰지 않습니다. 이름은 단순히 새로운 값에 바인딩됩니다.
이 일련의 연습은 아마도 이 과정에서 가장 강력하고 어려운 부분을 구현하도록 합니다. 많은 단계가 있으며, 과거 연습에서 나온 많은 개념들이 한꺼번에 모여 있습니다. 최종 솔루션은 약 25 줄의 코드에 불과하지만, 시간을 들여 각 부분을 확실히 이해하도록 하세요.
report.py 프로그램의 핵심 부분은 CSV 파일 읽기에 중점을 둡니다. 예를 들어, read_portfolio() 함수는 포트폴리오 데이터 행이 포함된 파일을 읽고, read_prices() 함수는 가격 데이터 행이 포함된 파일을 읽습니다. 이 두 함수 모두에서 많은 하위 수준의 "까다로운" 부분과 유사한 기능이 있습니다. 예를 들어, 둘 다 파일을 열고 csv 모듈로 래핑하며, 둘 다 다양한 필드를 새로운 유형으로 변환합니다.
실제로 많은 파일 파싱을 수행하는 경우, 이 중 일부를 정리하고 더 일반적인 목적으로 만들고 싶을 것입니다. 그것이 우리의 목표입니다.
이 연습을 시작하려면 fileparse.py라는 파일을 여세요. 여기서 작업을 수행할 것입니다.
시작하려면 CSV 파일을 딕셔너리 목록으로 읽는 문제에 집중해 보겠습니다. fileparse_3.3.py 파일에서 다음과 같은 함수를 정의하세요.
## fileparse_3.3.py
import csv
def parse_csv(filename):
'''
Parse a CSV file into a list of records
'''
with open(filename) as f:
rows = csv.reader(f)
## Read the file headers
headers = next(rows)
records = []
for row in rows:
if not row: ## Skip rows with no data
continue
record = dict(zip(headers, row))
records.append(record)
return records
이 함수는 파일을 열고, csv 모듈로 래핑하고, 빈 줄을 무시하는 등의 세부 사항을 숨기면서 CSV 파일을 딕셔너리 목록으로 읽습니다.
시도해 보세요:
힌트: python3 -i fileparse_3.3.py.
>>> portfolio = parse_csv('/home/labex/project/portfolio.csv')
>>> portfolio
[{'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'}]
>>>
이것은 좋지만, 모든 데이터가 문자열로 표현되기 때문에 데이터를 사용하여 유용한 계산을 수행할 수 없습니다. 곧 수정할 예정이지만, 계속해서 구축해 보겠습니다.
많은 경우, CSV 파일의 모든 데이터가 아닌 선택된 열에만 관심이 있습니다. parse_csv() 함수를 수정하여 다음과 같이 사용자가 지정한 열을 선택적으로 선택할 수 있도록 합니다.
>>> ## Read all of the data
>>> portfolio = parse_csv('/home/labex/project/portfolio.csv')
>>> portfolio
[{'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'}]
>>> ## Read only some of the data
>>> shares_held = parse_csv('/home/labex/project/portfolio.csv', select=['name','shares'])
>>> shares_held
[{'name': 'AA', 'shares': '100'}, {'name': 'IBM', 'shares': '50'}, {'name': 'CAT', 'shares': '150'}, {'name': 'MSFT', 'shares': '200'}, {'name': 'GE', 'shares': '95'}, {'name': 'MSFT', 'shares': '50'}, {'name': 'IBM', 'shares': '100'}]
>>>
열 선택기의 예는 연습 2.23 에서 제공되었습니다. 그러나 이를 수행하는 한 가지 방법은 다음과 같습니다.
## fileparse_3.4.py
import csv
def parse_csv(filename, select=None):
'''
Parse a CSV file into a list of records
'''
with open(filename) as f:
rows = csv.reader(f)
## Read the file headers
headers = next(rows)
## If a column selector was given, find indices of the specified columns.
## Also narrow the set of headers used for resulting dictionaries
if select:
indices = [headers.index(colname) for colname in select]
headers = select
else:
indices = []
records = []
for row in rows:
if not row: ## Skip rows with no data
continue
## Filter the row if specific columns were selected
if indices:
row = [ row[index] for index in indices ]
## Make a dictionary
record = dict(zip(headers, row))
records.append(record)
return records
이 부분에는 몇 가지 까다로운 부분이 있습니다. 아마도 가장 중요한 것은 열 선택을 행 인덱스에 매핑하는 것입니다. 예를 들어, 입력 파일에 다음과 같은 헤더가 있다고 가정해 보겠습니다.
>>> headers = ['name', 'date', 'time', 'shares', 'price']
>>>
이제 선택된 열이 다음과 같다고 가정해 보겠습니다.
>>> select = ['name', 'shares']
>>>
적절한 선택을 수행하려면 선택된 열 이름을 파일의 열 인덱스에 매핑해야 합니다. 이것이 이 단계에서 수행하는 작업입니다.
>>> indices = [headers.index(colname) for colname in select ]
>>> indices
[0, 3]
>>>
다시 말해, "name"은 열 0 이고 "shares"는 열 3 입니다. 파일에서 데이터 행을 읽을 때, 인덱스는 이를 필터링하는 데 사용됩니다.
>>> row = ['AA', '6/11/2007', '9:50am', '100', '32.20' ]
>>> row = [ row[index] for index in indices ]
>>> row
['AA', '100']
>>>
/home/labex/project/fileparse_3.5.py 디렉토리의 parse_csv() 함수를 수정하여 반환된 데이터에 타입 변환을 선택적으로 적용할 수 있도록 합니다. 예를 들어:
>>> portfolio = parse_csv('/home/labex/project/portfolio.csv', types=[str, int, float])
>>> portfolio
[{'name': 'AA', 'shares': 100, 'price': 32.2}, {'name': 'IBM', 'shares': 50, 'price': 91.1}, {'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.1}, {'name': 'IBM', 'shares': 100, 'price': 70.44}]
>>> shares_held = parse_csv('/home/labex/project/portfolio.csv', select=['name', 'shares'], types=[str, int])
>>> shares_held
[{'name': 'AA', 'shares': 100}, {'name': 'IBM', 'shares': 50}, {'name': 'CAT', 'shares': 150}, {'name': 'MSFT', 'shares': 200}, {'name': 'GE', 'shares': 95}, {'name': 'MSFT', 'shares': 50}, {'name': 'IBM', 'shares': 100}]
>>>
이미 연습 2.24 에서 이를 탐구했습니다. 다음 코드 조각을 솔루션에 삽입해야 합니다.
...
if types:
row = [func(val) for func, val in zip(types, row) ]
...
일부 CSV 파일에는 헤더 정보가 포함되어 있지 않습니다. 예를 들어, prices.csv 파일은 다음과 같습니다.
"AA",9.22
"AXP",24.85
"BA",44.85
"BAC",11.27
...
/home/labex/project/fileparse_3.6.py의 parse_csv() 함수를 수정하여 튜플 목록을 생성하여 이러한 파일로 작업할 수 있도록 합니다. 예를 들어:
>>> prices = parse_csv('/home/labex/project/prices.csv', types=[str,float], has_headers=False)
>>> prices
[('AA', 9.22), ('AXP', 24.85), ('BA', 44.85), ('BAC', 11.27), ('C', 3.72), ('CAT', 35.46), ('CVX', 66.67), ('DD', 28.47), ('DIS', 24.22), ('GE', 13.48), ('GM', 0.75), ('HD', 23.16), ('HPQ', 34.35), ('IBM', 106.28), ('INTC', 15.72), ('JNJ', 55.16), ('JPM', 36.9), ('KFT', 26.11), ('KO', 49.16), ('MCD', 58.99), ('MMM', 57.1), ('MRK', 27.58), ('MSFT', 20.89), ('PFE', 15.19), ('PG', 51.94), ('T', 24.79), ('UTX', 52.61), ('VZ', 29.26), ('WMT', 49.74), ('XOM', 69.35)]
>>>
이 변경을 하려면, 데이터의 첫 번째 줄이 헤더 라인으로 해석되지 않도록 코드를 수정해야 합니다. 또한, 키로 사용할 열 이름이 더 이상 없으므로 딕셔너리를 생성하지 않도록 해야 합니다.
CSV 파일은 매우 일반적이지만, 탭이나 공백과 같은 다른 열 구분 기호를 사용하는 파일을 만날 수도 있습니다. 예를 들어, portfolio.dat 파일은 다음과 같습니다.
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
csv.reader() 함수는 다음과 같이 다른 열 구분 기호를 제공할 수 있습니다.
rows = csv.reader(f, delimiter=' ')
/home/labex/project/fileparse_3.7.py의 parse_csv() 함수를 수정하여 구분 기호를 변경할 수 있도록 합니다.
예를 들어:
>>> portfolio = parse_csv('/home/labex/project/portfolio.dat', types=[str, int, float], delimiter=' ')
>>> portfolio
[{'name': 'AA', 'shares': 100, 'price': 32.2}, {'name': 'IBM', 'shares': 50, 'price': 91.1}, {'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.1}, {'name': 'IBM', 'shares': 100, 'price': 70.44}]
>>>
여기까지 오셨다면, 정말 유용한 멋진 라이브러리 함수를 만드신 것입니다. 파일을 내부적으로 처리하거나 csv 모듈에 대해 너무 걱정하지 않고도 임의의 CSV 파일을 구문 분석하고, 관심 있는 열을 선택하고, 유형 변환을 수행하는 데 사용할 수 있습니다.
축하합니다! 함수에 대한 추가 랩을 완료했습니다. LabEx 에서 더 많은 랩을 연습하여 기술을 향상시킬 수 있습니다.