14.5. Function Scope

  • Values defined in function does not leak out

  • Functions has access to global values

  • Shadowing is when you define variable with name identical to the one from outer scope

  • Shadowing in a function is valid only in a function

  • After function return, the original value of a shadowed variable is restored

  • global keyword allows modification of global variable

  • Using global keyword is considered as a bad practice

14.5.1. Values Leaking

  • Values defined in function does not leak out

>>> def login():
...     username = 'mwatney'
...     password = 'nasa'
>>>
>>>
>>> print(username)
Traceback (most recent call last):
NameError: name 'username' is not defined
>>>
>>> print(password)
Traceback (most recent call last):
NameError: name 'password' is not defined
>>>
>>>
>>> login()
>>>
>>> print(username)
Traceback (most recent call last):
NameError: name 'username' is not defined
>>>
>>> print(password)
Traceback (most recent call last):
NameError: name 'password' is not defined

14.5.2. Outer Scope

  • Functions has access to global values

>>> user = ('Mark', 'Watney')
>>>
>>>
>>> def login():
...     print(user)
>>>
>>>
>>> print(user)
('Mark', 'Watney')
>>>
>>> login()
('Mark', 'Watney')
>>>
>>> print(user)
('Mark', 'Watney')

14.5.3. Shadowing

  • When variable in function has the same name as in outer scope

  • Shadowing in a function is valid only in a function

  • Shadowed variable will be deleted upon function return

  • After function return, the original value of a shadowed variable is restored

>>> user = ('Mark', 'Watney')
>>>
>>>
>>> def login():
...     user = ('Melissa', 'Lewis')  # Shadows name 'user' from outer scope
...     print(user)
>>>
>>>
>>> print(user)
('Mark', 'Watney')
>>>
>>> login()
('Melissa', 'Lewis')
>>>
>>> print(user)
('Mark', 'Watney')

14.5.4. Global

  • global keyword allows modification of global variable

  • Using global keyword is considered as a bad practice

>>> user = ('Mark', 'Watney')
>>>
>>>
>>> def login():
...     global user
...     user = ('Melissa', 'Lewis')
...     print(user)
>>>
>>>
>>> print(user)
('Mark', 'Watney')
>>>
>>> login()
('Melissa', 'Lewis')
>>>
>>> print(user)
('Melissa', 'Lewis')

14.5.5. Global Scope

>>> globals()   
{'__name__': '__main__',
 '__doc__': None,
 '__package__': None,
 '__loader__': <class '_frozen_importlib.BuiltinImporter'>,
 '__spec__': None,
 '__annotations__': {},
 '__builtins__': <module 'builtins' (built-in)>}
>>> firstname = 'Mark'
>>> lastname = 'Watney'
>>>
>>> globals()  
{'__name__': '__main__',
 '__doc__': None,
 '__package__': None,
 '__loader__': <class '_frozen_importlib.BuiltinImporter'>,
 '__spec__': None,
 '__annotations__': {},
 '__builtins__': <module 'builtins' (built-in)>,
 'firstname': 'Mark',
 'lastname': 'Watney'}
>>> def login():
...     pass
>>>
>>>
>>> globals()  
{'__name__': '__main__',
 '__doc__': None,
 '__package__': None,
 '__loader__': <class '_frozen_importlib.BuiltinImporter'>,
 '__spec__': None,
 '__annotations__': {},
 '__builtins__': <module 'builtins' (built-in)>,
 'login': <function __main__.login()>}

14.5.6. Local Scope

  • Variables defined inside function

  • Variables are not available from outside

  • If outside the function, will return the same as globals()

>>> locals()  
{'__name__': '__main__',
 '__doc__': None,
 '__package__': None,
 '__loader__': <class '_frozen_importlib.BuiltinImporter'>,
 '__spec__': None,
 '__annotations__': {},
 '__builtins__': <module 'builtins' (built-in)>}
>>> def login():
...     username = 'mwatney'
...     password = 'nasa'
...     print(locals())
>>>
>>>
>>> login()
{'username': 'mwatney', 'password': 'nasa'}

If outside the function, will return the same as globals():

>>> locals() == globals()
True
>>>
>>> locals() is globals()
True

14.5.7. Shadowing Global Scope

  • Defining variable with the same name as in outer scope

  • Shadowed variable will be deleted upon function return

Shadowing of a global scope is used frequently in Mocks and Stubs. This way, we can simulate user input. Note that Mocks and Stubs will stay until the end of a program.

>>> def input(prompt):
...     return 'Mark Watney'
>>>
>>>
>>> name = input('Type your name: ')
>>> name
'Mark Watney'
>>>
>>> age = input('Type your age: ')
>>> age
'Mark Watney'
>>> from unittest.mock import MagicMock
>>> input = MagicMock(side_effect=['Mark Watney', '44'])
>>>
>>>
>>> name = input('Type your name: ')
>>> name
'Mark Watney'
>>>
>>> age = input('Type your age: ')
>>> age
'44'

To restore default behavior of input() function use:

>>> from builtins import input

14.5.8. Builtins

>>> import builtins
>>>
>>> dir(builtins)   
['ArithmeticError', 'AssertionError', 'AttributeError', 'BaseException',
 'BaseExceptionGroup', 'BlockingIOError', 'BrokenPipeError', 'BufferError',
 'BytesWarning', 'ChildProcessError', 'ConnectionAbortedError',
 'ConnectionError', 'ConnectionRefusedError', 'ConnectionResetError',
 'DeprecationWarning', 'EOFError', 'Ellipsis', 'EncodingWarning',
 'EnvironmentError', 'Exception', 'ExceptionGroup', 'False',
 'FileExistsError', 'FileNotFoundError', 'FloatingPointError',
 'FutureWarning', 'GeneratorExit', 'IOError', 'ImportError',
 'ImportWarning', 'IndentationError', 'IndexError', 'InterruptedError',
 'IsADirectoryError', 'KeyError', 'KeyboardInterrupt', 'LookupError',
 'MemoryError', 'ModuleNotFoundError', 'NameError', 'None',
 'NotADirectoryError', 'NotImplemented', 'NotImplementedError', 'OSError',
 'OverflowError', 'PendingDeprecationWarning', 'PermissionError',
 'ProcessLookupError', 'RecursionError', 'ReferenceError',
 'ResourceWarning', 'RuntimeError', 'RuntimeWarning', 'StopAsyncIteration',
 'StopIteration', 'SyntaxError', 'SyntaxWarning', 'SystemError',
 'SystemExit', 'TabError', 'TimeoutError', 'True', 'TypeError',
 'UnboundLocalError', 'UnicodeDecodeError', 'UnicodeEncodeError',
 'UnicodeError', 'UnicodeTranslateError', 'UnicodeWarning', 'UserWarning',
 'ValueError', 'Warning', 'ZeroDivisionError', '_', '__build_class__',
 '__debug__', '__doc__', '__import__', '__loader__', '__name__',
 '__package__', '__spec__', 'abs', 'aiter', 'all', 'anext', 'any', 'ascii',
 'bin', 'bool', 'breakpoint', 'bytearray', 'bytes', 'callable', 'chr',
 'classmethod', 'compile', 'complex', 'copyright', 'credits', 'delattr',
 'dict', 'dir', 'divmod', 'enumerate', 'eval', 'exec', 'exit', 'filter',
 'float', 'format', 'frozenset', 'getattr', 'globals', 'hasattr', 'hash',
 'help', 'hex', 'id', 'input', 'int', 'isinstance', 'issubclass', 'iter',
 'len', 'license', 'list', 'locals', 'map', 'max', 'memoryview', 'min',
 'next', 'object', 'oct', 'open', 'ord', 'pow', 'print', 'property',
 'quit', 'range', 'repr', 'reversed', 'round', 'set', 'setattr', 'slice',
 'sorted', 'staticmethod', 'str', 'sum', 'super', 'tuple', 'type', 'vars',
 'zip']

14.5.9. Use Case - 0x01

Simulate user input (for test automation)

>>> from unittest.mock import MagicMock
>>> input = MagicMock(side_effect=['lastname'])
>>> firstname = 'Mark'
>>> lastname = 'Watney'
>>>
>>>
>>> varname = input('Type variable name: ')   #input: 'lastname'
>>>
>>> globals()[varname]
'Watney'

14.5.10. Assignments

Code 14.21. Solution
"""
* Assignment: Function Scope Global
* Type: class assignment
* Complexity: easy
* Lines of code: 5 lines
* Time: 8 min

English:
    1. Define in global scope `SELECT: set[str]` with values:
       `'setosa', 'versicolor'`
    2. Define function `sumif(features, label)`
    3. Function sums `features`, only when `label` is in `SELECT`
    4. When `label` is not in `select` return `0` (zero)
    5. Run doctests - all must succeed

Polish:
    1. Zdefiniuj w przestrzeni globalnej `SELECT: set[str]` z wartościami:
       `'setosa', 'versicolor'`
    2. Zdefiniuj funkcję `sumif(features, label)`
    3. Funkcja sumuje `features`, tylko gdy `label` jest w `SELECT`
    4. Gdy `label` nie występuje w `select` zwróć `0` (zero)
    5. Uruchom doctesty - wszystkie muszą się powieść

Tests:
    >>> import sys; sys.tracebacklimit = 0
    >>> from inspect import isfunction

    >>> assert isfunction(sumif), \
    'Object `sumif` must be a function'
    >>> assert SELECT is not Ellipsis, \
    'Assign result to variable: `SELECT`'
    >>> assert type(SELECT) is set, \
    'Variable `result` has invalid type, should be set'
    >>> assert all(type(x) is str for x in SELECT), \
    'All rows in `result` should be str'

    >>> sum(sumif(row[:4],row[4]) for row in DATA[1:])
    49.1
"""

DATA = [
    ('sepal_length', 'sepal_width', 'petal_length', 'petal_width', 'species'),
    (5.8, 2.7, 5.1, 1.9, 'virginica'),
    (5.1, 3.5, 1.4, 0.2, 'setosa'),
    (5.7, 2.8, 4.1, 1.3, 'versicolor'),
    (6.3, 2.9, 5.6, 1.8, 'virginica'),
    (6.4, 3.2, 4.5, 1.5, 'versicolor'),
    (4.7, 3.2, 1.3, 0.2, 'setosa'),
]

# Define in global scope `SELECT: set[str]` with values: 'setosa', 'versicolor'
# Define function `sumif(features, label)`
# Function sums `features`, only when `label` is in `SELECT`
# When `label` is not in `select` return `0` (zero)