
Документация к сложным библиотекам на питоне (напр. pandas) хранится в doc-строках и разбросана по сотням страниц сайта. В этой статье мы с помощью небольшого кода упакуем её (информацию из документации для каждого класса и метода) в... датафрейм. Но зачем? Во-первых, это прикольно так её можно быстро искать и анализировать. Во-вторых, изучим некоторые встроенные питоновские средства работы с документацией. Наконец, такой датафрейм потенциально может стать основой для обучения/дообучения GPT-моделей генерировать более корректный, безошибочный, использующий всевозможные функции и их аргументы, код...
Переходя от текста, хоть и структурированного, к полностью формализованному табличному описанию, мы движемся в сторону неантропоморфности документации - то есть в сторону облегчения её понимания для машины, алгоритма, и, возможно, усложнения понимания для человека (не не всякого - тот, кто может оперировать датафреймами, извлечет из такой документации пользу намного быстрее и эффективнее! )
Используемая версия pandas - 2.1.4. Версия python 3.8.12.
Первый простой опыт упаковки документации в датафрейм
Сначала импортируем необходимые библиотеки:
import pandas as pd
import numpy as np
Для любого объекта, которым также является и сама библиотека pandas
, импортированная под именем pd
, можно использовать несколько способов посмотреть документацию или ее часть:
зайти на сайт с документацией;
использовать функцию help() - посмотреть справку по объекту;
вызвать строку документации __
doc__
вызвать список аргументов и методов с помощью дандер-метода (т.е. специального метода, с двойными подчеркиваниями в начале и в конце) __
dir__()
Далее, говоря о "методах
pandas
" мы будем говорить о "публично доступных объектахpandas
- классах, функциях, методах классов, атрибутах, описанных в документации"
Попробуем сначала вызвать список аргументов и методов с помощью дандер-метода (т.е. специального метода, с двойными подчеркиваниями вначале и в конце) __dir__()
.
А с помощью if not x.startswith('_')
мы исключим все внутренние переменные и дандер-методы:
[x for x in pd.__dir__() if not x.startswith('_')]
Получим очень много методов, но как их упорядочить???
['annotations',
'core',
'errors',
'util',
'compat',
'get_option',
'set_option',
'reset_option',
'describe_option',
'option_context',
'options',
'pandas',
'io',
'tseries',
'arrays',
'plotting',
'ArrowDtype',
'Int8Dtype',
'Int16Dtype',
...
'merge_ordered',
'crosstab',
'pivot',
'pivot_table',
'get_dummies',
'from_dummies',
'cut',
'qcut',
'api',
'testing',
'show_versions',
'ExcelFile',
'ExcelWriter',
'read_excel',
'read_csv',
'read_fwf',
'read_table',
'read_pickle',
'to_pickle',
'HDFStore',
'read_hdf',
'read_sql',
'read_sql_query',
'read_sql_table',
'read_clipboard',
'read_parquet',
'read_orc',
'read_feather',
'read_gbq',
'read_html',
'read_xml',
'read_json',
'read_stata',
'read_sas',
'read_spss',
'json_normalize',
'test',
'Float64Index',
'Int64Index',
'UInt64Index']
К сожалению, полученный список слишком большой и не дает нам понимания, как группируются эти методы. Поэтому воспользуемся прямым путём: откроем документацию по адресу https://pandas.pydata.org/docs/reference/index.html и обратим внимание на меню слева. Здесь перечислены все группы методов. Просто копипейстом занесем их в строку groups
. Теперь мы можем превратить строку в список с помощью .splitlines()
- метода, который по умолчанию разбивает строку, используя как разделитель перевод строки \n
. А с помощью метода .strip()
мы удаляем пробелы в начале и в конце строки:

groups = """Input/output
General functions
Series
DataFrame
pandas arrays, scalars, and data types
Index objects
Date offsets
Window
GroupBy
Resampling
Style
Plotting
Options and settings
Extensions
Testing"""
[x.strip() for x in groups.splitlines()]
['Input/output',
'General functions',
'Series',
'DataFrame',
'pandas arrays, scalars, and data types',
'Index objects',
'Date offsets',
'Window',
'GroupBy',
'Resampling',
'Style',
'Plotting',
'Options and settings',
'Extensions',
'Testing']
Теперь мы раскроем каждый из пунктов меню и по очереди перенесем их значения в строки. Первой создадим строку input_output
, содержащую методы ввода-вывода данных в разных форматах:
input_output = '''pandas.read_pickle
pandas.DataFrame.to_pickle
pandas.read_table
pandas.read_csv
pandas.DataFrame.to_csv
pandas.read_fwf
pandas.read_clipboard
pandas.DataFrame.to_clipboard
pandas.read_excel
pandas.DataFrame.to_excel
pandas.ExcelFile
pandas.ExcelFile.book
pandas.ExcelFile.sheet_names
pandas.ExcelFile.parse
pandas.io.formats.style.Styler.to_excel
pandas.ExcelWriter
pandas.read_json
pandas.json_normalize
pandas.DataFrame.to_json
pandas.io.json.build_table_schema
pandas.read_html
pandas.DataFrame.to_html
pandas.io.formats.style.Styler.to_html
pandas.read_xml
pandas.DataFrame.to_xml
pandas.DataFrame.to_latex
pandas.io.formats.style.Styler.to_latex
pandas.read_hdf
pandas.HDFStore.put
pandas.HDFStore.append
pandas.HDFStore.get
pandas.HDFStore.select
pandas.HDFStore.info
pandas.HDFStore.keys
pandas.HDFStore.groups
pandas.HDFStore.walk
pandas.read_feather
pandas.DataFrame.to_feather
pandas.read_parquet
pandas.DataFrame.to_parquet
pandas.read_orc
pandas.DataFrame.to_orc
pandas.read_sas
pandas.read_spss
pandas.read_sql_table
pandas.read_sql_query
pandas.read_sql
pandas.DataFrame.to_sql
pandas.read_gbq
pandas.read_stata
pandas.DataFrame.to_stata
pandas.io.stata.StataReader.data_label
pandas.io.stata.StataReader.value_labels
pandas.io.stata.StataReader.variable_labels
pandas.io.stata.StataWriter.write_file'''
[x.strip() for x in input_output.splitlines()]
... и сделаем это для остальных групп.
Как вариант, можно было бы написать парсер сайта документации
pandas
, но это заняло бы намного больше времени... Но парсер пригодился бы, если бы мы захотели сохранить историю всех версий pandas.
Теперь создадим словарь, в котором ключами groups_keys
будут названия групп методов, а значениями groups_items
- списки методов.
В groups_str_items
поместим список наших строк, каждая из которых содержит методы той или иной группы, создадим список списков groups_items
и с помощью функции zip
объединим каждый ключ с соответствующим списком значений в словарь groups_dict
:
groups_keys = [x.strip() for x in groups.splitlines()]
groups_str_items = [input_output, general_functions, series, dataframe,
arrays_scalars_data_types, index_objects, date_ofsets,
window, groupby, resampling, style, plotting,
options_settings, extensions, testing,]
groups_items = []
for item in groups_str_items:
group_item = []
for x in item.splitlines():
group_item.append(x.strip())
groups_items.append(group_item)
groups_dict = {key: value for key, value in zip(groups_keys, groups_items)}
Отлично, теперь мы можем создать простейший датафрейм, который позволит посчитать первичную статистику методов pandas
. У нас будут 2 столбца: 'group'
- название группы методов и 'method'
- собственно, сам метод с указанием родительских классов (для удобства).
Методом concat
мы можем объединять несколько датафреймов в один (в данном случае - построчно), ignore_index=True
означает, что существующие индексы игнорируются, итоговый индекс формируется заново, чтобы был сквозной индекс:
prime_df = pd.DataFrame(columns=['group', 'method'])
for key, item_list in groups_dict.items():
df = pd.DataFrame()
df['method'] = item_list
df['group'] = key
prime_df = pd.concat([prime_df, df], ignore_index=True)
print(prime_df)

Итак, pandas в версии 2.1.4 насчитывает 2085 методов! Выведем первичную статистику по группам методов. Для этого нужно применить к датафрейму всего три метода pandas
:
groupby()
с указанием столбца, по которому делаем группировку - в данном случае это столбец группы методов;agg()
- агрегировать данные группы, в данном случае мы используем'count'
- подсчет количества;sort_values()
- отсортировать по данным столбца (в нашем случае'method'
, содержащий количество методов в каждой группе), по убываниюascending=False:
first_grouped_df = prime_df.groupby('group').agg('count').sort_values('method',ascending=False)
first_grouped_df

Проще всего выгрузить данные в .csv, который можно открыть в Excel или другими электронными таблицами и даже загрузить в базы данных. Выгрузим без индексов, разделителем будет запятая. Создадим для этого папку Data в текущем каталоге.
# В Data/.csv , разделитель по умолчанию ','
prime_df.to_csv('Data/prime_df.csv',
index=False)
first_grouped_df.to_csv('Data/first_grouped_df.csv',
index=True)
Полученные файлы можно скачать: prime_df.csv, first_grouped_df.csv
Второй уровень группировки методов
Расширим нашу информацию: оказывается, внутри многих больших групп содержатся группы помельче! Например, раскрыв группу Input/output
, мы справа увидим еще множество подгрупп второго уровня:

Занесем их в переменную input_output_2 (на выходе будем иметь список с этим именем):
input_output_2 = """Pickling
Flat file
Clipboard
Excel
JSON
HTML
XML
Latex
HDFStore: PyTables (HDF5)
Feather
Parquet
ORC
SAS
SPSS
SQL
Google BigQuery
STATA"""
input_output_2 = [x.strip() for x in input_output_2.splitlines()]
И так сделаем с каждой большой группой.
А теперь нужно создать список, где есть первый метод "большой" группы, а также последний метод каждой подгруппы второго уровня.
Так, для input_output это будет:
input_output_2_methods = """read_pickle
DataFrame.to_pickle
read_fwf
DataFrame.to_clipboard
ExcelWriter
build_table_schema
Styler.to_html
DataFrame.to_xml
Styler.to_latex
HDFStore.walk
DataFrame.to_feather
DataFrame.to_parquet
DataFrame.to_orc
read_sas
read_spss
DataFrame.to_sql
read_gbq
StataWriter.write_file"""
input_output_2_methods = [x.strip() for x in input_output_2_methods.splitlines()]
Проделаем эту копипейст-операцию со всеми подгруппами. (Да, как вариант, можно было бы написать парсер сайта - но это заняло бы больше времени...)
Объединим все данные о малых группах в словарь, где ключ - группа, а значение - кортеж из списка малых групп и списка граничных методов.
Вот такой словарь:
groups_to_small_groups = { 'Input/output': (input_output_2, input_output_2_methods),
'General functions': (general_functions_2, general_functions_2_methods),
'Series': (series_2, series_2_methods),
'DataFrame': (dataframe_2, dataframe_2_methods),
'pandas arrays, scalars, and data types': (arrays_scalars_data_types_2, arrays_scalars_data_types_2_methods),
'Index objects': (index_objects_2, index_objects_2_methods),
'Date offsets': (date_ofsets_2, date_ofsets_2_methods),
'Window': (window_2, window_2_methods),
'GroupBy': (groupby_2, groupby_2_methods),
'Resampling': (resampling_2, resampling_2_methods),
'Style': (style_2, style_2_methods),
'Plotting': (plotting_2, plotting_2_methods),
'Options and settings': (options_settings_2, options_settings_2_methods),
'Extensions': (extensions_2, extensions_2_methods),
'Testing': (testing_2, testing_2_methods),
}
Теперь напишем код, который добавит в наш датафрейм еще один столбец small_group, в котором будет метод второго уровня. Он перебирает строки датафрейма, и для каждого метода каждой группы находит соответствующую ему малую группу из нашего словаря groups_to_small_groups (учитывая массу тонкостей, поэтому такое количество разных условий и проверок):
Код добавления в датафрейм признака малой группы:
prime_df['small_group'] = None # Заведем столбец для малых групп
counter = 0 # Это счетчик малых групп внутри списка малых групп
methods_counter = 0 # Это счетчик методов внутри списка методов
prev_group = None # Предыдущая основная группа
small_group_now = None # Малая группа в данный момент
small_groups_method_now_2 = None # Граничный метод в данный момент
is_same = False # Второй метод списка методов такой же, как и первый
for i, row in prime_df.iterrows(): # Перебираем по порядку все строки датафрейма
small_groups_group = groups_to_small_groups[row.group][0] # Список малых групп данной группы
small_groups_method = groups_to_small_groups[row.group][1] # Граничные методы данной группы
if row.group != prev_group: # Группа сменилась?
counter = 0 # Значит, счетчик малых групп на ноль
methods_counter = 0 # И счетчик граничных методов на ноль
pure_method = row.method.split('.')[-1] # Крайний слева метод
pure_method_2 = f"{row.method.split('.')[-2]}.{row.method.split('.')[-1]}" # 2 крайних слева метода в датафрейме
small_groups_method_now = small_groups_method[methods_counter].split('.')[-1] # Текущий малый метод
small_groups_method_len = len(small_groups_method[methods_counter].split('.')) # Длина в списке методовlit('.'))
if small_groups_method_len > 1: # Если можем захватить 2 крайних класса метода - захватываем!
small_groups_method_now_2 = f"{small_groups_method[methods_counter].split('.')[-2]}.{small_groups_method[methods_counter].split('.')[-1]}"
# Совпадает метод? - значит мы поймали конец малой группы!
if small_groups_method_len > 1 and pure_method_2 == small_groups_method_now_2: # У 2 крайних классов
is_same = True
elif small_groups_method_len == 1 and pure_method == small_groups_method_now: # Или хотя бы у одного
is_same = True
else:
is_same = False
if is_same:
if methods_counter == 0: # Это открывающий метод?
small_group_now = small_groups_group[counter] # Текущая малая группа
row['small_group'] = small_group_now # Запишем в датафрейм
# Проверка на дубль: если следующий такой же:
if len(small_groups_group) > 1: # В списке есть следующая малая группа
if small_groups_method_len > 1: # Если можем захватить метод с классом - захватываем!
if len(small_groups_method[methods_counter+1].split('.')) > 1: # Длина в списке методов
sgm_next_2 = f"{small_groups_method[methods_counter + 1].split('.')[-2]}.{small_groups_method[methods_counter + 1].split('.')[-1]}" # Следующий граничный метод с классом
if small_groups_method_len > 1 and sgm_next_2 == small_groups_method_now_2:
is_double = True # Закрывающий дублирует открывающий
elif small_groups_method_len == 1 and small_groups_method[methods_counter + 1].split('.')[-1] == small_groups_method_now:
is_same = True
else:
is_same = False
if is_same:
counter += 1 # Увеличиваем счетчик малых групп
methods_counter += 1 # Увеличиваем счетчик граничных методов - пропускаем дубль
small_group_now = small_groups_group[counter] # Малая группа сейчас
methods_counter += 1 # Увеличиваем счетчик граничных методов
else: # Это закрывающий метод - переходим на следующую малую группу
row['small_group'] = small_group_now # Запишем в датафрейм
counter += 1 # Увеличим счетчик малых групп
if len(small_groups_group) == counter: # Мы достигли конца списка малых групп?
continue
small_group_now = small_groups_group[counter] # Малая группа сейчас
methods_counter += 1 # Увеличиваем счетчик граничных методов
else:
row['small_group'] = small_group_now # Запишем в датафрейм
prev_group = row.group # Запоминаем предыдущую группу
Упорядочим группы (основная группа, подгруппа и метод) и посмотрим, что у нас получилось:
prime_df = prime_df[['group', 'small_group', 'method']]
prime_df

Метод info()
позволяет вывести информацию о столбцах, и особенно важно - нет ли там нулевых значений? У нас нет. Тип object
- это строковый тип в нашем случае.
prime_df.info()

Сохраним в файл prime_small_groups_df.csv
Статистика по основным и малым группам:
stat = prime_df.groupby(['group', 'small_group']).agg({'method': 'count'}).reset_index()
stat

Отсортируем по размеру малой группы (ignore_index=True
означает сбросить индексы, т.е. отнумеровать их от 0):
stat_sort = stat.sort_values('method',ascending=False, ignore_index=True)
stat_sort

Выложим отсортированные данные в папку, файл можно скачать: stat_sort.csv
stat_sort.to_csv('Data/stat_sort.csv',
index=False)
Как получать данные из строки документации __doc__ и некоторых дандер-методов
Рассмотрим первый же метод в нашем датафрейме - read_pickle
Он у нас в формате строки. Чтобы получить метод из его названия, вызовем через getattr
:
read_pickle = getattr(pd, 'read_pickle')
print(read_pickle.__doc__)
Получим очень длинный текст строки документации:
Load pickled pandas object (or any object) from file.
.. warning::
Loading pickled data received from untrusted sources can be
unsafe. See `here <https://docs.python.org/3/library/pickle.html>`__.
Parameters
----------
filepath_or_buffer : str, path object, or file-like object
String, path object (implementing ``os.PathLike[str]``), or file-like
object implementing a binary ``readlines()`` function.
.. versionchanged:: 1.0.0
Accept URL. URL is not limited to S3 and GCS.
compression : str or dict, default 'infer'
For on-the-fly decompression of on-disk data. If 'infer' and 'filepath_or_buffer' is
path-like, then detect compression from the following extensions: '.gz',
'.bz2', '.zip', '.xz', '.zst', '.tar', '.tar.gz', '.tar.xz' or '.tar.bz2'
(otherwise no compression).
If using 'zip' or 'tar', the ZIP file must contain only one data file to be read in.
Set to ``None`` for no decompression.
Can also be a dict with key ``'method'`` set
to one of {``'zip'``, ``'gzip'``, ``'bz2'``, ``'zstd'``, ``'tar'``} and other
key-value pairs are forwarded to
``zipfile.ZipFile``, ``gzip.GzipFile``,
``bz2.BZ2File``, ``zstandard.ZstdDecompressor`` or
``tarfile.TarFile``, respectively.
As an example, the following could be passed for Zstandard decompression using a
custom compression dictionary:
``compression={'method': 'zstd', 'dict_data': my_compression_dict}``.
.. versionadded:: 1.5.0
Added support for `.tar` files.
.. versionchanged:: 1.4.0 Zstandard support.
storage_options : dict, optional
Extra options that make sense for a particular storage connection, e.g.
host, port, username, password, etc. For HTTP(S) URLs the key-value pairs
are forwarded to ``urllib.request.Request`` as header options. For other
URLs (e.g. starting with "s3://", and "gcs://") the key-value pairs are
forwarded to ``fsspec.open``. Please see ``fsspec`` and ``urllib`` for more
details, and for more examples on storage options refer `here
<https://pandas.pydata.org/docs/user_guide/io.html?
highlight=storage_options#reading-writing-remote-files>`_.
.. versionadded:: 1.2.0
Returns
-------
unpickled : same type as object stored in file
See Also
--------
DataFrame.to_pickle : Pickle (serialize) DataFrame object to file.
Series.to_pickle : Pickle (serialize) Series object to file.
read_hdf : Read HDF5 file into a DataFrame.
read_sql : Read SQL query or database table into a DataFrame.
read_parquet : Load a parquet object, returning a DataFrame.
Notes
-----
read_pickle is only guaranteed to be backwards compatible to pandas 0.20.3
provided the object was serialized with to_pickle.
Examples
--------
>>> original_df = pd.DataFrame(
... {"foo": range(5), "bar": range(5, 10)}
... ) # doctest: +SKIP
>>> original_df # doctest: +SKIP
foo bar
0 0 5
1 1 6
2 2 7
3 3 8
4 4 9
>>> pd.to_pickle(original_df, "./dummy.pkl") # doctest: +SKIP
>>> unpickled_df = pd.read_pickle("./dummy.pkl") # doctest: +SKIP
>>> unpickled_df # doctest: +SKIP
foo bar
0 0 5
1 1 6
2 2 7
3 3 8
4 4 9
Разберём, что у нас есть в строковой переменной документации?
Первая строка - это краткое описание метода: Load pickled pandas object (or any object) from file.
У нас есть разделы строки документации (они подчеркнуты символами "-"), например, Parameters, Returns, See Also, Notes, Examples
Различные сноски, начинающиеся с "..", например, .. warning:: , .. versionchanged::
В параметрах (аргументах) метода (Parameters) каждый параметр начинается без пробелов вначале, а его описание - с 4 пробелами.
В описании параметра после двоеточия приведены его возможные типы, например: str, path object, or file-like object, а также если есть значение по умолчанию, оно тоже приведено, например: default 'infer'
Аналогично, в выводимых функцией значениях: Returns
Мы также можем выжать из дандер-методов функции некоторую информацию, которая будет помогать нам разбирать (парсить) сроку документации для каждого метода (разберем первый из них, read_pickle
):
С помощью __annotations__
мы получим словарь, в котором ключами являются аргументы метода, а значениями - типы переменных (строка, в которой через разделитель "|" перечислены типы):
read_pickle = getattr(pd, 'read_pickle')
print(read_pickle.__annotations__)
{'filepath_or_buffer': 'FilePath | ReadPickleBuffer',
'compression': 'CompressionOptions',
'storage_options': 'StorageOptions'}
Некоторые аргументы метода имеют значения по умолчанию, их кортеж можно вывести с помощью __defaults__
:
read_pickle = getattr(pd, 'read_pickle')
print(read_pickle.__defaults__)
('infer', None)
Как видим, это два последних аргумента, поскольку вначале в питоновских методах и функциях идут атрибуты, которые не имеют значений по умолчанию, а потом - имеющие.
Еще мы можем получить путь к модулю метода с помощью __module__
:
read_pickle = getattr(pd, 'read_pickle')
print(read_pickle.__module__)
'pandas.io.pickle'
С помощью __class__ мы можем вывести важную характеристику для сортировки. Так, методы будут обозначены как 'function' - ведь метод это и есть функция в классе:
read_pickle = getattr(pd, 'read_pickle')
print(read_pickle.__class__)
function
А вот класс DataFrame будет обозначен как 'type':
dataframe = getattr(pd, 'DataFrame')
print(dataframe.__class__)
type
Наконец, атрибут будет обозначен как 'property' или ссылкой на более сложный объект:
series_values = getattr(pd.Series, "values")
print(series_values.__class__)
property
series_index = getattr(pd.Series, "index")
print(series_index.__class__)
pandas._libs.properties.AxisProperty
Все эти параметры пригодятся нам в нашем датафрейме - своеобразной "базе данных" методов, классов и свойств pandas.
Добавим в наш датафрейм еще несколько столбцов:
name - имя метода (без родительских классов)
type - определяет, это метод (function), класс (type) или свойство (property)
module - путь к модулю метода
doc - строка документации (вызываемая через
__doc__
)about - краткое описание метода (первая строка документации)
parameters - список аргументов / атрибутов методов
annotations - словарь {атрибут: строка типов}
defaults - словарь {атрибут: значение по умолчанию}
Напишем код, который добавит в наш датафрейм новые столбцы:
def get_next_attr(class_, attr: str):
"""Возвращает ссылку на метод класса или None, если такого метода нет"""
try:
return getattr(class_, attr)
except:
return None
def parse_type(type_str:str) -> str:
"""Получает название метода/функции/класса, возвращает очищенным"""
return type_str.replace("<class '", '').replace("'>", '')
prime_df['name'] = None # имя метода / класса / свойства
prime_df['type'] = None # определяет, это метод (function), класс (type) или свойство (property)
prime_df['module'] = None # путь к модулю метода
prime_df['doc'] = None # строка документации
prime_df['about'] = None # краткое описание метода (первая строка документации)
prime_df['parameters'] = None # список аргументов / атрибутов методв
prime_df['annotations'] = None # словарь атрибут: строка типов
prime_df['defaults'] = None # словарь атрибут: значение по умолчанию
for i, row in prime_df.iterrows():
len_name = row['method'].split('.')
method_name = row['method'].split('.')[-1]
prev_class = row['method'].split('.')[-2]
middle = row['method'].split('.')[1:-1]
method_path = row['method'].split('.')[1:]
metod_object = pd
for j, item in enumerate(row['method'].split('.')):
metod_object = get_next_attr(metod_object, item)
if metod_object is None:
break
row['name'] = method_name
if metod_object is not None:
try:
row['type'] = f"{parse_type(str(metod_object.__class__))}"
except AttributeError:
pass
try:
row['module'] = metod_object.__module__
except AttributeError:
pass
try:
if metod_object.__doc__.startswith('\n'):
row['doc'] = metod_object.__doc__[1:]
else:
row['doc'] = metod_object.__doc__
except AttributeError:
pass
try:
if len(metod_object.__doc__.split('\n')) > 1 \
and not metod_object.__doc__.split('\n')[0]:
row['about'] = metod_object.__doc__.split('\n')[1] #splitlines()[0]
except AttributeError:
pass
try:
row['parameters'] = metod_object.__annotations__.keys()
except AttributeError:
pass
try:
row['annotations'] = metod_object.__annotations__
except AttributeError:
pass
try:
if metod_object.__annotations__:
len_annotations = len(metod_object.__annotations__.keys())
else:
len_annotations = None
if metod_object.__defaults__:
len_defaults = len(metod_object.__defaults__)
else:
len_defaults = 0
if len_annotations is not None and len_defaults:
n_skip = len_annotations - len_defaults
row['defaults'] = dict(zip(list(metod_object.__annotations__.keys())[n_skip:], metod_object.__defaults__))
except AttributeError:
pass
Посмотрим, что у нас получилось (итоговый файл в csv):
prime_df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 2085 entries, 0 to 2084
Data columns (total 11 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 group 2085 non-null object
1 small_group 2085 non-null object
2 method 2085 non-null object
3 name 2085 non-null object
4 type 1992 non-null object
5 module 1060 non-null object
6 doc 1693 non-null object
7 about 1675 non-null object
8 parameters 947 non-null object
9 annotations 947 non-null object
10 defaults 421 non-null object
dtypes: object(11)
memory usage: 179.3+ KB
Итак, у нас по-прежнему 2085 методов, но вот получить ссылку на метод (и определить его тип - класс, функция или атрибут), применяя "в лоб"getattr()
удалось только 1992 раза. Методы вроде pandas.io.formats.style.Styler.to_excel
или pandas.arrays.NumpyExtensionArray
оказались "крепкими орешками".
Если вам удастся написать код, который позволит вызвать __class__ и __doc__ у списка из 93 нераспознанных методов, напишите в комментариях. Это будет воистину
python_senior_code
!
Также мы видим, что строка документации есть у 1675 методов, не считая 93 не распознанных, поскольку атрибуты не имеют строки документации. Параметры (атрибуты) есть у 947 классов, методов и функций.
И, наконец, еще один момент: info()
выдает нам размер датафрейма - memory usage: 179.3+ KB.
Не маловато ли - ведь csv-файл весит 2,8 Мб?
Чтобы определить реально занимаемую датафреймом память, вызовем memory_usage()
с параметром deep=True
(искать глубоко по всем ссылкам):
prime_df.memory_usage(deep=True)
Index 128
group 145811
small_group 170679
method 185840
name 136464
type 159729
module 108671
doc 2610492
about 201014
parameters 57616
annotations 230696
defaults 147568
dtype: int64
Выясним, что итоговый объём занимаемой датафреймом памяти 4,15 Мб:
prime_df.memory_usage(deep=True).sum()
4154708
Структура итогового, расширенного датафрейма
Теперь мы изменим формат нашего датафрейма. До сего момента у нас строками датафрейма являлись методы (а также классы, функции, параметры), а столбцами - их типовые свойства (группа, класс, строка документации, список аргументов). Но, согласитесь, эта информация пока недостаточно детализирована. Нам нужно иметь более гибкую "базу данных", где каждой строкой было бы то или иное свойство того или иного метода/класса/атрибута pandas
.
Давайте посмотрим, какие вообще разделы существуют в строке документации (разделы подчеркнуты символами тире). А также - какие виды примечаний существуют? Для этого переберем наш датафрейм, и выведем множество всех разделов и множество всех примечаний.
Создадим множество (set) всех разделов строки документации. Раздел документации подчеркнут (на следующей строке) тире '-' и начинается с начала строки. Раздел продолжается до названия следующего раздела
Также создадим множество (set) примечаний документации - они содержат вначале пробелы и начинаются с двойных точек '..' (но нужно исключить случаи тройных точек '...' - с них начинается строка вывода в примере!) и заканчивается двойными двоеточиями: '::' Информация примечаний начинается с :: и продолжается до следующего примечания .. или названия следующего раздела.
Код, возвращающий множество всех разделов документации и множество примечаний
def get_uniq_sections_notes(element: str) -> tuple:
"""Принимает строку - название метода pandas, возвращает кортеж множеств разделов и примечаний документации"""
uniq_sections = set()
uniq_notes = set()
if element is None:
return uniq_sections, uniq_notes
lines = element.splitlines()
prev_line = ''
for line in lines:
if line.startswith('--'):
uniq_sections.add(prev_line)
continue
ind_end = -1
if line.strip().startswith('..') and not line.strip().startswith('...'):
note_line = line.strip().replace('..','').strip()
ind_end = note_line.find('::')
if ind_end != -1:
note_line = note_line[:ind_end].strip()
uniq_notes.add(note_line)
prev_line = line.strip()
return uniq_sections, uniq_notes
uniq_sections = set()
uniq_notes = set()
for value in prime_df['doc'].values:
uniq_sections_new, uniq_notes_new = get_uniq_sections_notes(value)
uniq_sections = uniq_sections | uniq_sections_new
uniq_notes = uniq_notes | uniq_notes_new
Итак, уникальные разделы документации (два первых исключаем - это не разделы, они встречаются только в примерах):
uniq_sections
{'# Column Non-Null Count Dtype',
'# Column Non-Null Count Dtype',
'Attributes',
'Examples',
'Methods',
'Notes',
'Parameters',
'Raises',
'Returns',
'See Also',
'Yields'}
Уникальные метки примечаний документации:
uniq_notes
{'code-block',
'deprecated',
'math',
'note',
'plot',
'versionadded',
'versionchanged',
'warning'}
Отлично, теперь мы можем описать структуру расширенного датафрейма - "базы данных" методов pandas
.
Создадим датафрейм, в котором каждая строка - это отдельный элемент описания (документации) какого-либо метода. Используя один (!) такой датафрейм, мы быстро сможем извлечь различную необходимую информацию или статистику. Здесь нам понадобится лишь три универсальных столбца:
object (в него мы переименуем method) - название функции/метода/класса, содержащее родительские объекты и начинающееся на pandas.
element_type - вид элемента описания, ниже мы разберем все варианты
element_value - значение элемента описания
pandas_doc_df = pd.DataFrame(columns=['object', 'element_type', 'element_value'])
Такие таблицы называются melted - расправленные. Данные из множества столбцов как бы "стекли" в строки...
Какие же типы элементов описания каждой функции мы введём? Во-первых, те, которые мы уже выбрали ранее:
group - группа в документации
small_group - малая группа в документации
name - имя объекта, может дублироваться
type - описание типа (класс, функция, свойство),
module - модуль
doc - большая строка документации
about - описание функции
parameters - список атрибутов
annotations - словарь {атрибут: типы}. Типы представляют строку, разделенную '|'
defaults - словарь {атрибут: значение по умолчанию}
Но кроме этих типов элементов, мы введем дополнительно:
about_detailed - более детальное описание с первой строки до следующего раздела, как правило, следующий раздел Parameters. Включая примечания, начинающиеся с '..'
parameter_types_default_str - словарь {параметр: строка, содержащая варианты типов}, а также тип по умолчанию (часть строки после названия параметра). Каждая строка датафрейма - отдельный параметр.
parameter_doc_dict - словарь {параметр: строка, содержащая все данные до следующего параметра}. Каждая строка датафрейма - отдельный параметр.
returns_str - строка описания до следующего раздела
attributes_dict - словарь, содержащий {атрибут: строка описания}. Каждая строка датафрейма - отдельный атрибут.
methods_dict - словарь, содержащий {атрибут: строка описания}. Каждая строка датафрейма - отдельный атрибут.
notes - строка раздела Примечание
examples - строка раздела с примерами
yields - раздел генерируемых значений
raises - строка раздела исключения
see_also - строка раздела См.также
Мы можем создать список из 3 значений строки датафрейма, сведя их в итоговый "список списков", а потом сразу загрузить такой список в датафрейм. Переберем все строки в prime_df
и вытащим данные из всех столбцов, сформировав по каждому строку для датафрейма. Далее возьмем строку документации __doc__
и распарсим её, сформировав еще несколько строк, по числу найденных элементов документации.
Напишем код для формирования такого датафрейма:
doc_data = []
def append_element(metod, element_type):
"""Добавляет в список doc_data список [metod, element_type, element_value]"""
if row[element_type] is not None:
element_value = row[element_type]
doc_data.append([metod, element_type, element_value])
for i, row in prime_df.iterrows():
metod = row['method']
# Добавим уже известные нам данные
for element_type in ['group', 'small_group', 'name', 'type',
'module', 'doc', 'about', 'parameters',
'annotations', 'defaults']:
append_element(metod, element_type)
# Парсим детализованное описание функции (до первого иного раздела в uniq_sections)
element_type = 'about_detailed'
if row['doc'] is not None:
doc_lines = row['doc'].splitlines()
element_value = ''
for j, doc_string in enumerate(doc_lines):
if j < len(doc_lines)-1 and doc_lines[j+1].strip() in uniq_sections:
break
else:
element_value = f"{element_value}\n{doc_string}"
doc_data.append([metod, element_type, element_value])
# Парсим параметры метода/фукции и их описание
element_type = 'parameter_types_default_str'
if row['doc'] is not None:
# Сформируем список строк с параметрами param_strings
element_value = None
param_strings = []
is_param = False
for j, doc_string in enumerate(doc_lines):
if doc_string.strip() == 'Parameters':
is_param = True
if doc_string.strip() in (uniq_sections - {'Parameters'}):
break
if is_param:
param_strings.append(doc_string)
# Сформируем список строк с параметрами param_strings
if param_strings:
param_doc = ''
for param_string in param_strings[2:]:
try:
in_list = param_string.split(' : ')[0].strip() in list(row['parameters'])
except:
in_list = False
if not param_string.startswith(' ') and ' : ' in param_string and in_list:
if param_doc != '':
element_type = 'parameter_doc_dict'
element_value = {element_value_key: param_doc}
doc_data.append([metod, element_type, element_value])
element_type = 'parameter_types_default_str'
element_value_key = param_string.split(':')[0].strip()
element_value_item = param_string.split(':')[1].strip()
element_value = {element_value_key: element_value_item}
doc_data.append([metod, element_type, element_value])
param_doc = f"{element_value_key}: {element_value_item}\n"
continue
param_doc = f"{param_doc}\n{param_string}"
element_type = 'parameter_doc_dict'
element_value = {element_value_key: param_doc}
doc_data.append([metod, element_type, element_value])
# Парсим данные, возвращаемые методом:
element_type = 'returns_str'
if row['doc'] is not None:
element_value = None
return_strings = []
is_return = False
for j, doc_string in enumerate(doc_lines):
if doc_string.strip() == 'Returns':
is_return = True
if doc_string.strip() in uniq_sections - {'Parameters', 'Returns'}:
break
if is_return:
return_strings.append(doc_string)
if return_strings:
element_value = '\n'.join(return_strings[2:])
if element_value:
doc_data.append([metod, element_type, element_value])
# Парсим атрибуты:
element_type = 'attributes_dict'
if row['doc'] is not None:
element_value = None
attr_strings = []
is_attr = False
for j, doc_string in enumerate(doc_lines):
if doc_string.strip() == 'Attributes':
is_attr = True
if is_attr and (doc_string.strip() in uniq_sections - {'Attributes'}):
break
if is_attr:
attr_strings.append(doc_string)
if doc_string.strip() in ['', 'None']:
attr_strings = []
is_attr = False
if attr_strings:
attr_doc = ''
for attr_string in attr_strings[2:]:
if not attr_string.startswith(' ') and ':' in attr_string:
if attr_doc != '':
element_type = 'attributes_dict'
element_value = {element_value_key: attr_doc}
doc_data.append([metod, element_type, element_value])
continue
element_value_key = attr_string.split(':')[0].strip()
element_value_item = attr_string.split(':')[1].strip()
element_value = {element_value_key: element_value_item}
attr_doc = f"{attr_doc}\n{attr_string}"
element_type = 'attributes_dict'
element_value = {element_value_key: attr_doc}
doc_data.append([metod, element_type, element_value])
element_type = 'methods_dict'
if row['doc'] is not None:
element_value = None
methods_strings = []
is_method = False
for j, doc_string in enumerate(doc_lines):
if doc_string.strip() == 'Methods':
is_method = True
if is_method and doc_string.strip() in uniq_sections - {'Methods'}:
break
if is_method:
methods_strings.append(doc_string)
if doc_string.strip() in ['', 'None']:
methods_strings = []
is_method = False
if methods_strings:
method_doc = ''
for method_string in methods_strings[2:]:
if not method_string.startswith(' ') and ':' in method_string:
if method_doc != '':
element_type = 'methods_dict'
element_value = {element_value_key: method_doc}
doc_data.append([metod, element_type, element_value])
continue
method_doc = f"{method_doc}\n{method_string}"
element_type = 'methods_dict'
element_value = {element_value_key: method_doc}
doc_data.append([metod, element_type, element_value])
element_type = 'notes'
if row['doc'] is not None:
element_value = None
notes_strings = []
is_notes = False
for j, doc_string in enumerate(doc_lines):
if doc_string.strip() == 'Notes':
is_notes = True
if is_notes and doc_string.strip() in uniq_sections - {'Notes'}:
break
if is_notes:
notes_strings.append(doc_string)
element_value = '\n'.join(notes_strings[2:])
if element_value:
doc_data.append([metod, element_type, element_value])
element_type = 'examples'
if row['doc'] is not None:
element_value = None
examples_strings = []
is_examples = False
for j, doc_string in enumerate(doc_lines):
if doc_string.strip() == 'Examples':
is_examples = True
if is_examples and doc_string.strip() in uniq_sections - {'Examples'}:
break
if is_examples:
examples_strings.append(doc_string)
element_value = '\n'.join(examples_strings[2:])
if element_value:
doc_data.append([metod, element_type, element_value])
element_type = 'yields'
if row['doc'] is not None:
element_value = None
yields_strings = []
is_yields = False
for j, doc_string in enumerate(doc_lines):
if doc_string.strip() == 'Yields':
is_yields = True
if is_yields and doc_string.strip() in uniq_sections - {'Yields'}:
break
if is_yields:
yields_strings.append(doc_string)
element_value = '\n'.join(yields_strings[2:])
if element_value:
doc_data.append([metod, element_type, element_value])
element_type = 'raises'
if row['doc'] is not None:
element_value = None
raises_strings = []
is_raises = False
for j, doc_string in enumerate(doc_lines):
if doc_string.strip() == 'Raises':
is_raises = True
if is_raises and doc_string.strip() in uniq_sections - {'Raises'}:
break
if is_raises:
raises_strings.append(doc_string)
element_value = '\n'.join(raises_strings[2:])
if element_value:
doc_data.append([metod, element_type, element_value])
element_type = 'see_also'
if row['doc'] is not None:
element_value = None
see_also_strings = []
is_see_also = False
for j, doc_string in enumerate(doc_lines):
if doc_string.strip() == 'See Also':
is_see_also = True
if is_see_also and doc_string.strip() in uniq_sections - {'See Also'}:
break
if is_see_also:
see_also_strings.append(doc_string)
element_value = '\n'.join(see_also_strings[2:])
if element_value:
doc_data.append([metod, element_type, element_value])
columns = ['object', 'element_type', 'element_value']
pandas_doc_df = pd.DataFrame(columns=columns, data = doc_data)
Посмотрим статистику по полученному датафрейму: 22589 записей!
pandas_doc_df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 22589 entries, 0 to 22588
Data columns (total 3 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 object 22589 non-null object
1 element_type 22589 non-null object
2 element_value 22589 non-null object
dtypes: object(3)
memory usage: 529.6+ KB
Подсчитаем статистику по количеству записей разных типов:
pandas_doc_df.groupby(['element_type']).agg({'object': 'count'}).reset_index().sort_values('object',ascending=False, ignore_index=True)

Полученный датафрейм с документацией можно скачать: pandas_doc_df.csv и со статистикой: stat_doc.csv
Итоговый размер файла, который вместил в себя максимум документации pandas: 6,2 Мб
Примеры выборок из полученного датафрейма:
Выборки можно делать обычными средствами pandas
Ищем все данные по методу pandas.read_pickle:
# Ищем все данные по методу pandas.read_pickle:
pandas_doc_df[pandas_doc_df.object == 'pandas.read_pickle']

# Найти определенные типы описания по известному методу, напр., 'pandas.read_pickle'
pandas_doc_df[(pandas_doc_df.object == 'pandas.read_pickle')
& (pandas_doc_df.element_type == 'parameter_doc_dict')]
# Ищем все методы со строкой документации:
pandas_doc_df[pandas_doc_df.element_type == 'doc']
# Ищем все методы со словом excel/Excel:
all_names = pandas_doc_df[pandas_doc_df.element_type == 'name']
all_names[all_names.element_value.str.contains('excel')
| all_names.element_value.str.contains('Excel')]
# Ищем все методы с yields:
pandas_doc_df[pandas_doc_df.element_type == 'yields']
В последнем это: pandas.HDFStore.walk
, pandas.DataFrame.items
, pandas.DataFrame.iterrows
Наконец, найдем методы, в которых больше всего параметров:
# Найдем методы с максимальным количеством параметров:
sort_param = pandas_doc_df[pandas_doc_df.element_type == 'parameter_doc_dict']
sort_param = sort_param.groupby('object').agg({'element_value': 'count'}).reset_index().sort_values('element_value',ascending=False)
sort_param

Итого
Код можно было бы еще доработать, разобрав некоторые составляющие документации на списки или словари. Часть кода можно сократить (DRY), введя функции (но это будут весьма сложные функции... и лучшее - враг того хорошего, которое можно сделать в сэкономленное на отказе от лучшего время). Итоговый код можно скачать здесь: parse_pandas.ipynb