วิธีจัดการทั้ง "ด้วย open (... )` และ "sys.stdout" อย่างดี?


93

บ่อยครั้งที่ฉันต้องการส่งออกข้อมูลไปยังไฟล์หรือหากไม่ได้ระบุไฟล์เป็น stdout ฉันใช้ข้อมูลโค้ดต่อไปนี้:

if target:
    with open(target, 'w') as h:
        h.write(content)
else:
    sys.stdout.write(content)

ฉันต้องการเขียนมันใหม่และจัดการทั้งสองเป้าหมายอย่างสม่ำเสมอ

ในกรณีที่ดีที่สุดคือ:

with open(target, 'w') as h:
    h.write(content)

แต่จะไม่ได้ผลเพราะ sys.stdout ถูกปิดเมื่อออกจากwithบล็อกและฉันไม่ต้องการสิ่งนั้น ฉันไม่ต้องการ

stdout = open(target, 'w')
...

เพราะฉันจะต้องจำไว้ว่าต้องกู้คืนมาตรฐานเดิม

ที่เกี่ยวข้อง:

แก้ไข

ฉันรู้ว่าฉันสามารถtargetรวมกำหนดฟังก์ชันแยกหรือใช้ตัวจัดการบริบทได้ ฉันมองหาวิธีแก้ปัญหาที่เรียบง่ายสง่างามและใช้สำนวนที่ไม่ต้องใช้เกิน 5 บรรทัด


น่าเสียดายที่คุณไม่ได้เพิ่มการแก้ไขก่อนหน้านี้) อย่างไรก็ตาม ... หรือคุณไม่ต้องกังวลกับการล้างไฟล์ที่เปิดอยู่: P
Wolph

คำตอบ:


93

แค่คิดนอกกรอบที่นี่วิธีที่กำหนดเองopen()ล่ะ?

import sys
import contextlib

@contextlib.contextmanager
def smart_open(filename=None):
    if filename and filename != '-':
        fh = open(filename, 'w')
    else:
        fh = sys.stdout

    try:
        yield fh
    finally:
        if fh is not sys.stdout:
            fh.close()

ใช้แบบนี้:

# For Python 2 you need this line
from __future__ import print_function

# writes to some_file
with smart_open('some_file') as fh:
    print('some output', file=fh)

# writes to stdout
with smart_open() as fh:
    print('some output', file=fh)

# writes to stdout
with smart_open('-') as fh:
    print('some output', file=fh)

29

ยึดติดกับรหัสปัจจุบันของคุณ มันง่ายมากและคุณสามารถบอกได้อย่างชัดเจนว่ากำลังทำอะไรอยู่เพียงแค่เหลือบมอง

อีกวิธีหนึ่งคือการใช้อินไลน์if:

handle = open(target, 'w') if target else sys.stdout
handle.write(content)

if handle is not sys.stdout:
    handle.close()

แต่นั่นไม่ได้สั้นไปกว่าสิ่งที่คุณมีและมันดูแย่ลงอย่างเห็นได้ชัด

คุณสามารถทำให้ไม่สามารถsys.stdoutปิดได้ แต่ดูเหมือนว่าจะไม่เหมือน Pythonic:

sys.stdout.close = lambda: None

with (open(target, 'w') if target else sys.stdout) as handle:
    handle.write(content)

2
คุณสามารถเก็บความสามารถในการไม่เปิดเผยไว้ได้นานเท่าที่คุณต้องการโดยการสร้างตัวจัดการบริบทสำหรับมันเช่นกัน: with unclosable(sys.stdout): ...โดยการตั้งค่าsys.stdout.close = lambda: Noneภายในตัวจัดการบริบทนี้และรีเซ็ตเป็นค่าเก่าในภายหลัง แต่ดูเหมือนจะไกลเกินไป ...
glglgl

3
ฉันขาดระหว่างการโหวตให้ "ปล่อยไว้คุณบอกได้เลยว่ากำลังทำอะไรอยู่" และลงคะแนนให้กับคำแนะนำที่ไม่สามารถเปิดเผยได้!
GreenAsJade

@GreenAsJade ฉันไม่คิดว่าเขากำลังแนะนำให้ทำการsys.stdoutปิดไม่ได้เพียงแค่สังเกตว่ามันสามารถทำได้ เป็นการดีกว่าที่จะแสดงความคิดที่ไม่ดีและอธิบายว่าทำไมพวกเขาถึงไม่ดีมากกว่าที่จะไม่พูดถึงพวกเขาและหวังว่าพวกเขาจะไม่สะดุดเพราะคนอื่น
cjs

8

ทำไมต้อง LBYL เมื่อคุณสามารถ EAFP?

try:
    with open(target, 'w') as h:
        h.write(content)
except TypeError:
    sys.stdout.write(content)

ทำไมต้องเขียนซ้ำเพื่อใช้with/ asblock อย่างสม่ำเสมอในเมื่อคุณต้องทำให้มันทำงานในลักษณะที่ซับซ้อน คุณจะได้เพิ่มมากขึ้นเส้นและลดประสิทธิภาพ


3
ไม่ควรใช้ข้อยกเว้นเพื่อควบคุมการไหลของกิจวัตร "ปกติ" ประสิทธิภาพ? ข้อผิดพลาดที่เกิดขึ้นจะเร็วขึ้นถ้า / else?
Jakub M.

2
ขึ้นอยู่กับความน่าจะเป็นที่คุณจะใช้อย่างใดอย่างหนึ่ง
2rs2ts

31
@JakubM. ข้อยกเว้นสามารถควรและใช้เช่นนี้ใน Python
Gareth Latty

13
เมื่อพิจารณาว่าforลูปของ Python ออกโดยการจับข้อผิดพลาด StopIteration ที่เกิดจากตัววนซ้ำที่วนซ้ำฉันจะบอกว่าการใช้ข้อยกเว้นสำหรับการควบคุมการไหลนั้นเป็น Pythonic อย่างสิ้นเชิง
Kirk Strauser

1
สมมติว่าtargetเป็นNoneเมื่อ sys.stdout มีจุดมุ่งหมายที่คุณต้องการที่จะจับมากกว่าTypeError IOError
ฉีก

5

อีกวิธีหนึ่งที่เป็นไปได้: อย่าพยายามหลีกเลี่ยงวิธีการออกจากตัวจัดการบริบทเพียงทำซ้ำ stdout

with (os.fdopen(os.dup(sys.stdout.fileno()), 'w')
      if target == '-'
      else open(target, 'w')) as f:
      f.write("Foo")

5

การปรับปรุงคำตอบของ Wolph

import sys
import contextlib

@contextlib.contextmanager
def smart_open(filename: str, mode: str = 'r', *args, **kwargs):
    '''Open files and i/o streams transparently.'''
    if filename == '-':
        if 'r' in mode:
            stream = sys.stdin
        else:
            stream = sys.stdout
        if 'b' in mode:
            fh = stream.buffer  # type: IO
        else:
            fh = stream
        close = False
    else:
        fh = open(filename, mode, *args, **kwargs)
        close = True

    try:
        yield fh
    finally:
        if close:
            try:
                fh.close()
            except AttributeError:
                pass

สิ่งนี้ช่วยให้ไบนารี IO และส่งผ่านอาร์กิวเมนต์ที่ไม่เกี่ยวข้องในที่สุดไปยังopenif filenameเป็นชื่อไฟล์


1

ฉันจะเลือกใช้ฟังก์ชัน wrapper แบบธรรมดาซึ่งอาจค่อนข้างง่ายหากคุณสามารถเพิกเฉยต่อโหมด (และส่งผลให้ stdin เทียบกับ stdout) ตัวอย่างเช่น:

from contextlib import contextmanager
import sys

@contextmanager
def open_or_stdout(filename):
    if filename != '-':
        with open(filename, 'w') as f:
            yield f
    else:
        yield sys.stdout

โซลูชันนี้ไม่ได้ปิดไฟล์อย่างชัดเจนทั้งในแบบปกติหรือการยุติข้อผิดพลาดของ with clause ดังนั้นจึงมีตัวจัดการบริบทไม่มากนัก ชั้นเรียนที่ดำเนินการเข้าและออกจะเป็นทางเลือกที่ดีกว่า
tdelaney

1
ฉันได้รับValueError: I/O operation on closed fileถ้าฉันพยายามเขียนไปยังไฟล์นอกwith open_or_stdout(..)บล็อก ฉันขาดอะไรไป? sys.stdout ไม่ได้หมายถึงการปิด
Tommi Komulainen

1

เอาล่ะถ้าเราเข้าสู่สงครามสายการบินเดียวนี่คือ:

(target and open(target, 'w') or sys.stdout).write(content)

ฉันชอบตัวอย่างดั้งเดิมของเจคอบตราบใดที่มีการเขียนบริบทในที่เดียวเท่านั้น มันจะเป็นปัญหาถ้าคุณเปิดไฟล์ขึ้นมาใหม่เพื่อเขียนหลาย ๆ ฉันคิดว่าฉันจะตัดสินใจเพียงครั้งเดียวที่ด้านบนของสคริปต์และปล่อยให้ระบบปิดไฟล์เมื่อออก:

output = target and open(target, 'w') or sys.stdout
...
output.write('thing one\n')
...
output.write('thing two\n')

คุณสามารถรวมตัวจัดการทางออกของคุณเองได้หากคุณคิดว่าเป็นระเบียบเรียบร้อยมากขึ้น

import atexit

def cleanup_output():
    global output
    if output is not sys.stdout:
        output.close()

atexit(cleanup_output)

ฉันไม่คิดว่าซับในของคุณปิดอ็อบเจ็กต์ไฟล์ ฉันผิดเหรอ?
2rs2ts

1
@ 2rs2ts - มัน ... ตามเงื่อนไข จำนวนการอ้างอิงของออบเจ็กต์ไฟล์ไปที่ศูนย์เนื่องจากไม่มีตัวแปรที่ชี้ไปที่มันดังนั้นจึงสามารถใช้เมธอด __del__ ที่เรียกว่าทันที (ใน cpython) หรือในภายหลังเมื่อมีการรวบรวมขยะ มีคำเตือนในเอกสารว่าอย่าวางใจว่าสิ่งนี้จะใช้ได้ผลเสมอ แต่ฉันใช้มันตลอดเวลาในสคริปต์ที่สั้นกว่า สิ่งใหญ่ ๆ ที่ใช้งานได้นานและเปิดไฟล์จำนวนมาก ... ฉันเดาว่าฉันจะใช้ "with" หรือ "try / final"
tdelaney

TIL. ฉันไม่รู้ว่าวัตถุของไฟล์__del__จะทำเช่นนั้น
2rs2ts

@ 2rs2ts: CPython ใช้ตัวรวบรวมขยะแบบนับการอ้างอิง (โดยมี GC "จริง" อยู่ข้างใต้เรียกใช้ตามต้องการ) เพื่อให้สามารถปิดไฟล์ได้ทันทีที่คุณวางการอ้างอิงทั้งหมดไปยังที่จับสตรีม Jython และ IronPython มีเพียง GC "จริง" ดังนั้นจึงไม่ปิดไฟล์จนกว่าจะถึง GC ในที่สุด
ฉีก

0

หากคุณต้องยืนกรานในสิ่งที่ "สง่างาม" มากกว่านั่นคือซับเดียว:

>>> import sys
>>> target = "foo.txt"
>>> content = "foo"
>>> (lambda target, content: (lambda target, content: filter(lambda h: not h.write(content), (target,))[0].close())(open(target, 'w'), content) if target else sys.stdout.write(content))(target, content)

foo.txtfooปรากฏขึ้นและมีข้อความ


ควรย้ายไปที่ CodeGolf StackExchange: D
kaiser

0

แล้วการเปิด fd ใหม่สำหรับ sys.stdout ล่ะ ด้วยวิธีนี้คุณจะไม่มีปัญหาในการปิด:

if not target:
    target = "/dev/stdout"
with open(target, 'w') as f:
    f.write(content)

1
น่าเศร้าที่การเรียกใช้สคริปต์ python นี้จำเป็นต้องมี sudo ในการติดตั้งของฉัน / dev / stdout เป็นของ root
Manur

ในหลาย ๆ สถานการณ์การเปิด fd เป็น stdout ซ้ำไม่ใช่สิ่งที่คาดหวัง ตัวอย่างเช่นรหัสนี้จะตัดทอน stdout ดังนั้นจึงทำให้เชลล์สิ่งต่างๆเช่น./script.py >> file เขียนทับไฟล์แทนที่จะต่อท้าย
salicideblock

สิ่งนี้จะใช้ไม่ได้กับ windows ที่ไม่มี / dev / stdout
Bryan Oakley


0
import contextlib
import sys

with contextlib.ExitStack() as stack:
    h = stack.enter_context(open(target, 'w')) if target else sys.stdout
    h.write(content)

เพียงสองบรรทัดพิเศษหากคุณใช้ Python 3.3 หรือสูงกว่า: หนึ่งบรรทัดสำหรับส่วนเกินimportและอีกหนึ่งบรรทัดสำหรับstack.enter_context.


0

หากsys.stdoutปิดตามwithร่างกายได้ดีคุณสามารถใช้รูปแบบเช่นนี้:

# Use stdout when target is "-"
with open(target, "w") if target != "-" else sys.stdout as f:
    f.write("hello world")

# Use stdout when target is falsy (None, empty string, ...)
with open(target, "w") if target else sys.stdout as f:
    f.write("hello world")

หรือมากกว่านั้นโดยทั่วไป:

with target if isinstance(target, io.IOBase) else open(target, "w") as f:
    f.write("hello world")
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.