Rounding out some of our work here with Python is to look extensions. If you’ve been following along with other posts you’ve probably already looked over some extensions in this post. If you’re brand new to this idea, check out that example first.
Rather than re-inventing the wheel and setting up a completely new example, let’s instead look at our previous example of making a logger and see how that would be different with extensions as compared to a module on demand.
A warning for those following along at home, we’re now knee deep in Python territory, so what’ we’ll find here is less specific to TouchDesigner and more of a look at using Classes in Python.
In our previous example looking at modules we wrote several functions that we left in a text DAT. We used the mod class in TouchDesigner to treat this text DAT as a python module. That’s a neat trick, and for some applications and situations this might be the right approach to take for a given problem. If we’re working on a stand alone component that we want to use and re-use with a minimal amount of additional effort, then extensions might be a great solution for us to consider.
Wait, what are extensions?! Extensions are way that we can extend a custom component that we make in TouchDesigner. This largely makes several scripting processes much simpler and allows us to treat a component more like an autonomous object rather than a complex set of dependent objects. If you find yourself re-reading that last statement let’s take a moment to see if we can find an analogy to help understand this abstract concept.
When we talked about for loops we used a simple analogy about washing dishes. In that example we didn’t bother to really think deeply about the process of washing dishes – we didn’t think about how much detergent, how much water, water temperature, washing method, et cetera. If we were to approach this problem more programatically we’d probably consider making a whole class to deal with the process of washing dishes:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class Dishes(): | |
def __init__( self ): | |
return | |
def Full_cycle( self, list_of_dishes ): | |
# what if we want to wash and then dry? | |
self.Wash( list_of_dishes, water_temp ) | |
self.Dry( list_of_dishes ) | |
return | |
def Wash( self, list_of_dishes, water_temp ): | |
# what does washing really entail | |
# all of that would go here | |
number_of_dishes = len( list_of_dishes ) | |
detergent_amt = self.Set_soap( number_of_dishes ) | |
water_temp = self.Set_water_temp( water_temp ) | |
return | |
def Dry( self, list_of_dishes ): | |
# what does washing really entail | |
# all of that would go here | |
number_of_dishes = len( list_of_dishes ) | |
dry_time = self.Set_dry( number_of_dishes ) | |
return | |
def Set_soap( self, number_of_dishes ): | |
return | |
def Set_water_temp( water_temp ): | |
if water_temp == 'hot': | |
temp = 98 | |
elif water_temp == 'medium': | |
temp = 80 | |
if water_temp == 'low': | |
temp = 60 | |
return temp | |
def Set_dry( self, number of dishes ): | |
return |
Okay, so the above seems awfully silly… what’s going on here? In our silly example we can see that rather than one long function for Wash() or Dry(), we instead use several smaller helper functions in the process. Why do this? Well, for one thing it lets you debug your code much more easily. It also means that by separating out some of these elements into different processes we can fix a single part of our pipeline without having to do a complete refactor of the entire Wash() or Dry() methods. Why does that matter? Well, what if we decide that there’s a better way to determine how much soap to use. Rather than having to sort through a single long complex method for Wash() we can instead just look at Set_soap(). It makes unit testing easier, and allows us to replace or develop that method outside the context of the larger method. It also means that if we’re collaborating with other programmers we can divide up the process of writing methods.
What’s this self. business? self. allows us to call a method from inside another method in the same class. This is where extensions begin to really shine as opposed to using modules on demand. We an also use things like attributes andinheritance.
Okay, so how can we think about actually using this?
Let’s take a look at what this approach looks like in python:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import datetime | |
log_file = op( 'text_log_file' ) | |
log_path = "example_extensions/log_files/log.txt" | |
full_text = '''{now} | |
Current Year | {year} | |
Current Month | {month} | |
Current Day | {day} | |
Current Hour | {hour} | |
Current Minute | {minute} | |
Current Second | {second} | |
Current Microsecond | {microsecond} | |
''' | |
raw_date_time = "On {month}-{day}-{year} at {hour}:{minute}:{second}" | |
verbose_log_message = '''============================ | |
VERBOSE MESSAGE | |
{date_time} | |
—————————- | |
operator || {operator} | |
At Network Location || {path} | |
—————————- | |
Logged | |
{message} | |
============================ | |
''' | |
log_message = '''—————————- | |
{now} | |
—————————- | |
{operator} | |
{path} | |
{message} | |
''' | |
class Ext_example(): | |
def __init__( self ): | |
'''The init function. | |
We're not doing anything with our init function in this example | |
so we'll leave this empty. | |
Notes | |
————— | |
''' | |
return | |
def Log_date( self ): | |
year = datetime.datetime.now().year | |
month = datetime.datetime.now().month | |
day = datetime.datetime.now().day | |
hour = datetime.datetime.now().hour | |
minute = datetime.datetime.now().minute | |
second = datetime.datetime.now().second | |
updated_log_date = log_date.format( | |
month = month, | |
day = day, | |
year = year, | |
hours = hour, | |
minutes = minute, | |
seconds = second | |
) | |
return updated_log_date | |
def Log_date_time( self ): | |
'''Create a formatted time stamp | |
A look at how we might create a formatted time stamp to use with | |
various logging applications. | |
Arguments | |
————— | |
None | |
Returns | |
————— | |
formatted_text( str ) – a string time stamp | |
Notes | |
————— | |
''' | |
now = datetime.datetime.now() | |
year = datetime.datetime.now().year | |
month = datetime.datetime.now().month | |
day = datetime.datetime.now().day | |
hour = datetime.datetime.now().hour | |
minute = datetime.datetime.now().minute | |
second = datetime.datetime.now().second | |
microsecond = datetime.datetime.now().microsecond | |
date_time = raw_date_time.format( | |
month = month, | |
day = day, | |
year = year, | |
hour = hour, | |
minute = minute, | |
second = second | |
) | |
return date_time | |
def Log_message( self, operator, message, verbose=False, text_port_print=True, append_log=True ): | |
'''Logging Method. | |
A simple look at how you might start to think about building a logger for a TouchDesigner | |
application. A logger is a great way to build out files with time stamped events. The | |
more complex a project becomes, the more important it can become to have some means | |
of logging the operations of your program. Here's a simple look at what that might | |
look like. | |
Arguments | |
————— | |
operator( touch object ) – the touch object whose path you'd like incldued in the log message | |
message( str ) – a message to include in the log | |
verbose( bool ) – a toggle for verbose or compact messages | |
text_port_print( bool ) – a toggle to print to the text port, or not | |
append_log( bool ) – a toggle to append to the log file , or not | |
Example | |
————— | |
target_op = op( 'constant1' ) | |
message = "This operator needs attention" | |
parent().Log_message( target_op, message ) | |
also | |
parent().Log_message( target_op, message, verbose = True ) | |
Returns | |
————— | |
None | |
Notes | |
————— | |
You'll notice that some arguments receive default values. This is so you don't have | |
to include them in the call. This means that by default the message will be compact, | |
will print to the text port, and will append the log file. | |
''' | |
path = operator.path | |
op_name = operator.name | |
# logic tests for verbose or compact | |
if verbose: | |
message = verbose_log_message.format( | |
date_time = self.Log_date_time(), | |
operator = op_name, | |
path = path, | |
message = message | |
) | |
else: | |
message = log_message.format( | |
now = self.Log_date_time(), | |
operator = op_name, | |
path = path, | |
message = message | |
) | |
# logic tests for text_port_print | |
if text_port_print: | |
print( message ) | |
else: | |
pass | |
# log tests for appending log | |
if append_log: | |
log_file.write( '\n' + message ) | |
else: | |
pass | |
# save the log file to disk – external from the TouchDesigner project | |
self.Save_log() | |
return | |
def Save_log( self ): | |
'''Saves log to disk. | |
This helper function saves the log file to disk. | |
Notes | |
————— | |
None | |
''' | |
op( log_file ).save( log_path ) | |
return | |
def Clear_log( self ): | |
'''Clears Log File. | |
This helper function clears the text dat used to hold the log file. | |
Notes | |
————— | |
None | |
''' | |
op( log_file ).clear() | |
return |
So getting started we set up a couple of variables that we were going to reuse several times. Next we declared our class, and then nested our methods inside of that class. You’ll notice that different from methods, we need to include the argument self in our method definitions:
# syntax for just a function
def Clear_log():
return
# syntax for just a method in a class
def Clear_log( self ):
return
We can also see how we’ve got several helper functions here to get us up and running – ways to add to the log, save the log to an external file, a way to clear the log. We can imagine in the future we might want to add another method that both clears the log, and saves an empty file, like a reset. Since we’ve already broken those functions into their own methods we could simply add this method like this:
def Reset_log():
self.Clear_log()
self.Save_log()
return
Now it’s time to experiment.
Over the course of this series we’ve looked at lots of fundamental pieces of working with Python in TouchDesigner. Now it’s your turn to start playing and experimenting.
Happy programming!
1 comment
Comments are closed.