Picture by Writer
Â
All of us write capabilities when coding in Python. However can we essentially write good capabilities? Nicely, let’s discover out.
Capabilities in Python allow you to write modular code. When you’ve gotten a process that you must carry out at a number of locations, you’ll be able to wrap the logic of the duty right into a Python perform. And you’ll name the perform each time that you must carry out that particular process. So simple as it appears to get began with Python capabilities, writing maintainable and performant capabilities just isn’t so easy.
And that’s why we’ll discover a number of practices that’ll show you how to write cleaner and easy-to-maintain Python capabilities. Let’s get began…
Â
1. Write Capabilities That Do Solely One Factor
Â
When writing capabilities in Python, it is usually tempting to place all associated duties right into a single perform. Whereas this can assist you code issues up shortly, it’ll solely make your code a ache to take care of within the close to future. Not solely will this make understanding what a perform does tougher but in addition results in different points similar to too many parameters (extra on that later!).
As a great observe, you need to all the time attempt to make your perform do just one factor—one process—and try this nicely. However generally, for a single process, you could have to work by a sequence of subtasks. So how do you resolve if and the way the perform ought to be refactored?
Relying on what the perform is making an attempt to do and the way complicated the duty is, you’ll be able to work out the separation of issues between subtasks. After which determine an appropriate degree at which you’ll be able to refactor the perform into a number of capabilities—every specializing in a particular subtask.
Â
Refactor capabilities | Picture by Writer
Â
Right here’s an instance. Take a look at the perform analyze_and_report_sales
:
# fn. to research gross sales information, calculate gross sales metrics, and write it to a file
def analyze_and_report_sales(information, report_filename):
total_sales = sum(merchandise['price'] * merchandise['quantity'] for merchandise in information)
average_sales = total_sales / len(information)
with open(report_filename, 'w') as report_file:
report_file.write(f"Total Sales: {total_sales}n")
report_file.write(f"Average Sales: {average_sales}n")
return total_sales, average_sales
Â
It is fairly simple to see that it may be refactored into two capabilities: one calculating the gross sales metrics and one other on writing the gross sales metrics to a file like so:
# refactored into two funcs: one to calculate metrics and one other to put in writing gross sales report
def calculate_sales_metrics(information):
total_sales = sum(merchandise['price'] * merchandise['quantity'] for merchandise in information)
average_sales = total_sales / len(information)
return total_sales, average_sales
def write_sales_report(report_filename, total_sales, average_sales):
with open(report_filename, 'w') as report_file:
report_file.write(f"Total Sales: {total_sales}n")
report_file.write(f"Average Sales: {average_sales}n")
Â
Now it’s simpler to debug any issues with the calculation of gross sales metrics and file operations individually. And right here’s a pattern perform name:
information = [{'price': 100, 'quantity': 2}, {'price': 200, 'quantity': 1}]
total_sales, average_sales = calculate_sales_metrics(information)
write_sales_report('sales_report.txt', total_sales, average_sales)
Â
You must be capable to see the ‘sales_report.txt’ file in your working listing with the gross sales metrics. It is a easy instance to get began, however that is useful particularly once you’re engaged on extra complicated capabilities.
Â
2. Add Sort Hints to Enhance Maintainability
Â
Python is a dynamically typed language. So you do not want to declare sorts for the variables you create. However you’ll be able to add kind hints to specify the anticipated information kind for variables. While you outline the perform, you’ll be able to add the anticipated information sorts for the parameters and the return values.
As a result of Python doesn’t implement sorts at runtime, including kind hints has no impact at runtime. However there nonetheless are advantages to utilizing kind hints, particularly on the maintainability entrance:
- Including kind hints to Python capabilities serves as inline documentation and offers a greater thought of what the perform does and what values it consumes and returns.
- While you add kind hints to your capabilities, you’ll be able to configure your IDE to leverage these kind hints. So that you’ll get useful warnings if you happen to attempt to go an argument of invalid kind in a number of perform calls, implement capabilities whose return values don’t match the anticipated kind, and the like. So you’ll be able to decrease errors upfront.
- You’ll be able to optionally use static kind checkers like mypy to catch errors earlier reasonably than letting kind mismatches introduce refined bugs which can be tough to debug.
Right here’s a perform that processes order particulars:
# fn. to course of orders
def process_orders(orders):
total_quantity = sum(order['quantity'] for order in orders)
total_value = sum(order['quantity'] * order['price'] for order in orders)
return {
'total_quantity': total_quantity,
'total_value': total_value
}
Â
Now let’s add kind hints to the perform like so:
# modified with kind hints
from typing import Record, Dict
def process_orders(orders: Record[Dict[str, float | int]]) -> Dict[str, float | int]:
total_quantity = sum(order['quantity'] for order in orders)
total_value = sum(order['quantity'] * order['price'] for order in orders)
return {
'total_quantity': total_quantity,
'total_value': total_value
}
Â
With the modified model, you get to know that the perform takes in an inventory of dictionaries. The keys of the dictionary ought to all be strings and the values can both be integers or floating level values. The perform additionally returns a dictionary. Let’s take a pattern perform name:
# Pattern information
orders = [
{'price': 100.0, 'quantity': 2},
{'price': 50.0, 'quantity': 5},
{'price': 150.0, 'quantity': 1}
]
# Pattern perform name
outcome = process_orders(orders)
print(outcome)
Â
Here is the output:
{'total_quantity': 8, 'total_value': 600.0}
Â
On this instance, kind hints assist us get a greater thought of how the perform works. Going ahead, we’ll add kind hints for all the higher variations of Python capabilities we write.
Â
3. Settle for Solely the Arguments You Really Want
Â
If you’re a newbie or have simply began your first dev function, it’s essential to consider the completely different parameters when defining the perform signature. It is fairly widespread to introduce further parameters within the perform signature that the perform by no means truly processes.
Guaranteeing that the perform takes in solely the arguments which can be truly crucial retains perform calls cleaner and extra maintainable basically. On a associated notice, too many parameters within the perform signature additionally make it a ache to take care of. So how do you go about defining easy-to-maintain capabilities with the precise variety of parameters?
If you end up writing a perform signature with a rising variety of parameters, step one is to take away all unused parameters from the signature. If there are too many parameters even after this step, return to tip #1: break down the duty into a number of subtasks and refactor the perform into a number of smaller capabilities. It will assist preserve the variety of parameters in examine.
Â
Preserve num_params in examine | Picture by Writer
Â
It’s time for a easy instance. Right here the perform definition to calculate scholar grades comprises the teacher
parameter that’s by no means used:
# takes in an arg that is by no means used!
def process_student_grades(student_id, grades, course_name, teacher'):
average_grade = sum(grades) / len(grades)
return f"Student {student_id} achieved an average grade of {average_grade:.2f} in {course_name}."
Â
You’ll be able to rewrite the perform with out the teacher
parameter like so:
# higher model!
def process_student_grades(student_id: int, grades: record, course_name: str) -> str:
average_grade = sum(grades) / len(grades)
return f"Student {student_id} achieved an average grade of {average_grade:.2f} in {course_name}."
# Utilization
student_id = 12345
grades = [85, 90, 75, 88, 92]
course_name = "Mathematics"
outcome = process_student_grades(student_id, grades, course_name)
print(outcome)
Â
Here is the output of the perform name:
Scholar 12345 achieved a mean grade of 86.00 in Arithmetic.
Â
Â
4. Implement Key phrase-Solely Arguments to Reduce Errors
Â
In observe, most Python capabilities absorb a number of arguments. You’ll be able to go in arguments to Python capabilities as positional arguments, key phrase arguments, or a mixture of each. Learn Python Operate Arguments: A Definitive Information for a fast overview of perform arguments.
Some arguments are naturally positional. However generally having perform calls containing solely positional arguments could be complicated. That is very true when the perform takes in a number of arguments of the identical information kind, some required and a few non-compulsory.
If you happen to recall, with positional arguments, the arguments are handed to the parameters within the perform signature within the identical order wherein they seem within the perform name. So change so as of arguments can introduce refined bugs kind errors.
It’s usually useful to make non-compulsory arguments keyword-only. This additionally makes including non-compulsory parameters a lot simpler—with out breaking current calls.
Right here’s an instance. The process_payment
perform takes in an non-compulsory description
string:
# instance fn. for processing transaction
def process_payment(transaction_id: int, quantity: float, forex: str, description: str = None):
print(f"Processing transaction {transaction_id}...")
print(f"Amount: {amount} {currency}")
if description:
print(f"Description: {description}")
Â
Say you need to make the non-compulsory description
a keyword-only argument. Right here’s how you are able to do it:
# implement keyword-only arguments to attenuate errors
# make the non-compulsory `description` arg keyword-only
def process_payment(transaction_id: int, quantity: float, forex: str, *, description: str = None):
print(f"Processing transaction {transaction_id}:")
print(f"Amount: {amount} {currency}")
if description:
print(f"Description: {description}")
Â
Let’s take a pattern perform name:
process_payment(1234, 100.0, 'USD', description='Fee for providers')
Â
This outputs:
Processing transaction 1234...
Quantity: 100.0 USD
Description: Fee for providers
Â
Now strive passing in all arguments as positional:
# throws error as we attempt to go in additional positional args than allowed!
process_payment(5678, 150.0, 'EUR', 'Bill fee')
Â
You’ll get an error as proven:
Traceback (most up-to-date name final):
File "/home/balapriya/better-fns/tip4.py", line 9, in
process_payment(1234, 150.0, 'EUR', 'Bill fee')
TypeError: process_payment() takes 3 positional arguments however 4 got
Â
5. Don’t Return Lists From Capabilities; Use Turbines As a substitute
Â
It is fairly widespread to put in writing Python capabilities that generate sequences similar to an inventory of values. However as a lot as attainable, you need to keep away from returning lists from Python capabilities. As a substitute you’ll be able to rewrite them as generator capabilities. Turbines use lazy analysis; so that they yield components of the sequence on demand reasonably than computing all of the values forward of time. Learn Getting Began with Python Turbines for an introduction to how mills work in Python.
For instance, take the next perform that generates the Fibonacci sequence as much as a sure higher restrict:
# returns an inventory of Fibonacci numbers
def generate_fibonacci_numbers_list(restrict):
fibonacci_numbers = [0, 1]
whereas fibonacci_numbers[-1] + fibonacci_numbers[-2]
Â
It’s a recursive implementation that’s computationally costly and populating the record and returning it appears extra verbose than crucial. Right here’s an improved model of the perform that makes use of mills:
# use mills as a substitute
from typing import Generator
def generate_fibonacci_numbers(restrict: int) -> Generator[int, None, None]:
a, b = 0, 1
whereas a
Â
On this case, the perform returns a generator object which you’ll be able to then loop by to get the weather of the sequence:
restrict = 100
fibonacci_numbers_generator = generate_fibonacci_numbers(restrict)
for num in fibonacci_numbers_generator:
print(num)
Â
Right here’s the output:
0
1
1
2
3
5
8
13
21
34
55
89
Â
As you’ll be able to see, utilizing mills could be far more environment friendly particularly for big enter sizes. Additionally, you’ll be able to chain a number of mills collectively, so you’ll be able to create environment friendly information processing pipelines with mills.
Â
Wrapping Up
Â
And that’s a wrap. You will discover all of the code on GitHub. Right here’s a overview of the completely different suggestions we went over:
- Write capabilities that do just one factor
- Add kind hints to enhance maintainability
- Settle for solely the arguments you really want
- Implement keyword-only arguments to attenuate errors
- Do not return lists from capabilities; use mills as a substitute
I hope you discovered them useful! If you happen to aren’t already, check out these practices when writing Python capabilities. Comfortable coding!
Â
Â
Bala Priya C is a developer and technical author from India. She likes working on the intersection of math, programming, information science, and content material creation. Her areas of curiosity and experience embrace DevOps, information science, and pure language processing. She enjoys studying, writing, coding, and low! Presently, she’s engaged on studying and sharing her information with the developer neighborhood by authoring tutorials, how-to guides, opinion items, and extra. Bala additionally creates partaking useful resource overviews and coding tutorials.