Чи потрібно передавати файли для відкриття або відкривати файли?


53

Припустимо, у мене є функція, яка виконує дії з текстовим файлом - наприклад, читає з нього та видаляє слово "a". Я можу або передати йому ім'я файлу та обробити функцію відкриття / закриття, або я міг би передати йому відкритий файл і очікувати, що той, хто його зателефонує, вирішить його закрити.

Перший спосіб здається кращим способом гарантувати, що жодні файли не залишаються відкритими, але заважає використовувати такі речі, як 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)

Чи є одна з цих загальних переваг? Чи взагалі очікується, що функція буде вести себе одним із цих двох способів? Або це має бути просто добре зафіксовано таким чином, щоб програміст міг використовувати цю функцію як слід?

Відповіді:


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) ". Необов’язковий аргумент "stream_name" your_functionможна використовувати в цьому відношенні.

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')та ввійти в рядок, TypeErrorякщо fileobjце не рядок.
user2357112

Ви сперечаєтесь про те, що введення качки використовується . Приклад забезпечує качку введення в дію --that є, користувачі отримують verоперацію незалежно від типу. Можливо, це також можливо реалізувати за verдопомогою набору качок, як ви говорите. Але генерування тоді вилучень винятків відбувається повільніше, ніж просте обстеження типу, і ІМО не приносить особливої ​​користі (ясність, загальність тощо). На мій досвід, друк качок є приголомшливим "у великому", але нейтральним до контрпродуктивного "у малому . "
Джонатан Юніс

3
Ні, те, що ти все ще робиш, не набирає качки. hasattr(fileobj, 'read')Тест буде качиної типізації; isinstance(fileobj, str)тест не є. Ось приклад різниці: isinstanceтест не вдається з іменами юнікоду, оскільки u'adsf.txt'це не a 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

Якщо ви передасте ім'я файлу навколо замість ручки файлу, тоді немає гарантії, що другий файл є тим самим файлом, що і перший, коли він відкривається; це може призвести до коректності помилок та безпечних отворів.


1
Правда. Але це повинно бути врівноважене з іншим компромісом: якщо ви обходите ручку файлу, усі читачі повинні координувати їх доступ до файлу, оскільки кожен, ймовірно, перемістить "поточну позицію файлу".
Джонатан Юніс

@JonathanEunice: Координація в якому сенсі? Все, що їм потрібно зробити, - це встановити положення файлу там, де вони хочуть.
Мехрдад

1
Якщо файл читає файл декількома сутностями, можуть виникнути залежності. Можливо, потрібно починати там, де інший зупинився (або в місці, визначеному даними, прочитаними попереднім читанням). Також читачі можуть працювати в різних нитках, відкриваючи інші координаційні банки з хробаками. Обмінені файлові об'єкти стають відкритими глобальним станом, з усіма проблемами (а також перевагами), що тягнуть за собою.
Джонатан Юніс

1
Це не ключовий шлях файлу. Маючи одну функцію (або клас, метод чи інший локус контролю), беруть на себе відповідальність за "повну обробку файлу". Якщо доступ до файлів десь інкапсульований , вам не потрібно обходити мінливий глобальний стан, наприклад, ручки відкритого файлу.
Джонатан Юніс

1
Що ж, ми можемо погодитися тоді не погодитися. Я кажу, що є вирішений мінус дизайну, який грізно проходить навколо мінливого глобального стану. Є і деякі переваги. Таким чином, "компроміс". Конструкції, які передають файлові шляхи, часто роблять введення-виведення одним махом, укладеним способом. Я бачу це як вигідне з'єднання. YMMV.
Джонатан Юніс

1

Йдеться про право власності та відповідальність за закриття файлу. Ви можете передати потік або обробку файлів або будь-що, що має бути закрито / розміщено в якийсь момент іншим методом, якщо ви впевнені, що їм належить, і впевнені, що його буде закрито власником, коли ви закінчите . Це, як правило, включає спробну конструкцію або одноразовий малюнок.


-1

Якщо ви вирішите передати відкриті файли, ви можете зробити щось на кшталт наступного, АЛЕ у вас немає доступу до імені файлу у функції, яка записується у файл.

Я би зробив це, якби я хотів мати клас, який на 100% відповідає за операції з файлами / потоками та інші класи або функції, які були б наївними і не очікували відкривати або закривати вказані файли / потоки.

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

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)

Як це краще / відрізняється від простого використання with open? Як це вирішує питання щодо використання файлових файлів проти файлоподібних об'єктів?
Даннно

Це показує вам спосіб приховати файл / потік відкрити / закрити поведінку. Як ви добре бачите в коментарях, це дає вам можливість додати логіку перед відкриттям потоку / файлу, прозорого для "автора". "Письменник" міг бути методом класу іншого пакету. По суті це обгортка відкритого. Також дякую за відповіді та голосування.
Vls

Ця поведінка вже вирішується, with openправда? І те, за що ви ефективно виступаєте, - це функція, яка використовує лише файлові об'єкти, і не байдуже, звідки вона з'явилася?
Даннно
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.