有多种功能可以传递两种对象非常有用:一个表示路径的对象(通常是一个字符串),以及一个表示某种流的对象(通常是从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...
这很有效。好极了。但我宁愿不必担心首先打开文件或传递流,至少有时......所以我重构能够采用像对象这样的文件路径而不是像对象这样的文件,并且{{{ 1}}声明:
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...
是文件类似的文件路径,还是返回f目标文件之类的第三个函数来组合其他两个版本会很有用。到上下文管理器。这样我就可以编写这样的代码:
file
......但也像这样:
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`
对于更好的方法有什么建议吗?我是否会偏离基地,需要完全不同地处理这个问题?
答案 0 :(得分:13)
对于我的钱,这是一个自以为是的答案,检查您需要的操作的类文件对象的属性是 a pythonic方式来确定对象的类型,因为这是pythonic duck tests/duck-typing的性质:
在Python中大量使用Duck类型,规范示例是类文件类(例如,
cStringIO
允许将Python字符串视为文件)。
Or from the python docs’ definition of duck-typing
一种编程风格,它不会查看对象的类型以确定它是否具有正确的接口;相反,简单地调用或使用方法或属性(“如果它看起来像鸭子,像鸭子一样呱呱叫,它必须是鸭子。”)通过强调界面而不是特定类型,设计的代码通过允许多态替换来提高其灵活性。鸭子打字避免使用
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__()
答案 1 :(得分:3)
可能不是您正在寻找的答案,但从品味的角度来看,我认为拥有只做一件事的功能会更好。通过这种方式推理他们会更容易。
我只有两个函数:spiffy_file_makerA(spiffy_obj, file)
,它处理你的第一个案例,以及一个包装spiffy_file_makerA
的便利函数,并为你创建一个文件。
答案 2 :(得分: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
是最基本的类来检查......)。你必须抓住所有这些可能的例外。
答案 3 :(得分:0)
这个问题的另一种解决方法受到Raymond Hettinger at 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)
这种方法与口头禅更为一致:显性优于隐性。