Importación de la biblioteca incorporada cuando existe un módulo con el mismo nombre

121

Situación: - Hay un módulo en mi carpeta_proyecto llamado calendario - Me gustaría usar la clase Calendario incorporada de las bibliotecas de Python - Cuando utilizo Calendario de importación de calendario, se queja porque está intentando cargar desde mi módulo.

He hecho algunas búsquedas y parece que no puedo encontrar una solución a mi problema.

¿Alguna idea sin tener que cambiar el nombre de mi módulo?

ramita
fuente
24
Es una buena práctica no nombrar los módulos para ocultar los módulos integrados.
the_drow
3
La solución es "elegir un nombre diferente". Su enfoque de no cambiar el nombre es una mala idea. ¿Por qué no puede cambiar el nombre de su módulo? ¿Qué hay de malo en cambiar el nombre?
S.Lott
En efecto. Debido a que no hay una buena respuesta a esta pregunta, se desaconseja tanto el remedo de módulos stdlib.
ncoghlan
Evité usar el mismo nombre de módulo ya que las soluciones parecían más problemáticas de lo que vale la pena. ¡Gracias!
twig
9
@the_drow Este consejo no escala, puro y simple. PEP328 lo reconoce fácilmente.
Konrad Rudolph

Respuestas:

4

La solución aceptada contiene un enfoque ahora obsoleto.

La documentación de importlib aquí da un buen ejemplo de la forma más apropiada de cargar un módulo directamente desde una ruta de archivo para python> = 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)

Por lo tanto, puede cargar cualquier archivo .py desde una ruta y configurar el nombre del módulo para que sea el que desee. Así que ajusta elmodule_name para que sea el nombre personalizado que le gustaría que tuviera el módulo al importar.

Para cargar un paquete en lugar de un solo archivo, file_pathdebe ser la ruta a la raíz del paquete__init__.py

Brandon Squizzato
fuente
Funciona como un encanto ... Usé esto para probar mientras desarrollaba una biblioteca, de modo que mis pruebas siempre usaran la versión en desarrollo y no la publicada (e instalada). En Windows 10 que tenía que escribir la ruta de acceso al módulo de la siguiente manera: file_path=r"C:\Users\My User\My Path\Module File.py". Luego llamé module_namecomo el módulo publicado para tener un script de trabajo completo que, eliminado de este fragmento, podría usarse en otras PC
Luke Savefrogs
141

No es necesario cambiar el nombre de su módulo. Por el contrario, puede utilizar absolute_import para cambiar el comportamiento de importación. Por ejemplo, con stem / socket.py importo el módulo de socket de la siguiente manera:

from __future__ import absolute_import
import socket

Esto solo funciona con Python 2.5 y superior; está habilitando el comportamiento que es el predeterminado en Python 3.0 y superior. Pylint se quejará del código pero es perfectamente válido.

Damian
fuente
4
Esta me parece la respuesta correcta. Consulte el registro de cambios 2.5 o PEP328 para obtener más información.
Pieter Ennes
5
Ésta es la solución correcta. Desafortunadamente, no funciona cuando se lanza código desde dentro del paquete porque entonces el paquete no se reconoce como tal y la ruta local se antepone a PYTHONPATH. Otra pregunta muestra cómo resolver eso.
Konrad Rudolph
5
Esta es la solucion. Verifiqué Python 2.7.6 y esto es obligatorio, todavía no es el predeterminado.
Havok
3
De hecho: la primera versión de Python donde este comportamiento es el predeterminado fue 3.0, según docs.python.org/2/library/__future__.html
nombre inapropiado
1
Entonces no nombre su módulo principal como uno que choca con un módulo incorporado.
Antti Haapala
38

En realidad, resolver esto es bastante fácil, pero la implementación siempre será un poco frágil, porque depende de los componentes internos del mecanismo de importación de Python y están sujetos a cambios en futuras versiones.

(el siguiente código muestra cómo cargar módulos locales y no locales y cómo pueden coexistir)

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

La mejor solución, si es posible, es evitar nombrar sus módulos con el mismo nombre que la biblioteca estándar o los nombres de los módulos integrados.

Boaz Yaniv
fuente
¿Cómo interactuará esto con sys.moduleslos intentos posteriores de cargar el módulo local?
Omnifarious
@Omnifarious: Agregará el módulo a sys.modules con su nombre, lo que evitará cargar el módulo local. Siempre puedes usar un nombre personalizado para evitar eso.
Boaz Yaniv
@Boaz Yaniv: Debería usar un nombre personalizado para el calendario local, no el estándar. Otros módulos de Python pueden intentar importar el estándar. Y si lo hace, lo que logra con esto es básicamente cambiar el nombre del módulo local sin tener que cambiar el nombre del archivo.
Omnifarious
@Omnifarious: Puedes hacerlo de cualquier manera. Algún otro código puede intentar cargar el módulo local y obtener el mismo error. Tendrá que hacer un compromiso y depende de usted decidir qué módulo admitir.
Boaz Yaniv
2
¡Gracias por eso, Booz! Aunque su fragmento es más corto (y documento), creo que es más fácil cambiar el nombre del módulo que tener un código pirata que pueda confundir a las personas (oa mí mismo) en el futuro.
ramita
15

La única forma de resolver este problema es secuestrar usted mismo la maquinaria de importación interna. Esto no es fácil y está plagado de peligros. Debe evitar la baliza en forma de grial a toda costa porque el peligro es demasiado peligroso.

En su lugar, cambie el nombre de su módulo.

Si desea aprender cómo secuestrar la maquinaria de importación interna, aquí es donde puede averiguar cómo hacerlo:

A veces hay buenas razones para correr este peligro. La razón que das no está entre ellos. Cambie el nombre de su módulo.

Si toma la ruta peligrosa, un problema que encontrará es que cuando carga un módulo, termina con un 'nombre oficial' para que Python pueda evitar tener que analizar el contenido de ese módulo nunca más. Se puede encontrar una asignación del 'nombre oficial' de un módulo al objeto del módulo en sí sys.modules.

Esto significa que si está import calendaren un lugar, cualquier módulo que se importe se considerará como el módulo con el nombre oficial calendary todos los demás intentos en import calendarcualquier otro lugar, incluso en otro código que forma parte de la biblioteca principal de Python, obtendrán ese calendario.

Podría ser posible diseñar un importador de clientes utilizando el módulo imputil en Python 2.x que provocó que los módulos cargados desde ciertas rutas buscaran los módulos que estaban importando en algo diferente asys.modules primero o algo así. Pero eso es algo extremadamente complicado y no funcionará en Python 3.x de todos modos.

Hay algo extremadamente feo y horrible que puede hacer que no implica enganchar el mecanismo de importación. Esto es algo que probablemente no debería hacer, pero probablemente funcionará. Convierte su calendarmódulo en un híbrido del módulo de calendario del sistema y su módulo de calendario. Gracias a Boaz Yaniv por el esqueleto de la función que utilizo . Pon esto al principio de tu calendar.pyarchivo:

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__])
De todo género
fuente
imputil se considera obsoleto. Deberías usar el módulo imp .
Boaz Yaniv
Que es perfectamente compatible con Python 3, por cierto. Y no tan peludo para usar en absoluto. Pero siempre debe tener en cuenta que el código que se basa en Python que trata las rutas de una manera o busca módulos en ese orden puede romperse tarde o temprano.
Boaz Yaniv
1
Correcto, pero en un caso tan aislado (colisión de nombre de módulo) enganchar el mecanismo de importación es una exageración. Y como es peludo e incompatible, es mejor dejarlo solo.
Boaz Yaniv
1
@jspacek no, hasta ahora todo bien, pero la colisión solo ocurriría cuando se usa el debbuger de PyDev, no en el uso regular. Y asegúrese de verificar el código más reciente (la URL en github), ya que cambió un poco con respecto a la respuesta anterior
MestreLion
1
@jspacek: Es un juego, no una biblioteca, por lo que en mi caso la compatibilidad con versiones anteriores no es una preocupación en absoluto. Y la colisión del espacio de nombres ocurre solo cuando se ejecuta a través de PyDev IDE (que usa el codemódulo std de Python ), lo que significa que solo una fracción de los desarrolladores podría tener algún problema con este "truco de fusión". Los usuarios no se verían afectados en absoluto.
MestreLion
1

Me gustaría ofrecer mi versión, que es una combinación de la solución de Boaz Yaniv y Omnifarious. Importará la versión del sistema de un módulo, con dos diferencias principales con respecto a las respuestas anteriores:

  • Admite la notación de 'punto', por ejemplo. package.module
  • Es un reemplazo directo de la declaración de importación en los módulos del sistema, lo que significa que solo tiene que reemplazar esa línea y si ya se están realizando llamadas al módulo, funcionarán como están

Ponga esto en algún lugar accesible para que pueda llamarlo (tengo el mío en mi archivo __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

Ejemplo

Quería importar mysql.connection, pero ya tenía un paquete local llamado mysql (las utilidades oficiales de mysql). Entonces, para obtener el conector del paquete mysql del sistema, reemplacé esto:

import mysql.connector

Con este:

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__])

Resultado

# 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
fuente
-2

Cambie la ruta de importación:

import sys
save_path = sys.path[:]
sys.path.remove('')
import calendar
sys.path = save_path
linuts
fuente
Esto no funcionará porque después de hacer esto no habrá forma de importar el módulo local sin ir y jugar con la maquinaria de importación usted mismo.
Omnifarious
@Omnifarious: ese es un problema diferente, que puede solucionar con un tercer módulo que hace una importación desde el calendario *.
linuts
No, esto probablemente no funcionaría porque Python almacena en caché el nombre del módulo sys.modulesy no volverá a importar un módulo con el mismo nombre.
Boaz Yaniv