From 0687b917b7485c3cffa351ba5060a8da6738f3f3 Mon Sep 17 00:00:00 2001 From: Steve Oney Date: Mon, 31 Jul 2023 23:43:55 -0400 Subject: [PATCH 1/4] Add content on decorators for functions and classes --- .../WPFunctionWrappingAndDecorators.rst | 117 ++++++++++++++++++ _sources/AdvancedFunctions/toctree.rst | 1 + _sources/Classes/WPClassDecorators.rst | 73 +++++++++++ _sources/Classes/toctree.rst | 1 + 4 files changed, 192 insertions(+) create mode 100644 _sources/AdvancedFunctions/WPFunctionWrappingAndDecorators.rst create mode 100644 _sources/Classes/WPClassDecorators.rst diff --git a/_sources/AdvancedFunctions/WPFunctionWrappingAndDecorators.rst b/_sources/AdvancedFunctions/WPFunctionWrappingAndDecorators.rst new file mode 100644 index 00000000..68f956e4 --- /dev/null +++ b/_sources/AdvancedFunctions/WPFunctionWrappingAndDecorators.rst @@ -0,0 +1,117 @@ + +.. _decorators: + +👩‍💻 Function Wrapping and Decorators +========================================= + +In Python, functions are "first class" objects, meaning they can be treated like any other object. Beyond calling functions, we can also reference them, pass them as arguments to other functions, or return them. Although we cannot directly manipulate the *behavior* of a function, we can wrap it in another function that does something before or after the original function is called or change the arguments that a function takes. This is called *function wrapping*. + +We could write a function ``compose`` that accepts two functions as arguments and returns a new function that calls the first function with the output of the second function:: + + def compose(func1, func2): # func1 and func2 are functions + return lambda x: func1(func2(x)) # return a *new* function that calls func1 with the output of func2 + +For example, below, we had a function ``subtract_32`` that accepts a number as an argument and returns ``32`` subtracted from that number, and a function ``multiply_5_9`` that accepts a number as an argument and returns the product of that number and ``5/9``. We can create a new function that "composes" these two functions by executing ``subtract_32`` first and then passing its output to ``multiply_5_9`` (which happens to be how we can convert Fahrenheit temperatures to Celsius temperatures): + +.. activecode:: ac15_6_1 + + def compose(func1, func2): # func1 and func2 are functions + # return a *new* function that calls func1 with the output of func2 + return lambda x: func1(func2(x)) + + def subtract_32(x): + return x - 32 + + def multiply_5_9(x): + return x * 5/9 + + f_to_c = compose(multiply_5_9, subtract_32) + + print(f_to_c(32)) # 0.0 + print(f_to_c(212)) # 100.0 + +Function wrapping is a powerful idea that can be applied to many problems but it can be difficult to grasp at first. For example, suppose we wanted to write a function that adds logging to another function. That is, we want to write a function ``addLogging`` that accepts a function as an argument and returns a new function that calls the original function but prints something before and after the function is called. In the code below, ``addLogging`` is analogous to the ``compose`` function except: 1. it accepts one argument (rather than two) 2. it is defined using ``def`` (rather than ``lambda``) 3. it calls ``print()`` before and after the function is called (rather than calling one function with the output of the other). + +.. activecode:: ac15_6_2 + + def addLogging(func): # The argument, func is a function + + def wrapper(x): # x is the argument that we're going to pass to func + print(f"About to call the function with argument {x}") + result = func(x) # actually call our function and store the result + print(f"Done with the function with argument {x}. Result: {result}") + return result # return whatever our function returned + + return wrapper # return our new function + + def double(x): + print("Inside double") + return x * 2 + + logged_double = addLogging(double) + + double_3 = logged_double(3) + print(f"logged_double(3) returned {double_3}") + + print("-"*20) + + logged_add_one = addLogging(lambda x: x + 1) + ten_plus_1 = logged_add_one(10) + print(f"logged_add_one(10) returned {ten_plus_1}") + +This kind of function wrapping is common enough that Python provides a special syntax for it called **decorators**. A decorator is a function that accepts a function as an argument and returns a new function. The new function is usually a "wrapped" version of the original function. The decorator syntax is to place an ``@`` symbol followed by the name of the decorator function on the line before the function definition. Now, we can wrap our ``double`` function with the ``addLogging`` decorator by placing ``@addLogging`` on the line before the function definition. This is equivalent to calling ``addLogging`` with ``double`` as an argument and assigning the result to ``double``: + +.. activecode:: ac15_6_3 + + def addLogging(func): # The argument, func is a function + + def wrapper(x): # x is the argument that we're going to pass to func + print(f"About to call the function with argument {x}") + result = func(x) # actually call our function and store the result + print(f"Done with the function with argument {x}. Result: {result}") + return result # return whatever our function returned + + return wrapper # return our new function + + @addLogging # equivalent to double = addLogging(double) + def double(x): + print("Inside double") + return x * 2 + + double(10) + +We can now easily "enable" or "disable" logging by commenting out the ``@addLogging`` line. This is much easier than having to change the code inside the ``double`` function itself. + +To give another example, suppose we wanted to "password protect" access to calling a function. We could create a function ``passwordProtect`` that will wrap our function inside of code that ensures the user has the correct password. + +.. activecode:: ac15_6_4 + # This is a decorator function that takes another function as an argument. + def passwordProtect(func): + + # This inner function is the one that will actually be called + # when we use the decorator on another function. + def wrappedFunc(): + password = input('Enter the password to call the function:') + + if password == 'password123': # correct password? then call the original function + func() + else: # If the password is not correct, deny access + print("Access denied. Sorry, you need to enter the correct password to get the secret message.") + + return wrappedFunc + + + @passwordProtect + def printSecretMessage(): + secretMessage = "Shhh...this is a secret message" + + # We print a series of "~" characters the same length as the message, + # then the message itself, then another series of "~" characters. + print("~" * len(secretMessage)) + print(secretMessage) + print("~" * len(secretMessage)) + + # By adding the decorator, we prompt the user for a password before printing the secret message. + printSecretMessage() + +Although this example is made up for illustration, this kind of function wrapping can be used in web applications protect access to sensitive pages. For example, code for a Web server might wrap code that transmits personal information with a decorator that checks if the user is logged in. Decorators give us a convenient syntax for modifying the behavior of functions we write. \ No newline at end of file diff --git a/_sources/AdvancedFunctions/toctree.rst b/_sources/AdvancedFunctions/toctree.rst index 211e7214..701657f5 100644 --- a/_sources/AdvancedFunctions/toctree.rst +++ b/_sources/AdvancedFunctions/toctree.rst @@ -10,5 +10,6 @@ Advanced Functions Anonymousfunctionswithlambdaexpressions.rst ProgrammingWithStyle.rst MethodInvocations.rst + WPFunctionWrappingAndDecorators.rst Exercises.rst ChapterAssessment.rst diff --git a/_sources/Classes/WPClassDecorators.rst b/_sources/Classes/WPClassDecorators.rst new file mode 100644 index 00000000..cb5255ed --- /dev/null +++ b/_sources/Classes/WPClassDecorators.rst @@ -0,0 +1,73 @@ +👩‍💻 Class Decorators +======================== + +Recall that Python has a :ref:`"decorator" syntax"` that allows us to modify the behavior of functions. We can use this same syntax to modify the behavior of classes. There are two ways we can use decorators with classes: (1) by decorating individual class methods or (2) by decorating the class itself. + +**Decorating class methods** is analogous to the function decorators we've already seen. For example, suppose we have the ``addLogging`` from :ref:`earlier `:: + + def addLogging(func): # The argument, func is a function + + def wrapper(x): # x is the argument that we're going to pass to func + print(f"About to call the function with argument {x}") + result = func(x) # actually call our function and store the result + print(f"Done with the function with argument {x}. Result: {result}") + return result # return whatever our function returned + + return wrapper # return our new function + +We first need to modify this method slightly to add ``self`` as the first argument. Then, we can use it to decorate any class method that accepts one argument: + +.. activecode:: ac20_15_1 + + def addLogging(func): # The argument, func is a function + + def wrapper(self, x): # x is the argument that we're going to pass to func + print(f"About to call the function with argument {x}") + result = func(self, x) # actually call our function and store the result + print(f"Done with the function with argument {x}. Result: {result}") + return result # return whatever our function returned + + return wrapper # return our new function + + class Car: + def __init__(self, make, model, color, mileage): + self.make = make + self.model = model + self.color = color + self.mileage = mileage + + @addLogging + def drive(self, miles): + self.mileage += miles + return self.mileage + + @addLogging + def rePaint(self, color): + self.color = color + + corvette = Car("Chevrolet", "Corvette", "red", 0) + + corvette.drive(100) + print("-"*20) + corvette.rePaint("blue") + print("-"*20) + corvette.drive(6) + +Beyond decorating class methods, we can also **decorate the class itself**. Just like functions in Python, classes are "first class", meaning they can be referenced like any other object, passed as arguments, returned, and wrapped. We decorate classes in almost the same way that we decorate functions, except that our decorator accepts a *class* as an argument, rather than a function. We could then modify the class, or return a new class. For example, suppose we want to create a decorator that adds an extra method (named ``beep``) to a class. We could do that as follows: + +.. activecode:: ac20_15_2 + + def addBeep(cls): + cls.beep = lambda self: print(f"{self.model} says 'Beep!'") + return cls + + @addBeep + class Car: + def __init__(self, make, model, color, mileage): + self.make = make + self.model = model + self.color = color + self.mileage = mileage + + mustang = Car("Ford", "Mustang", "blue", 0) + mustang.beep() # Mustang says 'Beep!' diff --git a/_sources/Classes/toctree.rst b/_sources/Classes/toctree.rst index 20e1cbbb..f8d716a5 100644 --- a/_sources/Classes/toctree.rst +++ b/_sources/Classes/toctree.rst @@ -18,6 +18,7 @@ Defining your own Classes ThinkingAboutClasses.rst TestingClasses.rst Tamagotchi.rst + WPClassDecorators.rst Glossary.rst Exercises.rst ChapterAssessment.rst From 73ebb69850dee7a206b4f56c9334315650a34557 Mon Sep 17 00:00:00 2001 From: Paul Resnick Date: Tue, 8 Aug 2023 13:40:53 -0400 Subject: [PATCH 2/4] add missing blank line; small improvements --- .../AdvancedFunctions/WPFunctionWrappingAndDecorators.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/_sources/AdvancedFunctions/WPFunctionWrappingAndDecorators.rst b/_sources/AdvancedFunctions/WPFunctionWrappingAndDecorators.rst index 68f956e4..f6c806a8 100644 --- a/_sources/AdvancedFunctions/WPFunctionWrappingAndDecorators.rst +++ b/_sources/AdvancedFunctions/WPFunctionWrappingAndDecorators.rst @@ -84,7 +84,10 @@ We can now easily "enable" or "disable" logging by commenting out the ``@addLogg To give another example, suppose we wanted to "password protect" access to calling a function. We could create a function ``passwordProtect`` that will wrap our function inside of code that ensures the user has the correct password. +Try running the code below and entering the correct password (``password123``) when prompted. Then, try running the code again and entering an incorrect password. Notice that the ``printSecretMessage`` function is only called if the user enters the correct password. + .. activecode:: ac15_6_4 + # This is a decorator function that takes another function as an argument. def passwordProtect(func): @@ -114,4 +117,4 @@ To give another example, suppose we wanted to "password protect" access to calli # By adding the decorator, we prompt the user for a password before printing the secret message. printSecretMessage() -Although this example is made up for illustration, this kind of function wrapping can be used in web applications protect access to sensitive pages. For example, code for a Web server might wrap code that transmits personal information with a decorator that checks if the user is logged in. Decorators give us a convenient syntax for modifying the behavior of functions we write. \ No newline at end of file +Although this example is made up for illustration, this kind of function wrapping can be used in web applications to protect access to sensitive pages. For example, code for a Web server might wrap code that transmits personal information with a decorator that checks if the user is logged in. Decorators give us a convenient syntax for modifying the behavior of functions we write. \ No newline at end of file From 13652651d86ba220d2e9052bfe24abca91e1c77c Mon Sep 17 00:00:00 2001 From: Paul Resnick Date: Tue, 8 Aug 2023 13:58:08 -0400 Subject: [PATCH 3/4] small improvements to code and explanations --- _sources/Classes/WPClassDecorators.rst | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/_sources/Classes/WPClassDecorators.rst b/_sources/Classes/WPClassDecorators.rst index cb5255ed..a9fd5fba 100644 --- a/_sources/Classes/WPClassDecorators.rst +++ b/_sources/Classes/WPClassDecorators.rst @@ -3,7 +3,7 @@ Recall that Python has a :ref:`"decorator" syntax"` that allows us to modify the behavior of functions. We can use this same syntax to modify the behavior of classes. There are two ways we can use decorators with classes: (1) by decorating individual class methods or (2) by decorating the class itself. -**Decorating class methods** is analogous to the function decorators we've already seen. For example, suppose we have the ``addLogging`` from :ref:`earlier `:: +**Decorating class methods** is analogous to the function decorators we've already seen. For example, suppose we have the ``addLogging`` function from :ref:`earlier `:: def addLogging(func): # The argument, func is a function @@ -15,16 +15,16 @@ Recall that Python has a :ref:`"decorator" syntax"` that allows us t return wrapper # return our new function -We first need to modify this method slightly to add ``self`` as the first argument. Then, we can use it to decorate any class method that accepts one argument: +We first need to modify this function slightly to add ``self`` as the first argument, since it will be a method of a class. Then, we can use the function to decorate any class method that accepts one argument: .. activecode:: ac20_15_1 - def addLogging(func): # The argument, func is a function + def addLogging(func): # The argument, func is a method of a class def wrapper(self, x): # x is the argument that we're going to pass to func - print(f"About to call the function with argument {x}") - result = func(self, x) # actually call our function and store the result - print(f"Done with the function with argument {x}. Result: {result}") + print(f"About to call the method with argument {x}") + result = func(self, x) # actually call the method and store the result + print(f"Done with the method invocation with argument {x} on instance {self}. Result: {result}") return result # return whatever our function returned return wrapper # return our new function @@ -44,6 +44,9 @@ We first need to modify this method slightly to add ``self`` as the first argume @addLogging def rePaint(self, color): self.color = color + + def __str__(self): + return(f"***{self.color} {self.make} {self.model} with {self.mileage} miles***") corvette = Car("Chevrolet", "Corvette", "red", 0) @@ -53,7 +56,7 @@ We first need to modify this method slightly to add ``self`` as the first argume print("-"*20) corvette.drive(6) -Beyond decorating class methods, we can also **decorate the class itself**. Just like functions in Python, classes are "first class", meaning they can be referenced like any other object, passed as arguments, returned, and wrapped. We decorate classes in almost the same way that we decorate functions, except that our decorator accepts a *class* as an argument, rather than a function. We could then modify the class, or return a new class. For example, suppose we want to create a decorator that adds an extra method (named ``beep``) to a class. We could do that as follows: +Beyond decorating class methods, we can also **decorate the class itself**. Just like functions in Python, classes are "first class", meaning they can be referenced like any other object, passed as arguments, returned, and wrapped. We decorate classes in almost the same way that we decorate functions, except that our decorator accepts a *class* as an argument, rather than a function. We could then modify the class, or return a new class. For example, suppose we want to create a decorator (named ``addBeep``) that adds an extra method (named ``beep``) to any class. We could do that as follows: .. activecode:: ac20_15_2 From f7817cbca06896f51b5f91a90d77eb3f7b2486a2 Mon Sep 17 00:00:00 2001 From: Paul Resnick Date: Wed, 9 Aug 2023 14:37:10 -0400 Subject: [PATCH 4/4] rename decorators files to not use WP in name; add explanation of why this advanced topic is covered --- ...gAndDecorators.rst => FunctionWrappingAndDecorators.rst} | 6 ++++-- _sources/AdvancedFunctions/toctree.rst | 2 +- .../Classes/{WPClassDecorators.rst => ClassDecorators.rst} | 2 +- _sources/Classes/toctree.rst | 2 +- 4 files changed, 7 insertions(+), 5 deletions(-) rename _sources/AdvancedFunctions/{WPFunctionWrappingAndDecorators.rst => FunctionWrappingAndDecorators.rst} (94%) rename _sources/Classes/{WPClassDecorators.rst => ClassDecorators.rst} (99%) diff --git a/_sources/AdvancedFunctions/WPFunctionWrappingAndDecorators.rst b/_sources/AdvancedFunctions/FunctionWrappingAndDecorators.rst similarity index 94% rename from _sources/AdvancedFunctions/WPFunctionWrappingAndDecorators.rst rename to _sources/AdvancedFunctions/FunctionWrappingAndDecorators.rst index f6c806a8..957e4d91 100644 --- a/_sources/AdvancedFunctions/WPFunctionWrappingAndDecorators.rst +++ b/_sources/AdvancedFunctions/FunctionWrappingAndDecorators.rst @@ -1,8 +1,10 @@ .. _decorators: -👩‍💻 Function Wrapping and Decorators -========================================= +Function Wrapping and Decorators +======================================= + +This section introduces an advanced python concept called *function wrapping* and a special syntax for it called *decorators*. It is not necessary to use decorators in your programming, but they are an elegant way to do function wrapping, and it will be helpful for you to understand what they do when you see them in other people's code. In Python, functions are "first class" objects, meaning they can be treated like any other object. Beyond calling functions, we can also reference them, pass them as arguments to other functions, or return them. Although we cannot directly manipulate the *behavior* of a function, we can wrap it in another function that does something before or after the original function is called or change the arguments that a function takes. This is called *function wrapping*. diff --git a/_sources/AdvancedFunctions/toctree.rst b/_sources/AdvancedFunctions/toctree.rst index 701657f5..29d20c70 100644 --- a/_sources/AdvancedFunctions/toctree.rst +++ b/_sources/AdvancedFunctions/toctree.rst @@ -10,6 +10,6 @@ Advanced Functions Anonymousfunctionswithlambdaexpressions.rst ProgrammingWithStyle.rst MethodInvocations.rst - WPFunctionWrappingAndDecorators.rst + FunctionWrappingAndDecorators.rst Exercises.rst ChapterAssessment.rst diff --git a/_sources/Classes/WPClassDecorators.rst b/_sources/Classes/ClassDecorators.rst similarity index 99% rename from _sources/Classes/WPClassDecorators.rst rename to _sources/Classes/ClassDecorators.rst index a9fd5fba..8067e4f1 100644 --- a/_sources/Classes/WPClassDecorators.rst +++ b/_sources/Classes/ClassDecorators.rst @@ -1,4 +1,4 @@ -👩‍💻 Class Decorators +Class Decorators ======================== Recall that Python has a :ref:`"decorator" syntax"` that allows us to modify the behavior of functions. We can use this same syntax to modify the behavior of classes. There are two ways we can use decorators with classes: (1) by decorating individual class methods or (2) by decorating the class itself. diff --git a/_sources/Classes/toctree.rst b/_sources/Classes/toctree.rst index f8d716a5..25e08a2c 100644 --- a/_sources/Classes/toctree.rst +++ b/_sources/Classes/toctree.rst @@ -18,7 +18,7 @@ Defining your own Classes ThinkingAboutClasses.rst TestingClasses.rst Tamagotchi.rst - WPClassDecorators.rst + ClassDecorators.rst Glossary.rst Exercises.rst ChapterAssessment.rst