Введение
Хотя функции были введены ранее, очень мало деталей было предоставлено о том, как они на самом деле работают на более глубоком уровне. Эта глава旨在填补一些空白 и обсудить вопросы, такие как соглашения о вызове, правила области видимости и т.д.
Вызов функции
Рассмотрим эту функцию:
def read_prices(filename, debug):
...
Вы можете вызывать функцию с позиционными аргументами:
prices = read_prices('prices.csv', True)
Или вы можете вызывать функцию с именованными аргументами:
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)
Во многих случаях именованные аргументы улучшают ясность кода - особенно для аргументов, которые служат флагами или которые связаны с необязательными функциями.
Лучшие практики в дизайне
Всегда давайте короткие, но осмысленные имена аргументам функций.
Кто-то, кто использует функцию, может захотеть использовать стиль вызова с именованными аргументами.
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
## ИЛИ
def foo(x):
statements ## Отсутствует `return`
b = foo(4) ## b = None
Несколько возвращаемых значений
Функции могут возвращать только одно значение. Однако функция может возвращать несколько значений, возвращая их в виде кортежа.
def divide(a,b):
q = a // b ## Частное
r = a % b ## Остаток
return q, r ## Возвращение кортежа
Пример использования:
x, y = divide(37,5) ## x = 7, y = 2
x = divide(37, 5) ## x = (7, 2)
Область видимости переменной
Программы присваивают значения переменным.
x = value ## Глобальная переменная
def foo():
y = value ## Локальная переменная
Присваивания переменным происходят вне и внутри определений функций. Переменные, определенные вне функций, являются "глобальными". Переменные внутри функции являются "локальными".
Локальные переменные
Переменные, присвоенные внутри функций, являются приватными.
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) ## Использование глобальной переменной `name`
Однако функции не могут изменять глобальные переменные:
name = 'Dave'
def spam():
name = 'Guido'
spam()
print(name) ## выводит 'Dave'
Помните: все присваивания внутри функций являются локальными.
Изменение глобальных переменных
Если вам необходимо изменить глобальную переменную, вы должны объявить ее как такую.
name = 'Dave'
def spam():
global name
name = 'Guido' ## Меняет глобальную переменную name выше
Объявление global должно быть расположено перед его использованием, и соответствующая переменная должна существовать в том же файле, что и функция. С учетом этого, стоит знать, что это считается плохой практикой. Фактически, старайтесь избегать использования global по возможности. Если вам нужно, чтобы функция изменяла какое-то состояние вне функции, лучше использовать класс вместо этого (об этом позже будет рассказано больше).
Передача аргументов
Когда вы вызываете функцию, переменные аргументов - это имена, которые ссылаются на переданные значения. Эти значения НЕ являются копиями. Если передаются изменяемые типы данных (например, списки, словари), они могут быть изменены на месте.
def foo(items):
items.append(42) ## Изменяет входной объект
a = [1, 2, 3]
foo(a)
print(a) ## [1, 2, 3, 42]
Основной момент: Функции не получают копию переданных аргументов.
Переприсваивание vs Изменение
Убедитесь, что вы понимаете незначительную разницу между изменением значения и переприсваиванием имени переменной.
def foo(items):
items.append(42) ## Изменяет входной объект
a = [1, 2, 3]
foo(a)
print(a) ## [1, 2, 3, 42]
## СРАВНИТЬ С
def bar(items):
items = [4,5,6] ## Меняет локальную переменную `items`, чтобы она ссылалась на другой объект
b = [1, 2, 3]
bar(b)
print(b) ## [1, 2, 3]
Напоминание: Присваивание переменной никогда не перезаписывает память. Имя просто связывается с новым значением.
В этом наборе упражнений вам предстоит реализовать, возможно, самую мощную и сложную часть курса. Здесь много шагов, и многие концепции из предыдущих упражнений объединяются сразу. Финальное решение состоит всего из примерно 25 строк кода, но не спеша и убедитесь, что вы понимаете каждую часть.
Центральная часть вашей программы report.py сосредоточена на чтении CSV-файлов. Например, функция read_portfolio() читает файл, содержащий строки с данными портфеля, а функция read_prices() читает файл, содержащий строки с данными о ценах. В обеих этих функциях есть много низкоуровневых "заботливых" моментов и похожих особенностей. Например, они оба открывают файл и оборачивают его с использованием модуля csv, и они оба преобразуют различные поля в новые типы.
Если бы вы часто делали разбор файлов на практике, вы, вероятно, захотели бы упорядочить это и сделать его более универсальным. Именно это и есть наша цель.
Начните выполнение этого упражнения, открыв файл fileparse.py. Именно здесь мы будем выполнять свою работу.
Упражнение 3.3: Чтение CSV-файлов
Для начала давайте сосредоточимся на задаче чтения CSV-файла в список словарей. В файле fileparse_3.3.py определите функцию, которая выглядит так:
## fileparse_3.3.py
import csv
def parse_csv(filename):
'''
Парсит CSV-файл в список записей
'''
with open(filename) as f:
rows = csv.reader(f)
## Считываем заголовки файла
headers = next(rows)
records = []
for row in rows:
if not row: ## Пропускаем строки без данных
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'}]
>>>
Это хорошо, за исключением того, что с данными нельзя производить никакие полезные вычисления, потому что все представлено в виде строк. Мы скоро это исправим, но давайте продолжим работать над этим.
Упражнение 3.4: Создание селектора столбцов
В многих случаях вам интересно только выбранные столбцы из CSV-файла, а не все данные. Измените функцию parse_csv(), чтобы она по выбору пользователя позволяла выбирать определенные столбцы следующим образом:
>>> ## Считываем все данные
>>> 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'}]
>>> ## Считываем только некоторые данные
>>> 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):
'''
Парсит CSV-файл в список записей
'''
with open(filename) as f:
rows = csv.reader(f)
## Считываем заголовки файла
headers = next(rows)
## Если был задан селектор столбцов, найдем индексы указанных столбцов.
## Также уточним набор заголовков, используемых для результирующих словарей
if select:
indices = [headers.index(colname) for colname in select]
headers = select
else:
indices = []
records = []
for row in rows:
if not row: ## Пропускаем строки без данных
continue
## Фильтруем строку, если были выбраны конкретные столбцы
if indices:
row = [ row[index] for index in indices ]
## Создаем словарь
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']
>>>
Упражнение 3.5: Выполнение преобразования типов
Измените функцию parse_csv() в директории /home/labex/project/fileparse_3.5.py так, чтобы она по выбору пользователя позволяла применять преобразования типов к возвращаемым данным. Например:
>>> 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) ]
...
Упражнение 3.6: Работа без заголовков
Некоторые CSV-файлы не содержат никакой информации о заголовках. Например, файл prices.csv выглядит так:
"AA",9.22
"AXP",24.85
"BA",44.85
"BAC",11.27
...
Измените функцию parse_csv() в /home/labex/project/fileparse_3.6.py так, чтобы она могла работать с такими файлами, создавая список кортежей вместо этого. Например:
>>> 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)]
>>>
Для внесения этих изменений вам нужно изменить код так, чтобы первая строка данных не интерпретировалась как строка заголовков. Также вам нужно убедиться, что вы не создаете словари, так как больше нет имен столбцов, которые можно использовать в качестве ключей.
Упражнение 3.7: Выбор другого разделителя столбцов
Хотя 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=' ')
Измените функцию parse_csv() в /home/labex/project/fileparse_3.7.py так, чтобы она также позволяла изменить разделитель.
Например:
>>> 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, чтобы улучшить свои навыки.