В этой лекции мы расскажем о том, что такое функции в языках программирования и для чего они нужны. Также мы рассмотрим понятия модуля и пакета, обсудим концепцию пространств имен и вкратце познакомим вас с парадигмой процедурного программирования.
Функция, также иногда называемая подпрограммой или процедурой - это фрагмент исходного кода (как правило, именованный), к которому можно обратиться из других частей программы. Можно говорить о том, что функция представляет собой маленькую законченную программу внутри основной программы (поэтому ее иногда и называют подпрограммой). Большинство функций имеют имя для того, чтобы к ним было удобно обращаться из остальных частей программы. По сути, понятие функции тесно связано с понятием переменной, только первая связывает некоторое имя с блоком исходного кода, а вторая - со значением в памяти (числовым, строковым и т.д.).
Функции в языке программирования Python создаются с помощью инструкции def
:
def function_name(parameters_list): code_block
Имя функции function_name должно быть допустимым идентификатором в языке Python. Поскольку функция как правило выполняет некоторое действие, то в ее имени рекомендуется использовать глаголы. В качестве примера можно привести такие имена функций: send_message
, get_inverted_value
, make_job
и т.д.
Функция может иметь параметры, описываемые в parameters_list, который представляет собой 0 или более переменных, разделенных запятыми. Эти переменные "видны" только внутри функции (в ее code_block) так, словно они были созданы в ней явно с помощью инструкций присваивания, и к ним нельзя обратится из других частей исходного кода.
Компонент code_block представляет собой последовательность произвольных инструкций языка программирования Python, в том числе других инструкций def
. Обратите внимание, что все инструкции в code_block должны иметь отступ относительно инструкции def
.
В любом месте внутри code_block функции можно использовать специальную инструкцию return
вместе со значением произвольного типа, с помощью которой функция может вернуть результат своей работы.
Функции используются следующим образом:
def
. Не любую последовательность инструкций имеет смысл делать функцией - как правило она должна делать небольшую и четко определенную работу, необходимость в которой может возникать в разных частях исходного кода основной программы. Процесс создания функции, а также соответствующая инструкция def
в исходном коде, часто называются определением функции.()
указывают значения для ее параметров. Это называется вызовом функции. Переменные и литералы, указанные в скобках при вызове функции называются ее аргументами.return
, интерпретатор берет значение, указанное в ней, вставляет его в место вызова функции (говорят, что функция возвращает значение), а затем продолжает выполнять программу с этой точки. Можно представлять себе это так, словно значение из инструкции return
заменило собой инструкцию вызова функции. В return
можно и не указывать никакого значения - в этом случае возвращается специальное значение None
.return
не встретилась, то интерпретатор обрабатывает code_block функции целиком, а затем выполняет действия, аналогичные тому, как если бы в конце code_block была инструкция return
без значения.В большинстве примеров в данной лекции мы используем функцию print
для вывода результатов работы наших программ на экран. Эта функция определена в самом языке Python и в качестве аргументов принимает список литералов, переменных или выражений, чьи значения затем выводит на экран, отделяя их пробелом друг от друга. Количество аргументов в вызове функции print
, а также их типы данных, может быть любым:
a = 10
b = True
# выводим 5 аргументов: строку, целое число, число с плавающей точкой,
# булевое значение и значение арифметического выражения
print('test', a, 10.3, b, a * 2 + 10)
test 10 10.3 True 30
Познакомившись с функцией print
, попробуем создать и вызвать нашу собственную функцию.
# определение функции
def print_hello(name): # name - это параметр функции, который "виден" только в ее code_block
print('hello,', name) # это code_block функции, только в нем мы можем обращаться к параметру name
# основная программа
name1 = 'Alice'
name2 = 'Bob'
print_hello(name1) # вызов функции print_hello, name1 - это аргумент, который присваивается параметру name
print_hello(name2) # вызов функции print_hello с другим аргументом (name2)
hello, Alice hello, Bob
Определение функции должно находится в исходном коде до ее вызовов. Это ограничение связано с тем, что интерпретатор выполняет исходный код последовательно сверху вниз, поэтому в момент вызова функции должен знать, какой блок кода связан с ней. В следующем пример интерпретатор генерирует исключение NameError
, так как в момент вызова функции он еще не обрабатывал ее определение:
# основная программа
print_hello_world()
# определение функции print_hello_world
# обратите внимание, что функция может не иметь параметров, однако скобки ()
# все равно должны присутствовать как в ее определении, так и в ее вызове
def print_hello_world():
print('hello, world')
--------------------------------------------------------------------------- NameError Traceback (most recent call last) <ipython-input-3-3a85752e0295> in <module>() 1 # основная программа 2 ----> 3 print_hello_world() 4 5 # определение функции print_hello_world NameError: name 'print_hello_world' is not defined
Напомним, что все переменные в Python являются ссылками. Это означает, что при инициализации параметров функции с помощью аргументов, первые начинают указывать на ту же область памяти, что и вторые. Если аргумент имеет неизменяемый тип данных, то модификация соответствующего параметра в code_block функции никак не повлияет на значение аргумента, в другом случае изменение параматра внутри функции приведет к изменению значения аргумента (подробнее об этом здесь). Поскольку пока из всех типов данных, что мы встречали, изменяемым был только тип Context
, используем его, чтобы продемонстрировать разницу между изменяемыми и неизменяемыми аргументами:
import decimal
def add_one_to_variable(var):
var += 1
print('in function: var =', var)
def add_one_to_prec(ctx):
ctx.prec += 1
print('in function: ctx.prec =', ctx.prec)
var = 1
ctx = decimal.getcontext()
print('before function: var =', var)
add_one_to_variable(var)
print('after function: var =', var)
print('before function: ctx.prec =', ctx.prec)
add_one_to_prec(ctx)
print('after function: ctx.prec =', ctx.prec)
before function: var = 1 in function: var = 2 after function: var = 1 before function: ctx.prec = 28 in function: ctx.prec = 29 after function: ctx.prec = 29
Как видите, модификация неизменяемого типа внутри функции видна только в ее code_block, в то время как модификация изменямого остается в силе и после возврата из функции.
В примерах, которые были только что рассмотрены, выигрыш от использования функций небольшой - можно было бы просто вместо них везде писать их code_block. Рассмотрим более полезную функцию, вычисляющую площадь треугольника по длинам его сторон (формула Герона):
def calc_triangle_area(a, b, c):
print('a =', a, ', b =', b, ', c =', c)
p = (a + b + c) / 2
return (p * (p - a) * (p - b) * (p - c)) ** 0.5
area = calc_triangle_area(3, 4, 5)
print('area =', area)
a = 3 , b = 4 , c = 5 area = 6.0
В этой функции мы видим инструкцию return
, возвращающую значение выражения для площади треугольника, которое затем присваивается переменной area
.
Аргументы, с которыми вызывается функция calc_triangle_area
в примере выше называются позиционными - они присиваиваются параметрам в том порядке, в котором указаны в инструкции вызова функции, то есть параметр a
становится равен трем, b
четырем, а c
- пяти.
При вызове функции также можно использовать именованные аргументы, которые явно указывают, какому параметру какое значение нужно присвоить. Именованные аргументы могут идти в любом порядке, и даже использоваться вместе с позиционными, правда обязательно после них. Какую бы комбинацию именованных и позиционных аргументов вы ни использовали, важно, чтобы в итоге все параметры были проинициализированы, или будет сгенерировано исключение.
# во всех примерах ниже параметры функции calc_triangle_area: a = 5, b = 10, c = 7
# позиционные аргументы
print('area1 =', calc_triangle_area(5, 10, 7))
# именованные аргументы
print('area2 =', calc_triangle_area(c=7, a=5, b=10))
# позиционны и именованные аргументы
print('area3 =', calc_triangle_area(5, c=7, b=10))
a = 5 , b = 10 , c = 7 area1 = 16.24807680927192 a = 5 , b = 10 , c = 7 area2 = 16.24807680927192 a = 5 , b = 10 , c = 7 area3 = 16.24807680927192
Приведем также примеры некорректных вызовов функции calc_triangle_area
:
calc_triangle_area(a=1, 2, 3) # позиционный аргумент идет после именованного
calc_triangle_area(1, b=2) # слишком мало аргументов
calc_triangle_area(a=1, b=2, c=3, d=4) # неизвестный параметр d
File "<ipython-input-7-82fee3c36ab6>", line 1 calc_triangle_area(a=1, 2, 3) # позиционный аргумент идет после именованного ^ SyntaxError: positional argument follows keyword argument
В нашей текущей реализации функции calc_triangle_area
не учтен тот факт, что не всякие три числа могут быть использованы в качестве длин сторон треугольника, поэтому давайте добавим в нее соответствующую проверку. Если параметры корректны, то мы будем высчитывать и возвращать площадь треугольника, а в случае ошибки - возвращать специальное значение None
, которое позволит обнаружить ее в том месте, где вызывается наша функция. Этот прием (использование None
в качестве результата в случае ошибки) очень распространен и встречается во многих функциях в Python.
def calc_triangle_area(a, b, c):
# из школьного курса геометрии известно, что сумма любых двух
# сторон треугольника должна быть строго больше третьей стороны
if a + b <= c or a + c <= b or b + c <= a:
return None # в принципе, можно было написать просто "return", но так понятнее
p = (a + b + c) / 2
return (p * (p - a) * (p - b) * (p - c)) ** 0.5
# длины сторон треугольника
a = 10
b = 11
c = 12
# вычисляем площадь (здесь a, b, c - позиционные аргументы, с помощью которых будут
# проинициализированны параметры функции)
area = calc_triangle_area(a, b, c)
# проверяем, что не произошло ошибки
if area is not None:
print('area =', area)
else:
print('bad triangle sides!')
area = 51.521233486786784
Обратите внимание, что для проверки того, равна ли переменная area
специальному значению None
, следует использовать операцию is
или is not
, а не ==
.
Убедимся, что наша программа работает правильно и при некорректных значениях длин сторон треугольника:
a = 1
b = 1
c = 5
area = calc_triangle_area(a, b, c)
if area is not None:
print('area =', area)
else:
print('bad triangle sides!')
bad triangle sides!
Заметим, что внутри функции может находиться определение другой функции. Такие функции иногда называют локальными, потому что их можно вызвать только в code_block функции, в которой они определены. В противовес этому, обычные функции, которые мы рассматривали выше, называют глобальными, потому что они могут использоваться в любой части исходного кода программы. Перепишем наш пример с использованием локальной функции:
def calc_triangle_area(a, b, c): # глобальная функция
def get_half_perimeter(): # локальная функция
return (a + b + c) / 2 # внутри локальной функции видны параметры глобальной!
if a + b <= c or a + c <= b or b + c <= a:
return None
p = get_half_perimeter() # вызываем локальную функцию
return (p * (p - a) * (p - b) * (p - c)) ** 0.5
calc_triangle_area(3, 7, 9)
8.78564169540279
При попытке вызвать локальную функцию вне глобальной, в которой она определена, мы получим уже хорошо знакомую нам ошибку NameError
:
get_half_perimeter()
--------------------------------------------------------------------------- NameError Traceback (most recent call last) <ipython-input-11-b71b920a0cad> in <module>() ----> 1 get_half_perimeter() NameError: name 'get_half_perimeter' is not defined
При определении функции с помощью инструкции def
для некоторых параметров можно указать значение по умолчанию. Такие параметры становятся необязательными, то есть при вызове функции можно не указывать аргументы для них, при этом параметры инициализируются своими значениями по умолчанию. Рассмотрим функцию, которая укорачивает произвольный текст до определенного количества символов, добавляя в конец индикатор того, что текст бы сокращен. В примере мы воспользуемся встроенной в Python функцией len
, которая принимает в качестве параметра строку и возвращает количество символов в ней.
def get_short_string(text, length=15, indicator='...'):
if len(text) <= length:
return text
indicator_len = len(indicator)
result = text[:length-indicator_len] + indicator
return result
У функции get_short_string
есть два необязательных параметра length
и indicator
, для которых задано значение по умолчанию (указывается с помощью знака =
в определении функции def
), а значит при ее вызове их можно не указывать:
text = 'To be or not to be, that is the question'
print(get_short_string(text, 20, '***')) # явно задаем значения для всех параметров
print(get_short_string(text, length=10)) # indicator будет иметь значение по умолчанию
print(get_short_string(text, indicator='***')) # length будет иметь значение по умолчанию
print(get_short_string(text)) # length и indicator будут иметь значение по умолчанию
To be or not to b*** To be o... To be or not*** To be or not...
Значения по умолчанию следует использовать для тех параметров функции, которые принимают как правило одно и то же значение. В этом случае функцией становится удобнее пользоваться: при ее вызове нужно передать лишь несколько аргументов, а остальные параметры будут автоматически проинициализированы значениями по умолчанию. При этом, в тех редких случаях, когда значение по умолчанию некоторого параметра нас не устраивает, мы можем явно указать нужный аргумент.
Если вы хотите сделать несколько параметров своей функции необязательными (т.е. имеющими значение по умолчанию), все их нужно указывать после обязательных параметров в инструкции def
, например, такое определение является некорректным:
def my_function(param1, param2=0, param3):
print('my function')
File "<ipython-input-14-29fa1a732243>", line 1 def my_function(param1, param2=0, param3): ^ SyntaxError: non-default argument follows default argument
Функции можно присваивать переменным, а затем вызывать их через эти переменные:
def decorate_text(text):
return '---' + text + '---'
a = decorate_text
decorated_text = a('hello')
print(decorated_text)
---hello---
Этот прием позволяет создавать интересные конструкции, например, использовать функцию в качестве параметра другой функции. Рассмотрим такой пример:
def decorate_text1(text):
return '---' + text + '---'
def decorate_text2(text):
return '***' + text + '***'
def decorate_text3(text):
return '+++' + text + '+++'
def print_hello(name, decorator=decorate_text1):
hello_text = 'hello, ' + name
hello_text = decorator(hello_text)
print(hello_text)
print_hello('Alice')
print_hello('Bob', decorate_text2)
---hello, Alice--- ***hello, Bob***
В языке Python можно создавать функции, которые вызывают сами себя. Такие функции называются рекурсивными, и они ничем не отличаются от обычных, кроме того, что в их code_block содержится инструкция вызова их самих. Рекурсивные функции являются элегантным способом реализации многих алгоритмов.
Рассмотрим простой пример функции, которая возводит число в некоторую целую неотрицательную степень:
def calc_power(num, exponent):
result = 1
while exponent > 0:
result *= num
exponent -= 1
return result
calc_power(5, 2)
25
А теперь давайте перепишем эту функцию, используя рекурсию. Мы знаем, что $x^n$ можно представить в виде $x*(x^{n-1})$, то есть вычисление степени $n$ можно свести к вычислению степени $n-1$. После этого эти же рассуждения можно применить и к выражению $x^{n-1}$ и так далее. Такую последовательность вычислений можно реализовать в виде рекурсивной функции (обратите внимание, что $x^0$ это 1):
def calc_power(num, exponent):
if exponent == 0:
return 1
return num * calc_power(num, exponent - 1)
calc_power(5, 2)
25
Давайте рассмотрим, как выполнялась наша программа:
calc_power(5, 2)
- при этом вызове параметр exponent
не равен 0, поэтому первое условие if
не выполняется, и мы переходим к строке return num * calc_power(num, exponent - 1)
. Чтобы вычислить результат умножения, интерпретатор должен знать значение обоих операндов, поэтому перед выполнением операции *
он обрабатывает вызов функции calc_power
с аргументами 5 и 1.calc_power(5, 1)
- все то же самое, в итоге происходит вызов calc_power(5, 0)
.calc_power(5, 0)
- выполняется условие if
, поэтому функция сразу возвращает 1. До этого момента мы как бы "погружались" внутрь нашей рекурсии, а теперь начнем подниматься на повехность.calc_power(5, 1)
- теперь интерпретатор знает значения обоих операндов, и возвращает из функции результат выражения 5 * 1
, то есть число 5.calc_power(5, 2)
- аналогично предыдущему пункту, интерпретатор возвращает в основную программу результат выражения 5 * 5
.Лямбда-функция представляет собой анонимную функцию, создаваемую следующей инструкцией:
lambda parameters_list: expression
Как и для обычных функций, parameters_list представляет собой 0 или более переменных, разделенных запятыми. Для них можно указывать значение по умолчанию по тем же правилам, что и для обычных функций.
Expression представляет собой некоторое выражение языка программирования Python, например, арифметическое, логическое или условное.
Приведем простой пример лямбда-функции, которая возвращает модуль числа:
abs_value = lambda x: x if x >= 0 else -x # используем условное выражение в качестве лямбда-функции
print(abs_value(5))
print(abs_value(-5))
5 5
Рассмотрим последний пример из предыдущего раздела. Хорошая функция должна представлять собой законченную небольшую программу, которая может многократно использоваться в других участках кода (подробнее об этом читайте ниже в разделе Процедурное программирование). Сказать это о функциях decorate_text
мы не можем - они используются лишь как параметры для функции print_hello
, и вряд ли понадобятся где-либо еще. Поэтому этот пример лучше переписать с использованием лямбда-функций:
# обратите внимание на то, как мы используем лямбда-функцию в качестве значения по умолчанию
def print_hello(name, decorator=lambda text: '---' + text + '---'):
hello_text = 'hello, ' + name
hello_text = decorator(hello_text)
print(hello_text)
print_hello('Alice')
---hello, Alice---
Если нам потребуется любой другой способ декорирования текста, мы можем указать его прямо при вызове функции print_hello
:
print_hello('Bob', lambda text: '***' + text + '***')
print_hello('Cooper', lambda text: '===' + text + '===')
***hello, Bob*** ===hello, Cooper===
Перед тем, как мы начнем изучать модули, давайте посмотрим, как еще можно создать программу на языке Python, не используя среду разработки Jupyter Notebook. На самом деле, все что нам нужно - это любой текстовый редактор и интерпретатор Python. При установке дистрибутива Anaconda интерпретатор уже был установлен, а в качестве текстового редактора можно использовать стандартный блокнот в ОС Windows. Запустим его и напишем в нем простейшую программу:
Теперь сохраним этот файл и запомним путь к нему. По общепринятому соглашению, файл с исходным кодом на языке Python должен иметь расширение .py, поэтому в качестве имени используем, например, hello.py.
Для того, чтобы выполнить эту программу, нужно запустить интерпретатор Python и передать ему в командной строке наш файл hello.py. Для начала однако давайте убедимся, что интерпретатор Python присутствует в нашей системе. Для этого запустим командную строку Windows и выполним команду "python --version". Вы должны увидеть примерно следующее:
Если вместо этого было выведено сообщение о том, что "python" является неизвестной командой, то возможно одно из двух: либо интерпретатор отсутствует в системе, либо путь к нему не прописан в специальной переменной среды Path. Если вы правильно установили дистрибутив Anaconda, никаких ошибок быть не должно.
Последним шагом является вызов интерпретатора и передача ему в командной строке нашего файла с программой. Для удобства, мы перейдем в папку, куда был сохранен файл hello.py (в нашем случае - c:/python), чтобы в дальнейшем не нужно было писать полный путь до него:
В принципе, можно сказать, что мы уже создали наш первый модуль, потому что любой файл с расширением .py, содержащий исходный код на языке Python, может им считаться. Однако, чтобы быть полезным, модуль обычно содержит определенный набор пременных, функций и различных сложных типов данных (о которых пойдет речь в лекции, посвященной классам), предназначенных для решения определенного класса задач. Такие модули затем можно использовать в своих программах и получать доступ к функциональности, реализованной в них. При этом говорят, что модуль экспортирует функциональность (константы, функции и т.д.), а программа, которая использует модуль, импортирует ее.
Существует огромное количество модулей для языка Python. Некоторые из них являются частью так называемой стандартной библиотеки Python, реализованной создателями языка и состоящей из множества модулей, предоставляющих средства для работы с операционной системой и файлами, сетевыми протоколами, криптографическими алгоритмами и многое другое. Другие реализуются сторонними разработчиками для узкого класса задач. Обилие существующих модулей является сильной стороной языка Python: необходимость реализовывать какой-нибудь сложный алгоритм возникает очень редко, потому что практически всегда удается найти модуль, в котором эта функциональность уже присутствует, и просто использовать его.
Давайте создадим собственный модуль, на этот раз более полезный, чем предыдущий hello.py. Мы поместим в него функции для вычисления площади некоторых геометрических фигур, а также константу $\pi$.
PI = 3.141592
def calc_triangle_area(a, b, c):
p = (a + b + c) / 2
return (p * (p - a) * (p - b) * (p - c)) ** 0.5
def calc_rectangle_area(a, b):
return a * b
def calc_circle_area(r):
return PI * (r ** 2)
Чтобы вынести этот код в отдельный модуль, создадим в блокноте файл area.py и скопируем код туда.
Для того, чтобы получить доступ к функциям из модуля area
его нужно импортировать в нашу программу с помощью инструкции import
(мы уже встречались с ней раньше, когда импортировали модуль decimal из стандратной библиотеки Python). Заметим, что все инструкции import
рекомендуется размещать в самом начале программы.
import area
--------------------------------------------------------------------------- ModuleNotFoundError Traceback (most recent call last) <ipython-input-23-bd7db151cf20> in <module>() ----> 1 import area ModuleNotFoundError: No module named 'area'
Как видите, интерпретатор не смог выполнить инструкцию import
и сгенерировал исключение. Это произошло потому, что интерпретатору не удалось найти файл area.py, ведь ему неизвестно, куда именно мы сохранили его. Полный список правил, в соответствии с которыми интерпретатор ищет файл модуля, достаточно обширен, и мы не будем подробно на нем останавливаться; скажем лишь, что интерпретатор обязательно проверяет папку, где лежит файл программы (в нашем случае - ноутбук-файл .ipynb), которая импортирует модуль. Давайте скопируем в эту папку файл area.py и попробуем выполнить инструкцию заново.
import area
Теперь никакой ошибки нет, и мы можем обращаться к функциям и переменным из подключенного модуля. Обратите внимание, что для этого обязательно нужно указывать имя модуля:
radius = 5
side = 10
print(area.calc_circle_area(radius))
print(area.calc_rectangle_area(side, side))
print(area.PI)
78.5398 100 3.141592
Иногда бывает полезно указать другое имя, по которому вы будете обращаться к модулю из своей программы. Для этого в инструкции import
можно использовать необязательное предложение as
, после которого указывается альтернативное имя модуля:
import area as calc_area
print(calc_area.calc_triangle_area(3, 5, 7))
6.49519052838329
Наконец, с помощью инструкции from ... import
можно импортировать определенные индентификаторы из модуля, а не весь модуль. К импортированным таким образом переменным, функциям или сложным типам данных можно обращаться, не указывая имя модуля:
from area import PI, calc_triangle_area
print(PI)
print(calc_triangle_area(5, 5, 5))
3.141592 10.825317547305483
Можно даже указать собственные имена, по которым вы хотите обращаться к импортированным идентификаторам:
from area import calc_rectangle_area as r_area
print(r_area(1, 2))
2
Наконец, в инструкции from ... import
можно указать, что нужно импортировать всё, что есть в модуле:
from area import * # из модуля area будут импортированы все переменные, функции и т.д.
print(calc_circle_area(3))
28.274328
Несмотря на то, что использовать идентификаторы, импортированные с помощью инструкции from ... import
, более удобно (поскольку не нужно писать постоянно имя модуля), применять ее стоит аккуратно, особенно вместе со специальным символом *
. Причина этого станет понятна после того, как мы поговорим о пространстве имен далее в этой лекции.
Пакет в Python представляет собой именованный набор из нескольких модулей, которые связаны по смыслу друг с другом и предоставляют функциональность для конкретного класса задач. Например, можно представить себе некий пакет Images
, содержащий модули Jpeg
, Gif
и Png
для работы с разными форматами изображений.
На уровне интерпретатора Python, пакет это просто каталог в файловой системе, отвечающий одному требованию: в нем обязан присутствовать файл *__init__.py. Этот файл может быть пустым, или содержать произвольный исходный код на языке Python, который выполняется интерпретатором, когда что-то из пакета импортируется в программу впервые. Код в файле __init__.py* обычно используется для того, чтобы инициализировать некоторые общие для всех модулей пакета константы.
Чтобы интерпретатор при импортировании модулей из пакета мог найти пакет, соответствующий пакету каталог должен находиться в одном из предопределенных мест в файловой системе. Самый простой способ - поместить каталог пакета в папку, где находятся ваши ноутбук-файлы .ipynb.
Давайте создадим свой пакет geometry
, в который мы поместим наш модуль area
, реализованный ранее. После создания каталога ./geometry и копирования в него файла area.py, не забудем добавить туда же пустой файл __init.py__. Если все сделано правильно, то мы сможем импортировать модули из пакета с помощью инструкции import
, указывая перед именем модуля имя пакета, в котором он содержится:
import geometry.area
print(geometry.area.PI)
3.141592
В инструкции import
с помощью предложения as
можно задавать альтернативное имя как для простых модулей, так и для модулей, содержащихся в пакете:
import geometry.area as calc_area
print(calc_area.calc_circle_area(6))
113.097312
Чтобы не писать каждый раз при обращении к модулю пакета имя пакета, можно воспользоваться инструкцией from ... import
, которая работает точно так же, как и для простых модулей:
# импортируем модуль area из пакета geometry и делаем его доступным в нашей программе напрямую
from geometry import area
print(area.calc_triangle_area(2, 5, 6))
4.683748498798798
Наконец, мы можем импортировать сразу конкретный идентификатор из определенного модуля в пакете таким образом:
from geometry.area import PI
print(PI)
3.141592
Как вам уже известно, любые переменные и функции (а также классы и объекты, о которых мы поговорим в следующей лекции), обладают своим собственным именем или идентификатором. Для того, чтобы правильно обработать инструкцию, в которой происходит обращение к некоторому имени, интерпретатор должен однозначно определить, на какой объект оно указывает. Например, если вы пишете инструкцию a + b
, интерпретатор должен определить, на какое значение в памяти указывают имена a
и b
, чтобы ее выполнить. Очевидно, что никакой неоднозначности при этом быть не должно - иначе программа будет работать непредсказуемым образом. Ситуация, когда одно имя ссылается на несколько объектов, в программировании называется конфликтом имен. Простейший способ устранить их - заставить программистов использовать уникальные имена для всего, что они создают. Две очевидные причины, из-за которых такой подход не работает в реальности, это:
Проблема, связанная с возможным конфликтом имен, в различных языках программирования (в том числе, Python) решается с помощью пространств имен, которые представляют собой некое отображение имен на объекты, на которые они указывают. В двух различных пространствах имен могут находиться одинаковые идентификаторы, указывающие на разные объекты, а интерпретатор или компилятор используют дополнительную информацию о структуре программы для того, чтобы определить, имя из какого пространства использовать в том или ином выражении.
print
, type
и т.д.) и типов (int
, bool
и т.д.).Во время выполнения программы несколько пространств имен существуют одновременно, образуя следующую иерархию:
Рассмотрим пример и рисунок, поясняющий в каком пространстве имен содержится то или иное имя:
import decimal as precise_number
from area import PI
var = 10 # определено вне любой функции, такие переменные называются глобальными
def global_function():
var = 20
def local_function():
var = 30
Из рисунка хорошо видно отличие между инструкциями import
и from ... import
:
import
интерпретатор создает пространство имен модуля decimal
, на которое ссылается добавленное в пространство имен нашей программы имя precise_number
. Как видите, в пространстве имен программы нет идентификаторов из модуля decimal
, однако мы можем обращаться к ним через имя precise_number
(например, precise_number.getcontext()
).from ... import
интерепретатор не создал никакого пространства имен для модуля area
, однако добавил к пространству имен нашей программы идентификатор PI
из него. Поэтому к переменной PI
из модуля area
мы обращаемся напрямую, словно она определена в нашей программе, но не имеем никакой возможности обратиться к другим идентификаторам модуля area
.Продолжим анализ рисунка. Обратим внимание, что каждое имя var
в программе находится в своем пространстве имен, и при этом указывает на разные значения в памяти. Когда в программе происходит обращение к идентификатору var
, интерпретатор определяет, в какой части программы оно находится, и использует соответствующее пространство имен в первую очередь. Если в нем идентификатор не найден, то он пытается найти его в объемлющем пространстве имен и так далее, вплоть до встроенного пространства имен. Если и там идентификатор не будет найден, интерпретатор сгенерирует исключение NameError
.
Давайте проверим, как это работает на практике:
var = 10
def global_function():
var = 20
def local_function():
var = 30
print(var)
var = 300
local_function()
print(var)
var = 200
global_function()
print(var)
30 20 10
Как видите, внутри функции local_function
переменная var
указывает на область памяти со значением 30, причем если его изменить, то это никак не повлияет на переменные var
из пространства имен программы или функции global_function
. Если бы переменная var
не была определена в local_function
, то интерпретатор пытался бы найти это имя в пространстве имен global_function
, а затем - в пространстве имен программы.
Существует две инструкции, с помощью которых можно изменить порядок, в котором интерпретатор просматривает пространства имен в поисках идентификатора:
global
- говорит интерпретатору, что имя нужно искать в глобальном пространстве именnonlocal
- имеет смысл только для вложенных функций, говорит интерпретатору, что имя нужно искать во внешней функцииРассмотрим пример, в котором мы пытаемся изменить значение глобальной переменной из функции:
var = 1
def change_var():
var = 2
change_var()
print(var)
1
Как видите, наш способ не сработал - значение var
не изменилось. Причина этого в том, что инструкция присваивания внутри функции change_var
выделяет область памяти для нового значения, однако заставляет ссылаться на него имя var
из локального пространства имен, а такое же имя из глобального пространства имен продолжает ссылаться на старое значение. Чтобы исправить это, нам нужно указать интерпретатору, что он должен использовать имя var
из глобального пространства имен:
var = 1
def change_var():
global var # использовать var из глобальной области видимости
var = 2
change_var()
print(var)
2
Для похожей цели используется и инструкция nonlocal
, только по отношению к вложенным друг в друга функциям:
def external_function():
var = 1
def internal_function():
nonlocal var # даем указание интерпретатору искать имя `var` во внешней функции
var = 2
internal_function()
print(var)
external_function()
2
В заключение упомянем встроенную функцию dir
, которая возвращает список идентификаторов, присутствующих в указанном пространстве имен. Если вызывать ее без аргументов, то будут возвращены идентификаторы из текущего (самого внутреннего с точки зрения иерархии) пространства имен. Рассмотрим пример:
def my_function():
var1 = 0
var2 = 'False'
print(dir()) # выведет имена из пространства имен функции my_function
print(dir(), '\n') # выведет имена из пространства имен программы
my_function()
['In', 'Out', '_', '__', '___', '__builtin__', '__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__', '_dh', '_i', '_i1', '_ih', '_ii', '_iii', '_oh', 'exit', 'get_ipython', 'my_function', 'quit'] ['var1', 'var2']
print(dir(__builtins__))
['ArithmeticError', 'AssertionError', 'AttributeError', 'BaseException', 'BlockingIOError', 'BrokenPipeError', 'BufferError', 'BytesWarning', 'ChildProcessError', 'ConnectionAbortedError', 'ConnectionError', 'ConnectionRefusedError', 'ConnectionResetError', 'DeprecationWarning', 'EOFError', 'Ellipsis', 'EnvironmentError', 'Exception', 'False', 'FileExistsError', 'FileNotFoundError', 'FloatingPointError', 'FutureWarning', 'GeneratorExit', 'IOError', 'ImportError', 'ImportWarning', 'IndentationError', 'IndexError', 'InterruptedError', 'IsADirectoryError', 'KeyError', 'KeyboardInterrupt', 'LookupError', 'MemoryError', 'ModuleNotFoundError', 'NameError', 'None', 'NotADirectoryError', 'NotImplemented', 'NotImplementedError', 'OSError', 'OverflowError', 'PendingDeprecationWarning', 'PermissionError', 'ProcessLookupError', 'RecursionError', 'ReferenceError', 'ResourceWarning', 'RuntimeError', 'RuntimeWarning', 'StopAsyncIteration', 'StopIteration', 'SyntaxError', 'SyntaxWarning', 'SystemError', 'SystemExit', 'TabError', 'TimeoutError', 'True', 'TypeError', 'UnboundLocalError', 'UnicodeDecodeError', 'UnicodeEncodeError', 'UnicodeError', 'UnicodeTranslateError', 'UnicodeWarning', 'UserWarning', 'ValueError', 'Warning', 'WindowsError', 'ZeroDivisionError', '__IPYTHON__', '__build_class__', '__debug__', '__doc__', '__import__', '__loader__', '__name__', '__package__', '__spec__', 'abs', 'all', 'any', 'ascii', 'bin', 'bool', 'bytearray', 'bytes', 'callable', 'chr', 'classmethod', 'compile', 'complex', 'copyright', 'credits', 'delattr', 'dict', 'dir', 'display', 'divmod', 'enumerate', 'eval', 'exec', 'filter', 'float', 'format', 'frozenset', 'get_ipython', 'getattr', 'globals', 'hasattr', 'hash', 'help', 'hex', 'id', 'input', 'int', 'isinstance', 'issubclass', 'iter', 'len', 'license', 'list', 'locals', 'map', 'max', 'memoryview', 'min', 'next', 'object', 'oct', 'open', 'ord', 'pow', 'print', 'property', 'range', 'repr', 'reversed', 'round', 'set', 'setattr', 'slice', 'sorted', 'staticmethod', 'str', 'sum', 'super', 'tuple', 'type', 'vars', 'zip']
Аналогичным образом можно посмотреть идентификаторы, определенные в некотором модуле:
import area
print(dir(area))
['PI', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'calc_circle_area', 'calc_rectangle_area', 'calc_triangle_area']
Разработка любой более-менее сложной программы начинается с этапов анализа и проектирования. Первый нужен для того, чтобы понять, какая функциональность потребуется от программы, а второй - как разбить ее на множество более мелких задач, из которых в свою очередь выделить еще более мелкие и так далее. При таком подходе итоговая программа представляет собой некий конструктор, собранный из значительно более простых деталей.
Такая декомпозиция очень важна для процесса создания программы, а также дальнейшего ее сопровождения. Объясним это на примере сборки автомобиля:
В процедурном программировании в качестве элементов, на которые разбивается программа, используются пакеты, модули и функции, которые обрабатывают некоторые данные. Поскольку функции представляют собой некоторые действия, декомпозиция сложной задачи осуществляется по тем операциям, которые в ней присутствуют.
В качестве примера рассмотрим интернет-браузер. Очевидно, что две основные его подсистемы - это работа с сетью и отображение веб-страниц. Поэтому мы можем создать два пакета network
и display
, в которые будем помещать наши модули. В пакет network
у нас попадут модули, реализующие различные сетевые протоколы, например HTTP, SSL и другие. Эти модули, вероятно, будут содержать функции connect
, send
, receive
, disconnect
и другие. В пакет display
мы добавим модули для поддержки HTML, CSS, Flash и т.д. В них попадут такие функции, как show_page
, run_flash_player
и другие.
В заключение скажем, что этап проектирования является самой важной и сложной частью в процессе создания крупных программ, требующей немало опыта и творческих усилий. Если на этом этапе разработчик не учтет что-то важное, в итоговой программе может наблюдаться целый ворох проблем: низкая производительность, большое количество ошибок, трудность модификации и т.д.
get_word_count
, которая возвращает количество слов в строке (подсказка - для простоты считайте, что слова разделяются одним или более символом пробела).geometry
, рассмотренный в лекции, модуль perimeter
, содержащий 3 функции: для вычисления периметра треугольника и прямоугольника, а также длины окружности. Обратите внимание, что в лекции модуль area
содержал константу PI
, которая потребуется вам и в модуле perimeter
. Ни в коем случае не создавайте ее там повторно, вместо этого подумайте, как сделать так, чтобы определить PI
в одном месте в пакете geometry
и использовать из всех модулей, входящих в него. Возможно несколько решений.