The Demeter Law
Python import can be a little tricky. The law of Demeter guideline and his applications to python import can help you mastering them and also writes more user-friendly, testable and reliable code.
According to wikipedia, the law of Demeter (or principle of least knowledge) is a software developing guideline that can be summarized this way:
- Each unit should have only limited knowledge about other units: only units "closely" related to the current unit.
- Each unit should only talk to its friends; don't talk to strangers.
- Only talk to your immediate friends.
If we want to apply these to python import (the python module would be the unit), it would become something like:
- Each module should only import public object of submodule, but only from the closest submodule. (aka "immediate friends")
The method
In order to accomplish that, the key is to declare explicitly in each module (though the _init_.py) which object are exposed. Then these objects are accessible to the parent module folder, which can also re-expose them if necessary.
The benefices of this method are:
Your _init_.py files become some kind of "documentation" of which objects should be accessed or not from other modules.
You fully control your import system, which is a key to loose-coupling.
The final user (other developper using your module) experience is enhanced, import are short and clear.
A downside is that it is more verbose.
First example
Assuming this setup:
project
│ __main__.py
└───demeter
│ │ __init__.py
│ └───module1
│ │ __init__.py
│ │ code1.py
# demeter/module1/code1.py
class MyClass1:
pass
With that setup, we can access directly MyClass1 class from _main_.py:
# __main__.py
from demeter.module1.code1 import MyClass1
class1 = MyClass1()
This works, however this is a demeter's law violation: _main_.py import MyClass1 from demeter/module1/code1.py which is not an "immediate friend".
A good smell to detect it are long import. When applying the law of demeter, import are usually pretty short.
A cleaner way to do that would be...
- Explicitly expose MyClass1 from module1 to upper level module:
# demeter/module1/__init__.py
from demeter.module1 import code1 as code1
MyClass1 = code1.MyClass1
- Re-expose it to the upper level:
# demeter/__init__.py
from demeter import module1
MyClass1 = module1.MyClass1
- Use it
# __main__.py
from demeter import module1
MyClass1 = module1.MyClass1
That way:
- _main_.py only have knowledge of the demeter module and the demeter module only have knowledge about the module1 module. The principe of least knowledge is respected
- modules (demeter and module1) explicitly expose what can be used from them(explicit is better than implicit, https://www.python.org/dev/peps/pep-0020/).
Second example
Assuming this extension of the previous setup:
project
│ __main__.py
└───demeter
│ │ __init__.py
│ └───module1
│ │ __init__.py
│ │ code1.py
| └───module2
│ │ __init__.py
│ │ code2.py
# demeter/module1/module2/code2.py
class MyClass2:
pass
To use MyClass2 in _main_.py we would...
Expose it to module1:
# demeter/module1/module2/__init__.py
from demeter.module1.module2 import code2 as code2
MyClass2 = code2.MyClass2
Re-expose it but this time from module1:
# demeter/module1/__init__.py
from demeter.module1 import code1 as code1
from demeter.module1 import module2 as module2
MyClass1 = code1.MyClass1
MyClass2 = module2.MyClass2
and...
# demeter/__init__.py
from demeter import module1
MyClass1 = module1.MyClass1
MyClass2 = module1.MyClass2
Finally, use it from your _main_.py:
# __main__.py
from demeter import module1
MyClass1 = module1.MyClass1
MyClass2 = module1.MyClass2
Whole module example
The law of demeter can also be applied to whole module.
The previous example could be something like:
# demeter/module1/code1.py
class MyClass1:
pass
# demeter/module1/module2/code2.py
class MyClass2:
pass
# demeter/module1/module2/__init__.py
from demeter.module1.module2 import code2 as code2
# demeter/module1/__init__.py
from demeter.module1 import code1 as code1
from demeter.module1 import module2 as module2
MyClass1 = code1.MyClass1
# demeter/__init__.py
from demeter import module1
MyClass1 = module1.MyClass1
# demeter/__main__.py
import demeter
module1 = demeter.module1
MyClass1 = module1.MyClass1
MyClass2 = module1.module2.code2.MyClass2
This second method looks very similar to the bad practice example, isn't it?
However, the key points are that every desired module and object are explicitly exposed.
So you still benefice the full control of your module system and you respect the principle of least knowledge.
VSCode, Pylance, Pyrights
If you use VSCode and Pylance (which use Pyrights under the hood), this method will avoid you to have some reportUnknownMemberType errors
Conclusion
I hope you learned something reading this article (my first one uWu ^^).
It's up to you to choose if you prefer expose full module or only object. I guess it depends on the context, the project, the people etc....
Even if it look a little "too much" and require more code and more rigorous , these methods really enhance control and quality on the long run as well as deeply understanding (and thinking about ;) ) responsibility of every part of your code.
Top comments (0)