В Python, если у вас есть цикл, в каждой итерации которого вы хотите записать в файл (маринование в моем случае), перезаписывая любые данные, которые уже есть, один из вариантов — открыть файл перед циклом, оставить его открытым, и обрезать его на каждой итерации, чтобы стереть предыдущие данные перед записью новых данных:
import pickle
with open(filename, 'wb') as file:
for blah in blahs:
file.truncate(0)
file.seek(0)
pickle.dump(blah, file)
а другой — просто повторно открывать файл на каждой итерации, так как открытие его в wb
автоматически усекает его:
import pickle
for blah in blahs:
with open(filename, 'wb') as file:
pickle.dump(blah, file)
Что лучше (с точки зрения производительности/скорости, обработки системных ресурсов и т. д.)? Есть ли лучший способ перезаписать данные в уже открытом файле, чем использовать file.truncate()
и file.seek()
, как указано выше?
Я знаю, что был задан аналогичный вопрос (Лучше ли открывать/закрывать файл каждый раз, чем держать его открытым до завершения процесса?), но, похоже, речь идет о том, когда вы хотите добавить к каждому итерация, а не перезапись, поэтому мне интересно, приводит ли процесс усечения и т. д. В последнем к какому-либо значительному снижению производительности, которое могло бы склонить чашу весов?
@FlyingTeller Вопрос больше в том, чтобы узнать, существуют ли общие независимые от системы концепции, которые сделали бы одну лучше другой (и что это за концепции), основанные на вещах, выходящих за рамки производительности (обработка ресурсов и т. д.), а также на любой другой общей мудрости и советы, относящиеся к этому
Я действительно не понимаю, почему вы хотите использовать цикл, если вы продолжаете перезаписывать данные. Вы можете просто написать последнюю запись в blahs
.
@Matthias Приведенный код является просто иллюстративным примером базовой структуры - он явно не предназначен для использования на практике. Примером практического варианта использования структуры является цикл while с динамически генерируемыми данными, и вы хотите записывать данные на каждой итерации в файл в качестве резервной копии перед их дальнейшей обработкой в цикле, а затем после того, как вы обработав его, вам больше не нужна резервная копия, поэтому ее можно впоследствии перезаписать.
На многих платформах truncate(0)
не заполняется. Вы можете подтвердить это, выполнив truncate(0)
, а затем написав 1 байт без seek(0)
. Таким образом, выполнение seek
, dump
, затем truncate
(при необходимости) в таком порядке уменьшит количество избыточного заполнения нулями. Однако разница намного меньше, чем время, необходимое для dump
, поэтому я не думаю, что это стоит делать. Другая возможность заключается в том, что если размер буфера достаточно больше, чем blah
, это может уменьшить количество фактических операций записи на диск. Этот эксплойт нельзя было бы повторно открыть, так как он всегда пишет при закрытии.
Я не люблю гадать, поэтому я описал два подхода:
import pickle
import tempfile
from random import choices
from string import ascii_lowercase, ascii_uppercase, digits
from pathlib import Path
from performance_measurement import run_performance_comparison
class Bla:
def __init__(self):
population = ascii_uppercase + digits + ascii_lowercase
self._content = str.join("", choices(population, k=50))
def truncate_approach(blahs: list[Bla], filename: str):
with open(filename, "wb") as file:
for blah in blahs:
file.truncate(0)
file.seek(0)
pickle.dump(blah, file)
def reopen_approach(blahs: list[Bla], filename: str):
for blah in blahs:
with open(filename, "wb") as file:
pickle.dump(blah, file)
def setup(N):
return [[Bla() for i in range(N)], Path(tempfile.NamedTemporaryFile().name)]
run_performance_comparison(
approaches=[truncate_approach, reopen_approach],
data_size=[10, 20, 30, 100, 200, 300, 1000, 2000, 3000],
setup=setup,
number_of_repetitions=10,
)
truncate_approach
немного быстрее. Я предполагаю, что это потому, что мы меньше взаимодействуем с диском и иногда урезаем содержимое и переустанавливаем буфер записи, прежде чем нам придется взаимодействовать с жестким диском.
Код профилирования:
import timeit
from functools import partial
import matplotlib.pyplot as plt
from typing import List, Dict, Callable
from contextlib import contextmanager
import matplotlib.pyplot as plt
import matplotlib.transforms as mtransforms
import matplotlib.ticker as ticker
import numpy as np
@contextmanager
def data_provider(data_size, setup=lambda N: N, teardown=lambda: None):
data = setup(data_size)
yield data
teardown(*data)
def run_performance_comparison(approaches: List[Callable],
data_size: List[int],
*,
setup=lambda N: [N],
teardown=lambda *N: None,
number_of_repetitions=5,
title='Performance Comparison',
data_name='N',
yscale='log',
xscale='log'):
approach_times: Dict[Callable, List[float]] = {approach: [] for approach in approaches}
for N in data_size:
with data_provider(N, setup, teardown) as data:
print(f'Running performance comparison for {data_name} = {N}')
for approach in approaches:
function = partial(approach, *data)
approach_time = min(timeit.Timer(function).repeat(repeat=number_of_repetitions, number=1))
approach_times[approach].append(approach_time)
for approach in approaches:
plt.plot(data_size, approach_times[approach], label=approach.__name__)
plt.yscale(yscale)
plt.xscale(xscale)
plt.xlabel(data_name)
plt.ylabel('Execution Time (seconds)')
plt.title(title)
plt.legend()
plt.show()
Which is best (in terms of performance
— похоже, вы могли бы легко рассчитать время для обоих подходов в ваших конкретных условиях.