forked from darshanime/notes
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpro_python.org
More file actions
1945 lines (1388 loc) · 64.1 KB
/
Copy pathpro_python.org
File metadata and controls
1945 lines (1388 loc) · 64.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
* Pro Python
** Marty Alchin
* Principles and Philosophy
- *Beautiful is better than ugly*
this is for using "is not" and not != etc
- Explicit is better than implicit
each variable must explicitly declared, each import must be individually imported
- Simple is better than complex
if value:
// code
better than
if value is not None:
// code
- Complex is better than complicated
complex - made up of many interconnected parts
complicated - so complex that it is difficult to understand
- Flat is better than nested
- sparse is better than dense
use blank lines to separate relevant parts of code
- readability counts
- special cases aren't special enough to break the rules
- Although practicality beats purity
- Errors should never pass silently
- Unless explicitly silenced
- In the face of ambiguity, refuse the temptation to guess
Errors - problems with code, more severe than exceptions
Exceptions - potential problems with code, less severe than errors
- the StopIteration exception is not a problem with code, it is just there to enhance the code flow
raise exception like this:
raise ValueError("Username must being with underscore")
catch exception like this:
#+begin_src python
try:
validate("username")
except ValueError, TypeError as e:
print(e)
#+end_src
- There should be only one way to do it
- Now is better than never
- Namespaces are one honking great idea - let's do more of those!
functions+classes go in module
modules go in packages
- Loose coupling
This is about splitting the project into different independent segments. By independent, we do not mean that they don't talk to each other. instead it is about how much each subsystem relies on the other subsystems to work. It should be able to work independently, this way:
- any bug in one subsystem won't propagate to other subsystems
- each subsystem can be developed independently
- each subsystem can be used in other projects as well
- Robustness principle
- be conservative in what you do, liberal in what you accept
- your functions should accept various inputs because the input can be malformed etc
- but you should stick to the rule (the specification) and in that way - programs that don't follow the specifications can interact with your program, and you can interact with them as well
* Advanced basics
** Iteration
- the different types of sequences in Python can be used in 2 ways - those that actually use the sequence as a whole and those that just need the items within it
- using the whole sequence - randomly accessing the data and modifying it.
- needing the items - printing them to the console eg
For some sequences, we don't need all the numbers in memory as we don't need to operate on them all at once.
eg: fibonacci sequence etc.
instead of storing the whole sequence in memory, we can calculate the next sequence on the fly by using a few state variables.
Summary: a sequence of items can be looked on as: a collection of items or a way to access a single item at a time
the former needs the entire sequence to be loaded in memory, while the latter needs just a few state variables from which the next sequence can be calculated.
Iteration refers to the 2nd option - each item at a time
eg: range(5)
to load the entire thing in memory - list(range(5))
*iterators (eg range) can be iterated thru many times* - they are special
eg:
#+begin_src python
a = range(10)
b = list(a) # here, a will be iterated thru once
for i in a: # here, it will be iterated thru again
print(i)
#+end_src
*you can write your own iterator objects*
** Transparency
Python doesn't support the notion of private variables in the typical manner, so all attributes are accessible to any object that requests them.
** Exceptions
we can do this:
#+begin_src python
try:
return len(open(filename, 'r').readlines())
except: # this will catch all the exceptions
logger.log("some error")
try:
return len(open(filename, 'r').readlines())
except TypeError as e: # this will catch only typeerror
logger.log("some error")
try:
return len(open(filename, 'r').readlines())
except (TypeError,EnvironmentError) as e : # this will catch both the exceptions as e
logger.log("some error", e)
try:
return len(open(filename, 'r').readlines())
except TypeError: # this will catch TypeError
logger.log("some typeerror")
except EnvironmentError: # this will catch TypeError
logger.log("some env error")
#+end_src
*Exception chains*
- when you are in the except phase, if there is an error, it will lead to an implicit chain of exceptions, because the exceptions are linked only by how they're encountered during execution.
eg:
#+begin_src python
try:
return len(open(filename, 'r').readlines())
except: # this will catch all the exceptions
log = open("logfile.txt", 'w')
log.write("some log")
log.close()
#+end_src
here, if the logfile.txt is a read only file, (the process doesn't have write permission), there will be an exception caused.
output:
#+begin_src python
<old exception>
During handling of the above exception, another exception occured
<new exception>
#+end_src
Explicit chain - when you raise the exception yourself
#+begin_src python
def validator(value):
if len(value)>10:
raise ValueError("cannot be more than 10")
def validate(value, validator):
try:
return validator(value)
except Exception as e:
raise ValueError("error") from e # note the new syntax, from e
#+end_src
You get this:
<old exception>
The above exception was the direct cause of the following exception:
<new exception>
*Like in Java, we have `else` which runs only when there is no exception*, we have that here as well
#+begin_src python
try:
len_ = len(open(filename, 'r').readlines())
except: # this will catch all the exceptions
logger.log("some error")
else:
logger.log("no error")
#+end_src
*Like in Java, we have `finally` which runs after the try/except/else clause*
#+begin_src python
try:
len_ = len(open(filename, 'r').readlines())
except: # this will catch all the exceptions
logger.log("some error")
else:
logger.log("no error")
finally: # always gets executed
logger.log("we are done with this now")
#+end_src
#+RESULTS:
Summary:
#+begin_src python
try:
# something
except Exception as e:
# if errors
else:
# if no errors
finally:
# always
#+end_src
** Looping
Python has while -
#+begin_src python
while <something>:
# code
# it also has do while
while True: # this is optimized as the interpreter won't need to check the value of conditional, and will run the code till it is intererrupted
# code # before Py3000, True/False weren't reserved keywords and so the interpreter needed to check conditional, so use while 1 for performance with legacy code
#+end_src
** context managers
Python2.6 and up have a context manager that eases the exception handling and the cleanup actions. to open a file:
#+begin_src python
with open('log.txt', 'r') as file:
return len(file.readlines())
#+end_src
here, the context manager knows how to handle the exceptions and perform the clearup actions for "open"
To use with clause in Python versions before 2.6, use this:
from __future__ import with_statement
*you can write your own context managers*
** Sequence unpacking
#+begin_src python
one, two = "one.two".split(".") # this works fine
one, two = "one.two.three".split(".") # too many values to unpack - ValueError
one, two, *more = "one.two.three.four.five".split(".") # the astric means more will be a list with the remaining entries
one --> "one"
two --> "two"
more --> ["three", "four", "five"]
#+end_src
** List comprehensions
When you have a sequence with more items that you really need, you can generate a new list and add the items that pass a test, or a modified version of each item using *list comprehension*
consider this:
#+begin_src python
min([value for value in range(10) if value > 5])
#+end_src
here, we make the entire new list and then throw it away!
we did not really have to reserve the space in the memory for this use case, we could have done it lazily
this is done by *generators*.
they lazily generate the elements of the list.
use the paranthesis to get a generator
#+begin_src python
gen = (value for value in range(10) if value>5)
gen
# <generator object <genexpr> at 0x...>
min(gen)
6
min(gen)
# Traceback: ValueError: min() arg is an empty sequence
a = (i for i in range(3))
print(list(a))
[0, 1, 2]
print(list(a))
[]
# always remember, generators generate the items only once
#+end_src
So, the generator generates the values as you iterate over it
(which is what min() does)
min() takes in an iterator(something which one can iterate)
the generator iterates once (it generates the values once) and the next time, it just returns an empty list
this is unlike range which is also a generator, but it can be traversed several times
so, it is upto the iterable itself (weather a generator or a normal list) to determine when and how the sequence gets reset
Set comprehensions
#+begin_src python
{str(value) for value in range(100) if value%2} # this will create a set of odd numbers till 100
#+end_src
:top: is in Python3000
earlier:
#+begin_src python
set(value for value in range(100) if value%2)
#+end_src
Dict comprehensions
#+begin_src python
# Py3
{value: str(value) for value in range(10) if value>5}
# Py2
dict((value, str(value) for value in range(10) if value>5))
#+end_src
*Chaining iterables together*:
itertools has the chain() function that takes in a number of iterables and returns a new generator that iterates thru them - one after the other
#+begin_src python
import itertools
list(itertools.chain(range(3), range(4), range(5)))
[0, 1, 2, 0, 1, 2, 3, 0, 1, 2, 3, 4] # len of list - len(1st iterator) + len(2nd iterator) + len(3rd iterator) etc
#+end_src
*Zipping iterables together*: - zip is std lib top level, don't need to import anything to use it
to iterate thru some iterables together:
#+begin_src python
list(zip(range(3), reverse(range(5))))
[(0,4), (1,3), (2,2)] # len of list - min(len(1st iterator), len(2nd iterator))
# can be also passed to dict
dict(zip(range(3), reverse(range(5))))
{0:4, 1:3, 2:2}
#+end_src
Sets are stored as HashSet in java, i.e. in a hash table.
sets don't have append() because *append is to add to the end*, and sets are unordered
so, we have add()
they also have update(<set>) to add the entire set to the old one
also, is present the remove() method
# remove will raise an error if item not present, discard won't
- pop() and clear() to remove one items and remove all items etc
- union(<set>), interaction, symmetric_difference
- set.issuperset(<set>), set.issubset(<set>)
empty set - set()
empty dicts - {}
empty lists - []
empty tuples - ()
Named tuples - *to maintain a fixed set of possible keys value pairs.*
- it gives you a dict with fixed keys which you can assign values to
eg:
#+begin_src python
from collections import namedtuple
Point = nampedtuple("point", "x y") # 1st arg - name of class, 2nd arg - args(keys) of that class
p = Point(13, 15)
p
# Point(x=13, y=15)
point.x
# 13
point[1]
# 15
#+end_src
Defaultdict - from collections import defaultdict
using defaultdict(int) will initialize each value to 0, so you do can dict_[key]+=1
int -> 0
str -> ""
list -> []
*any callable can be used* - int() gives 0, str() gives '', list() gives []
eg:
#+begin_src python
In [18]: def foo():
...: return []
...:
In [19]: b = defaultdict(foo)
In [20]: b
Out[20]: defaultdict(<function __main__.foo>, {})
In [21]: b['a'].append(2)
In [22]: b
Out[22]: defaultdict(<function __main__.foo>, {'a': [2]
#+end_src
Using __all__ to customize imports
#+begin_src python
from itertools import *
list(chain([1, 2, 3], [3, 4, 5]))
[1, 2, 3, 3, 4, 5]
#+end_src
Here, when you do import *, the namespace of itertools gets dumped in the present one
this means that all the functions, classes, variables etc which don't begin an underscore are imported
but, you as the module builder can control this by using the __all__ variable to define a list of things that will be imported when someone does from foo import *
eg: __all__ = ['func_one', 'class_one']
now, only these 2 entries will be imported on from foo import *
you can still use import foo
and access the other funs by foo.new_fn etc
or import new_fn explicitly - from foo import new_fn
you should not use * to import generally, this would make it difficult to see where the module came from
however, you should can use * for cases when you are wrapping it in another namespace - i.e. you want users to import a single namespace which will give you everything, you can do this.
*relattive imports*
for eg: if the *acme.shopping.cart* module needs to import from *acme.billing*, we can use:
from acme import billing # absolute import
from .. import billing # relative import
. refers to current module - shopping
.. - refers to acme module
*relative imports cannot work in the interactive intrepreter, the mode the intrepreter runs isn't actually in the filesystem, so, relative paths don't work*
* Functions
** Args
In python, functions are full - fledged objects that can be passed around in data structures, wrapped up in other functions or replaced entirely by new implementations
*args -- variable positional arguments
**kwargs - variable keyword arguments
Using kwargs makes for more readable code
# TODO - fix this example
# foo(1, 2, quiet=True)
# 1 and 2 are required arguments
If you want to accept a "list", accept it as *args - args will be a _tuple_ here, you can use list.extend(argTuple) etc
If you want to accept a dict, accept it as **kwargs - kwargs will be a dict here
** Different types of arguments
They are:
- required arguments
- optional arguments
- variable positional arguments - *args
- variable keyword arguments - **kwargs
so, what order should we sent the args in so that there is no ambiguity:
def create_element(name, editable=True, *children, **attributes)
but in :top:, we have to supply editable always to send in any children at all
To support this, python *also* allows you to change the order but after *children, you must specify everything with keywords
eg:
#+begin_src python
def join_with_prefix(prefix, *segments, delimtier):
return delimiter.join(prefix + segment for segment in segments)
join_with_prefix('P', 'ro', 'ython', delimiter=' ' )
# here, segments has ('ro', 'ython')
# another example
def join_with_prefix(*segments, delimtier=' ', prefix):
return delimiter.join(prefix + segment for segment in segments)
join_with_prefix('ro', 'ython', prefix='P' )
# here also segments has ('ro', 'ython')
# also, delimtier has a default arg, so we don't need to define it
# everything after *args needs to be keyvalue
#+end_src
** Preloading args
For any function that accepts a plethora of arguments, we can preload some of them and add to them as the function is passed in the code. finally, we can call the function when everything has been defined
There is a similar concept in Functional languages:
*curry* - if a function accepts 3 args, and you call that function with 1 arg, you get back a function that accepts 2 args. you call it with 2 args this time, it will execute
Python's *partial* - partials takes a function and some args and defines those args for that function. it returns a function with those args defined. you can assign the return value a new name and you now have a new function with some other default arg values
#+begin_src python
import os
def load_file(file, base_path='/', mode='rb'):
return open(os.path.join(base_path, file), mode)
f = load_file('example.txt')
f.mode
# 'rb'
f.close()
import functools
load_writable = functools.partial(load_file, mode='w') # here, we defined an entire new function load_writable that loads files in writable format
f = load_writable('example.txt')
f.mode
# 'w'
f.close()
#+end_src
*decorator* - passing one function to another to get a new function back
*partials* can be used to customize a more flexible function into something simpler, so that it can be passed into an API that doesn't know how to access that flexibility
To know the full arguments detail of any function, we can use:
#+begin_src python
import inspect
def example(a:int, b=1, *c, d, e=2, **f) -> str:
pass
ans = inspect.getfullargspec(example)
FullArgSpec(args=['a', 'b'], varargs='c', varkw='f', defaults=(1,), kwonlyargs=['d', 'e'], kwonlydefaults={'e': 2}, annotations={'a': <class 'int'>, 'return': <class 'str'>})
#+end_src
It returns a named tuple (a tuple with fixed number of named keys)
so,
#+begin_src python
ans[0]
# ['0', '1']
ans.args
# ['0', '1']
#+end_src
We have information about:
- args, varargs, varkw, defaults, kwonlyargs, kwonlydefaults, annotations
** Decorators
In large projects, one may need to perform some preprocessing (auth check, logging) or some post processing (caching) during a function call
decorators are well suited for that.
we don't need to write the auth check, logging everywhere, we just decorate the function and have the decorator do what is needed
*** Closures
Closures are functions(FN_INNER) that are defined another functions(FN_OUTER). Here, FN_INNER is a closure
The good part is that FN_INNER can use the variables in the namespace of FN_OUTER
eg:
#+begin_src python
def multiply_by(factor):
def multiply(value):
return factor*value
return multiply
times2 = multiply_by(2)
times2(5)
10
times3 = multiply_by(3)
times3(5)
15
#+end_src
Here, the multiply function which is a closure, used the factor arg from it's parent, and did not have it accept it itself
Closures make decorators possible - we can defined nested functions and use the variables to do some custom logic; wrap some extra functionality around the function
#+begin_src python
In [58]: def suppress_errors(func): # decorator
...: def wrapper(*args, **kwargs): # wrapper function
...: ''' some docstring for wrapper'''
...: try:
...: return func(*args, **kwargs)
...: except Exception:
...: pass
...: return wrapper
...:
...: @suppress_errors # this is just foo = supress_errors(foo)
...: def foo(): # function being wrapped
...: ''' some docstring for foo'''
...: raise ValueError
...:
In [59]: foo.__doc__
Out[59]: ' some docstring for wrapper'
In [60]: foo.__name__
Out[60]: 'wrapper'
#+end_src
Wrapping a function means some potentially useful information is lost; it's name, docstring, argument list etc
to preserve that information, we use:
#+begin_src python
import functools
def suppress_error(func): # decorator
@functools.wraps(func) # the functools decorator copies the name, docstring and some other info over to the wrapped function. cannot copy over the arg list however
def wrapper(*args, **kwargs): # wrapper function
try:
return func(*args, **kwargs)
except Exception:
pass
return wrapper
@suppress_errors # this is just foo = supress_errors(foo)
def foo(): # function being wrapped
raise ValueError
foo.__name__
'foo'
foo.__doc__
'some docstring for foo'
#+end_src
Note, the functools.wraps decorator takes a argument - func
it needs func to copy the information from
*** Decorators with args
Python actually evaluates the decorator statement as an expression
so:
@supress_errors
is evaluated. Here, it is just a function, so it is simple.
But in
@function.wraps(func)
def wrapper(func)
we get:
function.wraps(func)(wrapper)
what the function.wraps(func) returns, the wrapper function is passed to that; what the function.wraps(func) returns, is used as the decorator
Note, using this, we can write a decorator to churn out decorators.
we have to have another layer of nested function in the decorator maker.
The new outermost function accepts all arguments for the decorator and returns a new function *as a closure* over the argument variables - this just means the new function can use the argument variables
The decorator that takes an argument is generally a decorator that does different things based on the argument it receives, like the function.wraps(func) decorator.
example decorator with args:
#+begin_src python
def suppress_errors(log_func=None)
def decorator(func): # decorator
@functools.wraps(func)
def wrapper(*args, **kwargs): # wrapper function
try:
return func(*args, **kwargs)
except Exception:
pass
return wrapper
return decorator
@suppress_errors(log_func=myLogger) # this line is evaluated to return the decorator which will take foo as the arg
def foo(): # function being wrapped
raise ValueError
#+end_src
But in this scheme :top:, the arguments are compulsary, or we have to provide @suppress_errors() at least
We need to have a decorator that takes an optional argument - we would be able to use it without the paranthesis and with the paranthesis both
IE the outermost function must be able to accept both arbitary arguments *or* a single function, and behave accordingly.
The problem is deciding which flow is indented, based on the args provided
- what if, if the first arg is a function, it is the function flow, else, the arguments flow -- this would not work with functools.wraps(func) which takes a function as the first argument
- we know that the decorators always receive the function it operates on as a positional argument. we can use this and the constraint that any argument must be provided as a keyword argument
- this has the added advantage that the keyword arguments are more readable anyway
- we can implement this by providing the func as the first argument and all the other arguments as keyword args - all need default values
#+begin_src python
def suppress_errors(func=None, log_func=None):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except Exception as e:
if log_func is not None:
log_func(str(e))
return wrapper
if func is None:
return decorator # here, no func provided, the arugmets must have been provided, we just provide the decorator eg: suppress_errors(log_func=Mylogger) on the def foo()
else:
decorator(func) # here, the func is provided, so, no arguments provided, eg: @suppress_errors on def foo()
#+end_src
Always provide the arguments to the decorator as keyword args *if* you want to use it both with args sometimes and without args sometimes
functools.wraps(func) doesn't need keyword args because it always needs the first arg as func
*** Example: Memoization
This is what makes graph search simple, this makes DP possible
#+begin_src python
In [90]: def memoize(func):
...: vals = {}
...: @functools.wraps(func)
...: def wrapper(n):
...: if n not in vals.keys():
...: vals[n] = func(n)
...: return vals[n]
...: return wrapper
...:
...: @memoize
...: def f_num(n):
...: if n <= 0:
...: return 1
...: else:
...: return f_num(n-1) + f_num(n-2)
...:
In [91]: f_num(37)
Out[91]: 63245986
In [92]: f_num(370)
Out[92]: 247694960571651628711444594884429646292615632415916575771902992555242690154864
#+end_src
*** Decorator to create decorators
Notice the boilerplate involved around the decorators
This can be a pain if we create a lot of decorators that more or less do similar things.
we can put this boiler plate off into a decorator of it's own
Like so:
#+begin_src python
# TODO
#+end_src
** Function Annotations
Static typed languages - like Java - they provide what type of arguments are accepted(what types of values are acceptable for each argument) and what is is the type of returned value.
Python's response to that is function annotations
you can do this:
#+begin_src python
def add(a: int, b: int) -> int:
return a+b
#+end_src
:top: this doesn't mean that the function will take only ints, you can give it floats (or anything else) as well
We can annotate the function with any expression, not just types of class. Eg, we can use strings, or even inline functions (lambdas)
#+begin_src python
def add(a: int, b: int) -> "the sum":
return a+b
#+end_src
** Generators
Generator expressions are useful for lazy evaluation but sometimes we need more fine grained control over the iteration, the items being returned, when the loop terminates etc.
You need a real function that you would use to generate the values. then you would have the ultimate control.
The function uses *yield* to return values. When it returns one value, the control is taken away, then some other function runs and when the generator gets the control back, it starts running from where it left off. It runs again till it hits another *yield* statement.
Ex: an generator to generate f nos
#+begin_src python
def fibonacci(count):
a, b = 0, 1
while (count>0):
c = a+b
yield c
a, b = b, c
count = count - 1
a = fibonacci(5)
list(a)
[0, 1, 1, 2, 3]
list(a)
[] # generators can be iterated only once
list(fibonacci(5))
[0, 1, 1, 2, 3]
list(fibonacci(5)) # here, we are creating a new fibonacci generator everytime
[0, 1, 1, 2, 3]
#+end_src
** Lambdas
They are inline, anonymous functions. They are used to provide keys for sorting etc where defining a new function is an overkill
#+begin_src python
houses.sort(key=lambda h:h.price)
#+end_src
This is like the Comparable interface in Java
lambda: 1
this :top: lambda function takes no args and returns 1 whenever it is called
eg:
a = lambda : 1
a()
1
a()
1
Also, lambda can accept various args:
a = lambda x, y: (y, x) # it can return only 1 thing like a normal function
a(1, 2)
(2, 1)
*The entire body of the lambda function is just the return expression*
** Introspection
Every function has a __name__ attribute that stores it's name
for lambda fns, it is <lambda>
** Modules and packages
In python, functions and classes are placed inside of modules. The modules are a part of a package structure.
All functions and classes have a __module__ attribute, which contains the import location of the module where the code was defined
eg:
#+begin_src python
print(str.__module__) #--> 'builtins'
# in intrepreter, since there is no source file on which we are working, any functions or classes are given the __main__ as the module location
def foo():
return
print(foo.__module__) # __main__
#+end_src
The __name__ gives the name of the module?
eg: import mycroft, mycroft.__name__ --> 'mycroft'
Also present, the __doc__ var
this has the docstring of the function
to see it nicely formatted:
print(fnName.__doc__)
* Classes
The class encapsulates the behavior of an object, while an instance of the class represents the data for the object
Between objects, the data might change but the behavior will be the same
All classes inherit the *object* class.
Always inherit from the object class to get the "new-style" classes. "Old style" classes where removed from Python3, the objects were treated differently from builtins there.
** Inheritance
Python supports the traditional inheritance model
#+begin_src python
class Contact:
name = TextField()
email = EmailAddressField()
phone = PhoneNumberField()
class Person(Contact):
first_name = TextField()
last_name = TextField()
name = ComputedString('%(last_name)s, %(first_name)s')
class Company(Contact):
industry = TextField()
#+end_src
Here, you are making a more specific version of the previous class
** Multiple Inheritance
You can also inherit from multiple classes. This horizontal approach to class inheritance means you are building up a class as a set of components. By taking up the behavior from different classes
Such classes that provide some feature, such support classes are called *mixins*
Ex of a mixin:
#+begin_src python
class NoneAttributes:
def __getattr__(self, name):
return None
#+end_src
The NoneAttributes mixin returns None for when the class which inherits it does not have the attribute. (normally it would throw an error)
The __getattr__() magic method is called only when the attribute requested isn't available on the object, not otherwise.
In the wild, the vertical hierarchy will provide most of the functionality with the mixins throwing in some of the extras as necessary
** Method Resolution Order (MRO)
When accessing the class, with multiple inherited classes/mixins, Python needs to know where to call a requested method from
*Always, the first namespace checked is of the object namespace. Then, the class namespace. Then the parents/mixins etc.*
*** Vertical only
This is simple - if the method is there on the class itself, use that
If not, check it's base class.
All the way to the *object* type
Example:
#+begin_src python
class Book:
# code
class Novel(Book):
# code
class Mystery(Novel):
# code
#+end_src
the MRO for Book - [Book, object]
the MRO for Novel - [Novel, Book, object]
the MRO for Mystery - [Mystery, Novel, Book, object]
*** Horizontal only
Here it is simple as well. Go from left to right
#+begin_src python
class A:
# code
class B:
# code
class C:
# code
class D(A, B, C):
# code
D.mro()
[D, A, B, C, object]
#+end_src
*** Both mixed
Python uses a simple algorithm to tackle situation like this.
It starts from root - *object* class.
At each iteration, it will select a candidate to put in the MRO.
It takes into consideration the class lists of the parent MROs and to combine them, looks at the first candidate in each list to find a "valid candidate"
A valid candidate is the one which which exists in only the first position in any of the MRO lists being considered.
Example:
#+begin_src python
class A:
pass
class B(A):
pass
class C(B, A):
pass
#+end_src
Starting from root - *object*
# here, no parent lists to consider
object -> [object]
# here, we consider [object] - only 1 list to consider, only 1 valid candidate
A -> [A, object]
# here, we have [A, object] - only 1 list to consider, only 1 valid candidate
B -> [B, A, object]
# here, we have 2 lists to consider, [B, A, object] and [A, object]
C -> [C, B, A, object]
Here, if we had defined class C as class C(A, B) it would have resulted in an cannot create consistent MRO TypeError.
This is because B's MRO puts B before A. Thus, any subsequent subclass must also put B before A.
*** super method
In python, the super method returns an object. It takes in 2 params, a class and an instance of that class.
The instance object determines which MRO will be used to resolve any attributes on the returned object -- the instance object's MRO is used?
The class determines a subset of that MRO, super() only uses those entries in the MRO that occur after the class provided -- the class determines the portion of the MRO that is to be used?
Recommended usage - super(ClassName, self)
where ClassName is the name of the class where it is is used