Mocking by Adding Attributes to Functions
(Source/Credits: https://dev.to/rpalo/mocking-by-adding-attributes-to-functions-5ae0)
Python functions are first-class objects, which means we can stick whatever data we want on them!
title: Mocking by Adding Attributes to Functions published: true description: Python functions are first-class objects, which means we can stick whatever data we want on them! tags: - python canonical_url: https://www.assertnotmagic.com/2019/08/26/adding-properties-to-functions/
I was reading through a blog post on how to quickly mock the print function to test a function’s output to the command line and I came across this technique.
```python def fake_print(*args, sep=" ", end="\n"): fake_print.output.append(sep.join(args) + end)
fake_print.output = []
mymodule.print = fake_print
mymodule.run_function()
expected = """Hello there. How are you? I'm great, thanks! Banana. """
assert expected == "\n".join(fake_print.output) + "\n" ```
So, first-off, I know that there are better and more robust ways to do this. I’ve got a post on my list of drafts to do about that. And I also know that, in general, if you’re having to check STDOUT in order to ensure your function is working properly, there’s a reasonable chance that you could structure your code better and make it easier to test. In this case, a class assignment was having me perform some operations and I was going to be graded on whether or not my STDOUT output exactly matched the expected or not, so I wanted to exactly test what I was getting graded on, just in case I made a stupid printing error.
Anyways, I’m getting off track. The point of this post is this:
I know that functions are objects. You hear all the time that they’re “first-class” objects (which means that they get better snacks and their seats recline all the way). I’ve passed them as arguments, assigned them to variables, defined them inside other functions, and return them.
I never thought about the fact that, because they are objects, you can stick arbitrary properties to them whenever you want!
Now. A reasonable next question is: is this a good idea? How useful is this really? My answer: probably not all that much.
Realistically, all you are doing here is sneakily hiding what might be a global variable inside the properties of this function. Are you adding some nice self-documentation of what this variable does by doing this? Maybe. Are you cleaning up the global namespace? Sure. But for the most part, it smells a little bit of “clever in a bad way.”
Like I said, I’ve been programming Python for three or four years and mentoring for at least one, and I’ve never seen it. It’s never occurred to me. Am I the end-all Python knowledge guru arbiter of correctness? No. (OK, sometimes I tell my cat I am.) But, it seems like a good indicator that this isn’t exactly commonplace. And I don’t think that the small, neat namespacing benefits and little giggle I get from being clever are worth the mental overhead of anyone else who has to read my code.
Nonetheless, with all of that said, it’s still a neat trick and highlights some things that I love about Python in general. Here’s another example use-case to show it off: caching.
```python def fibonacci(n): if n in fibonacci.cache: return fibonacci.cache[n]
result = fibonacci(n - 1) + fibonacci(n - 2)
fibonacci.cache[n] = result
return result
fibonacci.cache = {} ```
This saves a bunch of function calls by saving the results of calculations in the cache
dict.
Side note: this is really just a lame version of functools.lru_cache
. Look it up. It’s great!
Anyways, I just wanted to share this neat, quirky way of handling something in Python. Thanks for reading!
Comments section
mark_nicol
•May 1, 2024
Clever - I've not seen it done like this myself, but worth storing away as a possible tool for the future. Thanks for sharing Ryan, you find such fun stuff.
rpalo Author
•May 1, 2024
Glad you liked it!
wrldwzrd89
•May 1, 2024
I'm sure you could get really creative by applying this concept at the metaclass level, to substitute mocks for the real thing without changing the actual function calls... but that's a topic for another post! Love the concept though.