Мне очень захотелось поделиться опытом и я хотел бы поговорить о том, почему важно проверять значения возвращаемые функцией. В качестве примера возьмём python и ctypes. Некоторое время назад я столкнулся с достаточно интересным багом суть которого сводилась к тому, что при запуске скрипта на Linux-системе были неправильные данные, но не было трэйсбэка, а на Windows-системе сразу же получали трэйсбэк. Исследование кода показало, что виноваты были некорректные данные даты приходящие в функцию strptime(). Теперь, давайте, посмотрим на пример работы с функцией strptime() в питоне.


Под Windows мы можем использовать функцию strptime() из модуля datetime


Пример с корректной датой:


from datetime import datetime

date_str = "30-10-2016 16:18"
format_str = "%d-%m-%Y %H:%M"

dt = datetime.strptime(date_str, format_str)

print repr(str(dt))

Вот что мы увидим в этом случае:


2016-10-30 16:18:00

Если в коде выше мы заменим строку даты на некорректную:


date_str = «30-10-2016 16:fhadjkfh»

то увидим следующий вывод:


File "E:\Python27\lib\_strptime.py", line 325, in _strptime
    (data_string, format))
ValueError: time data '30-10-2016 16:fhadjkfh' does not match format '%d-%m-%Y %H:%M'

При использовании Linux мы можем так же использовать функцию strptime() импортируя её из библиотеки libc


Подробнее о функции strptime() в Си лучше всего почитать здесь. Я же только отмечу, что в данном случае параметры даты будут сохраняться в следующую структуру:


struct tm {
               int tm_sec;    /* Seconds (0-60) */
               int tm_min;    /* Minutes (0-59) */
               int tm_hour;   /* Hours (0-23) */
               int tm_mday;   /* Day of the month (1-31) */
               int tm_mon;    /* Month (0-11) */
               int tm_year;   /* Year - 1900 */
               int tm_wday;   /* Day of the week (0-6, Sunday = 0) */
               int tm_yday;   /* Day in the year (0-365, 1 Jan = 0) */
               int tm_isdst;  /* Daylight saving time */
           };

Вот как может выглядеть в питоне использование функции strptime() при работе с модулем ctypes:


from ctypes import *

libc = CDLL('libc.so.6')

class TM(Structure):
    _fields_ = [
        ("tm_sec", c_int),
        ("tm_min", c_int),
        ("tm_hour", c_int),
        ("tm_mday", c_int),
        ("tm_mon", c_int),
        ("tm_year", c_int),
        ("tm_wday", c_int),
        ("tm_yday", c_int),
        ("tm_isdst", c_int)
    ]

tm_struct = TM()

for field_name, field_type in tm_struct._fields_:
    print("{}: {}".format(field_name, getattr(tm_struct, field_name)))

strptime = libc.strptime
strptime.restype = c_char_p

date_str = "30-10-2016 16:18"
format_str = "%d-%m-%Y %H:%M"

rez = strptime(date_str, format_str, pointer(tm_struct))

print("######")
for field_name, field_type in tm_struct._fields_:
    print("{}: {}".format(field_name, getattr(tm_struct, field_name)))

print "strptime returned: %s" % repr(rez)

И мы увидим следующий вывод


tm_sec: 0
tm_min: 0
tm_hour: 0
tm_mday: 0
tm_mon: 0
tm_year: 0
tm_wday: 0
tm_yday: 0
tm_isdst: 0
######
tm_sec: 0
tm_min: 18
tm_hour: 16
tm_mday: 30
tm_mon: 9
tm_year: 116
tm_wday: 0
tm_yday: 303
tm_isdst: 0
strptime returned: ''

Здесь важно отметить, что поля объекта tm_struct буду инициализированы нулями, а значением возвращённым функцией strptime() будет пустая строка.


Если же в коде выше мы заменим строку даты на некорректную:


date_str = «30-10-2016fahdkjfa 16:18»

то мы увидим следующий вывод(для краткости я убрал печать значений полей объекта tm_struct после его создания):


tm_sec: 0
tm_min: 0
tm_hour: 0
tm_mday: 30
tm_mon: 9
tm_year: 116
tm_wday: 0
tm_yday: 0
tm_isdst: 0
strptime returned: None

Здесь можно увидеть, что в случае некорректной даты в объекте tm_struct изменятся только те поля, которые удалось распознать в строке даты до некорректных данных, а остальные поля останутся с нулевыми значениями. А сама функция strptime() при этом вернёт значение None. При этом никаких трэйсбэков мы не получим. Вот поэтому важно быть внимательнее и проверять значение, возвращаемое функцией.


Правильным вариантом вызова здесь может быть, например, такой:


# Так как и '' и None соответствуют в питоне False, то нужно проверять именно на None
if strptime(date_str, format_str, pointer(tm_struct)) is None:
    raise ValueError("datestring `{}` does not match expected format `{}`".format(date_str, format_str))

Теперь давайте представим, например, что у нас есть собственный агрегатор расписаний чего-либо. И при некорректном коде такой баг может быть замечен только пользователем, в случае если он увидит разницу между тем расписанием, которое показывает наш агрегатор и расписанием на сайте, откуда мы его получали.

Поделиться с друзьями
-->

Комментарии (15)


  1. Sild
    14.10.2016 10:49
    +21

    Я правильно понял, что в windows вы используете функцию из модуля datetime, в Linux используете совершенно другую функцию и libc с таким же названием, и вы в шоке от того, что они ведут себя по-разному?


    1. kvothe
      14.10.2016 11:38

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


  1. youlose
    14.10.2016 11:35
    +3

    а почему вы под линукс не используете тот же datetime.strptime?


    1. kvothe
      14.10.2016 11:41

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


      1. igrishaev
        14.10.2016 12:25
        +4

        Это классический пример того, как ускорение кода костылями ведет к багам. Питон — это не про скорость работы, а про удобство разработки. С таким подходом аннулируются все его преимущества. Если так важна скорость, писали бы сразу на Гоу, например. Не ваш огород, просто мысли вслух.


        1. kvothe
          14.10.2016 13:03

          Согласен, питон — не про скорость работы, а про скорость/удобство разработки. Но представьте что нам надо вызывать функцию обработки даты большое количество раз. Я решил измерить сколько времени у нас займёт выполнение скриптов где strptime() вызывается 10000 раз, результаты под спойлером.


          Сравнение скорости работы

          1) $ cat python_strptime.py


          from datetime import datetime
          
          date_str = "30-10-2016 16:18"
          format_str = "%d-%m-%Y %H:%M"
          
          for i in xrange(1, 10001):
              datetime.strptime(date_str, format_str)
          

          Результат:
          $ time python python_strptime.py


          real    0m0.327s
          user    0m0.320s
          sys     0m0.007s

          2) $ cat c_strptime.py


          from ctypes import *
          
          libc = CDLL('libc.so.6')
          
          class TM(Structure):
              _fields_ = [
                  ("tm_sec", c_int),
                  ("tm_min", c_int),
                  ("tm_hour", c_int),
                  ("tm_mday", c_int),
                  ("tm_mon", c_int),
                  ("tm_year", c_int),
                  ("tm_wday", c_int),
                  ("tm_yday", c_int),
                  ("tm_isdst", c_int)
              ]
          
          tm_struct = TM()
          
          strptime = libc.strptime
          strptime.restype = c_char_p
          
          date_str = "30-10-2016 16:18"
          format_str = "%d-%m-%Y %H:%M"
          
          for i in xrange(1, 10001):
              strptime(date_str, format_str, pointer(tm_struct))

          Результат:
          $ time python c_strptime.py


          real    0m0.056s
          user    0m0.047s
          sys     0m0.007s


          1. synedra
            15.10.2016 08:32
            +1

            Ну ускорили вы эту функцию. Что-то такое типа четверти микросекунды на вызов, если я правильно посчитал. Если вы не обсчитываете миллиардами какие-нибудь записи из логов или ещё какие источники текста с датами — то ИМХО грошовое ускорение ценой переусложнённого кода и вот таких вот проблем.

            Как мне кажется, мораль этого случая в том, что если две разные функции делают одно и то же, это ещё не значит, что между ними нет каких-нибудь мелких различий, которыми можно прекрасно стрельнуть себе в ногу. Примерно как print() и file_like.write() c имплицитным '\n' и без оного.


  1. igrishaev
    14.10.2016 11:35
    +1

    Зачем лезть в кишки libc.so, когда есть обычный способ, да еще с защитой от неверного формата?


    1. kvothe
      14.10.2016 11:44

      Ответил выше что не могу ответить на этот вопрос. Добавлю только что этот код писался другой командой и какую функцию где вызывать решали они.


  1. WalterMort
    14.10.2016 11:47
    +3

    Хуже чем проверять значение функции, так это писать такие функции, которые требуют проверки.


  1. el777
    14.10.2016 12:37

    Так мораль какая? Как правильно разрабатывать, чтобы защититься от того, что кто-то вызвал не ту функцию?

    1. Проверять все на каждом шагу? Есть риск, что код сильно раздует и он станет хрупким и малоподдерживаемым.

    2. Использовать строгую статическую типизацию?

    3. Делать на все unittest`ы, чтобы проверялись и валидные и невалидные данные?


    1. kvothe
      14.10.2016 13:17
      -1

      Я бы ещё четвёртым пунктом дописал "Внедрить ревью кода?" :) Но вот как правильно разрабатывать и как бороться с подобными ошибками это, на мой взгляд, тема для отдельного разговора. Мне же просто хотелось поделиться опытом и показать что результаты некоторых багов могут быть замечены только пользователем и хорошо, если это не будет критической ситуацией.


      1. lytican
        14.10.2016 23:48

        Ждем статью про ревью кода. Тут пока говорить не о чем.
        Подрядчики могли знать и умолчать, и могли НЕ знать и просто скосячить. Тут слишком много переменных. И непонятной остается ваша личная роль в проекте — вы не в желании делать код ревью, но публично высмеиваете работу подрядных программистов, через раз отписываясь «я не могу ответить на этот вопрос».


        1. kvothe
          15.10.2016 03:30

          Промахнулся, ответил ниже.
          Ссылка на комментарий


  1. kvothe
    15.10.2016 03:29

    С чего Вы взяли, что я не желаю делать ревью кода? Где я это говорил? Юниттесты, ревью кода, парное программирование — разве это не способы сократить ошибки? На мой взгляд, это вещи которые следует внедрять, а вот что из этого применять на практике, когда и как применять — это достаточно большой вопрос и не для одного комментария.


    Почему Вы решили, что это писали подрядчики и что я кого-то высмеиваю? Я здесь не говорил, что это писали подрядчики. Я только говорил, что разными вещами занимались разные команды, не более. А был ли это косяк или что иное и были ли виноватые, обсуждать в публичном месте я не считаю уместным. Так же существуют такие понятия как коммерческая тайна, "внутренняя кухня" и я стараюсь формулировать так, что бы было понятно, что я хочу сказать, но при этом не разглашать детали.


    И, также, я не могу согласиться, что фраза «я не могу ответить на этот вопрос» была "отпиской" и, тем более, через раз — я это написал 2 раза в 2 разных комментариях, опубликованных в одно и то же время, в следствие чего авторы не могли видеть комментарии друг друга. И если Вам показалось, что я кого-то высмеиваю этой фразой — прощу прощения. Это была просто констатация факта, что я не знаю почему было принято такое решение, но зная принцип работы могу сделать предположение.


    UPD. Промахнулся, ответ на комментарий