Variable Scope in Python

Variable Scope in Python

When you declare a variable, that variable is visible in parts of your program, depending on where you declare it.

If you declare it outside of any function, the variable is visible to any code running after the declaration, including functions:

age = 8

def test():
    print(age)

print(age) # 8
test() # 8
        

We call it a global variable.

If you define a variable inside a function, that variable is a local variable, and it is only visible inside that function. Outside the function, it is not reachable:

def test():
    age = 8
    print(age)

test() # 8

print(age)
# NameError: name 'age' is not defined
        

How to Accept Arguments from the Command Line in Python

Python offers several ways to handle arguments passed when we invoke the program from the command line.

So far you've run programs either from a REPL, or using

python <filename>.py
        

You can pass additional arguments and options when you do so, like this:

python <filename>.py <argument1>
python <filename>.py <argument1> <argument2>
        

A basic way to handle those arguments is to use the sys module from the standard library.

You can get the arguments passed in the sys.argv list:

import sys
print(len(sys.argv))
print(sys.argv)
        

The sys.argv list contains as the first item the name of the file that was run, for example ['main.py'].

This is a simple way, but you have to do a lot of work. You need to validate arguments, make sure their type is correct, and you need to print feedback to the user if they are not using the program correctly.

Python provides another package in the standard library to help you: argparse.

First you import argparse and you call argparse.ArgumentParser(), passing the description of your program:

import argparse

parser = argparse.ArgumentParser(
    description='This program prints the name of my dogs'
)
        

Then you proceed to add arguments you want to accept. For example in this program we accept a -c option to pass a color, like this: python program.py -c red

import argparse

parser = argparse.ArgumentParser(
    description='This program prints a color HEX value'
)

parser.add_argument('-c', '--color', metavar='color', required=True, help='the color to search for')

args = parser.parse_args()

print(args.color) # 'red'
        

If the argument is not specified, the program raises an error:

?  python python program.py
usage: program.py [-h] -c color
program.py: error: the following arguments are required: -c
        

You can set an option to have a specific set of values, using choices:

parser.add_argument('-c', '--color', metavar='color', required=True, choices={'red','yellow'}, help='the color to search for')
        
?  python python program.py -c blue
usage: program.py [-h] -c color
program.py: error: argument -c/--color: invalid choice: 'blue' (choose from 'yellow', 'red')
        

There are more options, but those are the basics.

And there are community packages that provide this functionality, too, like Click and Python Prompt Toolkit.

Lambda Functions in Python

Lambda functions (also called anonymous functions) are tiny functions that have no name and only have one expression as their body.

In Python they are defined using the lambda keyword:

lambda <arguments> : <expression>
        

The body must be a single expression - an expression, not a statement.

This difference is important. An expression returns a value, a statement does not.

The simplest example of a lambda function is a function that doubles the value of a number:

lambda num : num * 2
        

Lambda functions can accept more arguments:

lambda a, b : a * b
        

Lambda functions cannot be invoked directly, but you can assign them to variables:

multiply = lambda a, b : a * b

print(multiply(2, 2)) # 4
        

The utility of lambda functions comes when combined with other Python functionality, for example in combination with map(), filter() and reduce().

Recursion in Python

A function in Python can call itself. That's what recursion is. And it can be pretty useful in many scenarios.

The common way to explain recursion is by using the factorial calculation.

The factorial of a number is the number n mutiplied by n-1, multiplied by n-2... and so on, until reaching the number 1:

3! = 3 * 2 * 1 = 6
4! = 4 * 3 * 2 * 1 = 24
5! = 5 * 4 * 3 * 2 * 1 = 120
        

Using recursion we can write a function that calculates the factorial of any number:

def factorial(n):
    if n == 1: return 1
    return n * factorial(n-1)

print(factorial(3)) #   6
print(factorial(4)) #  24
print(factorial(5)) # 120
        

If inside the factorial() function you call factorial(n) instead of factorial(n-1), you are going to cause an infinite recursion. Python by default will halt recursions at 1000 calls, and when this limit is reached, you will get a RecursionError error.

Recursion is helpful in many places, and it helps us simplify our code when there's no other optimal way to do it, so it's good to know this technique.

Nested Functions in Python

Functions in Python can be nested inside other functions.

A function defined inside a function is visible only inside that function.

This is useful to create utilities that are useful to a function, but not useful outside of it.

You might ask: why should I be "hiding" this function, if it does no harm?

One, because it's always best to hide functionality that's local to a function, and is not useful elsewhere.

Also, because we can make use of closures (more on this later).

Here is an example:

def talk(phrase):
    def say(word):
        print(word)

    words = phrase.split(' ')
    for word in words:
        say(word)

talk('I am going to buy the milk')
        

If you want to access a variable defined in the outer function from the inner function, you first need to declare it as nonlocal:

def count():
    count = 0

    def increment():
        nonlocal count
        count = count + 1
        print(count)

    increment()

count()
        

This is useful especially with closures, as we'll see next.

Closures in Python

If you return a nested function from a function, that nested function has access to the variables defined in that function, even if that function is not active any more.

Here is a simple counter example.

def counter():
    count = 0

    def increment():
        nonlocal count
        count = count + 1
        return count

    return increment

increment = counter()

print(increment()) # 1
print(increment()) # 2
print(increment()) # 3
        

We return the increment() inner function, and that still has access to the state of the count variable even though the counter() function has ended.

Decorators in Python

Decorators are a way to change, enhance, or alter in any way how a function works.

Decorators are defined with the @ symbol followed by the decorator name, just before the function definition.

Example:

@logtime
def hello():
    print('hello!')
        

This hello function has the logtime decorator assigned.

Whenever we call hello(), the decorator is going to be called.

A decorator is a function that takes a function as a parameter, wraps the function in an inner function that performs the job it has to do, and returns that inner function. In other words:

def logtime(func):
    def wrapper():
        # do something before
        val = func()
        # do something after
        return val
    return wrapper
        

Docstrings in Python

Documentation is hugely important, not just to communicate to other people what the goal of a function/class/method/module is, but it also communicates it to yourself.

When you come back to your code 6 or 12 months from now, you might not remember all the knowledge you are holding in your head. At that point, reading your code and understanding what it is supposed to do will be much more difficult.

Comments are one way to help yourself (and others) out:

# this is a comment

num = 1 #this is another comment
        

Another way is to use docstrings.

The utility of docstrings is that they follow conventions. As such they can be processed automatically.

This is how you define a docstring for a function:

def increment(n):
    """Increment a number"""
    return n + 1
        

This is how you define a docstring for a class and a method:

class Dog:
    """A class representing a dog"""
    def __init__(self, name, age):
        """Initialize a new dog"""
        self.name = name
        self.age = age

    def bark(self):
        """Let the dog bark"""
        print('WOF!')
        

Document a module by placing a docstring at the top of the file, for example supposing this is dog.py:

"""Dog module

This module does ... bla bla bla and provides the following classes:

- Dog
...
"""

class Dog:
    """A class representing a dog"""
    def __init__(self, name, age):
        """Initialize a new dog"""
        self.name = name
        self.age = age

    def bark(self):
        """Let the dog bark"""
        print('WOF!')
        

Docstrings can span multiple lines:

def increment(n):
    """Increment
    a number
    """
    return n + 1
        

Python will process those and you can use the help() global function to get the documentation for a class/method/function/module.

For example calling help(increment) will give you this:

Help on function increment in module
__main__:

increment(n)
    Increment
    a number
        

There are many different standards to format docstrings, and you can choose to adhere to your favorite one.

I like Google's standard: https://github.com/google/styleguide/blob/gh-pages/pyguide.md#38-comments-and-docstrings

Standards allow to have tools to extract docstrings and automatically generate documentation for your code.

Introspection in Python

Functions, variables, and objects can be analyzed using introspection.

First, using the help() global function we can get the documentation if provided in form of docstrings.

Then, you can use print() to get information about a function:

def increment(n):
    return n + 1

print(increment)

# <function increment at 0x7f420e2973a0>
        

or an object:

class Dog():
    def bark(self):
        print('WOF!')

roger = Dog()

print(roger)

# <__main__.Dog object at 0x7f42099d3340>
        

The type() function gives us the type of an object:

print(type(increment))
# <class 'function'>

print(type(roger))
# <class '__main__.Dog'>

print(type(1))
# <class 'int'>

print(type('test'))
# <class 'str'>
        

The dir() global function lets us find out all the methods and attributes of an object:

print(dir(roger))

# ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'bark']
        

The id() global function shows us the location in memory of any object:

print(id(roger)) # 140227518093024
print(id(1))     # 140227521172384
        

It can be useful to check if two variables point to the same object.

The inspect standard library module gives us more tools to get information about objects, and you can check it out here: https://docs.python.org/3/library/inspect.html

Annotations in Python

Python is dynamically typed. We do not have to specify the type of a variable or function parameter, or a function return value.

Annotations allow us to (optionally) do that.

This is a function without annotations:

def increment(n):
    return n + 1
        

This is the same function with annotations:

def increment(n: int) -> int:
    return n + 1
        

You can also annotate variables:

count: int = 0
        

Python will ignore those annotations. A separate tool called mypy can be run standalone, or integrated by IDE like VS Code or PyCharm to automatically check for type errors statically, while you are coding. It will also help you catch type mismatch bugs before even running the code.

A great help especially when your software becomes large and you need to refactor your code.

Exceptions in Python

It's important to have a way to handle errors, and Python gives us exception handling to do so.

If you wrap lines of code into a try: block:

try:
    # some lines of code
        

If an error occurs, Python will alert you and you can determine which kind of error occurred using a except blocks:

try:
    # some lines of code
except <ERROR1>:
    # handler <ERROR1>
except <ERROR2>:
    # handler <ERROR2>
        

To catch all exceptions you can use except without any error type:

try:
    # some lines of code
except <ERROR1>:
    # handler <ERROR1>
except:
    # catch all other exceptions
        

The else block is run if no exceptions were found:

try:
    # some lines of code
except <ERROR1>:
    # handler <ERROR1>
except <ERROR2>:
    # handler <ERROR2>
else:
    # no exceptions were raised, the code ran successfully
        

A finally block lets you perform some operation in any case, regardless of whether an error occurred or not:

try:
    # some lines of code
except <ERROR1>:
    # handler <ERROR1>
except <ERROR2>:
    # handler <ERROR2>
else:
    # no exceptions were raised, the code ran successfully
finally:
    # do something in any case
        

The specific error that's going to occur depends on the operation you're performing.

For example if you are reading a file, you might get an EOFError. If you divide a number by zero you will get a ZeroDivisionError. If you have a type conversion issue you might get a TypeError.

Try this code:

result = 2 / 0
print(result)
        

The program will terminate with an error:

Traceback (most recent call last):
  File "main.py", line 1, in <module>
    result = 2 / 0
ZeroDivisionError: division by zero
        

and the lines of code after the error will not be executed.

Adding that operation in a try: block lets us recover gracefully and move on with the program:

try:
    result = 2 / 0
except ZeroDivisionError:
    print('Cannot divide by zero!')
finally:
    result = 1

print(result) # 1
        

You can raise exceptions in your own code, too, using the raise statement:

raise Exception('An error occurred!')
        

This raises a general exception, and you can intercept it using:

try:
    raise Exception('An error occurred!')
except Exception as error:
    print(error)
        

You can also define your own exception class, extending from Exception:

class DogNotFoundException(Exception):
    pass
        
pass here means "nothing" and we must use it when we define a class without methods, or a function without code, too.
try:
    raise DogNotFoundException()
except DogNotFoundException:
    print('Dog not found!')        

要查看或添加评论,请登录

社区洞察

其他会员也浏览了