1. Лекция 2

28 сентября 2018 г.

Заметили ошибку или есть предложение? Напишите на почту: romansdidnotcrucify@gmail.com

2. Функции

Внезапно, мы начнём говорить о функциях ещё до того, как возникла тема условных операторов и циклов. Так сделано в учебнике, на котором основан курс, и вообще идея интересная: рассказывать про повторное использование кода до того, как рассказывать о том, как этот код писать. Тем более что мы с вами уже знакомы с понятием пространства имён, а что такое функция в питоне как не динамическое создание пространства имён?

2.1. Особенности понятия функции в Python

В большинстве языков программирования функцией (в большей или меньшей степени) считается некая математизированная абстракция, преобразование данных одного конкретного типа в данные другого конкретного типа. В питоне функция - нечто совсем другое.

Что это за объекты - вообще говоря, всё равно; лишь бы эти операции на них выполнялись. Вспоминаем утиную (неявную динамическую) типизацию.

2.2. (Встроенные) функции

Некоторые из них мы с вами уже видели. Например, функция print:

   1 >>> print("Hi there")
   2 Hi there
   3 

Она выводит через пробел какие-нибудь питоновские объекты:

   1 >>> print("We are number", 1) # Кстати сказать, абсолютно любые
   2 We are number 1
   3 >>> print("We are number",1, print)    
   4 We are number 1 <built-in function print>
   5 

Обратите внимание, что, как и полагается в питоне, любой объект может преобразован в строку и выведен, даже функция:

   1 >>> print
   2 <built-in function print>
   3 

Мы уже знаем и другие примеры функций:

   1 >>> min(42,100500,47,666)    # Guess what
   2 42    # Как мы и думали
   3 >>> min
   4 <built-in function min>
   5 

Как видите, min - тоже функция, причём встроенная (built-in). Она лежит в каком-то волшебном пространстве имён:

   1 >>> dir(__builtins__)    # dir(), напоминаю, показывает содержимое пространства имён. __builtins__ - ну очень волшебный namespace
   2 ['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', '_', '__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', 'divmod', 'enumerate', 'eval', 'exec', 'exit', 'filter', 'float', 'format', 'frozenset', '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', 'quit', 'range', 'repr', 'reversed', 'round', 'set', 'setattr', 'slice', 'sorted', 'staticmethod', 'str', 'sum', 'super', 'tuple', 'type', 'vars', 'zip']
   3 
   4 # Видим, что здесь и min, и print (ищите ближе к концу).
   5 # Это, кстати, пространство имён встроенных объектов, которые есть всегда.
   6 # Добрую половину из них занимают исключения (см. верхнюю часть вывода выше).
   7 # Изрядная часть того, что есть внизу - преобразования типов к разным встроенным питоновским типам.
   8 # Остаются как раз те функции, про которые мы с вами разговариваем.
   9 

Обсуждали в прошлый раз help - это тоже функция, как и dir:

   1 >>> help()
   2 
   3 Welcome to Python 3.6's help utility!
   4 
   5 If this is your first time using Python, you should definitely check out
   6 the tutorial on the Internet at https://docs.python.org/3.6/tutorial/.
   7 
   8 Enter the name of any module, keyword, or topic to get help on writing
   9 Python programs and using Python modules.  To quit this help utility and
  10 return to the interpreter, just type "quit".
  11 
  12 To get a list of available modules, keywords, symbols, or topics, type
  13 "modules", "keywords", "symbols", or "topics".  Each module also comes
  14 with a one-line summary of what it does; to list the modules whose name
  15 or summary contain a given string such as "spam", type "modules spam".
  16 
  17 help> q
  18 
  19 You are now leaving help and returning to the Python interpreter.
  20 If you want to ask for help on a particular object directly from the
  21 interpreter, you can type "help(object)".  Executing "help('string')"
  22 has the same effect as typing a particular string at the help> prompt.
  23 >>> dir    # Посмотрим на тип dir
  24 <built-in function dir>
  25 

Есть функции преобразования числа в различные представления (обратите внимание, что возвращается строка):

   1 >>> bin(12)    # Бинарное (двоичное)
   2 '0b1100'
   3 >>> hex(32)    # Шестнадцатеричное
   4 '0x20'
   5 >>> oct(64)    # Восьмеричное
   6 '0o100'
   7 

Замечу, что работает и обратное преобразование:

   1 >>> 0b10101010    # В отличие от C, в python есть и двоичное представление числа, что довольно удобно в некоторых алгоритмах
   2 170
   3 >>> 0xdeadbeef
   4 3735928559
   5 >>> 0o1232
   6 666
   7 

2.3. Callables (вызываемые объекты)

В питоне возможность вызывать нечто не прибита гвоздями к тому, что это нечто - функция. Например, преобразование типов - вызываемый объект, однако оно не является функцией; это класс:

   1 >>> int(1234)    # Вызываем конструктор класса
   2 1234
   3 >>> int("1234")    # Неудивительно, что int() можно передать объекты разных типов
   4 1234
   5 >>> int    # Убеждаемся, что int - класс
   6 <class 'int'>
   7 

Для проверки того, что объект вызываемый (callable), что его можно вызвать, применяется специальная функция callable:

   1 >>> callable(int)
   2 True
   3 >>> callable(print)
   4 True
   5 >>> a="Saul"    # Строка не является вызываемым объектом
   6 >>> a
   7 'Saul'
   8 >>> type(a)
   9 <class 'str'>
  10 >>> callable(a)
  11 False
  12 

Кстати, знаете какого типа используемое выше type?

   1 >>> type(type)
   2 <class 'type'>
   3 

Это класс. Из чего следует довольно забавный факт: type() возвращает нам не строчку с именем типа, а сам тип:

   1 >>> c=type(123)    # Мы поместили в 'c' объект типа "класс", а именно int
   2 >>> c
   3 <class 'int'>
   4 >>> c("179")    # А, значит, можно вызвать конструктор класса
   5 179
   6 >>> callable(c)    # callable авторитетно подтверждает очевидное
   7 True
   8 

Позже этот факт нам очень пригодится при изучении объектной модели питона.

2.4. Как задаётся функция

А очень просто - указывается ключевое слово def, за ним - имя функции, затем в скобках через запятую - формальные параметры, и двоеточие:

   1 >>> def fun(a, b):    # Тело пока не завезли

Затем на последующих строках делается обязательный отступ в 4 пробела. Он обозначает, что мы всё ещё находимся в теле функции. Уберём в строчке отступ - из тела функции вышли. Поставили не 4 пробела - получим ошибку. Допишем функцию:

   1 >>> def fun(a, b):    # Частая ошибка программистов на C/C++, изучающих python - забывать про двоеточие
   2 ...     return a*2 + b    # Обратите внимание, подсказка изменилась с '>>>' на '...'; интерпретатор понимает, что мы продолжаем вводить команду
   3 ...    # Не ставим пробелов, просто жмём Enter
   4 >>> dir()     # Убедимся, что fun теперь есть в пространстве имён
   5 ['__annotations__', '__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__', 'a', 'c', 'fun']
   6 >>> fun    # И что это функция
   7 <function fun at 0x0000025B8E53E268>
   8 >>> type(fun)    # В свою очередь, "функция" в питоне - это класс...
   9 <class 'function'>
  10 >>> fun(3,7)    # Наша новая функция вполне работает
  11 13
  12 

2.5. Что происходит на самом деле

Рассказывать подробно, что такое формальные и фактические параметры, я не буду. Вот как устроена работа с ними в питоне:

  1. объявили функцию;
  2. в момент, когда мы в неё входим, создаётся локальное пространство имён (по умолчанию оно пустое, содержит только некоторые вспомогательные штуки);
  3. поскольку у нашей функции два параметра, в этом пространстве имён сразу заводятся два имени - a и b;
  4. пока происходит выполнение функции, в этом локальном пространстве доступны как глобальные имена, объявленные извне функции, так и локальные (в данном случае - a и b);
  5. когда возникает необходимость выполнить return (выйти из функции), подготавливается возвращаемое значение (результат выражения справа от return). Это значение - единственный из локальных объектов, который выживет после окончания функции; (естественно, если на остальные локальные объекты не было ссылок извне);

  6. имена локальных объектов (a и b) удалятся, и сами локальные объекты, если на них не было ссылок извне, удалятся тоже.

Единственным гарантированно выжившим объектом - возвращаемым функцией значением - потом можно пользоваться:

   1 >>> c=fun(1.1,2.2)
   2 >>> c
   3 4.4    #  Забавно, что в этом примере представление числа получилось точным
   4 

Фактическим параметром функции может выступать и выражение - ведь его результат - обычный питоновский объект:

   1 >>> fun(3+4*88,7)    # Первый передаваемый параметр - результат указанного выражения
   2 717
   3 

Возвращаемым функцией значением может быть произвольный объект, составной тоже. Поэтому

   1 >>> def swap(a,b):
   2 ...     return b, a    # Верните кортеж, кому говорю!
   3 ...
   4 >>> swap(3,8)
   5 (8, 3)    # Так-то лучше
   6 

2.6. Баг или фича

Во время лекции я допустил очепятку в описании функции swap; повторю её здесь, чтобы продемонстрировать лишний раз динамическую природу языка.

   1 >>> def swap(a,b):
   2 ...    return b. a    # Я поставил точку вместо запятой  
   3 ...
   4 >>> swap(3,8) 
   5 Traceback (most recent call last):
   6   File "<stdin>", line 1, in <module>
   7   File "<stdin>", line 2, in swap
   8 AttributeError: 'int' object has no attribute 'a'    # И функция не заработала, потому что у '8' нет поля 'a', а оно требуется в fun

Однако давайте, забегая вперёд, создадим такой объект, для которого функция работает:

   1 >>> class B: pass    # Класс с пустым телом
   2 ...
   3 >>> b=B()    # Создадим объект этого класса
   4 >>> b.a=12    # и прикрутим к нему поле 'b'
   5 >>> swap(a,b)
   6 12    # Работает! Динамическая семантика языка в действии
   7 

2.7. Распаковка кортежей

Возвращаемый функцией кортеж можно распаковать, т.е. разобрать на те части, из которых он составлен:

   1 >>> a, b = 2,3    # Тоже пример множественного связывания, кстати
   2 >>> a, b = swap(a,b)    # Используем множественное связывание, чтобы распаковать кортеж
   3 >>> a
   4 3
   5 >>> b
   6 2
   7 

Впрочем, использовать в этом примере swap было вовсе не обязательно, ведь благодаря множественному связыванию можно просто написать:

   1 >>> a, b = b, a    # Поменяли a и b обратно
   2 >>> a
   3 2
   4 >>> b
   5 3
   6 

2.8. И ещё пару слов об утиной типизации

Посмотрим, как работает утиная типизация, на примере описанной нами функции fun:

   1 >>> fun(1,2)    # Числа можно умножать на число и складывать
   2 4
   3 >>> fun("Jo", "'s bizarre adventure")    # Строки можно умножать на число и складывать
   4 "JoJo's bizarre adventure"
   5 >>> fun(5, "S")    # А вот складывать число и строку уже нельзя
   6 Traceback (most recent call last):
   7   File "<stdin>", line 1, in <module>
   8   File "<stdin>", line 2, in fun
   9 TypeError: unsupported operand type(s) for +: 'int' and 'str'

При этом не забываем, что типизация в питоне - строгая! В любой момент про каждый объект мы можем точно сказать, какого он типа (хоть и не можем этого сделать до выполнения программы).

Для выполнения какой-либо операции требуется лишь, чтобы эта операция была определена для выбранных объектов. Если умножение строки на число работает, то это потому, что оно определено. Если сложение строк работает, то это потому, что оно определено.

При этом сделать так, чтобы операция была выполнима над данными объектами (и делала то, что нужно!) - задача программиста.

2.9. О текстовых редакторах и запуске программ

Когда вы изучаете какой-то язык, вредно и не стоит использовать развесистые среды программирования, которые многое сами за тебя делают, всё тебе подсказывают и т.д.. Они нужны для другого - для быстрой разработки.

Поэтому для обучения берите любой редактор плоского текста, лучше - с подсветкой синтаксиса и какими-нибудь дополнительными средствами выразительности относительно того языка, который вы будете использовать.

Я, конечно, предпочитаю редактор vim. Однако и помимо него есть много разных опций.

Во-первых, есть редактор (и, по совместительству, среда разработки), который идёт сразу вместе с питоном - IDLE (назван в честь Эрика Айдла, главного актёра "Летающего цирка Монти Пайтона"). В linux, скорее всего, у вас будет название idle3.

   1 >>> quit()
   2 $ idle3

По умолчанию запускается нечто вроде командной строки. Она чуть менее настоящая, чем собственно питоновская.1 Зато она умеет раскрашивать. И копировать текст при выделении.

Если вы не понимаете интуитивно, как пользоваться интерфейсом, посмотрите вот этот кусочек лекции.

Обратите внимание, что при написании программы в виде отдельного файла, а не просто в виде команд в интерпретаторе, я буду использовать оператор print: здесь вы уже не увидите просто так, если напишете какое-то выражение, последний залипший объект.

Посмотреть его в командной строке можно с помощью команды "_":

   1 >>> 345
   2 345
   3 >>> _
   4 345
   5 >>> a=4+5    # Здесь залипшего объекта нет, поэтому ничего не поменяется
   6 >>> _
   7 345
   8 >>> 'something'
   9 'something'
  10 >>> _
  11 'something'
  12 

Если вы, упаси бог, работаете с виндой [как редактор этого текста], при сохранения файла в IDLE внимательно смотрите, куда сохраняете файл - иначе сохраните его в такое глубокое место, что потом не найдёте.

Как выполнить программу? Запустите всё тот же интерпретатор питона и в качестве параметра передайте ему путь к программе (в примере ниже предполагается, что вы уже перешли в терминале в ту директорию, куда сохранили файл first.py):

   1 $ python3 first.py    # В windows, если вы добавили python в PATH, вместо 'python3' пишите просто 'python'. Если не добавили, указывайте полный путь до файла python.exe.
   2 69

Такой способ запуска программы довольно удобен (почему - обсудим позже), но, если вы просто пишете небольшую программу, для запуска можно воспользоваться встроенной опцией IDLE (сохраните файл и нажмите F5).

Дополнительное удобство IDLE - после выполнения программы мы остаёмся в том пространстве имён, которое программа сгенерировала:

   1 69
   2 >>> dir()
   3 ['__annotations__', '__builtins__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'fun']
   4 

Соответственно, дальше можно в интерпретаторе IDLE после выполнения программы что-нибудь вручную поотлаживать.

То же самое можно сделать в стандартном интерпретаторе питона, если запустить его с ключом -i (в этом случае сначала выполнится программа, а потом мы попадём в интерпретатор с пространством имён, заполненным программой):

   1 $ python3 -i first.py
   2 >>> fun(9,9)
   3 27

2.10. Правила видимости

Поговорим о глобальных переменных. Есть такая функция globals, которая выводит нам словарь (это такая структура данных в python, аналогичная map в C++), каждый элемент которого - пара, где левая часть - имя переменной, а правая - объект, на который это имя ссылается:

   1 >>> globals()    # Хотим посмотреть, что лежит в глобальном пространстве имён
   2 {'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>}
   3 >>> a = 345    # Объявили глобальную переменную a
   4 >>> globals()    # И сейчас увидим её в глобальном namespace
   5 {'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, 'a': 345}
   6 

globals() описывает глобальное пространство (внезапно), а locals() - пространство имён, которое заводится при входе в функцию и удаляется при выходе из неё.

Добавим два раза печать locals():

   1 def  fun(a, b):
   2     print(locals())
   3     c = a*2 + b
   4     print(locals())
   5     return c
   6 
   7 print(fun(12, 45))

И увидим, что второй print отличается от первого наличием переменной c:

   1 $ python3 first.py
   2 {'b': 45, 'a': 12}
   3 {'b': 45, 'a': 12, 'c': 69}
   4 69

Локальные имена не пересекаются с глобальными:

   1 def  fun(a, b):
   2     print(locals())
   3     c = a*2 + b    # Здесь a, b и c - локальные переменные
   4     print(locals())
   5     return c
   6 
   7 a=b=c=100500    # Значения глобальных переменных a,b,c после вызова функции не изменятся
   8 print(fun(12, 45))
   9 print(a,b,c)

   1 $ python3 first.py
   2 {'b': 45, 'a': 12}
   3 {'b': 45, 'a': 12, 'c': 69}    # В локальное пространство имён добавилось c
   4 69
   5 100500 100500 100500    # Значения глобальных переменных не изменились
   6 

Сравните с кодом, в котором в функции fun мы указываем, что с - глобальное имя:

   1 def  fun(a, b):
   2     global c    # Значит, в функции fun имя 'c' берётся из внешней, глобальной области видимости 
   3     print(locals())
   4     c = a*2 + b    # Значение глобальной переменной изменится
   5     print(locals())
   6     return c
   7 
   8 a=b=c=100500
   9 print(fun(12, 45))
  10 print(a,b,c)

   1 $ python3 first.py
   2 {'b': 45, 'a': 12}
   3 {'b': 45, 'a': 12}    # Т.к. мы указывали в функции, что используем глобальное имя 'c', локальное имя создано не было
   4 69
   5 100500 100500 69    # Бинго
   6 

Чтобы залезть из функции в глобальное пространство имён (использовать объект, имя которого объявлено вне функции), нужно либо указать, что мы хотим использовать глобальное имя (как сделано в примере выше - с помощью global), либо соответствующее имя не должно присутствовать в локальном пространстве имён. Поиск осуществляется сначала в локальном пространстве имён, и только потом - в глобальном:

   1 def  fun(a, b):
   2     global c
   3     print(locals())
   4     c = a*2 + b + d    # В локальном пространстве имён d не будет найдено - его здесь попросту нет
   5     print(locals())
   6     return c
   7 
   8 a=b=c=d=100500    # Поэтому d будет взято из глобального пространства имён
   9 print(fun(12, 45))     # Обратите внимание, вызов функции происходит уже после инициализации d, иначе были бы проблемы
  10 print(a,b,c)

   1 $ python3 first.py
   2 {'b': 45, 'a': 12}
   3 {'b': 45, 'a': 12}    # Локальное имя d не было создано - мы не выполняли связывание
   4 100569    # Но было найдено в глобальной области видимости, и потому всё работает
   5 100500 100500 100569

Важный пример: добавим в функцию локальное имя d, но уже после того, как d используется в выражении. Тогда, если интерпретатор увидит где-то в функции имя d, оно будет считаться локальным.

Однако локальная переменная на момент вычисления выражения у нас здесь не инициализирована, и мы получим ошибку выполнения:

   1 def  fun(a, b):
   2     c = a*2 + b + d    # Казалось бы, мы вычисляем выражение до того, как создаём локальную d
   3     d = 8
   4     return c
   5 
   6 a=b=c=d=100500
   7 print(fun(12, 45)) 
   8 print(a,b,c)

   1 $ python3 first.py    # Но получим ошибку
   2 Traceback (most recent call last):
   3   File "first.py", line 7, in <module>
   4     print(fun(12, 45))
   5   File "first.py", line 2, in fun
   6     c = a*2 + b + d
   7 UnboundLocalError: local variable 'd' referenced before assignment

Таким образом, сам факт связывания имени переменной и объекта внутри функции означает, что это имя - локальное.

Кроме того,

2.10.1. Архиважное замечание

Поэтому в примере выше мы поняли, что d - локальная переменная, ещё до того, как интерпретатор дошёл до строчки d = 8. Такое поведение - одно из тонких отличий логики работы python от других языков.

2.11. Замечательный ресурс, чтобы разобраться, что происходит

Ресурс называется Python Tutor и позволяет пошагово увидеть происходящее в программе, в том числе состояние пространств имён. Разбор примера выше можно посмотреть здесь. Взглянуть на это хотя бы один раз точно стоит.

2.12. Документация

В любой составной объект питона можно вставить т.н. строку2 документации. Под составными объектами имеются в виду:

  1. функция (в т.ч. метод класса);
  2. модуль;
  3. класс.

Всё, что нужно сделать - в самом начале тела, к примеру, функции добавить строку с тем текстом, который вы хотите:

   1 def  fun(a, b):
   2     """This is a dummy function on a,b.    # Тройные кавычки позволяют задать объект типа "строка", состоящий из нескольких строк (в смысле, последовательностей символов)
   3     It does nothing valuable!"""           # Строку документации принято указывать в тройных кавычках, даже если ваш текст состоит из одной строки - это облегчает
   4     c = a*2 + b                            # поддерживание вашего кода, поскольку строку документации в любой момент можно расширить или сократить, не думая о том,
   5     return c                               # сколько строк теперь занимает текст
   6 
   7 a=b=c=d=100500
   8 print(fun(12, 45)) 
   9 print(a,b,c)

Как это работает: строка, залипшая в пространстве имён функции, приезжает в эту функцию в поле под названием __doc__. После этого с ней можно работать как напрямую, так и через help():

   1 $ python3 -i first.py
   2 69
   3 100500 100500 100500
   4 >>> dir()    # Убедимся, что функция fun осталась в пространстве имён
   5 ['__annotations__', '__builtins__', '__cached__', '__doc__', '__loader__', '__name__', '__package__', '__spec__', 'a', 'b', 'c', 'd', 'fun']
   6 >>> dir(fun)    # И что в её пространстве имён есть имя __doc__
   7 ['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']
   8 >>> fun.__doc__    # Напечатаем его значение напрямую
   9 'This is a dummy function on a,b.\n    It does nothing valuable!'
  10 >>> help(fun)    # Help! I need somebody! (help(fun), not just anybody)
  11 Help on function fun in module __main__:
  12 
  13 fun(a, b)
  14     This is a dummy function on a,b.
  15     It does nothing valuable!
  16 

То есть просто повесив строчку в начале функции, мы получили вполне увесистый help.

Документирование в питоне - очень весёлое и ненапряжное занятие. Когда будем говорить про модули, увидим это с потрясающей чёткостью.

2.13. Распаковка и запаковка параметров

2.13.1. Распаковка

Единственная известная нам пока последовательность - это кортеж. Мы даже знаем, что их можно распаковывать.

>>> 1,2,3    # Создадим кортеж
(1, 2, 3)    # Так кортежи выглядят при выводе на печать в Python
>>> c = 1,3,5,67    # Создаём другой кортеж
>>> f,g,h,j=c    # И с помощью операции множественного связывания распаковываем его
>>> f
1
>>> g
3
>>> h
5
>>> j
67
>>> type(c)
<class 'tuple'>    # Кортеж собственной персоной 
>>> c
(1, 3, 5, 67)

Абсолютно та же картина - при передаче параметров в функцию. Нужно лишь указать с помощью символа *, что кортеж нужно распаковать:

   1 def fun(a,b,c):
   2     print(a,b,c)
   3 
   4 fun(1,2,3)
   5 c = "QWE", 100500, None
   6 fun(*c)    # Указание на то, что 'c' нужно распаковать; каждый элемент кортежа
   7            # станет переданным в функцию параметром 
   8 
   9 cc = 1,2    # Можно и по-хитрому этим пользоваться
  10 fun(23, *cc)

   1 $ python3 funp.py
   2 1 2 3
   3 QWE 100500 None
   4 23 1 2

Вопрос: насколько питону это тяжело? Запаковывать, распаковывать все эти параметры?

Ответ: вообще не тяжело. Функция в любом случае получает кортеж. Этот кортеж приходит при вызове, и только после этого функция начинает его именовать соответствующими именами.

2.13.2. Запаковка

Запаковка, т.е. обратная к распаковке операция, ещё удобнее:

   1 def fun(*args):    # Мы говорим функции, что передаваемые ей параметры - это элементы кортежа args, т.е. как бы запаковываем переданные нам параметры в кортеж 
   2     print(args, type(args))    # Убедимся, что на вход пришёл кортеж
   3 
   4 fun()    # Мы написали функцию с произвольным количеством параметров
   5 fun(1,2,3,4,5)
   6 fun("Thomas")    # Да ещё и любого типа

   1 $ python3 funp2.py
   2 () <class 'tuple'>    # Что и ожидалось
   3 (1, 2, 3, 4, 5) <class 'tuple'>
   4 ('ny',) <class 'tuple'>

Вы можете делать и кое-что поинтереснее:

   1 def fun(a, *args):    # Потребуем, чтобы один параметр (первый) был обязательным
   2     print(args, type(args))
   3 
   4 fun(5)    # Совсем без параметров функцию вызвать уже нельзя, поэтому добавим один
   5 fun(1,2,3,4,5)
   6 fun("ny")

   1 $ python3 funp2.py
   2 () <class 'tuple'>    # Первый параметр теперь попадает в 'a', a не в 'args'
   3 (2, 3, 4, 5) <class 'tuple'>
   4 () <class 'tuple'>

Строка - тоже последовательность! А операция распаковки возможна для любой последовательности:

   1 def fun(a, *args):
   2     print(args, type(args))
   3 
   4 fun(5)
   5 fun(1,2,3,4,5)
   6 fun("ny")
   7 fun(*"QWERTY")    # Уже догадались, что произойдёт?

   1 $ python3 funp2.py
   2 () <class 'tuple'>
   3 (2, 3, 4, 5) <class 'tuple'>
   4 () <class 'tuple'>
   5 ('W', 'E', 'R', 'T', 'Y') <class 'tuple'>    # Итсатрап: это не символы, а строки единичной длины!
   6 

2.14. Если у вас нет return

То подразумевается, что return есть - в конце функции - и возвращает он значение None:

   1 >>> def void():    # Функция, которая ничего не возвращает...
   2 ...     print("so empty...")
   3 ...
   4 >>> v = void()
   5 so empty...
   6 >>> v
   7 >>> type(v)
   8 <class 'NoneType'>
   9 >>> v == None    # ... на самом деле, возвращает ничего (т.е. None)
  10 True
  11 

3. Генераторы

Что если я захочу написать такой кусок кода, который будет реализовывать не один алгоритм при каждом вызове (как функция), а разные при разных вызовах?

Один раз вызвал - выполнился один алгоритм, посчиталось какое-то значение. Второй раз старик закинул невод - пришёл невод с одной тиной. Третий раз закинул невод - а там золотая рыбка...

То есть что если я хочу написать повторно входимую функцию, выполняющую при первом входе - один кусок кода, при втором - второй, при третьем - третий, и так сколько-то раз?

Это очень интересная штука, потому что даёт нам в руки инструмент под названием вычислимая последовательность.

Встаёт всего лишь два вопроса:

  1. Сколько раз? (мы можем вызвать повторно входимую функцию);
  2. Каков вообще протокол работы с повторной функцией, как это в языке организовать?

3.1. Как создать

В питоне это выглядит довольно прозрачно. Чтобы описать генератор (т.е. повторно входимую функцию), напишите обычную функцию, только вместо return используйте yield:

   1 >>> def gen(n):
   2 ...    yield n    # Используем yield вместо return
   3 ...    yield n//2
   4 ...    yield "ALL"
   5 ...
   6 >>> gen    # gen - обычная функция
   7 <function gen at 0x0000020A9A1BE1E0>
   8 >>> g=gen(5)
   9 >>> g    # Но возвращает она не искомое значение, а некий generator object
  10 <generator object gen at 0x0000020A9A02E5C8>
  11 >>> next(g)    # По которому мы можем, используя функцию next(), итерироваться, и результатом уже и будут различные искомые значения
  12 5
  13 >>> next(g)
  14 2
  15 >>> next(g)
  16 'ALL'
  17 

3.2. Ну сколько можно?

Ответ очень простой: пока в тексте вашей функции, использующей yield, не встретится return, или пока не закончится текст функции (что, как мы знаем, тоже вызов return, только неявный). После этого при попытке получить следующее значение возникнет исключение !StopIteration:

   1 # Продолжаем предыдущий кусок интерпретации
   2 >>> next(g)
   3 Traceback (most recent call last):
   4   File "<stdin>", line 1, in <module>
   5 StopIteration

3.3. Одноразовость

Генератор - это такой одноразовый пистолет с несколькими зарядами (возможно, с очень большим их количеством). При каждом следующем вызове next выполняется весь код от места, где остановились при прошлом вызове, до следующего yield (который определяет, что именно вызов next вернёт на этот раз). Дошли до вызова return в функции (явного или неявного), получили !StopIteration - всё, нужен новый пистолет генератор.

Посмотрим это на примере более подробной программы:

   1 def gen(a):
   2     print("First")
   3     yield a
   4     print("Second")
   5     yield a*2
   6     print("Third")
   7     yield -1

   1 $ python3 -i gen.py    # Напоминаем, пользуемся ключом -i, чтобы после выполнения программы попасть в интерпретатор с тем namespace, который остался в результате
   2 >>> dir()    # Функция gen после интерпретации файла есть в глобальном пространстве имён
   3 ['__annotations__', '__builtins__', '__cached__', '__doc__', '__loader__', '__name__', '__package__', '__spec__', 'gen']
   4 >>> g=gen(55)    # Получаем наш генератор
   5 >>> g    # Убеждаемся, что это он
   6 <generator object gen at 0x000001C3C174E5C8>
   7 >>> a=next(g)    # И начинаем вызывать его: первый раз
   8 First
   9 >>> a
  10 55
  11 >>> a=next(g)    # Второй
  12 Second
  13 >>> a
  14 110
  15 >>> a=next(g)    # Третий
  16 Third
  17 >>> a
  18 -1
  19 >>> a=next(g)    # Кина не будет: электричество кончилось
  20 Traceback (most recent call last):
  21   File "<stdin>", line 1, in <module>
  22 StopIteration
  23 >>> a=next(g)    # Да, генератор - одноразовый объект
  24 Traceback (most recent call last):
  25   File "<stdin>", line 1, in <module>
  26 StopIteration
  27 >>> g=gen("QQ")    # Поэтому, если хочешь продолжить, придётся получить новый
  28 >>> a=next(g)    # И вот новый генератор можно использовать столько же раз, сколько и старый
  29 First
  30 >>> a
  31 'QQ'
  32 >>> a=next(g)    # Два
  33 Second
  34 >>> a
  35 'QQQQ'
  36 >>> a=next(g)    # Три
  37 Third
  38 >>> a
  39 -1
  40 >>> a=next(g)    # Генератор исчерпал себя
  41 Traceback (most recent call last):
  42   File "<stdin>", line 1, in <module>
  43 StopIteration

3.4. Генератор - это последовательность! (вычислимая)

Казалось бы, пользовались генератором, получили StopIteration, больше не пользуемся. Плохо.

Но нет! Любой циклический обход последовательности в питоне ровно так и устроен - пользуемся генератором, возвращающим элементы последовательности, пока не получим исключение из-за того, что больше элементов нет.

По сути, генератор и есть последовательность, которая может наравне с другими последовательностями участвовать в различных операциях. Давайте, например, распакуем генератор, как уже делали с кортежами и строками:

   1 >>> f,g,h = gen(100)    # Совершенно легитимная операция
   2 First
   3 Second
   4 Third
   5 >>> f
   6 100
   7 >>> g
   8 200
   9 >>> h
  10 -1
  11 

Ещё раз, для закрепления: приведённая выше функция gen не возвращает при своём вызове никакое из интересующих нас значений. Она возвращает генератор, generator object, с помощью которого мы можем перебирать точки входа функции (эти точки - соответственно, начало функции, точка сразу после первого yield, точка сразу после второго yield и т.д.).

Можно даже передавать генератор в функцию, пользуясь распаковкой:

   1 def gen(a):
   2     print("First")
   3     yield a
   4     print("Second")
   5     yield a*2
   6     print("Third")
   7     yield -1
   8 
   9 def fun(a,b,c):    # Функция, которая просто печатает три передаваемых ей параметра
  10     print(a,b,c)
  11 
  12 fun(*gen("QQ"))    # Распаковываем генератор, который нам вернёт функция gen

   1 $ python3 -i gen.py
   2 First    # В процессе распаковки происходит итерация по последовательности, поэтому осуществляется последовательный вход во все точки входа функции gen 
   3 Second
   4 Third
   5 QQ QQQQ -1    # И видим вполне ожидаемый результат, как будто gen изначально и был последовательностью уже вычисленных значений
   6 

Ну а зачем нужны генераторы - поговорим в следующий раз.

4. Операторы ввода

4.1. input

Оператор вывода мы уже знаем - это функция print.

А вот со вводом ситуация чуть-чуть ехидно смеётся более сложная. Функция ввода называется input, и результат её работы - строка:

   1 >>> input()
   2 cakeisaliecakeisaliecakeisaliecakeisaliecakeisalie
   3 'cakeisaliecakeisaliecakeisaliecakeisaliecakeisalie'    # Это строка
   4 >>> input()
   5 123
   6 '123'    # И это строка
   7 

Это значит, что, если вы хотите, например, ввести число, с этой строчкой нужно что-то делать. Можно преобразовать её к нужному типу, используя соответствующее преобразование:

   1 >>> a=int(input())
   2 12345
   3 >>> a
   4 12345
   5 >>> type(a)
   6 <class 'int'>
   7 >>>
   8 

Но иногда хочется просто ввести числа через запятую, а писать преобразования лень:

   1 >>> a=input()
   2 1,2,45,6,8
   3 >>> a    # Ну получили мы строку. А хотелось бы из неё теперь получить кортеж. Что делать-то?
   4 '1,2,45,6,8'
   5 

4.2. ГРЯЗНЫЙ ХАК ДЛЯ ИСПОЛЬЗОВАНИЯ ИСКЛЮЧИТЕЛЬНО В НАШЕМ КОНТЕСТЕ И БОЛЬШЕ НИГДЕ И БОЖЕ УПАСИ ВАС ИСПОЛЬЗОВАТЬ ЕГО В ПРОДАКШЕН-КОДЕ

Функция eval.

eval позволяет проинтерпретировать как питоновский код ту строку, которую ему передали:

   1 >>> a="3+6"
   2 >>> a
   3 '3+6'
   4 >>> type(a)    # Нормальная строка
   5 <class 'str'>
   6 >>> eval(a)    # Проинтерпретируем её
   7 9
   8 >>> eval("a*2")    # Поговаривают, что на самом деле ударение нужно ставить на первый слог
   9 '3+63+6'
  10 >>> dir()
  11 ['__annotations__', '__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__', 'a']
  12 >>> h = 5
  13 >>> eval("h*8+1")    # Мало того, что он вычисляет то, что есть внутри, он ещё и ХОДИТ К ТЕКУЩЕМУ NAMESPACE
  14 41
  15 

А вот и та таблетка, которой мы будем пользоваться на контесте:

   1 >>> a=eval(input())    # Во всей красе
   2 1,2,3,4,5,6,7,8
   3 >>> a    # Это кортеж
   4 (1, 2, 3, 4, 5, 6, 7, 8)
   5 >>> a[4]    # Обычный кортеж
   6 5
   7 

Что с ней не так? А то, что она может делать вообще что угодно:

   1 >>> a=eval(input())    # Просмотреть ваш namespace
   2 dir()
   3 >>> a
   4 ['__annotations__', '__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__', 'a', 'h']
   5 >>> a=eval(input())    # Импортировать модули
   6 __import__("random").random()
   7 >>> a    # И это всё ещё безобидные вещи, а может ведь и в сеть сходить, и вообще выполнить произвольный код
   8 0.15582057392974524
   9 

В общем, использовать можно, но с огромной осторожностью.


  1. История в IDLE работает немного не так: при нажатии Enter появляется та строчка, на которую был установлен курсор. (1)

  2. Не самый удачный термин: здесь имеется в виду строка как тип данных, но строка как тип данных может состоять как из одной, так и из нескольких строк в смысле последовательностей символов, разделённых символом переноса строки (простите за каламбур). Так что проверяйте, точно ли вы понимаете смысл высказываний, содержащих слово "строка". (2)

LecturesCMC/PythonIntro2018/02_Functions/Conspect (last edited 2018-10-05 14:59:20 by RomanKrivonogov)