I'm currently working on a Python project where I need to extend the functionality of the pathlib.Path class. My goal is to utilize all the existing methods provided by pathlib.Path while adding my own custom methods that follow the model of path.py. These custom methods are aimed at enhancing file path manipulations, such as expanding environment variables and more advanced path operations not directly supported by pathlib.
I'm contemplating between two approaches to achieve this: Inheritance and Composition.
Here's a brief overview of what I'm considering:
Inheritance Approach: Creating a subclass of
pathlib.Pathand directly adding my custom methods to this subclass. This seems straightforward and allows direct access to the parent class's methods. However, I'm concerned about the implications of extending an immutable class and the correct usage of__new__for initialization.Composition Approach: Creating a new class that contains an instance of
pathlib.Pathas an attribute and delegates method calls to it. Custom methods would be added to this wrapper class. While this approach provides flexibility and keeps the originalPathclass untouched, I'm unsure about the efficiency and elegance of delegating calls for everypathlibmethod.
I'm looking for insights and recommendations from the community on which approach would be more appropriate for extending pathlib.Path with custom functionality. Specifically, I'm interested in:
- Which approach is generally considered more pythonic and maintainable?
- Are there any pitfalls or limitations I should be aware of when choosing between inheritance and composition in this context?
If anyone has experience with extending pathlib.Path, could you share your thoughts on best practices or examples?
What I tried and works in a notebook:
Inheritance approach:
from pathlib import Path import os class PPath(Path): _flavour = Path()._flavour def __new__(cls, *args, **kwargs): # Création d'une instance de MyFile qui est également une instance de pathlib.Path self = super().__new__(cls, *args, **kwargs) return self def expand(self): """ Expand environment variables and user tilde in the path, then resolve it to an absolute path. """ expanded_path = os.path.expandvars(str(self)) expanded_path = os.path.expanduser(expanded_path) resolved_path = type(self)(expanded_path).resolve() return resolved_path # Test my_file = PPath('/home/ludivine/Coding/dummy.pkl') print(my_file.exists()) print(my_file.is_file()) print(my_file.name) print(my_file.expand())Composition approach:
from pathlib import Path as BasePath import os class EmsPath: def __init__(self, *args, **kwargs): self._path = BasePath(*args, **kwargs) def __getattr__(self, name): print(f"Délégation de {name}") return getattr(self._path, name) def expand(self): """ Expand environment variables and user tilde in the path, then resolve it to an absolute path. """ expanded_path = os.path.expandvars(str(self)) expanded_path = os.path.expanduser(expanded_path) resolved_path = type(self)(expanded_path).resolve() return resolved_path def __str__(self): return str(self._path) # Test my_file = EmsPath('/home/ludivine/Coding/dummy.pkl') print(my_file.exists()) print(my_file.is_file()) print(my_file.name) print(my_file.expand())
I think neither approach will be very viable in practice (when you'll have libraries that are unaware of
PPaths orEmsPaths) – instead, I'd forget about OOP and go functional with a free helper:(Of course, with a function of that signature, you can monkey-patch it onto the
pathlib.Pathclass, but I'd advise against a sneakypathlib.Path.expand = expand...)