Tôi nên vượt qua tên tập tin để được mở, hoặc mở tập tin?


53

Giả sử tôi có một chức năng thực hiện mọi thứ với tệp văn bản - ví dụ đọc từ nó và xóa từ 'a'. Tôi có thể truyền cho nó một tên tệp và xử lý việc mở / đóng trong hàm, hoặc tôi có thể chuyển nó vào tệp đã mở và hy vọng rằng bất cứ ai gọi nó sẽ xử lý việc đóng nó.

Cách đầu tiên có vẻ như là một cách tốt hơn để đảm bảo không có tệp nào bị bỏ ngỏ, nhưng ngăn tôi sử dụng những thứ như các đối tượng StringIO

Cách thứ hai có thể hơi nguy hiểm - không có cách nào để biết liệu tệp có bị đóng hay không, nhưng tôi có thể sử dụng các đối tượng giống như tệp

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)

Là một trong những thường được ưa thích? Có phải nói chung dự kiến ​​rằng một chức năng sẽ hành xử theo một trong hai cách này? Hoặc nó chỉ nên được ghi chép tốt để lập trình viên có thể sử dụng chức năng cho phù hợp?

Câu trả lời:


39

Giao diện thuận tiện là tốt đẹp, và đôi khi cách để đi. Tuy nhiên, hầu hết thời gian khả năng kết hợp tốt quan trọng hơn sự tiện lợi , vì sự trừu tượng hóa có thể tổng hợp cho phép chúng tôi thực hiện các chức năng khác (bao gồm các trình bao bọc tiện lợi) trên đầu trang.

Cách chung nhất để chức năng của bạn sử dụng tệp là lấy một tệp xử lý mở làm tham số, vì điều này cho phép nó cũng sử dụng các thẻ điều khiển không phải là một phần của hệ thống tệp (ví dụ: ống, ổ cắm, Thẻ):

def your_function(open_file):
    return do_stuff(open_file)

Nếu đánh vần with open(filename, 'r') as f: result = your_function(f)quá nhiều để hỏi người dùng của bạn, bạn có thể chọn một trong các giải pháp sau:

  • your_functionlấy một tệp đang mở hoặc một tên tệp làm tham số. Nếu đó là tên tệp, tệp sẽ được mở và đóng và ngoại lệ được truyền bá. Có một chút vấn đề với sự mơ hồ ở đây có thể được giải quyết bằng cách sử dụng các đối số được đặt tên.
  • Cung cấp một trình bao bọc đơn giản chăm sóc mở tệp, ví dụ:

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

    Tôi thường cảm nhận các chức năng như API bloat, nhưng nếu chúng cung cấp chức năng thường được sử dụng, thì sự thuận tiện thu được là một đối số đủ mạnh.

  • Gói with openchức năng trong một chức năng tổng hợp khác:

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

    được sử dụng như with_file(name, your_function)hoặc trong các trường hợp phức tạp hơnwith_file(name, lambda f: some_function(1, 2, f, named=4))


6
Hạn chế duy nhất của phương pháp này là đôi khi cần phải có tên của đối tượng giống như tệp, ví dụ: để báo cáo lỗi: người dùng cuối thích xem "Lỗi trong foo.cfg (12)" thay vì "Lỗi trong <stream @ 0x03fd2bb6> (12) ". Một đối số "stream_name" tùy chọn your_functioncó thể được sử dụng trong vấn đề này.

22

Câu hỏi thực sự là một trong những đầy đủ. Là chức năng xử lý tệp của bạn là xử lý hoàn chỉnh tệp, hay chỉ là một phần trong chuỗi các bước xử lý? Nếu nó hoàn thành và tự nó, thì hãy đóng gói tất cả quyền truy cập tệp trong một hàm.

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

Điều này có thuộc tính rất hay là hoàn thiện tài nguyên (đóng tệp) vào cuối withcâu lệnh.

Tuy nhiên, nếu có thể cần xử lý một tệp đã mở, thì việc phân biệt của bạn ver_1ver_2có ý nghĩa hơn. Ví dụ:

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)

Đây là loại thử nghiệm loại rõ ràng thường được tán thành , đặc biệt là trong các ngôn ngữ như Java, Julia, và Go nơi loại- hoặc điều phối giao diện dựa trên được hỗ trợ trực tiếp. Tuy nhiên, trong Python, không có hỗ trợ ngôn ngữ nào cho việc gửi dựa trên kiểu. Thỉnh thoảng bạn có thể thấy những lời chỉ trích về kiểm tra kiểu trực tiếp trong Python, nhưng trong thực tế, nó cực kỳ phổ biến và khá hiệu quả. Nó cho phép một hàm có mức độ tổng quát cao, xử lý bất kỳ kiểu dữ liệu nào có khả năng đi theo cách của nó, còn gọi là "gõ vịt". Lưu ý gạch dưới hàng đầu trên _ver_file; đó là một cách thông thường để chỉ định một chức năng (hoặc phương thức) riêng tư. Mặc dù về mặt kỹ thuật có thể được gọi trực tiếp, nó cho thấy chức năng này không dành cho tiêu dùng bên ngoài trực tiếp.


Cập nhật 2019: Đưa ra các bản cập nhật gần đây trong Python 3, ví dụ: các đường dẫn hiện có khả năng được lưu trữ dưới dạng pathlib.Pathcác đối tượng không chỉ strhoặc bytes(3.4+) và gợi ý kiểu đó đã chuyển từ bí truyền sang chính thống (khoảng 3.6+, mặc dù vẫn đang tích cực phát triển), đây mã cập nhật đưa những tiến bộ này vào tài khoản:

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
Gõ vịt sẽ kiểm tra dựa trên những gì bạn có thể làm với đối tượng, thay vì kiểu của nó. Ví dụ: cố gắng gọi readmột cái gì đó có thể giống như tệp hoặc gọi open(fileobj, 'r')và bắt TypeErrornếu fileobjkhông phải là một chuỗi.
user2357112

Bạn đang tranh luận về việc gõ vịt đang sử dụng . Ví dụ cung cấp gõ vịt có hiệu lực - đó là, người dùng có được verhoạt động độc lập với loại. Cũng có thể thực hiện verthông qua gõ vịt, như bạn nói. Nhưng việc tạo ra các ngoại lệ bắt được chậm hơn so với kiểm tra loại đơn giản và IMO không mang lại bất kỳ lợi ích cụ thể nào (rõ ràng, tổng quát, v.v.) Theo kinh nghiệm của tôi, gõ vịt là tuyệt vời "trong lớn," nhưng trung tính đến phản tác dụng "trong nhỏ . "
Jonathan Eunice

3
Không, những gì bạn đang làm vẫn không phải là gõ vịt. Một hasattr(fileobj, 'read')bài kiểm tra sẽ là gõ vịt; một isinstance(fileobj, str)bài kiểm tra là không. Đây là một ví dụ về sự khác biệt: isinstancethử nghiệm thất bại với tên tệp unicode, vì u'adsf.txt'không phải là một str. Bạn đã thử nghiệm cho một loại quá cụ thể. Một bài kiểm tra gõ vịt, cho dù dựa trên cuộc gọi openhoặc một số does_this_object_represent_a_filenamechức năng giả định , sẽ không có vấn đề đó.
dùng2357112

1
Nếu mã là mã sản xuất chứ không phải là một ví dụ giải thích, tôi cũng sẽ không gặp phải vấn đề đó, vì tôi sẽ không sử dụng is_instance(x, str)mà là một cái gì đó giống như is_instance(x, string_types), với string_typesthiết lập đúng cho hoạt động đúng trên PY2 và PY3. Đưa ra một cái gì đó giống như một chuỗi, versẽ phản ứng đúng; đưa ra một cái gì đó quacks như một tập tin, giống nhau. Đối với một người sử dụng của ver, thì sẽ không có sự khác biệt - ngoại trừ việc thực hiện kiểm tra loại sẽ chạy nhanh hơn. Vịt tinh khiết: cảm thấy tự do không đồng ý.
Jonathan Eunice

5

Nếu bạn chuyển tên tệp xung quanh thay vì xử lý tệp thì không có gì đảm bảo rằng tệp thứ hai là cùng tệp với tệp đầu tiên khi được mở; điều này có thể dẫn đến lỗi chính xác và lỗ hổng bảo mật.


1
Thật. Nhưng điều đó phải được đối trọng với một sự đánh đổi khác: Nếu bạn vượt qua một phần xử lý tệp, tất cả người đọc phải điều phối quyền truy cập của họ vào tệp, vì mỗi người có khả năng di chuyển "vị trí tệp hiện tại".
Jonathan Eunice

@JonathanEunice: Phối hợp theo nghĩa nào? Tất cả những gì họ cần làm là đặt vị trí tệp ở bất cứ nơi nào họ muốn.
Mehrdad

1
Nếu có nhiều thực thể đọc tệp, có thể có phụ thuộc. Người ta có thể cần phải bắt đầu khi một người khác rời đi (hoặc ở một nơi được xác định bởi dữ liệu được đọc bởi lần đọc trước). Ngoài ra, độc giả có thể đang chạy trong các chủ đề khác nhau, mở ra các hộp phối hợp khác của sâu. Các đối tượng tệp được truyền xung quanh trở thành trạng thái toàn cầu tiếp xúc, với tất cả các vấn đề (cũng như lợi ích) đòi hỏi.
Jonathan Eunice

1
Nó không đi qua đường dẫn tệp đó là chìa khóa. Nó có một chức năng (hoặc lớp, phương thức hoặc địa điểm kiểm soát khác) chịu trách nhiệm cho "việc xử lý hoàn chỉnh tệp." Nếu truy cập tệp được gói gọn ở đâu đó , thì bạn không cần phải chuyển qua trạng thái toàn cầu có thể thay đổi như xử lý tệp mở.
Jonathan Eunice

1
Vâng, chúng tôi có thể đồng ý để không đồng ý sau đó. Tôi đang nói rằng có một nhược điểm quyết định đối với các thiết kế có thể vượt qua xung quanh trạng thái toàn cầu có thể thay đổi. Có một số lợi thế, quá. Do đó, một "sự đánh đổi." Các thiết kế vượt qua các đường dẫn tệp thường làm I / O trong một cú trượt, theo cách được gói gọn. Tôi thấy đó là một khớp nối thuận lợi. YMMV.
Jonathan Eunice

1

Đây là về quyền sở hữu và trách nhiệm đóng tệp. Bạn có thể chuyển một luồng hoặc xử lý tệp hoặc bất cứ điều gì đáng lẽ phải đóng / xử lý tại một thời điểm nào đó cho phương thức khác, miễn là bạn chắc chắn ai là người sở hữu nó và chắc chắn nó sẽ bị chủ sở hữu đóng khi bạn hoàn thành . Điều này thường liên quan đến một cấu trúc thử cuối cùng hoặc mẫu dùng một lần.


-1

Nếu bạn chọn chuyển các tệp đang mở, bạn có thể thực hiện một số thứ như NHƯNG sau đây, bạn không có quyền truy cập vào tên tệp trong hàm ghi vào tệp.

Tôi sẽ làm điều này nếu tôi muốn có một lớp chịu trách nhiệm 100% cho các hoạt động tệp / luồng và các lớp hoặc chức năng khác sẽ ngây thơ và không mong muốn mở hoặc đóng các tệp / luồng đã nói.

Hãy nhớ rằng các nhà quản lý bối cảnh làm việc như có một mệnh đề cuối cùng. Vì vậy, nếu một ngoại lệ được ném vào chức năng nhà văn, tập tin sẽ bị đóng bất kể điều gì.

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)

Làm thế nào là tốt hơn / khác hơn là chỉ sử dụng with open? Làm thế nào để giải quyết câu hỏi về việc sử dụng tên tệp và các đối tượng giống như tệp?
Dannnno

Điều này cho bạn thấy một cách để ẩn hành vi mở / đóng tệp / luồng. Như bạn có thể thấy rõ trong các bình luận, nó cung cấp cho bạn cách thêm logic trước khi mở luồng / tệp trong suốt cho "người viết". "Nhà văn" có thể là một phương thức của một lớp của gói khác. Về bản chất nó là một gói của mở. Ngoài ra, cảm ơn bạn đã trả lời và bỏ phiếu.
Vls

Hành vi đó đã được xử lý with openmặc dù, phải không? Và những gì bạn ủng hộ một cách hiệu quả là một chức năng chỉ sử dụng các đối tượng giống như tệp và không quan tâm nó đến từ đâu?
Dannnno
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.