Define a Proper Callable Object

PythonPythonBeginner
Practice Now

This tutorial is from open-source community. Access the source code

Introduction

Objectives:

  • Learn how to define a proper callable object

Files Modified: validate.py


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL python(("`Python`")) -.-> python/BasicConceptsGroup(["`Basic Concepts`"]) python(("`Python`")) -.-> python/FunctionsGroup(["`Functions`"]) python(("`Python`")) -.-> python/ControlFlowGroup(["`Control Flow`"]) python(("`Python`")) -.-> python/DataStructuresGroup(["`Data Structures`"]) python(("`Python`")) -.-> python/ModulesandPackagesGroup(["`Modules and Packages`"]) python(("`Python`")) -.-> python/ObjectOrientedProgrammingGroup(["`Object-Oriented Programming`"]) python(("`Python`")) -.-> python/ErrorandExceptionHandlingGroup(["`Error and Exception Handling`"]) python(("`Python`")) -.-> python/PythonStandardLibraryGroup(["`Python Standard Library`"]) python/BasicConceptsGroup -.-> python/comments("`Comments`") python/FunctionsGroup -.-> python/keyword_arguments("`Keyword Arguments`") python/BasicConceptsGroup -.-> python/variables_data_types("`Variables and Data Types`") python/BasicConceptsGroup -.-> python/numeric_types("`Numeric Types`") python/BasicConceptsGroup -.-> python/strings("`Strings`") python/ControlFlowGroup -.-> python/conditional_statements("`Conditional Statements`") python/ControlFlowGroup -.-> python/for_loops("`For Loops`") python/DataStructuresGroup -.-> python/lists("`Lists`") python/DataStructuresGroup -.-> python/tuples("`Tuples`") python/DataStructuresGroup -.-> python/sets("`Sets`") python/FunctionsGroup -.-> python/function_definition("`Function Definition`") python/FunctionsGroup -.-> python/default_arguments("`Default Arguments`") python/ModulesandPackagesGroup -.-> python/importing_modules("`Importing Modules`") python/ModulesandPackagesGroup -.-> python/using_packages("`Using Packages`") python/ModulesandPackagesGroup -.-> python/standard_libraries("`Common Standard Libraries`") python/ObjectOrientedProgrammingGroup -.-> python/classes_objects("`Classes and Objects`") python/ObjectOrientedProgrammingGroup -.-> python/constructor("`Constructor`") python/ObjectOrientedProgrammingGroup -.-> python/polymorphism("`Polymorphism`") python/ObjectOrientedProgrammingGroup -.-> python/encapsulation("`Encapsulation`") python/ErrorandExceptionHandlingGroup -.-> python/raising_exceptions("`Raising Exceptions`") python/PythonStandardLibraryGroup -.-> python/data_collections("`Data Collections`") python/BasicConceptsGroup -.-> python/python_shell("`Python Shell`") python/FunctionsGroup -.-> python/build_in_functions("`Build-in Functions`") subgraph Lab Skills python/comments -.-> lab-132513{{"`Define a Proper Callable Object`"}} python/keyword_arguments -.-> lab-132513{{"`Define a Proper Callable Object`"}} python/variables_data_types -.-> lab-132513{{"`Define a Proper Callable Object`"}} python/numeric_types -.-> lab-132513{{"`Define a Proper Callable Object`"}} python/strings -.-> lab-132513{{"`Define a Proper Callable Object`"}} python/conditional_statements -.-> lab-132513{{"`Define a Proper Callable Object`"}} python/for_loops -.-> lab-132513{{"`Define a Proper Callable Object`"}} python/lists -.-> lab-132513{{"`Define a Proper Callable Object`"}} python/tuples -.-> lab-132513{{"`Define a Proper Callable Object`"}} python/sets -.-> lab-132513{{"`Define a Proper Callable Object`"}} python/function_definition -.-> lab-132513{{"`Define a Proper Callable Object`"}} python/default_arguments -.-> lab-132513{{"`Define a Proper Callable Object`"}} python/importing_modules -.-> lab-132513{{"`Define a Proper Callable Object`"}} python/using_packages -.-> lab-132513{{"`Define a Proper Callable Object`"}} python/standard_libraries -.-> lab-132513{{"`Define a Proper Callable Object`"}} python/classes_objects -.-> lab-132513{{"`Define a Proper Callable Object`"}} python/constructor -.-> lab-132513{{"`Define a Proper Callable Object`"}} python/polymorphism -.-> lab-132513{{"`Define a Proper Callable Object`"}} python/encapsulation -.-> lab-132513{{"`Define a Proper Callable Object`"}} python/raising_exceptions -.-> lab-132513{{"`Define a Proper Callable Object`"}} python/data_collections -.-> lab-132513{{"`Define a Proper Callable Object`"}} python/python_shell -.-> lab-132513{{"`Define a Proper Callable Object`"}} python/build_in_functions -.-> lab-132513{{"`Define a Proper Callable Object`"}} end

Preparation

Back in Exercise 4.3, you created a series of Validator classes for performing different kinds of type and value checks. For example:

>>> from validate import Integer
>>> Integer.check(1)
>>> Integer.check('hello')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "validate.py", line 21, in check
    raise TypeError(f'Expected {cls.expected_type}')
TypeError: Expected <class 'int'>
>>>

You could use the validators in functions like this:

>>> def add(x, y):
        Integer.check(x)
        Integer.check(y)
        return x + y

>>>

In this exercise, we're going to take it just one step further.

Creating a Callable Object

In the file validate.py, start by creating a class like this:

## validate.py
...

class ValidatedFunction:
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        print('Calling', self.func)
        result = self.func(*args, **kwargs)
        return result

Test the class by applying it to a function:

>>> def add(x, y):
        return x + y

>>> add = ValidatedFunction(add)
>>> add(2, 3)
Calling <function add at 0x1014df598>
5
>>>

Enforcement

Modify the ValidatedFunction class so that it enforces value checks attached via function annotations. For example:

>>> def add(x: Integer, y:Integer):
        return x + y
>>> add = ValidatedFunction(add)
>>> add(2,3)
5
>>> add('two','three')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "validate.py", line 67, in __call__
    self.func.__annotations__[name].check(val)
  File "validate.py", line 21, in check
    raise TypeError(f'Expected {cls.expected_type}')
TypeError: expected <class 'int'>
>>>>

Hint: To do this, play around with signature binding. Use the bind() method of Signature objects to bind function arguments to argument names. Then cross reference this information with the __annotations__ attribute to get the different validator classes.

Keep in mind, you're making an object that looks like a function, but it's really not. There is magic going on behind the scenes.

Use as a Method (Challenge)

A custom callable often presents problems if used as a custom method. For example, try this:

class Stock:
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

    @property
    def cost(self):
        return self.shares * self.price

    def sell(self, nshares:Integer):
        self.shares -= nshares
    sell = ValidatedFunction(sell)     ## Fails

You'll find that the wrapped sell() fails miserably:

>>> s = Stock('GOOG', 100, 490.1)
>>> s.sell(10)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "validate.py", line 64, in __call__
    bound = self.signature.bind(*args, **kwargs)
  File "/usr/local/lib/python3.6/inspect.py", line 2933, in bind
    return args[0]._bind(args[1:], kwargs)
  File "/usr/local/lib/python3.6/inspect.py", line 2848, in _bind
    raise TypeError(msg) from None
TypeError: missing a required argument: 'nshares'
>>>

Bonus: Figure out why it fails--but don't spend too much time fooling around with it.

Summary

Congratulations! You have completed the Define a Proper Callable Object lab. You can practice more labs in LabEx to improve your skills.

Other Python Tutorials you may like