Python Version: 3.9.1 (Clang 12.0.0)
iPython Version: 7.19.0
doc string in a function is shown as part of help of the function output:
>>> def my_func(a, b):...     '''Returns the product of a and b'''...     # comments are not shown as part of help output...     return a * b>>>>>> help(my_func)Help on function my_func in module __main__:my_func(a, b)    Returns the product of a and b>>> # Dunder doc returns documentations of the function>>> my_func.__doc__'Returns the product of a and b'One can add annotations for variables:
>>> def my_func(a: 'please keep this as an integer',...             b: 'please keep this also as an integer'):...             '''this function just...             gives a^b as the output'''...             return a ** b...>>> help(my_func)Help on function my_func in module __main__:my_func(a: 'please keep this as an integer', b: 'please keep this also as an integer')    this function just    gives a^b as the output...>>> my_func.__doc__'this function just\n\t\tgives a^b as the output'Annotation can be any object and not just string:
>>> def my_func(a: str):...     return a * max(x,y)...>>> help(my_func)Help on function my_func in module __main__:my_func(a: str)We can also add annotation for return value (as seen below annotation can be an expresion as well and we can also set the default value for a argument along with annotation):
>>> x, y = 3, 5>>> def my_func(a: str='a') -> '"a" repeated ' + str(max(3, 5)) + ' times':...     return a * max(x,y)...>>> help(my_func)Help on function my_func in module __main__:my_func(a: str = 'a') -> '"a" repeated 5 times'How can one access annotations of the function?
>>> my_func.__annotations__{'a': <class 'str'>, 'return': '"a" repeated 5 times'}Anonymous function as it doesn't have name to it:
>>> def my_func(x):...     return x ** 2...>>> my_func(4)16>>>>>> f = lambda x: x ** 2>>> f(4)16One can also pass default values to args in lambda function:
>>> func_1 = lambda x, y = 10 : (x, y)>>> func_1(1, 2), func_1(10)((1, 2), (10, 10))*args, **kwargs can also be used with lambda functions:
>>> func_2 = lambda x, *args, y, **kwargs: (x, *args, y, {**kwargs})>>> func_2(1, 'a', 'b', y=100, a = 10, b = 24, c = 'python')(1, 'a', 'b', 100, {'a': 10, 'b': 24, 'c': 'python'})>>>>>> def apply_fn(fn, *args, **kwargs):...      return fn(*args, **kwargs)...>>> apply_fn(lambda x, y: x + y, 1, 2)3>>> apply_fn(lambda *args: sum(args), 1, 2, 3, 4, 5, 6, 7, 8)36>>> apply_fn(sum, (1, 2, 3, 4, 5, 6, 7, 8))36Can be used as part of sort function:
xxxxxxxxxx>>> l = ['a', 'B', 'c', 'D']>>> sorted(l)['B', 'D', 'a', 'c']>>> sorted(l, key = str.upper)['a', 'B', 'c', 'D']>>> sorted(l, key = lambda s: s.upper())['a', 'B', 'c', 'D']Sorting dictionaries:
xxxxxxxxxx>>> d = {'def': 300, 'abc': 200, 'ghi': 100, 'jlk': 50}>>> sorted(d)['abc', 'def', 'ghi', 'jlk']>>> sorted(d, key = lambda k: d[k])['jlk', 'ghi', 'abc', 'def']Sorting complex numbers:
xxxxxxxxxx>>> l = [3+3j, 1+1j, 0+0j]>>> sorted(l)Traceback (most recent call last):  File "<stdin>", line 1, in <module>TypeError: '<' not supported between instances of 'complex' and 'complex'>>> sorted(l, key = lambda x: (x.real)**2 + (x.imag)**2)[0j, (1+1j), (3+3j)]Since functions are objects, we can add attributes to it:
xxxxxxxxxx>>> def my_func(a,b):...     return a ** b...>>> my_func.short_desc = "multiple function">>> my_func.short_desc'multiple function'One can use dir() function to get list of attributes of a fuction:
xxxxxxxxxx>>> dir(my_func)['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'short_desc']One can use name attribute to get name of the function:
xxxxxxxxxx>>> def my_func(a, b = 2, c = 3, *, kw1, kw2= 2, **kwargs):...     pass...>>> f = my_func>>> my_func.__name__'my_func'>>> f.__name__'my_func'Can also look at default values:
xxxxxxxxxx>>> my_func.__defaults__(2, 3)One can analyze the function code as well using __code__ object:
xxxxxxxxxx>>> my_func.__code__<code object my_func at 0x109c76500, file "<stdin>", line 1>>>> dir(my_func.__code__)['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'co_argcount', 'co_cellvars', 'co_code', 'co_consts', 'co_filename', 'co_firstlineno', 'co_flags', 'co_freevars', 'co_kwonlyargcount', 'co_lnotab', 'co_name', 'co_names', 'co_nlocals', 'co_posonlyargcount', 'co_stacksize', 'co_varnames', 'replace']>>>>>> my_func.__code__.co_varnames('a', 'b', 'c', 'kw1', 'kw2', 'kwargs')>>> my_func.__code__.co_argcount # doesn't count args/kwargs, only local & global variables3Inspect Module: Can use this module to do much better inspection:
xxxxxxxxxx>>> import inspect>>> inspect.isfunction(my_func)True>>> inspect.ismethod(my_func) # false, since not bounded to a classFalseIn [5]: class MyClass:   ...:     def f_instance(self):   ...:         'Instance methods are bound to the instance/object of the class'   ...:         pass   ...:   ...:     @classmethod   ...:     def f_class(cls):   ...:         'Class method are bound to the CLASS, not instances'   ...:         pass   ...:   ...:     @staticmethod   ...:     def f_static():   ...:         'Statis methods are not bounded to either class or its objects/instances'   ...:         passIn [8]: inspect.isfunction(MyClass.f_instance), inspect.ismethod(MyClass.f_instance)Out[8]: (True, False)In [9]: inspect.isfunction(MyClass.f_class), inspect.ismethod(MyClass.f_class)Out[9]: (False, True)In [10]: inspect.isfunction(MyClass.f_static), inspect.ismethod(MyClass.f_static)Out[10]: (True, False)When a function is bounded to a class, it is method otherwise it is a function:
xxxxxxxxxxIn [12]: inspect.isfunction(my_obj.f_instance), inspect.ismethod(my_obj.f_instance)Out[12]: (False, True)In [13]: inspect.isfunction(MyClass.f_instance), inspect.ismethod(MyClass.f_instance)Out[13]: (True, False)In [14]: inspect.isfunction(my_obj.f_class), inspect.ismethod(my_obj.f_class),Out[14]: (False, True)In [15]: inspect.isfunction(my_obj.f_static), inspect.ismethod(my_obj.f_static)Out[15]: (True, False)One can use routine to check if it is function or method:
xxxxxxxxxxIn [17]: inspect.isroutine(MyClass.f_instance)Out[17]: TrueSource code of a function:
xxxxxxxxxxIn [18]: print(inspect.getsource(MyClass.f_instance))    def f_instance(self):        'Instance methods are bound to the instance/object of the class'        passGet module details:
xxxxxxxxxxIn [21]: import mathIn [22]: inspect.getmodule(math.sin)Out[22]: <module 'math' from '/usr/local/Cellar/python@3.9/3.9.1_2/Frameworks/Python.framework/Versions/3.9/lib/python3.9/lib-dynload/math.cpython-39-darwin.so'>Comments of a code (but cannot get comments inside a func):
xxxxxxxxxxIn [19]: # setting up variable    ...: i = 10    ...: # comment line 1    ...: # comment line 2    ...: def my_func(a, b = 1):    ...:     # comment inside my_func    ...:     pass    ...:In [20]: inspect.getcomments(my_func)    ...:Out[20]: '# comment line 1\n# comment line 2\n'Get signature of a function:
xxxxxxxxxxIn [23]: # TODO: Provide implementation    ...: def my_func(a: 'a string',    ...:             b: int = 1,    ...:             *args: 'additional argument args',    ...:             kw1: 'first keyword only arg',    ...:             kw2: 'second keyword only arg',    ...:             **kwargs: 'additional keyword only args') -> str:    ...:             """does something    ...:             or ther other"""    ...:             pass    ...:In [24]: inspect.signature(my_func)Out[24]: <Signature (a: 'a string', b: int = 1, *args: 'additional argument args', kw1: 'first keyword only arg', kw2: 'second keyword only arg', **kwargs: 'additional keyword only args') -> str>  In [26]: sig = inspect.signature(my_func)    ...:In [27]: for param_name, param in sig.parameters.items():    ...:     print(param_name, param)    ...:a a: 'a string'b b: int = 1args *args: 'additional argument args'kw1 kw1: 'first keyword only arg'kw2 kw2: 'second keyword only arg'kwargs **kwargs: 'additional keyword only args'One can write their own PyCharm like feature to get details of a function:
xxxxxxxxxxIn [28]: def print_into(f: "callable") -> None:    ...:     print(f.__name__)    ...:     print('=' * len(f.__name__), end = '\n\n')    ...:     print(f'{inspect.getcomments(f)}\n{inspect.cleandoc(f.__doc__)}')    ...:     print(f'Inputs\n{"-"*len("Inputs")}')    ...:    ...:     sig = inspect.signature(f)    ...:     for param in sig.parameters.values():    ...:         print("Name:", param.name)    ...:         print("default:", param.default)    ...:         print("annotation:", param.annotation)    ...:         print("kind:", param.kind)    ...:         print('----------------------------\n')    ...:    ...:     print(f'\n\nOutput {"-"*len("Output")}')    ...:     print(sig.return_annotation)    ...:    ...: print_into(my_func)my_func=======# TODO: Provide implementationdoes somethingor ther otherInputs------Name: adefault: <class 'inspect._empty'>annotation: a stringkind: POSITIONAL_OR_KEYWORD----------------------------Name: bdefault: 1annotation: <class 'int'>kind: POSITIONAL_OR_KEYWORD----------------------------Name: argsdefault: <class 'inspect._empty'>annotation: additional argument argskind: VAR_POSITIONAL----------------------------Name: kw1default: <class 'inspect._empty'>annotation: first keyword only argkind: KEYWORD_ONLY----------------------------Name: kw2default: <class 'inspect._empty'>annotation: second keyword only argkind: KEYWORD_ONLY----------------------------Name: kwargsdefault: <class 'inspect._empty'>annotation: additional keyword only argskind: VAR_KEYWORD----------------------------Output ------<class 'str'>For some of the built-in modules, python doesn't allow keyword arguments for positional arguments:
xxxxxxxxxxIn [29]: help(divmod)Help on built-in function divmod in module builtins:divmod(x, y, /)    Return the tuple (x//y, x%y).  Invariant: div*y + mod == x.In [31]: divmod(10, 3)Out[31]: (3, 1)In [32]: divmod(x = 10, y = 3)---------------------------------------------------------------------------TypeError                                 Traceback (most recent call last)<ipython-input-32-aeb448ab4ec4> in <module>----> 1 divmod(x = 10, y = 3)TypeError: divmod() takes no keyword argumentsAll functions & classes are callable:
xxxxxxxxxxIn [33]: callable(print)Out[33]: TrueIn [34]: from decimal import Decimal    ...:    ...: callable(Decimal)Out[34]: TrueClass instances maybe callable, one must define __call__ to make it callable:
xxxxxxxxxxIn [35]: class MyClass:    ...:     def __init__(self):    ...:         print('initialzing ... ')    ...:         self.counter = 0    ...:    ...:     def __call__(self, x = 1):    ...:         self.counter += x    ...:         print(self.counter)    ...:In [36]: my_obj = MyClass()initialzing ...In [37]: callable(my_obj.__call__)Out[37]: TrueIn [39]: callable(my_obj)Out[39]: TrueIn [40]: class MyClass:    ...:     def __init__(self):    ...:         print('initialzing ... ')    ...:         self.counter = 0    ...:    ...: my_obj = MyClass()initialzing ...In [41]: callable(my_obj)Out[41]: FalseA function which takes a function and returns a function is called higher-order functions
xxxxxxxxxxIn [42]: def fact(n):    ...:     return 1 if n < 2 else n * fact(n-1)    ...:In [43]: map(fact, [1, 2, 3, 4, 5]) # -> No computation is done here yet, because it is an iteratorOut[43]: <map at 0x111a1c070>In [44]: list(map(fact, [1, 2, 3, 4, 5])) # -> Computation is done hereOut[44]: [1, 2, 6, 24, 120]In [44]: list(map(fact, [1, 2, 3, 4, 5]))Out[44]: [1, 2, 6, 24, 120]In [45]: l = [0, 1, 1, 0, 1, False, True]    ...: list(filter(None, l))Out[45]: [1, 1, 1, True]In [46]: l = [0, 1, 1, 0, 1, False, True]    ...: list(filter(lambda x: x*5, l))Out[46]: [1, 1, 1, True]In [47]: l = [0, 1, 1, 0, 1, False, True] # [-1, 0, 0, -1, 0, -1, 0]    ...: list(filter(lambda x: x - 1, l))Out[47]: [0, 0, False]In [48]: filter(None, l)Out[48]: <filter at 0x111a507c0> # -> Output is an iteratorAvoid using map & filter as one can achieve the same using comprehensions! So where ever possible use comprehensions
xxxxxxxxxxIn [59]: list(filter(lambda y: y< 25, map(lambda x: x**2, range(10))))Out[59]: [0, 1, 4, 9, 16]In [60]: [x**2 for x in range(10) if x**2 < 25]Out[60]: [0, 1, 4, 9, 16]Iterators are exhaustible:
xxxxxxxxxxIn [49]: whatisthis = (fact(i) for i in l)    ...: for e in whatisthis:    ...:     print(e, end='')    ...:1111111In [50]: for e in whatisthis:    ...:     print(e)But iterables are not exhaustible:
xxxxxxxxxxIn [51]: r = range(10)    ...:    ...: for k in r:    ...:     print(k, end='')    ...:0123456789In [52]: for k in r:    ...:     print(k, end='')    ...:0123456789One can combine multiple lists using zip:
xxxxxxxxxxIn [54]: l1 = 1, 2, 3    ...: l2 = ['a', 'b', 'c', 'd']    ...: l3 = (1, 2, 3, 4, 5, 6, 7, 8)    ...: list(zip(l1, l2, l3))Out[54]: [(1, 'a', 1), (2, 'b', 2), (3, 'c', 3)]In [55]: l1 = range(100000000000)    ...: l2 = 'python'    ...:    ...: list(zip(l1, l2))Out[55]: [(0, 'p'), (1, 'y'), (2, 't'), (3, 'h'), (4, 'o'), (5, 'n')]In [58]: l1 = [1, 2, 3, 4, 5]    ...: l2 = [10, 20, 30, 40, 50]    ...:    ...: [z1+ z2 for z1, z2 in zip(l1, l2)]Out[58]: [11, 22, 33, 44, 55]Can use reduce function to perform cumulative calculation on iterables:
xxxxxxxxxxIn [64]: l = [1, 2, 3, 4, 5]In [66]: from functools import reduceIn [67]: reduce(lambda a, b: a if a > b else b, l)Out[67]: 5any can be used to check if any of the value is true:
xxxxxxxxxxIn [68]: l = [0, 0.01, False]    ...: any(l)Out[68]: TrueIn [69]: reduce(lambda a, b: bool(a or b), l) # our implementation of anyOut[69]: Trueall can be used to check if all of the values are true:
xxxxxxxxxxIn [70]: l = [True, 0.01, -1231231, None]    ...: all(l)Out[70]: FalseIn [71]: reduce(lambda a, b: bool(a and b), l) # our implementation of allOut[71]: FalseOne can sett initializer for reduce function:
xxxxxxxxxxIn [74]: l = []    ...:    ...: reduce(lambda x, y: x*y, l)---------------------------------------------------------------------------TypeError                                 Traceback (most recent call last)<ipython-input-74-fa376e086deb> in <module>      1 l = []      2----> 3 reduce(lambda x, y: x*y, l)TypeError: reduce() of empty sequence with no initial valueIn [75]: reduce(lambda x, y: x*y, l, 1)Out[75]: 1A function can be initalized with partial values:
xxxxxxxxxxIn [76]: from functools import partialIn [77]: def my_func(a, b, c):    ...:     print(a, b*b, c)    ...:    ...: f = partial(my_func, 10)    ...:    ...: f(4, 20)10 16 20Same can be achieved with lambda functions:
xxxxxxxxxxIn [79]: fn = lambda b, c: my_func(10, b, c)    ...:    ...: fn(4, 20)10 16 20Partial functions allows us to simplify our function, give new variants to it:
xxxxxxxxxxIn [80]: def my_func(a, b, *args, k1, k2, **kwargs):    ...:     print(a, b, args, k1, k2, kwargs)    ...: f = partial(my_func, 10, k1='a')    ...: f(20, 30, 40, 50, k2 = 'rohan', l=123, o=12312)10 20 (30, 40, 50) a rohan {'l': 123, 'o': 12312}In [81]: def power(base, exponent):    ...:     return base ** exponent    ...: square = partial(power, exponent=2)    ...: cube = partial(power, exponent=3)In [82]: square(4)Out[82]: 16In [83]: cube(4)Out[83]: 64In [84]: cube(base=4)Out[84]: 64In [85]: cube(base=4,exponent=2) # NOTE: one can still change arg valuesOut[85]: 16Another example to sort the coordinates based on dist from origin:
xxxxxxxxxxIn [86]: origin = (0, 0)    ...: l = [(1, 1), (0, 2), (-3, 2), (5, 4), (0, 0)]In [87]: dist2 = lambda x, y: (x[0] - y[0])**2 + (x[1] - y[1])**2    ...:In [88]: dist_from_origin = partial(dist2, (0, 0))In [89]: sorted(l, key=dist_from_origin)Out[89]: [(0, 0), (1, 1), (0, 2), (-3, 2), (5, 4)]Module to perform operations:
xxxxxxxxxxIn [92]: import operatorIn [93]: operator.add(1,2)Out[93]: 3In [94]: operator.floordiv(13, 2)Out[94]: 6In [95]: operator.truediv(13, 2)Out[95]: 6.5In [97]: operator.truth([1, 2])Out[97]: TrueIn [98]: operator.truth([])Out[98]: FalseIn [99]: operator.le(10, 100)Out[99]: TrueBecomes handy with reduce function
xxxxxxxxxxIn [96]: reduce(operator.mul, [1, 2, 3, 4,5 ])Out[96]: 120Operator does simple operators as well, but they handle lots of cases and also generic inputs:
xxxxxxxxxxIn [100]: my_list = [1, 2, 3, 4,5 ]In [101]: my_list[4]Out[101]: 5In [102]: operator.getitem(my_list, 4)Out[102]: 5
We can use it with Class as well:
In [104]: class MyClass:     ...:     def __init__(self):     ...:         self.a = 10     ...:         self.b = 20     ...:         self.c = 30     ...:     ...:     def test(self):     ...:         print('test method running...')     ...:In [105]: obj = MyClass()     ...:     ...: obj.a, obj.b, obj.cOut[105]: (10, 20, 30)In [106]: f = operator.attrgetter('a')     ...:     ...: f(obj)Out[106]: 10In [107]: obj2 = MyClass()     ...: obj2.a = 100     ...:     ...: f(obj), f(obj2)Out[107]: (10, 100)