Возможно, это поведение можно рассматривать как небольшую ошибку в distutils
-пакете. Тем не менее, это также показывает, что цитонизация / компиляция __init__.py
не очень популярна и использует некоторые недокументированные детали реализации, которые могут измениться в будущем, поэтому было бы разумнее воздержаться от вмешательства с __init__.py
.
* 1005. * Но если вы должны ...
Когда пакет импортируется явно, например,
import ctest
или неявно, например,
import ctest.something
FileFinder
увидит, что пакет, а не модуль импортирован, и попытается загрузить ctest/__init__.py
вместо ctest.py
(которого, скорее всего, не существует):
# Check if the module is the name of a directory (and thus a package).
if cache_module in cache:
base_path = _path_join(self.path, tail_module)
for suffix, loader_class in self._loaders:
init_filename = '__init__' + suffix
full_path = _path_join(base_path, init_filename)
if _path_isfile(full_path):
return self._get_spec(loader_class, fullname, full_path, [base_path], target)
Используется suffix, loader_class
для загрузки __init__.so
, __init__.py
и __init__.pyc
в этом порядке (см. Также SO-post ). Это означает, что __init__.so
будет загружено вместо __init__.py
, если нам удастся его создать.
Пока выполняется __init__.py
, свойство __name__
является именем пакета, то есть ctest
в вашем случае, а не __init__
, как можно подумать. Таким образом, имя init-функции, которую Python-интерпретатор будет вызывать при загрузке расширения __init__.so
, равно PyInit_ctest
в вашем случае (а не PyInit___init__
, как можно подумать).
Выше объясняетсяпочему все это работает на Linux из коробки? А как насчет Windows?
Загрузчик может использовать только символы из so / dll, которые не скрыты. По умолчанию все символы, созданные с помощью gcc, являются видимыми, но не для VisualStudio в Windows, где все символы скрыты по умолчанию (см., Например, SO-post ).
Однако init-функция C-расширения должна быть видимой (и только init-функцией), чтобы ее можно было вызывать с помощью загрузчика - решение состоит в том, чтобы экспортировать этот символ (т.е. PyInit_ctest
) при компоновке, в вашем случае этонеправильный вариант /EXPORT:PyInit___init__
для компоновщика.
Проблема может быть найдена в distutils или, более точно, в build_ext
-классе :
def get_export_symbols(self, ext):
"""Return the list of symbols that a shared extension has to
export. This either uses 'ext.export_symbols' or, if it's not
provided, "PyInit_" + module_name. Only relevant on Windows, where
the .pyd file (DLL) must export the module "PyInit_" function.
"""
initfunc_name = "PyInit_" + ext.name.split('.')[-1]
if initfunc_name not in ext.export_symbols:
ext.export_symbols.append(initfunc_name)
return ext.export_symbols
Здеськ сожалению, ext.name
содержит __init__
.
Отсюда одно возможное решение легко: переопределить get_export_symbols
, т.е. добавить следующее в ваш setup.py
-файл (читайте дальше дляеще более простая версия):
...
from distutils.command.build_ext import build_ext
def get_export_symbols_fixed(self, ext):
names = ext.name.split('.')
if names[-1] != "__init__":
initfunc_name = "PyInit_" + names[-1]
else:
# take name of the package if it is an __init__-file
initfunc_name = "PyInit_" + names[-2]
if initfunc_name not in ext.export_symbols:
ext.export_symbols.append(initfunc_name)
return ext.export_symbols
# replace wrong version with the fixed:
build_ext.get_export_symbols = get_export_symbols_fixed
...
Теперь достаточно набрать python setup.py build_ext -i
(потому что будет загружено __init__.so
вместо __init__.py
).
Тем не менее, как @DawidW указал, что Cython использует макрос PyMODINIT_FUNC
, который определяется как
#define PyMODINIT_FUNC Py_EXPORTED_SYMBOL PyObject*
с Py_EXPORTED_SYMBOL
помечается как видимый / экспортируемый в Windows:
#define Py_EXPORTED_SYMBOL __declspec(dllexport)
Таким образом, нет необходимости отмечать символ как видимый в командной строке. Что еще хуже, это является причиной предупреждения LNK4197 :
init .obj: предупреждение LNK4197: экспорт 'PyInit_ctest' указан несколько раз;с использованием первой спецификации
в качестве PyInit_test
помечается как __declspec(dllexport)
и одновременно экспортируется через опцию /EXPORT:
.
/EXPORT:
-опция будетпропущенный distutils , если export_symbols
пусто, мы можем использовать даже более простую версию command.build_ext
:
...
from distutils.command.build_ext import build_ext
def get_export_symbols_fixed(self, ext):
pass # return [] also does the job!
# replace wrong version with the fixed:
build_ext.get_export_symbols = get_export_symbols_fixed
...
Это даже лучше, чем первая версия, так как она также исправляет предупреждение LNK4197!