Как отличить файл как объект от пути к файлу, например объекта

Резюме:

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


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

spiff = MySpiffy()

def spiffy_file_makerA(spiffy_obj, file):
    file_str = '\n'.join(spiffy_obj.gen_file()) 
    file.write(file_str)

with open('spiff.out', 'x') as f:
    spiffy_file_makerA(spiff, f)
    ...do other stuff with f...

Это работает. Ура. Но я бы предпочел не беспокоиться о том, чтобы сначала открывать файл или передавать потоки, по крайней мере иногда... поэтому я реорганизую возможность использовать путь к файлу, подобный объекту, вместо файла, подобного объекту, и return:

def spiffy_file_makerB(spiffy_obj, file, mode):
    file_str = '\n'.join(spiffy_obj.gen_file()) 
    file = open(file, mode)
    file.write(file_str)
    return file

with spiffy_file_makerB(spiff, 'file.out', 'x') as f:
    ...do other stuff with f...

Но теперь я понимаю, что было бы полезно иметь третью функцию, которая объединяет две другие версии в зависимости от того, является ли file файлом или файловым путем, но возвращает файл назначения f, например объект, контекстный менеджер. Чтобы я мог писать код следующим образом:

with  spiffy_file_makerAB(spiffy_obj, file_path_like, mode = 'x') as f:
    ...do other stuff with f...

... но также вот так:

file_like_obj = get_some_socket_or_stream()

with spiffy_file_makerAB(spiffy_obj, file_like_obj, mode = 'x'):
    ...do other stuff with file_like_obj...
    # file_like_obj stream closes when context manager exits 
    # unless `closefd=False` 

Обратите внимание, что это потребует чего-то немного другого, чем приведенные выше упрощенные версии.

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

def spiffy_file_makerAB(spiffy_obj, file, mode, *, closefd=True):
    try: 
        # file-like (use the file descriptor to open)
        result_f = open(file.fileno(), mode, closefd=closefd)
    except TypeError: 
        # file-path-like
        result_f = open(file, mode)
    finally: 
        file_str = '\n'.join(spiffy_obj.gen_file()) 
        result_f.write(file_str)
        return result_f

Есть ли какие-либо предложения для лучшего способа? Неужели я так далеко от базы и должен заниматься этим совершенно по-другому?

Ответ 1

Для моих денег, и это упрямый ответ, проверка атрибутов файлового типа для операций, которые вам понадобятся, - это a питонический способ определения типа объектов, потому что это характер pythonic утиные тесты/утка-типизация:

Утиная печать широко используется в Python, причем канонический пример является файлоподобным классам (например, cStringIO позволяет обрабатывать строку Python как файл).

Или из определения duck-typing docs-docs

Стиль программирования, который не смотрит на тип объектов, чтобы определить, имеет ли он правильный интерфейс; вместо этого метод или атрибут просто вызывается или используется ( "Если он выглядит как утка и шарлатанцы, как утка, это должна быть утка".) Подчеркивая интерфейсы, а не конкретные типы, хорошо продуманный код улучшает его гибкость, позволяя полиморфное замещение. Утиная печать избегает тестов с использованием type() или isinstance(). (Обратите внимание, однако, что утиная печать может быть дополнена абстрактными базовыми классами.) Вместо этого обычно используются тесты hasattr() или программирование EAFP.

Если вы чувствуете себя очень сильно, что есть очень веская причина, что просто проверка интерфейса на пригодность недостаточно, вы можете просто отменить тест и тест для basestring или str, чтобы проверить, предоставлен ли предоставленный объект путь типа. Тест будет отличаться в зависимости от вашей версии python.

is_file_like = not isinstance(fp, basestring) # python 2
is_file_like = not isinstance(fp, str) # python 3

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

class SpiffyContextGuard(object):
    def __init__(self, spiffy_obj, file, mode, closefd=True):
        self.spiffy_obj = spiffy_obj
        is_file_like = all(hasattr(attr) for attr in ('seek', 'close', 'read', 'write'))
        self.fp = file if is_file_like else open(file, mode)
        self.closefd = closefd

    def __enter__(self):
        return self.fp

    def __exit__(self, type_, value, traceback):
        generated = '\n'.join(self.spiffy_obj.gen_file())
        self.fp.write(generated)
        if self.closefd:
            self.fp.__exit__()

И затем используйте его следующим образом:

with SpiffyContextGuard(obj, 'hamlet.txt', 'w', True) as f:
    f.write('Oh that this too too sullied flesh\n')

fp = open('hamlet.txt', 'a')
with SpiffyContextGuard(obj, fp, 'a', False) as f:
    f.write('Would melt, thaw, resolve itself into a dew\n')

with SpiffyContextGuard(obj, fp, 'a', True) as f:
    f.write('Or that the everlasting had not fixed his canon\n')

Если вы хотите использовать семантику try/catch для проверки пригодности типа, вы можете также обернуть операции с файлами, которые вы хотели бы открыть в своем контексте:

class SpiffyContextGuard(object):
    def __init__(self, spiffy_obj, file, mode, closefd=True):
        self.spiffy_obj = spiffy_obj
        self.fp = self.file_or_path = file 
        self.mode = mode
        self.closefd = closefd

    def seek(self, offset, *args):
        try:
            self.fp.seek(offset, *args)
        except AttributeError:
            self.fp = open(self.file_or_path, mode)
            self.fp.seek(offset, *args)

    # define wrappers for write, read, etc., as well

    def __enter__(self):
        return self

    def __exit__(self, type_, value, traceback):
        generated = '\n'.join(self.spiffy_obj.gen_file())
        self.write(generated)
        if self.closefd:
            self.fp.__exit__()

Ответ 2

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

У меня было бы только две функции: spiffy_file_makerA(spiffy_obj, file), которая обрабатывает ваш первый случай, и функцию удобства, которая обертывает spiffy_file_makerA и создает файл для вас.

Ответ 3

Мое предложение состоит в том, чтобы передать pathlib.Path objects. вы можете просто .write_bytes(...) или .write_text(...) к этим объектам.

другое, что вам нужно будет проверить тип вашей переменной file (так как полиморфизм может быть выполнен в python):

from io import IOBase

def some_function(file)
    if isinstance(file, IOBase):
        file.write(...)
    else:
        with open(file, 'w') as file_handler:
            file_handler.write(...)

(надеюсь, что io.IOBase является самым основным классом для проверки...). и вам придется поймать возможные исключения во всем этом.

Ответ 4

Другой подход к этой проблеме, вдохновленный этим разговором от Раймондом Хеттингером в PyCon 2013, заключался бы в том, чтобы обе функции были разделены, как было предложено двумя другими ответами, но объединить функции в класс с рядом альтернативных вариантов вывода объекта.

Продолжая пример, с которого я начал, он может выглядеть примерно так:

class SpiffyFile(object):
    def __init__(self, spiffy_obj, file_path = None, *, mode = 'w'):
        self.spiffy = spiffy_obj
        self.file_path = file_path
        self.mode = mode
    def to_str(self):
        return '\n'.join(self.spiffy.gen_file())
    def to_stream(self, fstream):
        fstream.write(self.to_str())
    def __enter__(self):
        try:
            # do not override an existing stream
            self.fstream
        except AttributeError:
            # convert self.file_path to str to allow for pathlib.Path objects
            self.fstream = open(str(self.file_path), mode = self.mode)
        return self
    def __exit__(self, exc_t, exc_v, tb):
        self.fstream.close()
        del self.fstream
    def to_file(self, file_path = None, mode = None):
        if mode is None:
            mode = self.mode
        try:
            fstream = self.fstream
        except AttributeError:
            if file_path is None:
                file_path = self.file_path
            # convert file_path to str to allow for pathlib.Path objects
            with open(str(file_path), mode = mode) as fstream:
                self.to_stream(fstream)
        else:
            if mode != fstream.mode:
                raise IOError('Ambiguous stream output mode: \
                           provided mode and fstream.mode conflict')
            if file_path is not None:
                raise IOError('Ambiguous output destination: \
                           a file_path was provided with an already active file stream.')
            self.to_stream(fstream)

Теперь у нас есть много разных опций для экспорта объекта MySpiffy с помощью объекта SpiffyFile. Мы можем просто записать его в файл напрямую:

from pathlib import Path
spiff = MySpiffy()
p = Path('spiffies')/'new_spiff.txt'
SpiffyFile(spiff, p).to_file()

Мы также можем переопределить путь:

SpiffyFile(spiff).to_file(p.parent/'other_spiff.text')

Но мы также можем использовать существующий открытый поток:

SpiffyFile(spiff).to_stream(my_stream)

Или, если мы хотим сначала отредактировать строку, мы можем сами открыть новый поток файлов и записать отредактированную строку:

my_heading = 'This is a spiffy object\n\n'
with open(str(p), mode = 'w') as fout:
    spiff_out = SpiffyFile(spiff).to_str()
    fout.write(my_heading + spiff_out)

И, наконец, мы можем просто использовать диспетчер контекста с объектом SpiffyFile непосредственно для множества разных местоположений или потоков - как нам нравится (обратите внимание, что мы можем напрямую передать объект pathlib.Path, не беспокоясь о преобразовании строк, который является изящным):

with SpiffyFile(spiff, p) as spiff_file:
    spiff_file.to_file()
    spiff_file.to_file(p.parent/'new_spiff.txt')
    print(spiff_file.to_str())
    spiff_file.to_stream(my_open_stream)

Этот подход более согласуется с мантрой: явный лучше, чем неявный.