Python: circular imports needed for type checking
Asked Answered
F

4

11

First of all: I do know that there are already many questions and answers to the topic of the circular imports.

The answer is more or less: "Design your Module/Class structure properly and you will not need circular imports". That is true. I tried very hard to make a proper design for my current project, I in my opinion I was successful with this.

But my specific problem is the following: I need a type check in a module that is already imported by the module containing the class to check against. But this throws an import error.

Like so:

foo.py:

from bar import Bar

class Foo(object):

    def __init__(self):
        self.__bar = Bar(self)

bar.py:

from foo import Foo

class Bar(object):

    def __init__(self, arg_instance_of_foo):
        if not isinstance(arg_instance_of_foo, Foo):
            raise TypeError()

Solution 1: If I modified it to check the type by a string comparison, it will work. But I dont really like this solution (string comparsion is rather expensive for a simple type check, and could get a problem when it comes to refactoring).

bar_modified.py:

from foo import Foo

class Bar(object):

    def __init__(self, arg_instance_of_foo):
        if not arg_instance_of_foo.__class__.__name__ == "Foo":
            raise TypeError()

Solution 2: I could also pack the two classes into one module. But my project has lots of different classes like the "Bar" example, and I want to seperate them into different module files.

After my own 2 solutions are no option for me: Has anyone a nicer solution for this problem?

Fideicommissary answered 17/3, 2010 at 11:50 Comment(1)
See #39741132 for the correct answer.Appalachia
L
6

The best solution is to not check types.

The other solution is to not create an instance of, and not reference at all, Foo or Bar until both classes are loaded. If the first module is loaded first, don't create a Bar or refer to Bar until after the class Foo statement is executed. Similarly, if the second module is loaded first, don't create a Foo or reference Foo until after the class Bar statement is executed.

This is basically the source of the ImportError, which could be avoided if you did "import foo" and "import bar" instead, and used foo.Foo where you now use Foo, and bar.Bar where you now use Bar. In doing this, you no longer refer to either of them until a Foo or Bar is created, which hopefully won't happen until after both are created (or else you'll get an AttributeError).

Lifeboat answered 17/3, 2010 at 11:59 Comment(0)
C
7

You can program against interface (ABC - abstract base class in python), and not specific type Bar. This is classical way to resolve package/module inter-dependencies in many languages. Conceptually it should also result in better object model design.

In your case you would define interface IBar in some other module (or even in module that contains Foo class - depends on the usage of that abc). You code then looks like this:

foo.py:

from bar import Bar, IFoo

class Foo(IFoo):
    def __init__(self):
        self.__bar = Bar(self)

# todo: remove this, just sample code
f = Foo()
b = Bar(f)
print f
print b
x = Bar('do not fail me please') # this fails

bar.py:

from abc import ABCMeta
class IFoo:
    __metaclass__ = ABCMeta

class Bar(object):
    def __init__(self, arg_instance_of_foo):
        if not isinstance(arg_instance_of_foo, IFoo):
            raise TypeError()
Czardas answered 17/3, 2010 at 12:10 Comment(1)
>> :) fair enough. In Java and other strong typed languages such issues appear quite often and have some "standard" solutions. This is "not a problem" in Python where duck typing is just "the way". Nonetheless, I assumed that once the question has been asked, there is a "requirement" to ensure the type.Czardas
L
6

The best solution is to not check types.

The other solution is to not create an instance of, and not reference at all, Foo or Bar until both classes are loaded. If the first module is loaded first, don't create a Bar or refer to Bar until after the class Foo statement is executed. Similarly, if the second module is loaded first, don't create a Foo or reference Foo until after the class Bar statement is executed.

This is basically the source of the ImportError, which could be avoided if you did "import foo" and "import bar" instead, and used foo.Foo where you now use Foo, and bar.Bar where you now use Bar. In doing this, you no longer refer to either of them until a Foo or Bar is created, which hopefully won't happen until after both are created (or else you'll get an AttributeError).

Lifeboat answered 17/3, 2010 at 11:59 Comment(0)
K
2

You could just defer the import in bar.py like this:

class Bar(object):

    def __init__(self, arg_instance_of_foo):
        from foo import Foo
        if not isinstance(arg_instance_of_foo, Foo):
            raise TypeError()
Krieg answered 17/3, 2010 at 11:59 Comment(4)
All imports should go at the top, unless there's a really good reason to not do it. This isn't it.Lifeboat
@Devin: Your answer is better, but this is a standard way of handling circular imports so the answer is still worthwhile.Vinegarish
@Devin Jeanpierre: What else could I do?Fideicommissary
@FogleBird: It's no standard (or idiom, etc.) that I've heard of.Lifeboat
B
1

Possible duplicate: Python type hinting without cyclic imports

You should use Forward Reference (PEP 484 - Type Hints):

When a type hint contains names that have not been defined yet, that definition may be expressed as a string literal, to be resolved later.

So instead of:

class Tree:
    def __init__(self, left: Tree, right: Tree):
        self.left = left
        self.right = right

do:

class Tree:
    def __init__(self, left: 'Tree', right: 'Tree'):
        self.left = left
        self.right = right
Berwick answered 13/7, 2019 at 7:11 Comment(1)
type hinting != type checkingMisname

© 2022 - 2024 — McMap. All rights reserved.