Why don't decorators execute each time the decorated function is called?

Viewed 602

I wrote the following code to learn closures and decorators in python.

The code executes fine in Pythonista on iPad.

But the decorator doesn’t work like I thought it would. The decorator is intended to cause the function to print out with a unique random color with each call. But it looks like the decorator is only called once for all calls to the function. Can someone explain why ?

import random
import console 

def random_color(func): 
  r = random.random()
  g = random.random()
  b = random.random()
  print(f'console.set_color({r},{g},{b})')
  console.set_color(r,g,b)
  return func

@random_color  # run set_tag function through decorator function. 
def set_tag(tag):  
  def enclose_text(text):
    print( f'<{tag}>{text}</{tag}>')    
  return enclose_text 

# save enclose_text function with a remembered tag
h1 = set_tag('h1')
p  = set_tag('p')
br = set_tag('br')

# execute enclose_text with different text strings 
h1('Chapter One')
p('It was a dreary day. The rain had begun to set in ...')
br('')
h1('Chapter Two')
p('By the second day, the sun had returned to full strength.')

The output of all lines are the same color. The next time I run it, all the lines have the same color - but a different color than from the first time it’s executed. I expected the decorator to cause each tag to have a random color.

Can someone explain what this is not the case ?

Below is the output:

<h1>Chapter One</h1>
<p>It was a dreary day. The rain had begun to set in ...</p>
<br></br>
<h1>Chapter Two</h1>
<p>By the second day, the sun had returned to full strength.</p>
1 Answers

The decorator executes when the function is defined; decorator syntax is just syntactic sugar for function application.

@random_color  # run set_tag function through decorator function. 
def set_tag(tag):  
  def enclose_text(text):
    print( f'<{tag}>{text}</{tag}>')    
  return enclose_text 

is equivalent to

def set_tag(tag):  
  def enclose_text(text):
    print( f'<{tag}>{text}</{tag}>')    
  return enclose_text 

set_tag = random_color(set_tag)

You should define your decorator like this instead:

def random_color(func): 
    def wrapper(*args, **kwargs):
        r = random.random()
        g = random.random()
        b = random.random()
        print(f'console.set_color({r},{g},{b})')
        console.set_color(r,g,b)
        return func(*args, **kwargs)
  return wrapper

That is, random_color should return a function which sets the console color, then calls the original function.

In addition, set_tag isn't the function you want to decorate: it's the function that set_tag creates:

def set_tag(tag):
    @random_color 
    def enclose_text(text):
        print( f'<{tag}>{text}</{tag}>')    
    return enclose_text 

Before, set_tag was a function that would pick a random color, set the console to use that color, then return a function that would generate a tag. I assuming that the call to set_color affects the terminal at this point, not when print eventually gets called. Now, it's a function that returns a function that both picks a random color and generates a tag using that color.

Related