1

I'm working in Python 2.7.

This is my folder structure. (temp is already added to this system path.)

temp
|
|--main.py
|
|--sub
   |
   |--__init__.py
   |
   |--sub2
      |
      |--__init__.py
      |
      |--square.py

The file contents are listed below.

main.py:

import sub.sub2 as sub2
sub2.run()
sub/__init__.py: empty
sub/sub2/__init__.py:

import sub.sub2.square as square
def run():
    square.square_it(3)
sub/sub2/square.py:

def square_it(x): return x**2

When I execute main.py, I receive the following error (ignore the line numbers):

Traceback (most recent call last):
  File "main.py", line 3, in <module>
    import sub.sub2 as sub2
  File "/home/gimlisonofgloin1/temp/sub/sub2/__init__.py", line 3, in <module>
    import sub.sub2.square as square
AttributeError: 'module' object has no attribute 'sub2'

I can fix this by changing the statement in which the error occurs to any of these statements (in the final three listed solutions, I have to change the function call appropriately, of course):

  • from sub.sub2 import square as square;
  • from sub.sub2.square import square_it;
  • from .square import square_it (as pointed out kindly in user NeErAj KuMaR's answer); or
  • import sub.sub2.square.

My question is: why does the original line of code yield an error, even though it is semantically equivalent to the working ("fixed") lines of code (in particular, the 1st and 4th solutions listed)?

In attempting to answer this question, I have stumbled across this bit of text from the Python 2.0 Reference Manual:

To avoid confusion, you cannot import sub-modules 'as' a different local name. So 'import module as m' is legal, but 'import module.submod as s' is not. The latter should be written as 'from module import submod as s', see below.

This is consistent with the errors I'm receiving. However, this (seemingly important) little blurb is not present anywhere in the Python 2.7 Reference Manual. Is that little blurb from the Python 2.0 reference still applicable in Python 2.7? Or am I getting this error for a completely different reason that I'm not aware of?

A.M.
  • 44
  • 7

2 Answers2

2

I think you stumbled into a little known Python "gotcha" that seems to be fixed in Python 3.7 Alpha 1 thanks to Serhiy Storchaka. Check this question: http://stackoverflow.com/questions/41845671/import-as-in-python-3.

This was discussed in Python Ideas, but import foo.bar as eggs is not the same as from foo import bar as eggs. They generate inconsistent bytecodes.

The root cause is the import cycle. I played around with dis and found the following (I suspect others have already found this but the thread was hard to follow for me initially):

>>> dis.dis('import a.b')
1         0 LOAD_CONST               0 (0)
          2 LOAD_CONST               1 (None)
          4 IMPORT_NAME              0 (a.b)
          6 STORE_NAME               1 (a)
          8 LOAD_CONST               1 (None)
         10 RETURN_VALUE
>>>

compared to

>>> dis.dis('import a.b as c')
1         0 LOAD_CONST               0 (0)
          2 LOAD_CONST               1 (None)
          4 IMPORT_NAME              0 (a.b)
          6 LOAD_ATTR                1 (b)      <-- error here
          8 STORE_NAME               2 (c)
         10 LOAD_CONST               1 (None)
         12 RETURN_VALUE
>>>

What this shows is that the implementation of "import a.b" and "import a.b as c" are different. The former calls import('a.b', ...) which returns the module 'a' and stores that in the variable 'a'. In the OP's case, because of the import cycle, while sys.modules['a.b'] exists, module 'a' does not yet have the attribute 'b'. That's the reason that in the latter example, the LOAD_ATTR opcode fails.

>>> dis("import sys.path as path")
1         0 LOAD_CONST               0 (0)
          3 LOAD_CONST               1 (None)
          6 IMPORT_NAME              0 (sys.path)
          9 LOAD_ATTR                1 (path)
         12 STORE_NAME               1 (path)
         15 LOAD_CONST               1 (None)
         18 RETURN_VALUE

For "import sys.path as path", the given module name is "sys.path", and the "from list" entry on the stack is None. This fails before it even reaches the LOAD_ATTR line, since "sys.path" isn't an importable module. Changing LOAD_ATTR to IMPORT_FROM would thus have no effect on its behaviour.

>>> dis("from sys import path")
1         0 LOAD_CONST               0 (0)
          3 LOAD_CONST               1 (('path',))
          6 IMPORT_NAME              0 (sys)
          9 IMPORT_FROM              1 (path)
         12 STORE_NAME               1 (path)
         15 POP_TOP
         16 LOAD_CONST               2 (None)
         19 RETURN_VALUE

For "from sys import path", the given module name is "sys", and the "from list" entry on the stack is a tuple containing the string "path". This works, since "sys" is importable and it has a "path" attribute.

I guess you have to wait for 3.7 or change your import.

Paulo Scardine
  • 73,447
  • 11
  • 124
  • 153
0

Replace code of sub2 __init__.py code as below

sub/sub2/__init__.py:

from .square import square_it
def run():
    square_it(3)
Paulo Scardine
  • 73,447
  • 11
  • 124
  • 153
Neeraj Kumar
  • 3,851
  • 2
  • 19
  • 41
  • I am aware that this (an explicit relative import) can be done, although I have not listed it. My question, however, is _why_ the particular statement `import sub.sub2.square as square` causes an error. I will list this as another successful alternative in my question. – A.M. Oct 05 '17 at 20:06
  • Check out this pull request: https://bugs.python.org/issue30024 – Paulo Scardine Oct 05 '17 at 20:12
  • @PauloScardine , thanks! That explained everything. The comments on the pull request link to a StackOverflow question/answer which is, I think, nearly-identical to mine. Should I answer my own slight variation on the question and link to that answer, or just mark my question as a duplicate? – A.M. Oct 05 '17 at 20:39
  • Lets mark this as a dupe then... – Paulo Scardine Oct 05 '17 at 20:56