Importowanie z wbudowanej biblioteki, gdy istnieje moduł o tej samej nazwie

121

Sytuacja: - W moim folderze_projektów znajduje się moduł o nazwie calendar - Chciałbym użyć wbudowanej klasy Calendar z bibliotek Pythona - Kiedy używam kalendarza Import Calendar, narzeka, ponieważ próbuje załadować z mojego modułu.

Przeprowadziłem kilka wyszukiwań i nie mogę znaleźć rozwiązania mojego problemu.

Jakieś pomysły bez konieczności zmiany nazwy mojego modułu?

Gałązka
źródło
24
Najlepszą praktyką jest nienazywanie modułów w celu ukrycia modułów wbudowanych.
the_drow
3
Rozwiązaniem jest „wybierz inną nazwę”. Twoje podejście do niezmieniania nazwy to zły pomysł. Dlaczego nie możesz zmienić nazwy swojego modułu? Co jest złego w zmianie nazwy?
S.Lott
W rzeczy samej. Ponieważ nie ma dobrej odpowiedzi na to pytanie, shadowing modułów stdlib jest tak mocno odradzany.
ncoghlan
Uniknąłem używania tej samej nazwy modułu, ponieważ rozwiązania wydawały się bardziej kłopotliwe niż warte. Dzięki!
gałązka
9
@the_drow Ta rada nie jest skalowalna, czysta i prosta. PEP328 łatwo to potwierdza.
Konrad Rudolph

Odpowiedzi:

4

Przyjęte rozwiązanie zawiera obecnie przestarzałe podejście.

Dokumentacja importlib tutaj podaje dobry przykład bardziej odpowiedniego sposobu ładowania modułu bezpośrednio ze ścieżki pliku dla pythona> = 3.5:

import importlib.util
import sys

# For illustrative purposes.
import tokenize
file_path = tokenize.__file__  # returns "/path/to/tokenize.py"
module_name = tokenize.__name__  # returns "tokenize"

spec = importlib.util.spec_from_file_location(module_name, file_path)
module = importlib.util.module_from_spec(spec)
sys.modules[module_name] = module
spec.loader.exec_module(module)

Możesz więc załadować dowolny plik .py ze ścieżki i ustawić nazwę modułu na dowolną. Więc po prostu dostosuj module_namedowolną niestandardową nazwę, jaką ma mieć moduł podczas importowania.

Aby załadować pakiet zamiast pojedynczego pliku, file_pathpowinna być ścieżką do katalogu głównego pakietu__init__.py

Brandon Squizzato
źródło
Działa jak urok ... Użyłem tego do testowania podczas tworzenia biblioteki, więc moje testy zawsze korzystały z wersji rozwojowej, a nie opublikowanej (i zainstalowanej). W oknach 10 miałem napisać ścieżkę do mojego modułu tak: file_path=r"C:\Users\My User\My Path\Module File.py". Potem zadzwoniłem module_nametak jak wydany moduł, dzięki czemu miałem pełny działający skrypt, który po usunięciu tego fragmentu mógł być używany na innych komputerach
Luke Savefrogs
141

Zmiana nazwy modułu nie jest konieczna. Zamiast tego, aby zmienić zachowanie importu, możesz użyć bezwzględnego importu. Na przykład za pomocą stem / socket.py importuję moduł gniazda w następujący sposób:

from __future__ import absolute_import
import socket

Działa to tylko z Pythonem 2.5 i nowszymi; umożliwia zachowanie, które jest domyślne w Pythonie 3.0 i nowszych. Pylint będzie narzekał na kod, ale jest on całkowicie prawidłowy.

Damian
źródło
4
To wydaje mi się poprawna odpowiedź. Zobacz dziennik zmian 2.5 lub PEP328, aby uzyskać więcej informacji.
Pieter Ennes
5
To jest poprawne rozwiązanie. Niestety, to nie działa, gdy kod z pakietu jest uruchamiany, ponieważ wtedy pakiet nie jest rozpoznawany jako taki, a ścieżka lokalna jest poprzedzona PYTHONPATH. Kolejne pytanie pokazuje, jak rozwiązać ten problem.
Konrad Rudolph
5
To jest rozwiązanie. Sprawdziłem pod kątem Pythona 2.7.6 i jest to wymagane, nadal nie jest domyślne.
Havok
3
Rzeczywiście: pierwszą wersją Pythona, w której to zachowanie jest domyślne, była 3.0, zgodnie z docs.python.org/2/library/__future__.html
błędna nazwa
1
Następnie nie nazywaj swojego modułu głównego jako takiego, który koliduje z modułem wbudowanym.
Antti Haapala,
38

Właściwie rozwiązanie tego jest dość łatwe, ale implementacja zawsze będzie nieco krucha, ponieważ zależy to od wewnętrznych mechanizmów importu Pythona i mogą one ulec zmianie w przyszłych wersjach.

(poniższy kod pokazuje, jak załadować lokalne i nielokalne moduły i jak mogą one współistnieć)

def import_non_local(name, custom_name=None):
    import imp, sys

    custom_name = custom_name or name

    f, pathname, desc = imp.find_module(name, sys.path[1:])
    module = imp.load_module(custom_name, f, pathname, desc)
    f.close()

    return module

# Import non-local module, use a custom name to differentiate it from local
# This name is only used internally for identifying the module. We decide
# the name in the local scope by assigning it to the variable calendar.
calendar = import_non_local('calendar','std_calendar')

# import local module normally, as calendar_local
import calendar as calendar_local

print calendar.Calendar
print calendar_local

Najlepszym rozwiązaniem, jeśli to możliwe, jest unikanie nazywania modułów o takiej samej nazwie, jak nazwy bibliotek standardowych lub wbudowanych modułów.

Boaz Yaniv
źródło
W jaki sposób będzie to współdziałać z sys.modulespóźniejszymi próbami załadowania modułu lokalnego?
Omnifarious
@Omnifarious: Doda moduł do sys.modules z jego nazwą, co uniemożliwi załadowanie modułu lokalnego. Aby tego uniknąć, zawsze możesz użyć własnej nazwy.
Boaz Yaniv
@Boaz Yaniv: Powinieneś używać niestandardowej nazwy kalendarza lokalnego, a nie standardowej. Inne moduły Pythona mogą próbować importować standardowy. A jeśli to zrobisz, to, co dzięki temu osiągniesz, to w zasadzie zmiana nazwy modułu lokalnego bez konieczności zmiany nazwy pliku.
Omnifarious
@Omnifarious: Możesz to zrobić tak czy inaczej. Inny kod może próbować załadować moduł lokalny i otrzymać ten sam błąd. Będziesz musiał pójść na kompromis i od Ciebie zależy, który moduł będzie obsługiwany.
Boaz Yaniv
2
Dzięki za tego Booza! Chociaż twój fragment jest krótszy (i dokument), myślę, że po prostu łatwiej jest zmienić nazwę modułu niż mieć jakiś hackerski kod, który może zmylić ludzi (lub mnie) w przyszłości.
gałązka
15

Jedynym sposobem rozwiązania tego problemu jest samodzielne przejęcie wewnętrznej maszyny importowej. Nie jest to łatwe i wiąże się z ryzykiem. Za wszelką cenę powinieneś unikać latarni w kształcie Graala, ponieważ niebezpieczeństwo jest zbyt niebezpieczne.

Zamiast tego zmień nazwę modułu.

Jeśli chcesz się dowiedzieć, jak przejąć wewnętrzne maszyny importujące, oto, gdzie możesz się dowiedzieć, jak to zrobić:

Czasami są dobre powody, by wpaść w to niebezpieczeństwo. Powód, dla którego podajesz, nie znajduje się wśród nich. Zmień nazwę modułu.

Jeśli wybierzesz niebezpieczną ścieżkę, jednym z problemów, które napotkasz, jest to, że po załadowaniu modułu otrzymuje on „oficjalną nazwę”, dzięki czemu Python może uniknąć konieczności ponownego analizowania zawartości tego modułu. Mapowanie „oficjalnej nazwy” modułu do samego obiektu modułu można znaleźć w plikusys.modules .

Oznacza to, że jeśli jesteś import calendarw jednym miejscu, dowolny importowany moduł będzie traktowany jako moduł z oficjalną nazwą calendari wszystkimi innymi próbamiimport calendar dowolnego miejsca, w tym w innym kodzie, który jest częścią głównej biblioteki Pythona, otrzyma ten kalendarz.

Możliwe byłoby zaprojektowanie importera klienta przy użyciu modułu imputil w Pythonie 2.x, który powodowałby, że moduły ładowane z określonych ścieżek szukały modułów, które importowały w czymś innym niżsys.modules pierwszy lub coś podobnego. Ale jest to niezwykle skomplikowana rzecz, a i tak nie zadziała w Pythonie 3.x.

Jest bardzo brzydka i okropna rzecz, którą możesz zrobić, która nie wymaga podpięcia mechanizmu importu. To jest coś, czego prawdopodobnie nie powinieneś robić, ale prawdopodobnie zadziała. Zmienia Twój calendarmoduł w hybrydę modułu kalendarza systemowego i modułu kalendarza. Podziękowania dla Boaz Yaniv za szkielet funkcji, z której korzystam . Umieść to na początku calendar.pypliku:

import sys

def copy_in_standard_module_symbols(name, local_module):
    import imp

    for i in range(0, 100):
        random_name = 'random_name_%d' % (i,)
        if random_name not in sys.modules:
            break
        else:
            random_name = None
    if random_name is None:
        raise RuntimeError("Couldn't manufacture an unused module name.")
    f, pathname, desc = imp.find_module(name, sys.path[1:])
    module = imp.load_module(random_name, f, pathname, desc)
    f.close()
    del sys.modules[random_name]
    for key in module.__dict__:
        if not hasattr(local_module, key):
            setattr(local_module, key, getattr(module, key))

copy_in_standard_module_symbols('calendar', sys.modules[copy_in_standard_module_symbols.__module__])
Wszelaki
źródło
imputil jest uważany za przestarzały. Powinieneś użyć modułu imp .
Boaz Yaniv
Co, nawiasem mówiąc, jest całkowicie zgodne z Pythonem 3. I wcale nie taki włochaty w użyciu. Ale zawsze należy mieć świadomość, że kod, który polega na traktowaniu ścieżek w Pythonie w jeden sposób lub wyszukiwaniu modułów w tej kolejności, może prędzej czy później ulec uszkodzeniu.
Boaz Yaniv
1
Zgadza się, ale w tak odosobnionym przypadku (kolizja nazw modułów) podpięcie mechanizmu importu jest przesadą. A ponieważ jest włochaty i niekompatybilny, lepiej zostawić go w spokoju.
Boaz Yaniv
1
@jspacek nope, jak na razie dobrze, ale kolizja wystąpiłaby tylko podczas korzystania z debbugera PyDev, a nie w zwykłym użyciu. I upewnij się, że sprawdziłeś najnowszy kod (adres URL na githubie), ponieważ zmienił się nieco od powyższej odpowiedzi
MestreLion
1
@jspacek: To gra, a nie biblioteka, więc w moim przypadku wsteczna kompatybilność nie jest w ogóle problemem. A kolizja przestrzeni nazw występuje tylko podczas używania środowiska PyDev IDE (które używa codemodułu std Pythona ), co oznacza, że ​​tylko ułamek programistów może kiedykolwiek mieć jakiekolwiek problemy z tym „hackiem scalającym”. Użytkownicy nie byliby w ogóle dotknięci.
MestreLion
1

Chciałbym zaoferować moją wersję, która jest połączeniem rozwiązania Boaz Yaniv i Omnifarious. Zaimportuje systemową wersję modułu, z dwiema głównymi różnicami w stosunku do poprzednich odpowiedzi:

  • Obsługuje notację „kropkową”, np. package.module
  • Jest zastępczym zamiennikiem instrukcji import w modułach systemu, co oznacza, że ​​wystarczy zamienić tę jedną linię, a jeśli są już wykonywane połączenia do modułu, będą działać tak, jak są

Umieść to w miejscu dostępnym, abyś mógł to nazwać (mam mój w moim pliku __init__.py):

class SysModule(object):
    pass

def import_non_local(name, local_module=None, path=None, full_name=None, accessor=SysModule()):
    import imp, sys, os

    path = path or sys.path[1:]
    if isinstance(path, basestring):
        path = [path]

    if '.' in name:
        package_name = name.split('.')[0]
        f, pathname, desc = imp.find_module(package_name, path)
        if pathname not in __path__:
            __path__.insert(0, pathname)
        imp.load_module(package_name, f, pathname, desc)
        v = import_non_local('.'.join(name.split('.')[1:]), None, pathname, name, SysModule())
        setattr(accessor, package_name, v)
        if local_module:
            for key in accessor.__dict__.keys():
                setattr(local_module, key, getattr(accessor, key))
        return accessor
    try:
        f, pathname, desc = imp.find_module(name, path)
        if pathname not in __path__:
            __path__.insert(0, pathname)
        module = imp.load_module(name, f, pathname, desc)
        setattr(accessor, name, module)
        if local_module:
            for key in accessor.__dict__.keys():
                setattr(local_module, key, getattr(accessor, key))
            return module
        return accessor
    finally:
        try:
            if f:
                f.close()
        except:
            pass

Przykład

Chciałem zaimportować mysql.connection, ale miałem już lokalny pakiet o nazwie mysql (oficjalne narzędzia mysql). Aby więc pobrać złącze z systemowego pakietu mysql, wymieniłem to:

import mysql.connector

Z tym:

import sys
from mysql.utilities import import_non_local         # where I put the above function (mysql/utilities/__init__.py)
import_non_local('mysql.connector', sys.modules[__name__])

Wynik

# This unmodified line further down in the file now works just fine because mysql.connector has actually become part of the namespace
self.db_conn = mysql.connector.connect(**parameters)
Casey
źródło
-2

Zmień ścieżkę importu:

import sys
save_path = sys.path[:]
sys.path.remove('')
import calendar
sys.path = save_path
linuty
źródło
To nie zadziała, ponieważ po wykonaniu tej czynności nie będzie możliwości zaimportowania modułu lokalnego bez samodzielnego korzystania z maszyny importu.
Wszechstronny
@Omnifarious: to inny problem, który można obejść za pomocą trzeciego modułu, który wykonuje import z kalendarza *.
linuty
Nie, to prawdopodobnie nie zadziała, ponieważ Python buforuje nazwę modułu w sys.modulesi nie zaimportuje ponownie modułu o tej samej nazwie.
Boaz Yaniv