Должен ли я передать имена файлов, которые будут открыты, или открыть файлы?

53

Предположим, у меня есть функция, которая работает с текстовым файлом - например, читает из него и удаляет слово «а». Я мог бы либо передать ему имя файла и обработать открытие / закрытие в функции, либо я мог бы передать ему открытый файл и ожидать, что тот, кто его вызовет, будет иметь дело с его закрытием.

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

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

def ver_1(filename):
    with open(filename, 'r') as f:
        return do_stuff(f)

def ver_2(open_file):
    return do_stuff(open_file)

print ver_1('my_file.txt')

with open('my_file.txt', 'r') as f:
    print ver_2(f)

Является ли один из них вообще предпочтительным? Обычно ожидается, что функция будет вести себя одним из этих двух способов? Или это должно быть хорошо задокументировано, чтобы программист мог использовать эту функцию соответствующим образом?

Dannnno
источник

Ответы:

39

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

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

def your_function(open_file):
    return do_stuff(open_file)

Если изложение with open(filename, 'r') as f: result = your_function(f)слишком много, чтобы спросить у ваших пользователей, вы можете выбрать одно из следующих решений:

  • your_functionпринимает открытый файл или имя файла в качестве параметра. Если это имя файла, файл открывается и закрывается, а исключения распространяются. Здесь есть некоторая проблема с неоднозначностью, которую можно обойти, используя именованные аргументы.
  • Предложите простую оболочку, которая позаботится об открытии файла, например

    def your_function_filename(file):
        with open(file, 'r') as f:
            return your_function(f)

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

  • Оберните with openфункциональность в другую составную функцию:

    def with_file(filename, callback):
        with open(filename, 'r') as f:
            return callback(f)

    используется как with_file(name, your_function)или в более сложных случаяхwith_file(name, lambda f: some_function(1, 2, f, named=4))

Амон
источник
6
Единственный недостаток этого подхода заключается в том, что иногда требуется имя файлового объекта, например, для сообщения об ошибках: конечные пользователи предпочитают видеть «Ошибка в foo.cfg (12)», а не «Ошибка в <stream @ 0x03fd2bb6> (12)». В your_functionэтом отношении может использоваться необязательный аргумент «stream_name» .
22

Настоящий вопрос заключается в полноте. Является ли ваша функция обработки файлов полной обработкой файла, или это всего лишь одна часть в цепочке этапов обработки? Если он завершен сам по себе, то не стесняйтесь инкапсулировать весь доступ к файлам внутри функции.

def ver(filepath):
    with open(filepath, "r") as f:
        # do processing steps on f
        return result

Это имеет очень приятное свойство завершения ресурса (закрытия файла) в конце withоператора.

Однако если есть необходимость в обработке уже открытого файла, тогда различие между вами ver_1и ver_2имеет больший смысл. Например:

def _ver_file(f):
    # do processing steps on f
    return result

def ver(fileobj):
    if isinstance(fileobj, str):
        with open(fileobj, 'r') as f:
            return _ver_file(f)
    else:
        return _ver_file(fileobj)

Этот вид явного тестирования типов часто не одобряется , особенно в таких языках, как Java, Julia и Go, где непосредственная поддержка диспетчеризации на основе типов или интерфейсов поддерживается. В Python, однако, нет языковой поддержки для диспетчеризации на основе типов. Иногда вы можете увидеть критику прямого типового тестирования в Python, но на практике это очень распространенное и довольно эффективное действие. Это позволяет функции иметь высокую степень универсальности, обрабатывая любые типы данных, которые могут появиться на ее пути, то есть «типизацию утки». Обратите внимание на подчеркивание _ver_file; это обычный способ обозначения «частной» функции (или метода). Хотя технически это можно назвать напрямую, оно предполагает, что функция не предназначена для прямого внешнего потребления.


Обновление 2019: учитывая недавние обновления в Python 3, например, что пути теперь потенциально могут храниться как pathlib.Pathобъекты, а не просто strили bytes(3.4+), и что подсказка типа перешла от эзотерической к основной (около 3.6+, хотя все еще активно развивается), вот обновленный код, который учитывает эти достижения:

from pathlib import Path
from typing import IO, Any, AnyStr, Union

Pathish = Union[AnyStr, Path]  # in lieu of yet-unimplemented PEP 519
FileSpec = Union[IO, Pathish]

def _ver_file(f: IO) -> Any:
    "Process file f"
    ...
    return result

def ver(fileobj: FileSpec) -> Any:
    "Process file (or file path) f"
    if isinstance(fileobj, (str, bytes, Path)):
        with open(fileobj, 'r') as f:
            return _ver_file(f)
    else:
        return _ver_file(fileobj)
Джонатан Юнис
источник
1
Утиная печать будет проверяться на основе того, что вы можете сделать с объектом, а не на том, какой у него тип. Например, попытка вызвать readчто-то, похожее на файл, или вызов open(fileobj, 'r')и отлавливание TypeErrorif fileobjне является строкой.
user2357112
Вы утверждаете, что в печати используется утка . В примере приведен эффективный тип утки - пользователи получают verоперацию независимо от типа. Как вы могли бы verсказать, это также можно реализовать с помощью утки. Но генерирование тогда отлавливающих исключений происходит медленнее, чем простая проверка типов, и IMO не дает какой-либо конкретной выгоды (ясность, универсальность и т. Д.). По моему опыту, типизация утки замечательна "в целом", но нейтральна для контрпродуктивности "в малых «.
Джонатан Юнис
3
Нет, ты все еще не набираешь утку hasattr(fileobj, 'read')Тест будет утиной типизации; isinstance(fileobj, str)тест не является. Вот пример различия: isinstanceтест завершается неудачно с именами файлов в Юникоде, поскольку u'adsf.txt'не является str. Вы проверили на слишком конкретный тип. Тест на утку, основанный на вызове openили некоторой гипотетической does_this_object_represent_a_filenameфункции, не будет иметь такой проблемы.
user2357112
1
Если бы код был производственным кодом, а не пояснительным примером, у меня также не было бы этой проблемы, потому что я бы не использовал, is_instance(x, str)а скорее что-то вроде is_instance(x, string_types), с string_typesдолжным образом настроенным для правильной работы в PY2 и PY3. Учитывая что-то, что крякает как строка, verбудет правильно реагировать; дано то, что крякает как файл, то же самое. К пользователю о ver, не было бы никакой разницы - за исключением того, что осуществление инспекции типа будет работать быстрее. Пуристы утки: не стесняйтесь не соглашаться.
Джонатан Юнис
5

Если вы передаете имя файла вместо дескриптора файла, то нет никакой гарантии, что второй файл при открытии совпадает с первым; это может привести к исправлению ошибок и брешей в безопасности.

Mehrdad
источник
1
Правда. Но это должно быть уравновешено другим компромиссом: если вы передаете дескриптор файла, все читатели должны координировать свои обращения к файлу, потому что каждый, вероятно, переместит «текущую позицию файла».
Джонатан Юнис
@JonathanEunice: координировать, в каком смысле? Все, что им нужно сделать, - это установить положение файла так, как им нужно.
Мердад
1
Если есть несколько объектов, читающих файл, могут быть зависимости. Может потребоваться начать с того места, где остановился другой (или в месте, определяемом данными, прочитанными предыдущим чтением). Кроме того, читатели могут работать в разных потоках, открывая другие координационные банки червей. Передаваемые файловые объекты становятся открытыми в глобальном состоянии со всеми вытекающими отсюда проблемами (а также преимуществами).
Джонатан Юнис
1
Ключом не является путь к файлу. Он имеет одну функцию (или класс, метод или другой локус контроля), который берет на себя ответственность за «полную обработку файла». Если доступ к файлу где-то инкапсулирован , вам не нужно передавать изменяемое глобальное состояние, такое как дескрипторы открытого файла.
Джонатан Юнис
1
Ну, тогда мы можем согласиться не соглашаться. Я говорю, что есть определенные недостатки в дизайнах, которые легко передают изменчивое глобальное состояние. Есть и некоторые преимущества. Таким образом, «компромисс». Проекты, которые передают пути к файлам, часто выполняют ввод-вывод одним махом, инкапсулированным способом. Я вижу это как выгодное соединение. YMMV.
Джонатан Юнис
1

Это касается владения и ответственности за закрытие файла. Вы можете передать дескриптор потока или файла или любую другую вещь, которая должна быть закрыта / удалена в какой-то момент, другому методу, при условии, что вы уверены, что ясно, кто ей владеет, и уверены, что она будет закрыта владельцем, когда вы закончите. , Как правило, это включает в себя конструкцию try-finally или одноразовый шаблон.

Мартин Маат
источник
-1

Если вы решите передать открытые файлы, вы можете сделать что-то вроде следующего, НО у вас нет доступа к имени файла в функции, которая записывает в файл.

Я сделал бы это, если бы хотел иметь класс, который на 100% отвечал бы за операции с файлами / потоками, а также другие классы или функции, которые были бы наивными и не предполагали, что они будут открывать или закрывать указанные файлы / потоки.

Помните, что менеджеры контекста работают так, как будто в них есть предложение finally. Таким образом, если в функцию записи выдается исключение, файл будет закрыт, несмотря ни на что.

import contextlib

class FileOpener:

    def __init__(self, path_to_file):
        self.path_to_file = path_to_file

    @contextlib.contextmanager
    def open_write(self):
        # ...
        # Here you can add code to create the directory that will accept the file.
        # ...
        # And you can add code that will check that the file does not exist 
        # already and maybe raise FileExistsError
        # ...
        try:            
            with open(self.path_to_file, "w") as file:
                print(f"open_write: has opened the file with id:{id(file)}")            
                yield file                
        except IOError:
            raise
        finally:
            # The try/catch/finally is not mandatory (except if you want to manage Exceptions in an other way, as file objects have predefined cleanup actions 
            # and when used with a 'with' ie. a context manager (not the decorator in this example) 
            # are closed even if an error occurs. Finally here is just used to demonstrate that the 
            # file was really closed.
            print(f"open_write: has closed the file with id:{id(file)} - {file.closed}")        


def writer(file_open, data, raise_exc):
    with file_open() as file:
        print("writer: started writing data.")
        file.write(data)
        if raise_exc:
            raise IOError("I am a broken data cable in your server!")
        print("writer: wrote data.")
    print("writer: finished.")

if __name__ == "__main__":
    fo = FileOpener('./my_test_file.txt')    
    data = "Hello!"  
    raise_exc = False  # change me to True and see that the file is closed even if an Exception is raised.
    writer(fo.open_write, data, raise_exc)
Vls
источник
Чем это лучше / отличается от простого использования with open? Как это решает вопрос об использовании имен файлов против файловых объектов?
Данннно
Это показывает вам способ скрыть поведение открытия / закрытия файла / потока. Как вы можете ясно видеть в комментариях, это дает вам возможность добавить логику перед открытием потока / файла, который прозрачен для «писателя». «Writer» может быть методом класса другого пакета. По сути это обертка открытая. Также спасибо за ответ и голосование.
Vls
Это поведение уже обрабатывается, with openправда? И что вы эффективно защищаете, так это функцию, которая использует только файловые объекты и не заботится, откуда она взялась?
Данннно