4

I'm working to create a library in python that myself and a few colleagues will use. I'm struggling to conceptually understand how to best organize some code that feels like it doesn't cleanly fit into a class. None my ideas seem all that clean.

Right now I have several functions that all take the same input and they all do slightly different things with that input. Let's use these as examples:

(Approach 1)

from pathlib import Path

def find_files(directory: str):
    return Path(directory).glob('some_pattern')

def foo(directory: str):
    return

I don't like this implementation for two reasons:

  1. It doesn't validate the input. I'd like to at least check that directory is a directory that exists. And it appears that if I validate that parameter in every function---besides being a bad idea---breaks the SRP.
  2. I get neuron activation seeing the same call signature in each function and thinking classes "bundle data and functionality together".

I could make this a class:

(Approach 2)

class Foo:
   def __init__(self, directory: str):
       self.dir = Path(directory)
       # check that the input directory exists in a protected method

   def find_files(self):
       return self.dir.glob('some_pattern')

   def foo(self):
       return 

I can now can do all the checks I want in the constructor to validate the inputs! But I'm left feeling that this structure is just an awkward collection of functions. They don't manipulate the state of the object (perhaps it's just me, but I find find_files(dir) much cleaner than f = Foo(dir) and then f.find_files()) and I'd delete the object after one use.

I could also make Foo protected and tell the user to never directly interact with it, instead using functions.

(Approach 3)

def find_files(directory: str):
   f = _Foo(directory)
   return f.find_files()

I now get all the checks I want since the input is routed through the constructor and the user doesn't have to work with this awkward object. But now I have to write a function for every corresponding method in Foo---which doesn't seem efficient.

How should I structure this? Should I assume the user will provide good input (I think this as bad assumption, but I do see several large python libraries take this approach) and if not, which approach is better (or is it one I haven't thought of)?

1
  • "And it appears that if I validate that parameter in every function breaks the SRP." - I guess you were conflating the SRP with the DRY principle? Please clarify. And please tell us more about the validation - what exactly are you going to do when the specifified directory does not exist? Is it always the same action in every function?
    – Doc Brown
    Jan 24, 2022 at 6:47

2 Answers 2

2

At first glance, a group of functions that accept the same first parameter seem related. Do not jump to conclusions, however. Remember that a class is not just a group of functions. A class is an abstraction. You will need to analyse each of those free functions to determine if they are cohesive, or share a common purpose. If they do, then move them into a class that accepts a directory as a constructor argument. If the functions do "file system stuff" with no clear connection to one another, then just keep them as free functions.

Other languages, like Java and C#, force you to group functions into classes. Python imposes no such restriction. This gives you the freedom to group functions into a class when they form a complete abstraction, or leave them ungrouped if no clear abstraction can be identified.

But it is not enough to analyse the functions. You should also understand how they are used. A function that calls find_files(string) is dependent on that function. Any call to find_files will be difficult to mock during unit testing. Grouping these functions into a class that "does file system stuff" would allow you to isolate code that requires file system access in order to write unit tests. This might be enough reason to create a class. You can mock free functions in Python. I don't have much experience in Python, but from the searching I've done mocking an object is much easier than mocking a function.

1

You could go for an immutable class, perhaps a NamedTuple, or a frozen dataclass. This will also give you some useful methods for free (such as hashing / repr / eq).

As something simpler, if you just want to extend an object with a few extra methods, you have the option of subclassing Path. Generally I'd advise composition over inheritance, but if you keep your heirarchy one deep + avoid clashing with Path's internals, it's not a bad way to go.

Edit:

Actually, given that you only want to add one bit of functionality here, perhaps a decorator is the right way to go? Something like:

from functools import wraps


def check_path_exists(f):
    @wraps(f)
    def inner(directory, *args, **kwargs):
        # insert directory exists code here
        return f(directory, *args, **kwargs)
    return inner

And then you can use that wherever you like:

 @check_path_exists
 def find_files(directory: str):
    return Path(directory).glob('some_pattern')

 @check_path_exists
 def foo(directory: str):
    return

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.