Contents:
Integers
Floats
Introduction
The Zen of Python emphasizes simplicity and clarity:
Prefer beauty, clarity, and simplicity.
Choose flat over nested structures, and sparse over dense.
Readability and practicality are crucial.
Special cases shouldn't override general rules.
Silence errors only intentionally.
Avoid guessing in ambiguity.
Ideally, there's one clear way to do things.
Timeliness matters, but thoughtful delay can be prudent.
Solutions should be easy to explain to be considered good.
Embrace more namespaces as they are beneficial.
A quick refresher - basics review
Multiline Statements: Understanding Physical vs. Logical Newlines
Implicit Physical Newlines:
Automatically merged into a single logical line.
Applicable in lists, tuples, dictionaries, sets, and function arguments/parameters.
Support inline comments.
Explicit Physical Newlines:
Not automatically merged; require manual intervention.
Used to break statements using the backslash (
\
) character.Do not support inline comments.
Multiline String Literals:
Created using triple delimiters (
'''
or"""
), suitable for spanning strings across multiple lines.
Variable Naming and Conventions
Variable Names:
Begin with an underscore (_) or a letter (a-z, A-Z), but not a digit.
Can include any combination of numbers, underscores, letters, or digits.
Avoid reserved words.
Conventions:
Single Underscore (_my_var): Indicates internal use or private objects. Such objects are excluded from imports like
from module import *
.One-Sided Double Underscore (__my_var): Mangles class attributes, beneficial in inheritance scenarios.
Double-Sided Double Underscores (dunder, my_var): Reserved for system-defined names with specific interpreter meanings. Use only predefined dunders.
Naming Standards (PEP8 Style Guide):
Packages: Short, all-lowercase, ideally without underscores (e.g.,
utilities
).Modules: Short, all-lowercase, may include underscores (e.g.,
db_utils
,dbutils
).Classes: Use CapWords or upper camel case (e.g.,
BankAccount
).Functions: Lowercase with underscores (snake_case, e.g.,
open_account
).Variables: Lowercase with underscores (snake_case, e.g.,
account_id
).Constants: All-uppercase with underscores (e.g.,
MIN_APR
).
Classes
Classes implement special methods to define custom behaviors:
__str__
: Provides a readable representation for end-users. If undefined, falls back to__repr__
.
def __str__(self):
return 'Rectangle (width={}, height={})'.format(self.width, self.height)
__repr__
: Gives a detailed, unambiguous string for debugging that could recreate the object's state.
def __repr__(self):
return 'Rectangle({}, {})'.format(self.width, self.height)
__eq__
: Defines equality comparison, considering two objects equal based on specific attributes.
def __eq__(self, other):
return isinstance(other, Rectangle) and (self.width, self.height) == (other.width, other.height)
__lt__
: Specifies less-than comparison, often based on a computed attribute like area.
def __lt__(self, other):
return isinstance(other, Rectangle) and self.area() < other.area()
Getters & Setters: Encapsulate data access and modification, implemented with decorators for simplicity.
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
@property
def width(self):
return self._width
@width.setter
def width(self, value):
if value <= 0:
raise ValueError('Width must be positive.')
self._width = value
@property
def height(self):
return self._height
@height.setter
def height(self, value):
if value <= 0:
raise ValueError('Height must be positive.')
self._height = value
Python type hierarchy
Numbers
Integral: Integers, Booleans
Non-Integral: Floats, Complex numbers, Decimals, Fractions
Collections
Sequences
Mutable: Lists
Immutable: Tuples, Strings
Sets
Mutable: Sets
Immutable: Frozen Sets
Mappings: Dictionaries
Callables
User-Defined Functions
Generators
Classes
Instance Methods
Class Instances (call())
Built-in Functions: (e.g., len(), open())
Built-in Methods: (e.g., list.append(x))
Variables and memory
Variables are memory references
Heap - a section of memory allocated for storing objects while a program runs.
The
id()
function provides the memory address of an object as a base-10 integer.
Reference counting
Reference Counting: A memory management strategy where objects track the number of references to them.
Automatic Deletion: Memory is freed when an object’s reference count hits zero, helping prevent memory leaks and automate memory management in Python.
sys.getrefcount(my_var)
: Increases the reference count by creating an extra reference.ctypes.c_long.from_address(address).value
: Directly accesses the memory address (usingid(my_var)
) without affecting the reference count.
Garbage collection
Circular References:
Occur when objects are mutually referenced in a cycle.
Standard reference counting fails as the involved objects’ counts never drop to zero due to their interdependencies.
Garbage Collector:
Manages memory by identifying and reclaiming memory from objects that are no longer in use.
Includes a cycle detector to resolve circular references, freeing up memory that cannot be reclaimed by reference counting alone.
Garbage Collection Control:
Enabled by default to handle memory efficiently and prevent memory leaks.
Can be manually controlled via the
gc
module; allows turning off, forcing garbage collection runs, or custom cleanup.Turning it off is risky unless you’re certain your code lacks circular references.
Dynamic vs Static typing
Static Typing:
Languages like Java, C++, and Swift require explicit declaration of data types during variable declaration. This ensures type consistency throughout the program.
Dynamic Typing:
In dynamic languages like Python, data types do not need to be declared with the variable. The type is determined at runtime, allowing for greater flexibility in coding.
Variable re-assignment
Variable Reassignment:
Reassigning a variable to a new value updates its memory address, as it typically points to a new object in memory.
Immutability of Integers:
Integer values are immutable, meaning the actual numeric value at a given memory address cannot be changed.
Memory Address Sharing:
Two variables holding the same integer value often reference the same memory address to optimize memory usage, thanks to Python's internal optimizations.
Object mutability
Memory Address Content:
A memory address holds the data type and the state (the actual data).
Mutable Objects:
Objects whose internal state can be changed. Examples include:
Lists
Sets
Dictionaries
User-Defined Classes
Immutable Objects:
Objects whose internal state cannot be altered. Examples include:
Numbers (int, float, Booleans)
Strings
Tuples
Frozen Sets
Some User-Defined Classes
Immutable objects can contain mutable elements, such as a tuple containing lists. While the tuple itself cannot change its structure (the references it holds), the content of the mutable elements (lists) within can change.
Impact of Mutability on Memory Addresses:
Mutability methods influence memory addresses. For example, appending to a list does not change its memory address, but concatenating lists creates a new list at a new address.
Function arguments and mutability
Variable Values in Functions:
Changes to variable values within functions adhere to the same rules of mutability and reference assignment as elsewhere in the program.
Function Attribute References:
Attributes within a function refer to the same memory addresses as the variables passed to it, maintaining consistency with external references.
Immutable Objects:
Immutable objects prevent unintended side-effects in functions because their internal state cannot be altered.
Mutable Objects:
Mutable objects are susceptible to unintended side-effects within functions due to their changeable internal state.
Shared references and mutability
Immutable variables with the same value share a reference to a memory address.
Changing an immutable variable creates a new memory address for the changed value, ensuring safety.
This sharing mechanism doesn't apply to mutable variables.
Variable equality
Memory Address
is / is not
:Identity operator.
Compares memory addresses.
Object State (data):
== / !=
:Equality operator.
Compares object states (data).
The None object:
Is a real object managed by the Python memory manager.
The memory manager always uses a shared reference when assigning a variable to None.
Everything is an object
Any object can be assigned to a variable including functions
Any object can be passed to a function including functions
Any object can be returned from a function including functions
Python Optimizations: Interning
CPython Implementation:
Standard Python implementation (written in C).
When
a = 6
andb = 6
, both variables reference the same memory address.But for
a = 500
andb = 500
, they reference different memory addresses.
Interning:
Reuses objects on-demand.
Python pre-loads a global list of integers in the range [-5; 256].
Small integers are optimized since they appear frequently.
Any reference to an integer in that range uses the cached version.
Singletons:
Classes that can only be instantiated once.
Python Optimizations: String interning
String Interning:
Some strings are automatically interned, but not all.
During Python code compilation, identifiers (or those resembling identifiers) are interned.
Identifiers starting with _ or (a-z A-Z) and consisting of _, (a-z A-Z 0-9) are interned.
Strings with white spaces are not interned.
Not all strings are automatically interned, but you can force intern them using
sys.intern()
.However, it's generally unnecessary to intern strings manually unless required.
Comparing references with
is / is not
is faster than character by character comparison with== / !=
.
# Example 1:
a = 'hello'
b = 'hello'
a is b -> True
a == b -> True
# Example 2:
a = 'hello world'
b = 'hello world'
a is b -> False
a == b -> True
Example 3:
a = 'hello_world'
b = 'hello_world'
a is b -> True
a == b -> True
Python Optimizations: Peephole
Constant Expressions:
Used for immutable variables, numeric calculations, and short sequences with length < 20.
Examples:
24 * 60 -> 1440 (1, 2) * 2 -> (1, 2, 1, 2) 'abc' * 2 -> 'abcabc' 'helo' + ' world' -> 'hello world' 'the quick brown fox' * 10 -> too many characters (> 20)
Membership Tests:
Mutables are replaced by immutables for membership tests.
Lists are replaced by tuples, sets by frozen sets.
Set membership is much faster than list or tuple membership.
Use
{}
for membership tests whenever possible.Instead of
if e in [1, 2, 3]
orif e in (1, 2, 3)
, useif e in {1, 2, 3}
.
Numeric Types
5 main types of numbers
Boolean Truth Values: Represented by
bool
.Integer Numbers (Z): Represented by
int
.Rational Numbers (Q): Represented by
fractions.Fraction
.Real Numbers (R): Represented by
float
ordecimal.Decimal
.Complex Numbers (C): Represented by
complex
.
Hierarchy:
Integers (Z) < Rational Numbers (Q) < Real Numbers (R) < Complex Numbers (C).
Integers: Data Types
How large an integer can be depends on how many bits are used to store the number
In some languages like Java and C, integer data types use a fixed number of bits.
Examples in Java include
byte
(8-bit),short
(16-bit),int
(32-bit), andlong
(64-bit).
In Python, the int
object uses a variable number of bits.
It can use 4 bytes (32 bits), 8 bytes (64 bits), 12 bytes (96 bits), etc.
Theoretically, its size is limited only by the available memory.
You can use sys.getsizeof()
from the sys
module to determine the amount of memory used by the attribute.
Integers: Operations
Division always returns a float.
n = d * (n // d) + (n % d)
, wheren
is the numerator andd
is the denominator.Floor division (
//
ordiv
) returns an integer.The floor of a real number
a
is the largest integer less than or equal toa
.For negative numbers, floor division returns the largest integer smaller than or equal to
a
.Example:
math.floor(-3.1) -> -4
.
Floor division is different from truncation.
Modulo operator (
%
ormod
) returns the remainder as an integer.
Integers: Constructors and Bases
The
int
class provides multiple constructors:int(10) -> 10
int(10.9) -> truncation 10
int(-10.9) -> truncation -10
int(True) -> 1
int(Decimal('10.9')) -> truncation 10
int('10') -> 10
Number Base:
When used with a string, the constructor has an optional second parameter: base (2<= base <= 36).
If the base is not specified, the default is base 10.
Examples (answers are in base 10):
int('1010', base=2) -> 10
int('A12F', base=16) -> 41263
int('a12f', base=16) -> 41263
int('534', base=8) -> 348
int('A', base=11) -> 10
int('B', base=11) -> ValueError: invalid literal for int() with base 11: B
Reverse Process: Changing an int from base 10 to another base:
bin()
- Base 2 (binary).bin(10) -> '0b1010'
oct()
- Base 8.oct(10) -> '0o12'
hex()
- Base 16.hex(10) -> '0xa'
The prefixes in the string help document the base of the number to avoid confusion.
The prefixes are consistent with literal integers using a base prefix (no strings attached!).
type(0xa) -> int
Base Change Algorithm:
Given a number
n
and a baseb
.If
b
is less than 2 orn
is negative, raise an exception.If
n
equals 0, return[0]
.Initialize an empty list
digits
.While
n
is greater than 0:Calculate the remainder
m
when dividingn
byb
.Divide
n
byb
and assign the result back ton
.Append
m
to the beginning of thedigits
list.
Encoding:
Python's encoding utilizes characters ranging from 0 to 9 and a to z (case insensitive), limiting the base to 36.
However, the choice of characters to represent the digits is flexible and can be customized based on your encoding map.
Encoding Algorithm:
Given a list of
digits
and an encodingmap
.Initialize an empty string
encoding
.For each digit
d
in thedigits
list:Append the character corresponding to
d
in the encodingmap
to theencoding
string.
Here's the simplified version:
# Encoding Algorithm
digits = [...] # List of digits
map = '...' # Encoding map
encoding = ''
# Construct the encoding string
for d in digits:
encoding += map[d]
# Or more simply:
encoding = ''.join([map[d] for d in digits])
Rational numbers
Rational numbers, represented as fractions of integer numbers or as real numbers with a finite number of digits after the decimal point, can be conveniently handled in Python using the Fraction
class from the fractions
module. Here are some key points about using fractions:
Fraction Class:
Rational numbers are represented using the
Fraction
class in Python.Fractions are automatically reduced to their simplest form.
Example:
Fraction(6, 10)
becomesFraction(3, 5)
.
Negative signs are attached to the numerator.
Example:
Fraction(1, -4)
becomesFraction(-1, 4)
.
Constructors:
Fraction(numerator=0, denominator=1)
Fraction(other_fraction)
Fraction(float)
Fraction(decimal)
Fraction(string)
Example:
Fraction('0.125')
becomesFraction(1, 8)
.
Standard arithmetic operators (
+, -, *, /
) are supported and result inFraction
objects.Numerator and denominator can be accessed using
numerator
anddenominator
attributes.
Handling Irrational Numbers:
The
Fraction
class can handle irrational numbers likemath.pi
ormath.sqrt(2)
, although the result is an approximation, not exact.
Constraining the Denominator:
In computer representations, numbers like
0.3
may not be exact, leading to approximations.limit_denominator(max_denominator)
method helps in finding an approximate equivalent fraction with a constrained denominator.Finds the closest rational with a denominator not exceeding
max_denominator
.
Example:
For
x = Fraction(0.3)
, the result isFraction(5404319552844595, 18014398509481984)
.But
x.limit_denominator(10)
givesFraction(3, 10)
, providing a more human-readable result.
Floats: Internal representations
The float
class in Python is the default implementation for representing real numbers. Here are some key points about Python's float
:
Implementation:
Python's
float
is implemented using the C double type, which typically conforms to the IEEE 754 double-precision binary float standard, also known as binary64.It uses a fixed number of bytes: 8 bytes or 64 bits.
Components:
Sign: 1 bit (0 for positive, 1 for negative).
Exponent: 11 bits, giving a range of [-1022, 1023].
Significant digits: 52 bits, representing 15-17 significant (base-10) digits.
For simplicity, all digits are included except leading and trailing zeros.
Examples of 5 Significant Digits:
1.2345
1234.5
12345000000
0.00012345
12345e-50
Finite Decimal vs. Binary Representation:
Some numbers that have a finite decimal representation do not have a finite binary representation, and vice versa.
Floats: Equality testing
Working with floating-point numbers in Python, especially when testing for equality, can be tricky due to precision issues. Here's why:
Some decimals with finite decimal representation cannot be represented with a finite binary representation, leading to precision discrepancies.
python
Copy code
x = 0.1 + 0.1 + 0.1 y = 0.3 x == y # Evaluates to False due to precision discrepancies
To test for equality, two common methods are:
Rounding:
Round both sides of the equality expression to a certain number of significant digits.
round(x, 5) == round(y, 5)
Using an Appropriate Range (Epsilon):
Define a function to check if the absolute difference between two numbers is less than a specified tolerance (epsilon).
def is_equal(x, y, eps):
return math.fabs(x - y) < eps
However, there are non-trivial issues with these methods:
Absolute Tolerance (abs_tol):
Works well with numbers away from zero.
Setting an exact number as epsilon may not work well with numbers away from zero.
Relative Tolerance (rel_tol):
Works well with numbers close to zero.
Setting a percentage-based epsilon may not work well with numbers close to zero.
We can calculate the tolerance using
rel_tol * max(|x|, |y|)
.
To overcome these issues, it's recommended to use the math.isclose()
function, which handles absolute and relative tolerances intelligently:
math.isclose(a, b, rel_tol=1e-09, abs_tol=0.0)
If you omit abs_tol
, it defaults to 0, potentially causing issues when comparing numbers close to zero.
In general, it's advisable to avoid using the ==
operator with floats. If precise equality comparison is necessary, consider converting numbers to fractions.
Floats: Coercing to integers
When converting a float to an integer, there's always some data loss. Here are several ways to perform this conversion:
Truncation:
math.trunc()
: Returns the integer portion of the number by ignoring everything after the decimal point.int(float)
: Using theint()
method also performs truncation.
Floor:
math.floor()
: Returns the largest integer less than or equal to the number.For positive numbers, floor and truncation are equivalent, but for negative numbers, they differ.
Floor division (
//
) is an example of this method.
Ceiling:
math.ceil()
: Returns the smallest integer greater than or equal to the number.
Rounding:
Rounding can be performed using the
round()
function or by using theround
method of thefloat
class.It rounds to the nearest integer, with ties rounded away from zero by default.
Floats: Rounding
The round()
function in Python rounds a number x
to the closest multiple of 10 to the power of -n
, where n
can be negative as well. If n
is not specified, it defaults to 0, and the function returns an integer. Here are some key points:
Usage:
round(x)
: Returns an integer.round(x, n)
: Returns a number of the same type asx
.round(x, 0)
: Returns a number of the same type asx
.
Exception:
When rounding ties, such as
round(1.25, 1)
, the rounding method follows the Banker's rounding technique.
Banker's Rounding:
Follows the IEEE 754 standard, rounding ties to the nearest value with an even least significant digit.
Example:
round(1.25, 1) -> 1.2
round(1.35, 1) -> 1.4
round(15, -1) -> 20
round(25, -1) -> 20
Why Banker's Rounding?:
It's less biased than rounding ties away from zero, especially when averaging numbers.
Rounding Towards Positive Infinity:
An incorrect method is
int(x + 0.5)
, which doesn't work for negative numbers.The correct method involves considering the sign of
x
and adding 0.5:
def round_up(x):
from math import copysign
return int(x + copysign(0.5, x))
This function ensures proper rounding behavior, even for negative numbers.
Decimals
The decimal
module in Python, introduced in PEP 327, provides an alternative to using binary floats, helping to avoid approximation issues inherent in floating-point arithmetic. While the Fraction
class is another option, it poses challenges when adding fractions due to the need to find a common denominator, which can be complex and memory-intensive. Here's why the decimal
module is preferred:
Decimal Context:
The
decimal
module introduces a context mechanism that controls various aspects of working with decimals.Contexts can be either global or local:
Global Context:
Affects operations performed throughout the codebase.
Accessed and modified using
decimal.getcontext()
.
Local Context:
Temporarily modifies settings without affecting the global settings.
Created using
decimal.localcontext()
as a context manager.
Precision and Rounding:
The context allows control over precision and rounding mechanisms:
ctx.prec
: Sets or retrieves the precision (an integer).ctx.rounding
: Sets or retrieves the rounding mechanism (a string).Options include
ROUND_UP
,ROUND_DOWN
,ROUND_CEILING
,ROUND_FLOOR
,ROUND_HALF_UP
,ROUND_HALF_DOWN
, andROUND_HALF_EVEN
.
Using the decimal
module with appropriate context settings provides more control over arithmetic operations involving decimals, ensuring precision and avoiding approximation issues.
Decimals: Constructors and contexts
The Decimal(x)
constructor in Python's decimal
module allows creating Decimal objects from various types of data. Here's a breakdown of the supported input types for x
:
Integers:
Example:
a = Decimal(10)
creates a Decimal object representing the integer 10.
Other Decimal Objects:
Example:
a = Decimal('0.1')
creates a Decimal object representing the decimal number 0.1.
Strings:
Example:
a = Decimal('0.1')
creates a Decimal object representing the decimal number 0.1.
Tuples:
Example:
a = Decimal((1, (3, 1, 4, 1, 5), -4))
creates a Decimal object representing the decimal number -3.1415.Format:
(sign, (d1, d2, d3, ...), exp)
, where sign is 0 for positive and 1 for negative.
Floats:
While floats can be used, it's not usually recommended due to precision issues.
Example:
Decimal(0.1)
may result in a slightly imprecise representation like0.100000000000000005551
.It's better to use strings or tuples instead for exact representation.
Context Precision and the Constructor:
Context precision affects mathematical operations but not the constructor.
Example:
import decimal
from decimal import Decimal
decimal.getcontext().prec = 6
a = Decimal('0.12345') # a -> 0.12345
b = Decimal('0.12345') # b -> 0.12345
print(a + b) # -> 0.24690
with decimal.localcontext() as ctx:
ctx.prec = 2
c = a + b
print(c) # -> 0.25
print(c) # -> 0.25
In this example, the precision set in the local context affects the result of the addition operation (c
), but it doesn't affect the constructor (a
and b
).
Decimals: Math operations
When working with Decimals, the //
(floor division) and %
(modulus) operators don't behave exactly the same as they do with integers, although they still satisfy the equation n = d * (n // d) + (n % d)
. Here's how they differ:
Floor Division (//
):
For integers: Performs floor division, resulting in the floor of the quotient.
For Decimals: Performs truncated division, resulting in the truncated quotient.
Modulus (%
):
For integers: Returns the remainder after division.
For Decimals: The behavior changes for negative Decimals, but the usual equation still holds true.
While the Decimal class defines various mathematical operations like square root and logarithms, not all functions from the math module are available. Using math functions with Decimal objects involves casting them to floats, which loses the precision mechanism provided by Decimal objects. It's recommended to use math functions defined in the Decimal class whenever possible to maintain precision.
Decimals: Performance considerations
When comparing the Decimal
class to the float
class, there are several drawbacks to using Decimal
:
Ease of Coding:
Decimal
objects are not as easy to construct compared to floats. They require construction via strings or tuples, which may be less intuitive for some developers.
Limited Math Functions:
Not all math functions available in the
math
module have a counterpart in theDecimal
class. This limits the functionality available when working withDecimal
objects.
Memory Overhead:
Decimal
objects typically have more memory overhead compared to floats. This can become a concern when dealing with large datasets or performance-critical applications.
Performance:
Decimal
arithmetic operations are generally slower compared to floats. This relative slowness can impact the performance of applications, especially those involving extensive numerical calculations.
Despite these drawbacks, the Decimal
class offers precise arithmetic and is suitable for applications requiring exact decimal representation, such as financial calculations or where accuracy is paramount. However, for applications where performance is critical and exact precision is not required, floats may be preferred due to their simplicity and faster arithmetic operations.
Complex numbers
Complex Class:
Constructor:
complex(x, y)
for rectangular coordinates wherex
is the real part andy
is the imaginary part.Literals:
x + yJ
.
Rectangular Coordinates:
Real and imaginary parts (
x
andy
) are stored as floats.
Instance Properties and Methods:
.real
: Returns the real part..imag
: Returns the imaginary part..conjugate()
: Returns the complex conjugate.
Arithmetic Operators:
Standard arithmetic operators (+, -, *, /, **) work as expected.
Real and complex numbers can be mixed.
//
and%
operators (divmod) are NOT supported.
Other Operations:
==
and!=
are supported but may corrupt due to float approximation problems.Comparison operators are not supported.
Use the
cmath
module instead ofmath
.
Rectangular to Polar:
cmath.phase(x)
: Returns the argument (phase) of the complex numberx
in [-pi, pi].abs(x)
: Returns the magnitude (r) ofx
.
Polar to Rectangular:
cmath.rect(r, phi)
: Returns a complex number (rectangular coordinates) equivalent to the complex number defined by (r, phi) in polar coordinates.
Euler's Identity:
eiπ + 1 = 0
RHS = cmath.exp(complex(0, math.pi)) + 1
is very close to zero, not exact (due to float's approximation problems).cmath.isclose(RHS, 0, abs_tol=0.0001)
returns 0, confirming the exactness of Euler's identity.
Booleans
Bool Class:
Represents Boolean values.
Subclass of the int class.
Possesses properties and methods of integers.
Specialized methods like
and
,or
, etc.True
and1
are not the same object (True is 1
isFalse
).True == 1
evaluates toTrue
.Two constants:
True
andFalse
.Singleton objects of type
bool
.Retain the same memory address.
Equality can be assessed using
is
/is not
or==
/!=
.Many classes define how to cast instances to a Boolean, known as truth value.
bool(0)
evaluates toFalse
.bool(x)
evaluates toTrue
whenx
is not0
.
Booleans: Truth values
Truth Value in Python:
Every object in Python has an associated truth value.
Exceptions include:
None
False
0
in any numeric typeEmpty sequences
Empty mapping types
Custom classes that implement a
__bool__
or__len__
method returningFalse
or0
.
Implementation:
Classes define their truth value by implementing a special instance method:
__bool__(self)
or__len__(self)
.
When
bool(x)
is called, Python executesx.__bool__()
orx.__len__()
, if__bool__
is not defined.If neither method is defined, then
if my_list:
is equivalent toif my_list is not None and len(my_list) > 0
.
Booleans: Precedence and Short Circuiting
Properties of Boolean Operators:
Commutativity:
A or B == B or A
A and B == B and A
Distributivity:
A and (B or C) == (A and B) or (A and C)
A or (B and C) == (A or B) and (A or C)
Associativity:
A or (B or C) == (A or B) or C == A or B or C
A and (B and C) == (A and B) and C == A and B and C
De Morgan's Theorem:
not(A or B) == (not A) and (not B)
not(A and B) == (not A) or (not B)
Miscellaneous:
not(x < y) == x >= y
not(x > y) == x <= y
not(x <= y) == x > y
not(x >= y) == x < y
not(not A) == A
Operator Precedence:
Highest precedence
( )
< > <= >= == != in is
not
and
or
Lowest precedence
Short-Circuiting:
If
X
isTrue
, thenX or Y
will beTrue
regardless of the value ofY
.If
X
isFalse
, thenX and Y
will beFalse
regardless of the value ofY
.
Booleans: Boolean operators
X or Y:
If
X
is truthy, returnsX
, otherwise evaluatesY
and returns it (Short-Circuiting).
X and Y:
If
X
is falsy, returnsX
, otherwise evaluatesY
and returns it (Short-Circuiting).Can avoid a division by zero error using the
and
operator and Short-Circuiting:x = 0 and total/0
evaluates to0
(since0
isFalse
, Python does not evaluatetotal/0
).
Examples:
Computing an average:
avg = n and sum/n
.Returning the first character of a string
s
, or an empty string if the string isNone
or empty:return (x and s[0]) or ''
.
not X:
True if
X
is falsy.False if
X
is truthy.
AND Operator Orientation:
Oriented on returning a False value.
OR Operator Orientation:
Oriented on returning a True value.
Comparison operators
Binary Operators: Evaluate to a bool value.
Categories:
Identity Operations:
is
/is not
: Compares memory addresses (any type).
Value Comparisons:
==
/!=
: Compares values (different types OK, but must be compatible).
Ordering Comparisons:
<
/<=
/>
/>=
: Doesn't work for all types.
Membership Operations:
in
/not in
: Used with iterable types.
Numeric Types:
Value comparisons work with all numeric types.
Mixed types (except complex) in value and ordering comparisons are supported.
Be careful with floats (equality approximation problems).
Chained Comparisons:
a == b == c
is equivalent toa == b and b == c
.a < b < c
is equivalent toa < b and b < c
.a < b > c < d
is equivalent toa < b and b > c and c < d
.Supports Short-Circuiting.
Function parameters
Argument vs Parameter
Semantics:
Parameters:
Variables local to a function during function declaration.
Arguments:
Variables local to a function that are passed to a function.
Arguments are passed by reference, i.e., the memory addresses of arguments are passed.
Often used interchangeably.
Positional and Keyword arguments
Positional Arguments:
Most common way of assigning arguments to parameters: via the order in which they are passed, i.e., their position.
Defined in the function signature.
Default Values:
A positional argument can be made optional by specifying a default value for the corresponding parameter.
If a positional parameter is defined with a default value, every positional parameter after it must also be given a default value.
Keyword Arguments (Named Arguments):
Positional arguments can optionally be specified by using the parameter name, regardless of whether the parameters have default values.
Example:
my_func(a=2, c=2)
assignsa=2
,b=5
(default),c=2
.Once you use a named argument, all arguments thereafter must be named too when calling the function.
Unpacking iterables
Tuple Definition:
What defines a tuple in Python is not
()
but,
(comma).
Packed Values:
Refer to values that are bundled together in some way, i.e., iterables.
Unpacking:
The act of splitting packed values into individual variables contained in a list or tuple.
Based on the relative positions of each element.
Example:
a, b, c = (a1, a2, a3)
assignsa = a1
,b = a2
,c = a3
.
Swapping Values of Two Variables:
Traditional approach:
tmp = a a = b b = tmp
Using unpacking (parallel assignment):
a, b = b, a
.This works because in Python, the entire RHS is evaluated first (creating a tuple) and completely then assignments are made to the LHS.
Unpacking Sets and Dictionaries:
Dictionaries (and Sets) are unordered types.
They can be iterated, but there is no guarantee the order of the results will match your return.
In practice, we rarely unpack sets and dictionaries in precisely this way.
Extended unpacking
Usage of * Operator with Ordered Types:
The
*
operator, when used in unpacking assignments, is employed to unpack the remaining values of an iterable into another variable (or list if multiple variables).Example:
a, *b, c = l
unpacks the listl
, assigning the first value toa
, the middle values tob
as a list, and the last value toc
.
Concatenation using * Operator:
The
*
operator can also be used for concatenation.Example:
l = [*l1, *l2]
concatenates two listsl1
andl2
into a single listl
.
Usage of * Operator with Unordered Types:
While iterating sets and dictionaries may not guarantee the preservation of order, unpacking still works.
However, it's rarely used to unpack sets and dictionaries directly.
Nested Unpacking:
Nested unpacking allows for extracting values from iterables within iterables.
Example:
a, *b, (c, *d) = [1, 2, 3, 'abcd']
assigns the first value toa
, the middle values tob
, the first value of the nested iterable toc
, and the remaining values tod
.Result:
a = 1
b = [2, 3]
c = 'a'
d = ['b', 'c', 'd']
Unpacking vs Slicing:
Unpacking returns a list, while slicing a string returns a string.
Unpacking works with all iterables, including mixed types, while slicing does not work with unordered iterables.
When slicing to multiple variables, the returned type is the same as the type of the iterable being sliced (e.g., list -> list, set -> set, etc.).
*args
Used to declare positional arguments passed to a function:
Example:
a, b, *c = 10, 20, 'a', 'b'
In function definitions,
*args
collects extra positional arguments:Example:
def func1(a, b, *c): # code
When calling the function, unpacking is required:
Example:
func(10, 20, 'a', 'b')
assignsa = 10
,b = 20
,c = ('a', 'b')
Note: This forms a tuple, not a list.
The name
*args
is a convention but not mandatory:You can choose any name for the variable after the
*
, like*values
or*items
.
*args
exhausts positional arguments:No additional positional arguments can be added after
*args
.
Example:
def func(a, b, c): # code l = [10, 20, 30] func(l) # will NOT work func(*l) # assigns a = 10, b = 20, c = 30
Keyword arguments
Positional Arguments:
Can optionally be passed as named (keyword) arguments.
When a default value is used for a positional parameter, every positional parameter must have a default value.
Keyword Arguments:
Can be assigned a default value.
Unlike positional parameters, if a default value is assigned to a keyword parameter, subsequent keyword parameters do not need default values.
Can be made mandatory by creating parameters after the positional parameters have been exhausted using the
*args
method:Example:
def func(a, b, *args, d): # code
Here,
*args
exhausts all positional arguments, andd
must be passed as a keyword (named) argument.func(1, 2)
will not work since there is no keyword argument 'd'.
Mandatory positional arguments can even be omitted:
Example:
def func(*args, d): # code
func(d=100)
assignsargs = ()
andd = 100
.
No positional arguments at all can be forced using
*
:Example:
def func(*, d): # code
func(1, 2, 3, d=100)
raises an Exception Error.
Putting it All Together:
Example:
def func(a, b=1, *args, d, e=True): # code
Parameters:
a
: Mandatory positional argument (can be specified using a named argument).b
: Optional positional argument (can be specified positionally, as a named argument, or not at all), defaults to 1.*args
: Catch-all for any additional positional arguments (optional).*
: No additional positional arguments allowed.d
: Mandatory keyword argument.e
: Optional keyword argument, defaults to True.
Function parameters
Argument vs Parameter
Semantics
Parameteters are variables local to a function during function declaration
Arguments are variables local to a function that are passed to a function
arguments are passed by reference, i.e. the memory addresses of arguments are passed
Often used interchangebaly
Positional and Keyword arguments
Positional arguments:
Most common way of assigning arguments to parameters: via the order in which they are passed, i.e. their position
def my_func(a, b)
Default values
A positional arguments can be made optional by specifying a default value for the corresponding parameter
If a positional parameter is defined with a default value every poistional parameter after it must also be given a default value
def my_func(a, b=100)
Keyword arguments (named arguments)
Positional arguments can, optionally, be specified by using the parameter name whether or not the parameters have default values
my_func(a=2, c=2)
-> a=1, b=5(default), c=2Once you use a named argument, all arguments thereafter must be named too (when calling the function)
Unpacking iterables
What defines a tuple in Python, is not (), but , (coma)
Packed values referes to values that are budled together in some way, i.e. iterables
Unpacking
is the act of splitting packed values into individual variables contained in a list or tuple
is based on the relative positions of each element
a, b, c = (a1, a2, a3)
-> a = a1, b = a2, c = a3swapping values of two variables
Traditional approach
tmp = a
a = b
b = tmp
using unpacking (parallel assignment)
a, b = b, a
This worls because in Python, the entire RHS is evaluated first (creating a tuple) and completely then assignments are made to the LHS
Unpacking Sets and Dictionaries
Dictionaries (and Sets) are unordered types
They can be iterated, but there is no guarantee the order of the results will match your return
In practice, we rarely unpack sets and dictionaries in precisely this way
Extended unpacking
Operator is used to unpack the remaining values of an iterable into another variable (or list if multiple variables):
Usage of * operator with ordered types
l = [1, 2, 3, 4, 5, 6]
a = l[0]
b = l[1:]
or
a, b = l[0], l[1:] (aka parallel assignment)
ora, *b, c = l
-> a = 1, b = [2, 3, 4,5], c = 6Apart from cleaner sintax, it (* - asterix) also works with any iterable, not just sequence types!
The * operator can only be used once in the LHS an unpacking assignment
Concatination using * operator:
l1 = [1, 2, 3]
l2 = [4, 5, 6]
l = [*l1, *l2] -> l = [1, 2, 3, 4, 5, 6]
Usage of * operator with unordered types
Iterating sets and dictionaries will not guarantee the preservance of order upon creation, but still works
In practice, rarelt used to unpack sets and dictionaries directly
Useful in cases where the you need to see all the items, keys or values from multiple unordered iterables to a separate lst
** operator
pass the contents of a dictionary as keyword arguments to a function or to create a new dictionary by merging two dictionaries.
d1 = {'a': 1, 'b': 2}
{'a': 10, 'c': 3, **d1} -> {a: 1, 'b': 2, 'c': 3} (a got overwritten by the 'a' key in d1, provided later positionally)
{**d1, 'a': 10, 'c': 3} -> {a: 10, 'b': 2, 'c': 3} ('a' value got overwritte by the value provided the latest)
nested unpacking
l = [1, 2, [3, 4]] a, b, (c, d) = l``` -> a = 1, b = 2, c = [3, 4]
a, *b, (c, d, e) = [1, 2, 3, 'XYZ']
-> a = 1, b = [2, 3], c = 'X', d = 'Y', e = 'Z'a, *b, (c, *d) = [1, 2, 3, 'abcd']
-> a = 1, b = [2, 3], c = 'a', d = ['b', 'c', 'd']Although this looks like we are using * twice in the same expression, the second * is actually in a nested unpacking - so that's OK
Unpacking vs slicing
unapcking of a string will return list
unpacking works with all iterable (can work with mixed types of iterables)
slicing a string will return string
slicing does not work with unordered iterables
if slicing to a multiple variables, it returns the same type of iterable it is slicing (list -> list, set -> set etc)
*args
also used to declare positional arguments passed to a function
a, b, *c = 10, 20, 'a', 'b'
def func1(a, b, *c):
# code
func (10, 20, 'a', 'b') -> a = 10, b = 20, c = ('a', 'b') -> this is a tuple, not a list
The * parameter name is arbitrary - you can make it whatever you want
it is customary (but not required) to name it *args
args exhausts positional arguments
You can not add more positional arguments after *args
Example:
def func(a, b, c):
# code
l = [10, 20, 30]
func(l) -> will NOT work
func(*l) -> a = 10, b = 20, c = 30
Keyword arguments
Positional arguments
positional parameters can, optionally be passed as named (keyword) arguments
when default value is used for a positional parameter, every positional parameter has to have a default value
Keyword arguments
can be assigned a default value
As opposed to positional parameter, if default value is assigned to a keyword parameter, next keyword parameters do not have to have a default value
can be made mandatory
To do so, we create parameters after the positional parameters have been exhausted (with *args method)
def func(a, b, *args, d): #code
in this case, *args effectively exhausts all positional arguments and d must be passed as a keyword (named) argument.
func(1, 2)
will not work since there is no keyword argument 'd'
we can even omit any mandatory positional arguments
def func(*args, d): #code
func(d=100)
-> args = (), d = 100we can force no positional arguments at all
def func(*, d): #code
* indicates the 'end' of positional arguments]func(1, 2, 3, d=100)
-> Exception Error!
Putting all together
def func(a, b=1, *args, d, e=True): #code
def func(a, b=1, *, d, e=True): #code
a: mandatory positional argument (may be specified using a named argument)
b: optional positional argument (may be specified positionally, as a named argument, or not at all), defaults to 1
*args: catch-all for any (optional) additional positional arguments
*: no additional positional arguments allowed
d: mandatory keyword argument
e: optional keyword argument, defaults to True
**kwargs
*args:
Used to scoop up a variable amount of remaining positional arguments into a tuple.
The parameter name
args
is arbitrary;*
is the real performer here.If positional arguments are passed using parameter names,
*args
cannot be used. This is because the first arguments become keyword-only arguments, and*args
requires positional arguments, which cannot be used after keyword arguments.If default values are used for the positional parameters before
*args
, the ability to use their default values is lost.
**kwargs:
Used to scoop up a variable amount of remaining keyword arguments into a dictionary.
The parameter name
kwargs
is arbitrary;**
is the real performer here.**kwargs
can be specified even if the positional arguments have not been exhausted, unlike keyword-only arguments.No parameters can come after
**kwargs
.
Example:
def func(*args, **kwargs):
# code
func(1, 2, a=10, b=20)
assignsargs = (1, 2)
andkwargs = {'a': 10, 'b': 20}
.func()
assignsargs = ()
andkwargs = {}
.
Putting it all together
Parameter defaults
Function Object Creation:
The function object is created, and
func
references it.The integer object
10
is evaluated/created and is assigned as the default fora
.
Function Execution:
func()
is called.By the time this happens, the default value for
a
has already been evaluated and assigned. It is not re-evaluated when the function is called again.
Re-evaluation with Default Value:
If you want the parameter to get re-evaluated every time the function is called (e.g., set a current datetime):
Set a default to
None
.If
dt
isNone
, set it to the current date/time.If not, use the provided
dt
.dt = dt or datetime.utcnow()
.
Beware of using a mutable object (or a callable) for an argument default, as it will not be re-evaluated.
Exception Example:
def factorial(n, cache={}): if n < 1: return 1 elif n in cache: return cache[n] else: print('Calculating {0}!'.format(n)) result = n * factorial(n-1) cache[n] = result return result
cache
being a mutable default is fine in this case.
First-Class Functions
First-Class Objects:
Can be passed to a function as an argument.
Can be returned from a function.
Can be assigned to a variable.
Can be stored in a data structure (e.g., list, tuple, dictionary, etc.).
Types such as
int
,float
,string
,tuple
,list
, and many more are first-class objects.Functions (
function
) are also first-class objects.
Higher-Order Functions:
Take a function as an argument and/or
Return a function.
Docstrings and annotations
Docstrings:
To document functions (and modules, classes, etc.), use docstrings.
If the first line in the function body is a string (not an assignment, not a comment, just a string by itself), it will be interpreted as a docstring.
Multi-line docstrings are achieved using multi-line strings (
'''
).Docstrings are stored in the function's
__doc__
property.def func(x): "This is a documentation" # Accessing docstring: func.__doc__ # Output: 'This is a documentation' help(func) # Output: func(x) This is a documentation
Function Annotations:
Metadata attached to the parameters.
Annotations can be any expression.
Annotations are mainly used by external tools and modules, such as documentation generators like Sphinx.
Example:
def my_func(a: str = 1, b: 'int > 0' = 2) -> str: return a * b # Annotations containing functions: x = 3 y = 5 def my_func(a: str) -> 'a repeated ' + str(max(x, y)) + ' times': return a * max(x, y) Accessing annotations: my_func.__annotations__ # Output: {'a': 'info on a', 'b': int, 'return': float}
Docstrings and annotations are entirely optional and do not enforce anything in our Python code.
Lambda expressions
aka anonymous functions
Definition:
Lambda functions are anonymous functions defined using the
lambda
keyword.They are useful for creating small, one-off functions without the need for a formal
def
statement.
Usage:
Lambdas consist of a single expression, which gets evaluated and returned automatically (no need for a return statement).
They can be assigned to a variable or passed as an argument.
Example:
my_func = lambda x: x ** 2 type(my_func) # Output: function my_func(3) # Output: 9 my_func(4) # Output: 16
Limitations:
The body of a lambda is limited to a single expression.
No assignments are allowed inside the lambda body.
No annotations can be used.
However, parameters in a lambda can be assigned default values.
Single logical lines of code are allowed, and line continuation is acceptable, but the lambda must still consist of just one expression.
Lambdas and sorting
sorted()
Function:
Definition:
The
sorted()
function returns a new sorted list from the elements of the given iterable.It takes the iterable as its first positional argument.
Optional keyword arguments include
key
andreverse
.
Basic Usage:
If no
key
function is supplied,sorted()
sorts the elements based on their natural order (e.g., ASCII value for strings).Example:
l = ['B', 'a', 'c', 'D'] sorted(l) # Output: ['B', 'D', 'a', 'c']
Custom Sorting with
key
:The
key
parameter allows custom sorting based on a function applied to each element.Example:
sorted(l, key=lambda s: s.upper()) # Output: ['a', 'B', 'c', 'D']
Usage for Non-Comparable Types:
The
key
function parameter is especially useful for sorting elements that do not support relational operators (e.g., complex numbers).
Preservation of Order:
If two elements in the iterable are equal,
sorted()
retains their original order (stable sort).This means that the order in which equal elements are positioned in the original iterable is preserved in the sorted list.
Function introspection
Attaching Attributes to Functions:
Functions in Python can have attributes attached to them.
Example:
def my_func(a, b): return a + b my_func.category = 'math' my_func.sub_category = 'arithmetic' print(my_func.category) # Output: 'math' print(my_func.sub_category) # Output: 'arithmetic'
Using dir()
Function:
The built-in
dir()
function returns a list of valid attributes for an object.Example attributes for functions:
__name__
: Returns the name of the function.__defaults__
: Returns a tuple containing positional parameter defaults.__kwdefaults__
: Returns a dictionary containing keyword-only parameter defaults.__code__
: Provides information about the function body.__code__.co_varnames
: Returns the names of parameters and local variables.__code__.co_argcount
: Returns the number of positional parameters.
Function vs. Method:
Functions and methods are both objects that can have attributes.
A callable attribute bound to a class or object is called a method.
Using the inspect
Module:
The
inspect
module provides functions for inspecting objects.Examples:
inspect.ismethod(obj)
: Returns True if an object is a method.inspect.isfunction(obj)
: Returns True if an object is a function.inspect.isroutine(obj)
: Returns True if an object is a function or method.inspect.getsource(my_func)
: Returns the entiredef
statement of a function.inspect.getmodule(print)
: Finds the module in which the function was created.inspect.getcomments(my_func)
: Retrieves flagged comments (e.g., with 'TODO:') attached to the function.
Callable Signatures:
The
inspect.signature(my_func)
function returns aSignature
instance.inspect.signature(my_func).parameters
provides metadata about the function parameters, including names, defaults, annotations, and kinds.
Callables
Callables are objects that can be invoked using the ()
operator. Here are the types of callables:
Built-in Functions: Functions provided by Python, such as
print()
orlen()
.Built-in Methods: Methods associated with built-in types or objects, like
str.upper()
orlist.append()
.User-Defined Functions: Functions defined using the
def
statement or lambda expressions.Methods: Functions that are bound to objects, such as methods of classes.
Classes: Classes themselves can be callable. When called, they typically invoke their
__new__()
and__init__()
methods to create and initialize objects.Class Instances: Instances of classes can also be callable if the class implements the
__call__()
method.Generators, Coroutines, and Asynchronous Generators: Special kinds of callables used for generating sequences of values or handling asynchronous operations.
To determine if an object is callable, you can use the built-in function callable()
. It returns True
if the object can be called, otherwise False
.
Map, filter, zip and list comprehensions
Map Function:
map(func, *iterables)
func
is a function that takes as many arguments as there are iterable objects passed toiterables
.Returns an iterator that computes the function applied to each element of the iterables.
Stops when any iterable is exhausted.
Example:
l1 = [1, 2, 3] l2 = [10, 20, 30, 40, 50] list(map(lambda x, y: x + y, l1, l2)) -> [11, 22, 33]
Filter Function:
filter(func, iterable)
func
is a function that takes a single argument.Returns an iterator containing elements of the iterable for which the function call is Truthy.
If
func
isNone
, returns Truthy elements of the iterable.
Zip Function:
zip(*iterables)
Returns a tuple of tuples positionally paired from each iterable.
Not a higher-order function.
Example:
l1 = [1, 2, 3] l2 = [10, 20, 30, 40] l3 = 'python' list(zip(l1, l2, l3)) -> [(1, 10, 'p'), (2, 20, 'y'), (3, 30, 't')]
List Comprehension:
Alternative to
map
andfilter
.[<expression> for <var_name> in <iterable>]
for mapping.[<expression_1> for <var_name> in <iterable> if <expression_2>]
for filtering.Example:
l1 = [1, 2, 3, 4] l2 - [10, 20, 33] [x + y for x, y in zip(l1, l2)] -> [11, 22, 33]
Deferred calculation example:
result = (x**2 for x in range(10) if x**2 < 25)
Reducing functions
Definition: Functions that recombine iterables recursively into a single return value, also known as accumulators or aggregators.
Example: Finding the maximum value in an iterable:
l = [5, 8, 6, 10, 9]
def _reduce(fn, sequence):
result = sequence[0]
for e in sequence[1:]:
result = fn(result, e)
return result
max_func = lambda a, b: a if a > b else b
_reduce(max_func, l) # maximum
functools module: Python's implementation of reduce
function, handling any iterable similarly to the custom function above.
from functools import reduce
l = [5, 8, 6, 10, 9]
max_func = lambda a, b: a if a > b else b
reduce(max_func, l) # max -> 10
Reduce Initializer: A third optional parameter in reduce
, adding a value in front of the iterable. Often used to handle empty iterables.
Example for summation:
reduce(lambda x, y: x+y, l, 0)
returns0
ifl
is empty.Example for multiplication:
reduce(lambda x, y: x*y, l, 1)
returns1
ifl
is empty.
Built-in Reducing Functions:
min
,max
,sum
any
: Uses OR operator, returns bool.all
: Uses AND operator, returns bool.
Partial functions
Reducing Function Arguments:
Using
partial
: Importpartial
fromfunctools
module.from functools import partial def my_func(a, b, c): print(a, b, c) f = partial(my_func, 10) f(20, 30) # Output: 10, 20, 30
partial
function imports the second positional argument as the first argument to the specified function.
Handling Complex Arguments:
from functools import partial
def pow(base, exponent):
return base ** exponent
square = partial(pow, exponent=2)
cube = partial(pow, exponent=3)
square(5) # Output: 25
cube(5) # Output: 125
Beware:
square(5, exponent=3)
would result in125
.When using variables, be cautious as they reference memory addresses, not their values.
The operator module
Arithmetic Functions:
add(a, b)
mul(a, b)
pow(a, b)
mod(a, b)
floordiv(a, b)
neg(a)
Comparison and Boolean Operators:
lt(a, b)
le(a, b)
gt(a, b)
ge(a, b)
eq(a, b)
ne(a, b)
is_(a, b)
is_not(a, b)
and_(a, b)
or_(a, b)
not_(a, b)
Sequence/Mapping Operators:
concat(s1, s2)
contains(s1, s2)
countOf(s1, s2)
getitem(s, i)
setitem(s, i, val)
(mutable objects only)delitem(s, i)
(mutable objects only)
Item Getters:
The
itemgetter
function returns a callable.f = itemgetter(1, 3, 4) s = [1, 2, 3, 4, 5, 6] f(s) # Output: 2, 4, 5
Attribute Getters:
The
attrgetter
function returns a callable that retrieves object attributes.my_obj.a = 10 my_obj.b = 20 my_obj.c = 30 f = attrgetter('a') f(my_obj) # Output: 10
Calling Another Callable:
attrgetter('upper')('python')
returns the upper method ofs
.To call the method:
attrgetter('upper')('python')()
returns'PYTHON'
.methodcaller('upper')('python')
returns'PYTHON'
.It can handle multiple arguments.