Математический анализ. 3е издание
Скачать 4.86 Mb.
|
Пример функциигенератора Генераторы и итераторы – это достаточно сложные особенности язы ка, поэтому обязательно загляните в руководства по стандартной биб лиотеке языка Python, где найдете исчерпывающую информацию. Чтобы проиллюстрировать основные моменты, рассмотрим следую щий фрагмент, где определяется функциягенератор, которая может использоваться для генерации серии квадратов чисел: 1 >>> def gensquares(N): ... for i in range(N): ... yield i ** 2 # Позднее продолжить работу с этого места Эта функция поставляет значение и тем самым возвращает управление вызывающей программе на каждой итерации цикла – когда она возоб новляет работу, восстанавливается ее предыдущее состояние и управ ление передается непосредственно в точку, находящуюся сразу же за инструкцией yield. Например, при использовании в заголовке цикла for управление возвращается функции на каждой итерации в точку, находящуюся сразу же за инструкцией yield: >>> for i in gensquares(5): # Возобновить работу функции 1 Генераторы появились в языке Python, начиная с версии 2.2. В версии 2.2 для их использования необходимо было применять специальную инструк цию import: __future__ import generators (подробнее об этой форме инструк ции рассказывается в главе 18). Генераторы стали доступны еще в версии 2.2 во многом благодаря тому, что лежащий в их основе протокол не требовал нового ключевого слова yield, нарушающего обратную совместимость. 462 Глава 17. Расширенные возможности функций ... print i, ':', # Вывести последнее полученное значение 0 : 1 : 4 : 9 : 16 : >>> Для завершения генерации значений функция может либо воспользо ваться инструкцией return без значения, либо просто позволить пото ку управления достичь конца функции. Если вам интересно узнать, что происходит внутри цикла for, вызови те функциюгенератор напрямую: >>> x = gensquares(4) >>> x Здесь обратно был получен объектгенератор, который поддерживает протокол итераций (т. е. имеет метод next, который запускает функ цию или возобновляет ее работу с места, откуда было поставлено по следнее значение, а также возбуждает исключение StopIteration по достижении конца последовательности значений): >>> x.next() 0 >>> x.next() 1 >>> x.next() 4 >>> x.next() 9 >>> x.next() Traceback (most recent call last): File " ", line 1, in x.next() StopIteration Циклы for работают с генераторами точно так же – вызывают метод next в цикле, пока не будет перехвачено исключение. Если итерируе мый объект не поддерживает этот протокол, вместо него цикл for ис пользует протокол доступа к элементам по индексам. Обратите внимание, что в этом примере мы могли бы просто сразу соз дать список всех значений: >>> def buildsquares(n): ... res = [] ... for i in range(n): res.append(i**2) ... return res >>> for x in buildsquares(5): print x, ':', 0 : 1 : 4 : 9 : 16 : Еще раз об итераторах: генераторы 463 В такой ситуации мы могли бы использовать любой из приемов: цикл for , функцию map или генератор списков: >>> for x in [n**2 for n in range(5)]: ... print x, ':', 0 : 1 : 4 : 9 : 16 : >>> for x in map((lambda x:x**2), range(5)): ... print x, ':', 0 : 1 : 4 : 9 : 16 : Однако генераторы дают возможность избежать необходимости выпол нять всю работу сразу, что особенно удобно, когда список результатов имеет значительный объем или когда вычисление каждого значения занимает продолжительное время. Генераторы распределяют время, необходимое на создание всей последовательности значений, по отдель ным итерациям цикла. Кроме того, в более сложных случаях использо вания они обеспечивают простую альтернативу сохранению состояния вручную между вызовами в объектах классов (подробнее о классах рас сказывается в шестой части книги) – в случае с генераторами перемен ные функций сохраняются и восстанавливаются автоматически. Расширенный протокол функцийгенераторов: send и next В версии Python 2.5 в протокол функцийгенераторов был добавлен метод send. Метод send не только перемещается к следующему элемен ту в последовательности результатов, как это делает метод next, но еще и обеспечивает для вызывающей программы способ взаимодейство вать с генератором, влияя на его работу. С технической точки зрения yield в настоящее время является не инст рукцией, а выражением, которое возвращает элемент, передаваемый методу send (несмотря на то, что его можно использовать любым из двух способов, – как yield X или как A = yield(X)). Значения передают ся генератору вызовом метода send(value). После этого программный код генератора возобновляет работу, и выражение yield возвращает значение, полученное от метода send. Когда вызывается обычный ме тод next(), выражение yield возвращает None. Метод send может использоваться, например, чтобы реализовать гене ратор, который можно будет завершать из вызывающей программы. Кроме того, генераторы в версии 2.5 поддерживают метод throw(type) для возбуждения исключения внутри генератора в последнем выраже нии yield и метод close(), который возбуждает исключение Generator Exit внутри генератора, чтобы вынудить его завершить итерации. Мы не будем углубляться здесь в эти расширенные возможности – за допол нительной информацией обращайтесь к стандартным руководствам по языку Python. 464 Глава 17. Расширенные возможности функций Итераторы и встроенные типы Как мы видели в главе 13, встроенные типы данных спроектированы так, чтобы воспроизводить объекты итераторов в ответ на вызов встро енной функции iter. Итераторы словарей, например, во время итера ций воспроизводят список ключей: >>> D = {'a':1, 'b':2, 'c':3} >>> x = iter(D) >>> x.next() 'a' >>> x.next() 'c' Кроме того, все разновидности итераций (включая циклы for, функ цию map, генераторы списков и других, с которыми мы встречались в главе 13) в свою очередь спроектированы так, чтобы для определе ния – поддерживается ли протокол автоматически вызывать встроен ную функцию iter. Именно поэтому существует возможность выпол нить обход ключей словаря, не прибегая к вызову метода keys, строк в файле – без вызова метода readlines или xreadlines и т. д.: >>> for key in D: ... print key, D[key] a 1 c 3 b 2 Мы также видели, что при использовании итераторов файлов интер претатор Python просто загружает строки из файла по мере необходи мости: >>> for line in open('temp.txt'): ... print line, Tis but a flesh wound. Кроме того, существует возможность реализовать произвольные объ ектыгенераторы с помощью классов, которые соответствуют протоко лу итераторов и поэтому могут использоваться в циклах for и в других итерационных контекстах. Такие классы определяют специальный метод __iter__, возвращающий объектитератор (что более предпочти тельно, чем использование метода __getitem__, обеспечивающего дос туп к элементам по индексу). Однако эта тема далеко выходит за рам ки данной главы – обращайтесь к шестой части книги, где приводится информация о классах, и к главе 24 в частности, где приводятся при меры классов, реализующих протокол итераторов. Еще раз об итераторах: генераторы 465 Выражениягенераторы: итераторы и генераторы списков В последних версиях Python понятия итератора и генератора списков были объединены в новую языковую конструкцию – выражения+гене+ раторы . Синтаксически выражения напоминают обычные генераторы списков, но они заключаются не в квадратные, а в круглые скобки: >>> [x ** 2 for x in range(4)] # Генератор списков: создает список [0, 1, 4, 9] >>> (x ** 2 for x in range(4)) # Выражениегенератор: создает # итерируемый объект Однако с функциональной точки зрения выражениягенераторы кар динально отличаются от генераторов списков – вместо того, чтобы соз давать в памяти список с результатами, они возвращают объектгене ратор, который в свою очередь поддерживает итерационный протокол, поставляя по одному элементу списка за раз в любом итерационном контексте: >>> G = (x ** 2 for x in range(4)) >>> G.next() 0 >>> G.next() 1 >>> G.next() 4 >>> G.next() 9 >>> G.next() Traceback (most recent call last): File " ", line 1, in G.next() StopIteration Как правило, нам не приходится наблюдать итерационную механику действий выраженийгенераторов в виде вызовов метода next, как в дан ном примере, потому что циклы for вызывают его автоматически: >>> for num in (x ** 2 for x in range(4)): ... print '%s, %s' % (num, num / 2.0) 0, 0.0 1, 0.5 4, 2.0 9, 4.5 Фактически именно таким образом работает любой итерационный контекст, включая встроенные функции sum, map и sorted, и другие ите рационные инструменты, которые мы рассматривали в главе 13, такие как встроенные функции all, any и list. 466 Глава 17. Расширенные возможности функций Обратите внимание, что круглые скобки вокруг выражениягенерато ра можно опустить, если оно является единственным элементом, за ключенным в другие круглые скобки, например в вызове функции. Однако круглые скобки необходимы во втором вызове функции sorted: >>> sum(x ** 2 for x in range(4)) 14 >>> sorted(x ** 2 for x in range(4)) [0, 1, 4, 9] >>> sorted((x ** 2 for x in range(4)), reverse=True) [9, 4, 1, 0] >>> import math >>> map(math.sqrt, (x ** 2 for x in range(4))) [0.0, 1.0, 2.0, 3.0] Выражениягенераторы в первую очередь оптимизируют использова ние памяти – они не требуют создания в памяти полного списка с ре зультатами, как это делают генераторы списков в квадратных скобках. Кроме того, на практике они могут работать несколько медленнее, по этому их лучше использовать, только когда объем результатов очень велик, – и мы естественным образом переходим к следующему разделу. Хронометраж итерационных альтернатив В этой книге нам встретилось несколько итерационных альтернатив. Чтобы подвести итог, коротко проанализируем ситуацию, соединив все, что мы узнали об итерациях и функциях. Я уже упоминал, что генераторы списков обладают более высокой ско ростью выполнения, чем циклы for, а скорость работы функции map мо жет быть выше или ниже в зависимости от конкретной решаемой зада чи. Выражениягенераторы, рассматривавшиеся в предыдущем разде ле, обычно немного медленнее, чем генераторы списков, но при этом они минимизируют требования к объему используемой памяти. Все это справедливо на сегодняшний день, но относительная произво дительность может измениться со временем (интерпретатор Python по стоянно оптимизируется). Если вам захочется проверить это самим, попробуйте запустить следующий сценарий на своем компьютере, со своей версией интерпретатора: # файл timerseqs.py import time, sys reps = 1000 size = 10000 def tester(func, *args): startTime = time.time() for i in range(reps): Хронометраж итерационных альтернатив 467 func(*args) elapsed = time.time() startTime return elapsed def forStatement(): res = [] for x in range(size): res.append(abs(x)) def listComprehension(): res = [abs(x) for x in range(size)] def mapFunction(): res = map(abs, range(size)) def generatorExpression(): res = list(abs(x) for x in range(size)) print sys.version tests = (forStatement, listComprehension, mapFunction, generatorExpression) for testfunc in tests: print testfunc.__name__.ljust(20), '=>', tester(testfunc) Этот сценарий тестирует все альтернативные способы создания спи сков и, как видно из листинга, выполняет по 10 миллионов итераций каждым из способов, т. е. каждый из тестов создает список из 10 000 элементов 1000 раз. Обратите внимание, как выражениегенератор вызывается через вызов встроенной функции list, чтобы вынудить его выдать все значения, – если бы этого не было сделано, мы бы просто создали генератор, кото рый не выполняет никакой работы. Кроме того, заметьте, как про граммный код в самом конце сценария выполняет обход кортежа из четырех функций и выводит значение атрибута __name__ для каждой из них: это встроенный атрибут, который возвращает имя функции. Когда я запустил этот сценарий в среде IDLE в Windows XP, где уста новлен Python 2.5, я обнаружил следующее: генератор списков ока зался почти в два раза быстрее эквивалентной инструкции цикла for, функция map оказалась немного быстрее генератора списков при ото бражении встроенной функции abs (возвращает абсолютное значение): 2.5 (r25:51908, Sep 19 2006, 09:52:17) [MSC v.1310 32 bit (Intel)] forStatement => 6.10899996758 listComprehension => 3.51499986649 mapFunction => 2.73399996758 generatorExpression => 4.11600017548 Но вот как изменилось положение дел, когда сценарий был изменен так, чтобы он выполнял настоящую операцию, такую как сложение: def forStatement(): res = [] 468 Глава 17. Расширенные возможности функций for x in range(size): res.append(x + 10) def listComprehension(): res = [x + 10 for x in range(size)] def mapFunction(): res = map((lambda x: x + 10), range(size)) def generatorExpression(): res = list(x + 10 for x in range(size)) Присутствие вызова функции сделало вызов map таким же медленным, как и цикл for, несмотря на то, что инструкция цикла содержит боль ше программного кода: 2.5 (r25:51908, Sep 19 2006, 09:52:17) [MSC v.1310 32 bit (Intel)] forStatement => 5.25699996948 listComprehension => 2.68400001526 mapFunction => 5.96900010109 generatorExpression => 3.37400007248 Так как внутренние механизмы интерпретатора сильно оптимизирова ны, анализ производительности, как в данном случае, становится очень непростым делом. В действительности невозможно заранее утвер ждать, какой метод лучше – лучшее, что можно сделать, это провести хронометраж своего программного кода, на своем компьютере, со сво ей версией Python. В этом случае все, что можно сказать наверняка, – это то, что в данной версии Python использование пользовательской функции в вызове map может привести к снижению производительно сти по крайней мере в 2 раза и что в этом испытании генератор списков оказался самым быстрым. Однако, как уже говорилось ранее, производительность не должна быть главной целью при создании программ на языке Python – основное вни мание должно уделяться удобочитаемости и простоте программного ко да, и только потом код можно будет оптимизировать, если это действи тельно необходимо. Вполне возможно, что все четыре варианта облада ют достаточной скоростью обработки имеющихся наборов данных – в этом случае основной целью должна быть ясность программного кода. Чтобы еще глубже вникнуть в ситуацию, попробуйте изменить коли чество повторений в начале сценария или рассмотрите возможность использования новейшего модуля timeit, который автоматизирует хронометраж кода и позволяет избежать проблем, связанных с ис пользуемой платформой (на некоторых платформах, к примеру, пред почтительнее использовать функцию time.time, а не time.clock). Кроме того, обратите внимание на модуль profile из стандартной библиоте ки, где вы найдете полные исходные тексты инструментов профилиро вания программного кода. Концепции проектирования функций 469 Концепции проектирования функций Когда начинают использоваться функции, возникает проблема выбо ра, как лучше связать элементы между собой, например, как разло жить задачу на функции (связность), как должны взаимодействовать функции (взаимодействие) и т. д. Вы должны учитывать такие поня тия, как слаженность, взаимодействие и размер функций, – часть ко торых относится к категории структурного анализа и проектирова ния. Некоторые понятия, имеющие отношение к взаимодействию функций и модулей, были представлены в предыдущей главе, а здесь мы коротко рассмотрим некоторые основные правила для тех, кто на чинает осваивать язык Python: • Взаимодействие: для передачи значений функции используйте ар! гументы, для возврата результатов – инструкцию return. Всегда следует стремиться сделать функцию максимально независимой от того, что происходит за ее пределами. Аргументы и инструкция re turn часто являются лучшими способами ограничить внешнее воз действие небольшим числом известных мест в программном коде. • Взаимодействие: используйте глобальные переменные, только ес! ли это действительно необходимо. Глобальные переменные (т. е. имена в объемлющем модуле) обычно далеко не самый лучший спо соб организации взаимодействий с функциями. Они могут порож дать зависимости и проблемы согласованности, которые существен но осложняют отладку программ. • Взаимодействие: не воздействуйте на изменяемые аргументы, если вызывающая программа не предполагает этого. Функции могут оказывать воздействие на части изменяемых объектов, получае мых в виде аргументов, но, как и в случае с глобальными перемен ными, это предполагает слишком тесную связь между вызывающей программой и вызываемой функцией, что может сделать функцию слишком специфичной и неустойчивой. • Связность: каждая функция должна иметь единственное назначе! ние. Хорошо спроектированная функция должна решать одну зада чу, которую можно выразить в одном повествовательном предложе нии. Если это предложение допускает слишком широкое толкова ние (например: «эта функция реализует всю программу целиком») или содержит союзы (например: «эта функция дает возможность клиентам составлять и отправлять заказ на доставку пиццы»), то стоит подумать над тем, чтобы разбить ее на отдельные и более про стые функции. В противном случае окажется невозможным по вторно использовать программный код функции, в котором смеша ны различные действия. |