Как обернуть или встроить генераторы?

#python #generator

#питон #генератор #уступка

Вопрос:

Я пытаюсь предоставить единый интерфейс для извлечения всех файлов из одного каталога или списка каталогов.

 def get_files(dir_or_dirs):
    def helper(indir):
        file_list = glob.glob("*.txt")
        for file in file_list:
            yield file

    if type(dir_or_dirs) is list:
        # a list of source dirs
        for dir in dir_or_dirs:
            yield helper(dir)
    else:
        # a single source dir
        yield helper(dir_or_dirs)

def print_all_files(file_iter):
    for file in file_iter:
        print(file)        # error here!
 

Вопросы:

  1. Ошибка гласит, что «файл» по-прежнему является генератором, независимо от того, является ли ввод одним каталогом или его списком. Почему это все еще генератор?
  2. Можно ли обернуть или встроить генераторы в функции? Если да, то как заставить это работать?

Комментарии:

1. Почему вы пытаетесь yield вернуть значение helper ? Помощник — это функция генератора, и она возвращает итератор генератора. Если вы хотите получить все, что выдает генератор, это yield from .

2. пытался получить генератор для всех файлов во всех каталогах. Спасибо за «выход из» хедз-апа!

Ответ №1:

Вы helper() каждый раз уступаете:

 yield helper(dir)
 

но helper() сам по себе является генератором.

В Python 3.3 и новее используйте yield from вместо:

 yield from helper(dir)
 

Это делегирует управление другому генератору. Из документации Yield expressions:

Когда yield from <expr> используется, он обрабатывает предоставленное выражение как подзаголовок. Все значения, создаваемые этим подзаголовком, передаются непосредственно вызывающей стороне методов текущего генератора.

В более старых версиях Python, включая Python 2.x, используйте другой цикл:

 for file in helper(dir):
    yield file
 

Для получения дополнительной информации о том, что yield from делает, см. PEP 380 — Синтаксис делегирования подгенератору .

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

Вам также необходимо исправить вашу функцию для фактического использования indir ; в настоящее время вы игнорируете этот аргумент, поэтому получаете только текстовые файлы из текущего рабочего каталога.

Далее, вы хотите использовать glob.iglob() вместо glob.glob() того, чтобы выполнять отложенную оценку os.scandir() , а не загружать все результаты в память сразу. Я бы просто превратил dir_or_dirs значение, отличное от списка, в список, а затем просто использовал один цикл:

 import glob
import os.path

def get_files(dirs):
    if not isinstance(dirs, list):
        # make it a list with one element
        dirs = [dirs]

    for dir in dirs:
        pattern = os.path.join(dir, '*.txt')
        yield from glob.iglob(pattern)
 

Теперь вместо одного аргумента, который является либо строкой, либо списком, я бы использовал переменное количество аргументов с синтаксисом *args параметра:

 def get_files(*dirs):
    for dir in dirs:
        pattern = os.path.join(dir, '*.txt')
        yield from glob.iglob(pattern)
 

Это может быть вызвано с 0 или более каталогами:

 for file in get_files('/path/to/foo', '/path/to/bar'):
    # ...
 

Комментарии:

1. Если вы просто хотите знать, можете ли вы что-то перебирать, isinstance(dirs, collections.abc.Iterable) лучше использовать проверку, поскольку это работает для любого итерируемого, а не только для списка. Более питоническим способом может быть использование dirs в качестве vararg: def get_files(*dirs) , тогда вызывающий может вызывать с помощью одного аргумента, varargs или списка get_dirs(*iterable_argument) .

2. @zstewart: нет, использование collections.abc.Iterable не лучшая проверка, потому что строки тоже являются итеративными. Затем один каталог будет обрабатываться как отдельные отдельные символы.

3. Спасибо! советы py2.7 сработали! Я тоже попробую с py3. Также спасибо за иллюстрацию и дополнительные советы по улучшению качества кода на python!

4. @MartijnPieters О, точно. Дух. Я бы лично все равно выбрал решение, которое работает для любого итеративного. *dirs может быть лучшим вариантом, поскольку он оставляет открытой утиную типизацию как отдельных аргументов, так и итеративного аргумента. Хотя это означает, что любой итерируемый аргумент оценивается с нетерпением. Другой вариант — явно проверить типы аргументов, принятых os.path.join ( str , bytes , и в версии 3.6 os.PathLike ), и преобразовать их только в новый одноэлементный список / кортеж. Тогда get_files можно было бы лениво оценить переданный ему ленивый итератор.

5. @zstewart: в этот момент вы просто опускаете ногу, и API принимает только одну итерацию каталогов и просто избегает ducktyping.