Functions are a critical tool in computer programming which allow you to package sections of code making the logic much more reuseable throughout our code
In this guide we will learn how to wrap sections of code in custom functions so it is easily reusable throughout our code. So far, we have seen how we can use variables in Python to store different kinds of data and how we can use the 'flow control' structures conditionals and loops to change the way in which lines of code get executed. With these tools we can already start to express some fairly complex logics in our scripts.
However, any sufficiently complex script would start to get very long, since every time we wanted to do a certain process we would have to rewrite all of its code within our script (probably by copying the code and pasting it wherever it was needed in our script). Even worse, the code would become extremely hard to manage since any change to the logic would have to be manually updated everywhere it was implemented. This process would not only be time consuming but would introduce potential bugs from human error.
This is where functions come in. Functions allow us to "wrap" sections of code representing a specific functionality while exposing a set of inputs that control that functionality and a set of outputs that the function produces. Once a function is defined we can then "call" that function anywhere in our code, passing in the required inputs as data and receiving the outputs as a result. This is similar to how components work in Grasshopper, but implemented with code.
Defining a function makes the logic within the function's code easily reuseable anywhere it's needed in the rest of our program. It also allows for much easier management of changes since the change only needs to be done once (in the function's definition) and will be run whenever the function is called.
You may not have realized, but we've already seen and used some functions such as type()
, str()
, and range()
which are included with Python. But what are functions really?
As in math, a function is a basic structure that can accept inputs, perform some processing on those inputs, and give back a result. Let's create a basic function in Python that will add two to a given number and give us back the result. You can write this code directly in the script editor of a Python
component in Grasshopper or any other Python environment:
def add_function(input_number):
result = input_number + 2
return result
A function's definition begins with the keyword def
. After this is the function's name, which follows the same naming conventions as variables. The function's name is always followed immediately by a set of parenthesis where you can place any number of input variables separated by commas (,
). When the function is called these inputs will be passed to the function and become available within the body of the function as variables. If a function does not expect any inputs you still include the parenthesis, however in this case there won't be anything inside of them, for example: my_function_name()
.
On its own, this code will only define what the function does, but will not actually run any code. To execute the code inside the function you have to call it somewhere within the script and pass it the proper inputs:
print(add_function(2))
Here we call the function by writing its name and passing in '2' as the input. The result of the function (the number '4') will then be passed into the print()
function which will print '4' to the output window.
When you call a function, you can either directly pass values or pass variables that have values stored inside of them. For example, this code will call the function in the same way:
var = 2
print(add_function(var))
Here the value of the var
variable, which in this case is 2, is being passed to the add_function
function, and is then available within that function through the input_number
variable. Notice that the names of the two variables var
and input_number
don't have to match. When a value gets passed to a function it is reassigned to the variable name declared in the function definition, and the value is then available through that name.
In this case we refer to var
as a global variable since it stores the value '2' in the main script, while input_number
is a local variable since it stores that value only within the body of that function. In this way, functions 'encapsulate' specific tasks along with all the data that is necessary to execute that task to limit the number of global variables necessary within the main script. In general, reducing the number of global variables in your script by minimizing repetitive code and implementing functions is good practice since it makes the code easier to manage and debug.
The first line declaring the function and its inputs ends with a colon (:
), which should be familiar by now. The the rest of the function body is defined below with each line in the body starting inset one tab from the definition line. Optionally, if you want to return a value from the function back to the main script, you can end the function with the keyword return
, followed by the value or variable you want to return. Once the function gets to a return
statement, it will skip over the rest of the body and return the associated value. This can be used to create more complex behavior within the function using one or more return
statements:
def add_function(input_number):
if input_number < 0:
return 'Number must be positive!'
result = input_number + 2
return result
print(add_function(-2))
print(add_function(2))
In this case, if the input is less than zero the conditional will be met, which causes the first return
statement to run, skipping the rest of the code in the function. However, if the number is equal to or greater than zero, the conditional will be skipped causing the rest of the function to run, ending with the second return
statement.
You can pass any number of inputs into a function, but the number of inputs must always match between what is defined in the function and what is passed into it when the function is called. For example, we can expand our simple addition function to accept two numbers to be added:
def add_two_numbers(input_number_1, input_number_2):
result = input_number_1 + input_number_2
return result
print(add_two_numbers(2, 3))
You can also return multiple values by separating them with a comma (,
) after the return statement. In this case, you also need to provide the same number of variables to which to assign the results of executing the function. Let's expand our function to return both the addition and multiplication of two numbers:
def two_numbers(input_number_1, input_number_2):
addition = input_number_1 + input_number_2
multiplication = input_number_1 * input_number_2
return addition, multiplication
val_1, val_2 = twoNumbers(2, 3)
print('addition: ' + str(val_1))
print('multiplication: ' + str(val_2))
Functions are extremely useful for creating efficient, manageable, and legible code. Wrapping up sections of code into custom functions allow you (and possibly others) to reuse your logic in a very efficient way, and also forces you to be explicit about the set of operations involved in accomplishing a certain task in your code, as well as the data needed to execute it.
You can see that the basic definition of functions is quite simple, however you can quickly start to define more advanced logics, where functions call each other and pass around inputs and returns in highly complex ways. You can even pass a function as an input into another function and create functions which call themselves. These are called recursive functions which we will look at in a later guide.
The following exercise will give you hands on experience with writing and using functions by applying them to the attractor point example developed in the previous exercise. This time, we will extend the definition to use a custom function to wrap part of our logic for generating circles, which will allow us to extend our definition to create more circles at each grid point by reusing the function. If you don't have the file from the last exercise handy you can download it here:
Let's get some practice writing functions by creating a custom function in our script that wraps the logic we already developed for calculating the radius of each circle based on the distance of its center point to an attractor point. Double-click on the Python
component to open up the script and replace the code with the following:
import Rhino.Geometry as rh
def calc_radius(dist):
if dist < 2:
radius = 0.25
else:
radius = 0.5
return radius
circles = []
points = []
for x in range(int(num_points)):
for y in range(int(num_points)):
point = rh.Point3d(x, y, 0)
points.append(point)
dist = point.DistanceTo(attractor)
circle = rh.Circle(point, calc_radius(dist))
circles.append(circle)
In this code we defined a new function called calc_radius()
using the def
keyword. We also specified that the function requires one input which we name dist
. This will be the distance to the attractor point calculated for each circle center. Then within the body of the function we place the conditional that specifies the radius based on the distance, which is the returned from the function using the return
keyword. Once the function is defined, we can use this function to calculate the radius which we can pass directly to the Circle()
constructor.
So now we know how to create a custom function, but the more important question is why would we want to do this? As is, the function does not add anything to our code except for some extra lines. However, even when a function does not introduce new functionality, wrapping logic that accomplishes a specific tasks is good practice because it makes your code more modular, rational, and easier to interpret by other programmers. It also allows you to 'encapsulate' variables that pertain only to the inner workings of the function as 'local' variables. This reduces the number of global variables you have to keep track of in the main script, thereby reducing the chance of bugs.
The main advantage of using functions, however, is that it allows you to easily reuse repeating logic within your code without having to write or maintain repetitive code. For example, now that we've defined a function for calculating the radius we can easily add a couple more lines to our script to create an additional circle at each grid point without repeating the conditional code. To vary the size of the circle we will add an additional input to the function which is a factor we multiply the calculated radius by to scale the size of each circle:
import Rhino.Geometry as rh
def calc_radius(dist, factor=1):
if dist < 2:
radius = 0.25
else:
radius = 0.5
return radius * factor
circles = []
points = []
for x in range(int(num_points)):
for y in range(int(num_points)):
point = rh.Point3d(x, y, 0)
points.append(point)
dist = point.DistanceTo(attractor)
circle = rh.Circle(point, calc_radius(dist))
circles.append(circle)
circle = rh.Circle(point, calc_radius(dist, 1.5))
circles.append(circle)
Notice that in the function definition we added a default value for the new factor
input by setting it with the =
operator. This makes the input optional, which means that the value can be provided when calling the function, or if it is not provided the default value will be used. Implementing new inputs with default values can be useful since it allows us to keep our existing call to the function the same. However, if you do provide values for optional inputs, the inputs must still come in the same order. This means that optional inputs are always placed at the end of the list of inputs, and any inputs listed before a specified optional input must be specified as well. For more information on working with optional inputs in Python functions you can go through this useful guide.
Now that we have the additional input parameter specified, we can add an additional Circle()
constructor to create a new circle at the same point, this time scaled 1.5 times. Remember to add the new circle to the circles
list so that it is passed from the Python script and visualized in the Rhino document.
If you wanted to create many circles at each location, copying and pasting the constructor code may not be the most elegant or maintainable option. To control the way the circles are created without repeating code we can create another loop to iterate over a set of factor values and use them to create a set of circles. Let's replace our existing code with:
import Rhino.Geometry as rh
def calc_radius(dist, factor=1):
if dist < 2:
radius = 0.25
else:
radius = 0.5
return radius * factor
circles = []
points = []
for x in range(int(num_points)):
for y in range(int(num_points)):
point = rh.Point3d(x, y, 0)
points.append(point)
dist = point.DistanceTo(attractor)
for factor in range(1, 10):
circle = rh.Circle(point, calc_radius(dist, factor * 0.2))
circles.append(circle)
Here we've added a third for
loop to iterate over a set of values which we use to control the factor scaling each circle. Notice we've used an additional input for the range()
function to pass in a starting value for our range so we get back the 9 values [1, ..., 9]. Since the range() function only supports integer inputs we further scale the factor within our function call by multiplying it by 0.2. You can experiment with different ranges and scaling, along with modifying the conditional inside the calc_radius()
function to create different patterns in the circle grid.
This concludes our extension of the attractor point tutorial using custom functions. In case you got lost along the way, you can download a finished version of the demo here:
+CHALLENGE 7: Two attractors
Can you modify the definition to work with two attractor points instead of one?
HINT: Start by creating an additional point in Rhino and referencing it into the Grasshopper definition. Then input the new point into the
Python
component and use it's distance to each point along with that of the first point to calculate the final radius of each Circle. You can try a variety of ways to combine the effect of both attractors, for example adding the two distances together, or taking the minimum of the two distances using Python's built-inmin()
function.
In this guide we've seen how functions can be used to wrap sections of code that define specific functionality to make it much easier to reuse throughout our code. In the next section we will use functions to represent a particularly useful computational logic called recursion, which will allow us to model complex structures which can be controlled through a relatively small set of parameters.