Поиск мертвого кода в большом проекте Python

Я видел Как найти неиспользуемые функции в коде Python?, но он очень старый и не отвечает на мой вопрос.

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

Я знаю, что найти весь мертвый код невозможно. Все, что мне нужно, это инструмент, который найдет все функции, которые нигде не вызываются. Мы не делаем ничего особенного с вызовом функций на основе строки имени функции, поэтому меня не беспокоит что-либо патологическое ...

Я только что установил pylint, но, похоже, он основан на файлах и не уделяет особого внимания межфайловым зависимостям или даже зависимостям функций.

Ясно, что я мог бы использовать grep для def во всех файлах, получить из них все имена функций и выполнить команду grep для каждого из этих имен функций. Я просто надеюсь, что уже есть что-то более умное, чем это.

ETA: Обратите внимание, что я не ожидаю и не хочу чего-то идеального. Я знаю свое доказательство проблемы остановки точно так же, как любой (нет, на самом деле я преподавал теорию вычислений, которую знаю, когда смотрю на что-то рекурсивно перечислимое). Любая вещь, которая пытается приблизиться к этому, фактически выполняя код, займет слишком много времени. Мне просто нужно что-то, что синтаксически проходит через код и говорит: «Эта функция определенно используется. Эта функция МОЖЕТ быть использована, и эта функция определенно НЕ используется, никто другой, кажется, даже не знает о ее существовании!» И первые две категории не важны.


person Brian Postow    schedule 01.03.2012    source источник
comment
Уже есть такой, который работает 2,5 года. В основном, я думаю, потому что они знают, что в самом тяжелом случае это невозможно математически.   -  person Brian Postow    schedule 02.03.2012
comment
Похоже, что вам этого хватит. Или, может быть, использование parser даст вам более точный контроль.   -  person Peter Wood    schedule 02.03.2012


Ответы (7)


Вы можете попробовать стервятник. Он не может уловить все из-за динамической природы Python, но он улавливает совсем немного, не нуждаясь в полном наборе тестов, например, в покрытии .py и других, которые должны работать.

person Keith Gaughan    schedule 14.08.2013
comment
Это прекрасно, рад, что наконец-то был дан настоящий ответ. Vulture проводит консервативный анализ мертвого кода, который искал исходный запросчик. - person deontologician; 01.10.2013
comment
vulture кажется отличным, но на самом деле он не работает с django ... и, к сожалению, плагин django-покрытия настолько стар, что ему нужны давно не работающие зависимости. :( - person szeitlin; 09.03.2016

Попробуйте запустить Неда Батчелдера rel = "nofollow noreferrer"> cover.py.

Coverage.py - это инструмент для измерения покрытия кода программ Python. Он отслеживает вашу программу, отмечая, какие части кода были выполнены, а затем анализирует исходный код, чтобы определить код, который мог быть выполнен, но не был.

person Peter Wood    schedule 01.03.2012
comment
Это потребует запуска кода во всех возможных конфигурациях. Я не хочу этого делать. Мне не нужен полный список всего мертвого кода. Мне просто нужно быстрое и грязное приближение. Оставить мертвый код - это нормально. - person Brian Postow; 02.03.2012
comment
Оставлять мертвый код - это нормально - я действительно не думаю, что это ВСЕГДА нормально. - person madCode; 19.08.2013
comment
Я бы сказал, что оставлять мертвый код - это плохо, но вряд ли достижимо. - person Patrick Bassut; 23.01.2017

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

class A(object):
    def f(self):
        pass

class B(A):
    def f(self):
        pass

a = []
a.append(A())
a.append(B())
a[1].f()

Ничего особенного здесь не происходит, но любому сценарию, который пытается определить, вызывается ли A.f() или B.f(), будет довольно сложно сделать это без фактического выполнения кода.

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

Как указывалось ранее, просто обнаруживая вызовы простых функций формы

function(...)

or

module.function(...)

будет довольно легко. Вы можете использовать модуль ast для анализа ваших исходных файлов. Вам нужно будет записать весь импорт и имена, используемые для импорта других модулей. Вам также необходимо будет отслеживать определения функций верхнего уровня и вызовы внутри этих функций. Это даст вам график зависимостей, и вы сможете использовать NetworkX для обнаружения связанных компонентов этого графа.

Хотя это может показаться довольно сложным, вероятно, для этого потребуется менее 100 строк кода. К сожалению, почти во всех крупных проектах Python используются классы и методы, поэтому от этого мало пользы.

person Sven Marnach    schedule 02.03.2012
comment
обсуждение классов и разнородных списков - хорошее дело. Я думаю, что в настоящее время я просто скажу, что функция f используется, поэтому я предполагаю, что обе необходимы. И действительно, если у меня есть A и B, у которых есть функция F, и они НЕ входят в один и тот же разнородный список или используются взаимозаменяемо, тогда у меня проблема с именованием функций ... - person Brian Postow; 05.03.2012
comment
Не уверен, действительно ли я назвал бы это проблемой с именованием функций. Приведу пример из стандартной библиотеки: dict.get(), queue.Queue.get() и pickle.Pickler.get() совершенно не связаны. Почему-то весь смысл пространств имен состоит в том, чтобы позволить использовать одно и то же имя для разных вещей. - person Sven Marnach; 05.03.2012
comment
Хорошо, достаточно честно. Полагаю, я предполагаю, что вещи со стандартными именами, такими как get, set, equals, init и т. Д., Все где-то будут использоваться, поэтому меня это не особо беспокоит. Но да, вы правы. - person Brian Postow; 06.03.2012

Вот решение, которое я использую, по крайней мере, ориентировочно:

grep 'def ' *.py > defs
# ...
# edit defs so that it just contains the function names
# ...
for f in `cat defs` do
    cat $f >> defCounts
    cat *.py | grep -c $f >> defCounts
    echo >> defCounts
done

Затем я смотрю на отдельные функции, на которые очень мало ссылок (например, ‹3)

это некрасиво и дает мне лишь приблизительные ответы, но я думаю, что для начала достаточно. Что вы-все думаете?

person Brian Postow    schedule 02.03.2012
comment
Я должен упомянуть, что цикл for находится в синтаксисе bash. - person Brian Postow; 02.03.2012
comment
Примечание: этот код полностью не работает по очень большому количеству причин ... не в последнюю очередь из-за того, что он имеет синтаксические ошибки и ищет неправильную вещь (должно быть просто имя функции и т. д.) ... не запускай это. но это по-прежнему неплохая идея ... простой подсчет строк и т. д. - person Erik Aronesty; 12.09.2019
comment
Я сказал, что это только приблизительно .... - person Brian Postow; 12.09.2019
comment
Доработанная версия скрипта. ничего ручного - gist.github.com/peeyushsrj/87f90919e489994572eaf75c10c9d - person peeyushsrj; 19.12.2019

В следующей строке вы можете перечислить все определения функций, которые, очевидно, не используются в качестве атрибута, вызова функции, декоратора или возвращаемого значения. Это примерно то, что вы ищете. Он не идеален, он медленный, но у меня никогда не было ложных срабатываний. (В Linux вам нужно заменить ack на ack-grep)

for f in $(ack --python --ignore-dir tests -h --noheading "def ([^_][^(]*).*\):\s*$" --output '$1' | sort| uniq); do c=$(ack --python -ch "^\s*(|[^#].*)(@|return\s+|\S*\.|.*=\s*|)"'(?<!def\s)'"$f\b"); [ $c == 0 ] && (echo -n "$f: "; ack --python --noheading "$f\b"); done
person diefans    schedule 06.08.2013
comment
Вы можете улучшить скорость, заменив ack на grep --include "*.py" - person Jonathan Hartley; 23.10.2017

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

person yedpodtrzitko    schedule 01.03.2012
comment
За исключением того, что у вас могут быть тесты на мертвый код. т.е. он не используется в остальной части системы - person John La Rooy; 02.03.2012
comment
У меня очень мало тестов в коде. Добавление модульных тестов - это то, что я должен сделать после того, как выясню, что на самом деле нужно протестировать ... если я протестирую весь мертвый код, я эффективно верну его к жизни, что я не хотеть. - person Brian Postow; 02.03.2012

ИМО, чего можно было достичь довольно быстро с помощью простого плагина pylint, который:

  • запомнить каждую проанализированную функцию / метод (/ класс?) в наборе S1
  • отслеживать каждую вызываемую функцию / метод (/ класс?) в наборе S2
  • отображать S1 - S2 в отчете

Тогда вам придется вызывать pylint для всей базы кода, чтобы получить что-то осмысленное. Конечно, как уже говорилось, это необходимо проверить, так как могли быть сбои логического вывода или такие, которые могут привести к ложному срабатыванию. В любом случае это, вероятно, значительно уменьшит количество выполняемых grep.

У меня еще не так много времени, чтобы делать это самому, но любой найдет помощь в списке рассылки [email protected].

person sthenault    schedule 02.03.2012
comment
FWIW Раньше я писал плагины pylint (не для этого, а для других вещей), и, хотя это было нетривиально (вы пишете утверждения в AST проверенного кода), их на удивление легче заставить работать, чем я ожидал. - person Jonathan Hartley; 23.10.2017