2
\$\begingroup\$

As a beginner, in order to cement my knowledge of functions in Python, I’ve prepared this Jupyter notebook First-Class Functions. The code in it is working, but before moving on to closures and decorators I would like you to comment on my code please.

def cube(x):
    return x ** 3

cube_two = cube(2)    # Call the function, set cube_two to the value returned

print(cube)            # It says cube is a function
print(cube_two)       # The value stored in cube_two

# <function cube at 0x010E71E0>
# 8

Returned values as list elements and dict values:

nums = [4, cube(4), cube(4) / cube(4) ** 0.5]    # returned values as list elements
print(nums)

cube_dict = {'two_cubed': cube(2), 'five_cubed': cube(5)}   # as dict values
print(cube_dict)

# [4, 64, 8.0]
# {'two_cubed': 8, 'five_cubed': 125}

Setting a variable to the cube() function:

make_cube = cube    # Take out those parentheses

print(cube)
print(make_cube)    # make_cube points to the cube() function

# <function cube at 0x010E71E0>
# <function cube at 0x010E71E0>

the variable make_cube can be used as a function, like so:

print(cube)
print(make_cube(4))

# <function cube at 0x010E71E0>
# 64

map taking a function (int) as an argument:

str_nums = ['1', '2', '3']
one, two, three = map(int, str_nums)  # similar to [int(n) for n in str_nums] which is pythonic

print(str_nums)
print([one, two, three])

# ['1', '2', '3']
# [1, 2, 3]

reduce is another example: it applies a function of two arguments cumulatively to the items of an iterable.

from functools import reduce

cumu_diff = reduce(lambda x, y: x - y, [3, 2, 1])    # cumulative difference
print(cumu_diff)

# 0

Creating a map-like function (well, kind of).

def map_it(func, seq):    # func (without parentheses) as an argument
    for x in seq:
        yield func(x)

str_nums = ['1', '2', '3']
one, two, three = map_it(int, str_nums)    # same as map(int, str_nums)

print([one, two, three])

# [1, 2, 3]

Returning a function from another function:

def greet(name):

    def print_message():        
        print("Hello, " + name + "!")     # print_message() gets the name from greet()

    return print_message                  # returning the function, not calling it

greet_et = greet('E.T.')    # greet_et points to print_message()

print(greet)
print(greet_et)
print(greet_et())

# <function greet at 0x01090C90>
# <function greet.<locals>.print_message at 0x010B36A8>
# Hello, E.T.!
# None

Here's another example.

def make_paragraph(tag):

    def wrapper(text):
        return '<{0}>{1}</{0}>'.format(tag, text)

    return wrapper


para_one = make_paragraph('p')

print(para_one('This is a small paragraph.'))   # returns '<p>This is a small paragraph.</p>'

#<p>This is a small paragraph.</p>
\$\endgroup\$
3
  • 2
    \$\begingroup\$ You need to include the code you want reviewed in the body of your question for it to be on-topic for this site. \$\endgroup\$
    – Phrancis
    Oct 10, 2017 at 21:31
  • \$\begingroup\$ @Phrancis, sorry for not including it. Just edited my post. \$\endgroup\$ Oct 10, 2017 at 22:06
  • 1
    \$\begingroup\$ Thanks. I have edited your code to comment out console output. \$\endgroup\$
    – Phrancis
    Oct 10, 2017 at 22:33

1 Answer 1

2
\$\begingroup\$

There are a few general improvements I would suggest for readability and clarity and apply throughout.

  • Use docstring to document your functions, modules and classes.

  • Since Python 3, you can now use type hints for your function and method signatures, and use static code analysis tools. This also makes the code easier to read for humans.


Applying these to your cube function, you would get something like this:

def cube(number: int) -> int:
    """Return the cube of a number,
    i.e. the number to the power of 3."""
    return number ** 3

Note that I renamed the x argument to a more descriptive number argument. What the above means is that the function expects number to be an integer, and that the function should return an integer.

Another problem I see with this function is that it does not check for correct input. If you type cube("hello") then the program just crashes with an error TypeError: unsupported operand type(s) for ** or pow(): 'str' and 'int'. It would be better to handle the error. There are at least two ways to do this.

Using isinstance:

def cube(number: int) -> int:
    if not isinstance(number, int):
        return TypeError("{0} is not an integer".format(number))
    return number ** 3

Using try and except:

def cube(number: int) -> int:
    try:
        return number ** 3
    except TypeError as err:
        return TypeErrorTypeError("{0} is not an integer".format(number))

Using the latter, you could also just except TypeError as err and return err which will return the normal error listed above.

You can also use assert which will instead raise an AssertionError if the type is incorrect, for instance assert(isinstance(number, int)). This is particularly useful when setting up tests for your code.

I personally find that the first method is simpler to use, and you can use it to check multiple types at once, but the latter is the standard way to handle something that may cause an error. Choose whichever is more appropriate for the situation.


Applying the same ideas to map_it, this is slightly more complicated since the type hints have to be imported from typing library. Again, I renamed x to a more descriptive name elem.

from typing import Callable, Sequence, Iterable

def map_it(func: Callable, seq: Sequence) -> Iterable:
    """Applies a given function to a sequence and yields the result."""
    for elem in seq:
        yield func(elem)

You could also test using instanceof here if you liked, which may or may not be necessary depending on your use cases.


Your greet function can also have type errors, if you call it with a non-string type, e.g., greet(42). The simple fix for this (and I recommend to use it any time you need to put variables/arguments into strings) is to use .format() instead of +'ing string and values.

from typing import Callable

def greet(name: str) -> Callable:
    """Returns a callable greeting with given name."""
    def print_message() -> None:
        """Callable to print a greeting to stdout."""
        print("Hello, {0}!".format(name))
    return print_message

One more thing, I noticed your code is peppered with # line comments, in general comments like that should only be used to clarify or explain why something is done, in most cases they are not needed as the code speaks for itself.

\$\endgroup\$
2
  • 1
    \$\begingroup\$ I use docstrings but forgot to write them before submitting this post. I read about type hints, but have never used the typing module so far. Thanks for it. I’m using it right away. I’m also in favor of descriptive names for variables, parameters, functions etc. I’m taking note of the advice on validating input. I agree that sometimes formatting strings is better than concatenating them. I used the line comments to help keep track of what I was trying to achieve, but as you said most of them are not necessary as the code speaks for itself :) \$\endgroup\$ Oct 11, 2017 at 7:49
  • \$\begingroup\$ I've never met a case where .format() was detrimental, I'd say just always use it for formatting strings. \$\endgroup\$
    – Phrancis
    Oct 16, 2017 at 4:22

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.