Программирование на Python 3. Руководство издательство СимволПлюс
Скачать 3.74 Mb.
|
385 пустой или удаленной записи (или значение None, если в файле нет пус тых или удаленных записей) и соответственно этому значению произ водится усечение файла. Альтернативное решение задачи уплотнения файла состоит в копиро вании записей в другой файл, что можно использовать для создания резервных копий. Это решение реализовано в методе compact(), кото рый показан ниже. def compact(self, keep_backup=False): compactfile = self.__fh.name + ".$$$" backupfile = self.__fh.name + ".bak" self.__fh.flush() self.__fh.seek(0) fh = open(compactfile, "wb") while True: data = self.__fh.read(self.__record_size) if not data: break if data[:1] == _OKAY: fh.write(data) fh.close() self.__fh.close() os.rename(self.__fh.name, backupfile) os.rename(compactfile, self.__fh.name) if not keep_backup: os.remove(backupfile) self.__fh = open(self.__fh.name, "r+b") Этот метод создает два файла – уплотненный файл и резервную копию оригинального файла. Имя уплотненного файла совпадает с именем оригинального файла, но к нему добавляется расширение .$$$, точно так же имя файла резервной копии совпадает с именем оригинального файла, но имеет расширение .bak. Метод читает записи из оригиналь ного файла одну за другой и все непустые и неудаленные записи запи сываются в уплотненный файл. (Обратите внимание, что записываются истинные записи, то есть байт состояния плюс запись пользователя.) Инструкция if data[:1] == _OKAY: таит в себе одну хит рость. Оба объекта – и объект data и объект _OKAY – явля ются объектами типа bytes. Нам необходимо сравнить первый байт (один байт) объекта data с объектом _OKAY. Когда к объекту типа bytes применяется операция среза, возвращается объект bytes, но когда извлекается единст венный байт, например, data[0], возвращается объект типа int – значение байта. Поэтому здесь сравниваются 1байтовый срез объекта data (его первый байт, байт со стояния) с 1байтовым объектом _OKAY. (Сравнение мож но было бы реализовать как if data[0] == _OKAY[0]:, в этом случае сравнивались бы два значения типа int.) Типы данных bytes и bytearray , стр. 344 386 Глава 7. Работа с файлами В конце оригинальному файлу присваивается имя резервной копии, а уплотненному файлу – имя оригинального файла. После этого, если аргумент keep_backup имеет значение False (по умолчанию), файл ре зервной копии удаляется. В заключение, чтобы подготовиться к по следующим операциям чтения и записи, уплотненный файл (который теперь имеет имя оригинального файла) открывается. Класс BinaryRecordFile.BinaryRecordFile содержит весьма низкоуровне вую реализацию, но он может служить основой для классов более вы сокого уровня, где необходима возможность произвольного доступа к данным в файлах, хранящих записи фиксированного размера; это будет показано в следующем подразделе. Пример: классы в модуле BikeStock Модуль BikeStock использует класс BinaryRecordFile.BinaryRecordFile для управления простым хранилищем информации. Элементами хранения является информация о велосипедах, каждый из которых представля ет собой экземпляр класса BikeStock.BikeStock. Класс BikeStock.Bike Stock содержит в себе словарь, ключами которого являются идентифи каторы велосипедов, а значениями – индексы соответствующих запи сей в BinaryRecordFile.BinaryRecordFile. Ниже приводится короткий пример, дающий некоторое представление о том, как работают эти классы: bicycles = BikeStock.BikeStock(bike_file) value = 0.0 for bike in bicycles: value += bike.value bicycles.increase_stock("GEKKO", 2) for bike in bicycles: if bike.identity.startswith("B4U"): if not bicycles.increase_stock(bike.identity, 1): print("stock movement failed for", bike.identity) Этот фрагмент программного кода открывает файл хранилища инфор мации о велосипедах, выполняет итерации по всем содержащимся в нем записям и определяет общую стоимость (сумма произведений цена × количество) велосипедов на складе. Затем он увеличивает на два коли чество велосипедов «GEKKO», хранящихся на складе, и на один – ко личество велосипедов, названия которых начинаются с «B4U». Все эти действия выполняются непосредственно с информацией на диске, поэтому любые другие процессы, обращающиеся к файлу хранилища, имеют доступ к самой свежей информации. Класс BinaryRecordFile.BinaryRecordFile работает с файлом в терминах индексов, тогда как класс BikeStock.BikeStock работает в терминах идентификаторов велосипедов. Это возможно благодаря тому, что эк земпляр класса BikeStock.BikeStock хранит словарь, устанавливаю Произвольный доступ к двоичным данным в файлах 387 щий отношения между идентификаторами велосипедов и индексами записей. Сначала рассмотрим инструкцию class и метод инициализации класса BikeStock.Bike , затем обсудим некоторые методы класса BikeStock.Bike Stock и в заключение посмотрим на программный код, играющий роль связующего звена между объектами BikeStock.Bike и двоичными запи сями, представляющими их в BinaryRecordFile.BinaryRecordFile (весь программный код находится в файле BikeStock.py). class Bike: def __init__(self, identity, name, quantity, price): assert len(identity) > 3, ("invalid bike identity '{0}'" .format(identity)) self.__identity = identity self.name = name self.quantity = quantity self.price = price Все атрибуты класса Bike доступны внешнему программному коду как свойства – идентификатор велосипеда (self.__identity) представляет свойство Bike.identity, доступное только для чтения, остальные свой ства доступны как для чтения, так и для записи и обеспечивают допол нительную проверку корректности записываемых данных с помощью инструкции assert. Дополнительно имеется свойство Bike.value, дос тупное только для чтения, возвращающее произведение цены на коли чество. (Здесь не приводится программный код реализации свойств, так как он похож на программный код, который приводился ранее.) Класс BikeStock.BikeStock реализует собственные методы манипулиро вания объектами типа BikeStock.Bike, которые используют свойства объектов класса BikeStock.Bike, доступные для записи. class BikeStock: def __init__(self, filename): self.__file = BinaryRecordFile.BinaryRecordFile(filename, _BIKE_STRUCT.size) self.__index_from_identity = {} for index in range(len(self.__file)): record = self.__file[index] if record is not None: bike = _bike_from_record(record) self.__index_from_identity[bike.identity] = index Класс BikeStock.BikeStock – это наш собственный класс коллекций, аг регирующий экземпляр класса BinaryRecordFile.BinaryRecordFile (self. __file ) и словарь (self.__index_from_identity), ключами которого явля ются идентификаторы велосипедов, а значениями – индексы записей с информацией о них. 388 Глава 7. Работа с файлами После открытия файла (или создания, если перед этим файл не суще ствовал) выполняются итерации по записям, содержащимся в нем (ес ли таковые имеются). Каждая извлеченная запись преобразуется из объекта типа bytes в объект BikeStock.Bike с помощью частной функ ции __bike_from_record(), после чего идентификатор велосипеда и ин декс записи добавляются в словарь self.__index_from_identity. def append(self, bike): index = len(self.__file) self.__file[index] = _record_from_bike(bike) self.__index_from_identity[bike.identity] = index Чтобы добавить новый велосипед, необходимо определить подходя щий номер позиции и поместить в эту позицию запись с двоичным представлением информации о велосипеде. При этом мы не забываем дополнить словарь self.__index_from_identity. def __delitem__(self, identity): del self.__file[self.__index_from_identity[identity]] Удаление записи с информацией о велосипеде выполняется очень про сто – достаточно по идентификатору определить номер позиции и уда лить запись в этой позиции. В классе BikeStock.BikeStock не предпола гается использовать возможность восстановления удаленных записей, предусматриваемую классом BinaryRecordFile.BinaryRecordFile. def __getitem__(self, identity): record = self.__file[self.__index_from_identity[identity]] return None if record is None else _bike_from_record(record) Записи с информацией о велосипедах извлекаются по идентификатору велосипеда. Если в словаре self.__index_from_identity отсутствует за прошенный идентификатор, возбуждается исключение KeyError, а ес ли запись пустая или была удалена, объект BinaryRecordFile.BinaryRe cordFile вернет значение None. Но если запись существует, она возвра щается в виде объекта BikeStock.Bike. def __change_stock(self, identity, amount): index = self.__index_from_identity[identity] record = self.__file[index] if record is None: return False bike = _bike_from_record(record) bike.quantity += amount self.__file[index] = _record_from_bike(bike) return True increase_stock = (lambda self, identity, amount: self.__change_stock(identity, amount)) decrease_stock = (lambda self, identity, amount: self.__change_stock(identity, amount)) Произвольный доступ к двоичным данным в файлах 389 Частный метод __change_stock() содержит реализацию для методов increase_stock() и decrease_stock(). Он определяет индекс записи и из влекает ее в двоичном представлении. Затем запись преобразуется в объект BikeStock.Bike, к этому объекту применяются необходимые изменения, после чего двоичная запись в файле затирается двоичным представлением измененного объекта. (Существует также метод __change_bike() , содержащий реализацию методов change_name() и chan ge_price() , но ни один из них не будет рассматриваться здесь, так как они очень похожи на методы, продемонстрированные выше.) def __iter__(self): for index in range(len(self.__file)): record = self.__file[index] if record is not None: yield _bike_from_record(record) Этот метод обеспечивает возможность итераций через объекты Bike Stock.BikeStock , как через списки, возвращая на каждой итерации объ ект BikeStock.Bike и пропуская пустые и удаленные записи. Частные функции _bike_from_record() и _record_from_bike() отделяют двоичное представление объектов класса BikeStock.Bike от класса Bike Stock.BikeStock , хранящего коллекцию велосипедов. Логическая струк тура записи с информацией о велосипеде в файле показана на рис. 7.4. Физическая структура записи несколько отличается, потому что каж дая запись содержит дополнительный байт состояния. _BIKE_STRUCT = struct.Struct("<8s30sid") def _bike_from_record(record): ID, NAME, QUANTITY, PRICE = range(4) parts = list(_BIKE_STRUCT.unpack(record)) parts[ID] = parts[ID].decode("utf8").rstrip("\x00") parts[NAME] = parts[NAME].decode("utf8").rstrip("\x00") return Bike(*parts) def _record_from_bike(bike): return _BIKE_STRUCT.pack(bike.identity.encode("utf8"), запись0 запись1 запись2 записьN int 32 float 64 идентификатор название количество 30 × байты в кодировке UTF 8 8 × байты в кодировке UTF 8 цена Рис. 7.4. Логическая структура файла, хранящего записи с информацией о велосипедах 390 Глава 7. Работа с файлами bike.name.encode("utf8"), bike.quantity, bike.price) При преобразовании двоичной записи в объект BikeStock.Bike сначала выполняется преобразование кортежа, возвращаемого методом un pack() , в список. Это позволяет выполнять модификацию элементов, в данном случае – преобразовывать байты в кодировке UTF8 в строки, с усечением завершающих байтов 0x00. После этого с помощью опера тора распаковывания последовательностей (*) осуществляется переда ча отдельных полей записи методу инициализации класса Bike Stock.Bike . Упаковывание данных выполняется намного проще, при этом не следует забывать о необходимости преобразования строк в по следовательности байтов UTF8. Потребность в прикладных программах, осуществляющих произволь ный доступ к двоичным данным в файлах, уменьшается по мере уве личения объемов оперативной памяти и скорости работы дисков в со временных настольных системах. А когда возникает потребность в та кой функциональности, часто бывает проще использовать файлы DBM или базы данных SQL. Тем не менее существуют системы, где может оказаться востребованной функциональность, продемонстрированная выше, например, во встроенных системах и других системах с ограни ченными ресурсами. В заключение В этой главе были продемонстрированы широко используемые прие мы сохранения коллекций данных в файлах и загрузки их из файлов. Мы увидели, насколько прост в использовании модуль pickle и как можно обрабатывать сжатые и несжатые файлы, не зная заранее, ис пользовалось ли сжатие. Мы узнали, какую заботу необходимо проявлять при записи и чтении двоичных данных, и увидели, насколько длинным может получиться программный код, когда требуется обеспечить обработку строк пере менной длины. Но мы также узнали, что использование двоичных форматов дает в результате файлы наименьшего размера и обеспечи вает наивысшую скорость записи и чтения. Кроме того, мы узнали на сколько важно использовать сигнатуры для идентификации типа фай ла и номера версий, чтобы упростить изменение формата файла в бу дущем. В этой главе мы увидели, что простой текстовый формат наиболее удо бен для восприятия человеком и что при хорошо продуманной струк туре он сможет легко обрабатываться дополнительными инструмента ми, которые будут созданы для манипулирования данными. Однако анализ текста может оказаться непростым делом. Мы видели, как можно читать текстовые данные вручную и с помощью регулярных выражений. Упражнения 391 XML – весьма популярный формат обмена данными и, вообще говоря, будет совсем нелишним предусмотреть в программе хотя бы возмож ность импортирования и экспортирования данных в формате XML, да же если основным используемым форматом является двоичный или текстовый. Мы увидели, как вручную выполнять запись данных в фор мате XML, включая корректное экранирование значений атрибутов и текстовой информации, и как записывать эти данные средствами де рева элементов и модели DOM. Мы также узнали, как выполнять пар синг содержимого файлов XML с помощью парсеров дерева элементов, DOM и SAX, которые предоставляются стандартной библиотекой язы ка Python. В заключительном разделе главы мы увидели, как создать универ сальный класс для обеспечения произвольного доступа к двоичным данным в файлах, хранящих записи фиксированного размера, и затем увидели, как использовать этот класс в конкретном контексте. Этой главой заканчивается изучение фундаментальных основ програм мирования на языке Python. Уже сейчас можно прекратить чтение книги и, используя полученные знания, писать отличные программы. Но было бы неразумно останавливаться на достигнутом, потому что язык Python может предложить намного больше, начиная от приемов, позволяющих сократить и упростить программный код, и заканчивая ошеломляющими средствами, о существовании которых полезно хотя бы знать, даже если они будут востребованы нечасто. В следующей гла ве мы продолжим изучение вопросов процедурного и объектноориен тированного программирования, но дополнительно познакомимся с функциональным программированием. Затем, в последующих гла вах, мы сосредоточимся на изучении более широких приемов програм мирования, включая программирование многопоточных приложений, организацию сетевых взаимодействий, работу с базами данных, ис пользование регулярных выражений и создание программ с графиче ским интерфейсом пользователя. Упражнения В первом упражнении предлагается создать более простой модуль для работы с двоичным файлом по сравнению с тем, что был представлен в этой главе. Истинный размер записи в этом файле точно совпадает с размером, который указывается пользователем. Во втором упражне нии предлагается изменить модуль BikeStock так, чтобы он использо вал новый модуль для работы с двоичным файлом. В третьем упражне нии предлагается написать программу с самого начала – операции с файлом в ней не отличаются сложностью, но форматирование вывода может оказаться трудным в реализации. 1. Создайте новую версию более простого модуля BinaryRecordFile, в котором не используется байт состояния записи. В этой версии 392 Глава 7. Работа с файлами размер записи, устанавливаемый пользователем, должен совпадать с истинным размером записи. Новые записи должны добавляться с помощью нового метода append(), который просто перемещает ука затель в конец файла и производит вывод записи в файл. Метод __setitem__() должен позволять замещать только существующие за писи – это легко реализовать с помощью метода __seek_to_index(). Изза отсутствия байта состояния размер метода __getitem__() дол жен сократиться до трех строк. Метод __delitem__() придется пол ностью переписать, так как он должен будет перемещать все запи си, следующие за удаленной, чтобы заполнить освободившийся промежуток. Сделать это можно с помощью чуть больше половины десятка строк, но над реализацией придется подумать. Метод unde lete() нужно будет полностью убрать, так как теперь операция вос становления поддерживаться не будет. Точно так же надо будет уб рать методы compact() и inplace_compact(), так как они больше не нужны. Чтобы внести описанные изменения, придется добавить не более 20 новых и строк и удалить по крайней мере 60 строк по сравнению с оригинальным модулем, и это без учета доктестов. Пример реше ния приводится в файле BinaryRecordFile_ans.py. 2. Как только вы будете уверены, что ваш более простой класс Binary RcordFile работает, скопируйте файл BikeStock.py и измените его так, чтобы он работал с вашим классом BinaryRcordFile. Для этого придется изменить всего несколько строк. Пример решения приво дится в файле BikeStock_ans.py. 3. Отладка действий с двоичными форматами может оказаться доста точно сложным делом, и в этом может помочь инструмент, который выводит шестнадцатеричные дампы содержимого двоичных фай лов. Напишите программу, которая выводила бы в консоли следую щий текст справки: Usage: xdump.py [options] file1 [file2 [... fileN]] Options: h, help show this help message and exit b BLOCKSIZE, blocksize=BLOCKSIZE block size (8..80) [default: 16] d, decimal decimal block numbers [default: hexadecimal] e ENCODING, encoding=ENCODING encoding (ASCII..UTF32) [default: UTF8] (Перевод: Порядок использования: xdump.py [параметры] file1 [file2 [... fileN]] Параметры: h, help вывести это справочное сообщение и выйти b BLOCKSIZE, blocksize=BLOCKSIZE Размер блока (8..80) [по умолчанию: 16] d, decimal блоки десятичных чисел [по умолчанию: шестнадцатеричные] |