Annotating Return Values
00:00 Annotating the Return Values of Factory Functions and Generators In this section of the course, you’ll take a look at how to annotate return values from two common Python constructs, factory functions, and generators.
00:15 A factory function is a higher-order function that produces a new function from scratch. The factory’s parameters determine this new function’s behavior. In particular, a function that takes a callable and also returns.
00:27 One is called a decorator in Python. Continuing with the previous examples, what if you wanted to write a decorator to time the execution of other functions in your code?
00:37
Here’s how you’d measure how long your parse_email()
function takes to finish.
01:03
The time
it
decorator takes a callable with arbitrary inputs and outputs as an argument and returns a callable with the same inputs and outputs.
01:11
The ParamSpec
annotation indicates the arbitrary inputs in the first element of Callable
, while the TypeVar
indicates the arbitrary outputs in the second argument.
01:24
The time_it
decorator defines an inner function wrapper
that uses a timer function to measure how long it takes to execute the callable given as the argument. This inner function stores the current time in the start
variable, executes the decorated function while capturing its return value and stores the new time in the end
variable.
01:44
It then prints out the calculated duration before returning the value of the decorated function. After defining time_it
, you can decorate any function with it using the @
symbol as syntactic sugar instead of manually calling it as if it were a factory function.
01:59
You can use the time_it
decorator around parse_email()
to create a new function with an additional behavior responsible for timing its own execution.
02:12
When you call the decorated parse_email()
function, it returns the expected values, but also prints a message describing how long the original function took to execute.
02:23 You’ve added new capability to your function in a declarative style without modifying its source code, which is elegant, but somewhat goes against the Zen of Python.
02:34 One could argue that decorators make your code less explicit, but at the same time, they can make your code look simpler, improving its readability. Sometimes, you may want to use a generator to yield pieces of data one at a time instead of storing them all in memory for better efficiency, particularly for larger data sets.
02:54
You can annotate a generator function using type hints in Python, and one way to do so is to use the Generator
type from collections.abc
.
03:03 Continuing on from the earlier code, imagine now that you have a long list of emails to parse. Instead of storing every parsed result in memory and having the function return everything all at once, you can use a generator to yield the parsed usernames and domains one at a time.
03:19
To do so, you can write a generator function, which yields this information and use the Generator
type as a type hint for the return type.
03:31
The parse_email_generator()
function doesn’t take any arguments as you’ll send them to the resulting generator object. The rest of the generator is a fairly straightforward conversion of a previously seen function into a generator.
04:03 If you want to learn more about generators, then Real Python has you covered with this course.
04:10
Notice that the Generator
type hint expects three parameters, the last two of which are optional. Yield Type: The first parameter is what the generator yields.
04:20 In this case, it’s a tuple containing two strings, one for the username and the other for the domain both parsed from the email address. Alternatively, the generator may yield an error string when the email address is invalid.
04:35 Send Type: The second parameter describes what you are sending into the generator. This is also a string as you’ll be sending email addresses to the generator. Return Type: The third parameter represents what the generator returns when it’s done producing values.
04:50
In this case, the function returns the string done
. On screen, you can see how to use the generator function.
05:03
You start by calling the parse_email_generator()
function, which returns a new generator object. Then you advance the generator to the first yield
statement by calling the built-in next()
function.
05:15 After that, you can start sending email addresses to the generator to parse.
05:29 The generator terminates when you send an empty string.
05:37
Because generators are also iterators, namely generator iterators, you can alternatively use the collections.abc
Iterator
type instead of Generator
as a type hint to convey a similar meaning, but because you won’t be able to specify the send and return types using a pure iterator type hint, collections.abc
Iterator
will only work as long as your generator yields values alone.
06:07
This flavor of the parse_email()
function takes a list of strings and returns a generator object that iterates over them in a lazy fashion using a for
loop.
06:16 Even though a generator is more specific than an iterator, the latter is still broadly applicable and easier to read, so it’s a valid choice.
06:27
On screen, you can see the email iterator in action. An instance of the parse_emails()
iterator is created passing a list of values with valid and invalid email addresses.
06:45
Then next()
is used on the iterator, and you can see that only the valid emails are returned.
06:56
Once the iterator is exhausted, a StopIteration
exception is returned.
07:04
Sometimes Python programmers use the even less restrictive and more general collections.abc
Iterable
type to annotate such a generator without leaking the implementation details.
07:21
Here, you annotate both the function’s argument and its return type with the Iterable
type to make the function more versatile. It can now accept any iterable object instead of just a list.
07:36 Conversely, the function caller doesn’t need to know whether it returns a generator or a sequence of items as long as they can loop over it. This adds flexibility because you can change the implementation from an eager list container to a lazy generator without breaking the contract with the caller established through type hints.
07:54 You can do this when you anticipate the return data will be large enough to need a generator. While you can reserve the freedom to change the return type later, as in the example seen previously, in most cases, you should strive to be as specific in your return type annotations as possible.
08:11 An important part of using type hints is to improve code readability, and in the next section of the course you’ll see how to extend this by using type aliases.
Become a Member to join the conversation.