Extending pathlib.Path in Python: Inheritance vs. Composition for Custom Path Operation

67 views Asked by At

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.Path and 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.Path as 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 original Path class untouched, I'm unsure about the efficiency and elegance of delegating calls for every pathlib method.

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())
    
1

There are 1 answers

4
AKX On

I think neither approach will be very viable in practice (when you'll have libraries that are unaware of PPaths or EmsPaths) – instead, I'd forget about OOP and go functional with a free helper:

# path_helpers.py
import os
import pathlib


def expand(p: pathlib.Path) -> pathlib.Path:
    """
    Expand environment variables and user tilde in the path, then resolve it to an absolute path.
    """
    expanded_path = os.path.expandvars(str(p))
    expanded_path = os.path.expanduser(expanded_path)
    return type(p)(expanded_path).resolve()

(Of course, with a function of that signature, you can monkey-patch it onto the pathlib.Path class, but I'd advise against a sneaky pathlib.Path.expand = expand...)