Глава 10. Рефакторинг

Добавлено 31 мая 2020 в 12:13

Содержание главы

Погружение

Нравится вам или нет, но баги случаются. Несмотря на все усилия при создании полных модульных тестов, баги всё равно существуют. Что я подразумеваю под словом «баг»? Баг – это тестовый случай, который ещё не написан.

>>> import roman7
>>> roman7.from_roman('') ①
0
  1. Строка 2. Собственно, баг. Пустая строка (как и любая другая последовательность символов, не представляющая корректное римское число) должна вызвать выброс исключения InvalidRomanNumeralError.

После воспроизведения бага и до его исправления следует написать тестовый случай, завершающийся провалом, таким образом, иллюстрируя баг.

class FromRomanBadInput(unittest.TestCase):  
    .
    .
    .
    def testBlank(self):
        '''from_roman should fail with blank string'''
        self.assertRaises(roman6.InvalidRomanNumeralError, roman6.from_roman, '') ①
  1. Строка 7. Всё предельно просто. Вызываем from_roman() с пустой строкой и проверяем, что выбрасывается исключение InvalidRomanNumeralError. Самое сложное было найти баг; теперь, когда известно, что такая ошибка существует, проверка на нее не займёт много времени.

Так как код содержит баг, и у нас есть тест проверки этого бага, то данный тестовый случай завершается провалом:

you@localhost:~/diveintopython3/examples$ python3 romantest8.py -v
from_roman should fail with blank string ... FAIL
from_roman should fail with malformed antecedents ... ok
from_roman should fail with repeated pairs of numerals ... ok
from_roman should fail with too many repeated numerals ... ok
from_roman should give known result with known input ... ok
to_roman should give known result with known input ... ok
from_roman(to_roman(n))==n for all n ... ok
to_roman should fail with negative input ... ok
to_roman should fail with non-integer input ... ok
to_roman should fail with large input ... ok
to_roman should fail with 0 input ... ok

======================================================================
FAIL: from_roman should fail with blank string
----------------------------------------------------------------------
Traceback (most recent call last):
  File "romantest8.py", line 117, in test_blank
    self.assertRaises(roman8.InvalidRomanNumeralError, roman8.from_roman, '')
AssertionError: InvalidRomanNumeralError not raised by from_roman

----------------------------------------------------------------------
Ran 11 tests in 0.171s

FAILED (failures=1)

Только теперь мы можем исправить этот баг.

def from_roman(s):
    '''convert Roman numeral to integer'''
    if not s:                                                                  ①
        raise InvalidRomanNumeralError('Input can not be blank')
    if not re.search(romanNumeralPattern, s):
        raise InvalidRomanNumeralError('Invalid Roman numeral: {}'.format(s))  ②

    result = 0
    index = 0
    for numeral, integer in romanNumeralMap:
        while s[index:index+len(numeral)] == numeral:
            result += integer
            index += len(numeral)
    return result
  1. Строка 3. Требуется всего 2 строки кода: явная проверка на пустую строку и выброс исключения.
  2. Строка 6. Не уверен, упоминалось ли в этой книге ранее, поэтому пусть это будет последний урок о форматировании строк. Начиная с Python 3.1, разрешается опускать числа при использовании индексов позиции в спецификаторе форматирования. То есть, вместо использования спецификатора {0} для ссылки на первый параметр метода format(), вы можете писать {}, и Python заполнит соответствующий индекс позиции за вас. Это выполняется для любого количества аргументов: первые скобки {} равносильны {0}, вторые скобки {} равносильны {1}, и так далее.
you@localhost:~/diveintopython3/examples$ python3 romantest8.py -v
from_roman should fail with blank string ... ok  ①
from_roman should fail with malformed antecedents ... ok
from_roman should fail with repeated pairs of numerals ... ok
from_roman should fail with too many repeated numerals ... ok
from_roman should give known result with known input ... ok
to_roman should give known result with known input ... ok
from_roman(to_roman(n))==n for all n ... ok
to_roman should fail with negative input ... ok
to_roman should fail with non-integer input ... ok
to_roman should fail with large input ... ok
to_roman should fail with 0 input ... ok

----------------------------------------------------------------------
Ran 11 tests in 0.156s

OK  ②
  1. Строка 2. Тест на обработку пустой строки теперь проходит, значит, баг исправлен.
  2. Строка 17. Все остальные тестовые случаи по-прежнему выполняются без ошибок, а это значит, что при исправлении этой ошибки не мы не добавили новых. Самое время, чтобы остановиться писать код!

Программирование через написание тестов не облегчает процесс исправления багов. Для исправления простых ошибок (как в приведённом примере) необходимы простые тесты; сложные ошибки, конечно же, требуют сложных тестов. Если разработка проекта ведётся через тестирование, то может показаться, что исправление бага займёт больше времени, так как вам придётся описать в коде, что баг есть (написать тестовый случай), и затем исправить баг. Если тест опять завершается не успешно, то придётся разобраться, верно ли был исправлен баг, или сам тест содержит ошибки. Тем не менее, при длительной разработке, эти переключения между кодом приложения и тестовым кодом окупают себя, так как, скорее всего, баг будет исправлен с первого раза. Также, поскольку вы можете легко перезапускать все тесты, включая новый, то при исправлении бага вы вряд ли «испортите» старый код. Сегодняшние модульные тесты завтра превратятся в тесты, возвращающие в прежнее состояние.

10.2 Обработка меняющихся требований

Несмотря на все ваши усилия, направленные на то, чтобы прижать ваших клиентов к земле и извлечь из них точные требования под страхом ужасных неприятных вещей, связанных с ножницами и горячим воском, требования меняются. Большинство клиентов не знают, чего они хотят, пока не увидят это; и даже если это не так, они не настолько хорошо формулируют то, чего точно они хотят. И даже если они это сделают, они всё равно захотят большего в следующем релизе. Поэтому будьте готовы при изменении требований обновлять свои тестовые случаи.

Предположим, например, что вы хотите расширить диапазон функций преобразования римских чисел. Обычно ни один символ римского числа не может повторяться более трех раз подряд. Но римляне были готовы сделать исключение из этого правила, имея в строке 4 символа М, представляющих 4000. Если вы выполните это изменение, вы сможете расширить диапазон конвертируемых чисел с 1..3999 до 1..4999. Но сначала вам нужно внести некоторые изменения в свои тесты.

Скачать файл roman8.py.

class KnownValues(unittest.TestCase):
    known_values = ( (1, 'I'),
                      .
                      .
                      .
                     (3999, 'MMMCMXCIX'),
                     (4000, 'MMMM'),                                      ①
                     (4500, 'MMMMD'),
                     (4888, 'MMMMDCCCLXXXVIII'),
                     (4999, 'MMMMCMXCIX') )

class ToRomanBadInput(unittest.TestCase):
    def test_too_large(self):
        '''to_roman should fail with large input'''
        self.assertRaises(roman8.OutOfRangeError, roman8.to_roman, 5000)  ②

    .
    .
    .

class FromRomanBadInput(unittest.TestCase):
    def test_too_many_repeated_numerals(self):
        '''from_roman should fail with too many repeated numerals'''
        for s in ('MMMMM', 'DD', 'CCCC', 'LL', 'XXXX', 'VV', 'IIII'):     ③
            self.assertRaises(roman8.InvalidRomanNumeralError, roman8.from_roman, s)

    .
    .
    .

class RoundtripCheck(unittest.TestCase):
    def test_roundtrip(self):
        '''from_roman(to_roman(n))==n for all n'''
        for integer in range(1, 5000):                                    ④
            numeral = roman8.to_roman(integer)
            result = roman8.from_roman(numeral)
            self.assertEqual(integer, result)
  1. Строка 7. Существующие известные значения не меняются (они всё еще подходят для тестирования), но вам нужно добавить еще несколько значений в диапазон от 4000. Здесь я включил 4000 (самое короткое), 4500 (второе самое короткое), 4888 (самое длинное) и 4999 (самое большое).
  2. Строка 15. Изменилось определение «большого входного значения». Этот тест раньше вызывал to_roman() с 4000 и ожидал ошибки; теперь, когда 4000-4999 являются корректными значениями, вам нужно увеличить это значение до 5000.
  3. Строка 24. Определение «слишком много повторяющихся цифр» также изменилось. Этот тест раньше вызывал from_roman() с 'MMMM' и ожидал ошибки; теперь, когда ММММ считается корректным римским числом, вам нужно увеличить это значение до «МММММ».
  4. Строка 34. Проверка работоспособности проходит через все числа в диапазоне от 1 до 3999. Поскольку диапазон теперь расширился, этот цикл for также необходимо обновить, чтобы добраться до 4999.

Теперь наши тестовые случаи соответствуют новым требованиям, а наш код – нет, поэтому мы ожидаем, что несколько тестовых случаев не пройдут.

you@localhost:~/diveintopython3/examples$ python3 romantest9.py -v
from_roman should fail with blank string ... ok
from_roman should fail with malformed antecedents ... ok
from_roman should fail with non-string input ... ok
from_roman should fail with repeated pairs of numerals ... ok
from_roman should fail with too many repeated numerals ... ok
from_roman should give known result with known input ... ERROR          ①
to_roman should give known result with known input ... ERROR            ②
from_roman(to_roman(n))==n for all n ... ERROR                          ③
to_roman should fail with negative input ... ok
to_roman should fail with non-integer input ... ok
to_roman should fail with large input ... ok
to_roman should fail with 0 input ... ok

======================================================================
ERROR: from_roman should give known result with known input
----------------------------------------------------------------------
Traceback (most recent call last):
  File "romantest9.py", line 82, in test_from_roman_known_values
    result = roman9.from_roman(numeral)
  File "C:\home\diveintopython3\examples\roman9.py", line 60, in from_roman
    raise InvalidRomanNumeralError('Invalid Roman numeral: {0}'.format(s))
roman9.InvalidRomanNumeralError: Invalid Roman numeral: MMMM

======================================================================
ERROR: to_roman should give known result with known input
----------------------------------------------------------------------
Traceback (most recent call last):
  File "romantest9.py", line 76, in test_to_roman_known_values
    result = roman9.to_roman(integer)
  File "C:\home\diveintopython3\examples\roman9.py", line 42, in to_roman
    raise OutOfRangeError('number out of range (must be 0..3999)')
roman9.OutOfRangeError: number out of range (must be 0..3999)

======================================================================
ERROR: from_roman(to_roman(n))==n for all n
----------------------------------------------------------------------
Traceback (most recent call last):
  File "romantest9.py", line 131, in testSanity
    numeral = roman9.to_roman(integer)
  File "C:\home\diveintopython3\examples\roman9.py", line 42, in to_roman
    raise OutOfRangeError('number out of range (must be 0..3999)')
roman9.OutOfRangeError: number out of range (must be 0..3999)

----------------------------------------------------------------------
Ran 12 tests in 0.171s

FAILED (errors=3)
  1. Строка 7. Тест известных значений from_roman() завершится неудачей, как только он достигнет значения 'MMMM', потому что from_roman() по-прежнему считает, что это недопустимое римское число.
  2. Строка 8. Тест известных значений to_roman() завершится неудачей, как только он достигнет значения 4000, потому что to_roman() всё еще думает, что оно находится вне диапазона.
  3. Строка 9. Проверка туда и обратно также не будет выполнена, как только она достигнет значения 4000, потому что to_roman() всё еще думает, что оно находится вне диапазона.

Теперь, когда вы знаете тестовые случаи, которые не проходят из-за новых требований, вы можете подумать об исправлении кода, чтобы привести его в соответствие с тестовыми случаями. (Когда вы начинаете работать с юнит-тестами, может показаться странным, что тестируемый код никогда не «опережает» тестовые случаи. Пока он позади, у вас еще есть над чем поработать; и как только он догонит тесты, вы прекращаете писать код. После того, как вы к этому привыкнете, вы удивитесь, как вы когда-то программировали без тестов.)

Скачать файл roman9.py.

roman_numeral_pattern = re.compile('''
    ^                   # beginning of string
    M{0,4}              # thousands - 0 to 4 Ms  ①
    (CM|CD|D?C{0,3})    # hundreds - 900 (CM), 400 (CD), 0-300 (0 to 3 Cs),
                        #            or 500-800 (D, followed by 0 to 3 Cs)
    (XC|XL|L?X{0,3})    # tens - 90 (XC), 40 (XL), 0-30 (0 to 3 Xs),
                        #        or 50-80 (L, followed by 0 to 3 Xs)
    (IX|IV|V?I{0,3})    # ones - 9 (IX), 4 (IV), 0-3 (0 to 3 Is),
                        #        or 5-8 (V, followed by 0 to 3 Is)
    $                   # end of string
    ''', re.VERBOSE)

def to_roman(n):
    '''convert integer to Roman numeral'''
    if not isinstance(n, int):
        raise NotIntegerError('non-integers can not be converted')
    if not (0 < n < 5000):                        ②
        raise OutOfRangeError('number out of range (must be 1..4999)')

    result = ''
    for numeral, integer in roman_numeral_map:
        while n >= integer:
            result += numeral
            n -= integer
    return result

def from_roman(s):
    .
    .
    .
  1. Строка 3. Вам вообще не нужно вносить какие-либо изменения в функцию from_roman(). Единственное изменение – roman_numeral_pattern. Если вы посмотрите внимательно, вы заметите, что в первом фрагменте регулярного выражения я изменил максимальное количество дополнительных символов M с 3 на 4. Это позволит использовать римские числовые эквиваленты 4999 вместо 3999. Функция from_roman() полностью универсальна; она просто ищет повторяющиеся символы римских цифр и складывает их, не заботясь о том, сколько раз они повторяются. Единственная причина, по которой она раньше не обрабатывала MMMM, заключается в том, что мы явно останавливали ее с помощью сопоставления с шаблоном регулярного выражения.
  2. Строка 17. Для функции to_roman() требуется только одно небольшое изменение в проверке диапазона. Если раньше мы проверяли 0 < n < 4000, то теперь мы проверяем 0 < n < 5000. Мы также меняем сообщение об ошибке, чтобы отразить новый корректный диапазон (1..4999 вместо 1..3999). Нам не нужно вносить какие-либо изменения в оставшуюся часть функции; она уже обрабатывает новые случаи. (Она весело добавляет 'М' к каждой найденной тысяче; из полученных 4000 она выдает 'ММММ'. Единственная причина, по которой она не делала этого раньше, – это то, что мы явно останавливали ее с помощью проверки диапазона.)

Вы можете скептически относиться к тому, что эти два небольших изменения – это всё, что нам было нужно. Не верьте мне на слово; посмотрите сами.

you@localhost:~/diveintopython3/examples$ python3 romantest9.py -v
from_roman should fail with blank string ... ok
from_roman should fail with malformed antecedents ... ok
from_roman should fail with non-string input ... ok
from_roman should fail with repeated pairs of numerals ... ok
from_roman should fail with too many repeated numerals ... ok
from_roman should give known result with known input ... ok
to_roman should give known result with known input ... ok
from_roman(to_roman(n))==n for all n ... ok
to_roman should fail with negative input ... ok
to_roman should fail with non-integer input ... ok
to_roman should fail with large input ... ok
to_roman should fail with 0 input ... ok

----------------------------------------------------------------------
Ran 12 tests in 0.203s

OK  ①
  1. Строка 18. Все тесты пройдены. Прекращаем писать код.

Комплексное модульное тестирование означает, что никогда не нужно полагаться на программиста, который говорит: «Поверь мне».

10.3 Рефакторинг

Самое лучшее в полном модульном тестировании – это не чувство, когда все ваши тесты наконец-то проходят, и даже не чувство, когда кто-то обвиняет вас в том, что вы сломали их код, а вы можете доказать, что вы этого не делали. Самое лучшее в модульном тестировании – то, что оно дает вам свободу беспощадного рефакторинга.

Рефакторинг – это процесс улучшения работающего кода. Обычно «лучше» означает «быстрее», хотя это также может означать «использование меньшего количества памяти», или «использование меньшего дискового пространства», или просто «более элегантно». Что бы это ни значило для вас, для вашего проекта, рефакторинг важен для долгосрочной жизнеспособности любой программы.

Здесь «лучше» означает «быстрее» и «проще в обслуживании». В частности, функция from_roman() медленнее и сложнее, чем мне бы хотелось, из-за того большого отвратительного регулярного выражения, которое мы используем для проверки римских цифр. Теперь вы можете подумать: «Конечно, регулярное выражение большое и страшное, но как еще я должен подтвердить, что произвольная строка является корректным римским числом?»

Ответ: их всего 5000; почему просто не создать таблицу соответствия? Эта идея становится еще лучше, когда вы понимаете, что вам вообще не нужно использовать регулярные выражения вовсе. Когда вы строите таблицу поиска для преобразования целых чисел в римские числа, вы можете построить обратную таблицу поиска для преобразования римских чисел в целые числа. К тому времени, когда вам будет нужно проверить, является ли произвольная строка корректным римским числом, вы соберете все корректные римские числа. «Проверка» сводится к поиску в одном словаре.

И что самое приятное, у вас уже есть полный набор юнит-тестов. Вы можете изменить более половины кода в модуле, но модульные тесты останутся прежними. Это означает, что вы можете доказать (себе и другим), что новый код работает так же, как и оригинал.

Скачать файл roman10.py.

class OutOfRangeError(ValueError): pass
class NotIntegerError(ValueError): pass
class InvalidRomanNumeralError(ValueError): pass

roman_numeral_map = (('M',  1000),
                     ('CM', 900),
                     ('D',  500),
                     ('CD', 400),
                     ('C',  100),
                     ('XC', 90),
                     ('L',  50),
                     ('XL', 40),
                     ('X',  10),
                     ('IX', 9),
                     ('V',  5),
                     ('IV', 4),
                     ('I',  1))

to_roman_table = [ None ]
from_roman_table = {}

def to_roman(n):
    '''convert integer to Roman numeral'''
    if not (0 < n < 5000):
        raise OutOfRangeError('number out of range (must be 1..4999)')
    if int(n) != n:
        raise NotIntegerError('non-integers can not be converted')
    return to_roman_table[n]

def from_roman(s):
    '''convert Roman numeral to integer'''
    if not isinstance(s, str):
        raise InvalidRomanNumeralError('Input must be a string')
    if not s:
        raise InvalidRomanNumeralError('Input can not be blank')
    if s not in from_roman_table:
        raise InvalidRomanNumeralError('Invalid Roman numeral: {0}'.format(s))
    return from_roman_table[s]

def build_lookup_tables():
    def to_roman(n):
        result = ''
        for numeral, integer in roman_numeral_map:
            if n >= integer:
                result = numeral
                n -= integer
                break
        if n > 0:
            result += to_roman_table[n]
        return result

    for integer in range(1, 5000):
        roman_numeral = to_roman(integer)
        to_roman_table.append(roman_numeral)
        from_roman_table[roman_numeral] = integer

build_lookup_tables()

Давайте разберем этот код на понятные фрагменты. Возможно, самая важная строка – последняя:

build_lookup_tables()

Заметьте, что это вызов функции, но в нем нет оператора if. Это не блок if __name__ == '__main__'; он вызывается при импорте модуля. (Важно понимать, что модули импортируются только один раз, а затем кэшируются. Если вы импортируете уже импортированный модуль, ничего не происходит. Поэтому этот код будет вызываться только при первом импорте данного модуля.)

Так что же делает функция build_lookup_tables()? Я рад, что вы спросили.

to_roman_table = [ None ]
from_roman_table = {}
.
.
.
def build_lookup_tables():
    def to_roman(n):                                ①
        result = ''
        for numeral, integer in roman_numeral_map:
            if n >= integer:
                result = numeral
                n -= integer
                break
        if n > 0:
            result += to_roman_table[n]
        return result

    for integer in range(1, 5000):
        roman_numeral = to_roman(integer)          ②
        to_roman_table.append(roman_numeral)       ③
        from_roman_table[roman_numeral] = integer
  1. Строка 7. Это заумное программирование... возможно, слишком заумное. Функция to_roman() определена выше; она ищет значения в таблице поиска и возвращает их. Но функция build_lookup_tables() переопределяет функцию to_roman() для реального выполнения работы (как это делали предыдущие примеры, прежде чем вы добавили таблицу поиска). В функции build_lookup_tables() вызов to_roman() вызовет эту переопределенную версию. После выхода из функции build_lookup_tables() переопределенная версия исчезает – она определяется только в локальной области действия функции build_lookup_tables().
  2. Строка 19. Эта строка кода вызовет переопределенную функцию to_roman(), которая фактически вычисляет число в римском представлении.
  3. Строка 20. Получив результат (из переопределенной функции to_roman()), вы добавляете целое число и его римское представление в обе таблицы поиска.

После создания таблиц поиска оставшаяся часть кода становится простой и быстрой.

def to_roman(n):
    '''convert integer to Roman numeral'''
    if not (0 < n < 5000):
        raise OutOfRangeError('number out of range (must be 1..4999)')
    if int(n) != n:
        raise NotIntegerError('non-integers can not be converted')
    return to_roman_table[n]                                            ①

def from_roman(s):
    '''convert Roman numeral to integer'''
    if not isinstance(s, str):
        raise InvalidRomanNumeralError('Input must be a string')
    if not s:
        raise InvalidRomanNumeralError('Input can not be blank')
    if s not in from_roman_table:
        raise InvalidRomanNumeralError('Invalid Roman numeral: {0}'.format(s))
    return from_roman_table[s]                                          ②
  1. Строка 7. После выполнения той же проверки, что и раньше, функция to_roman() просто находит соответствующее значение в таблице поиска и возвращает его.
  2. Строка 17. Точно так же функция from_roman() сводится к проверке некоторых границ и одной строке кода. Регулярных выражений больше нет. Больше нет циклов. Временная сложность преобразования в и из римских цифр – O(1).

Но работает ли это всё? А почему нет? Всё работает. И я могу это доказать.

you@localhost:~/diveintopython3/examples$ python3 romantest10.py -v
from_roman should fail with blank string ... ok
from_roman should fail with malformed antecedents ... ok
from_roman should fail with non-string input ... ok
from_roman should fail with repeated pairs of numerals ... ok
from_roman should fail with too many repeated numerals ... ok
from_roman should give known result with known input ... ok
to_roman should give known result with known input ... ok
from_roman(to_roman(n))==n for all n ... ok
to_roman should fail with negative input ... ok
to_roman should fail with non-integer input ... ok
to_roman should fail with large input ... ok
to_roman should fail with 0 input ... ok

----------------------------------------------------------------------
Ran 12 tests in 0.031s                                                  ①

OK
  1. Строка 16. Не то чтобы вы спрашивали, но это было быстро! Почти в 10 раз быстрее. Конечно, это не совсем справедливое сравнение потому, что для импорта этой версии требуется больше времени (в это время она создает таблицы поиска). Но поскольку импорт выполняется только один раз, стоимость запуска амортизируется по всем вызовам функций to_roman() и from_roman(). Так как тесты выполняют несколько тысяч вызовов функций (только тест туда и обратно выполняет 10 000 вызовов), эта экономия накапливается!

Мораль этой истории?

  • Простота – это добродетель.
  • Особенно, когда речь идет о регулярных выражениях.
  • Юнит-тесты могут дать вам уверенность при проведении масштабного рефакторинга.

10.4 Заключение

Модульное тестирование – это мощная концепция, которая при правильной реализации может снизить затраты на поддержку и повысить гибкость в любом долгосрочном проекте. Также важно понимать, что юнит-тестирование – это не панацея, не волшебный решатель проблем и не серебряная пуля. Написание хороших тестовых случаев сложно, а поддержание их в актуальном состоянии требует дисциплины (особенно когда клиенты кричат об исправлениях критических ошибок). Модульное тестирование не заменяет другие формы тестирования, включая функциональное тестирование, интеграционное тестирование и тестирование пользователем при приемке. Но оно выполнимо, и оно работает, и как только вы увидите, как оно работает, вы удивитесь, как вы без него обходились.

Эти несколько глав охватили много вопросов, и большая их часть не была даже специфичной для Python. Для многих языков существуют фреймворки модульного тестирования, каждый из которых требует от вас понимания одних и тех же основных понятий:

  • разработка тестовых случаев, являющихся конкретными, автоматизированными и независимыми;
  • написание тестов перед кодом, который они тестируют;
  • написание тестов, которые проверяют правильность ввода и проверяют правильность результатов;
  • написание тестов, которые проверяют неверный ввод и проверяют правильность ответов об ошибках;
  • написание и обновление тестовых случаев для отражения новых требований;
  • безжалостный рефакторинг для повышения производительности, масштабируемости, удобочитаемости, удобства обслуживания или любых других вещей, которых вам не хватает.

Источник:

  • Mark Pilgrim. Dive Into Python 3

Теги

PythonВысокоуровневые языки программированияМодульное тестирование / Юнит-тестирование / Unit testingОбучениеРефакторингЯзыки программирования

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.