Отладка медленной программы; Перезагрузка с середины
У меня есть программа с медленными вычислениями, и я хочу отладить алгоритм. Теперь очень утомительно всегда повторять все, и я предпочел бы возобновить работу с середины программы. Можете ли вы придумать какой-нибудь умный способ добиться этого?
Первая неопределенная идея заключается в определении контрольных точек (где я делаю вызов функции), где я сохраняю локальные и большие данные с помощью pickle и/или sqlite (sqlite, чтобы иметь возможность проверять промежуточные данные). Позже я мог попытаться вызвать программу, сообщающую ей перезапустить на определенной контрольной точке. Однако я не хочу разделить все куски кода между контрольно-пропускными пунктами именно для этой цели.
У кого-то есть умная идея решить эту проблему отладки?
Ответы
Ответ 1
Google go указал мне на CryoPID, который может выполнять эту работу, если вы разрабатываете систему на основе Linux. Он утверждает, что может приостановить процесс и сохранить его в файле, а затем перезапустить его, даже на другом компьютере. Я не тестировал его, хотя.
Ответ 2
Сделайте вашу программу более модульной. В идеале основной блок кода должен выглядеть примерно так:
import config
import my_numerics
import post_processing
my_numerics.configure(config.numerics)
values = my_numerics.run()
post_processing.run(values, config.post_processing)
Вы получаете идею. Затем легко создать объект "mock", который возвращает предварительно вычисленные данные и передает его в пост-обработку.
EDIT: Я до сих пор не понимаю. Является ли следующий точный псевдокод для вашей проблемы?
for _ in range(lots):
do_slow_thing_one()
for _ in range(many):
do_slow_thing_two()
for _ in range(lots_many)
do_slow_thing_three()
То есть, вы хотите прервать число на полпути через их прогон (не в конце), скажем, в начале третьего цикла, не перезагружая первые два?
Если это так, даже если петли не содержат большого количества кода, вы должны модулировать дизайн:
input_data = np.load(some_stuff)
stage_one = do_thing_one(input_data)
stage_two = do_thing_two(stage_one)
stage_three = do_thing_three(stage_two)
Первый способ сделать это - передача данных между отдельными этапами через неявный интерфейс; а именно, словарь локальных переменных. Это плохо, потому что вы не определили, какие переменные используются, и поэтому вы не можете имитировать их для тестирования/отладки. Второй способ определяет (рудиментарный) интерфейс между вашими функциями. Теперь вам не все равно, что делает do_thing_one
, если он принимает некоторые входные данные и возвращает некоторые выходные данные. Это означает, что для отладки do_thing_three
вы можете просто сделать
stage_two = np.load(intermediate_stuff)
stage_three = do_thing_three(stage_two)
Пока данные в stage_two
имеют правильный формат, не имеет значения, откуда оно взялось.
Ответ 3
Тесты устройств
Вот почему существуют тесты единиц. Попробуйте pyunit
с небольшими "образцами данных" или doctest
для чрезвычайно простых функций.
Мини-тестовые программы
Если по какой-то причине вам действительно нужна интерактивность, я обычно пишу интерактивную мини-программу, как эффективно unit test.
def _interactiveTest():
...
import code
code.interact(local=locals())
if __name__=='__main__':
_interactiveTest()
Вы можете позволить себе игнорировать загрузку больших фрагментов основной программы, если вы только тестируете определенную часть; при необходимости отрегулируйте свою архитектуру, чтобы избежать инициализации ненужных частей программы. Именно по этой причине люди могут сказать "сделать вашу программу более модульной", и это то, что модульность означает: небольшие куски программы автономны, позволяя вам повторно использовать их или (в этом случае) загружать их отдельно.
Вызов интерпретатора в программе
Вы также можете опуститься в интерпретатор и передать местным жителям (как показано выше) в любой момент вашей программы. Это своего рода "отладчик плохого человека", но я считаю его достаточно эффективным. =)
Монолитные алгоритмы
Был там, сделал это. Иногда ваш рабочий процесс не может быть более модульным, и все начинает становиться громоздким.
Ваша интуиция для создания контрольных точек очень хорошая, и я использую то же самое: если вы работаете в среде интерпретатора или внедряете интерпретатор, вам не придется иметь дело с этой проблемой так часто, как если бы вы просто переигрывали ваши скрипты. Сериализация ваших данных может работать, но она вводит большие накладные расходы на чтение и запись с диска; вы хотите, чтобы ваш набор данных оставался в памяти. Затем вы можете сделать что-то вроде test1 = algorithm(data), test2 = algorithm(data)
(это предполагает, что ваш алгоритм не является алгоритмом на месте, если он есть, используйте copy-on-write или делайте копию своих данных перед каждым тестом).
Если у вас все еще есть проблемы после того, как вы попробуете все вышеперечисленное, возможно, вы либо:
- используя ваш реальный набор данных; вам нужен только небольшой тестовый набор данных для прототипирования!
- с использованием неэффективного алгоритма.
В крайнем случае вы можете просмотреть свой код, чтобы найти узкие места.
Другое
Есть, вероятно, мощные отладчики python. У Eclipse есть один, который я думаю.
Кроме того, я лично избегал reload <modulename>
, который я всегда нашел, вызвал больше головных болей, чем он решил проблемы.
Ответ 4
Joblib обрабатывает кэширование результатов совершенно прозрачным способом. Вот пример из их документации:
>>> from joblib import Memory
>>> mem = Memory(cachedir='/tmp/joblib')
>>> import numpy as np
>>> a = np.vander(np.arange(3))
>>> square = mem.cache(np.square)
>>> b = square(a)
________________________________________________________________________________
[Memory] Calling square...
square(array([[0, 0, 1],
[1, 1, 1],
[4, 2, 1]]))
___________________________________________________________square - 0.0s, 0.0min
>>> c = square(a)
>>> # The above call did not trigger an evaluation because the result is cached
Результаты расчета автоматически сохраняются на диске, поэтому Joblib может удовлетворить ваши потребности.
Ответ 5
Идея BuildBot может работать. Он использует python reload() для перезагрузки изменений модулей, а затем он перемещает состояние от старых к новым объектам полуупругим способом.
Процесс buildbot всегда работает, но его можно сигнализировать для перезагрузки извне, и в этом случае это происходит.
Итак, если вы сохранили промежуточные результаты своих алгоритмов в объектах (что-то вроде VTK, чтобы уменьшить вычисление), вы можете перезагрузите и заново создайте объекты вашего алгоритма, попросите их перезагрузить старые данные и затем напишите некоторую логику, чтобы повторно запустить вычисление на этих объектах, если модуль python действительно изменился.
Таким образом, вы можете перезагрузить процесс, только если файлы на диске меняются. Обратите внимание: если есть синтаксис или ошибки времени выполнения, все может быть немного волосатым, и вам, возможно, придется перезапустить (если только вы не можете попробовать и вернуться к старым объектам)
Итак, да, контрольные точки нужны. Но, возможно, не так уж плохо иметь такую структуру.:)
На самом деле, просто модуляция шагов позволит вам кэшировать данные на диск. Это может решить проблему. Это определенно поможет в тестировании, как сказал @katrielalex.
Ответ 6
Я подумал об этом, и, наконец, я записал то, что я сначала имел смутно. Предлагая редизайн по-прежнему, он говорит, как пропускать блоки, загружать кешированные данные и т.д.:
class DebugCheckpoints:
def __init__(self, data, start):
self.checkpoint_passed=False
self.start=start
self.data=data
def __call__(self, variables):
return Checkpoint(self, variables.split())
class CheckpointNotReached(Exception): pass
class Checkpoint:
def __init__(self, debug_checkpoints, variables):
self.variables=variables
self.debug_checkpoints=debug_checkpoints
def tag(self, tag_name):
if self.debug_checkpoints.checkpoint_passed or \
tag_name==self.debug_checkpoints.start:
self.debug_checkpoints.checkpoint_passed=True
else:
raise CheckpointNotReached()
def __enter__(self):
return self
def __exit__(self,exc_type, exc_val, exc_tb):
if exc_type==CheckpointNotReached: # check if the context was supposed to be skipped
for v in self.variables:
globals()[v]=self.debug_checkpoints.data[v] # load globals from data
return True
else:
for v in self.variables:
self.debug_checkpoints.data[v]=globals()[v] # save globals to data
return False
#------------------------------------------------------------------------------
data={"x":1, "w":4} # this is supposed to be any persistent dict
checkpoint=DebugCheckpoints(data, start="B") # start from B, skip block A but still load x and w from data
with checkpoint("x w") as c: # variable x and w is to be loaded
c.tag("A") # this will force cancellation of this block, but x and w will be loaded from data
x=1
w=4
print("Doing A")
with checkpoint("y") as c:
c.tag("B") # as the start is B, this tag will no cancel this block
y=2
print("Doing B")
with checkpoint("z") as c:
c.tag("C")
z=3
print("Doing C")
print(checkpoint.data)
print(x,y,z,w)
Это простая структура, позволяющая вводить контрольные точки в код без чрезмерного написания. Поскольку просто определение сотни одноразовых функций для каждого маленького шага, вероятно, было бы ужасом кодирования, и, кроме того, переменные в функциях являются локальными (просто визуализация, вставляющая каждые 5 строк кода в функцию). Я не хочу возвращать все переменные из блока и объявлять все глобальным (что также объясняет, почему украшатель для реализации инфраструктуры контрольных точек не очень хорош).
Возможно, я переработаю некоторые вызовы, когда я проверю случаи, но я думаю, что это хороший старт. Не уверен, что должно идти в линию и что внутри блока (например, .tag
). Я не смог поместить исключение контрольной точки в __enter__
?