Главная страница
Навигация по странице:

  • Генераторы и инструкция yield

  • Сопрограммы и выражения yield

  • Использование генераторов и сопрограмм

  • Декларативное программирование

  • Функции eval(), exec() и compile()

  • справочник по Python. мм isbn 9785932861578 9 785932 861578


    Скачать 4.21 Mb.
    Названиемм isbn 9785932861578 9 785932 861578
    Анкорсправочник по Python
    Дата08.05.2022
    Размер4.21 Mb.
    Формат файлаpdf
    Имя файлаBizli_Python-Podrobnyy-spravochnik.440222.pdf
    ТипСправочник
    #518195
    страница12 из 82
    1   ...   8   9   10   11   12   13   14   15   ...   82
    python.__closure__
    (,)
    >>> python.__closure__[0].cell_contents
    ‘http://www.python.org’
    >>> jython.__closure__[0].cell_contents
    ‘http://www.jython.org’
    >>>
    Замыкание может быть весьма эффективным способом сохранения инфор- мации о состоянии между вызовами функции. Например, рассмотрим сле- дующий пример, в котором реализован простой счетчик:
    def countdown(n):
    def next():
    nonlocal n r = n n -= 1
    return r return next
    ёё

    Декораторы
    139
    # Пример использования next = countdown(10)
    while True:
    v = next() # Получить следующее значение if not v: break
    В этом примере для хранения значения внутреннего счетчика n использу- ется замыкание. Вложенная функция next() при каждом вызове уменьша- ет значение счетчика и возвращает его предыдущее значение. Программи- сты, незнакомые с замыканиями, скорее всего реализовали бы такой счет- чик с помощью класса, например:
    class Countdown(object):
    def __init__(self,n):
    self.n = n def next(self):
    r = self.n self.n -= 1
    return r
    ёё
    # Пример использования c = Countdown(10)
    while True:
    v = c.next() # Получить следующее значение if not v: break
    Однако если увеличить начальное значение обратного счетчика и произве- сти простейшие измерения производительности, можно обнаружить, что версия, основанная на замыканиях, выполняется значительно быстрее (на машине автора прирост скорости составил почти 50%).
    Тот факт, что замыкания сохраняют в себе окружение вложенных функ- ций, делает их удобным инструментом, когда требуется обернуть суще- ствующую функцию с целью расширить ее возможности. Об этом расска- этом расска- этом расска- расска- расска- зывается в следующем разделе.
    Декораторы
    Декоратор
    – это функция, основное назначение которой состоит в том, что- бы служить оберткой для другой функции или класса. Главная цель та- кого обертывания – изменить или расширить возможности обертываемого объекта. Синтаксически декораторы оформляются добавлением специаль- ного символа @ к имени, как показано ниже:
    @trace def square(x):
    return x*x
    Предыдущий фрагмент является сокращенной версией следующего фраг- мента:
    def square(x):
    return x*x square = trace(square)

    140
    Глава 6. Функции и функциональное программирование
    В этом примере объявляется функция square(). Однако сразу же вслед за объявлением объект функции передается функции trace(), а возвращае- мый ею объект замещает оригинальный объект square. Теперь рассмотрим реализацию функции trace, чтобы выяснить, что полезного она делает:
    enable_tracing = True if enable_tracing:
    debug_log = open(“debug.log”,”w”)
    ёё
    def trace(func):
    if enable_tracing:
    def callf(*args,**kwargs):
    debug_log.write(“Вызов %s: %s, %s\n” %
    (func.__name__, args, kwargs))
    r = func(*args,**kwargs)
    debug_log.write(“%s вернула %s\n” % (func.__name, r))
    return r return callf else:
    return func
    В этом фрагменте функция trace() создает функцию-обертку, которая за- писывает некоторую отладочную информацию в файл и затем вызывает оригинальный объект функции. То есть, если теперь вызвать функцию square()
    , в файле debug.log можно будет увидеть результат вызова метода write()
    в функции-обертке. Функция callf, которая возвращается функци- ей trace(), – это замыкание, которым замещается оригинальная функция.
    Но самое интересное в этом фрагменте заключается в том, что возможность трассировки включается только с помощью глобальной переменной enable_
    tracing
    . Если этой переменной присвоить значение False, декоратор trace() просто вернет оригинальную функцию, без каких-либо изменений. То есть, когда трассировка отключена, использование декоратора не влечет за со- бой снижения производительности.
    При использовании декораторы должны помещаться в отдельной строке, непосредственно перед объявлением функции или класса. Кроме того, до- пускается указывать более одного декоратора. Например:
    @foo
    @bar
    @spam def grok(x):
    pass
    В этом случае декораторы применяются в порядке следования. Результат получается тот же самый, что и ниже:
    def grok(x):
    pass grok = foo(bar(spam(grok)))
    Кроме того, декоратор может принимать аргументы. Например:
    @eventhandler(‘BUTTON’)
    def handle_button(msg):
    ...

    Генераторы и инструкция yield
    141
    @eventhandler(‘RESET’)
    def handle_reset(msg):
    ...
    При наличии аргументов декораторы имеют следующую семантику:
    def handle_button(msg):
    ...
    temp = eventhandler(‘BUTTON’) # Вызов декоратора с аргументами handle_button = temp(handle_button) # Вызов функции, возвращаемой декоратором
    В этом случае функция декоратора со спецификатором @ просто принимает аргументы. Затем она возвращает функцию, которая вызывается с декори- руемой функцией в виде аргумента. Например:
    # Декоратор обработчика события event_handlers = { }
    def eventhandler(event):
    def register_function(f):
    event_handlers[event] = f return f return register_function
    Декораторы могут также применяться к определениям классов. Например:
    @foo class Bar(object):
    def __init__(self,x):
    self.x = x def spam(self):
    инструкции
    Функция-декоратор, применяемая к классу, всегда должна возвращать объект класса. Программному коду, действующему с оригинальным клас- сом, может потребоваться напрямую обращаться к методам и атрибутам класса, например: Bar.spam. Это будет невозможно, если функция-декоратор foo()
    будет возвращать функцию.
    Декораторы могут оказывать нежелательное воздействие на такие аспекты функций, как рекурсия, строки документирования и атрибуты. Эти про- блемы описываются ниже в этой главе.
    Генераторы и инструкция yield
    Если функция использует ключевое слово yield, тем самым объявляется объект, который называется генератором. Генератор – это функция, кото- рая воспроизводит последовательность значений и может использоваться при выполнении итераций. Например:
    def countdown(n):
    print(“Обратный отсчет, начиная с %d” % n)
    while n > 0:
    yield n n -= 1
    return

    142
    Глава 6. Функции и функциональное программирование
    Е
    сли вызвать эту функцию, можно заметить, что ни одна инструкция в ее теле не будет выполнена. Например:
    >>> c = countdown(10)
    >>>
    Вместо выполнения функции интерпретатор создает и возвращает объект- генератор. Объект-генератор, в свою очередь, выполняет функцию, когда вызывается его метод next() (или __next__(), в Python 3). Например:
    >>> c.next() # В Python 3 нужно вызвать метод c.__next__()
    Обратный отсчет, начиная с 10 10
    >>> c.next()
    9
    При вызове метода next() инструкции функции-генератора выполняются, пока не будет встречена инструкция yield. Инструкция yield возвращает результат, и выполнение функции приостанавливается в этой точке, пока снова не будет вызван метод next(). После этого выполнение функции воз- обновляется, начиная с инструкции, следующей за инструкцией yield.
    Как правило, в обычной ситуации не приходится непосредственно вызы- вать метод next() генератора, потому что чаще всего генераторы использу- ются в инструкции for, в функции sum() и некоторых других операциях над последовательностями. Например:
    for n in countdown(10):
    инструкции
    a = sum(countdown(10))
    Функция-генератор сигнализирует о завершении последовательности зна- чений и прекращении итераций, возбуждая исключение StopIteration. Для генераторов считается недопустимым по завершении итераций возвращать значение, отличное от None.
    С функциями-генераторами связана одна трудноуловимая проблема, когда последовательность, создаваемая такой функцией, извлекается только ча- стично. Например, рассмотрим следующий фрагмент:
    for n in countdown(10):
    if n == 2: break
    statements
    В этом примере цикл for прерывается инструкцией break, и ассоциирован- ный с ней генератор никогда не достигнет конца генерируемой последова- тельности. Для решения этой проблемы объекты-генераторы предоставля- ют метод close(), который используется, чтобы известить объект о завер- шении итераций. Когда генератор больше не используется или удаляется, вызывается метод close(). Обычно нет необходимости вызывать close(), тем не менее его можно вызвать вручную, как показано ниже:
    >>> c = countdown(10)
    >>> c.next()
    Обратный отсчет, начиная с 10 10

    Сопрограммы и выражения yield
    143
    >>> c.next()
    9
    >>> c.close()
    >>> c.next()
    Traceback (most recent call last):
    File “”, line 1, in
    StopIteration
    >>>
    При вызове метода close() внутри функции-генератора, в инструкции yield, возбуждается исключение GeneratorExit. При необходимости это исключе- ние можно перехватить и выполнить завершающие действия.
    def countdown(n):
    print(“Обратный отсчет, начиная с %d” % n)
    try:
    while n > 0:
    yield n n = n - 1
    except GeneratorExit:
    print(“Достигнуто значение %d” % n)
    Несмотря на имеющуюся возможность перехватить исключение Genera- torExit
    , для функций-генераторов считается недопустимым обрабатывать исключения и продолжать вывод других значений с помощью инструкции yield
    . Кроме того, когда программа выполняет итерации по значениям, возвращаемым генератором, не следует асинхронно вызывать метод close() этого генератора в другом потоке выполнения или в обработчике сигналов.
    Сопрограммы и выражения yield
    Внутри функций инструкция yield может также использоваться как вы- ражение, стоящее справа от оператора присваивания. Например:
    def receiver():
    print(“Готов к приему значений”)
    while True:
    n = (yield)
    print(“Получено %s” % n)
    Функция, использующая инструкцию yield таким способом, называется
    сопрограммой
    и выполняется в ответ на попытку передать ей значение.
    Своим поведением такие функции очень похожи на генераторы. Например:
    >>> r = receiver()
    >>> r.next() # Выполнить до первой инструкции yield (r.__next__() в Python 3)
    Готов к приему значений
    >>> r.send(1)
    Получено 1
    >>> r.send(2)
    Получено 2
    >>> r.send(“Привет”)
    Получено Привет
    >>>

    144
    Глава 6. Функции и функциональное программирование
    В этом примере первый вызов next() необходим, чтобы выполнить инструк- ции в теле сопрограммы, предшествующие первому выражению yield.
    В этой точке выполнение сопрограммы приостанавливается, пока ей не будет отправлено значение с помощью метода send() объекта-генератора r
    . Значение, переданное методу send(), возвращается выражением (yield) в теле сопрограммы. После получения значения сопрограмма продолжает выполнение, пока не будет встречена следующая инструкция yield.
    Программисты часто допускают ошибку, забывая о необходимости иници- ирующего вызова метода next() сопрограммы. По этой причине рекоменду- ется оборачивать сопрограммы декоратором, который автоматически вы- полняет этот шаг.
    def coroutine(func): def start(*args,**kwargs):
    g = func(*args,**kwargs)
    g.next()
    return g return start
    При наличии этого декоратора его можно было бы применить к сопро- граммам:
    @coroutine def receiver():
    print(“Готов к приему значений”)
    while True:
    n = (yield)
    print(“Получено %s” % n)
    ёё
    # Пример использования r = receiver()
    r.send(“Привет, Мир!”) # Внимание: начальный вызов .next() не требуется
    Сопрограмма обычно может выполняться до бесконечности, пока она явно не будет остановлена или пока не завершится сама. Закрыть поток вход- ных данных можно вызовом метода close(), например:
    >>> r.close()
    >>> r.send(4)
    Traceback (most recent call last):
    File “”, line 1, in
    StopIteration
    Если после завершения сопрограммы попытаться передать ей значения, интерпретатор возбудит исключение StopIteration. Вызов метода close() возбуждает исключение GeneratorExit внутри сопрограммы, как уже было описано в разделе о генераторах. Например:
    def receiver():
    print(“Готов к приему значений”)
    try:
    while True:
    n = (yield)
    print(“Получено %s” % n)

    Сопрограммы и выражения yield
    145
    except GeneratorExit:
    print(“Прием завершен”)
    Исключения внутри сопрограммы можно также возбуждать с помощью ме- тода throw(exctype [, value [, tb]]), где exctype – тип исключения, value – зна- чение исключения и tb объект с трассировочной информацией. Например:
    >>> r.throw(RuntimeError,”Вас надули!”)
    Traceback (most recent call last):
    File “”, line 1, in
    File “”, line 4, in receiver
    RuntimeError: Вас надули!
    Исключения возбуждаются так, как если бы их источником была инструк- ция yield в сопрограмме. Сопрограмма может перехватывать и обрабаты- вать такие исключения. Никогда не следует асинхронно вызывать метод throw()
    , из другого потока выполнения или из обработчика сигнала, так как это небезопасно.
    Сопрограмма может одновременно принимать и возвращать значения, ис- пользуя одну и ту же инструкцию yield, для чего достаточно добавить в вы- ражение yield возвращаемое значение. Следующий пример иллюстрирует этот прием:
    def line_splitter(delimiter=None):
    print(“Все готово к разбиению строки”)
    result = None while True:
    line = (yield result)
    result = line.split(delimiter)
    В данном случае сопрограмма используется точно так же, как и прежде.
    Однако теперь метод send() возвращает результат. Например:
    >>> s = line_splitter(“,”)
    >>> s.next()
    Все готово к разбиению строки
    >>> s.send(“A,B,C”)
    [‘A’, ‘B’, ‘C’ ]
    >>> s.send(“100,200,300”)
    [‘100’, ‘200’, ‘300’]
    >>>
    Понимание порядка операций, выполняемых в этом примере, имеет важ- ное значение. Первый вызов метода next() выполняет сопрограмму до пер- вого выражения (yield result), которое возвращает объект None – начальное значение переменной result. При последующих вызовах send() принимае- мое значение помещается в переменную line и преобразуется в список, ко- торый сохраняется в переменной result. Значение, возвращаемое методом send()
    , – это значение, переданное следующей инструкции yield.
    Другими словами, значение, возвращаемое методом send(), поступает от выражения yield, а не от инструкции, ответственной за прием значения, посылаемого сопрограмме с помощью метода send(). Если сопрограмма воз-

    146
    Глава 6. Функции и функциональное программирование вращает какие-либо значения, необходимо предусмотреть в ней обработ- ку исключений, возбуждаемых с помощью метода throw(). Если возбудить исключение вызовом метода throw(), то значение, переданное выражению yield в сопрограмме, будет возвращено методом throw(). Если вы забудете сохранить это значение, оно будет безвозвратно утрачено.
    Использование генераторов и сопрограмм
    На первый взгляд не совсем очевидно, как можно было бы использовать генераторы и сопрограммы для решения практических задач. Однако ге- нераторы и сопрограммы могут быть весьма эффективным инструментом решения проблем в системном программировании, в разработке сетевых приложений или в реализации распределенных вычислений. Например, функции-генераторы могут использоваться для организации конвейерной обработки данных, подобной той, что широко используется в командной оболочке UNIX. Один из примеров такого использования генераторов при-
    UNIX. Один из примеров такого использования генераторов при-
    . Один из примеров такого использования генераторов при- водится в главе 1 «Вводное руководство». Ниже приводится еще один при- мер, в котором задействовано несколько функций-генераторов, выполняю- щих поиск, открытие, чтение и обработку файлов:
    import os import fnmatch
    ёё
    def find_files(topdir, pattern):
    for path, dirname, filelist in os.walk(topdir):
    for name in filelist:
    if fnmatch.fnmatch(name, pattern):
    yield os.path.join(path,name)
    ёё
    import gzip, bz2
    def opener(filenames):
    for name in filenames:
    if name.endswith(“.gz”): f = gzip.open(name)
    elif name.endswith(“.bz2”): f = bz2.BZ2File(name)
    else: f = open(name)
    yield f
    ёё
    def cat(filelist):
    for f in filelist:
    for line in f:
    yield line
    ёё
    def grep(pattern, lines):
    for line in lines:
    if pattern in line:
    yield line
    Ниже приводится пример использования этих функций для организации конвейерной обработки:
    wwwlogs = find(“www”,”access-log*”)
    files = opener(wwwlogs)
    lines = cat(files)
    pylines = grep(“python”, lines)

    Использование генераторов и сопрограмм
    147
    for line in pylines:
    sys.stdout.write(line)
    Программа, представленная в этом примере, обрабатывает все строки во всех файлах с именами “access-log*”, присутствующих во всех подкатало- гах, находящихся в корневом каталоге “www”. Для каждого файла “access- log”
    выясняется, является ли он сжатым, после чего каждый из них откры- вается с помощью соответствующего механизма. Все строки объединяются вместе и обрабатываются фильтром, который отыскивает текст “python”.
    Вся программа управляется инструкцией for в конце. Каждая итерация этого цикла извлекает новое значение из конвейера и выводит его. Кроме того, эта реализация отличается весьма низким потреблением памяти, потому что она не создает ни временных списков, ни каких-либо других структур данных большого размера.
    Сопрограммы могут использоваться в программах, занимающихся об- работкой потоков данных. Программы, организованные таким способом, напоминают перевернутые конвейеры. Вместо того, чтобы в цикле for из- влекать значения из последовательности функций-генераторов, такие про- граммы передают значения коллекции взаимосвязанных сопрограмм.
    Ниже приводится пример функций сопрограмм, имитирующих функции- генераторы, представленные выше:
    import os import fnmatch
    ёё
    @coroutine def find_files(target):
    while True:
    topdir, pattern = (yield)
    for path, dirname, filelist in os.walk(topdir):
    for name in filelist:
    if fnmatch.fnmatch(name,pattern):
    target.send(os.path.join(path,name))
    ёё
    import gzip, bz2
    @coroutine def opener(target):
    while True:
    name = (yield)
    if name.endswith(“.gz”): f = gzip.open(name)
    elif name.endswith(“.bz2”): f = bz2.BZ2File(name)
    else: f = open(name)
    target.send(f)
    ёё
    @coroutine def cat(target):
    while True:
    f = (yield)
    for line in f:
    target.send(line)
    ёё
    @coroutine def grep(pattern, target):

    148
    Глава 6. Функции и функциональное программирование while True:
    line = (yield)
    if pattern in line:
    target.send(line)
    ёё
    @coroutine def printer():
    while True:
    line = (yield)
    sys.stdout.write(line)
    Н
    иже показано, как можно связать все эти сопрограммы, чтобы создать конвейер для обработки потока данных:
    finder = find_files(opener(cat(grep(“python”,printer()))))
    ёё
    # Теперь передать значение finder.send((“www”,”access-log*”))
    finder.send((“otherwww”,”access-log*”))
    В этом примере каждая сопрограмма передает данные другой сопрограмме, указанной в аргументе target. В отличие от примера с генераторами, здесь все управление осуществляется передачей данных первой сопрограмме find_files()
    . Эта сопрограмма, в свою очередь, передает данные следующей стадии. Критическим аспектом этого примера является то, что конвейер может оставаться активным неопределенно долго или пока явно не будет вызван метод close(). Благодаря этому программа может продолжать пере- давать данные сопрограмме так долго, сколько потребуется, например, вы- полнить подряд два вызова send(), как показано в примере.
    Сопрограммы могут использоваться для реализации своего рода много- задачности. Например, центральный диспетчер задач или цикл событий могут создавать и передавать данные огромным коллекциям из сотен или даже тысяч сопрограмм, выполняющих различные виды обработки. Тот факт, что входные данные «посылаются» сопрограмме, означает также, что сопрограммы легко могут внедряться в программы, использующие очереди сообщений и передачу данных для организации взаимодействий между различными компонентами программы. Дополнительные сведения по этой теме можно найти в главе 20 «Потоки выполнения».
    Генераторы списков
    На практике достаточно часто возникает необходимость применить неко- торую функцию ко всем элементам списка, чтобы создать новый список с результатами. Например:
    nums = [1, 2, 3, 4, 5]
    squares = []
    for n in nums:
    squares.append(n * n)
    Так как потребность в подобной операции возникает очень часто, она была реализована в виде оператора, который называется генератором списков.
    Ниже приводится простой пример такого генератора:

    Генераторы списков
    149
    nums = [1, 2, 3, 4, 5]
    squares = [n * n for n in nums]
    В общем виде генераторы списков имеют следующий синтаксис:
    [
    expression for item1 in iterable1 if condition1
    for
    item2 in iterable2 if condition2
    ...
    for
    itemN in iterableN if conditionN ]
    Этот синтаксис можно примерно выразить следующим программным ко- дом:
    s = []
    for
    item1 in iterable1:
    if
    condition1:
    for
    item2 in iterable2:
    if
    condition2:
    ...
    for
    itemN in iterableN:
    if
    conditionN: s.append(expression)
    Для иллюстрации ниже приводится несколько примеров:
    a = [-3,5,2,-10,7,8]
    b = ‘abc’
    ёё
    c = [2*s for s in a] # c = [-6,10,4,-20,14,16]
    d = [s for s in a if s >= 0] # d = [5,2,7,8]
    e = [(x,y) for x in a # e = [(5,’a’),(5,’b’),(5,’c’),
    for y in b # (2,’a’),(2,’b’),(2,’c’),
    if x > 0 ] # (7,’a’),(7,’b’),(7,’c’),
    # (8,’a’),(8,’b’),(8,’c’)]
    ёё
    f = [(1,2), (3,4), (5,6)]
    g = [math.sqrt(x*x+y*y) # f = [2.23606, 5.0, 7.81024]
    for x,y in f]
    Последовательности, включаемые в генераторы списков, не обязательно должны иметь одну и ту же длину, потому что итерации по их элементам выполняются с помощью вложенных циклов for, как было показано выше.
    Получающийся список содержит последовательные значения выражений.
    Инструкция if является необязательной; однако при ее использовании
    выражение
    вычисляется и его результат добавляется в список, только если
    условие
    истинно.
    Если генератор списков используется для конструирования списка корте- жей, значения кортежа должны заключаться в круглые скобки. Напри- мер, такой генератор списков является допустимым: [(x,y) for x in a for y in b]
    , а такой – нет: [x,y for x in a for y in b].
    Наконец, важно отметить, что в Python 2 переменные циклов, определяе-
    Python 2 переменные циклов, определяе-
    2 переменные циклов, определяе- мые внутри генератора списков, размещаются в текущей области видимо- сти и остаются доступными по завершении работы генератора списка. На- пример, переменная цикла x, используемая в генераторе списков [x for x in a]
    , получит значение, которое затрет значение, хранившееся в ней ранее,

    150
    Глава 6. Функции и функциональное программирование и по завершении работы генератора оно будет равно значению последнего элемента полученного списка. К счастью, эта проблема была решена в Py-
    Py- thon 3, где переменная цикла остается частной.
    Выражения-генераторы
    Выражение-генератор
    – это объект, который производит те же самые вы- числения, что и генератор списков, но возвращает свои результаты в про- цессе выполнения итераций. Выражения-генераторы имеют тот же син- таксис, что и генераторы списков, за исключением использования круглых скобок вместо квадратных. Например:
    (
    expression for item1 in iterable1 if condition1
    for
    item2 in iterable2 if condition2
    ...
    for
    itemN in iterableN if conditionN)
    В отличие от генераторов списков, выражения-генераторы не создают спи- ски и не вычисляют немедленно выражения в скобках. Вместо этого созда- ется объект генератора, который будет воспроизводить значения в процес- се итераций. Например:
    >>> a = [1, 2, 3, 4]
    >>> b = (10*i for i in a)
    >>> b

    >>> b.next()
    10
    >>> b.next()
    20
    ...
    Генераторы списков и выражения-генераторы имеют важное, но не бросаю- щееся в глаза отличие. Генераторы списков фактически создают списки, содержащие результаты вычислений, тогда как выражения-генераторы создают объекты генераторов, которые просто могут воспроизводить дан- ные по требованию. В некоторых случаях это может дать существенный прирост производительности и снизить объем потребляемой памяти. На- пример:
    # Чтение файла f = open(“data.txt”) # Открыть файл lines = (t.strip() for t in f) # Прочитать строки и
    # удалить пробелы в начале и в конце comments = (t for t in lines if t[0] == ‘#’) # Все комментарии for c in comments:
    print(c)
    Выражение-генератор, которое в этом примере извлекает строки и удаля- ет пробельные символы в начале и в конце каждой строки, в действитель- ности не считывает содержимое файла в память целиком. То же относит- ся и к выражению, которое извлекает комментарии. Фактическое чтение строк из файла начинается, когда программа входит в цикл for, следую-

    Декларативное программирование
    151
    щий ниже. В процессе этих итераций выполняется чтение строк из файла и их фильтрация. В действительности, при таком подходе файл никогда не будет загружен в память целиком. То есть он представляет собой весь- ма эффективный способ извлечения комментариев из исходных файлов на языке Python огромных размеров.
    В отличие от генераторов списков, выражения-генераторы не создают объ- екты, действующие как последовательности. К ним нельзя применить опе- рацию индексирования или любую другую операцию, обычную для спи- сков (например, append()). Однако выражение-генератор можно преобразо- вать в список с помощью встроенной функции list():
    clist = list(comments)
    Декларативное программирование
    Генераторы списков и выражения-генераторы неразрывно связаны с опера- циями, которые можно обнаружить в декларативных языках программи- рования. По сути эти две особенности выводятся (нестрого) из математи- ческой теории множеств. Например, выражение [x*x for x in a if x > 0] сходно с заданием множества { x
    2
    | x
    ∈ a, x > 0 }.
    Вместо того чтобы писать программы, которые вручную выполняют обход данных, с помощью этих декларативных возможностей можно структури- ровать программу, представив ее как серию операций, манипулирующих всеми данными сразу. Например, предположим, что имеется файл «port- port- folio.txt», содержащий информацию о ценных бумагах, имеющихся в на-
    .txt», содержащий информацию о ценных бумагах, имеющихся в на- txt», содержащий информацию о ценных бумагах, имеющихся в на-
    », содержащий информацию о ценных бумагах, имеющихся в на- личии, например:
    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
    Ниже приводится программа, написанная в декларативном стиле, которая вычисляет общую стоимость, суммируя произведения значений во втором и в третьем столбцах:
    lines = open(“portfolio.txt”)
    fields = (line.split() for line in lines)
    print(sum(float(f[1]) * float(f[2]) for f in fields))
    В этой программе отсутствует цикл построчного чтения содержимого фай- ла. Вместо этого здесь просто объявляется последовательность операций, которые должны быть выполнены над всеми данными. При таком подходе мы получаем очень компактный программный код, который, к тому же, выполняется быстрее, чем более традиционная версия:
    total = 0
    for line in open(“portfolio.txt”):
    fields = line.split()

    152
    Глава 6. Функции и функциональное программирование total += float(fields[1]) * float(fields[2])
    print(total)
    Декларативный стиль программирования отчасти напоминает операции, которые можно выполнять в командной оболочке UNIX. Например, преды- дущий пример, использующий выражения-генераторы, можно записать в виде однострочной команды awk:
    % awk ‘{ total += $2 * $3} END { print total }’ portfolio.txt
    44671.2
    %
    Кроме того, декларативный стиль генераторов списков и выражений-ге не- раторов можно использовать для имитации поведения инструкции select в языке SQL, часто применяемой при работе с базами данных. Например, рассмотрим следующий пример, обрабатывающий данные, получаемые из списка словарей:
    fields = (line.split() for line in open(“portfolio.txt”))
    portfolio = [ {‘name’ : f[0],
    ‘shares’ : int(f[1]),
    ‘price’ : float(f[2]) }
    for f in fields]
    ёё
    # Несколько запросов msft = [s for s in portfolio if s[‘name’] == ‘MSFT’]
    large_holdings = [s for s in portfolio if s[‘shares’]*s[‘price’] >= 10000]
    На практике при работе с модулями, реализующими доступ к базе данных
    (глава 17), часто бывает удобно объединять генераторы списков и запросы к базе данных в единое целое. Например:
    sum(shares*cost for shares,cost in cursor.execute(“select shares, cost from portfolio”)
    if shares*cost >= 10000)
    Оператор lambda
    С помощью инструкции lambda можно создавать анонимные функции, име- ющие форму выражения:
    lambda args :
    expression
    args
    – это список аргументов, разделенных запятыми, а expression – выра- жение, использующее эти аргументы. Например:
    a = lambda x,y : x+y r = a(2,3) # r получит значение 5
    Программный код в инструкции lambda должен быть допустимым выраже- нием. Внутри инструкции lambda нельзя использовать несколько инструк- ций или использовать другие инструкции, не являющиеся выражениями, такие как for или while. lambda-выражения подчиняются тем же правилам области видимости, что и функции.

    Рекурсия
    153
    Основное назначение lambda-выражений состоит в том, чтобы обеспечить возможность объявления коротких функций обратного вызова. Например, сортировку списка имен без учета регистра символов можно было бы реа- лизовать так:
    names.sort(key=lambda n: n.lower())
    Рекурсия
    Рекурсивные вызовы функций оформляются очень просто. Например:
    def factorial(n):
    if n <= 1: return 1
    else: return n * factorial(n - 1)
    Однако следует помнить, что существует ограничение на глубину рекур- сии. Текущее максимальное значение количества рекурсивных вызовов можно получить с помощью функции sys.getrecursionlimit(), а изменить – с помощью функции sys.setrecursionlimit(). По умолчанию этот предел ра- вен 1000. Несмотря на существующую возможность изменить это значение, глубина рекурсии в программах по-прежнему ограничена размером стека, который устанавливается операционной системой. По достижении макси- мальной глубины рекурсии возбуждается исключение RuntimeError. Интер- претатор Python не предусматривает оптимизацию хвостовой рекурсии, которая зачастую поддерживается функциональными языками програм- мирования, такими как Scheme.
    Как и следовало бы ожидать, рекурсия не может использоваться в функци- ях-генераторах и в сопрограммах. Например, следующий фрагмент выво- дит все элементы многоуровневой коллекции списков:
    def flatten(lists):
    for s in lists:
    if isinstance(s,list):
    flatten(s)
    else:
    print(s)
    ёё
    items = [[1,2,3],[4,5,[5,6]],[7,8,9]]
    flatten(items) # Выведет 1 2 3 4 5 6 7 8 9
    Но если заменить оператор print инструкцией yield, эта функция переста- нет работать. Это обусловлено тем, что рекурсивный вызов flatten() просто приведет к созданию еще одного объекта генератора, по которому факти- чески не будет выполнено ни одной итерации. Ниже приводится действую- щая рекурсивная версия генератора:
    def genflatten(lists):
    for s in lists:
    if isinstance(s,list):
    for item in genflatten(s):
    yield item else:
    yield item

    154
    Глава 6. Функции и функциональное программирование
    Н
    ужно внимательно отслеживать использование совместно рекурсивных функций и декораторов. Если применить декоратор к рекурсивной функ- ции, все внутренние рекурсивные вызовы будут обращаться к декориро- ванной версии. Например:
    @locked def factorial(n):
    if n <= 1: return 1
    else: return n * factorial(n - 1) # Вызов обернутой версии factorial
    Если декоратор имеет какое-либо отношение к управлению системными ресурсами, такими как механизмы синхронизации или блокировки, от ре- курсии лучше воздержаться.
    Строки документирования
    На практике часто можно увидеть на месте первой инструкции функции строку документирования, описывающую порядок использования этой функции. Например:
    def factorial(n):
    “””Вычисляет факториал числа n. Например:
    ёё
    >>> factorial(6)
    120
    >>>
    “””
    if n <= 1: return 1
    else: return n*factorial(n-1)
    Строка документирования сохраняется в атрибуте __doc__ функции, кото- рый часто используется интегрированными средами разработки для пре- доставления интерактивной справки.
    Однако обертывание функций с помощью декораторов может препятство- вать работе справочной системы, связанной со строками документирова- ния. Рассмотрим в качестве примера следующий фрагмент:
    def wrap(func):
    call(*args,**kwargs):
    return func(*args,**kwargs)
    return call
    @wrap def factorial(n):
    “””Вычисляет факториал числа n.”””
    ...
    Если теперь запросить справочную информацию для этой версии функции factorial()
    , интерпретатор вернет довольно странное пояснение:
    >>> help(factorial)
    Help on function call in module __main__:
    (Справка для функции call в модуле __main__:)
    ёё

    Атрибуты функций
    155
    call(*args, **kwargs)
    (END)
    >>>
    Чтобы исправить эту проблему, декоратор функций должен скопировать имя и строку документирования оригинальной функции в соответствую- щие атрибуты декорированной версии. Например:
    def wrap(func):
    call(*args,**kwargs):
    return func(*args,**kwargs)
    call.__doc__ = func.__doc__
    call.__name__ = func.__name__
    return call
    Для решения этой достаточно типичной проблемы в модуле functools име- ется функция wraps, которая автоматически копирует эти атрибуты. Не- удивительно, что она тоже является декоратором: from functools import wraps def wrap(func):
    @wraps(func)
    call(*args,**kwargs):
    return func(*args,**kwargs)
    return call
    Декоратор @wraps(func), объявленный в модуле functools, копирует атрибу- ты функции func в атрибуты обернутой версии функции.
    Атрибуты функций
    Функции допускают возможность присоединения к ним любых атрибутов.
    Например:
    def foo():
    инструкции
    ёё
    foo.secure = 1
    foo.private = 1
    Атрибуты функции сохраняются в словаре, доступном в виде атрибута
    __dict__
    функции.
    В основном атрибуты функций используются в узкоспециализированных приложениях, таких как генераторы парсеров и прикладные фреймворки, которые часто пользуются возможностью присоединения дополнительной информации к объектам функций.
    Как и в случае со строками документирования, атрибуты функций тре- буют особого внимания при использовании декораторов. Если некоторая функция обернута декоратором, попытки обращения к ее атрибутам фак- тически будут переадресовываться к декорированной версии. Это может быть как желательно, так и нежелательно, в зависимости от потребностей приложения. Чтобы скопировать уже определенные атрибуты функции

    156
    Глава 6. Функции и функциональное программирование в атрибуты декорированной версии, можно использовать приведенный да- лее подход или воспользоваться декоратором functools.wraps(), как было по- казано в предыдущем разделе:
    def wrap(func):
    call(*args,**kwargs):
    return func(*args,**kwargs)
    call.__doc__ = func.__doc__
    call.__name__ = func.__name__
    call.__dict__.update(func.__dict__)
    return call
    Функции eval(), exec() и compile()
    Функция eval(str [,globals [,locals]]) выполняет выражение в строке str и возвращает результат. Например:
    a = eval(‘3*math.sin(3.5+x) + 7.2’)
    Аналогично функция exec(str [, globals [, locals]]) выполняет строку str, содержащую произвольный программный код на языке Python. Этот про-
    Python. Этот про-
    . Этот про- граммный код выполняется так, как если бы он фактически находился на месте exec(). Например:
    a = [3, 5, 10, 13]
    exec(“for i in a: print(i)”)
    Следует отметить, что в Python 2 функция exec() определена как инструк- ция. То есть в устаревшем программном коде можно увидеть обращения к инструкции exec, в которой отсутствуют круглые скобки, например: exec
    “for i in a: print i”
    . Такой способ по-прежнему можно использовать в вер- сии Python 2.6, но он считается недопустимым в Python 3. В новых про-
    Python 2.6, но он считается недопустимым в Python 3. В новых про-
    2.6, но он считается недопустимым в Python 3. В новых про-
    Python 3. В новых про-
    3. В новых про- граммах эта инструкция должна вызываться как функция exec().
    Обе эти функции выполняют программный код в пространстве имен вызы- вающего программного кода (которое используется для разрешения любых имен, появляющихся в строке или в файле). Кроме того, функции eval() и exec() могут принимать один или два необязательных объекта отображе- ний, которые для выполняемого программного кода будут играть роль гло- бального и локального пространств имен соответственно. Например:
    globals = {‘x’: 7,
    ‘y’: 10,
    ‘birds’: [‘Parrot’, ‘Swallow’, ‘Albatross’]
    }
    locals = { }
    ёё
    # Словари, объявленные выше, используются, как глобальное и
    # локальное пространства имен при выполнении следующей инструкции a = eval(“3 * x + 4 * y”, globals, locals)
    exec(“for b in birds: print(b)”, globals, locals)
    Если опустить одно из этих пространств имен, будут использоваться теку- щие глобальное или локальное пространство имен. Кроме того, из-за про-

    Функции eval(), exec() и compile()
    157
    блем, связанных с возможностью вложения областей видимости, вызов exec()
    внутри некоторой функции может приводить к исключению Syntax-
    Error
    , если эта функция содержит вложенные определения функций или lambda
    -операторы.
    При передаче строки функции exec() или eval() интерпретатор сначала скомпилирует ее в байт-код. Это делает весь процесс достаточно дорогосто- ящим делом, поэтому если программный код предполагается использовать неоднократно, его лучше сначала скомпилировать, а затем пользоваться скомпилированной версией.
    Функция compile(str,filename,kind) компилирует строку в байт-код, где str – это строка с программным кодом, подлежащим компиляции, а filename – файл, в котором эта строка определена (для использования в сообщениях с трассировочной информацией). Аргумент kind определяет тип компи- лируемого программного кода: ‘single’ – для единственной инструкции,
    ‘exec’
    – для множества инструкций и ‘eval’ – для выражений. Объект с программным кодом, возвращаемый функцией compile(), может переда- ваться функции eval() и инструкции exec(). Например:
    s = “for i in range(0,10): print(i)”
    c = compile(s,’’,’exec’) # Скомпилировать в объект с программным кодом exec(c) # Выполнить
    ёё
    s2 = “3 * x + 4 * y”
    c2 = compile(s2, ‘’, ‘eval’) # Скомпилировать в выражение result = eval(c2) # Выполнить

    1   ...   8   9   10   11   12   13   14   15   ...   82


    написать администратору сайта