0

I'm coming back to python and having trouble with import paths. Can someone explain the logic, or a better way of doing this? If I have a class called Sentence in structure like

base.py
models/Sentence.py

then inside base.py

import models.Sentence

why do I still have to do

s = models.Sentence.Sentence('arg')

I can't seem to fix it on import either

import models.Sentence as Sentence # ok but no help
import models.Sentence.Sentence as Sentence  # illegal and weird

I realize there's some magic I can do within the __init__ module file, but that seems like a magic hack.

It seems very odd to have to refer to the whole filepath each time I create a class instance so I'd like to know what I'm doing wrong. JS' explicit export and imports seem much clearer.

dcsan
  • 11,333
  • 15
  • 77
  • 118

2 Answers2

0

When you call import models.Sentence, you're telling Python to load your Sentence.py file, and put all of its variables inside the models.Sentence namespace. Thus, your Sentence class also becomes listed under models.Sentence, and so it becomes models.Sentence.Sentence.

In terms of using __init__.py, you can put something like this in that file:

from .Sentence import * # or import Sentence if that's all you want

and then in your base.py:

import models

my_sentence = models.Sentence("arg")

What your __init__.py does now is load everything from Sentence.py into its own namespace, instead of models.Sentence. So that means your Sentence class is no longer models.Sentence.Sentence, and is now just models.Sentence.

NB: If you want to be able to run your __init__.py file as the main file, or run any script inside the models folder, you should put this code in __init__.py instead:

import __main__, os

if os.path.dirname(__main__.__file__) == os.path.dirname(__file__):
    # executed script in this folder
    from Sentence import *
else:
    # executed script from elsewhere
    from .Sentence import *

EDIT:

To understand why you need to use models.Sentence.Sentence('arg'), let's look at a clearer example:

Suppose I have a module I've made called package. In that module, I have sub-module this_useful_part.py.

This is the folder structure:

main.py
package
    this_useful_part.py

In this_useful_part.py:

class my_very_helpful_class:
    pass

In main.py:

import package.this_useful_part

my_instance = package.this_useful_part.my_very_helpful_class()

(I realise I may not have chosen the best names)

The reason I require package.this_useful_part.my_very_helpful_class, and you require models.Sentence.Sentence is because the class is not the file, but stored within the file, and the file is not the folder, but stored within the folder.

The ways around this:

  • You can put the class definition inside __init__.py, so when you import models, it can be referenced as models.Sentence.

  • You do as I did above, where you import everything from Sentence.py, so that the class definition is also stored directly inside the __init__.py file, and so it can be referenced as models.Sentence.

I hope this clears things up.

EDIT 2:

After a bit more research, I found that it is possible to make a module callable. (From here)

You don't require an __init__.py file if you change the code in your Sentence.py file to this:

import sys

class Sentence:
    def __init__(self, arg):
        self.arg = arg

sys.modules[__name__] = Sentence

You can now do

import models.Sentence

my_sentence = models.Sentence('arg')

Sorry for my mistake.

Ed Ward
  • 2,333
  • 2
  • 10
  • 16
  • thanks but `import models` would surely import ALL models any time one model is used? Doesn't seem on point. Also resorting to overriding exports in an `__init__ ` file just feels like a hack. Is there really no way to refer to a class by it's classname without this stuff? Also `.Sentence` filename with a path in it ;;( – dcsan Dec 31 '19 at 16:27
  • What would you like to be able to do? Basically, variables have to be organised into namespaces, so the only ways of calling `models.Sentence('arg')` is to do one of the things above to create a reference to the `Sentence` class inside `__init__.py`. – Ed Ward Dec 31 '19 at 16:48
  • usually I keep one class per file, so this just adds a lot of redundancy to imports. I want to import the class, not the full path to that. So your example: `my_instance = package.useful_part.helpful_class()` would actually be `inst = package.class.class(opts)` ie duplicative. But from your addendum we can somehow patch `sys.modules` to add a (duplicate) short circuit reference. Two wrongs don't make a right... – dcsan Jan 01 '20 at 18:31
  • what the `sys.modules[__name__]` line does is reassigns the reference to the module to your class. Therefore, when you try to call the module (which is not normally allowed), you are instead calling your class... – Ed Ward Jan 01 '20 at 20:27
0

I think that the python documentation explains it quite well and that I can not do a better job, so I'll just quote and provide you the links.


Python documentation on packages (This is just the portion relevant to this question, but I strongly encourage you to open the link and read it from there.):

Users of the package can import individual modules from the package, for example:

import sound.effects.echo

This loads the submodule sound.effects.echo. It must be referenced with its full name.

sound.effects.echo.echofilter(input, output, delay=0.7, atten=4)

An alternative way of importing the submodule is:

from sound.effects import echo

This also loads the submodule echo, and makes it available without its package prefix, so it can be used as follows:

echo.echofilter(input, output, delay=0.7, atten=4)

Yet another variation is to import the desired function or variable directly:

from sound.effects.echo import echofilter

Again, this loads the submodule echo, but this makes its function echofilter() directly available:

echofilter(input, output, delay=0.7, atten=4)

Documentation about the import statement (Again, I encourage you to open the link and read from there.):

The basic import statement (no from clause) is executed in two steps:

  1. find a module, loading and initializing it if necessary
  2. define a name or names in the local namespace for the scope where the import statement occurs.

[...]

If the requested module is retrieved successfully, it will be made available in the local namespace in one of three ways:

  • If the module name is followed by as, then the name following as is bound directly to the imported module.
  • If no other name is specified, and the module being imported is a top level module, the module’s name is bound in the local namespace as a reference to the imported module
  • If the module being imported is not a top level module, then the name of the top level package that contains the module is bound in the local namespace as a reference to the top level package. The imported module must be accessed using its full qualified name rather than directly

The from form uses a slightly more complex process:

  1. find the module specified in the from clause, loading and initializing it if necessary;
  2. for each of the identifiers specified in the import clauses:

    1. check if the imported module has an attribute by that name
    2. if not, attempt to import a submodule with that name and then check the imported module again for that attribute
    3. if the attribute is not found, ImportError is raised.
    4. otherwise, a reference to that value is stored in the local namespace, using the name in the as clause if it is present, otherwise using the attribute name

Examples:

import foo                 # foo imported and bound locally
import foo.bar.baz         # foo.bar.baz imported, foo bound locally
import foo.bar.baz as fbb  # foo.bar.baz imported and bound as fbb
from foo.bar import baz    # foo.bar.baz imported and bound as baz
from foo import attr       # foo imported and foo.attr bound as attr

And finally, for more details you can read the import system.

daniel
  • 1
  • 2
  • Welcome to SO! Yes, I read the docs but that is what lead me to the current mess I have. I assumed it was my error but it just seems the python way is just ... a bit uncomfortable. I guess you just learn that's the way it is and get used to it. – dcsan Jan 01 '20 at 18:35