Add partial functions to your tool belt
Under The Hood 6 - The 10% of times where you use it will look great!
Critical thinking is a skill that comes with seniority - in any area, really.
A senior pizza maker has enough critical thinking not to add pineapple topping to a pizza. (shots fired, pineapple pizza mentioned)
In software engineering, critical thinking shows itself when we choose simplicity and add fluff only when necessary.
All of this to say: there's a neat technique called partial functions, one that your Python standard package has installed under the functools
module.
Let's say you have a base function with X, Y, and Z parameters. In 30% of your code, you use this function with a set A of Y and Z parameters. In 50% of your code base, X and Z are always a B set of arguments. Finally, in the last 20%, you have a super flexible way of signing the function.
However, since it's the same function, the same behavior, and the same signature, we'd fall in DRY territory if we were to create three separate functions for each of the use cases.
Meet partial functions
The point of partial functions is to “freeze” a certain number of parameters while leaving the others free to be changed.
You can achieve this in Python by doing this:
from functools import partial
# your base function
def multiply_and_add(x, y, z):
return x * y + z
# our partial function for our usecases
partial_func = partial(multiply_and_add, 2, z=3)
When to consider using it
Custom API Calls
Sometimes, you call an API with a base set of arguments, but some of them change, like the headers or any of the params.
You can retain some of the parameters for use cases A, B, and C:
import requests
from functools import partial
#base request
def api_call(url, headers, params):
response = requests.get(url, headers=headers, params=params)
return response.json()
#use case A - secured calls
safe_headers = {'Authorization': 'Bearer your_token_here'}
secured_call = partial(api_call, headers=safe_headers)
#use case B - different objects
clients_params = {'objectType': 'clients'}
clients_call = partial(api_call, params=clients_path)
use case C - backfill
backfill_params = {'fillMethod': 'backfill'}
backfill_call = partial(api_call, params=backfill_params)
Database connections
You can declare partialmethods
as well, and this comes in handy when dealing with database connections:
from functools import partialmethod
import psycopg2
class DatabaseManager:
def __init__(self, host, database, user, password):
self.connection = psycopg2.connect(host=host, database=database, user=user, password=password)
#base method
def execute_query(self, query, params=None):
with self.connection.cursor() as cursor:
cursor.execute(query, params or ())
if query.lower().startswith('select'):
return cursor.fetchall()
else:
self.connection.commit()
#partial methods for use case
select_users = partialmethod(execute_query, query="SELECT * FROM users")
delete_user = partialmethod(execute_query, query="DELETE FROM users WHERE id = %s")
Factory functions
Another cool use case is for factory functions. They have the sole purpose of building objects. You may have a base behavior and different use cases throughout your code base.
One example we can mention is a SQL String factory:
from functools import partial
#base function
def create_sql_query(table, condition, value):
return f"SELECT * FROM {table} WHERE {condition} = '{value}';"
#use case A - by username
get_users_by_username = partial(create_sql_query, table='users', condition='username')
#use case B - by category
get_products_by_category = partial(create_sql_query, table='products', condition='category')
Adapting to third-party libraries
You may have the requirement of using an existing 3rd party library with a different signature than the one it's implemented with.
A good way of doing it is using partial functions, where you can have standard and flexible functions depending on what the original function expects and what behavior you want to achieve.
import boto3
from functools import partial
sqs_client = boto3.client('sqs')
def send_sqs_message(client, queue_url, message_body, attributes):
return client.send_message(
QueueUrl=queue_url,
MessageBody=message_body,
MessageAttributes=attributes
)
#use case A - send to DEV
send_to_dev_queue = partial(
send_sqs_message,
client=sqs_client,
queue_url='https://sqs.us-east-1.amazonaws.com/123456789012/dev-queue',
attributes={'AttributeType': {'StringValue': 'dev', 'DataType': 'String'}}
)
#use case B - send to PROD
send_to_prod_queue = partial(
send_sqs_message,
client=sqs_client,
queue_url='https://sqs.us-east-1.amazonaws.com/123456789012/prod-queue',
attributes={'AttributeType': {'StringValue': 'prod', 'DataType': 'String'}}
)
End thoughts
As I said in the intro, this shouldn't be used all the time. But there are good use cases that make your code easier to use and maintain, increasing your overall development experience.
Hope you liked it and learned something new!