DEV Community

Konstantin
Konstantin

Posted on

Solving the "Chicken-Egg" problem with Python

Hello!

Nothing could be more tasty to Python than chicken and eggs. So today our Python decided to resolve an old phylosophical question: what was the first, chicken or egg. In order to do it, it has decided to create two classes and then to perform some modelling to make an ultimate answer to this question.

Ok, let's start. Our Python is a grand-pa of all pythons, and it's serial number is 2.7. To begin with, it created 3 files:

# filename: models/egg.py

import models.chicken


class Egg(object):
    def __init__(self, name):
        super(Egg, self).__init__()
        self.name = name

    def wait(self):
        return models.chicken.Chicken("Chicken from [{self.name}]".format(self=self))
# filename: models/chicken.py

import models.egg


class Chicken(object):
    def __init__(self, name):
        super(Chicken, self).__init__()
        self.name = name

    def create_egg(self):
        return models.egg.Egg("Egg of [{self.name}]".format(self=self))
# filename: main.py

import models.chicken


if __name__ == '__main__':
    # Decision maker!
    c = models.chicken.Chicken("Ryaba")
    print(c.create_egg().wait().create_egg().name)

Grand-pa Python is happy with the results:

$ python2.7 main.py 
Egg of [Chicken from [Egg of [Ryaba]]]

For those of you who are sceptical about the circular dependencies in Python - yes, they are supported, but only to some extent. Generally, when the interpreter sees import models.egg it checks if this module is imported and if it is, uses the address of this module from the cache. If it is not, it immediately creates a record in the cache and then starts doing actual import. That's why if you only have import <module_name> statements in your code you are safe.

As soon as we decide to use from <module_name> import <object> statement, our circular dependencies will not be able to be resolved. Let's try it!

# filename: models/egg.py

from models.chicken import Chicken


class Egg(object):
    def __init__(self, name):
        super(Egg, self).__init__()
        self.name = name

    def wait(self):
        return Chicken("Chicken from [{self.name}]".format(self=self))
# filename: models/chicken.py

from models.egg import Egg


class Chicken(object):
    def __init__(self, name):
        super(Chicken, self).__init__()
        self.name = name

    def create_egg(self):
        return Egg("Egg of [{self.name}]".format(self=self))
$ python2.7 main.py 
Traceback (most recent call last):
  File "main.py", line 1, in <module>
    import models.chicken
  File "/data/models/chicken.py", line 1, in <module>
    from models.egg import Egg
  File "/data/models/egg.py", line 1, in <module>
    from models.chicken import Chicken
ImportError: cannot import name Chicken

$ python3.7 main.py 
Traceback (most recent call last):
  File "main.py", line 1, in <module>
    import models.chicken
  File "/data/models/chicken.py", line 1, in <module>
    from models.egg import Egg
  File "/data/models/egg.py", line 1, in <module>
    from models.chicken import Chicken
ImportError: cannot import name 'Chicken' from 'models.chicken' (/data/models/chicken.py)

This is more or less predictable and it is a real chicken-egg problem. In order to import Chicken class to the egg module you need to parse chicken module (so, just an address of a module is not enough), and in order to completely parse chicken module, you need to complete parsing of an egg module. So, no way.

OK. And now I need your full attention: we are going to replace import <package>.<module> by from <package> import <module>.

# filename: models/egg.py

from models import chicken


class Egg(object):
    def __init__(self, name):
        super(Egg, self).__init__()
        self.name = name

    def wait(self):
        return chicken.Chicken("Chicken from [{self.name}]".format(self=self))
# filename: models/chicken.py

from models import egg


class Chicken(object):
    def __init__(self, name):
        super(Chicken, self).__init__()
        self.name = name

    def create_egg(self):
        return egg.Egg("Egg of [{self.name}]".format(self=self))

And the result is a bit unexpectable:

$ python2.7 main.py 
Traceback (most recent call last):
  File "main.py", line 1, in <module>
    import models.chicken
  File "/data/models/chicken.py", line 1, in <module>
    from models import egg
  File "/data/models/egg.py", line 1, in <module>
    from models import chicken
ImportError: cannot import name chicken

$ python3.7 main.py 
Egg of [Chicken from [Egg of [Ryaba]]]

So, Python3 changed the way how packages are imported, generally they are appended to the sys.modules earlier than Python2. Good to know.

And the last surprise for today. Now we are using import <package>.<module> as <alias> syntax.

# filename: models/egg.py

import models.chicken as chicken


class Egg(object):
    def __init__(self, name):
        super(Egg, self).__init__()
        self.name = name

    def wait(self):
        return chicken.Chicken("Chicken from [{self.name}]".format(self=self))
# filename: models/chicken.py

import models.egg as egg


class Chicken(object):
    def __init__(self, name):
        super(Chicken, self).__init__()
        self.name = name

    def create_egg(self):
        return egg.Egg("Egg of [{self.name}]".format(self=self))

And can you predict the output?

$ python2.7 main.py 
Traceback (most recent call last):
  File "main.py", line 1, in <module>
    import models.chicken
  File "/data/models/chicken.py", line 1, in <module>
    import models.egg as egg
  File "/data/models/egg.py", line 1, in <module>
    import models.chicken as chicken
AttributeError: 'module' object has no attribute 'chicken'

$ python3.6 main.py 
Traceback (most recent call last):
  File "main.py", line 1, in <module>
    import models.chicken
  File "/data/models/chicken.py", line 1, in <module>
    import models.egg as egg
  File "/data/models/egg.py", line 1, in <module>
    import models.chicken as chicken
AttributeError: module 'models' has no attribute 'chicken'

$ python3.7 main.py 
Egg of [Chicken from [Egg of [Ryaba]]]

Python3.7 won :) Ok. What is really happened here is that there was a bug in the import A.B.C as D syntax which was fixed in the version 3.7.

Thanks for reading!

Top comments (0)