Qgelm

Make Your Python Prettier With Decorators

Originalartikel

Backup

<html> <p>Many Pythonistas are familiar with using decorators, but far fewer understand what&#8217;s happening under the hood and can write their own. It takes a little effort to learn their subtleties but, once grasped, they&#8217;re a great tool for writing concise, elegant Python.</p> <p>This post will briefly introduce the concept, start with a basic decorator implementation, then walk through a few more involved examples one by one.</p> <h1>What is a decorator</h1> <p>Decorators are most commonly used with the

@decorator

&#160;syntax. You may have seen Python that looks something like these examples.</p> <pre class=„brush: python; title: ; notranslate“ title=„“> @app.route(„/home“) def home():

  return render_template("index.html")

@performance_analysis def foo():

  pass

@property def total_requests(self):

  return self._total_requests

</pre> <p>To understand what a decorator does, we first have to take a step back and look at some of the things we can do with functions in Python.</p> <pre class=„brush: python; title: ; notranslate“ title=„“> def get_hello_function(punctuation):

  """Returns a hello world function, with or without punctuation."""
  def hello_world():
      print("hello world")
  def hello_world_punctuated():
      print("Hello, world!")
  if punctuation:
      return hello_world_punctuated
  else:
      return hello_world

if name == 'main':

  ready_to_call = get_hello_function(punctuation=True)
  ready_to_call()
  # "Hello, world!" is printed

</pre> <p>In the above snippet,

get_hello_function

&#160;returns a function. The returned function gets assigned and then called. This flexibility in the way functions can be used and manipulated is key to the operation of decorators.</p> <p>As well as returning functions, we can also pass functions as arguments. In the example below, we <strong>wrap</strong> a function, adding a delay before it&#8217;s called.</p> <pre class=„brush: python; title: ; notranslate“ title=„“> from time import sleep def delayed_func(func):

  """Return a wrapper which delays `func` by 10 seconds."""
  def wrapper():
      print("Waiting for ten seconds...")
      sleep(10)
      # Call the function that was passed in
      func()
  return wrapper

def print_phrase():

  print("Fresh Hacks Every Day")

if name == 'main':

  delayed_print_function = delayed_func(print_phrase)
  delayed_print_function()

</pre> <p>This can feel a bit confusing at first, but we&#8217;re just defining a new function&#160;

wrapper

, which sleeps before calling

func

. It&#8217;s important to note that we haven&#8217;t changed the behaviour of

func

&#160;itself, we&#8217;ve only returned a different function which calls

func

&#160;after a delay.</p> <p>When the code above is run, the following output is produced:</p> <pre class=„brush: plain; title: ; notranslate“ title=„“> $ python decorator_test.py Waiting for ten seconds… Fresh Hacks Every Day </pre> <h2>Let&#8217;s make it pretty</h2> <p>If you rummage around the internet for information on decorators, the phrase you&#8217;ll see again and again is &#8220;syntactic sugar&#8221;. This does a good job of explaining what decorators are: simply a shortcut to save typing and improve readability.</p> <p>The

@decorator

&#160;syntax makes it very easy to apply our wrapper to any function. We could re-write our delaying code above like this:</p> <pre class=„brush: python; title: ; notranslate“ title=„“> from time import sleep def delayed_func(func):

  """Return `func`, delayed by 10 seconds."""
  def wrapper():
      print("Waiting for ten seconds...")
      sleep(10)
      # Call the function that was passed in
      func()
  return wrapper

@delayed_func def print_phrase():

  print("Fresh Hacks Every Day")

if name == 'main':

  print_phrase()

</pre> <p>Decorating&#160;

print_phrase

&#160;with

@delayed_func

&#160;automatically does the wrapping, meaning that whenever

print_phrase

&#160;is called we get the delayed wrapper instead of the original function;

print_phrase

&#160;has been replaced by

wrapper

.</p> <h1>Why is this useful?</h1> <p>Decorators can&#8217;t change a function, but they can extend its behaviour, modify and validate inputs and outputs, and implement any other external logic.&#160;The benefit of writing decorators comes from their ease of use once written. In the example above we could easily add

@delayed_func

&#160;to any function of our choice.</p> <p>This ease of application is useful for debug code as well as program code. One of the most common applications for decorators is to provide debug information on the performance of a function. Let&#8217;s write a simple decorator which logs the datetime the function was called at, and the time taken to run.</p> <pre class=„brush: python; title: ; notranslate“ title=„“> import datetime import time from app_config import log def log_performance(func):

  def wrapper():
      datetime_now = datetime.datetime.now()
      log.debug(f"Function {func.__name__} being called at {datetime_now}")
      start_time = time.time()
      func()
      log.debug(f"Took {time.time() - start_time} seconds")
  return wrapper

@log_performance def calculate_squares():

  for i in range(10_000_000):
      i_squared = i**2

if name == 'main':

  calculate_squares()

</pre> <p>In the code above we use our

log_performance

&#160;decorator on a function which calculates the squares of the&#160; numbers 0 to 10,000,000. This is the output when run:</p> <pre class=„brush: plain; title: ; notranslate“ title=„“> $ python decorator_test.py Function calculate_squares being called at 2018-08-23 12:39:02.112904 Took 2.5019338130950928 seconds </pre> <h1>Dealing with parameters</h1> <p>In the example above, the

calculate_squares

&#160;function didn&#8217;t need any parameters, but what if we wanted to make our

log_performance

&#160;decorator work with any function that takes any parameters?</p> <p>The solution is simple: allow

wrapper

&#160;to accept arguments, and pass those arguments directly into&#160;

func

. To allow for any number of arguments and keyword arguments, we&#8217;ve used&#160;

*args, **kwargs

, passing all of the arguments to the wrapped function.</p> <pre class=„brush: python; title: ; notranslate“ title=„“> import datetime import time from app_config import log def log_performance(func):

  def wrapper(*args, **kwargs):
      datetime_now = datetime.datetime.now()
      log.debug(f"Function {func.__name__} being called at {datetime_now}")
      start_time = time.time()
      result = func(*args, **kwargs)
      log.debug(f"Took {time.time() - start_time} seconds")
      return result
  return wrapper

@log_performance def calculate_squares(n):

  """Calculate the squares of the numbers 0 to n."""
  for i in range(n):
      i_squared = i**2

if name == 'main':

  calculate_squares(10_000_000) # Python 3!

</pre> <p>Note that we also capture the result of the

func

call and use it as the return value of the wrapper.</p> <h1>Validation</h1> <p>Another common use case of decorators is to validate function arguments and return values.</p> <p>Here&#8217;s an example where we&#8217;re dealing with multiple functions which return an IP address and port in the same format.</p> <pre class=„brush: python; title: ; notranslate“ title=„“> def get_server_addr():

  """Return IP address and port of server."""
  ...
  return ('192.168.1.0', 8080)

def get_proxy_addr():

  """Return IP address and port of proxy."""
  ...
  return ('127.0.0.1', 12253)

</pre> <p>If we wanted to do some basic validation on the returned port, we could write a decorator like so:</p> <pre class=„brush: python; title: ; notranslate“ title=„“> PORTS_IN_USE = [1500, 1834, 7777] def validate_port(func):

  def wrapper(*args, **kwargs):
      # Call `func` and store the result
      result = func(*args, **kwargs)
      ip_addr, port = result
      if port &lt; 1024:
          raise ValueError("Cannot use priviledged ports below 1024")
      elif port in PORTS_IN_USE:
          raise RuntimeError(f"Port {port} is already in use")
      # If there were no errors, return the result
      return result
  return wrapper

</pre> <p>Now it&#8217;s easy to ensure our ports are validated, we simply decorate any appropriate function with

@validate_port

.</p> <pre class=„brush: python; title: ; notranslate“ title=„“> @validate_port def get_server_addr():

  """Return IP address and port of server."""
  ...
  return ('192.168.1.0', 8080)

@validate_port def get_proxy_addr():

  """Return IP address and port of proxy."""
  ...
  return ('127.0.0.1', 12253)

</pre> <p>The advantage of this approach is that validation is done externally to the function &#8211; there&#8217;s no risk that changes to the internal function logic or order will affect validation.</p> <h2>Dealing with function attributes</h2> <p>Let&#8217;s say we now want to access some of the metadata of the

get_server_addr

&#160;function above, like the name and docstring.</p> <pre class=„brush: python; title: ; notranslate“ title=„“> &gt;&gt;&gt; get_server_addr.name 'wrapper' &gt;&gt;&gt; get_server_addr.doc &gt;&gt;&gt; </pre> <p>Disaster! Since our

validate_port

&#160;decorator essentially <strong>replaces&#160;</strong>the functions it decorates with our wrapper, all of the function attributes are those of

wrapper

, not the original function.</p> <p>Fortunately, this problem is common, and the

functools

&#160;module in the standard library has a solution:&#160;

wraps

. Let&#8217;s use it in our

validate_port

&#160;decorator, which now looks like this:</p> <pre class=„brush: python; title: ; notranslate“ title=„“> from functools import wraps def validate_port(func):

  @wraps(func)
  def wrapper(*args, **kwargs):
      # Call `func` and store the result
      result = func(*args, **kwargs)
      ip_addr, port = result
      if port &lt; 1024:
          raise ValueError("Cannot use priviledged ports below 1024")
      elif port in PORTS_IN_USE:
          raise RuntimeError(f"Port {port} is already in use")
      # If there were no errors, return the result
      return result
  return wrapper

</pre> <p>Line 4 indicates that

wrapper

should preserve the metadata of

func

, which is exactly what we want. Now when we try and access metadata, we get what we expect.</p> <pre class=„brush: python; title: ; notranslate“ title=„“> &gt;&gt;&gt; get_server_addr.name 'get_server_addr' &gt;&gt;&gt; get_server_addr.doc 'Return IP address and port of server.' &gt;&gt;&gt; </pre> <h1>Summary</h1> <p>Decorators are a great way to make your codebase more flexible and easy to maintain. They provide a simple way to do runtime validation on functions and are handy for debugging as well. Even if writing custom decorators isn&#8217;t your thing, an understanding of what makes them tick will be a significant asset when understanding third-party code and for utilising decorators which are already written.</p> </html>

Cookies helfen bei der Bereitstellung von Inhalten. Diese Website verwendet Cookies. Mit der Nutzung der Website erklären Sie sich damit einverstanden, dass Cookies auf Ihrem Computer gespeichert werden. Außerdem bestätigen Sie, dass Sie unsere Datenschutzerklärung gelesen und verstanden haben. Wenn Sie nicht einverstanden sind, verlassen Sie die Website.Weitere Information