Подклассификация файловых объектов (для продолжения операций открытия и закрытия) в python 3
Предположим, что я хочу расширить встроенную абстракцию файла с помощью дополнительных операций в open
и close
. В Python 2.7 это работает:
class ExtFile(file):
def __init__(self, *args):
file.__init__(self, *args)
# extra stuff here
def close(self):
file.close(self)
# extra stuff here
Теперь я ищу обновление программы до Python 3, в которой open
- это функция factory, которая может возвращать экземпляр любого из нескольких разных классов из модуля io
в зависимости от того, как он называется. Я мог бы в принципе подклассировать их всех, но это утомительно, и мне пришлось бы переопределить диспетчерство, которое open
делает. (В Python 3 различие между двоичными и текстовыми файлами имеет большее значение, чем в 2.x, и мне нужны оба.) Эти объекты будут переданы в библиотечный код, который может сделать что угодно с ними, поэтому идиома создания "файла-подобного" класса с утиным типом, который обертывает возвращаемое значение open
и пересылает необходимые методы, будет наиболее подробным.
Может ли кто-нибудь предложить подход 3.x, который включает в себя как можно больше дополнительного шаблона за пределами отображаемого кода 2.x?
Ответы
Ответ 1
Вместо этого вы можете использовать диспетчер контекста. Например, этот:
class SpecialFileOpener:
def __init__ (self, fileName, someOtherParameter):
self.f = open(fileName)
# do more stuff
print(someOtherParameter)
def __enter__ (self):
return self.f
def __exit__ (self, exc_type, exc_value, traceback):
self.f.close()
# do more stuff
print('Everything is over.')
Затем вы можете использовать его следующим образом:
>>> with SpecialFileOpener('C:\\test.txt', 'Hello world!') as f:
print(f.read())
Hello world!
foo bar
Everything is over.
Использование контекстного блока с with
в любом случае предпочтительнее для файловых объектов (и других ресурсов).
Ответ 2
tl; dr Используйте контекстный менеджер. См. Нижнюю часть этого ответа для важных предупреждений о них.
В Python файлы стали более сложными 3. Хотя существуют некоторые методы, которые могут использоваться в обычных пользовательских классах, эти методы не работают со встроенными классами. Один из способов заключается в смешивании в желаемом классе до его запуска, но для этого нужно знать, что должен быть первым классом mix-in:
class MyFileType(???):
def __init__(...)
# stuff here
def close(self):
# more stuff here
Поскольку существует так много типов, и в будущем возможно добавление большего количества (маловероятно, но возможно), и мы точно не знаем, что будет возвращено, пока после вызова open
этот метод не будет работа.
Другим методом является изменение как нашего настраиваемого типа для возвращаемого файла ___bases__
, так и изменение атрибута возвращенного экземпляра __class__
к нашему пользовательскому типу:
class MyFileType:
def close(self):
# stuff here
some_file = open(path_to_file, '...') # ... = desired options
MyFileType.__bases__ = (some_file.__class__,) + MyFile.__bases__
но это дает
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: __bases__ assignment: '_io.TextIOWrapper' deallocator differs from 'object'
Еще одним методом, который может работать с чистыми пользовательскими классами, является создание настраиваемого типа файла "на лету" непосредственно из возвращаемого класса экземпляра, а затем обновление возвращаемого класса экземпляра:
some_file = open(path_to_file, '...') # ... = desired options
class MyFile(some_file.__class__):
def close(self):
super().close()
print("that all, folks!")
some_file.__class__ = MyFile
но снова:
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: __class__ assignment: only for heap types
Итак, он выглядит как лучший метод, который будет работать вообще в Python 3, и, к счастью, также будет работать в Python 2 (полезно, если вы хотите, чтобы одна и та же база кода работала в обеих версиях) заключается в том, чтобы иметь собственный менеджер контекста
class Open(object):
def __init__(self, *args, **kwds):
# do custom stuff here
self.args = args
self.kwds = kwds
def __enter__(self):
# or do custom stuff here :)
self.file_obj = open(*self.args, **self.kwds)
# return actual file object so we don't have to worry
# about proxying
return self.file_obj
def __exit__(self, *args):
# and still more custom stuff here
self.file_obj.close()
# or here
и использовать его:
with Open('some_file') as data:
# custom stuff just happened
for line in data:
print(line)
# data is now closed, and more custom stuff
# just happened
Важно помнить: любое необработанное исключение в __init__
или __enter__
предотвратит запуск __exit__
, поэтому в этих двух местах вам все равно нужно использовать try
/except
и/или try
/finally
, чтобы убедиться, что вы не утечка ресурсов.
Ответ 3
У меня была аналогичная проблема и требование поддержки как Python 2.x, так и 3.x. То, что я сделал, было похоже на следующее (текущая полная версия):
class _file_obj(object):
"""Check if `f` is a file name and open the file in `mode`.
A context manager."""
def __init__(self, f, mode):
if isinstance(f, str):
self.file = open(f, mode)
else:
self.file = f
self.close_file = (self.file is not f)
def __enter__(self):
return self
def __exit__(self, *args, **kwargs):
if (not self.close_file):
return # do nothing
# clean up
exit = getattr(self.file, '__exit__', None)
if exit is not None:
return exit(*args, **kwargs)
else:
exit = getattr(self.file, 'close', None)
if exit is not None:
exit()
def __getattr__(self, attr):
return getattr(self.file, attr)
def __iter__(self):
return iter(self.file)
Он передает все вызовы основным объектам файла и может быть инициализирован из открытого файла или из имени файла. Также работает в качестве менеджера контекста. Вдохновленный этим ответом.