├── .gitignore ├── Chapter01 ├── C01R01_AnnotatingFunctions.py ├── C01R02_ListComprehensions.py ├── C01R03_DictComprehensions.py ├── C01R04_Generators.py ├── C01R05_DecoratorAnatomy.py ├── C01R06_DecoratorWithArguments.py └── README.md ├── Chapter02 ├── C02R01_BasicClass.py ├── C02R02_NonPublicMembers.py ├── C02R04_PropertiesviaDecorator.py ├── C02R05_PropertiesviaFunction.py ├── C02R06_PassingStateinConstruction.py ├── C02R07_CreatingClassAttributes.py ├── C02R08_CreatingClassMethods.py ├── C02R09_CreatingStaticMethods.py ├── C02R10_BasicDataclassDefinition.py ├── README.md ├── descriptor-play.py └── requirements.txt ├── Chapter03 ├── C03R01_PersonStudent.py ├── C03R02_MethodOverriding.py ├── C03R03_MultipleInheritance.py ├── C03R04_CreatingInterfaces.py ├── C03R05_EncapulateWhatVaries.py ├── README.md └── asset_files │ ├── dna_of_candy.pdf │ ├── dna_of_candy.pptx │ ├── example.odp │ └── jellybellies.jpg ├── Chapter04 ├── C04R01_MetaclassBasics.py ├── C04R02_ClassRequirementMetaclass.py ├── C04R03_SubclassRegistrationMetaclass.py ├── C04R04_EmulatingNumericType.py ├── C04R05_EmulatingStringType.py ├── C04R06_ExtendingNumericType.py ├── C04R07_TypedCollections.py ├── C04R08_CustomExceptions.py └── README.md ├── Chapter05 ├── C05R01_SimpleStacks.py ├── C05R02_QueuesListsDeque.py ├── C05R03_LinkedLists.py ├── C05R04_TreesInPython.py ├── C05R05_PythonOfficialEnums.py └── README.md ├── Chapter06 └── README.md ├── Chapter07 └── README.md ├── Chapter08 └── README.md ├── Chapter09 └── README.md ├── Chapter10 └── README.md ├── LICENSE ├── README.md ├── VirtualEnvironmentNotes.md └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | *.pyc 3 | -------------------------------------------------------------------------------- /Chapter01/C01R01_AnnotatingFunctions.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | The complete code of Ch. 1, Recipe 1 -- Annotating Functions 4 | """ 5 | 6 | def print_line( 7 | leader:str, line:(str,None)=None, 8 | indent:int=0, body_start:int=28 9 | ) -> str: 10 | """ 11 | Prints a formatted line of text, with a dot-leader between the 12 | lead and the line supplied 13 | """ 14 | output = '' 15 | if indent: 16 | output = ' '*(3*(indent-1)) + '+- ' 17 | output += leader 18 | if line: 19 | output = ( 20 | (output + ' ').ljust(body_start - 1, '.') 21 | + ' ' + line 22 | ) 23 | print(output) 24 | 25 | def all_arg_types( 26 | arg:str, 27 | arg2:(bool,None)=None, 28 | *args:(int,float), 29 | kwdonly:str, 30 | kwdonly2:(bool,None)=None, 31 | **kwargs:type 32 | ) -> (str,None): 33 | print_line('all_arg_types called:') 34 | print_line('arg', str(arg), 1) 35 | print_line('arg2', str(arg2), 1) 36 | print_line('args', str(args), 1) 37 | print_line('kwdonly', str(kwdonly), 1) 38 | print_line('kwdonly2', str(kwdonly2), 1) 39 | print_line('kwargs:', None, 1) 40 | for kwdarg in kwargs: 41 | print_line(kwdarg, kwargs[kwdarg], 2) 42 | 43 | if __name__ == '__main__': 44 | import sys 45 | # - Call the all_arg_types function 46 | all_arg_types( 47 | 'first arg', True, 3.14, -1, kwdonly='kwdonly', 48 | kwdonly2=None, key1='value1', key2='value2' 49 | ) 50 | # - Examine the all_arg_types function 51 | print(all_arg_types) 52 | print_line('__annotations__:') 53 | for item in all_arg_types.__annotations__: 54 | print_line(item, 55 | str(all_arg_types.__annotations__[item]), 1 56 | ) 57 | # - Import getfullargspec from the inspect module 58 | from inspect import getfullargspec 59 | # - Get the full arg-spec of the annotated function 60 | argspec = getfullargspec(all_arg_types) 61 | print_line('fullargspec(%s):' % all_arg_types) 62 | print_line('varargs', str(argspec.varargs), 1) 63 | print_line('varkw', str(argspec.varkw), 1) 64 | print_line('defaults', str(argspec.defaults), 1) 65 | print_line('kwonlyargs', str(argspec.kwonlyargs), 1) 66 | print_line('kwonlydefaults', str(argspec.kwonlydefaults), 1) 67 | print_line('annotations', None, 1) 68 | for item in argspec.annotations: 69 | print_line(item, 70 | str(argspec.annotations[item]), 2 71 | ) 72 | # ~ # - Examine the print_line function 73 | print(print_line) 74 | print_line('__annotations__:') 75 | for item in print_line.__annotations__: 76 | print_line(item, 77 | str(print_line.__annotations__[item]), 1 78 | ) 79 | argspec = getfullargspec(print_line) 80 | print_line('fullargspec(%s):' % all_arg_types) 81 | print_line('varargs', str(argspec.varargs), 1) 82 | print_line('varkw', str(argspec.varkw), 1) 83 | print_line('defaults', str(argspec.defaults), 1) 84 | print_line('kwonlyargs', str(argspec.kwonlyargs), 1) 85 | print_line('kwonlydefaults', str(argspec.kwonlydefaults), 1) 86 | print_line('annotations', None, 1) 87 | for item in argspec.annotations: 88 | print_line(item, 89 | str(argspec.annotations[item]), 2 90 | ) 91 | 92 | -------------------------------------------------------------------------------- /Chapter01/C01R02_ListComprehensions.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | The complete code of Ch. 1, Recipe 2 -- List Comprehensions 4 | """ 5 | 6 | date_list = [ 7 | datetime(1970, 1, 1), 8 | datetime(2038, 1, 19), 9 | datetime(1994, 1, 26), 10 | datetime(1998, 11, 25), 11 | datetime(1966, 9, 8), 12 | datetime(1969, 6, 3), 13 | datetime(1987, 9, 28), 14 | datetime(1994, 5, 23), 15 | datetime(1969, 7, 20), 16 | datetime(1972, 12, 11), 17 | ] 18 | 19 | pangram = 'sphinx of black quartz judge my vow' 20 | sentence = 'this is just a sentence' 21 | 22 | if __name__ == '__main__': 23 | 24 | ### BASIC examples 25 | # A list comprehension can be built from any iterable: 26 | # - All the items in an existing list, for example, though 27 | # that's not likely to be of much use, since it just 28 | # results in a copy of the original list... 29 | all_dates = [d for d in date_list] 30 | print(all_dates == date_list) 31 | # - All the characters in a string -- strings are iterables 32 | # as well: 33 | pangram_chars = [c for c in pangram] 34 | sentence_chars = [c for c in sentence] 35 | print(pangram_chars) 36 | print(sentence_chars) 37 | 38 | ### MAPPING examples 39 | # - Examples of applying a function to each element of a 40 | # list, transforming it in the process, and returning a 41 | # new list of values with a list comprehension. This is 42 | # a mapping, equivalent to calling 43 | # map(lambda d: d.strftime(format), date_list) 44 | ymd_date_list = [ 45 | date.strftime(ymd_format) 46 | for date in date_list 47 | ] 48 | print(ymd_date_list) 49 | 50 | dmy_date_list = [ 51 | date.strftime(dmy_format) 52 | for date in date_list 53 | ] 54 | print(dmy_date_list) 55 | 56 | # - Any sequence-type (lists, tuples, strings, sets, etc.) 57 | # can be used as the "in" value: 58 | pangram_chars = [hex(ord(c)) for c in pangram] 59 | print(pangram_chars) 60 | 61 | sentence_chars = [c for c in sentence] 62 | print(sentence_chars) 63 | 64 | ### FILTERING examples 65 | # - Examples of applying criteria to each element as it's 66 | # being evaluated for inclusion in the new list generated 67 | # by the comprehension. This is filtering, equivalent to 68 | # calling 69 | # filter(lambda d: d.year>=1990 and d.year<2000, date_list) 70 | dates_1990s = [ 71 | d for d in date_list 72 | if d.year>=1990 and d.year<2000 73 | ] 74 | print(dates_1990s) 75 | 76 | dates_1960s = [ 77 | d for d in date_list 78 | if d.year>=1960 and d.year<1970 79 | ] 80 | print(dates_1960s) 81 | 82 | dates_1960s_90s = [ 83 | d for d in date_list 84 | if (d.year>=1990 and d.year<2000) 85 | or (d.year>=1960 and d.year<1970) 86 | ] 87 | print(dates_1960s_90s) 88 | 89 | ### COMBINATION of MAPPING and FILTERING 90 | # - map AND filter equivalent: 91 | # map( 92 | # lambda m: m.strftime(ymd_format), 93 | # filter( 94 | # lambda d: d.year>=1990 and d.year<2000, 95 | # date_list 96 | # ) 97 | # ) 98 | ymd_dates_1990s = [ 99 | date.strftime(ymd_format) 100 | for date in date_list 101 | if date.year>=1990 and date.year<2000 102 | ] 103 | print(ymd_dates_1990s) 104 | 105 | dmy_dates_1960s = [ 106 | date.strftime(dmy_format) 107 | for date in date_list 108 | if date.year>=1960 and date.year<1970 109 | ] 110 | print(dmy_dates_1990s) 111 | 112 | -------------------------------------------------------------------------------- /Chapter01/C01R03_DictComprehensions.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | The complete code of Ch. 1, Recipe 3 -- Dictionary Comprehensions 4 | """ 5 | 6 | from datetime import datetime 7 | 8 | ymd_format = '%Y-%m-%d' 9 | dmy_format = '%d %b %Y' 10 | 11 | events = { 12 | 'UNIX Epoch Start': datetime(1970, 1, 1), 13 | 'UNIX Epoch End': datetime(2038, 1, 19), 14 | 'B5 First Episode':datetime(1994, 1, 26), 15 | 'B5 Last Episode':datetime(1998, 11, 25), 16 | 'ST:TOS First Episode':datetime(1966, 9, 8), 17 | 'ST:TOS Last Episode':datetime(1969, 6, 3), 18 | 'ST:TNG First Episode':datetime(1987, 9, 28), 19 | 'ST:TNG Last Episode':datetime(1994, 5, 23), 20 | 'First Moon Landing':datetime(1969, 7, 20), 21 | 'Last Moon Landing':datetime(1972, 12, 11), 22 | } 23 | 24 | if __name__ == '__main__': 25 | 26 | from pprint import pprint 27 | 28 | ### MAPPING examples 29 | # - Examples of applying a function to each element of a 30 | # list, transforming it in the process, and returning a 31 | # new dict of values with a dict comprehension. 32 | ymd_date_dict = { 33 | key:events[key].strftime(ymd_format) 34 | for key in events 35 | } 36 | pprint(ymd_date_dict) 37 | 38 | dmy_date_dict = { 39 | key:events[key].strftime(dmy_format) 40 | for key in events 41 | } 42 | pprint(dmy_date_dict) 43 | 44 | ### FILTERING examples 45 | # - Examples of applying criteria to each element as it's 46 | # being evaluated for inclusion in the new dict generated 47 | # by the comprehension. 48 | sf_dates_dict = { 49 | key:events[key] 50 | for key in events 51 | if key.startswith('ST:') or key.startswith('B5') 52 | } 53 | pprint(sf_dates_dict) 54 | 55 | ### COMBINATION of MAPPING and FILTERING 56 | sf_dates_dict = { 57 | key:events[key].strftime(ymd_format) 58 | for key in events 59 | if key.startswith('ST:') or key.startswith('B5') 60 | } 61 | pprint(sf_dates_dict) 62 | -------------------------------------------------------------------------------- /Chapter01/C01R04_Generators.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | The complete code of Ch. 1, Recipe 4 -- Generators 4 | """ 5 | 6 | import math 7 | 8 | from datetime import datetime 9 | 10 | ymd_format = '%Y-%m-%d' 11 | dmy_format = '%d %b %Y' 12 | 13 | date_list = [ 14 | datetime(1970, 1, 1), 15 | datetime(2038, 1, 19), 16 | datetime(1994, 1, 26), 17 | datetime(1998, 11, 25), 18 | datetime(1966, 9, 8), 19 | datetime(1969, 6, 3), 20 | datetime(1987, 9, 28), 21 | datetime(1994, 5, 23), 22 | datetime(1969, 7, 20), 23 | datetime(1972, 12, 11), 24 | ] 25 | 26 | if __name__ == '__main__': 27 | # - Create the date-list generator 28 | all_dates = ( 29 | d.strftime(ymd_format) for d in date_list 30 | if d.year>=1990 and d.year<2000 31 | ) 32 | # - Print it to show that it's not a list, tuple, etc. 33 | print(all_dates) 34 | # - But it can still be iterated against 35 | for date in all_dates: 36 | print(date) 37 | 38 | # - A prime-number-finding function that returns a list 39 | # of values 40 | def prime_numbers_list(start, end): 41 | result = [] 42 | number = start 43 | while number <= end: 44 | limit = math.sqrt(number) 45 | factors = [ 46 | n for n in range(1, int(limit+1)) 47 | if int(number/n) == number/n 48 | ] 49 | if len(factors) == 1: 50 | result.append(number) 51 | number += 1 52 | return result 53 | # - Get prime_numbers results for the same ranges of numbers, 54 | # and print them 55 | primes_2_30 = prime_numbers_list(2,30) 56 | primes_100_150 = prime_numbers_list(100,150) 57 | print(primes_2_30) 58 | print(primes_100_150) 59 | 60 | # - A prime-number-finding function that returns a 61 | # *generator* of member-values 62 | def prime_numbers(start, end): 63 | number = start 64 | while number <= end: 65 | limit = math.sqrt(number) 66 | factors = [ 67 | n for n in range(1, int(limit+1)) 68 | if int(number/n) == number/n 69 | ] 70 | if len(factors) == 1: 71 | # NOTE: We're using yield here! 72 | yield number 73 | number += 1 74 | 75 | # - prime_numbers still looks like a standard function, 76 | # externally: 77 | print(prime_numbers) 78 | print(type(prime_numbers)) 79 | # - Get prime_numbers results for the same ranges of numbers, 80 | # and print them 81 | primes_2_30 = prime_numbers(2,30) 82 | primes_100_150 = prime_numbers(100,150) 83 | # - The results are generators, though: 84 | print(primes_2_30) 85 | print(primes_100_150) 86 | # - The entire result-set can be retrieved as a list if needed 87 | print(list(primes_2_30)) 88 | print(list(primes_100_150)) 89 | 90 | -------------------------------------------------------------------------------- /Chapter01/C01R05_DecoratorAnatomy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | The complete code of Ch. 1, Recipe 5 -- Anatomy of a Decorator 4 | """ 5 | 6 | def print_line( 7 | leader:str, line:(str,None)=None, 8 | indent:int=0, body_start:int=28 9 | ) -> str: 10 | """ 11 | Prints a formatted line of text, with a dot-leader between the 12 | lead and the line supplied 13 | """ 14 | output = '' 15 | if indent: 16 | output = ' '*(3*(indent-1)) + '+- ' 17 | output += leader 18 | if line: 19 | output = ( 20 | (output + ' ').ljust(body_start - 1, '.') 21 | + ' ' + line 22 | ) 23 | print(output) 24 | 25 | # - A bare-bones decorator accepts just the decorated item as 26 | # its sole argument 27 | def decorator(decorated_item): 28 | print('#' + '-'*64 + '#') 29 | print_line('decorator called:') 30 | print_line('decorated_item', str(decorated_item), 1) 31 | def inner_decorator(*iargs, **ikwargs): 32 | print_line('inner_decorator called:', None,) 33 | print_line('decorated_item', str(decorated_item), 1) 34 | print_line('iargs', str(iargs), 1) 35 | print_line('ikwargs', str(ikwargs), 1) 36 | # - Do something with the decorated item (which might 37 | # use the outer decorator's arguments even apart 38 | # from decorated_item). For now, just call the 39 | # original decorated function: 40 | return decorated_item(*iargs, **ikwargs) 41 | print_line('inner_decorator', str(inner_decorator)) 42 | print('#' + '-'*64 + '#') 43 | return inner_decorator 44 | 45 | # - Decorating classes is possible too: 46 | def class_wrapper(decorated_item): 47 | def inner_wrapper(): 48 | # - Arguments for an inner-decorator class could be 49 | # difficult to manage, but it's doable... just 50 | # potentially complicated. 51 | class class_wrapper: 52 | def __init__(self, original_class): 53 | self.original_class = original_class 54 | # - Return here is the inner-decorator *class* 55 | return class_wrapper(decorated_item) 56 | # - Return here is the decorator *function* 57 | return inner_wrapper 58 | 59 | # - Multiple decorators to show how application of more than one 60 | # decorator executes. 61 | def decorator_1(decorated_item): 62 | print_line('decorator_1 called:') 63 | print_line('decorated_item', str(decorated_item), 1) 64 | def inner_decorator(*iargs, **ikwargs): 65 | print_line('Calling decorator_1.inner_decorator') 66 | # - Do something with the decorated item (which might 67 | # use the outer decorator's arguments even apart 68 | # from decorated_item). For now, just call the 69 | # original decorated function: 70 | return decorated_item(*iargs, **ikwargs) 71 | return inner_decorator 72 | 73 | def decorator_2(decorated_item): 74 | print_line('decorator_2 called:') 75 | print_line('decorated_item', str(decorated_item), 1) 76 | def inner_decorator(*iargs, **ikwargs): 77 | print_line('Calling decorator_2.inner_decorator') 78 | # - Do something with the decorated item (which might 79 | # use the outer decorator's arguments even apart 80 | # from decorated_item). For now, just call the 81 | # original decorated function: 82 | return decorated_item(*iargs, **ikwargs) 83 | return inner_decorator 84 | 85 | def decorator_3(decorated_item): 86 | print_line('decorator_3 called:') 87 | print_line('decorated_item', str(decorated_item), 1) 88 | def inner_decorator(*iargs, **ikwargs): 89 | print_line('Calling decorator_3.inner_decorator') 90 | # - Do something with the decorated item (which might 91 | # use the outer decorator's arguments even apart 92 | # from decorated_item). For now, just call the 93 | # original decorated function: 94 | return decorated_item(*iargs, **ikwargs) 95 | return inner_decorator 96 | 97 | if __name__ == '__main__': 98 | 99 | print_line('Decorating decorated w/', str(decorator)) 100 | @decorator 101 | def decorated(*args, **kwargs): 102 | print_line('decorated called:', None, 1) 103 | print_line('args', str(args), 2) 104 | print_line('kwargs', str(kwargs), 2) 105 | print('decorated: %s' % decorated) 106 | 107 | print('Calling decorated function (%s)' % decorated) 108 | decorated('decorated', decoration=True) 109 | 110 | @decorator 111 | def decorated2(*args, **kwargs): 112 | print_line('decorated2 called:', None, 1) 113 | print_line('args', str(args), 2) 114 | print_line('kwargs', str(kwargs), 2) 115 | print('decorated2: %s' % decorated2) 116 | 117 | print('Calling decorated2 function (%s)' % decorated2) 118 | decorated2('decorated2', decoration=True) 119 | 120 | # - Name-checking example 121 | def name_must_be_string(decorated_item): 122 | def name_check(name): 123 | if type(name) != str: 124 | raise TypeError( 125 | '%s expects a string value for its name ' 126 | 'argument, but was passed "%s" (a %s)' % 127 | ( 128 | decorated_item.__name__, name, 129 | type(name).__name__ 130 | ) 131 | ) 132 | return decorated_item(name) 133 | return name_check 134 | 135 | @name_must_be_string 136 | def set_name(name): 137 | print_line('set_name', name) 138 | 139 | @name_must_be_string 140 | def do_something_with_name(name): 141 | print_line('do_something_with_name', name) 142 | 143 | @name_must_be_string 144 | def do_something_else_name(name): 145 | print_line('do_something_else_name', name) 146 | 147 | set_name('John Sheridan') 148 | do_something_with_name('Londo Mollari') 149 | do_something_else_name('Kosh Naranek') 150 | try: 151 | set_name(3.14) 152 | except TypeError as error: 153 | print( 154 | '%s (expected): %s' % 155 | (error.__class__.__name__, error) 156 | ) 157 | try: 158 | do_something_with_name(3.14) 159 | except TypeError as error: 160 | print( 161 | '%s (expected): %s' % 162 | (error.__class__.__name__, error) 163 | ) 164 | try: 165 | do_something_else_name(3.14) 166 | except TypeError as error: 167 | print( 168 | '%s (expected): %s' % 169 | (error.__class__.__name__, error) 170 | ) 171 | 172 | @class_wrapper 173 | class wrap_me: 174 | pass 175 | 176 | instance = wrap_me() 177 | print_line('instance', str(instance)) 178 | print_line( 179 | 'instance.original_class', str(instance.original_class) 180 | ) 181 | 182 | print('='*80) 183 | 184 | @decorator_1 185 | @decorator_2 186 | @decorator_3 187 | def multiple_decorations(*args, **kwargs): 188 | print_line('multiple_decorations called:', None, 1) 189 | print_line('args', str(args), 2) 190 | print_line('kwargs', str(kwargs), 2) 191 | 192 | print('Calling multiple_decorations (%s)' % multiple_decorations) 193 | multiple_decorations('multiple_decorations', decoration=True) 194 | -------------------------------------------------------------------------------- /Chapter01/C01R06_DecoratorWithArguments.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | The complete code of Ch. 1, Recipe 6 -- Decorators with Arguments 4 | """ 5 | 6 | def print_line( 7 | leader:str, line:(str,None)=None, 8 | indent:int=0, body_start:int=28 9 | ) -> str: 10 | """ 11 | Prints a formatted line of text, with a dot-leader between the 12 | lead and the line supplied 13 | """ 14 | output = '' 15 | if indent: 16 | output = ' '*(3*(indent-1)) + '+- ' 17 | output += leader 18 | if line: 19 | output = ( 20 | (output + ' ').ljust(body_start - 1, '.') 21 | + ' ' + line 22 | ) 23 | print(output) 24 | 25 | def doesnt_work(*args): 26 | print_line('Calling doesnt_work') 27 | print_line('args', str(args),1) 28 | def _decorator(*args, **kwargs): 29 | print_line('Calling doesnt_work') 30 | print_line('args', str(args),2) 31 | return decorated_item(*args, **kwargs) 32 | print_line('Returning', str(_decorator)) 33 | return _decorator 34 | 35 | # - A decorator with arguments operates in three steps. 36 | # The first is the outer decorator call: 37 | # @decorator(arguments) 38 | def decorator(*args, **kwargs): 39 | print_line('decorator called:') 40 | print_line('args', str(args), 1) 41 | print_line('kwargs', str(kwargs), 1) 42 | # - The second is the "real" decorator-function, that 43 | # accepts *only* the element being decorated 44 | # (decorated_item): 45 | def _decorator(decorated_item): 46 | print_line('_decorator called:', None, 2) 47 | print_line('decorated_item', str(decorated_item), 3) 48 | # - The third is the "real" decoration, which has 49 | # access to all of the decorator and _decorator 50 | # arguments: 51 | def inner_decorator(*iargs, **ikwargs): 52 | # - Do something with the decorated item (which 53 | # can use the outer decorator's arguments even 54 | # apart from decorated_item). 55 | # For now, just call the original decorated 56 | # function (with the inner_decorator arguments): 57 | print_line('inner_decorator called:', None, 4) 58 | print_line('decorated_item', str(decorated_item), 5) 59 | print_line('args', str(args), 5) 60 | print_line('kwargs', str(kwargs), 5) 61 | print_line('iargs', str(iargs), 5) 62 | print_line('ikwargs', str(ikwargs), 5) 63 | decorated_item(*iargs, **ikwargs) 64 | print_line('inner_decorator', str(inner_decorator), 3) 65 | return inner_decorator 66 | print_line('_decorator', str(_decorator), 1) 67 | return _decorator 68 | 69 | def document_arg(name, description): 70 | print_line('document_arg called:', None, 1) 71 | print_line('name', str(name), 2) 72 | print_line('description', str(description), 2) 73 | def _decorator(func): 74 | # - Since this decorator is adding data to the 75 | # __annotations__ of the target function (func), 76 | # all it needs to do is make those additions... 77 | try: 78 | # - Try to add the description to 79 | # func.__annotations__['__doc__'] 80 | func.__annotations__['__doc__'][name] = description 81 | except KeyError: 82 | # - If __annotations__['__doc__'] doesn't exist, 83 | # create it with the only item provided so far 84 | func.__annotations__['__doc__']={name:description} 85 | # ... and return the *original* function. 86 | return func 87 | return _decorator 88 | 89 | def format_line( 90 | leader:str, line:(str,None)=None, 91 | indent:int=0, body_start:int=28 92 | ): 93 | """ 94 | Returns a formatted line of text, with a dot-leader between the 95 | lead and the line supplied 96 | """ 97 | output = '' 98 | if indent: 99 | output = ' '*(3*indent) + '+- ' 100 | else: 101 | output = '+- ' 102 | output += leader 103 | if line: 104 | output = ( 105 | (output + ' ').ljust(body_start - 1, '.') 106 | + ' ' + line 107 | ) 108 | return output 109 | 110 | def detailed_docs(target): 111 | # - Start by acquiring the original doc-string of the target 112 | result = (' '.join( 113 | [line.strip() for line in target.__doc__ .split('\n')] 114 | ) or 'No documentation defined').strip() 115 | # - Look for annotations, and build the argument 116 | # documentation if we can 117 | if target.__annotations__: 118 | _docitems = target.__annotations__.get('__doc__') 119 | if _docitems: 120 | result += '\n\nArguments:\n' 121 | for item in _docitems: 122 | item_types = target.__annotations__.get(item) 123 | if item_types: 124 | if type(item_types) == tuple: 125 | if len(item_types): 126 | item_types = '|'.join([t.name for t in item_types]) 127 | else: 128 | item_types = item_types.__name__ 129 | else: 130 | item_types = 'Not specified' 131 | result += format_line(item, '(%s) %s\n' % (item_types, _docitems[item]), 0, 24) 132 | return result 133 | 134 | if __name__ == '__main__': 135 | 136 | # ~ try: 137 | # ~ @doesnt_work('decorating') 138 | # ~ def decorated(*args, **kwargs): 139 | # ~ print_line('decorated called:', None, 1) 140 | # ~ print_line('args', str(args), 2) 141 | # ~ print_line('kwargs', str(kwargs), 2) 142 | # ~ except NameError as error: 143 | # ~ print_line( 144 | # ~ 'Raises', '%s: %s' % (error.__class__.__name__, error) 145 | # ~ ) 146 | 147 | # ~ print_line( 148 | # ~ 'Decorating a function with decorator (%s)' % 149 | # ~ decorator 150 | # ~ ) 151 | # ~ @decorator('decorating decorated', keys=True) 152 | # ~ def decorated(*args, **kwargs): 153 | # ~ print_line('decorated called:', None, 1) 154 | # ~ print_line('args', str(args), 2) 155 | # ~ print_line('kwargs', str(kwargs), 2) 156 | # ~ print_line('decorated', str(decorated)) 157 | 158 | # ~ decorated('arg1', 'arg2', key1='value1') 159 | 160 | @document_arg('name', 'The name of the item') 161 | @document_arg( 162 | 'active', 'Flag indicating whether the item is active ' 163 | '(True) or not (False)' 164 | ) 165 | def documented_function(name:str, active:bool=True) -> None: 166 | """ 167 | The original docstring of the function. 168 | """ 169 | pass 170 | 171 | # ~ from inspect import getfullargspec 172 | 173 | # ~ print(documented_function) 174 | # ~ print_line('__annotations__:') 175 | # ~ for item in documented_function.__annotations__: 176 | # ~ print_line(item, 177 | # ~ str(documented_function.__annotations__[item]), 1 178 | # ~ ) 179 | # ~ argspec = getfullargspec(documented_function) 180 | # ~ print_line('fullargspec(%s):' % documented_function) 181 | # ~ print_line('varargs', str(argspec.varargs), 1) 182 | # ~ print_line('varkw', str(argspec.varkw), 1) 183 | # ~ print_line('defaults', str(argspec.defaults), 1) 184 | # ~ print_line('kwonlyargs', str(argspec.kwonlyargs), 1) 185 | # ~ print_line('kwonlydefaults', str(argspec.kwonlydefaults), 1) 186 | # ~ print_line('annotations', '(See __annotations__, above)', 1) 187 | 188 | # ~ print(detailed_docs(documented_function)) 189 | 190 | # ~ print('Calling decorated function (%s)' % decorated) 191 | # ~ decorated('decorated', decoration=True) 192 | 193 | # ~ print('Calling decorated2 function (%s)' % decorated2) 194 | # ~ decorated2('decorated2', decoration=True) 195 | 196 | -------------------------------------------------------------------------------- /Chapter01/README.md: -------------------------------------------------------------------------------- 1 | # Chapter 1 — Idiomatic Python 2 | 3 | > Part of the "Python Object Oriented Programming Cookbook" title, 4 | > published by Packt. 5 | 6 | > Code in this chapter was originally written against version 3.6.6 of Python, 7 | > as installed by default on the Author's then-current Linux Mint 19 (Cinnamon) 8 | > installation. 9 | 10 | [Recipe 1: **Annotating Functions**](C01R01_AnnotatingFunctions.py) — 11 | Annotates a function with all seven styles of arguments, shows how those 12 | annotations can be accessed 13 | 14 | [Recipe 2: **List Comprehensions**](C01R02_ListComprehensions.py) — 15 | Shows basic, mapping, filtering and mapping-and-filtering list-comprehension 16 | code, along with their `map`- and `filter`-function equivalents 17 | 18 | [Recipe 3: **Dictionary Comprehensions**](C01R03_DictComprehensions.py) — 19 | Shows basic, mapping, filtering and mapping-and-filtering dict-comprehension 20 | code 21 | 22 | [Recipe 4: **Generators**](C01R04_Generators.py) — 23 | Shows a simple inline-generator example *and* a prime-number-list 24 | calculator function in two versions: One as a standard list-return, 25 | one as a generator function. 26 | 27 | [Recipe 5: **Anatomy of a Decorator**](C01R05_DecoratorAnatomy.py) — 28 | Defines four standard (no-arguments-allowed) function-decorators and one 29 | class decorator, shows what happens when they execute. 30 | 31 | [Recipe 6: **Decorators with Arguments**](C01R06_DecoratorWithArguments.py) — 32 | Defines and describes a generic decorator-with-arguments function, and 33 | shows an example of such a decorator that adds argument-documentation 34 | to an annotated function. 35 | -------------------------------------------------------------------------------- /Chapter02/C02R01_BasicClass.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | The complete code of Ch. 2, Recipe 1 -- Basic Class Definition 4 | """ 5 | 6 | class BasicClass: 7 | """Document the class as needed""" 8 | # Comments are allowed in classes 9 | # - Object initialization 10 | def __init__(self, name, description): 11 | """Object initialization""" 12 | # - Initialize instance properties from arguments 13 | self.name = name 14 | self.description = description 15 | # - Instance method 16 | def summarize(self): 17 | """Document method""" 18 | if self.description: 19 | return '%s: %s' % (self.name, self.description) 20 | else: 21 | return self.name 22 | 23 | if __name__ == '__main__': 24 | print('#'*66) 25 | print('# BasicClass'.ljust(65, ' ') + '#') 26 | print('#'*66) 27 | print(BasicClass) 28 | # - Create an instance of the class 29 | instance = BasicClass('name', 'description') 30 | print('#- BasicClass instance '.ljust(65, '-') + '#') 31 | print(instance) 32 | print(instance.summarize()) 33 | -------------------------------------------------------------------------------- /Chapter02/C02R02_NonPublicMembers.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | The complete code of Ch. 2, Recipe 2 -- Restricting member access 4 | """ 5 | 6 | class BasicClass: 7 | """Document the class as needed""" 8 | # Comments are allowed in classes 9 | # - Object initialization 10 | def __init__(self, name, description): 11 | """Object initialization""" 12 | # - Initialize instance properties from arguments 13 | self.name = name 14 | self.description = description 15 | # - Instance method 16 | def summarize(self): 17 | """Document method""" 18 | if self.description: 19 | return '%s: %s' % (self.name, self.description) 20 | else: 21 | return self.name 22 | 23 | # - Private method 24 | def __private_method(self): 25 | return '__private_method called' 26 | 27 | # - Protected method 28 | def _protected_method(self): 29 | return '_protected_method called' 30 | 31 | if __name__ == '__main__': 32 | print('#'*66) 33 | print('# BasicClass'.ljust(65, ' ') + '#') 34 | print('#'*66) 35 | print(BasicClass) 36 | # - Create an instance of the class 37 | my_object = BasicClass('my name', 'my description') 38 | print('#- BasicClass instance '.ljust(65, '-') + '#') 39 | print(my_object) 40 | # - This is allowed, but is NOT according to convention 41 | print(my_object._protected_method()) 42 | # - The name-mangling of private members prevents casual 43 | # access to them: 44 | try: 45 | print(my_object.__private_method()) 46 | except Exception as error: 47 | print('### %s: %s' % (error.__class__.__name__, error)) 48 | # - But they can still be accessed. Again, this is **NOT** 49 | # according to Python conventions, so don't do this! 50 | print(my_object._BasicClass__private_method()) 51 | -------------------------------------------------------------------------------- /Chapter02/C02R04_PropertiesviaDecorator.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | The complete code of Ch. 2, Recipe 4 -- Defining properties using property as a decorator 4 | """ 5 | 6 | class BasicClass: 7 | """Document the class as needed""" 8 | 9 | # - Object initialization 10 | def __init__(self, name:str, description:str): 11 | """Object initialization""" 12 | # - Initialize instance properties from arguments 13 | self.name = name 14 | self.description = description 15 | 16 | # Defining name as a property 17 | # - Start with the getter assignment 18 | @property 19 | def name(self) -> (str,None): 20 | """ 21 | Gets, sets or deletes the name associated with the instance 22 | """ 23 | try: 24 | return self._name 25 | except AttributeError: 26 | return None 27 | 28 | # - Assign a protected method that checks for a string 29 | # type as the setter for name 30 | @name.setter 31 | def name(self, value:str) -> None: 32 | """Sets the name associated with the instance""" 33 | if type(value) != str: 34 | raise TypeError('name expects a string value') 35 | self._name = value 36 | 37 | # - Assign a deleter-method 38 | @name.deleter 39 | def name(self) -> None: 40 | try: 41 | del self._name 42 | except AttributeError: 43 | pass 44 | 45 | # - Show the documentation of the property 46 | print('1) %s' % BasicClass.name.__doc__.strip()) 47 | # - Create an instance. 48 | my_object = BasicClass('name', 'description') 49 | # - Show the original value 50 | print('2) %s' % my_object.name) 51 | # - Alter the value, and show it again 52 | my_object.name='A new name' 53 | print('3) %s' % my_object.name) 54 | # - Show that the type-checking works 55 | try: 56 | my_object.name = 123.456 57 | except Exception as error: 58 | print('4) %s: %s' % (error.__class__.__name__, error)) 59 | 60 | -------------------------------------------------------------------------------- /Chapter02/C02R05_PropertiesviaFunction.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | The complete code of Ch. 2, Recipe 5 -- Defining properties using property as a function 4 | """ 5 | 6 | class BasicClass: 7 | """Document the class as needed""" 8 | 9 | # - Object initialization 10 | def __init__(self, name, description): 11 | """Object initialization""" 12 | # - Initialize instance properties from arguments 13 | self.name = name 14 | self.description = description 15 | 16 | # Defining description as a property using the 17 | # function approach 18 | 19 | # - Define a getter-method 20 | def _get_description(self): 21 | try: 22 | return self._description 23 | except AttributeError: 24 | return None 25 | 26 | # - Define a setter-method 27 | def _set_description(self, value): 28 | if type(value) != str: 29 | raise TypeError('description expects a string value') 30 | self._description = value 31 | 32 | # - Define a deleter-method 33 | def _del_description(self): 34 | try: 35 | del self._description 36 | except AttributeError: 37 | pass 38 | 39 | # - Assemble them into a property 40 | description = property( 41 | _get_description, _set_description, _del_description, 42 | 'Gets, sets or deletes the description associated with the instance' 43 | ) 44 | 45 | # - Show the documentation of the property 46 | print('1) %s' % BasicClass.description.__doc__.strip()) 47 | # - Create an instance. 48 | my_object = BasicClass('name', 'description') 49 | # - Show the original value 50 | print('2) %s' % my_object.description) 51 | # - Alter the value, and show it again 52 | my_object.description='A new description' 53 | print('3) %s' % my_object.description) 54 | # - Show that the type-checking works 55 | try: 56 | my_object.description = 123.456 57 | except Exception as error: 58 | print('4) %s: %s' % (error.__class__.__name__, error)) 59 | 60 | -------------------------------------------------------------------------------- /Chapter02/C02R06_PassingStateinConstruction.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | The complete code of Ch. 2, Recipe 6 -- Passing object-state during construction 4 | """ 5 | 6 | class Person: 7 | """A *very* basic representation of a person""" 8 | 9 | def __init__(self, 10 | # - Required Person arguments 11 | given_name:str, family_name:str, 12 | # - Optional Person arguments 13 | birth_date:(str,None)=None, email_address:(str,None)=None 14 | ): 15 | """Object initialization""" 16 | self.given_name = given_name 17 | self.family_name = family_name 18 | self.birth_date = birth_date 19 | self.email_address = email_address 20 | 21 | def __str__(self): 22 | """Returns a string representation of the object""" 23 | return ( 24 | '<%s [%s] given_name=%s family_name=%s>' % 25 | ( 26 | self.__class__.__name__, hex(id(self)), 27 | self.given_name, self.family_name, 28 | ) 29 | ) 30 | 31 | if __name__ == '__main__': 32 | author = Person('Brian', 'Allbee') 33 | print(author) 34 | 35 | alice = Person( 36 | 'Alice', 'Exeter', None, 'aexeter@some-company.com' 37 | ) 38 | print(alice) 39 | print(alice.birth_date) 40 | print(alice.email_address) 41 | 42 | bob = Person('Robert', 'Jones', '1959-03-21') 43 | print(bob) 44 | print(bob.birth_date) 45 | print(bob.email_address) 46 | 47 | -------------------------------------------------------------------------------- /Chapter02/C02R07_CreatingClassAttributes.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | The complete code of Ch. 2, Recipe 7 -- Creating class attributes 4 | """ 5 | 6 | class HasClassAttributes: 7 | """ 8 | A class that provides class attributes to play with 9 | """ 10 | name = 'Name defined in class' 11 | number = 'one' 12 | 13 | class InstanceAccess: 14 | # - Class attributes 15 | _active_instances = 0 16 | 17 | @property 18 | def active_instances(self): 19 | return self.__class__._active_instances 20 | 21 | @active_instances.setter 22 | def active_instances(self, value): 23 | self.__class__._active_instances = value 24 | 25 | def __init__(self): 26 | self.active_instances += 1 27 | 28 | def __del__(self): 29 | self.active_instances -= 1 30 | 31 | if __name__ == '__main__': 32 | # - Print the class attributes 33 | print(HasClassAttributes.name) # Name defined in class 34 | print(HasClassAttributes.number) # one 35 | 36 | ### What happens if a class attribute is created and an 37 | # instance refers to it? 38 | # - Create an instance 39 | instance = HasClassAttributes() 40 | print(instance.name) # Name defined in class 41 | print(instance.number) # one 42 | 43 | ### What happens if an instance’s attribute-values are changed? 44 | # - Create an instance 45 | instance = HasClassAttributes() 46 | # - Alter the instance's values 47 | instance.name = 'New instance name' 48 | instance.number = 'two' 49 | # - Show the instance-values' changes 50 | print(instance.name) # New instance name 51 | print(instance.number) # two 52 | # - Show the class-attributes' values 53 | print(HasClassAttributes.name) # Name defined in class 54 | print(HasClassAttributes.number) # one 55 | 56 | ### What happens to an instance's values if the class values 57 | # are changed after the instance's values are changed? 58 | # - Create an instance 59 | instance = HasClassAttributes() 60 | instance.name = 'New instance name' 61 | instance.number = 'two' 62 | # - Alter the class' values 63 | HasClassAttributes.name = 'New class name' 64 | HasClassAttributes.number = 'three' 65 | # - Show the class-attributes' values 66 | print(HasClassAttributes.name) # New class name 67 | print(HasClassAttributes.number) # three 68 | # - Show the instance-values' changes 69 | print(instance.name) # New instance name 70 | print(instance.number) # two 71 | # - Create a NEW instance and show its values 72 | instance = HasClassAttributes() 73 | print(instance.name) # New class name 74 | print(instance.number) # three 75 | 76 | # - Reset the class to the original values 77 | HasClassAttributes.name = 'Name defined in class' 78 | HasClassAttributes.number = 'one' 79 | 80 | ### What happens to an instance's values if the class values 81 | # are changed after the instance has been created? 82 | # - Create an instance 83 | instance = HasClassAttributes() 84 | # - Alter the class' values 85 | HasClassAttributes.name = 'New class name' 86 | HasClassAttributes.number = 'three' 87 | # - Show the instance-values 88 | print(instance.name) # New class name 89 | print(instance.number) # three 90 | # - Reset the class to the original values 91 | HasClassAttributes.name = 'Name defined in class' 92 | HasClassAttributes.number = 'one' 93 | # - Create an instance 94 | instance = HasClassAttributes() 95 | # - Show ONE of the instance-values 96 | print(instance.name) # Name defined in class 97 | # - Alter the class' values 98 | HasClassAttributes.name = 'New class name' 99 | HasClassAttributes.number = 'three' 100 | # - Show the OTHER of the instance-values 101 | print(instance.number) # three 102 | 103 | inst1 = InstanceAccess() 104 | print('inst1.active_instances ... %d' % inst1.active_instances) 105 | inst2 = InstanceAccess() 106 | print('inst1.active_instances ... %d' % inst1.active_instances) 107 | print('inst2.active_instances ... %d' % inst2.active_instances) 108 | del inst1 109 | print('inst2.active_instances ... %d' % inst2.active_instances) 110 | -------------------------------------------------------------------------------- /Chapter02/C02R08_CreatingClassMethods.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | The complete code of Ch. 2, Recipe 8 -- Creating class methods 4 | """ 5 | 6 | class WithClassMethod: 7 | # - Define a class attribute that the class-method can use 8 | class_attribute = 'Spam' 9 | 10 | # - Define a class-method 11 | @classmethod 12 | def example(cls): 13 | print( 14 | '%s.example called on %s: %s' % 15 | (cls.__name__, cls, cls.class_attribute) 16 | ) 17 | 18 | if __name__ == '__main__': 19 | # - Calling the class-method on the class it originated in 20 | WithClassMethod.example() 21 | 22 | # - Creating an instance 23 | wcm_instance = WithClassMethod() 24 | # - Calling the class-method from an instance is allowed, 25 | # but it acquires the class from the instance, keeping 26 | # the class-reference instead of using the instance: 27 | wcm_instance.example() 28 | 29 | # - Class-methods are inherited 30 | class InheritsClassMethod(WithClassMethod): 31 | class_attribute = 'Eggs' 32 | # - Since InheritsClassMethod.class_attribute is different 33 | # from WithClassMethod.class_attribute, we get a different 34 | # result -- the class-method references the class it's 35 | # being called against, and uses that value... 36 | InheritsClassMethod.example() 37 | # - Instance behavior is the same... 38 | icm_instance = InheritsClassMethod() 39 | icm_instance.example() 40 | -------------------------------------------------------------------------------- /Chapter02/C02R09_CreatingStaticMethods.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | The complete code of Ch. 2, Recipe 9 -- Creating static methods 4 | """ 5 | 6 | class WithStaticMethod: 7 | def instance_method(self): 8 | pass 9 | @staticmethod 10 | def static_method(): 11 | print('WithStaticMethod.static_method called') 12 | 13 | if __name__ == '__main__': 14 | print(type(WithStaticMethod)) 15 | # - All of these report that they are *functions* rather 16 | # than methods... 17 | print(WithStaticMethod.static_method) 18 | print(WithStaticMethod.instance_method) 19 | 20 | # - For comparison purposes, what does a normal function look 21 | # like when it's printed? 22 | def some_function(): 23 | pass 24 | print(some_function) 25 | 26 | # - Creating an instance... 27 | wsm_instance = WithStaticMethod() 28 | print(wsm_instance) 29 | # - Still reports that it's a function 30 | print(wsm_instance.static_method) 31 | # - Since this is associated with an instance, it reports 32 | # that it's a bound method 33 | print(wsm_instance.instance_method) 34 | wsm_instance.static_method() 35 | 36 | # - The static method is inherited unchanged... 37 | class InheritsStaticMethod(WithStaticMethod): 38 | pass 39 | 40 | print(InheritsStaticMethod.static_method) 41 | InheritsStaticMethod.static_method() 42 | ism_instance = InheritsStaticMethod() 43 | ism_instance.static_method() 44 | -------------------------------------------------------------------------------- /Chapter02/C02R10_BasicDataclassDefinition.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | The complete code of Ch. 2, Recipe 10 -- Simplifying class definitions: The dataclasses module 4 | """ 5 | 6 | from dataclasses import dataclass 7 | 8 | @dataclass(order=True) 9 | class Person: 10 | """ 11 | A *very* basic representation of a person, created as a dataclass 12 | """ 13 | given_name: str 14 | family_name: str 15 | birth_date: str = None 16 | email_address: str = None 17 | 18 | if __name__ == '__main__': 19 | # - Classes defined as dataclasses are still classes... 20 | print(Person) 21 | # - They still appear as normal "type" types 22 | print(type(Person)) 23 | # - They have additional members 24 | print('__annotations__ ........ %s' % Person.__annotations__) 25 | print('__dataclass_fields__ ... %s' % Person.__dataclass_fields__) 26 | print('__dataclass_params__ ... %s' % Person.__dataclass_params__) 27 | # - And have some that have default implementations provided: 28 | author = Person('Brian', 'Allbee') 29 | print(author) 30 | alice = Person( 31 | 'Alice', 'Exeter', None, 'aexeter@some-company.com' 32 | ) 33 | print(alice) 34 | # - order=True has to be passed as an argument to the 35 | # dataclass decorator for these to work: 36 | print('author == alice ... %s' % (author == alice)) 37 | print('author != alice ... %s' % (author != alice)) 38 | print('author < alice .... %s' % (author < alice)) 39 | print('author <= alice ... %s' % (author <= alice)) 40 | print('author > alice .... %s' % (author > alice)) 41 | print('author >= alice ... %s' % (author >= alice)) 42 | 43 | author = PersonWithProperties('Brian', 'Allbee') 44 | print(author) 45 | -------------------------------------------------------------------------------- /Chapter02/README.md: -------------------------------------------------------------------------------- 1 | # Chapter 2 — Introduction and Basic OOP Concepts 2 | 3 | > Part of the "Python Object Oriented Programming Cookbook" title, 4 | > published by Packt. 5 | 6 | > Code in this chapter was originally written against version 3.6.6 of Python, 7 | > as installed by default on the Author's then-current Linux Mint 19 (Cinnamon) 8 | > installation. 9 | 10 | [Recipe 1: **Basic Class Definition**](C02R01_BasicClass.py) — 11 | A bare-bones class definition 12 | 13 | [Recipe 2: **Restricting member access**](C02R02_NonPublicMembers.py) — 14 | Implementing "protected" and "private" class-members in Python 15 | 16 | [Recipe 4: **Managing attributes using `property` as a decorator**](C02R03_PropertiesviaDecorator.py) — 17 | Creating properties (managed attributes) using Python's `property` function as a decorator 18 | 19 | [Recipe 5: **Managing attributes using `property` as a function**](C02R04_PropertiesviaFunction.py) — 20 | Creating properties (managed attributes) using Python's `property` function to directly 21 | assign getter-, setter- and deleter-methods, as well as documentation 22 | 23 | [Recipe 6: **Passing object-state during construction**](C02R05_PassingStateinConstruction.py) — 24 | A typical code-structure for initializing object-instances' state-data with arguments 25 | provided during creation 26 | 27 | [Recipe 7: **Creating class attributes**](C02R06_CreatingClassAttributes.py) — 28 | 29 | [Recipe 8: **Creating class methods**](C02R07_CreatingClassMethods.py) — 30 | 31 | [Recipe 9: **Creating static methods**](C02R08_CreatingStaticMethods.py) — 32 | 33 | [Recipe 10: **Simplifying class definitions: The dataclasses module**](C02R09_BasicDataclassDefinition.py) — 34 | Exploring an alternative syntax for quickly creating classes 35 | -------------------------------------------------------------------------------- /Chapter02/descriptor-play.py: -------------------------------------------------------------------------------- 1 | class Descriptor: 2 | 3 | def init(self): 4 | self.__value = '' 5 | 6 | def __get__(self, instance, owner): 7 | print('Getting: %s' % self.__value) 8 | print('+- instance ... %s' % instance) 9 | print('+- owner ...... %s' % owner) 10 | return self.__value 11 | 12 | def __set__(self, instance, value): 13 | print('Setting: "%s"' % value) 14 | print('+- instance ... %s' % instance) 15 | print('+- value ...... %s' % value) 16 | self.__value = value.title() 17 | 18 | def __delete__(self, instance): 19 | print('Deleting: %s' % self.__value) 20 | print('+- instance ... %s' % instance) 21 | del self.__value 22 | 23 | class Person: 24 | name = Descriptor() 25 | 26 | def __init__(self, name): 27 | self.name = name 28 | 29 | if __name__ == "__main__": 30 | 31 | user = Person('name') 32 | print(user.name) 33 | user.name = 'john smith' 34 | print(user.name) 35 | del user.name 36 | 37 | import inspect 38 | print(inspect.isdatadescriptor(Descriptor)) 39 | -------------------------------------------------------------------------------- /Chapter02/requirements.txt: -------------------------------------------------------------------------------- 1 | dataclasses==0.6 2 | -------------------------------------------------------------------------------- /Chapter03/C03R01_PersonStudent.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | The complete code of Ch. 3, Recipe 1 -- Basic inheritance 4 | """ 5 | 6 | class Person: 7 | """ 8 | Represents a Person 9 | """ 10 | 11 | # PROPERTY DEFINITIONS 12 | # - Email address 13 | @property 14 | def email_address(self): 15 | try: 16 | return self._email_address 17 | except AttributeError: 18 | return None 19 | @email_address.setter 20 | def email_address(self, value): 21 | self._email_address = value 22 | @email_address.deleter 23 | def email_address(self): 24 | try: 25 | del self._email_address 26 | except AttributeError: 27 | pass 28 | 29 | # - Family name 30 | @property 31 | def family_name(self): 32 | try: 33 | return self._family_name 34 | except AttributeError: 35 | return None 36 | @family_name.setter 37 | def family_name(self, value): 38 | self._family_name = value 39 | @family_name.deleter 40 | def family_name(self): 41 | try: 42 | del self._family_name 43 | except AttributeError: 44 | pass 45 | 46 | # - Full name, a calculated read-only property 47 | @property 48 | def full_name(self): 49 | results = [] 50 | if self.given_name != None: 51 | results.append(self.given_name) 52 | if self.initials != None: 53 | results.append(self.initials) 54 | if self.family_name != None: 55 | results.append(self.family_name) 56 | if results: 57 | return ' '.join(results) 58 | 59 | # - Given name 60 | @property 61 | def given_name(self): 62 | try: 63 | return self._given_name 64 | except AttributeError: 65 | return None 66 | @given_name.setter 67 | def given_name(self, value): 68 | self._given_name = value 69 | @given_name.deleter 70 | def given_name(self): 71 | try: 72 | del self._given_name 73 | except AttributeError: 74 | pass 75 | 76 | # - Initials 77 | @property 78 | def initials(self): 79 | try: 80 | return self._initials 81 | except AttributeError: 82 | return None 83 | @initials.setter 84 | def initials(self, value): 85 | self._initials = value 86 | @initials.deleter 87 | def initials(self): 88 | try: 89 | del self._initials 90 | except AttributeError: 91 | pass 92 | 93 | # OBJECT INITIALIZATION 94 | def __init__(self, 95 | given_name=None, initials=None, family_name=None, 96 | email_address=None 97 | ): 98 | """ 99 | Object initialization 100 | given_name ........ (str|None, optional, defaults to None) The 101 | given (first) name of the person the 102 | instance represents. 103 | initials .......... (str|None, optional, defaults to None) The 104 | middle initial(s) of the person the 105 | instance represents. 106 | family_name ....... (str|None, optional, defaults to None) The 107 | family (last) name of the person the 108 | instance represents. 109 | email_address ..... (str|None, optional, defaults to None) The 110 | email address of the person the 111 | instance represents. 112 | """ 113 | self.given_name = given_name 114 | self.initials = initials 115 | self.family_name = family_name 116 | self.email_address = email_address 117 | 118 | # STRING RENDERING (a convenience for debugging/display 119 | # purposes) 120 | def __str__(self): 121 | return ( 122 | '<%s at %s (given_name=%s initials=%s ' 123 | 'family_name=%s email_address=%s)>' % 124 | ( 125 | self.__class__.__name__, hex(id(self)), 126 | self.given_name, self.initials, 127 | self.family_name, self.email_address 128 | ) 129 | ) 130 | 131 | def send_email(self, message): 132 | raise NotImplementedError( 133 | 'Person.send_email has not been implemented yet.' 134 | ) 135 | 136 | class Student(Person): 137 | """ 138 | Represents a Student in a post-secondary context/setting 139 | """ 140 | 141 | # PROPERTY DEFINITIONS 142 | # - Major -- The declared major for a student, if any 143 | @property 144 | def major(self): 145 | try: 146 | return self._major 147 | except AttributeError: 148 | return None 149 | @major.setter 150 | def major(self, value): 151 | self._major = value 152 | @major.deleter 153 | def major(self): 154 | try: 155 | del self._major 156 | except AttributeError: 157 | pass 158 | 159 | # - Minor -- The declared minor for a student, if any 160 | @property 161 | def minor(self): 162 | try: 163 | return self._minor 164 | except AttributeError: 165 | return None 166 | @minor.setter 167 | def minor(self, value): 168 | self._minor = value 169 | @minor.deleter 170 | def minor(self): 171 | try: 172 | del self._minor 173 | except AttributeError: 174 | pass 175 | 176 | # - Student ID -- The unique identifier of a student 177 | @property 178 | def student_id(self): 179 | try: 180 | return self._student_id 181 | except AttributeError: 182 | return None 183 | @student_id.setter 184 | def student_id(self, value): 185 | self._student_id = value 186 | @student_id.deleter 187 | def student_id(self): 188 | try: 189 | del self._student_id 190 | except AttributeError: 191 | pass 192 | 193 | # OBJECT INITIALIZATION 194 | def __init__(self, 195 | student_id=0, 196 | # - Arguments from Person 197 | given_name=None, initials=None, family_name=None, 198 | email_address=None, 199 | # - Other student-specific arguments 200 | major=None, minor=None 201 | ): 202 | """ 203 | Object initialization 204 | student_id ........ (int|None, optional, defaults to 0 [zero]) 205 | The unique identifying number of the student 206 | given_name ........ (str|None, optional, defaults to None) The 207 | given (first) name of the student the 208 | instance represents. 209 | initials .......... (str|None, optional, defaults to None) The 210 | middle initial(s) of the student the 211 | instance represents. 212 | family_name ....... (str|None, optional, defaults to None) The 213 | family (last) name of the student the 214 | instance represents. 215 | email_address ..... (str|None, optional, defaults to None) The 216 | email address of the student the 217 | instance represents. 218 | major ............. (str|None, optional, defaults to None) The 219 | declared major (if any) of the student the 220 | instance represents. 221 | minor ............. (str|None, optional, defaults to None) The 222 | declared minor (if any) of the student the 223 | instance represents. 224 | """ 225 | self.given_name = given_name 226 | self.initials = initials 227 | self.family_name = family_name 228 | self.email_address = email_address 229 | self.student_id = student_id 230 | self.major = major 231 | self.minor = minor 232 | 233 | def get_schedule(self): 234 | raise NotImplementedError( 235 | '%s.get_schedule has not been implemented yet.' % 236 | (self.__class__.__name__) 237 | ) 238 | 239 | if __name__ == '__main__': 240 | 241 | ### Looking at the interfaces of both classes 242 | # - Gather up all of the members of both classes 243 | import inspect 244 | all_members = { 245 | str(Person):{ 246 | 'properties':{ 247 | name:str(item) for (name, item) 248 | in inspect.getmembers(Person, inspect.isdatadescriptor) 249 | if name != '__weakref__' 250 | }, 251 | 'methods':{ 252 | name:str(item) for (name, item) 253 | in inspect.getmembers(Person, inspect.isfunction) 254 | }, 255 | }, 256 | str(Student):{ 257 | 'properties':{ 258 | name:str(item) for (name, item) 259 | in inspect.getmembers(Student, inspect.isdatadescriptor) 260 | if name != '__weakref__' 261 | }, 262 | 'methods':{ 263 | name:str(item) for (name, item) 264 | in inspect.getmembers(Student, inspect.isfunction) 265 | }, 266 | } 267 | } 268 | # - Print it out in a reasonably easy-to-follow format 269 | import json 270 | print(json.dumps(all_members, indent=4)) 271 | 272 | ### Create and display some instances 273 | # - Person 274 | me = Person('Brian', 'D', 'Allbee') 275 | print(me) 276 | 277 | # - Student 278 | my_student = Student( 279 | 1, 'Brooke', None, 'Owens', None, 'CIS', None 280 | ) 281 | print(my_student) 282 | 283 | # - Show that send_email is calling Person.send_email for both 284 | # types of instances 285 | try: 286 | me.send_email('some message') 287 | except Exception as error: 288 | print('%s: %s' % (error.__class__.__name__, error)) 289 | 290 | try: 291 | my_student.send_email('some message') 292 | except Exception as error: 293 | print('%s: %s' % (error.__class__.__name__, error)) 294 | -------------------------------------------------------------------------------- /Chapter03/C03R02_MethodOverriding.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | The complete code of Ch. 3, Recipe 2 -- Overriding inherited members 4 | """ 5 | 6 | class Person: 7 | """ 8 | Represents a Person 9 | """ 10 | 11 | # PROPERTY DEFINITIONS 12 | # - Email address 13 | @property 14 | def email_address(self): 15 | try: 16 | return self._email_address 17 | except AttributeError: 18 | return None 19 | @email_address.setter 20 | def email_address(self, value): 21 | self._email_address = value 22 | @email_address.deleter 23 | def email_address(self): 24 | try: 25 | del self._email_address 26 | except AttributeError: 27 | pass 28 | 29 | # - Family name 30 | @property 31 | def family_name(self): 32 | try: 33 | return self._family_name 34 | except AttributeError: 35 | return None 36 | @family_name.setter 37 | def family_name(self, value): 38 | self._family_name = value 39 | @family_name.deleter 40 | def family_name(self): 41 | try: 42 | del self._family_name 43 | except AttributeError: 44 | pass 45 | 46 | # - Full name, a calculated read-only property 47 | @property 48 | def full_name(self): 49 | results = [] 50 | if self.given_name != None: 51 | results.append(self.given_name) 52 | if self.initials != None: 53 | results.append(self.initials) 54 | if self.family_name != None: 55 | results.append(self.family_name) 56 | if results: 57 | return ' '.join(results) 58 | 59 | # - Given name 60 | @property 61 | def given_name(self): 62 | try: 63 | return self._given_name 64 | except AttributeError: 65 | return None 66 | @given_name.setter 67 | def given_name(self, value): 68 | self._given_name = value 69 | @given_name.deleter 70 | def given_name(self): 71 | try: 72 | del self._given_name 73 | except AttributeError: 74 | pass 75 | 76 | # - Initials 77 | @property 78 | def initials(self): 79 | try: 80 | return self._initials 81 | except AttributeError: 82 | return None 83 | @initials.setter 84 | def initials(self, value): 85 | self._initials = value 86 | @initials.deleter 87 | def initials(self): 88 | try: 89 | del self._initials 90 | except AttributeError: 91 | pass 92 | 93 | # OBJECT INITIALIZATION 94 | def __init__(self, 95 | given_name=None, initials=None, family_name=None, 96 | email_address=None 97 | ): 98 | """ 99 | Object initialization 100 | given_name ........ (str|None, optional, defaults to None) The 101 | given (first) name of the person the 102 | instance represents. 103 | initials .......... (str|None, optional, defaults to None) The 104 | middle initial(s) of the person the 105 | instance represents. 106 | family_name ....... (str|None, optional, defaults to None) The 107 | family (last) name of the person the 108 | instance represents. 109 | email_address ..... (str|None, optional, defaults to None) The 110 | email address of the person the 111 | instance represents. 112 | """ 113 | self.given_name = given_name 114 | self.initials = initials 115 | self.family_name = family_name 116 | self.email_address = email_address 117 | 118 | # STRING RENDERING (a convenience for debugging/display 119 | # purposes) 120 | def __str__(self): 121 | return ( 122 | '<%s at %s (given_name=%s initials=%s ' 123 | 'family_name=%s email_address=%s)>' % 124 | ( 125 | self.__class__.__name__, hex(id(self)), 126 | self.given_name, self.initials, 127 | self.family_name, self.email_address 128 | ) 129 | ) 130 | 131 | def send_email(self, message): 132 | raise NotImplementedError( 133 | '%s.send_email has not been implemented yet.' % 134 | (self.__class__.__name__) 135 | ) 136 | 137 | class Student(Person): 138 | """ 139 | Represents a Student in a post-secondary context/setting 140 | """ 141 | 142 | # PROPERTY DEFINITIONS 143 | # - Full name, a calculated read-only property 144 | # OVERRIDDEN from Person to include student_id 145 | @property 146 | def full_name(self): 147 | results = [] 148 | if self.given_name != None: 149 | results.append(self.given_name) 150 | if self.initials != None: 151 | results.append(self.initials) 152 | if self.family_name != None: 153 | results.append(self.family_name) 154 | if self.student_id != None: 155 | results.append('[%s]' % self.student_id) 156 | if results: 157 | return ' '.join(results) 158 | 159 | # - Major -- The declared major for a student, if any 160 | @property 161 | def major(self): 162 | try: 163 | return self._major 164 | except AttributeError: 165 | return None 166 | @major.setter 167 | def major(self, value): 168 | self._major = value 169 | @major.deleter 170 | def major(self): 171 | try: 172 | del self._major 173 | except AttributeError: 174 | pass 175 | 176 | # - Minor -- The declared minor for a student, if any 177 | @property 178 | def minor(self): 179 | try: 180 | return self._minor 181 | except AttributeError: 182 | return None 183 | @minor.setter 184 | def minor(self, value): 185 | self._minor = value 186 | @minor.deleter 187 | def minor(self): 188 | try: 189 | del self._minor 190 | except AttributeError: 191 | pass 192 | 193 | # - Student ID -- The unique identifier of a student 194 | @property 195 | def student_id(self): 196 | try: 197 | return self._student_id 198 | except AttributeError: 199 | return None 200 | @student_id.setter 201 | def student_id(self, value): 202 | self._student_id = value 203 | @student_id.deleter 204 | def student_id(self): 205 | try: 206 | del self._student_id 207 | except AttributeError: 208 | pass 209 | 210 | # OBJECT INITIALIZATION 211 | def __init__(self, 212 | student_id=0, 213 | # - Arguments from Person 214 | given_name=None, initials=None, family_name=None, 215 | email_address=None, 216 | # - Other student-specific arguments 217 | major=None, minor=None 218 | ): 219 | """ 220 | Object initialization 221 | student_id ........ (int|None, optional, defaults to 0 [zero]) 222 | The unique identifying number of the student 223 | given_name ........ (str|None, optional, defaults to None) The 224 | given (first) name of the student the 225 | instance represents. 226 | initials .......... (str|None, optional, defaults to None) The 227 | middle initial(s) of the student the 228 | instance represents. 229 | family_name ....... (str|None, optional, defaults to None) The 230 | family (last) name of the student the 231 | instance represents. 232 | email_address ..... (str|None, optional, defaults to None) The 233 | email address of the student the 234 | instance represents. 235 | major ............. (str|None, optional, defaults to None) The 236 | declared major (if any) of the student the 237 | instance represents. 238 | minor ............. (str|None, optional, defaults to None) The 239 | declared minor (if any) of the student the 240 | instance represents. 241 | """ 242 | # - We can use super() to call the parent class' __init__ 243 | super().__init__( 244 | given_name, initials, family_name, email_address 245 | ) 246 | # - But we ALSO need to initialize properties that are 247 | # members of THIS class 248 | self.student_id = student_id 249 | self.major = major 250 | self.minor = minor 251 | 252 | def get_schedule(self): 253 | raise NotImplementedError( 254 | '%s.get_schedule has not been implemented yet.' % 255 | (self.__class__.__name__) 256 | ) 257 | 258 | # STRING RENDERING (a convenience for debugging/display 259 | # purposes) OVERRIDDEN from Person to include Student- 260 | # specific data 261 | def __str__(self): 262 | return ( 263 | '<%s at %s (student_id=%s given_name=%s initials=%s ' 264 | 'family_name=%s email_address=%s major=%s ' 265 | 'minor=%s)>' % 266 | ( 267 | self.__class__.__name__, hex(id(self)), 268 | self.student_id, self.given_name, self.initials, 269 | self.family_name, self.email_address, 270 | self.major, self.minor 271 | ) 272 | ) 273 | 274 | if __name__ == '__main__': 275 | 276 | 277 | ### Looking at the interfaces of both classes 278 | # - Gather up all of the members of both classes 279 | import inspect 280 | all_members = { 281 | str(Person):{ 282 | 'properties':{ 283 | name:str(item) for (name, item) 284 | in inspect.getmembers(Person, inspect.isdatadescriptor) 285 | if name != '__weakref__' 286 | }, 287 | 'methods':{ 288 | name:str(item) for (name, item) 289 | in inspect.getmembers(Person, inspect.isfunction) 290 | }, 291 | }, 292 | str(Student):{ 293 | 'properties':{ 294 | name:str(item) for (name, item) 295 | in inspect.getmembers(Student, inspect.isdatadescriptor) 296 | if name != '__weakref__' 297 | }, 298 | 'methods':{ 299 | name:str(item) for (name, item) 300 | in inspect.getmembers(Student, inspect.isfunction) 301 | }, 302 | } 303 | } 304 | # - Print it out in a reasonably easy-to-follow format 305 | import json 306 | print(json.dumps(all_members, indent=4)) 307 | 308 | raise RuntimeError 309 | 310 | me = Person('Brian', 'D', 'Allbee') 311 | print(me) 312 | 313 | my_student = Student( 314 | 1, 'Brooke', None, 'Owens', None, 'CIS', None 315 | ) 316 | print(my_student) 317 | print(my_student.full_name) 318 | 319 | try: 320 | me.send_email('some message') 321 | except Exception as error: 322 | print('%s: %s' % (error.__class__.__name__, error)) 323 | 324 | try: 325 | my_student.send_email('some message') 326 | except Exception as error: 327 | print('%s: %s' % (error.__class__.__name__, error)) 328 | 329 | try: 330 | me.get_schedule() 331 | except Exception as error: 332 | print('%s: %s' % (error.__class__.__name__, error)) 333 | 334 | try: 335 | my_student.get_schedule() 336 | except Exception as error: 337 | print('%s: %s' % (error.__class__.__name__, error)) 338 | -------------------------------------------------------------------------------- /Chapter03/C03R03_MultipleInheritance.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | The complete code of Ch. 3, Recipe 3 -- Inheriting from 4 | multiple parent classes 5 | """ 6 | 7 | class DataObject: 8 | """ 9 | Provides common functionality for objects that can store their 10 | state-data in some kind of back-end data-store (a database, a 11 | file-system, whatever). 12 | """ 13 | 14 | # OBJECT INITIALIZATION 15 | def __init__(self): 16 | print( 17 | '%s calling DataObject.__init__' % 18 | self.__class__.__name__ 19 | ) 20 | 21 | # NOTE: The members implemented here don't actually DO 22 | # anything at this point, but can be called to 23 | # prove that they are being inherited... 24 | 25 | # CRUD Operations (Create, Read, Update and Delete) 26 | # - Create 27 | def create(self): 28 | raise NotImplementedError( 29 | '%s.create (from DataObject) has not been ' 30 | 'implemented yet' % (self.__class__.__name__) 31 | ) 32 | # - Read -- defined as a class-method so that no existing 33 | # instance is required to read an instance 34 | @classmethod 35 | def read(cls): 36 | raise NotImplementedError( 37 | '%s.read (from DataObject) has not been ' 38 | 'implemented yet' % (cls.__name__) 39 | ) 40 | # - Update 41 | def update(self): 42 | raise NotImplementedError( 43 | '%s.update (from DataObject) has not been ' 44 | 'implemented yet' % (self.__class__.__name__) 45 | ) 46 | # - Delete -- also defined as a class-method so that no 47 | # existing instance is required for deletion 48 | @classmethod 49 | def delete(cls): 50 | raise NotImplementedError( 51 | '%s.delete (from DataObject) has not been ' 52 | 'implemented yet' % (cls.__name__) 53 | ) 54 | 55 | class Person: 56 | """ 57 | Represents a Person 58 | """ 59 | 60 | # PROPERTY DEFINITIONS 61 | # - Email address 62 | @property 63 | def email_address(self): 64 | try: 65 | return self._email_address 66 | except AttributeError: 67 | return None 68 | @email_address.setter 69 | def email_address(self, value): 70 | self._email_address = value 71 | @email_address.deleter 72 | def email_address(self): 73 | try: 74 | del self._email_address 75 | except AttributeError: 76 | pass 77 | 78 | # - Family name 79 | @property 80 | def family_name(self): 81 | try: 82 | return self._family_name 83 | except AttributeError: 84 | return None 85 | @family_name.setter 86 | def family_name(self, value): 87 | self._family_name = value 88 | @family_name.deleter 89 | def family_name(self): 90 | try: 91 | del self._family_name 92 | except AttributeError: 93 | pass 94 | 95 | # - Full name, a calculated read-only property 96 | @property 97 | def full_name(self): 98 | results = [] 99 | if self.given_name != None: 100 | results.append(self.given_name) 101 | if self.initials != None: 102 | results.append(self.initials) 103 | if self.family_name != None: 104 | results.append(self.family_name) 105 | if results: 106 | return ' '.join(results) 107 | 108 | # - Given name 109 | @property 110 | def given_name(self): 111 | try: 112 | return self._given_name 113 | except AttributeError: 114 | return None 115 | @given_name.setter 116 | def given_name(self, value): 117 | self._given_name = value 118 | @given_name.deleter 119 | def given_name(self): 120 | try: 121 | del self._given_name 122 | except AttributeError: 123 | pass 124 | 125 | # - Initials 126 | @property 127 | def initials(self): 128 | try: 129 | return self._initials 130 | except AttributeError: 131 | return None 132 | @initials.setter 133 | def initials(self, value): 134 | self._initials = value 135 | @initials.deleter 136 | def initials(self): 137 | try: 138 | del self._initials 139 | except AttributeError: 140 | pass 141 | 142 | # OBJECT INITIALIZATION 143 | def __init__(self, 144 | given_name=None, initials=None, family_name=None, 145 | email_address=None 146 | ): 147 | """ 148 | Object initialization 149 | given_name ........ (str|None, optional, defaults to None) The 150 | given (first) name of the person the 151 | instance represents. 152 | initials .......... (str|None, optional, defaults to None) The 153 | middle initial(s) of the person the 154 | instance represents. 155 | family_name ....... (str|None, optional, defaults to None) The 156 | family (last) name of the person the 157 | instance represents. 158 | email_address ..... (str|None, optional, defaults to None) The 159 | email address of the person the 160 | instance represents. 161 | """ 162 | self.given_name = given_name 163 | self.initials = initials 164 | self.family_name = family_name 165 | self.email_address = email_address 166 | 167 | # STRING RENDERING (a convenience for debugging/display 168 | # purposes) 169 | def __str__(self): 170 | return ( 171 | '<%s at %s (given_name=%s initials=%s ' 172 | 'family_name=%s email_address=%s)>' % 173 | ( 174 | self.__class__.__name__, hex(id(self)), 175 | self.given_name, self.initials, 176 | self.family_name, self.email_address 177 | ) 178 | ) 179 | 180 | def send_email(self, message): 181 | raise NotImplementedError( 182 | '%s.send_email has not been implemented yet.' % 183 | (self.__class__.__name__) 184 | ) 185 | 186 | class Student(Person, DataObject): 187 | """ 188 | Represents a Student in a post-secondary context/setting 189 | """ 190 | 191 | # PROPERTY DEFINITIONS 192 | # - Full name, a calculated read-only property 193 | # OVERRIDDEN from Person to include student_id 194 | @property 195 | def full_name(self): 196 | results = [] 197 | if self.given_name != None: 198 | results.append(self.given_name) 199 | if self.initials != None: 200 | results.append(self.initials) 201 | if self.family_name != None: 202 | results.append(self.family_name) 203 | if self.student_id != None: 204 | results.append('[%s]' % self.student_id) 205 | if results: 206 | return ' '.join(results) 207 | 208 | # - Major -- The declared major for a student, if any 209 | @property 210 | def major(self): 211 | try: 212 | return self._major 213 | except AttributeError: 214 | return None 215 | @major.setter 216 | def major(self, value): 217 | self._major = value 218 | @major.deleter 219 | def major(self): 220 | try: 221 | del self._major 222 | except AttributeError: 223 | pass 224 | 225 | # - Minor -- The declared minor for a student, if any 226 | @property 227 | def minor(self): 228 | try: 229 | return self._minor 230 | except AttributeError: 231 | return None 232 | @minor.setter 233 | def minor(self, value): 234 | self._minor = value 235 | @minor.deleter 236 | def minor(self): 237 | try: 238 | del self._minor 239 | except AttributeError: 240 | pass 241 | 242 | # - Student ID -- The unique identifier of a student 243 | @property 244 | def student_id(self): 245 | try: 246 | return self._student_id 247 | except AttributeError: 248 | return None 249 | @student_id.setter 250 | def student_id(self, value): 251 | self._student_id = value 252 | @student_id.deleter 253 | def student_id(self): 254 | try: 255 | del self._student_id 256 | except AttributeError: 257 | pass 258 | 259 | # OBJECT INITIALIZATION 260 | def __init__(self, 261 | student_id=0, 262 | # - Arguments from Person 263 | given_name=None, initials=None, family_name=None, 264 | email_address=None, 265 | # - Other student-specific arguments 266 | major=None, minor=None 267 | ): 268 | """ 269 | Object initialization 270 | student_id ........ (int|None, optional, defaults to 0 [zero]) 271 | The unique identifying number of the student 272 | given_name ........ (str|None, optional, defaults to None) The 273 | given (first) name of the student the 274 | instance represents. 275 | initials .......... (str|None, optional, defaults to None) The 276 | middle initial(s) of the student the 277 | instance represents. 278 | family_name ....... (str|None, optional, defaults to None) The 279 | family (last) name of the student the 280 | instance represents. 281 | email_address ..... (str|None, optional, defaults to None) The 282 | email address of the student the 283 | instance represents. 284 | major ............. (str|None, optional, defaults to None) The 285 | declared major (if any) of the student the 286 | instance represents. 287 | minor ............. (str|None, optional, defaults to None) The 288 | declared minor (if any) of the student the 289 | instance represents. 290 | """ 291 | # - We can't use super in this case, because there are 292 | # multiple parent classes. Each parent's __init__ has 293 | # to be called explicitly instead: 294 | Person.__init__( 295 | self, given_name, initials, family_name, email_address 296 | ) 297 | DataObject.__init__(self) 298 | # - But we ALSO need to initialize properties that are 299 | # members of THIS class 300 | self.student_id = student_id 301 | self.major = major 302 | self.minor = minor 303 | 304 | def get_schedule(self): 305 | raise NotImplementedError( 306 | '%s.get_schedule has not been implemented yet.' % 307 | (self.__class__.__name__) 308 | ) 309 | 310 | # STRING RENDERING (a convenience for debugging/display 311 | # purposes) OVERRIDDEN from Person to include Student- 312 | # specific data 313 | def __str__(self): 314 | return ( 315 | '<%s at %s (student_id=%s given_name=%s initials=%s ' 316 | 'family_name=%s email_address=%s major=%s ' 317 | 'minor=%s)>' % 318 | ( 319 | self.__class__.__name__, hex(id(self)), 320 | self.student_id, self.given_name, self.initials, 321 | self.family_name, self.email_address, 322 | self.major, self.minor 323 | ) 324 | ) 325 | 326 | if __name__ == '__main__': 327 | 328 | my_student = Student( 329 | 1, 'Brooke', None, 'Owens', None, 'CIS', None 330 | ) 331 | print(my_student) 332 | 333 | try: 334 | my_student.send_email('some message') 335 | except Exception as error: 336 | print('%s: %s' % (error.__class__.__name__, error)) 337 | 338 | try: 339 | my_student.create() 340 | except Exception as error: 341 | print('%s: %s' % (error.__class__.__name__, error)) 342 | 343 | try: 344 | Student.read() 345 | except Exception as error: 346 | print('%s: %s' % (error.__class__.__name__, error)) 347 | 348 | try: 349 | my_student.update() 350 | except Exception as error: 351 | print('%s: %s' % (error.__class__.__name__, error)) 352 | 353 | try: 354 | Student.delete() 355 | except Exception as error: 356 | print('%s: %s' % (error.__class__.__name__, error)) 357 | 358 | person = Person() 359 | try: 360 | person.create() 361 | except Exception as error: 362 | print('%s: %s' % (error.__class__.__name__, error)) 363 | 364 | # - How Python solves the diamond problem 365 | class RootClass: 366 | def some_method(self): 367 | print('RootClass.some_method called') 368 | 369 | class SubclassA(RootClass): 370 | def some_method(self): 371 | print('SubclassA.some_method called') 372 | 373 | class SubclassB(RootClass): 374 | def some_method(self): 375 | print('SubclassB.some_method called') 376 | 377 | class FinalClass1(SubclassA, SubclassB): 378 | pass 379 | 380 | class FinalClass2(SubclassB, SubclassA): 381 | pass 382 | 383 | final_instance1 = FinalClass1() 384 | final_instance1.some_method() 385 | final_instance2 = FinalClass2() 386 | final_instance2.some_method() 387 | -------------------------------------------------------------------------------- /Chapter03/C03R04_CreatingInterfaces.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | The complete code of Ch. 3, Recipe 5 -- Creating interface requirements 4 | """ 5 | 6 | # - Import the abc module to provide abstraction mechanisms 7 | import abc 8 | 9 | class BasePerson(metaclass=abc.ABCMeta): 10 | """ 11 | Provides baseline functionality, interface requirements, and 12 | type-identity for objects that can represent any of several types 13 | of people in a system 14 | """ 15 | 16 | # PROPERTY DEFINITIONS 17 | # - Email address 18 | @property 19 | def email_address(self): 20 | try: 21 | return self._email_address 22 | except AttributeError: 23 | return None 24 | @email_address.setter 25 | def email_address(self, value): 26 | self._email_address = value 27 | @email_address.deleter 28 | def email_address(self): 29 | try: 30 | del self._email_address 31 | except AttributeError: 32 | pass 33 | 34 | # - Family name 35 | @property 36 | def family_name(self): 37 | try: 38 | return self._family_name 39 | except AttributeError: 40 | return None 41 | @family_name.setter 42 | def family_name(self, value): 43 | self._family_name = value 44 | @family_name.deleter 45 | def family_name(self): 46 | try: 47 | del self._family_name 48 | except AttributeError: 49 | pass 50 | 51 | # - Full name, an abstract property definition here, 52 | # that will require an implementation in derived classes 53 | @abc.abstractproperty 54 | def full_name(self): 55 | # - Methods in Python, even if they are abstract, 56 | # must have *something* in their body, even if 57 | # it's just a "pass" statement. Just to be on 58 | # the safe side, we'll raise a NotImplementedError 59 | # in abstract members... 60 | raise NotImplementedError( 61 | '%s.full_name has not been implemented, as required ' 62 | 'by BasePerson' % (self.__class__.__name__) 63 | ) 64 | 65 | # - Given name 66 | @property 67 | def given_name(self): 68 | try: 69 | return self._given_name 70 | except AttributeError: 71 | return None 72 | @given_name.setter 73 | def given_name(self, value): 74 | self._given_name = value 75 | @given_name.deleter 76 | def given_name(self): 77 | try: 78 | del self._given_name 79 | except AttributeError: 80 | pass 81 | 82 | # - Initials 83 | @property 84 | def initials(self): 85 | try: 86 | return self._initials 87 | except AttributeError: 88 | return None 89 | @initials.setter 90 | def initials(self, value): 91 | self._initials = value 92 | @initials.deleter 93 | def initials(self): 94 | try: 95 | del self._initials 96 | except AttributeError: 97 | pass 98 | 99 | # OBJECT INITIALIZATION 100 | def __init__(self, 101 | given_name=None, initials=None, family_name=None, 102 | email_address=None 103 | ): 104 | """ 105 | Object initialization 106 | given_name ........ (str|None, optional, defaults to None) The 107 | given (first) name of the person the 108 | instance represents. 109 | initials .......... (str|None, optional, defaults to None) The 110 | middle initial(s) of the person the 111 | instance represents. 112 | family_name ....... (str|None, optional, defaults to None) The 113 | family (last) name of the person the 114 | instance represents. 115 | email_address ..... (str|None, optional, defaults to None) The 116 | email address of the person the 117 | instance represents. 118 | """ 119 | self.given_name = given_name 120 | self.initials = initials 121 | self.family_name = family_name 122 | self.email_address = email_address 123 | 124 | # STRING RENDERING (a convenience for debugging/display 125 | # purposes) 126 | # - This is made abstract because *how* the derived classes' 127 | # string-representations get rendered is expected to vary. 128 | # - Note that we're *not* raising a NotImplementedError here; 129 | # we *could*, as was done above with full_name, but it's 130 | # not *required* 131 | @abc.abstractmethod 132 | def __str__(self): 133 | """ 134 | Requires implementation of a __str__ method in derived classes. 135 | The output should look something like: 136 | <[ClassName] at [id(self) as hex-number] ([property=value...])> 137 | """ 138 | pass 139 | 140 | def send_email(self, message): 141 | raise NotImplementedError( 142 | '%s.send_email has not been implemented yet.' % 143 | (self.__class__.__name__) 144 | ) 145 | 146 | class Faculty(BasePerson): 147 | """ 148 | Represents a faculty-member in a post-secondary context/setting 149 | """ 150 | 151 | # PROPERTY DEFINITIONS 152 | # - Full name, a calculated read-only property 153 | # Implemented as required by BasePerson, includes 154 | # student_id 155 | @property 156 | def full_name(self): 157 | results = [] 158 | if self.prefix != None: 159 | results.append(self.prefix) 160 | if self.given_name != None: 161 | results.append(self.given_name) 162 | if self.initials != None: 163 | results.append(self.initials) 164 | if self.family_name != None: 165 | results.append(self.family_name) 166 | if self.suffix != None: 167 | results.append(', %s' % self.suffix) 168 | if results: 169 | return ' '.join(results).replace(' ,', ',') 170 | 171 | # - Department -- The department that the faculty-person 172 | # is a member of 173 | @property 174 | def department(self): 175 | try: 176 | return self._department 177 | except AttributeError: 178 | return None 179 | @department.setter 180 | def department(self, value): 181 | self._department = value 182 | @department.deleter 183 | def department(self): 184 | try: 185 | del self._department 186 | except AttributeError: 187 | pass 188 | 189 | # - Prefix -- The prefix of the faculty-member, if any 190 | @property 191 | def prefix(self): 192 | try: 193 | return self._prefix 194 | except AttributeError: 195 | return None 196 | @prefix.setter 197 | def prefix(self, value): 198 | self._prefix = value 199 | @prefix.deleter 200 | def prefix(self): 201 | try: 202 | del self._prefix 203 | except AttributeError: 204 | pass 205 | 206 | # - Suffix -- The suffix of the faculty-member, if any 207 | @property 208 | def suffix(self): 209 | try: 210 | return self._suffix 211 | except AttributeError: 212 | return None 213 | @suffix.setter 214 | def suffix(self, value): 215 | self._suffix = value 216 | @suffix.deleter 217 | def suffix(self): 218 | try: 219 | del self._suffix 220 | except AttributeError: 221 | pass 222 | 223 | # OBJECT INITIALIZATION 224 | def __init__(self, 225 | # - Putting "prefix" first 226 | prefix=None, 227 | # - Arguments from Person 228 | given_name=None, initials=None, family_name=None, 229 | # - More local arguments 230 | suffix=None, 231 | # - More Person arguments 232 | email_address=None, 233 | # - Other faculty-specific arguments 234 | department=None 235 | ): 236 | """ 237 | Object initialization 238 | prefix ............ (str|None, optional, defaults to None) The 239 | prefix (Dr., Prof., etc.) of the faculty- 240 | member the instance represents, if any. 241 | given_name ........ (str|None, optional, defaults to None) The 242 | given (first) name of the faculty-member the 243 | instance represents. 244 | initials .......... (str|None, optional, defaults to None) The 245 | middle initial(s) of the faculty-member the 246 | instance represents. 247 | family_name ....... (str|None, optional, defaults to None) The 248 | family (last) name of the faculty-member the 249 | instance represents. 250 | suffix ............ (str|None, optional, defaults to None) The 251 | suffix (PhD, etc.) of the faculty-member the 252 | instance represents, if any. 253 | email_address ..... (str|None, optional, defaults to None) The 254 | email address of the faculty-member the 255 | instance represents. 256 | department ........ (str|None, optional, defaults to None) The 257 | department that the faculty-member the 258 | instance represents is a member of, if any. 259 | """ 260 | # - We can use super() to call the parent class' __init__ 261 | # because there's only one parent class... 262 | super().__init__( 263 | given_name, initials, family_name, email_address 264 | ) 265 | # - But we ALSO need to initialize properties that are 266 | # members of THIS class 267 | self.department = department 268 | self.prefix = prefix 269 | self.suffix = suffix 270 | 271 | def get_schedule(self): 272 | raise NotImplementedError( 273 | '%s.get_schedule has not been implemented yet.' % 274 | (self.__class__.__name__) 275 | ) 276 | 277 | # STRING RENDERING (a convenience for debugging/display 278 | # purposes) 279 | # Implemented as required by BasePerson, includes student_id 280 | def __str__(self): 281 | return ( 282 | '<%s at %s (prefix=%s given_name=%s initials=%s ' 283 | 'family_name=%s suffix=%s email_address=%s ' 284 | 'department=%s)>' % 285 | ( 286 | self.__class__.__name__, hex(id(self)), 287 | self.prefix, self.given_name, self.initials, 288 | self.family_name, self.suffix, 289 | self.email_address, self.department 290 | ) 291 | ) 292 | 293 | class Staff(BasePerson): 294 | """ 295 | Represents a staff-member in a post-secondary context/setting 296 | """ 297 | 298 | # PROPERTY DEFINITIONS 299 | # - Full name, a calculated read-only property 300 | # Implemented as required by BasePerson, includes 301 | # student_id 302 | @property 303 | def full_name(self): 304 | results = [] 305 | if self.given_name != None: 306 | results.append(self.given_name) 307 | if self.initials != None: 308 | results.append(self.initials) 309 | if self.family_name != None: 310 | results.append(self.family_name) 311 | if results: 312 | return ' '.join(results) 313 | 314 | # - Department -- The department that the faculty-person 315 | # is a member of 316 | @property 317 | def department(self): 318 | try: 319 | return self._department 320 | except AttributeError: 321 | return None 322 | @department.setter 323 | def department(self, value): 324 | self._department = value 325 | @department.deleter 326 | def department(self): 327 | try: 328 | del self._department 329 | except AttributeError: 330 | pass 331 | 332 | # - Prefix -- The prefix of the faculty-member, if any 333 | @property 334 | def prefix(self): 335 | try: 336 | return self._prefix 337 | except AttributeError: 338 | return None 339 | @prefix.setter 340 | def prefix(self, value): 341 | self._prefix = value 342 | @prefix.deleter 343 | def prefix(self): 344 | try: 345 | del self._prefix 346 | except AttributeError: 347 | pass 348 | 349 | # - Suffix -- The suffix of the faculty-member, if any 350 | @property 351 | def suffix(self): 352 | try: 353 | return self._suffix 354 | except AttributeError: 355 | return None 356 | @suffix.setter 357 | def suffix(self, value): 358 | self._suffix = value 359 | @suffix.deleter 360 | def suffix(self): 361 | try: 362 | del self._suffix 363 | except AttributeError: 364 | pass 365 | 366 | # OBJECT INITIALIZATION 367 | def __init__(self, 368 | # - Arguments from Person 369 | given_name=None, initials=None, family_name=None, 370 | email_address=None, 371 | # - Other staff-specific arguments 372 | department=None 373 | ): 374 | """ 375 | Object initialization 376 | given_name ........ (str|None, optional, defaults to None) The 377 | given (first) name of the faculty-member the 378 | instance represents. 379 | initials .......... (str|None, optional, defaults to None) The 380 | middle initial(s) of the faculty-member the 381 | instance represents. 382 | family_name ....... (str|None, optional, defaults to None) The 383 | family (last) name of the faculty-member the 384 | instance represents. 385 | email_address ..... (str|None, optional, defaults to None) The 386 | email address of the faculty-member the 387 | instance represents. 388 | department ........ (str|None, optional, defaults to None) The 389 | department that the faculty-member the 390 | instance represents is a member of, if any. 391 | """ 392 | # - We can use super() to call the parent class' __init__ 393 | # because there's only one parent class... 394 | super().__init__( 395 | given_name, initials, family_name, email_address 396 | ) 397 | # - But we ALSO need to initialize properties that are 398 | # members of THIS class 399 | self.department = department 400 | 401 | def get_schedule(self): 402 | raise NotImplementedError( 403 | '%s.get_schedule has not been implemented yet.' % 404 | (self.__class__.__name__) 405 | ) 406 | 407 | # STRING RENDERING (a convenience for debugging/display 408 | # purposes) 409 | # Implemented as required by BasePerson, includes student_id 410 | def __str__(self): 411 | return ( 412 | '<%s at %s (given_name=%s initials=%s family_name=%s ' 413 | 'email_address=%s department=%s)>' % 414 | ( 415 | self.__class__.__name__, hex(id(self)), 416 | self.given_name, self.initials, self.family_name, 417 | self.email_address, self.department 418 | ) 419 | ) 420 | 421 | class Student(BasePerson): 422 | """ 423 | Represents a Student in a post-secondary context/setting 424 | """ 425 | # PROPERTY DEFINITIONS 426 | # - Full name, a calculated read-only property 427 | # Implemented as required by BasePerson, includes 428 | # student_id 429 | @property 430 | def full_name(self): 431 | results = [] 432 | if self.given_name != None: 433 | results.append(self.given_name) 434 | if self.initials != None: 435 | results.append(self.initials) 436 | if self.family_name != None: 437 | results.append(self.family_name) 438 | if self.student_id != None: 439 | results.append('[%s]' % self.student_id) 440 | if results: 441 | return ' '.join(results) 442 | 443 | # - Major -- The declared major for a student, if any 444 | @property 445 | def major(self): 446 | try: 447 | return self._major 448 | except AttributeError: 449 | return None 450 | @major.setter 451 | def major(self, value): 452 | self._major = value 453 | @major.deleter 454 | def major(self): 455 | try: 456 | del self._major 457 | except AttributeError: 458 | pass 459 | 460 | # - Minor -- The declared minor for a student, if any 461 | @property 462 | def minor(self): 463 | try: 464 | return self._minor 465 | except AttributeError: 466 | return None 467 | @minor.setter 468 | def minor(self, value): 469 | self._minor = value 470 | @minor.deleter 471 | def minor(self): 472 | try: 473 | del self._minor 474 | except AttributeError: 475 | pass 476 | 477 | # - Student ID -- The unique identifier of a student 478 | @property 479 | def student_id(self): 480 | try: 481 | return self._student_id 482 | except AttributeError: 483 | return None 484 | @student_id.setter 485 | def student_id(self, value): 486 | self._student_id = value 487 | @student_id.deleter 488 | def student_id(self): 489 | try: 490 | del self._student_id 491 | except AttributeError: 492 | pass 493 | 494 | # OBJECT INITIALIZATION 495 | def __init__(self, 496 | student_id=0, 497 | # - Arguments from Person 498 | given_name=None, initials=None, family_name=None, 499 | email_address=None, 500 | # - Other student-specific arguments 501 | major=None, minor=None 502 | ): 503 | """ 504 | Object initialization 505 | student_id ........ (int|None, optional, defaults to 0 [zero]) 506 | The unique identifying number of the student 507 | given_name ........ (str|None, optional, defaults to None) The 508 | given (first) name of the student the 509 | instance represents. 510 | initials .......... (str|None, optional, defaults to None) The 511 | middle initial(s) of the student the 512 | instance represents. 513 | family_name ....... (str|None, optional, defaults to None) The 514 | family (last) name of the student the 515 | instance represents. 516 | email_address ..... (str|None, optional, defaults to None) The 517 | email address of the student the 518 | instance represents. 519 | major ............. (str|None, optional, defaults to None) The 520 | declared major (if any) of the student the 521 | instance represents. 522 | minor ............. (str|None, optional, defaults to None) The 523 | declared minor (if any) of the student the 524 | instance represents. 525 | """ 526 | # - We can use super() to call the parent class' __init__ 527 | # because there's only one parent class... 528 | super().__init__( 529 | given_name, initials, family_name, email_address 530 | ) 531 | # - But we ALSO need to initialize properties that are 532 | # members of THIS class 533 | self.student_id = student_id 534 | self.major = major 535 | self.minor = minor 536 | 537 | def get_schedule(self): 538 | raise NotImplementedError( 539 | '%s.get_schedule has not been implemented yet.' % 540 | (self.__class__.__name__) 541 | ) 542 | 543 | # STRING RENDERING (a convenience for debugging/display 544 | # purposes) 545 | # Implemented as required by BasePerson, includes student_id 546 | def __str__(self): 547 | return ( 548 | '<%s at %s (student_id=%s given_name=%s initials=%s ' 549 | 'family_name=%s email_address=%s major=%s ' 550 | 'minor=%s)>' % 551 | ( 552 | self.__class__.__name__, hex(id(self)), 553 | self.student_id, self.given_name, self.initials, 554 | self.family_name, self.email_address, 555 | self.major, self.minor 556 | ) 557 | ) 558 | 559 | if __name__ == '__main__': 560 | 561 | class ExamplePerson(BasePerson): 562 | pass 563 | 564 | try: 565 | me = ExamplePerson('Brian', 'D', 'Allbee', None) 566 | except Exception as error: 567 | print('%s: %s' % (error.__class__.__name__, error)) 568 | 569 | my_student = Student( 570 | 1, 'Brooke', None, 'Owens', None, 'CIS', None 571 | ) 572 | print(my_student) 573 | print(my_student.full_name) 574 | 575 | my_faculty = Faculty( 576 | 'Dr.', 'Chris', 'L', 'Powell', 'PhD', 'clpowell@uu.edu', 577 | 'CIS' 578 | ) 579 | print(my_faculty) 580 | print(my_faculty.full_name) 581 | 582 | my_staff = Staff( 583 | 'Edwin', None, 'Coyne', 'ecoyne@uu.edu', 'Lab Assistant' 584 | ) 585 | print(my_staff) 586 | print(my_staff.full_name) 587 | -------------------------------------------------------------------------------- /Chapter03/C03R05_EncapulateWhatVaries.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | The complete code of Ch. 3, Recipe 6 -- Encapsulating what varies 4 | """ 5 | 6 | import abc 7 | 8 | # - Define a base class that captures all the common aspects 9 | # of ANY asset 10 | 11 | class BaseAsset(metaclass=abc.ABCMeta): 12 | """ 13 | Provides baseline functionality, interface requirements and type- 14 | identity for objects that can represent a digital asset 15 | """ 16 | 17 | # PROPERTY DEFINITIONS 18 | @abc.abstractproperty 19 | def metadata(self) -> dict: 20 | """ 21 | Gets the metadata associated with the asset 22 | """ 23 | raise NotImplementedError( 24 | '%s.metadata has not been implemented as required by ' 25 | 'BaseAsset' % (self.__class__.__name__) 26 | ) 27 | 28 | # OBJECT INITIALIZATION 29 | def __init__(self, asset_file:str): 30 | if self.__class__ == BaseAsset: 31 | raise NotImplementedError( 32 | 'BaseAsset is a foundational ABC, and should ' 33 | 'not be instantiated' 34 | ) 35 | self.asset_file = asset_file 36 | 37 | # REQUIRED/ABSTRACT METHODS 38 | @abc.abstractmethod 39 | def generate_previews(self) -> list: 40 | """ 41 | Requires that derived classes implement a method to generate a 42 | set of preview-images of the asset. 43 | """ 44 | # NOTE: The expectation is that assets will have one to 45 | # many preview-images associated with them - image- 46 | # assets will have one, other assets will have one 47 | # (at minimum), but may have several! 48 | raise NotImplementedError( 49 | '%s.generate_previews has not been implemented as ' 50 | 'required by BaseAsset' % (self.__class__.__name__) 51 | ) 52 | 53 | # - Define the intermediate classes, that provide concrete 54 | # implementations of BaseAsset functionality where possible 55 | 56 | class ImageAsset(BaseAsset): 57 | """ 58 | Provides concrete implementation of functionality required by 59 | BaseAsset that is common to all assets that are images of some 60 | sort (JPG, PNG, etc.) 61 | """ 62 | 63 | def __init__(self, asset_file:str): 64 | if self.__class__ == ImageAsset: 65 | raise NotImplementedError( 66 | 'ImageAsset is an intermediate ABC, and should ' 67 | 'not be instantiated' 68 | ) 69 | BaseAsset.__init__(self, asset_file) 70 | 71 | @property 72 | def metadata(self) -> dict: 73 | """Gets the metadata associated with the instance""" 74 | try: 75 | return self._metadata 76 | except AttributeError: 77 | self._metadata = { 78 | 'mime_type':self.__class__._mime_type, 79 | 'asset_file':self.asset_file, 80 | 'pages':1, 81 | 'original_resolution':0, 82 | 'original_height':0, 83 | 'original_width':0, 84 | 'original_color_space':'TBD', 85 | } 86 | return self._metadata 87 | 88 | def generate_previews(self) -> list: 89 | """Generates a set of preview-images of the asset.""" 90 | # NOTE: This assumes a single common mechanism, like 91 | # ImageMagick or the Pillow library is available 92 | # and can do what is needed 93 | # - For now, we'll just print what needs to happen 94 | print( 95 | '+- %s.generate_previews called' % 96 | (self.__class__.__name__) 97 | ) 98 | print(' +- Get original file') 99 | print(' +- Resize and convert to output format') 100 | print(' +- Return list (one item) of preview-files') 101 | 102 | class LayoutAsset(BaseAsset): 103 | """ 104 | Provides concrete implementation of functionality required by 105 | BaseAsset that is common to all assets that are layout-documents 106 | of some sort (InDesign INDD files, for example) 107 | """ 108 | 109 | def __init__(self, asset_file:str): 110 | if self.__class__ == LayoutAsset: 111 | raise NotImplementedError( 112 | 'LayoutAsset is an intermediate ABC, and should ' 113 | 'not be instantiated' 114 | ) 115 | 116 | @property 117 | def metadata(self) -> dict: 118 | """Gets the metadata associated with the instance""" 119 | try: 120 | return self._metadata 121 | except AttributeError: 122 | self._metadata = { 123 | 'mime_type':self.__class__._mime_type, 124 | 'asset_file':self.asset_file, 125 | 'pages':0, 126 | 'original_height':0, 127 | 'original_width':0, 128 | 'original_color_space':'TBD' 129 | } 130 | return self._metadata 131 | 132 | def generate_previews(self) -> list: 133 | """Generates a set of preview-images of the asset.""" 134 | # NOTE: This assumes a single common mechanism, like 135 | # GhostScript is available, and can convert PDFs 136 | # into a series of page-images. 137 | # It also requires the same image-manipulation 138 | # capabilities that ImageAsset does, to resize 139 | # those page-images. 140 | # - For now, we'll just print what needs to happen 141 | print( 142 | '+- %s.generate_previews called' % 143 | (self.__class__.__name__) 144 | ) 145 | print(' +- Get original file-name') 146 | print( '+- Find and fetch a PDF with the same name') 147 | print(' +- If no PDF is available, raise an error') 148 | print(' +- "Print" each page to a high-res image') 149 | print(' +- Resize/convert each to output format') 150 | print(' +- Return list of preview-files') 151 | 152 | class PresentationAsset(BaseAsset): 153 | """ 154 | Provides concrete implementation of functionality required by 155 | BaseAsset that is common to all assets that are presentation- 156 | documents of some sort (PowerPoint, OpenDocument odp files, etc.) 157 | """ 158 | 159 | def __init__(self, asset_file:str): 160 | if self.__class__ == PresentationAsset: 161 | raise NotImplementedError( 162 | 'PresentationAsset is an intermediate ABC, and ' 163 | 'should not be instantiated' 164 | ) 165 | BaseAsset.__init__(self, asset_file) 166 | 167 | @property 168 | def metadata(self) -> dict: 169 | """Gets the metadata associated with the instance""" 170 | try: 171 | return self._metadata 172 | except AttributeError: 173 | self._metadata = { 174 | 'mime_type':self.__class__._mime_type, 175 | 'asset_file':self.asset_file, 176 | 'pages':0, 177 | 'original_height':0, 178 | 'original_width':0, 179 | 'original_color_space':'TBD' 180 | } 181 | return self._metadata 182 | 183 | def generate_previews(self) -> list: 184 | """Generates a set of preview-images of the asset.""" 185 | # NOTE: This assumes a single common mechanism, like 186 | # LibreOffice (headless) is available that can 187 | # read and render documents to a PDF. 188 | # It also requires GhostScript or an equivalent, 189 | # like LayoutAsset and PDFAsset do, to generate 190 | # high-res page-images. 191 | # It ALSO requires the same image-manipulation 192 | # capabilities that ImageAsset does, to resize 193 | # those page-images. 194 | # - For now, we'll just print what needs to happen 195 | print( 196 | '+- %s.generate_previews called' % 197 | (self.__class__.__name__) 198 | ) 199 | print(' +- Get original file') 200 | print(' +- Open with LibreOffice and convert to PDF') 201 | print(' +- "Print" each PDF page to a high-res image') 202 | print(' +- Resize/convert each to output format') 203 | print(' +- Return list of preview-files') 204 | 205 | class WordProcessingAsset(BaseAsset): 206 | """ 207 | Provides concrete implementation of functionality required by 208 | BaseAsset that is common to all assets that are word-processor 209 | documents of some sort (MS Word, or OpenDocument odt files, 210 | for example) 211 | """ 212 | 213 | def __init__(self, asset_file:str): 214 | if self.__class__ == WordProcessingAsset: 215 | raise NotImplementedError( 216 | 'WordProcessingAsset is an intermediate ABC, ' 217 | 'and should not be instantiated' 218 | ) 219 | 220 | @property 221 | def metadata(self) -> dict: 222 | """Gets the metadata associated with the instance""" 223 | try: 224 | return self._metadata 225 | except AttributeError: 226 | self._metadata = { 227 | 'mime_type':self.__class__._mime_type, 228 | 'asset_file':self.asset_file, 229 | 'pages':0, 230 | 'original_height':0, 231 | 'original_width':0, 232 | 'original_color_space':'TBD' 233 | } 234 | return self._metadata 235 | 236 | def generate_previews(self) -> list: 237 | """Generates a set of preview-images of the asset.""" 238 | # NOTE: Same notes/requirements as PresentationAsset 239 | # - For now, we'll just print what needs to happen 240 | print( 241 | '+- %s.generate_previews called' % 242 | (self.__class__.__name__) 243 | ) 244 | print(' +- Get original file') 245 | print(' +- Open with LibreOffice and convert to PDF') 246 | print(' +- "Print" each PDF page to a high-res image') 247 | print(' +- Resize/convert each to output format') 248 | print(' +- Return list of preview-files') 249 | 250 | # - Define the concrete classes 251 | 252 | class DOCXAsset(WordProcessingAsset): 253 | 254 | _mime_type = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' 255 | 256 | def __init__(self, asset_file:str): 257 | WordProcessingAsset.__init__(self, asset_file) 258 | 259 | class JPEGAsset(ImageAsset): 260 | 261 | _mime_type = 'image/jpeg' 262 | 263 | def __init__(self, asset_file:str): 264 | ImageAsset.__init__(self, asset_file) 265 | 266 | class INDDAsset(LayoutAsset): 267 | 268 | _mime_type = 'application/octet-stream' 269 | 270 | def __init__(self, asset_file:str): 271 | LayoutAsset.__init__(self, asset_file) 272 | 273 | class ODPAsset(PresentationAsset): 274 | 275 | _mime_type = ( 276 | 'application/vnd.oasis.opendocument.presentation' 277 | ) 278 | 279 | def __init__(self, asset_file:str): 280 | PresentationAsset.__init__(self, asset_file) 281 | 282 | class ODTAsset(WordProcessingAsset): 283 | 284 | _mime_type = 'application/vnd.oasis.opendocument.text' 285 | 286 | def __init__(self, asset_file:str): 287 | WordProcessingAsset.__init__(self, asset_file) 288 | 289 | class PNGAsset(ImageAsset): 290 | 291 | _mime_type = 'image/png' 292 | 293 | def __init__(self, asset_file:str): 294 | ImageAsset.__init__(self, asset_file) 295 | 296 | class PPTXAsset(PresentationAsset): 297 | 298 | _mime_type = ( 299 | 'application/vnd.openxmlformats-officedocument.' 300 | 'presentationml.presentation' 301 | ) 302 | 303 | def __init__(self, asset_file:str): 304 | PresentationAsset.__init__(self, asset_file) 305 | 306 | class PDFAsset(BaseAsset): 307 | """ 308 | Provides concrete implementation of functionality required by 309 | BaseAsset for PDF assets 310 | """ 311 | 312 | _mime_type = 'application/pdf' 313 | 314 | def __init__(self, asset_file:str): 315 | BaseAsset.__init__(self, asset_file) 316 | 317 | @property 318 | def metadata(self) -> dict: 319 | """Gets the metadata associated with the instance""" 320 | try: 321 | return self._metadata 322 | except AttributeError: 323 | self._metadata = { 324 | 'mime_type':self.__class__._mime_type, 325 | 'asset_file':self.asset_file, 326 | 'pages':0, 327 | 'original_height':0, 328 | 'original_width':0, 329 | 'original_color_space':'TBD' 330 | } 331 | return self._metadata 332 | 333 | def generate_previews(self) -> list: 334 | """Generates a set of preview-images of the asset.""" 335 | # NOTE: Same notes as LayoutAsset's generate_previews 336 | # - For now, we'll just print what needs to happen 337 | print( 338 | '%s.generate_previews called' % 339 | (self.__class__.__name__) 340 | ) 341 | print('+- Get original file') 342 | print('+- "Print" each page to a high-res image') 343 | print(' +- Resize/convert each to output format') 344 | print('+- Return list of preview-files') 345 | 346 | # - Also define a "generic" asset-type to deal with unrecognized 347 | # file-/asset-types 348 | class GenericAsset(BaseAsset): 349 | 350 | _mime_type = 'application/octet-stream' 351 | 352 | def __init__(self, asset_file:str): 353 | BaseAsset.__init__(self, asset_file) 354 | 355 | @property 356 | def metadata(self) -> dict: 357 | """Gets the metadata associated with the instance""" 358 | try: 359 | return self._metadata 360 | except AttributeError: 361 | self._metadata = { 362 | 'mime_type':self.__class__._mime_type, 363 | 'asset_file':self.asset_file, 364 | } 365 | return self._metadata 366 | 367 | def generate_previews(self) -> list: 368 | """Generates a set of preview-images of the asset.""" 369 | # NOTE: Same notes as LayoutAsset's generate_previews 370 | # - For now, we'll just print what needs to happen 371 | print( 372 | '+- %s.generate_previews called' % 373 | (self.__class__.__name__) 374 | ) 375 | print(' +- Cannot generate a preview') 376 | 377 | if __name__ == '__main__': 378 | # - Get the list of asset-files in the asset_files directory 379 | import os 380 | asset_files = os.listdir('asset_files') 381 | assets = {} 382 | for asset in asset_files: 383 | # - The sequence here is trying to make sure that more 384 | # common asset-types are identified first, just in case 385 | # the asset-object creation-process is lengthy... 386 | asset_ext = os.path.splitext(asset.lower())[-1] 387 | if asset_ext == '.pptx': 388 | assets[asset] = PPTXAsset(asset) 389 | elif asset_ext == '.odp': 390 | assets[asset] = ODPAsset(asset) 391 | elif asset_ext in ('.jpg', '.jpeg'): 392 | assets[asset] = JPEGAsset(asset) 393 | elif asset_ext in ('.indd', '.ind'): 394 | assets[asset] = LayoutAsset(asset) 395 | elif asset_ext == '.pdf': 396 | assets[asset] = PDFAsset(asset) 397 | elif asset_ext == '.png': 398 | assets[asset] = PNGAsset(asset) 399 | elif asset_ext == '.docx': 400 | assets[asset] = DOCXAsset(asset) 401 | elif asset_ext == '.odt': 402 | assets[asset] = ODTAsset(asset) 403 | else: 404 | assets[asset] = GenericAsset(asset) 405 | # - Print a nicely-formatted list of assets 406 | if assets: 407 | print('%d assets:' % len(assets)) 408 | for asset in sorted(assets): 409 | print( 410 | ( 411 | '%s' % assets[asset].__class__.__name__ 412 | ).ljust(23, '.') 413 | + ' %s' % asset 414 | ) 415 | print(assets[asset].metadata) 416 | assets[asset].generate_previews() 417 | else: 418 | print('No assets found in asset_files directory') 419 | -------------------------------------------------------------------------------- /Chapter03/README.md: -------------------------------------------------------------------------------- 1 | # Chapter 3 — The Pillars of Object Oriented Programming 2 | 3 | > Part of the "Python Object Oriented Programming Cookbook" title, 4 | > published by Packt. 5 | 6 | > Code in this chapter was originally written against version 3.6.6 of Python, 7 | > as installed by default on the Author's then-current Linux Mint 19 (Cinnamon) 8 | > installation. 9 | 10 | [Recipe 1: **Basic inheritance**](C03R01_PersonStudent.py) — 11 | Description 12 | 13 | [Recipe 2: **Overriding inherited members**](C03R02_MethodOverriding.py) — 14 | Description 15 | 16 | [Recipe 3: **Inheriting from multiple parent classes**](C03R03_MultipleInheritance.py) — 17 | Description 18 | 19 | [Recipe 4: **Creating interface requirements**](C03R04_CreatingInterfaces.py) — 20 | Description 21 | 22 | [Recipe 5: **Encapsulating what varies**](C03R05_EncapulateWhatVaries.py) — 23 | Description 24 | 25 | -------------------------------------------------------------------------------- /Chapter03/asset_files/dna_of_candy.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Object-Oriented-Programming-Cookbook/4840b0ee9e155c8ed664886c0aad20d44d48dac2/Chapter03/asset_files/dna_of_candy.pdf -------------------------------------------------------------------------------- /Chapter03/asset_files/dna_of_candy.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Object-Oriented-Programming-Cookbook/4840b0ee9e155c8ed664886c0aad20d44d48dac2/Chapter03/asset_files/dna_of_candy.pptx -------------------------------------------------------------------------------- /Chapter03/asset_files/example.odp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Object-Oriented-Programming-Cookbook/4840b0ee9e155c8ed664886c0aad20d44d48dac2/Chapter03/asset_files/example.odp -------------------------------------------------------------------------------- /Chapter03/asset_files/jellybellies.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Object-Oriented-Programming-Cookbook/4840b0ee9e155c8ed664886c0aad20d44d48dac2/Chapter03/asset_files/jellybellies.jpg -------------------------------------------------------------------------------- /Chapter04/C04R01_MetaclassBasics.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | The complete code of Ch. 4, Recipe 1 -- 4 | Taking control of class creation with metaclasses 5 | """ 6 | 7 | print('Defining the BasicMetaclass metaclass') 8 | 9 | class BasicMetaclass(type): 10 | def __new__(cls, *args, **kwargs): 11 | # - Print some information, so that what's happening 12 | # is visible 13 | print('BasicMetaclass.__new__ called:') 14 | # - The args passed are the name of the new class, the 15 | # base classes that the new class inherits from, and 16 | # a dict of items that includes the module, the 17 | # qualified name of the class, and a reference to 18 | # the __init__ to be called... 19 | print('+- *args ....... %s' % str(args)) 20 | print('+- *kwargs ..... %s' % str(kwargs)) 21 | # - Delegate to superclass for actual object-creation 22 | new_class = super().__new__(cls, *args, **kwargs) 23 | print('+- new_class ... %s' % new_class) 24 | # - Return the new class 25 | return new_class 26 | 27 | print('BasicMetaclass defined') 28 | 29 | print('Defining the UsesMeta class that uses the metaclass') 30 | 31 | class UsesMeta(metaclass=BasicMetaclass): 32 | def __init__(self, arg, *args, **kwargs): 33 | print('UsesMeta.__init__ called:') 34 | print('+- *arg ........ %s' % arg) 35 | print('+- *args ....... %s' % str(args)) 36 | print('+- *kwargs ..... %s' % str(kwargs)) 37 | 38 | print('UsesMeta defined') 39 | 40 | instance = UsesMeta('argument', 'args1', 'args2', keyword='value') 41 | 42 | print(type(instance)) 43 | -------------------------------------------------------------------------------- /Chapter04/C04R02_ClassRequirementMetaclass.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | The complete code of Ch. 4, Recipe 2 -- 4 | Enforcing class requirements with metaclass 5 | """ 6 | 7 | ### Enforcing class-attribute requirements with a metaclass 8 | # - This class, intended to be used as a metaclass, requires 9 | # that a CLASS DEFINITION using it include some required 10 | # class-attributes. This requirement is enforced DURING 11 | # THE DEFINITION OF THE CLASS, rather than during instantiation 12 | # of an instance of the class because it's implemented in 13 | # __new__ 14 | 15 | class RequireParms(type): 16 | # - Define the attribute-names that are required when this 17 | # metaclass is attached to another class 18 | _required_attrs = ( 19 | '_required', # - Some required attribute 20 | '_also_required', # - Another required attribute 21 | ) 22 | 23 | def __new__(cls, *args, **kwargs): 24 | # - Delegate to superclass for actual object-creation 25 | new_class = super().__new__(cls, *args, **kwargs) 26 | # - Check for required class-attributes 27 | for required_attribute in \ 28 | RequireParms._required_attrs: 29 | if not hasattr(new_class, required_attribute) \ 30 | or getattr(new_class, required_attribute) == None: 31 | raise AttributeError( 32 | '%s does not supply a non-None %s attribute' % 33 | (new_class.__name__, required_attribute) 34 | ) 35 | return new_class 36 | 37 | if __name__ == '__main__': 38 | # - This class-definition, for example, will fail, because it 39 | # doesn't have the required class-attributes defined... 40 | try: 41 | class Oook(metaclass=RequireParms): 42 | # - Uncomment these to see how the tests progress 43 | #_required = 'Required attribute' 44 | #_also_required = 'Another required attribute' 45 | pass 46 | except Exception as error: 47 | print('%s: %s' % (error.__class__.__name__, error)) 48 | 49 | # - This one will work, though -- It *does* define the 50 | # required class-attributes... 51 | class Eeek(metaclass=RequireParms): 52 | _required = True 53 | _also_required = 1 54 | 55 | print( 56 | '_required: %s; _also_required: %s' % 57 | (Eeek._required, Eeek._also_required) 58 | ) 59 | test_instance = Eeek() 60 | print( 61 | '_required: %s; _also_required: %s' % 62 | (test_instance._required, test_instance._also_required) 63 | ) 64 | -------------------------------------------------------------------------------- /Chapter04/C04R03_SubclassRegistrationMetaclass.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | The complete code of Ch. 4, Recipe 3 -- 4 | Using metaclass to automatically register a subclass 5 | """ 6 | 7 | import abc 8 | 9 | file_extensions = {} 10 | mime_types = {} 11 | 12 | class AssetMeta(abc.ABCMeta): 13 | # - Because AssetMeta is being applied to classes that derive 14 | # from ABCMeta, *this* class must also derive from 15 | # ABCMeta... 16 | # - Classes that represent assets must provide a list of file- 17 | # extentions and mime-types that can be used to map to the 18 | # classes that can represent assets of those types 19 | _required_attributes = ( 20 | 'file_extensions', 21 | 'mime_types', 22 | ) 23 | # - Override the constructor 24 | def __new__(cls, *args, **kwargs): 25 | new_class = super().__new__(cls, *args, **kwargs) 26 | # - Check for required class-attributes 27 | for required_attribute in AssetMeta._required_attributes: 28 | if not hasattr(new_class, required_attribute) \ 29 | or type(getattr(new_class, required_attribute)) \ 30 | != list: 31 | raise AttributeError( 32 | '%s does not supply a non-None %s ' 33 | 'attribute' % 34 | (new_class.__name__, required_attribute) 35 | ) 36 | # - If the class has the required attributes, then 37 | # register it with those values 38 | # - By file-extension 39 | for ext in new_class.file_extensions: 40 | # - Check to make sure that the extension isn't 41 | # already registered 42 | if file_extensions.get(ext): 43 | raise AttributeError( 44 | 'The "%s" file-extension cannot be ' 45 | 'registered with AssetClassifier: an asset-' 46 | 'class has already been registered with ' 47 | 'it (%s)' % ( 48 | ext, file_extensions[ext] 49 | ) 50 | ) 51 | file_extensions[ext] = new_class 52 | # - By MIME-type 53 | for ext in new_class.mime_types: 54 | # - Check to make sure that the mime-type isn't 55 | # already registered 56 | if mime_types.get(ext): 57 | raise AttributeError( 58 | 'The "%s" MIME-type cannot be registered ' 59 | 'with AssetClassifier: an asset-class has ' 60 | 'already been registered with it (%s)' % 61 | (ext, mime_types[ext]) 62 | ) 63 | mime_types[ext] = new_class 64 | return new_class 65 | 66 | class BaseAsset(metaclass=abc.ABCMeta): 67 | """ 68 | Provides baseline functionality, interface requirements and type- 69 | identity for objects that can represent a digital asset 70 | """ 71 | 72 | # PROPERTY DEFINITIONS (Not implemented, since implementation 73 | # isn't needed for the recipe) 74 | # - 75 | @abc.abstractproperty 76 | def metadata(self): 77 | """ 78 | Gets the metadata associated with the asset 79 | """ 80 | raise NotImplementedError( 81 | '%s.metadata has not been implemented as required by ' 82 | 'BaseAsset' % (self.__class__.__name__) 83 | ) 84 | 85 | # OBJECT INITIALIZATION 86 | def __init__(self, asset_file): 87 | self.asset_file = asset_file 88 | 89 | # REQUIRED/ABSTRACT METHODS 90 | @abc.abstractmethod 91 | def generate_previews(self): 92 | """ 93 | Requires that derived classes implement a method to generate a 94 | set of preview-images of the asset. 95 | """ 96 | # NOTE: The expectation is that assets will have one to 97 | # many preview-images associated with them - image- 98 | # assets will have one, presentation assets will 99 | # have one (minimum), but may have several! 100 | raise NotImplementedError( 101 | '%s.generate_previews has not been implemented as ' 102 | 'required by BaseAsset' % (self.__class__.__name__) 103 | ) 104 | 105 | # - Define the intermediate classes, that provide concrete 106 | # implementations of BaseAsset functionality where possible 107 | 108 | class ImageAsset(BaseAsset): 109 | """ 110 | Provides concrete implementation of functionality required by 111 | BaseAsset that is common to all assets that are images of some 112 | sort (JPG, PNG, etc.) 113 | """ 114 | 115 | @property 116 | def metadata(self): 117 | """Gets the metadata associated with the instance""" 118 | try: 119 | return self._metadata 120 | except AttributeError: 121 | self._metadata = { 122 | 'pages':1, 123 | 'original_resolution':0, 124 | 'original_height':0, 125 | 'original_width':0, 126 | 'original_color_space':'TBD' 127 | } 128 | return self._metadata 129 | 130 | def generate_previews(self): 131 | """Generates a set of preview-images of the asset.""" 132 | # NOTE: This assumes a single common mechanism, like 133 | # ImageMagick or the Pillow library is available 134 | # and can do what is needed 135 | # - For now, we'll just print what needs to happen 136 | print( 137 | '+- %s.generate_previews called' % 138 | (self.__class__.__name__) 139 | ) 140 | print(' +- Get original file') 141 | print(' +- Resize and convert to output format') 142 | print(' +- Return list (one item) of preview-files') 143 | 144 | class LayoutAsset(BaseAsset): 145 | """ 146 | Provides concrete implementation of functionality required by 147 | BaseAsset that is common to all assets that are layout-documents 148 | of some sort (InDesign INDD files, for example) 149 | """ 150 | 151 | @property 152 | def metadata(self): 153 | """Gets the metadata associated with the instance""" 154 | try: 155 | return self._metadata 156 | except AttributeError: 157 | self._metadata = { 158 | 'pages':0, 159 | 'original_height':0, 160 | 'original_width':0, 161 | 'original_color_space':'TBD' 162 | } 163 | return self._metadata 164 | 165 | def generate_previews(self): 166 | """Generates a set of preview-images of the asset.""" 167 | # NOTE: This assumes a single common mechanism, like 168 | # GhostScript is available, and can convert PDFs 169 | # into a series of page-images. 170 | # It also requires the same image-manipulation 171 | # capabilities that ImageAsset does, to resize 172 | # those page-images. 173 | # - For now, we'll just print what needs to happen 174 | print( 175 | '+- %s.generate_previews called' % 176 | (self.__class__.__name__) 177 | ) 178 | print(' +- Get original file-name') 179 | print( '+- Find and fetch a PDF with the same name') 180 | print(' +- If no PDF is available, raise an error') 181 | print(' +- "Print" each page to a high-res image') 182 | print(' +- Resize/convert each to output format') 183 | print(' +- Return list of preview-files') 184 | 185 | class PresentationAsset(BaseAsset): 186 | """ 187 | Provides concrete implementation of functionality required by 188 | BaseAsset that is common to all assets that are presentation- 189 | documents of some sort (PowerPoint, OpenDocument odp files, etc.) 190 | """ 191 | 192 | @property 193 | def metadata(self): 194 | """Gets the metadata associated with the instance""" 195 | try: 196 | return self._metadata 197 | except AttributeError: 198 | self._metadata = { 199 | 'pages':0, 200 | 'original_height':0, 201 | 'original_width':0, 202 | 'original_color_space':'TBD' 203 | } 204 | return self._metadata 205 | 206 | def generate_previews(self): 207 | """Generates a set of preview-images of the asset.""" 208 | # NOTE: This assumes a single common mechanism, like 209 | # LibreOffice (headless) is available that can 210 | # read and render documents to a PDF. 211 | # It also requires GhostScript or an equivalent, 212 | # like LayoutAsset and PDFAsset do, to generate 213 | # high-res page-images. 214 | # It ALSO requires the same image-manipulation 215 | # capabilities that ImageAsset does, to resize 216 | # those page-images. 217 | # - For now, we'll just print what needs to happen 218 | print( 219 | '+- %s.generate_previews called' % 220 | (self.__class__.__name__) 221 | ) 222 | print(' +- Get original file') 223 | print(' +- Open with LibreOffice and convert to PDF') 224 | print(' +- "Print" each PDF page to a high-res image') 225 | print(' +- Resize/convert each to output format') 226 | print(' +- Return list of preview-files') 227 | 228 | class WordProcessingAsset(BaseAsset): 229 | """ 230 | Provides concrete implementation of functionality required by 231 | BaseAsset that is common to all assets that are word-processor 232 | documents of some sort (MS Word, or OpenDocument odt files, 233 | for example) 234 | """ 235 | 236 | @property 237 | def metadata(self): 238 | """Gets the metadata associated with the instance""" 239 | try: 240 | return self._metadata 241 | except AttributeError: 242 | self._metadata = { 243 | 'pages':0, 244 | 'original_height':0, 245 | 'original_width':0, 246 | 'original_color_space':'TBD' 247 | } 248 | return self._metadata 249 | 250 | def generate_previews(self): 251 | """Generates a set of preview-images of the asset.""" 252 | # NOTE: Same notes/requirements as PresentationAsset 253 | # - For now, we'll just print what needs to happen 254 | print( 255 | '+- %s.generate_previews called' % 256 | (self.__class__.__name__) 257 | ) 258 | print(' +- Get original file') 259 | print(' +- Open with LibreOffice and convert to PDF') 260 | print(' +- "Print" each PDF page to a high-res image') 261 | print(' +- Resize/convert each to output format') 262 | print(' +- Return list of preview-files') 263 | 264 | # - Define the concrete classes 265 | 266 | class DOCXAsset(WordProcessingAsset, metaclass=AssetMeta): 267 | file_extensions=['.doc', '.docx'] 268 | mime_types = [ 269 | 'application/msword', 270 | 'application/vnd.openxmlformats-officedocument.' 271 | 'wordprocessingml.document' 272 | ] 273 | 274 | class JPEGAsset(ImageAsset, metaclass=AssetMeta): 275 | file_extensions=['.jpg', '.jpeg'] 276 | mime_types = ['image/jpeg'] 277 | 278 | class INDDAsset(LayoutAsset, metaclass=AssetMeta): 279 | file_extensions=['.ind', '.indd'] 280 | mime_types = [] 281 | 282 | class ODPAsset(PresentationAsset, metaclass=AssetMeta): 283 | file_extensions=['.odp'] 284 | mime_types = [ 285 | 'application/vnd.oasis.opendocument.presentation' 286 | ] 287 | 288 | class ODTAsset(WordProcessingAsset, metaclass=AssetMeta): 289 | file_extensions=['.odt'] 290 | mime_types = [ 291 | 'application/vnd.oasis.opendocument.text' 292 | ] 293 | 294 | class PNGAsset(ImageAsset, metaclass=AssetMeta): 295 | file_extensions=['.png'] 296 | mime_types = ['image/png'] 297 | 298 | class PPTXAsset(PresentationAsset, metaclass=AssetMeta): 299 | file_extensions=['.ppt', '.pptx'] 300 | mime_types = [ 301 | 'application/vnd.ms-powerpoint', 302 | 'application/vnd.openxmlformats-officedocument.' 303 | 'presentationml.presentation' 304 | ] 305 | 306 | class PDFAsset(BaseAsset, metaclass=AssetMeta): 307 | """ 308 | Provides concrete implementation of functionality required by 309 | BaseAsset for PDF assets 310 | """ 311 | 312 | file_extensions=['.pdf'] 313 | mime_types = ['application/pdf'] 314 | 315 | @property 316 | def metadata(self): 317 | """Gets the metadata associated with the instance""" 318 | try: 319 | return self._metadata 320 | except AttributeError: 321 | self._metadata = { 322 | 'pages':0, 323 | 'original_height':0, 324 | 'original_width':0, 325 | 'original_color_space':'TBD' 326 | } 327 | return self._metadata 328 | 329 | def generate_previews(self): 330 | """Generates a set of preview-images of the asset.""" 331 | # NOTE: Same notes as LayoutAsset's generate_previews 332 | # - For now, we'll just print what needs to happen 333 | print( 334 | '%s.generate_previews called' % 335 | (self.__class__.__name__) 336 | ) 337 | print('+- Get original file') 338 | print('+- "Print" each page to a high-res image') 339 | print(' +- Resize/convert each to output format') 340 | print('+- Return list of preview-files') 341 | 342 | # - Also define a "generic" asset-type to deal with unrecognized 343 | # file-/asset-types 344 | class GenericAsset(BaseAsset, metaclass=AssetMeta): 345 | 346 | file_extensions=['.*'] 347 | mime_types = ['application/octet-stream'] 348 | 349 | @property 350 | def metadata(self): 351 | """Gets the metadata associated with the instance""" 352 | try: 353 | return self._metadata 354 | except AttributeError: 355 | self._metadata = {} 356 | return self._metadata 357 | 358 | def generate_previews(self): 359 | """Generates a set of preview-images of the asset.""" 360 | # NOTE: Same notes as LayoutAsset's generate_previews 361 | # - For now, we'll just print what needs to happen 362 | print( 363 | '+- %s.generate_previews called' % 364 | (self.__class__.__name__) 365 | ) 366 | print(' +- Cannot generate a preview') 367 | 368 | if __name__ == '__main__': 369 | from pprint import pprint 370 | pprint(file_extensions) 371 | pprint(mime_types) 372 | 373 | # - This preserves the abstraction too... If, for example, 374 | # GenericAsset.generate_previews is commented out or 375 | # renamed, and we try to create an instance: 376 | generic = GenericAsset('') 377 | # - It will raise: 378 | # TypeError: Can't instantiate abstract class GenericAsset with abstract methods generate_previews 379 | -------------------------------------------------------------------------------- /Chapter04/C04R04_EmulatingNumericType.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | The complete code of Ch. 4, Recipe 4 -- 4 | Emulating a numeric type with magic methods 5 | """ 6 | 7 | class IPv4Address: 8 | """Represents an IPv4 Address""" 9 | 10 | _min_range = 0 11 | _max_range = 256**4-1 12 | 13 | def __init__(self, value:(int,str)): 14 | if type(value) == str: 15 | octets = [int(o) for o in value.split('.')[0:4]] 16 | if len(octets) != 4: 17 | raise ValueError() 18 | octets.reverse() 19 | value = sum( 20 | [ 21 | value * 256**power for (power, value) 22 | in enumerate(octets) 23 | ] 24 | ) 25 | if type(value) == int: 26 | if value < self._min_range or value > self._max_range: 27 | raise ValueError() 28 | self._int_value = value 29 | else: 30 | raise TypeError() 31 | 32 | def __str__(self): 33 | octets = [] 34 | value = self._int_value 35 | for power in range(0,4): 36 | octet, value = (value % 256, int(value/256)) 37 | octets.insert(0, octet) 38 | return '.'.join([str(o) for o in octets]) 39 | 40 | def __repr__(self): 41 | return ( 42 | '<%s at %s (%s)>' % 43 | ( 44 | self.__class__.__name__, hex(id(self)), 45 | self.__str__() 46 | ) 47 | ) 48 | 49 | def __int__(self): 50 | return self._int_value 51 | 52 | # - Other methods that complete the numeric-type emulation 53 | def __add__ (self, other): 54 | result = self._int_value + int(other) 55 | if result < self._min_range or result > self._max_range: 56 | raise ValueError( 57 | 'The result of this operation would result in an ' 58 | 'invalid IPv4 address' 59 | ) 60 | return IPv4Address(result) 61 | 62 | def __sub__ (self, other): 63 | result = self._int_value - int(other) 64 | if result < self._min_range or result > self._max_range: 65 | raise ValueError( 66 | 'The result of this operation would result in an ' 67 | 'invalid IPv4 address' 68 | ) 69 | return IPv4Address(result) 70 | 71 | def __mul__ (self, other): 72 | result = self._int_value * int(other) 73 | if result < self._min_range or result > self._max_range: 74 | raise ValueError( 75 | 'The result of this operation would result in an ' 76 | 'invalid IPv4 address' 77 | ) 78 | return IPv4Address(result) 79 | 80 | def __div__ (self, other): 81 | result = self._int_value / int(other) 82 | if result < self._min_range or result > self._max_range \ 83 | or type(result) != int: 84 | raise ValueError( 85 | 'The result of this operation would result in an ' 86 | 'invalid IPv4 address' 87 | ) 88 | return IPv4Address(result) 89 | 90 | def __mod__ (self, other): 91 | result = self._int_value % int(other) 92 | if result < self._min_range or result > self._max_range: 93 | raise ValueError( 94 | 'The result of this operation would result in an ' 95 | 'invalid IPv4 address' 96 | ) 97 | return IPv4Address(result) 98 | 99 | def __lshift__ (self, other): 100 | result = self._int_value << int(other) 101 | if result < self._min_range or result > self._max_range: 102 | raise ValueError( 103 | 'The result of this operation would result in an ' 104 | 'invalid IPv4 address' 105 | ) 106 | return IPv4Address(result) 107 | 108 | def __rshift__ (self, other): 109 | result = self._int_value >> int(other) 110 | if result < self._min_range or result > self._max_range: 111 | raise ValueError( 112 | 'The result of this operation would result in an ' 113 | 'invalid IPv4 address' 114 | ) 115 | return IPv4Address(result) 116 | 117 | def __and__ (self, other): 118 | result = self._int_value & int(other) 119 | if result < self._min_range or result > self._max_range: 120 | raise ValueError( 121 | 'The result of this operation would result in an ' 122 | 'invalid IPv4 address' 123 | ) 124 | return IPv4Address(result) 125 | 126 | def __xor__ (self, other): 127 | result = self._int_value ^ int(other) 128 | if result < self._min_range or result > self._max_range: 129 | raise ValueError( 130 | 'The result of this operation would result in an ' 131 | 'invalid IPv4 address' 132 | ) 133 | return IPv4Address(result) 134 | 135 | def __or__ (self, other): 136 | result = self._int_value | int(other) 137 | if result < self._min_range or result > self._max_range: 138 | raise ValueError( 139 | 'The result of this operation would result in an ' 140 | 'invalid IPv4 address' 141 | ) 142 | return IPv4Address(result) 143 | 144 | if __name__ == '__main__': 145 | 146 | # - Basic object-creation 147 | ip = IPv4Address('127.0.0.1') 148 | print('ip._int_value ... %s' % ip._int_value) 149 | print('int(ip) ......... %s' % int(ip)) 150 | print('ip .............. %s' % ip) 151 | 152 | # - Arithmetic opersions: addition 153 | ip += 2 154 | print('int(ip) ......... %s' % int(ip)) 155 | print('ip .............. %s' % ip) 156 | # ...subtraction 157 | ip -= 1 158 | print('int(ip) ......... %s' % int(ip)) 159 | print('ip .............. %s' % ip) 160 | 161 | # - Instance-creation with an int argument 162 | ip = IPv4Address(270000000) 163 | print('int(ip) ......... %s' % int(ip)) 164 | print('ip .............. %s' % ip) 165 | 166 | # - One of the basics of determining whether one IPv4 address is 167 | # in a network range is available just by using a boolean AND 168 | # operation between the two, but there is a binary mask (the 169 | # "24" in "10.0.0.0/24") that would have to be applied in some 170 | # fashion that would make it equivalent to 10.255.255.255: 171 | my_ip = IPv4Address('10.1.100.40') 172 | ip_range = IPv4Address('10.0.0.0') 173 | print( 174 | 'my IP (%s) in IP range (%s): %s' % 175 | (my_ip, ip_range, True if int(my_ip & ip_range) else False) 176 | ) 177 | ip_range = IPv4Address('192.0.0.0') 178 | print( 179 | 'my IP (%s) in IP range (%s): %s' % 180 | (my_ip, ip_range, True if int(my_ip & ip_range) else False) 181 | ) 182 | 183 | ips = [ 184 | IPv4Address(ip) for ip in 185 | ('10.0.0.0', '192.168.0.0', '127.0.0.1') 186 | ] 187 | print(ips) 188 | 189 | ips = sorted(ips, key=lambda ip:int(ip)) 190 | print(ips) 191 | 192 | try: 193 | ips = sorted(ips) 194 | except Exception as error: 195 | print('%s: %s' % (error.__class__.__name__, error)) 196 | -------------------------------------------------------------------------------- /Chapter04/C04R05_EmulatingStringType.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | The complete code of Ch. 4, Recipe 5 -- 4 | Emulating a string type with magic methods 5 | """ 6 | 7 | class EmailAddress: 8 | 9 | @property 10 | def address(self): 11 | """Gets or sets the actual email address""" 12 | return self._address 13 | 14 | @address.setter 15 | def address(self, value): 16 | if type(value) != str: 17 | raise TypeError() 18 | value = value.strip() 19 | bad_chars = [c for c in value if ord(c)<32 or ord(c)>126] 20 | if not value: 21 | if bad_chars: 22 | raise ValueError('Invalid characters in address') 23 | else: 24 | raise ValueError('Invalid address') 25 | # TODO: Work out a well-formed-email-address validation 26 | # process and apply it here, raising a ValueError 27 | # if the supplied address is not at least well- 28 | # formed. See https://emailregex.com/ as a 29 | # possible starting-point? 30 | self._address = value 31 | 32 | @property 33 | def name(self): 34 | """Gets, sets or deletes the name""" 35 | try: 36 | return self._name 37 | except AttributeError: 38 | return None 39 | 40 | @name.setter 41 | def name(self, value): 42 | if value != None: 43 | if type(value) != str: 44 | raise TypeError() 45 | value = value.strip() 46 | bad_chars = [c for c in value if ord(c)<32 or ord(c)>126] 47 | if not value: 48 | if bad_chars: 49 | raise ValueError('Invalid characters in name') 50 | else: 51 | raise ValueError('Invalid name') 52 | self._name = value 53 | 54 | @name.deleter 55 | def name(self): 56 | try: 57 | del self._name 58 | except AttributeError: 59 | pass 60 | 61 | def __init__(self, value): 62 | # - Make sure we're dealing with either a string or an 63 | # instance of the class 64 | if type(value) not in (str, self.__class__): 65 | raise TypeError() 66 | # - Convert it to a string value to work with 67 | value = str(value) 68 | # - Start by assuming that the address is valid 69 | is_valid = True 70 | # - Break it into its components: 71 | # - email.address@domain.com 72 | # - User Name 73 | if '<' in value and value.strip().endswith('>'): 74 | try: 75 | self.name, self.address = [ 76 | s.strip() for s in value.split('<') 77 | ] 78 | self.address = self.address[0:-1] 79 | except ValueError: 80 | # - Too many values to unpack 81 | is_valid = False 82 | else: 83 | self.address = value 84 | 85 | def __str__(self): 86 | if self.name: 87 | return '%s <%s>' % (self.name, self.address) 88 | return self.address 89 | 90 | def __repr__(self): 91 | return '%s(%s)' % (self.__class__.__name__, str(self)) 92 | 93 | # - Magic and non-magic methods that provide string-type 94 | # emulation capabilities 95 | 96 | def __contains__(self, item): 97 | return str(self).__contains__(item) 98 | 99 | def __eq__(self, other): 100 | if isinstance(other, self.__class__): 101 | other = str(other) 102 | return str(self) == other 103 | 104 | def __ge__(self, other): 105 | if isinstance(other, self.__class__): 106 | other = str(other) 107 | return str(self) >= other 108 | 109 | def __gt__(self, other): 110 | if isinstance(other, self.__class__): 111 | other = str(other) 112 | return str(self) > other 113 | 114 | def __le__(self, other): 115 | if isinstance(other, self.__class__): 116 | other = str(other) 117 | return str(self) <= other 118 | 119 | def __lt__(self, other): 120 | if isinstance(other, self.__class__): 121 | other = str(other) 122 | return str(self) < other 123 | 124 | def __neq__(self, other): 125 | if isinstance(other, self.__class__): 126 | other = str(other) 127 | return str(self) != other 128 | 129 | def index(self, sub, *args): 130 | return str(self).index(sub, *args) 131 | 132 | # - Other string-related methods that don't make much sense 133 | # to implement, because they would either return invalid 134 | # values, or values of different types. 135 | 136 | def __add__(self, other): 137 | if isinstance(other, self.__class__): 138 | other = str(other) 139 | return EmailAddress(str(self) + other) 140 | 141 | # def __getitem__(self, index): 142 | # return str(self).__getitem__(index) 143 | 144 | # def __iter__(self): 145 | # return str(self).__iter__() 146 | 147 | # def __len__(self): 148 | # return len(str(self)) 149 | 150 | if __name__ == '__main__': 151 | my_address=EmailAddress('someone@gmail.com') 152 | print(my_address) 153 | my_address.name = 'Brian Allbee' 154 | print(my_address) 155 | 156 | my_address=EmailAddress('Brian Allbee ') 157 | print(my_address) 158 | print(my_address.name) 159 | print(my_address.address) 160 | 161 | addr1 = EmailAddress('someone@gmail.com') 162 | addr2 = EmailAddress('someone-else@gmail.com') 163 | print('addr1 == addr2 ... %s' % (addr1 == addr2)) 164 | print('addr1 < addr2 .... %s' % (addr1 < addr2)) 165 | print('addr1 > addr2 .... %s' % (addr1 > addr2)) 166 | addr_list = [addr1, addr2] 167 | addr_list.sort() 168 | print(addr_list) 169 | 170 | print('gmail' in my_address) 171 | print(my_address.index('gmail')) 172 | 173 | addr1 = EmailAddress('someone@gmail.com') 174 | addr1 += EmailAddress('someone-else@gmail.com') 175 | print(addr1) 176 | print(type(addr1)) 177 | -------------------------------------------------------------------------------- /Chapter04/C04R06_ExtendingNumericType.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | The complete code of Ch. 4, Recipe 6 -- 4 | Extending built-in types: Ipv4Address revisited 5 | """ 6 | 7 | class IPv4Address(int): 8 | """Represents an IPv4 Address""" 9 | 10 | _min_range = 0 11 | _max_range = 256**4-1 12 | 13 | def __new__(cls, *args, **kwargs): 14 | # - The first item in *args is the value of the int, 15 | # so we need to extract that to allow for non-int 16 | # values. We're preserving any other args, just in 17 | # case they are needed later. 18 | value = args[0] 19 | args = args[1:] 20 | # - Handle the incoming value, converting if needed 21 | if type(value) == str: 22 | octets = [int(o) for o in value.split('.')[0:4]] 23 | if len(octets) != 4: 24 | raise ValueError() 25 | octets.reverse() 26 | value = sum( 27 | [ 28 | value * 256**power for (power, value) 29 | in enumerate(octets) 30 | ] 31 | ) 32 | if type(value) == int: 33 | if value < cls._min_range or value > cls._max_range: 34 | raise ValueError() 35 | else: 36 | raise TypeError() 37 | # - Delegate to superclass for actual object-creation. 38 | # Note that value is being passed explicitly 39 | instance = super().__new__(cls, value, *args, **kwargs) 40 | # - Return the new instance 41 | return instance 42 | 43 | def __str__(self): 44 | octets = [] 45 | value = super().__int__() 46 | for power in range(0,4): 47 | octet, value = (value % 256, int(value/256)) 48 | octets.insert(0, octet) 49 | return '.'.join([str(o) for o in octets]) 50 | 51 | def __repr__(self): 52 | return ( 53 | '<%s at %s (%s)>' % 54 | ( 55 | self.__class__.__name__, hex(id(self)), 56 | self.__str__() 57 | ) 58 | ) 59 | 60 | # - Other methods that complete the numeric-type functionality 61 | def __add__ (self, other): 62 | result = super().__int__() + int(other) 63 | if result < self._min_range or result > self._max_range: 64 | raise ValueError( 65 | 'The result of this operation would result in an ' 66 | 'invalid IPv4 address' 67 | ) 68 | return IPv4Address(result) 69 | 70 | def __sub__ (self, other): 71 | result = super().__int__() - int(other) 72 | if result < self._min_range or result > self._max_range: 73 | raise ValueError( 74 | 'The result of this operation would result in an ' 75 | 'invalid IPv4 address' 76 | ) 77 | return IPv4Address(result) 78 | 79 | def __mul__ (self, other): 80 | result = super().__int__() * int(other) 81 | if result < self._min_range or result > self._max_range: 82 | raise ValueError( 83 | 'The result of this operation would result in an ' 84 | 'invalid IPv4 address' 85 | ) 86 | return IPv4Address(result) 87 | 88 | def __div__ (self, other): 89 | result = super().__int__() / int(other) 90 | if result < self._min_range or result > self._max_range \ 91 | or type(result) != int: 92 | raise ValueError( 93 | 'The result of this operation would result in an ' 94 | 'invalid IPv4 address' 95 | ) 96 | return IPv4Address(result) 97 | 98 | def __mod__ (self, other): 99 | result = super().__int__() % int(other) 100 | if result < self._min_range or result > self._max_range: 101 | raise ValueError( 102 | 'The result of this operation would result in an ' 103 | 'invalid IPv4 address' 104 | ) 105 | return IPv4Address(result) 106 | 107 | def __lshift__ (self, other): 108 | result = self._int_value << int(other) 109 | if result < self._min_range or result > self._max_range: 110 | raise ValueError( 111 | 'The result of this operation would result in an ' 112 | 'invalid IPv4 address' 113 | ) 114 | return IPv4Address(result) 115 | 116 | def __rshift__ (self, other): 117 | result = super().__int__() >> int(other) 118 | if result < self._min_range or result > self._max_range: 119 | raise ValueError( 120 | 'The result of this operation would result in an ' 121 | 'invalid IPv4 address' 122 | ) 123 | return IPv4Address(result) 124 | 125 | def __and__ (self, other): 126 | result = super().__int__() & int(other) 127 | if result < self._min_range or result > self._max_range: 128 | raise ValueError( 129 | 'The result of this operation would result in an ' 130 | 'invalid IPv4 address' 131 | ) 132 | return IPv4Address(result) 133 | 134 | def __or__ (self, other): 135 | result = super().__int__() | int(other) 136 | if result < self._min_range or result > self._max_range: 137 | raise ValueError( 138 | 'The result of this operation would result in an ' 139 | 'invalid IPv4 address' 140 | ) 141 | return IPv4Address(result) 142 | 143 | def __xor__ (self, other): 144 | result = super().__int__() ^ int(other) 145 | if result < self._min_range or result > self._max_range: 146 | raise ValueError( 147 | 'The result of this operation would result in an ' 148 | 'invalid IPv4 address' 149 | ) 150 | return IPv4Address(result) 151 | 152 | if __name__ == '__main__': 153 | 154 | ip1 = IPv4Address('127.0.0.1') 155 | ip2 = IPv4Address(1000000001) 156 | 157 | print('ip1 .......... %s' % ip1) 158 | print('int(ip1) ..... %d' % ip1) 159 | print('ip2 .......... %s' % ip2) 160 | print('int(ip2) ..... %d' % ip2) 161 | 162 | ip3 = IPv4Address('127.0.0.1') 163 | ip3 += 1 164 | ips = [ 165 | IPv4Address(ip) for ip in 166 | ('10.0.0.0', '192.168.0.0', '127.0.0.1') 167 | ] 168 | 169 | print('ip3 .......... %s' % ip3) 170 | print('int(ip3) ..... %d' % ip3) 171 | print('ip2 == ip3 ... %s' % (ip2 == ip3)) 172 | print('ip2 < ip3 .... %s' % (ip2 < ip3)) 173 | print('ip2 > ip3 .... %s' % (ip2 > ip3)) 174 | print(ips) 175 | 176 | ips_sorted = sorted(ips, key=lambda ip:int(ip)) 177 | print(ips_sorted) 178 | 179 | # - One of the basics of determining whether one IPv4 address is 180 | # in a network range is available just by using a boolean AND 181 | # operation between the two, but there is a binary mask (the 182 | # "24" in "10.0.0.0/24") that would have to be applied in some 183 | # fashion that would make it equivalent to 10.255.255.255: 184 | # my_ip = IPv4Address('10.1.100.40') 185 | # ip_range = IPv4Address('10.255.255.255') 186 | # print( 187 | # 'my IP (%s) in IP range (%s): %s' % 188 | # (my_ip, ip_range, True if my_ip & ip_range else False) 189 | # ) 190 | # ip_range = IPv4Address('10.5.255.255') 191 | # print( 192 | # 'my IP (%s) in IP range (%s): %s' % 193 | # (my_ip, ip_range, True if my_ip & ip_range else False) 194 | # ) 195 | 196 | print('-'*80) 197 | ip=IPv4Address('255.255.255.255') 198 | ip += 1 199 | print(ip) 200 | -------------------------------------------------------------------------------- /Chapter04/C04R07_TypedCollections.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | The complete code of Ch. 4, Recipe 7 -- 4 | Extending built-in types: Enforcing member-type on collections 5 | """ 6 | 7 | class TypedList(list): 8 | """ 9 | Provides a list-based sequence that only allows certain 10 | member-types 11 | """ 12 | 13 | # - Keep track of allowed member-types as a read-only property 14 | @property 15 | def member_types(self): 16 | try: 17 | return self._member_types 18 | except AttributeError: 19 | return tuple() 20 | 21 | def __init__(self, values, **kwargs): 22 | # - Make sure that member_types is a sequence of values 23 | if type(values) not in (list, tuple): 24 | raise TypeError( 25 | '%s expects a list or tuple of values, but ' 26 | 'was passed "%s" (%s)' % 27 | ( 28 | self.__class__.__name__, str(values), 29 | type(values).__name__ 30 | ) 31 | ) 32 | member_types = kwargs.get('member_types') 33 | if not member_types: 34 | raise ValueError( 35 | '%s expects a list or tuple of allowed ' 36 | 'types to be specified as a member_types ' 37 | 'keyword argument, but none were supplied' % 38 | ( 39 | self.__class__.__name__, str(values), 40 | type(values).__name__ 41 | ) 42 | ) 43 | bad_types = [v for v in member_types if type(v) != type] 44 | if bad_types: 45 | raise ValueError( 46 | '%s expects a list or tuple of allowed ' 47 | 'types to be specified as a member_types ' 48 | 'keyword argument, but was passed "%s" (%s), ' 49 | 'which contained invalid value-types (%s)' % 50 | ( 51 | self.__class__.__name__, str(values), 52 | type(values).__name__, ', '.join(bad_types) 53 | ) 54 | ) 55 | # - Set the allowed member-types 56 | self._member_types = tuple(member_types) 57 | # - Check the provided values 58 | for member in values: 59 | self._type_check(member) 60 | # - If everything checks as valid, then call the parent 61 | # object-initializer (list.__init__) 62 | list.__init__(self, values) 63 | 64 | # - Create a type-checking helper-method 65 | def _type_check(self, member): 66 | # - Using isinstance instead of a straight type- 67 | # comparison, so that extensions of types will be 68 | # accepted too 69 | if not isinstance(member, self.member_types): 70 | raise TypeError( 71 | 'This instance of %s only accepts %s values: ' 72 | '%s (%s) is not allowed' % 73 | ( 74 | self.__class__.__name__, 75 | '|'.join( 76 | [t.__name__ for t in self.member_types] 77 | ), str(member), type(member).__name__ 78 | ) 79 | ) 80 | 81 | # - Wrap all of the list methods that involve adding a member 82 | # with type-checking 83 | def __add__(self, other): 84 | # - Called when + is executed 85 | for member in other: 86 | self._type_check(member) 87 | # - list.__add__ returns a new list with the new members 88 | return TypedList( 89 | list.__add__(self, other), 90 | member_types=self.member_types 91 | ) 92 | 93 | def __iadd__(self, other): 94 | # - Called when += is executed 95 | for member in other: 96 | self._type_check(member) 97 | # - list.__iadd__ returns the instance after it's 98 | # been modified 99 | return list.__iadd__(self, other) 100 | 101 | def __mul__(self, other): 102 | # - Called when * is executed 103 | # - list.__mul__ returns a new list with the new members 104 | return TypedList( 105 | list.__mul__(self, other), 106 | member_types=self.member_types 107 | ) 108 | 109 | def append(self, member): 110 | self._type_check(member) 111 | return list.append(self, member) 112 | 113 | def extend(self, other): 114 | for member in other: 115 | self._type_check(member) 116 | return list.extend(self, other) 117 | 118 | def insert(self, index, member): 119 | self._type_check(member) 120 | return list.insert(self, index, member) 121 | 122 | number_list = TypedList([1,], member_types=(float,int)) 123 | print(number_list) 124 | print(type(number_list)) 125 | 126 | try: 127 | number_list = TypedList(['not-a-number',], member_types=(float,int)) 128 | print(number_list) 129 | print(type(number_list)) 130 | except Exception as error: 131 | print('%s: %s' % (error.__class__.__name__, error)) 132 | 133 | number_list = TypedList([1,], member_types=(float,int)) 134 | number_list = number_list + [3.14] 135 | print(number_list) 136 | print(type(number_list)) 137 | 138 | number_list = number_list * 2 139 | print(number_list) 140 | print(type(number_list)) 141 | 142 | # ~ number_list = TypedList([1,], member_types=(float,int)) 143 | # ~ number_list += [2.3] 144 | # ~ print(number_list) 145 | # ~ print(type(number_list)) 146 | 147 | # ~ number_list.append(4.5) 148 | # ~ print(number_list) 149 | # ~ print(type(number_list)) 150 | 151 | number_list = TypedList([1,], member_types=(float,int)) 152 | number_list *= 2 153 | print(number_list) 154 | print(type(number_list)) 155 | 156 | number_list = TypedList([1,], member_types=(float,int)) 157 | # ~ number_list.insert(0, 0) 158 | # ~ print(number_list) 159 | # ~ print(type(number_list)) 160 | 161 | number_list = TypedList([1,], member_types=(float,int)) 162 | # ~ number_list.extend([7,8.9]) 163 | # ~ print(number_list) 164 | # ~ print(type(number_list)) 165 | 166 | -------------------------------------------------------------------------------- /Chapter04/C04R08_CustomExceptions.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | The complete code of Ch. 4, Recipe 7 -- 4 | Creating and using custom exceptions 5 | """ 6 | 7 | # - The bare-bones example, commented out so that this module 8 | # can be executed and show the final error being raised 9 | # class OutOfCheeseError(Exception): 10 | # pass 11 | 12 | # if not cheese_count: 13 | # raise OutOfCheeseError( 14 | # 'The current cheese count is %d' % cheese_count 15 | # ) 16 | 17 | class OutOfCheeseError(Exception): 18 | def __init__(self, msg, last_cheese=None): 19 | if last_cheese: 20 | msg += ': the last cheese was %s' % last_cheese 21 | self.msg = msg 22 | 23 | class HTTPError(Exception): 24 | http_status = None 25 | http_message = None 26 | 27 | @property 28 | def status(self): 29 | return '%s %s' % (self.http_status, self.http_message) 30 | 31 | def __init__(self, *args, **kwargs): 32 | if not self.http_status or type(self.http_status) != int: 33 | raise AttributeError( 34 | '%s does not specify a numeric http_status ' 35 | 'value. Please specify a valid HTTP status ' 36 | 'code' % self.__class__.__name__ 37 | ) 38 | if not self.http_message or type(self.http_message) != str: 39 | raise AttributeError( 40 | '%s does not specify an http_message value. ' 41 | 'Please specify a valid HTTP status ' 42 | 'message' % self.__class__.__name__ 43 | ) 44 | Exception.__init__(self, *args, **kwargs) 45 | 46 | class HTTP_400(HTTPError): 47 | http_status = 400 48 | http_message = 'Bad Request' 49 | 50 | class HTTP_404(HTTPError): 51 | http_status = 404 52 | http_message = 'Not Found' 53 | 54 | 55 | if __name__ == '__main__': 56 | ### DEMO CODE: Uncomment one of the following, then run the 57 | ### script; then re-comment that item, and uncomment 58 | ### something different to see how errors are handled... 59 | demo_error = None 60 | # demo_error = OutOfCheeseError 61 | # demo_error = RuntimeError 62 | # demo_error = TypeError 63 | # demo_error = ValueError 64 | 65 | try: 66 | print('Trying something that might raise errors...') 67 | if not demo_error: 68 | pass 69 | elif demo_error == OutOfCheeseError: 70 | cheese_count = 0 71 | current_cheese = 'Camembert' 72 | raise OutOfCheeseError( 73 | 'The current cheese count is %d' % cheese_count, 74 | current_cheese 75 | ) 76 | else: 77 | raise demo_error('%s raised' % demo_error.__name__) 78 | except RuntimeError: 79 | print('(runtime branch) RuntimeError caught') 80 | # - Re-raise the SAME error 81 | raise 82 | except (TypeError, ValueError) as error: 83 | print( 84 | '(type/value branch) %s caught: %s' % 85 | (error.__class__.__name__, error) 86 | ) 87 | except Exception as error: 88 | print( 89 | '%s caught (general exception handling): %s' % 90 | (error.__class__.__name__, error) 91 | ) 92 | else: 93 | print('(else branch) No error encountered') 94 | finally: 95 | print('(finally branch) Executing "finally" code') 96 | 97 | try: 98 | # - Uncomment one of these to raise a specific error 99 | 3 raise HTTP_400('This is a bad request') 100 | # raise HTTP_404('This request cannot be fulfilled') 101 | pass 102 | except HTTPError as error: 103 | print( 104 | '%s (%s): %s' % ( 105 | error.__class__.__name__, error.status, 106 | error 107 | ) 108 | ) 109 | -------------------------------------------------------------------------------- /Chapter04/README.md: -------------------------------------------------------------------------------- 1 | # Chapter 4 — Advanced Python for OOP 2 | 3 | > Part of the "Python Object Oriented Programming Cookbook" title, 4 | > published by Packt. 5 | 6 | > Code in this chapter was originally written against version 3.6.6 of Python, 7 | > as installed by default on the Author's then-current Linux Mint 19 (Cinnamon) 8 | > installation. 9 | 10 | [Recipe 1: **Taking control of class creation with metaclasses**](C04R01_MetaclassBasics.py) — 11 | The basics of defining and using metaclasses 12 | 13 | [Recipe 2: **Enforcing class requirements with metaclass**](C04R02_ClassRequirementMetaclass.py) — 14 | Using a metaclass to enforce class-structure requirements 15 | 16 | [Recipe 3: **Using metaclass to automatically register a subclass**](C04R03_SubclassRegistrationMetaclass.py) — 17 | Using a metaclass to register a class with some other class or process 18 | 19 | [Recipe 4: **Emulating a numeric type with magic methods**](C04R04_EmulatingNumericType.py) — 20 | Constructing a custom type (class) that emulates the built-in int 21 | 22 | [Recipe 5: **Emulating a string type with magic methods**](C04R05_EmulatingStringType.py) — 23 | Constructing a custom type (class) that acts like a built-in str 24 | 25 | [Recipe 6: **Extending built-in types: Ipv4Address revisited**](C04R06_ExtendingNumericType.py) — 26 | Constructing a custom type (class) that extends the built-in int 27 | 28 | [Recipe 7: **Extending built-in types: Enforcing member-type on collections**](C04R07_TypedCollections.py) — 29 | Constructing a custom collection-type that extends the built-in 30 | list type, but enforces member-type constraints 31 | 32 | [Recipe 8: **Creating and using custom exceptions**](C04R08_CustomExceptions.py) 33 | -------------------------------------------------------------------------------- /Chapter05/C05R01_SimpleStacks.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | The complete code of Ch. 5, Recipe 1 -- 4 | Implementing a simple stack 5 | """ 6 | 7 | from random import randint 8 | 9 | def print_line( 10 | leader:str, line:(str,None)=None, 11 | indent:int=0, body_start:int=28 12 | ) -> str: 13 | """ 14 | Prints a formatted line of text, with a dot-leader between the 15 | lead and the line supplied 16 | """ 17 | output = '' 18 | if indent: 19 | output = ' '*(3*(indent-1)) + '+- ' 20 | output += leader 21 | if line: 22 | output = ( 23 | (output + ' ').ljust(body_start - 1, '.') 24 | + ' ' + line 25 | ) 26 | print(output) 27 | 28 | # - Create a basic list to use as a stack 29 | my_stack = [] 30 | print_line('my_stack (initially)', str(my_stack)) 31 | 32 | # - "push" values into the stack with my_stack.append 33 | my_stack.append(1) 34 | my_stack.append(2) 35 | my_stack.append(3) 36 | 37 | print_line('my_stack (pushed 1,2,3)', str(my_stack)) 38 | 39 | # - pop a value from the stack 40 | value = my_stack.pop() 41 | print_line('value (popped)', str(value), 1) 42 | print_line('my_stack (after pop)', str(my_stack)) 43 | 44 | # - Push a couple random values 45 | new_value = randint(49,99) 46 | my_stack.append(new_value) 47 | print_line('my_stack (pushed %s)' % new_value, str(my_stack)) 48 | new_value = randint(1,49) 49 | my_stack.append(new_value) 50 | print_line('my_stack (pushed %s)' % new_value, str(my_stack)) 51 | 52 | # - Pop all values from the stack 53 | while len(my_stack): 54 | value = my_stack.pop() 55 | print_line('value (popped)', str(value), 1) 56 | print_line('my_stack (after pop)', str(my_stack), 1) 57 | 58 | # - Using a UserList to more tightly define a list-based 59 | # stack-implementation 60 | 61 | from collections import UserList 62 | 63 | class stack(UserList): 64 | def push(self, value): 65 | self.data.append(value) 66 | def pop(self): 67 | return self.data.pop() 68 | def sort(self, *args, **kwargs): 69 | raise AttributeError( 70 | '%s instances are not sortable' % 71 | self.__class__.__name__ 72 | ) 73 | def __setitem__(self, *args, **kwargs): 74 | raise RuntimeError( 75 | '%s instances cannot be altered except by ' 76 | 'using push' % 77 | self.__class__.__name__ 78 | ) 79 | 80 | empty_stack = stack() 81 | print_line('empty_stack', str(empty_stack)) 82 | my_stack = stack([9,8,7,6,5,4,3,2,1]) 83 | print_line('my_stack', str(my_stack)) 84 | try: 85 | my_stack.sort() 86 | except Exception as error: 87 | print_line( 88 | 'my_stack.sort', '%s: %s' % 89 | (error.__class__.__name__, error) 90 | ) 91 | try: 92 | my_stack[4] = 23 93 | except Exception as error: 94 | print_line( 95 | 'my_stack[4] = ', '%s: %s' % 96 | (error.__class__.__name__, error) 97 | ) 98 | try: 99 | my_stack[4:5] = [99,98] 100 | except Exception as error: 101 | print_line( 102 | 'my_stack[4:5] = ', '%s: %s' % 103 | (error.__class__.__name__, error) 104 | ) 105 | -------------------------------------------------------------------------------- /Chapter05/C05R02_QueuesListsDeque.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | The complete code of Ch. 5, Recipe 2 -- 4 | Queueing options: Lists and deques 5 | """ 6 | 7 | from collections import deque 8 | from random import randint 9 | from time import time 10 | 11 | def print_line( 12 | leader:str, line:(str,None)=None, 13 | indent:int=0, body_start:int=28 14 | ) -> str: 15 | """ 16 | Prints a formatted line of text, with a dot-leader between the 17 | lead and the line supplied 18 | """ 19 | output = '' 20 | if indent: 21 | output = ' '*(3*(indent-1)) + '+- ' 22 | output += leader 23 | if line: 24 | output = ( 25 | (output + ' ').ljust(body_start - 1, '.') 26 | + ' ' + line 27 | ) 28 | print(output) 29 | 30 | my_queue = deque([3,2,1]) 31 | print_line('my_queue (populated)', str(my_queue)) 32 | my_queue = deque() 33 | print_line('my_queue (empty)', str(my_queue)) 34 | 35 | # - "push" values into the queue with my_queue.append 36 | my_queue.append(1) 37 | my_queue.append(2) 38 | my_queue.append(3) 39 | 40 | print_line('my_queue (pushed 1,2,3)', str(my_queue)) 41 | 42 | # - pop a value from the queue 43 | value = my_queue.popleft() 44 | print_line('value (popped)', str(value), 1) 45 | print_line('my_queue (after pop)', str(my_queue)) 46 | 47 | # - Push a couple random values 48 | new_value = randint(49,99) 49 | my_queue.append(new_value) 50 | print_line('my_queue (pushed %s)' % new_value, str(my_queue)) 51 | new_value = randint(1,49) 52 | my_queue.append(new_value) 53 | print_line('my_queue (pushed %s)' % new_value, str(my_queue)) 54 | 55 | print_line('deque iterable members') 56 | print_line('len(my_queue)', str(len(my_queue)), 1) 57 | print_line('my_queue[1]', str(my_queue[1]), 1) 58 | 59 | # - Iterate over all members and show their values 60 | print('Members of my_queue:') 61 | for value in my_queue: 62 | print_line('item value', str(value), 1) 63 | 64 | # - Pop all values from the queue 65 | print('Popping all members in a while loop') 66 | while my_queue: 67 | value = my_queue.popleft() 68 | print_line('value (popped)', str(value), 1) 69 | print_line('my_queue (after pop)', str(my_queue), 1) 70 | 71 | -------------------------------------------------------------------------------- /Chapter05/C05R03_LinkedLists.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | The complete code of Ch. 5, Recipe 3 -- 4 | Creating Linked Lists in Python 5 | """ 6 | 7 | o1=object() 8 | o2=object() 9 | o3=object() 10 | 11 | example_list = [o1, o2, o3] 12 | print(hex(id(example_list))) 13 | print([hex(id(o)) for o in example_list]) 14 | 15 | o4=object() 16 | o5=object() 17 | o6=object() 18 | 19 | example_list += [o6, o5, o4] 20 | print(hex(id(example_list))) 21 | print([hex(id(o)) for o in example_list]) 22 | 23 | class ListMember: 24 | """ 25 | Represents a member (or node) in a linked list. 26 | Each member has a value, and keeps track of what the 27 | next member is. 28 | """ 29 | def __init__(self, value): 30 | self.value = value 31 | self.next_item=None 32 | 33 | def __str__(self): 34 | return ('ListMember(value=%s)' % (self.value)) 35 | 36 | class LinkedList: 37 | """Provides a Linked List of members""" 38 | def __init__(self, *members): 39 | self.first_member = None 40 | self.last_member = None 41 | for member in members: 42 | self.append(member) 43 | 44 | def append(self, member): 45 | new_member = ListMember(member) 46 | if self.last_member == None: 47 | # - Since self.last_member is None, we have to 48 | # create the first_member value 49 | self.first_member = new_member 50 | # - At this point, the first member is also the 51 | # last member, so keep track of that too... 52 | self.last_member = new_member 53 | else: 54 | # - Since we have a last_member in this branch, 55 | # we need to set its next_item to the new member: 56 | self.last_member.next_item = new_member 57 | # - Then re-set self.last_member to point to the 58 | # *new* last_member: 59 | self.last_member = new_member 60 | 61 | def __iter__(self): 62 | if not self.first_member: 63 | raise StopIteration 64 | else: 65 | self.__iteritem = self.first_member 66 | return self 67 | 68 | def __next__(self): 69 | if self.__iteritem: 70 | result = self.__iteritem 71 | self.__iteritem = self.__iteritem.next_item 72 | return result 73 | raise StopIteration 74 | 75 | 76 | class IndexedLinkedList(LinkedList): 77 | """ 78 | Extends LinkedList to allow retrieval of members by a numeric index 79 | """ 80 | def __getitem__(self, index:int): 81 | for i in range(0,index): 82 | if i == 0: 83 | current_item = self.first_member 84 | else: 85 | try: 86 | current_item = current_item.next_item 87 | except AttributeError: 88 | raise IndexError('list index out of range') 89 | return current_item 90 | 91 | class ArrayMember: 92 | """ 93 | Represents a member (or node) in a linked list. 94 | Each member has a value, and keeps track of what the 95 | next member is. 96 | """ 97 | def __init__(self, key, value): 98 | self.key = key 99 | self.value = value 100 | self.next_item=None 101 | 102 | def __str__(self): 103 | return ( 104 | 'ListMember(key=%s value=%s)' % 105 | (self.key, self.value) 106 | ) 107 | 108 | class AssociativeArray(LinkedList): 109 | 110 | def append(self, member): 111 | new_member = ArrayMember(member) 112 | if self.last_member == None: 113 | # - Since self.last_member is None, we have to 114 | # create the first_member value 115 | self.first_member = new_member 116 | # - At this point, the first member is also the 117 | # last member, so keep track of that too... 118 | self.last_member = new_member 119 | else: 120 | # - Since we have a last_member in this branch, 121 | # we need to set its next_item to the new member: 122 | self.last_member.next_item = new_member 123 | # - Then re-set self.last_member to point to the 124 | # *new* last_member: 125 | self.last_member = new_member 126 | 127 | def __getitem__(self, index): 128 | if type(index) == int: 129 | for i in range(0,index): 130 | if i == 0: 131 | current_item = self.first_member 132 | else: 133 | try: 134 | current_item = current_item.next_item 135 | except AttributeError: 136 | raise IndexError( 137 | 'list index out of range' 138 | ) 139 | return current_item 140 | else: 141 | current_item = self.first_member 142 | while current_item: 143 | if current_item.key == index: 144 | return current_item 145 | current_item = current_item.next_item 146 | raise KeyError(index) 147 | 148 | 149 | if __name__ == '__main__': 150 | 151 | print('LinkedList example') 152 | my_list = LinkedList(1,2,3) 153 | print(my_list) 154 | current_item = my_list.first_member 155 | while current_item: 156 | print(current_item) 157 | current_item = current_item.next_item 158 | 159 | print('__iter__ results') 160 | for i in my_list: 161 | print(i) 162 | 163 | print('Basis for a search:') 164 | search_results = [str(i) for i in my_list if i.value == 2] 165 | print(search_results) 166 | 167 | print('Basis for filtering:') 168 | filter_results = [str(i) for i in my_list if i.value % 2] 169 | print(filter_results) 170 | 171 | print('IndexedLinkedList example') 172 | my_list = IndexedLinkedList(1,2,3) 173 | print(my_list) 174 | print(my_list.last_member) 175 | current_item = my_list.first_member 176 | while current_item: 177 | print(current_item) 178 | current_item = current_item.next_item 179 | print(my_list[1]) 180 | -------------------------------------------------------------------------------- /Chapter05/C05R04_TreesInPython.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | The complete code of Ch. 5, Recipe 5 -- 4 | Creating Data-trees in Python 5 | """ 6 | 7 | class Node: 8 | """ 9 | Provides a (very) simple tree data-structure, where each node 10 | is allowed to have a "left" and "right" node (also Node 11 | instances) 12 | """ 13 | 14 | @property 15 | def left_node(self): 16 | try: 17 | return self._left_node 18 | except AttributeError: 19 | return None 20 | 21 | @left_node.setter 22 | def left_node(self, value): 23 | if value != None: 24 | if not isinstance(value, self.__class__): 25 | raise TypeError( 26 | '%s.left_node expects an instance of Node, ' 27 | 'but was passed "%s" (%s)' % ( 28 | self.__class__.__name__, value, 29 | type(value).__name__ 30 | ) 31 | ) 32 | self._left_node = value 33 | 34 | @left_node.deleter 35 | def left_node(self): 36 | try: 37 | del self._left_node 38 | except AttributeError: 39 | pass 40 | 41 | @property 42 | def right_node(self): 43 | try: 44 | return self._right_node 45 | except AttributeError: 46 | return None 47 | 48 | @right_node.setter 49 | def right_node(self, value): 50 | if value != None: 51 | if not isinstance(value, self.__class__): 52 | raise TypeError( 53 | '%s.right_node expects an instance of Node, ' 54 | 'but was passed "%s" (%s)' % ( 55 | self.__class__.__name__, value, 56 | type(value).__name__ 57 | ) 58 | ) 59 | self._right_node = value 60 | 61 | @right_node.deleter 62 | def right_node(self): 63 | try: 64 | del self._right_node 65 | except AttributeError: 66 | pass 67 | 68 | def __init__(self, data, left_node=None, right_node=None): 69 | self.data = data 70 | self.left_node = left_node 71 | self.right_node = right_node 72 | 73 | def __str__(self): 74 | return 'Node(data=%s)' % self.data 75 | 76 | def print_tree(self, indent=0): 77 | if not indent: 78 | print(self) 79 | else: 80 | print(' '*(3 * (indent-1)) + '+- %s' % self) 81 | if self.left_node: 82 | self.left_node.print_tree(indent=indent+1) 83 | if self.right_node: 84 | self.right_node.print_tree(indent=indent+1) 85 | 86 | def traverse(self): 87 | yield self 88 | if self.left_node: 89 | for node in self.left_node.traverse(): 90 | yield node 91 | if self.right_node: 92 | for node in self.right_node.traverse(): 93 | yield node 94 | 95 | def __iter__(self): 96 | return self.__next__() 97 | 98 | def __next__(self): 99 | yield self 100 | if self.left_node: 101 | yield from self.left_node 102 | if self.right_node: 103 | yield from self.right_node 104 | raise StopIteration 105 | 106 | if __name__ == '__main__': 107 | my_tree = Node('Root', 108 | Node('L01', 109 | Node('L01L01'), 110 | Node('L01R01', 111 | None, 112 | Node('L01R01R01') 113 | ), 114 | ), 115 | Node('R01', 116 | Node('R01L01'), 117 | Node('R01R01') 118 | ), 119 | ) 120 | 121 | print('\n### print_tree results:') 122 | my_tree.print_tree() 123 | 124 | print('Basis for a search:') 125 | search_results = [str(i) for i in my_tree if i.data == 'L01R01R01'] 126 | print(search_results) 127 | 128 | print('Basis for filtering:') 129 | filter_results = [str(i) for i in my_tree if 'R0' in i.data] 130 | print(filter_results) 131 | 132 | print('\n### __iter__ results:') 133 | for node in my_tree: 134 | print(node) 135 | 136 | print('\n### tree-data access by node-specification') 137 | root = Node('Root') 138 | L01 = Node('L01') 139 | root.left_node = L01 140 | L01L01 = Node('L01L01') 141 | L01.left_node = L01L01 142 | L01R01 = Node('L01R01') 143 | L01.left_node = L01R01 144 | 145 | # - Drilling into the tree to get to the Node with a 146 | # data-value of L01R01R01 147 | print(my_tree.left_node.right_node.right_node) 148 | 149 | # - Inserting a new value (or sub-tree) into a tree 150 | print(my_tree.right_node) 151 | my_tree.right_node = Node('new R01', 152 | None, 153 | Node('new R01R01') 154 | ) 155 | print(my_tree.right_node) 156 | print(my_tree.right_node.right_node) 157 | 158 | # 'left':{'value':'L01', 'left':{}, 'right':{},}, 159 | # 'right':{'value':'R01', 'left':{}, 'right':{},}, 160 | 161 | example_as_dict = { 162 | 'root':{ 163 | 'value':'root', 164 | 'left':{ 165 | 'value':'L01', 166 | 'left':{'value':'L01L01'}, 167 | 'right':{ 168 | 'value':'L01R01', 169 | 'right':{'value':'L01R01R01'}, 170 | }, 171 | }, 172 | 'right':{ 173 | 'value':'R01', 174 | 'left':{'value':'R01L01'}, 175 | 'right':{'value':'R01R01'}, 176 | }, 177 | } 178 | } 179 | 180 | import json 181 | print(json.dumps(example_as_dict, indent=4).replace('"', "'")) 182 | -------------------------------------------------------------------------------- /Chapter05/C05R05_PythonOfficialEnums.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | The complete code of Ch. 5, Recipe 7 -- 4 | Enumerating values the official way 5 | """ 6 | 7 | from collections import namedtuple 8 | from enum import Enum, IntEnum, auto 9 | 10 | class NumbersByName(Enum): 11 | zero = 0 12 | one = 1 13 | two = 2 14 | three = 3 15 | four = 4 16 | five = 5 17 | six = 6 18 | seven = 7 19 | eight = 8 20 | nine = 9 21 | 22 | @classmethod 23 | def values(cls): 24 | return [i.value for i in cls] 25 | 26 | class NumbersByName2(Enum): 27 | zero = auto() 28 | one = auto() 29 | two = auto() 30 | three = auto() 31 | four = auto() 32 | five = auto() 33 | six = auto() 34 | seven = auto() 35 | eight = auto() 36 | nine = auto() 37 | 38 | def IsArabicNumeral(value): 39 | return value in NumbersByName.values() 40 | 41 | NumbersByNameTuple = namedtuple('NumbersByName', 42 | [ 43 | 'zero', 'one', 'two', 'three', 'four', 'five', 44 | 'six', 'seven', 'eight', 'nine' 45 | ] 46 | )( 47 | zero = 0, 48 | one = 1, 49 | two = 2, 50 | three = 3, 51 | four = 4, 52 | five = 5, 53 | six = 6, 54 | seven = 7, 55 | eight = 8, 56 | nine = 9, 57 | ) 58 | 59 | 60 | if __name__ == '__main__': 61 | print(NumbersByName) 62 | # - Members are accessible by name 63 | print('NumbersByName.three .... %s' % NumbersByName.three) 64 | print('NumbersByName.one ...... %s' % NumbersByName.one) 65 | print('NumbersByName.four ..... %s' % NumbersByName.four) 66 | # - only members that were defined are available 67 | try: 68 | print(NumbersByName.thirteen) 69 | except Exception as error: 70 | print('%s: %s' % (error.__class__.__name__, error)) 71 | # - Members' *values* are accessible by name 72 | print('NumbersByName.three .... %s' % NumbersByName.three.value) 73 | print('NumbersByName.one ...... %s' % NumbersByName.one.value) 74 | print('NumbersByName.four ..... %s' % NumbersByName.four.value) 75 | 76 | # - Members' *values* are accessible by name 77 | print('NumbersByName2.three ... %s' % NumbersByName2.three.value) 78 | print('NumbersByName2.one ..... %s' % NumbersByName2.one.value) 79 | print('NumbersByName2.four .... %s' % NumbersByName2.four.value) 80 | 81 | print('NumbersByName.__members__:') 82 | print(NumbersByName.__members__) 83 | print() 84 | 85 | print('Listing the members of NumbersByName:') 86 | print([i for i in NumbersByName]) 87 | print() 88 | 89 | print('Listing the *values* of NumbersByName:') 90 | print([i.value for i in NumbersByName]) 91 | print() 92 | 93 | print('Calling IsArabicNumeral(8):') 94 | print(IsArabicNumeral(8)) 95 | print() 96 | 97 | print('NumbersByNameTuple:') 98 | print(NumbersByNameTuple) 99 | try: 100 | print(NumbersByNameTuple.thirteen) 101 | except Exception as error: 102 | print('%s: %s' % (error.__class__.__name__, error)) 103 | try: 104 | NumbersByNameTuple.six = 'six' 105 | except Exception as error: 106 | print('%s: %s' % (error.__class__.__name__, error)) 107 | print( 108 | 'NumbersByNameTuple.three .... %s' % NumbersByNameTuple.three 109 | ) 110 | print( 111 | 'NumbersByNameTuple.one ...... %s' % NumbersByNameTuple.one 112 | ) 113 | print( 114 | 'NumbersByNameTuple.four ..... %s' % NumbersByNameTuple.four 115 | ) 116 | print( 117 | '8 in NumbersByNameTuple: %s' % (8 in NumbersByNameTuple) 118 | ) 119 | -------------------------------------------------------------------------------- /Chapter05/README.md: -------------------------------------------------------------------------------- 1 | # Chapter 5 — Object-Oriented Data-Structures 2 | 3 | > Part of the "Python Object Oriented Programming Cookbook" title, 4 | > published by Packt. 5 | 6 | > Code in this chapter was originally written against version 3.6.6 of Python, 7 | > as installed by default on the Author's then-current Linux Mint 19 (Cinnamon) 8 | > installation. 9 | 10 | [Recipe 1: **Implementing a simple stack**](C05R01_SimpleStacks.py) — 11 | Description 12 | 13 | [Recipe 2: **Implementing high-performance queues with deque**](C05R02_QueuesListsDeque.py) — 14 | Description 15 | 16 | [Recipe 3: **Creating Linked Lists in Python**](C05R03_LinkedLists.py) — 17 | Description 18 | 19 | [Recipe 4: **Creating Data-trees in Python**](C05R04_TreesInPython.py) — 20 | Description 21 | 22 | [Recipe 5: **Enumerating values the official way**](C05R05_PythonOfficialEnums.py) — 23 | Description 24 | -------------------------------------------------------------------------------- /Chapter06/README.md: -------------------------------------------------------------------------------- 1 | # Chapter 6 — CHAPTER_TITLE 2 | 3 | > Part of the "Python Object Oriented Programming Cookbook" title, 4 | > published by Packt. 5 | 6 | > Code in this chapter was originally written against version 3.6.6 of Python, 7 | > as installed by default on the Author's then-current Linux Mint 19 (Cinnamon) 8 | > installation. 9 | 10 | [Recipe 1: **Decoupling object construction with the Factory Method pattern**](C06R01_FactoryMethodPattern.py) — 11 | Description 12 | 13 | [Recipe 2: **Using the Abstract Factory pattern**](C06R02_AbstractFactoryPattern.py) — 14 | Description 15 | 16 | [Recipe 3: **Leveraging Python features in Factory-like code**](C06R03_LeveragingPythonInFactories.py) — 17 | Description 18 | 19 | [Recipe 4: **Working with the Builder pattern**](C06R04_BuilderPattern.py) — 20 | Description 21 | 22 | [Recipe 5: **Staying ready with the Object Pool pattern**](C06R05_ObjectPoolPattern.py) — 23 | Description 24 | 25 | [Recipe 6: **There can be only one: The Singleton pattern**](C06R06_SingletonPattern.py) — 26 | Description 27 | 28 | [Recipe 00: **RECIPE_NAME**](FILE_NAME) — 29 | Description 30 | 31 | -------------------------------------------------------------------------------- /Chapter07/README.md: -------------------------------------------------------------------------------- 1 | # Chapter 7 — Structural Design Patterns 2 | 3 | > Part of the "Python Object Oriented Programming Cookbook" title, 4 | > published by Packt. 5 | 6 | > Code in this chapter was originally written against version 3.6.6 of Python, 7 | > as installed by default on the Author's then-current Linux Mint 19 (Cinnamon) 8 | > installation. 9 | 10 | [Recipe 1: **Object interchangeability with the Adapter pattern**](C07R01_AdapterPattern.py) — 11 | Description 12 | 13 | [Recipe 2: **Objects as the sum of their parts: The Composite pattern**](C07R02_CompositePattern.py) — 14 | Description 15 | 16 | [Recipe 3: **Changing object capabilities at runtime with the Decorator pattern**](C07R03_DecoratorPattern.py) — 17 | Description 18 | 19 | [Recipe 4: **RECIPE_NAME**](C07R04_FacadePattern.py) — 20 | Description 21 | 22 | [Recipe 5: **RECIPE_NAME**](C07R05_FlyweightPattern.py) — 23 | Description 24 | 25 | [Recipe 6: **RECIPE_NAME**](C07R06_CustomDescriptors.py) — 26 | Description 27 | 28 | -------------------------------------------------------------------------------- /Chapter08/README.md: -------------------------------------------------------------------------------- 1 | # Chapter 8 — CHAPTER_TITLE 2 | 3 | > Part of the "Python Object Oriented Programming Cookbook" title, 4 | > published by Packt 5 | 6 | > Code in this chapter was originally written against version 3.6.6 of Python, 7 | > as installed by default on the Author's then-current Linux Mint 19 (Cinnamon) 8 | > installation. 9 | 10 | [Recipe 00: **RECIPE_NAME**](FILE_NAME) — 11 | Description 12 | 13 | [Recipe 00: **RECIPE_NAME**](FILE_NAME) — 14 | Description 15 | 16 | [Recipe 00: **RECIPE_NAME**](FILE_NAME) — 17 | Description 18 | 19 | [Recipe 00: **RECIPE_NAME**](FILE_NAME) — 20 | Description 21 | 22 | [Recipe 00: **RECIPE_NAME**](FILE_NAME) — 23 | Description 24 | 25 | [Recipe 00: **RECIPE_NAME**](FILE_NAME) — 26 | Description 27 | 28 | -------------------------------------------------------------------------------- /Chapter09/README.md: -------------------------------------------------------------------------------- 1 | # Chapter 9 — CHAPTER_TITLE 2 | 3 | > Part of the "Python Object Oriented Programming Cookbook" title, 4 | > published by Packt 5 | 6 | > Code in this chapter was originally written against version 3.6.6 of Python, 7 | > as installed by default on the Author's then-current Linux Mint 19 (Cinnamon) 8 | > installation. 9 | 10 | [Recipe 00: **RECIPE_NAME**](FILE_NAME) — 11 | Description 12 | 13 | [Recipe 00: **RECIPE_NAME**](FILE_NAME) — 14 | Description 15 | 16 | [Recipe 00: **RECIPE_NAME**](FILE_NAME) — 17 | Description 18 | 19 | [Recipe 00: **RECIPE_NAME**](FILE_NAME) — 20 | Description 21 | 22 | [Recipe 00: **RECIPE_NAME**](FILE_NAME) — 23 | Description 24 | 25 | [Recipe 00: **RECIPE_NAME**](FILE_NAME) — 26 | Description 27 | 28 | -------------------------------------------------------------------------------- /Chapter10/README.md: -------------------------------------------------------------------------------- 1 | # Chapter 10 — CHAPTER_TITLE 2 | 3 | > Part of the "Python Object Oriented Programming Cookbook" title, 4 | > published by Packt 5 | 6 | > Code in this chapter was originally written against version 3.6.6 of Python, 7 | > as installed by default on the Author's then-current Linux Mint 19 (Cinnamon) 8 | > installation. 9 | 10 | [Recipe 00: **RECIPE_NAME**](FILE_NAME) — 11 | Description 12 | 13 | [Recipe 00: **RECIPE_NAME**](FILE_NAME) — 14 | Description 15 | 16 | [Recipe 00: **RECIPE_NAME**](FILE_NAME) — 17 | Description 18 | 19 | [Recipe 00: **RECIPE_NAME**](FILE_NAME) — 20 | Description 21 | 22 | [Recipe 00: **RECIPE_NAME**](FILE_NAME) — 23 | Description 24 | 25 | [Recipe 00: **RECIPE_NAME**](FILE_NAME) — 26 | Description 27 | 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Packt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python-Object-Oriented-Programming-Cookbook 2 | 3 | > Part of the "Python Object Oriented Programming Cookbook" title, 4 | > published by Packt 5 | 6 | [*Virtual Environment Set-up Notes*](VirtualEnvironmentNotes.md) — 7 | How to duplicate the Python Virtual Environment used to write the code 8 | in these recipes (if needed). 9 | 10 | [Chapter 1: **Idiomatic Python**](Chapter01/README.md) — 11 | Code for recipes dealing with various Python idioms that occur 12 | through the book 13 | 14 | [Chapter 2: **Introduction and Basic OOP Concepts**](Chapter02/README.md) — 15 | Description 16 | 17 | [Chapter 3: **The Pillars of Object Oriented Programming**](Chapter03/README.md) — 18 | Description 19 | 20 | [Chapter 4: **Advanced Python for OOP**](Chapter04/README.md) — 21 | Description 22 | 23 | [Chapter 5: **Object-Oriented Data-Structures**](Chapter05/README.md) — 24 | Description 25 | 26 | [Chapter 6: **CHAPTER_TITLE**](Chapter06/README.md) — 27 | Description 28 | 29 | [Chapter 7: **CHAPTER_TITLE**](Chapter07/README.md) — 30 | Description 31 | 32 | [Chapter 8: **CHAPTER_TITLE**](Chapter08/README.md) — 33 | Description 34 | 35 | [Chapter 9: **CHAPTER_TITLE**](Chapter09/README.md) — 36 | Description 37 | 38 | [Chapter 10: **CHAPTER_TITLE**](Chapter010/README.md) — 39 | Description 40 | 41 | 42 | -------------------------------------------------------------------------------- /VirtualEnvironmentNotes.md: -------------------------------------------------------------------------------- 1 | # Creating the Python VE (if needed) 2 | 3 | Because the default version of Python on the Author's system was in the 4 | 2.7.x range, a Python Virtual Environment was created specifically to 5 | provide the most current version of Python that the upstream provider 6 | actively supports. Code written against this VE (virtual environment) is 7 | generally expected to work for later versions (3.7.x) as well, but if 8 | there are any known point where that will fail, they will be noted. 9 | 10 | ## Linux instructions 11 | 12 | - Creating the virtual environment (`pyoopc`): 13 | 14 | `python3 -m venv ~/py_envs/pyoopc` 15 | 16 | Or use whatever other path is convenient/preferred over the `py-envs` 17 | specified — whatever that path is, it would be needed in the activation 18 | command-line call, below, as well) 19 | 20 | - Activating the `pyoopc` virtual environment: 21 | 22 | `source ~/py_envs/pyoopc/bin/activate` 23 | 24 | - Deactivating the `pyoopc` virtual environment: 25 | 26 | `deactivate` 27 | 28 | - Updating the associated `requirements.txt` file: 29 | 30 | `pip freeze | grep -v ".*==0\.0\.0" > requirements.txt` 31 | 32 | The `grep` removes any packages with malformed version metadata, 33 | which is a concern for any Ubuntu-based Linux distros — they tend to 34 | contain 35 | 36 | `pkg-resources==0.0.0` 37 | 38 | with some frequency 39 | 40 | 56 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | 2 | --------------------------------------------------------------------------------