Многие люди читают о хорошей практике Python, и есть много информации об этом в Интернете. Многие советы включены в книгу, которую я написал в этом году, «Руководство хакера по Python» . Сегодня я хотел бы показать конкретный пример кода, который я не считаю современным.
В моей последней статье, где я рассказывал о своем новом проекте Gnocchi, я писал о том, как я тестировал, взламывал, а затем бросал шепотом . Здесь я собираюсь объяснить часть моего мыслительного процесса и несколько вещей, которые удивили меня при взломе этого кода.
Прежде чем я начну, пожалуйста, не поймите дух этой статьи неправильно. Это никоим образом не является личной атакой на авторов и авторов (которых я не знаю). Кроме того, « шепот» — это фрагмент кода, который создается тысячами установок и хранит метрики годами. Хотя я могу утверждать, что считаю, что код не соответствует передовому опыту, он определенно работает достаточно хорошо и достоин многих людей.
тесты
Первое, что я заметил при попытке взломать шепотом , это отсутствие теста. Есть только один файл, содержащий тесты, имя test_whisper.pyи охват, который он обеспечивает, довольно низкий. Это можно проверить с помощью инструмента покрытия .
$ coverage run test_whisper.py ........... ---------------------------------------------------------------------- Ran 11 tests in 0.014s OK $ coverage report Name Stmts Miss Cover ---------------------------------- test_whisper 134 4 97% whisper 584 227 61% ---------------------------------- TOTAL 718 231 67%
Хотя можно было бы подумать, что 61% — это не так уж и плохо, быстрый взгляд на реальный код теста показывает, что тесты не завершены. Почему я подразумеваю под неполным, что они, например, используют библиотеку для хранения значений в базе данных, но они никогда не проверяют, могут ли результаты быть получены и получены ли результаты точно. Вот веская причина, по которой никогда не следует слепо доверять проценту покрытия теста как метрике качества.
Когда я попытался изменить шепот , так как тесты не проверяли весь цикл значений, введенных в базу данных, я закончил тем, что делал неправильные изменения, но тесты все еще проходили.
Нет PEP 8, нет Python 3
Кодекс не соответствует PEP 8. Пробег flake8 + взлом показывает 732 ошибки … Хотя это не влияет на сам код, это более болезненно взломать на него , чем на большинстве проектов Python.
Инструмент для взлома также показывает, что код не готов к работе с Python 3, поскольку используется только синтаксис Python 2.
Хороший способ исправить это — настроить tox и добавить несколько целей для проверок PEP 8 и Python 3. Даже если набор тестов не завершен, начиная с запуска flake8 без ошибок и нескольких модульных тестов, работающих с Python 3, следует представить проект в лучшем свете.
Не использовать идиоматический Python
Большая часть кода может быть упрощена с помощью идиоматического Python. Давайте рассмотрим простой пример:
def fetch(path,fromTime,untilTime=None,now=None):
fh = None
try:
fh = open(path,'rb')
return file_fetch(fh, fromTime, untilTime, now)
finally:
if fh:
fh.close()
Этот кусок кода может быть легко переписан как:
def fetch(path,fromTime,untilTime=None,now=None):
with open(path, 'rb') as fh:
return file_fetch(fh, fromTime, untilTime, now)
Таким образом, функция выглядит на самом деле настолько простой, что можно даже задаться вопросом, почему она должна существовать, а почему нет.
Использование петель также может быть сделано более Pythonic:
for i,archive in enumerate(archiveList):
if i == len(archiveList) - 1:
break
может быть на самом деле:
for i, archive in enumerate(itertools.islice(archiveList, len(archiveList) - 1):
Это уменьшает размер кода и облегчает чтение кода.
Неправильный уровень абстракции
Кроме того, одна вещь, которую я заметил шепотом , это то, что он абстрагирует свои функции на неправильном уровне.
Возьмем create()функцию, это довольно очевидно:
def create(path,archiveList,xFilesFactor=None,aggregationMethod=None,sparse=False,useFallocate=False):
# Set default params
if xFilesFactor is None:
xFilesFactor = 0.5
if aggregationMethod is None:
aggregationMethod = 'average'
#Validate archive configurations...
validateArchiveList(archiveList)
#Looks good, now we create the file and write the header
if os.path.exists(path):
raise InvalidConfiguration("File %s already exists!" % path)
fh = None
try:
fh = open(path,'wb')
if LOCK:
fcntl.flock( fh.fileno(), fcntl.LOCK_EX )
aggregationType = struct.pack( longFormat, aggregationMethodToType.get(aggregationMethod, 1) )
oldest = max([secondsPerPoint * points for secondsPerPoint,points in archiveList])
maxRetention = struct.pack( longFormat, oldest )
xFilesFactor = struct.pack( floatFormat, float(xFilesFactor) )
archiveCount = struct.pack(longFormat, len(archiveList))
packedMetadata = aggregationType + maxRetention + xFilesFactor + archiveCount
fh.write(packedMetadata)
headerSize = metadataSize + (archiveInfoSize * len(archiveList))
archiveOffsetPointer = headerSize
for secondsPerPoint,points in archiveList:
archiveInfo = struct.pack(archiveInfoFormat, archiveOffsetPointer, secondsPerPoint, points)
fh.write(archiveInfo)
archiveOffsetPointer += (points * pointSize)
#If configured to use fallocate and capable of fallocate use that, else
#attempt sparse if configure or zero pre-allocate if sparse isn't configured.
if CAN_FALLOCATE and useFallocate:
remaining = archiveOffsetPointer - headerSize
fallocate(fh, headerSize, remaining)
elif sparse:
fh.seek(archiveOffsetPointer - 1)
fh.write('\x00')
else:
remaining = archiveOffsetPointer - headerSize
chunksize = 16384
zeroes = '\x00' * chunksize
while remaining > chunksize:
fh.write(zeroes)
remaining -= chunksize
fh.write(zeroes[:remaining])
if AUTOFLUSH:
fh.flush()
os.fsync(fh.fileno())
finally:
if fh:
fh.close()
Функция делает все : проверяет, не существует ли уже файл, открывает его, строит структурированные данные, записывает это, строит больше структуры, затем записывает это и т. Д.
Это означает, что вызывающая сторона должна указать путь к файлу, даже если она просто хочет, чтобы структура данных whipser хранилась в другом месте. StringIO()может использоваться для фальсификации обработчика файла, но он потерпит неудачу, если вызов fcntl.flock()не отключен — и в любом случае он неэффективен.
В коде много других функций, таких как, например , которые смешивают обработку файлов — даже делают что-то подобное — при манипулировании структурированными данными. Это определенно не очень хороший дизайн, особенно для библиотеки, поскольку оказывается, что повторное использование функции в другом контексте практически невозможно. setAggregationMethod() os.fsync()
Расовые условия
Есть условия гонки, например, в create()(см. Добавленный комментарий):
if os.path.exists(path):
raise InvalidConfiguration("File %s already exists!" % path)
fh = None
try:
# TOO LATE I ALREADY CREATED THE FILE IN ANOTHER PROCESS YOU ARE GOING TO
# FAIL WITHOUT GIVING ANY USEFUL INFORMATION TO THE CALLER 🙁
fh = open(path,'wb')
Этот код должен быть:
try:
fh = os.fdopen(os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL), 'wb')
except OSError as e:
if e.errno = errno.EEXIST:
raise InvalidConfiguration("File %s already exists!" % path)
чтобы избежать каких-либо условий гонки.
Нежелательная оптимизация
Ранее мы видели fetch()функцию, которая едва полезна, поэтому давайте посмотрим на функцию, которую она вызывает. file_fetch()
def file_fetch(fh, fromTime, untilTime, now = None): header = __readHeader(fh) [...]
Первое, что делает функция — это читает заголовок из обработчика файла. Давайте посмотрим на эту функцию:
def __readHeader(fh):
info = __headerCache.get(fh.name)
if info:
return info
originalOffset = fh.tell()
fh.seek(0)
packedMetadata = fh.read(metadataSize)
try:
(aggregationType,maxRetention,xff,archiveCount) = struct.unpack(metadataFormat,packedMetadata)
except:
raise CorruptWhisperFile("Unable to read header", fh.name)
[...]
Первое, что делает функция — заглядывает в кеш. Почему там кеш?
На самом деле он кэширует заголовок на основе индекса на основе пути к файлу ( fh.name). За исключением того, что если кто-то, например, решит не использовать файлы и читы StringIO, он не имеет никакого атрибута имени. Так что этот путь к коду вызовет AttributeError.
Нужно установить поддельное имя вручную на StringIOэкземпляре, и оно должно быть уникальным, чтобы никто не мешал с кешем
import StringIO packedMetadata = <some source> fh = StringIO.StringIO(packedMetadata) fh.name = "myfakename" header = __readHeader(fh)
Кеш может действительно быть полезен при доступе к файлам, но он определенно бесполезен, когда файлы не используются. Но это не обязательно правда, что сложность (даже небольшая), которую добавляет кеш, того стоит. Я сомневаюсь , что большинство из шепота инструменты , основанные на давно запускать процессы, поэтому кэш , который действительно используется при доступе к файлам , это один управляется ядром операционной системы, и это один будет гораздо более эффективным , так или иначе, и совместно обрабатываются. Также нет срока действия этого кэша, который может привести к потере тонны используемой памяти.
Строки документации
Ни одна из строк документации не написана в синтаксическом синтаксисе, подобном Sphinx . Это означает, что вы не можете сгенерировать любую документацию в хорошем формате, который разработчик, использующий библиотеку, мог бы легко прочитать.
Документация также не обновлена:
def fetch(path,fromTime,untilTime=None,now=None): """fetch(path,fromTime,untilTime=None) [...] """ def create(path,archiveList,xFilesFactor=None,aggregationMethod=None,sparse=False,useFallocate=False): """create(path,archiveList,xFilesFactor=0.5,aggregationMethod='average') [...] """
Этого можно избежать, если выбрать правильный формат для записи строки документации. Инструмент может быть замечен, когда есть разница между действительной сигнатурой функции и документированной, как, например, отсутствие аргумента.
Дублированный код
И последнее, но не менее важное: в сценариях, предоставляемых whisper в его binкаталоге, много кода дублируется . Тезисы сценарии должны быть очень легкими и быть с помощью средства Setuptools , но они на самом деле содержат много (непроверенную) коды. Кроме того, часть этого кода частично продублирована из библиотеки, которая против DRY . console_scriptswhisper.py
Вывод
Есть еще несколько вещей, которые заставили меня перестать думать о шепоте , но это часть функций шепота , а не обязательно качества кода. Можно также указать, что код очень сжат и труден для чтения, и это более общая проблема того, как он организован и абстрагирован.
Многие из этих недостатков на самом деле и стали причиной того, что я начал писать «Руководство хакера по Python» год назад. Попадание в этот вид кода заставляет меня думать, что было действительно хорошей идеей написать книгу с советами по написанию лучшего кода на Python!