├── .gitignore ├── 01_variables ├── bubble_sort.py ├── comments.py ├── complex_variable.py ├── consistency_of_var.py ├── too_many_vars.py └── typing_error.py ├── 02_number_string ├── bool_as_int.py ├── hello.txt ├── inf.py ├── int_precompile.py ├── jinja2_demo.py ├── literal_mystery.py ├── long_string.py ├── output.txt ├── partition_example.py ├── str_bytes.py ├── str_format.py ├── str_is_iterable.py ├── temp ├── text_vs_orm.py └── time_str_cat.py ├── 03_containers ├── ap_af.py ├── comments ├── dequeue_append_appendleft.py ├── dict_basic.py ├── generator_basic.py ├── intro.py ├── list_append_insert.py ├── list_comp.py ├── merge_dict.py ├── mutable.py ├── parse_access_log │ ├── analyzer_v1.py │ ├── analyzer_v2.py │ ├── generate_file.py │ └── logs.txt ├── remove_even.py ├── set_basic.py ├── set_instead_of_list.py ├── tuple_basic.py └── why_not_dict.py ├── 04_if_else ├── all_any.py ├── define_bool.py ├── define_eq.py ├── method_instead_of_expression.py ├── movies_ranker.py ├── movies_ranker_v2.py ├── nested_vs_flatten.py ├── not_a_and_not_b.py └── repetitive_codes.py ├── 05_exceptions ├── assert.py ├── context_manager.py ├── else_block.py ├── keyboard_int.py ├── min_try.py ├── null_obj_after.py ├── null_obj_before.py ├── return_bool_and_errmsg.py ├── return_bool_and_errmsg_v2.py ├── try_basics.py ├── try_vs_if.py └── wrap_exc.py ├── 06_loop ├── else_block.py ├── intro.py ├── islice_read_data.py ├── iter_product.py ├── iter_read.py ├── labeled_break.py ├── python_doc.txt ├── range7_gen.py ├── range7_iterator.py ├── reddit_titles.txt ├── small_file.txt └── yield_func.py ├── 07_function ├── arguments_patterns.py ├── closure_demo.py ├── fib.py ├── first_album.py ├── first_album_new.py ├── func_states.py ├── functools_demo.py ├── intro.py ├── more_returns.py └── none.py ├── 08_decorators ├── basic.py ├── class_as_deco.py ├── class_decorator.py ├── deco_pattern.py ├── optional_arguments.py ├── wraps.py └── wrapt_exam.py ├── 09_oop ├── abc_validator.py ├── basic.py ├── composition.py ├── composition_v2.py ├── decorators.py ├── duck_typing.py ├── func_oop.py ├── iter_demo.py ├── mixin.py ├── mro.py └── polymorphism.py ├── 10_solid_p1 ├── news_digester.py ├── news_digester_O1.py ├── news_digester_O2.py ├── news_digester_O3.py ├── news_digester_O_before.py ├── news_digester_S1.py ├── news_digester_S2.py └── type_hints.py ├── 11_solid_p2 ├── hn_site_grouper.py ├── hn_site_grouper_D1.py ├── hn_site_grouper_D2.py ├── lsp_1.py ├── lsp_2.py ├── lsp_3.py ├── lsp_rect_square.py ├── static_hn.html └── test_hn_site_groups.py ├── 12_data_model ├── com_op_override.py ├── dangerous_hash.py ├── data_non_data.py ├── del_demo.py ├── descriptor.py ├── dig_users.py ├── info_descriptor.py ├── obj_getitem.py └── str_demo.py ├── 13_engineering ├── black_demo.py ├── flake8_demo.py ├── flake8_error.py ├── isort_demo.py ├── string_utils.py ├── test_string_utils.py └── test_upper.py ├── LICENSE ├── Makefile └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /01_variables/bubble_sort.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | def magic_bubble_sort_2(numbers): 5 | """有魔力的冒泡排序算法,所有的偶数都被认为比奇数大""" 6 | j = len(numbers) - 1 7 | while j > 0: 8 | for i in range(j): 9 | if numbers[i] % 2 == 0 and numbers[i + 1] % 2 == 1: 10 | numbers[i], numbers[i + 1] = numbers[i + 1], numbers[i] 11 | continue 12 | elif (numbers[i + 1] % 2 == numbers[i] % 2) and numbers[i] > numbers[i + 1]: 13 | numbers[i], numbers[i + 1] = numbers[i + 1], numbers[i] 14 | continue 15 | j -= 1 16 | return numbers 17 | 18 | 19 | from typing import List 20 | 21 | 22 | def magic_bubble_sort(numbers: List[int]): 23 | """有魔力的冒泡排序算法,所有的偶数都被认为比奇数大 24 | 25 | :param numbers: 需要排序的列表,函数将会直接修改原始列表 26 | """ 27 | stop_position = len(numbers) - 1 28 | while stop_position > 0: 29 | for i in range(stop_position): 30 | current, next_ = numbers[i], numbers[i + 1] 31 | current_is_even, next_is_even = current % 2 == 0, next_ % 2 == 0 32 | should_swap = False 33 | 34 | # 交换位置的两个条件: 35 | # - 前面是偶数,后面是奇数 36 | # - 前面和后面同为奇数或者偶数,但是前面比后面大 37 | if current_is_even and not next_is_even: 38 | should_swap = True 39 | elif current_is_even == next_is_even and current > next_: 40 | should_swap = True 41 | 42 | if should_swap: 43 | numbers[i], numbers[i + 1] = numbers[i + 1], numbers[i] 44 | stop_position -= 1 45 | return numbers 46 | 47 | 48 | if __name__ == "__main__": 49 | l = [23, 32, 1, 3, 4, 19, 20, 2, 4] 50 | print(magic_bubble_sort(l)) 51 | print(magic_bubble_sort_2(l)) 52 | -------------------------------------------------------------------------------- /01_variables/comments.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | def resize_image(image, size): 5 | """将图片缩放为指定尺寸,并返回新的图片。 6 | 7 | 该函数将使用 Pilot 模块读取文件对象,然后调用 .resize() 方法将其缩放为指定尺寸。 8 | 9 | 但由于 Pilot 模块自身限制,这个函数不能很好的处理尺寸过大的文件,当文件大小 10 | 超过 5MB 时,resize() 方法的性能就会因为内存分配问题急剧下降,详见 Pilot 模块的 11 | Issue #007。因此,对于超过 5MB 的图片文件,请使用 resize_big_image() 替代,后者 12 | 基于 Pillow 模块开发,很好的解决了内存分配问题,性能更好。 13 | 14 | :param image: 图片文件对象 15 | :param size: 包含宽高的元组:(width, height) 16 | :return: 新图片对象 17 | """ 18 | 19 | 20 | def resize_image(image, size): 21 | """将图片缩放为指定尺寸,并返回新的图片。 22 | 23 | 注意:当文件超过 5MB 时,请使用 resize_big_image() 24 | 25 | :param image: 图片文件对象 26 | :param size: 包含宽高的元组:(width, height) 27 | :return: 新图片对象 28 | """ -------------------------------------------------------------------------------- /01_variables/complex_variable.py: -------------------------------------------------------------------------------- 1 | # 为所有性别为女性,或者级别大于 3 的活跃用户发放 10000 个金币 2 | if user.is_active and (user.sex == 'female' or user.level > 3): 3 | user.add_coins(10000) 4 | return -------------------------------------------------------------------------------- /01_variables/consistency_of_var.py: -------------------------------------------------------------------------------- 1 | def get_users() -> str: 2 | # users 本身是一个 Dict 3 | users = {"data": ["piglei", "raymond"]} 4 | # 尝试复用 users 这个变量,把它变成 List 类型 5 | users = [] 6 | return users 7 | 8 | 9 | if __name__ == "__main__": 10 | get_users() 11 | -------------------------------------------------------------------------------- /01_variables/too_many_vars.py: -------------------------------------------------------------------------------- 1 | def import_users_from_file(fp): 2 | """尝试从文件对象读取用户,然后导入到数据库中 3 | 4 | :param fp: 可读文件对象 5 | :return: 成功与失败数量 6 | """ 7 | # 初始化变量:重复用户、黑名单用户、正常用户 8 | duplicated_users, banned_users, normal_users = [], [], [] 9 | for line in fp: 10 | parsed_user = parse_user(line) 11 | # ... 进行判断处理,修改上面定义的 {X}_users 变量 12 | 13 | succeeded_count, failed_count = 0, 0 14 | # ... 读取 {X}_users 变量,写入数据库并修改成功失败数量 15 | return succeeded_count, failed_count 16 | 17 | 18 | class ImportedSummary: 19 | """保存导入结果摘要的数据类""" 20 | 21 | def __init__(self): 22 | self.succeeded_count = 0 23 | self.failed_count = 0 24 | 25 | 26 | class ImportingUserGroup: 27 | """用于暂存用户导入处理的数据类""" 28 | 29 | def __init__(self): 30 | self.duplicated = [] 31 | self.banned = [] 32 | self.normal = [] 33 | 34 | 35 | def import_users_from_file(fp): 36 | """尝试从文件对象读取用户,然后导入到数据库中 37 | 38 | :param fp: 可读文件对象 39 | :return: 成功与失败数量 40 | """ 41 | importing_user_group = ImportingUserGroup() 42 | for line in fp: 43 | parsed_user = parse_user(line) 44 | # ... 进行判断处理,修改上面定义的 importing_user_group 变量 45 | 46 | summary = ImportedSummary() 47 | # ... 读取 importing_user_group,写入数据库并修改成功失败数量 48 | return summary.succeeded_count, summary.failed_count 49 | -------------------------------------------------------------------------------- /01_variables/typing_error.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | 4 | def remove_invalid(items: List[int]): 5 | pass 6 | 7 | 8 | remove_invalid(["piglei", "raymond"]) 9 | -------------------------------------------------------------------------------- /02_number_string/bool_as_int.py: -------------------------------------------------------------------------------- 1 | numbers = [1, 2, 4, 5, 7] 2 | 3 | count = 0 4 | for i in numbers: 5 | if i % 2 == 0: 6 | count += 1 7 | 8 | print(count) 9 | 10 | count = sum(i % 2 == 0 for i in numbers) 11 | print(count) 12 | -------------------------------------------------------------------------------- /02_number_string/hello.txt: -------------------------------------------------------------------------------- 1 | Hello, 中文。 2 | -------------------------------------------------------------------------------- /02_number_string/inf.py: -------------------------------------------------------------------------------- 1 | users = {"tom": 19, "jenny": 13, "jack": None, "andrew": 43} 2 | 3 | 4 | def sort_users(users): 5 | 6 | # 普通写法:生成一份复合排序 key:(是否没有年龄,年龄) 7 | def key_func(username): 8 | age = users[username] 9 | # 当年龄为空时,第一个元素值为 True,永远会被排在最后面 10 | return age is None, age 11 | 12 | return sorted(users.keys(), key=key_func) 13 | 14 | 15 | def sort_users_inf(users): 16 | def key_func(username): 17 | age = users[username] 18 | # 当年龄为空时,返回正无穷大做为 key,因此就会被排到最后面 19 | return age if age is not None else float('inf') 20 | 21 | return sorted(users.keys(), key=key_func) 22 | 23 | 24 | print(sort_users(users)) 25 | print(sort_users_inf(users)) 26 | -------------------------------------------------------------------------------- /02_number_string/int_precompile.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import dis 3 | 4 | 5 | def do_something(delta_seconds): 6 | # 如果时间已经过去11 天(或者更久),不做任何事 7 | if delta_seconds < 11 * 24 * 3600: 8 | return 9 | ... 10 | 11 | 12 | dis.dis(do_something) 13 | -------------------------------------------------------------------------------- /02_number_string/jinja2_demo.py: -------------------------------------------------------------------------------- 1 | def render_movies(username, movies): 2 | """ 3 | 文本方式展示电影列表信息 4 | """ 5 | welcome_text = 'Welcome, {}.\n'.format(username) 6 | text_parts = [welcome_text] 7 | for name, rating in movies: 8 | # 没有提供评分的电影,以 [NOT RATED] 代替 9 | rating_text = rating if rating else '[NOT RATED]' 10 | movie_item = '* {}, Rating: {}'.format(name, rating_text) 11 | text_parts.append(movie_item) 12 | return '\n'.join(text_parts) 13 | 14 | 15 | from jinja2 import Template 16 | 17 | _MOVIES_TMPL = '''\ 18 | Welcome, {{username}}. 19 | {%for name, rating in movies %} 20 | * {{ name }}, Rating: {{ rating|default("[NOT RATED]", True) }} 21 | {%- endfor %} 22 | ''' 23 | 24 | 25 | def render_movies_j2(username, movies): 26 | tmpl = Template(_MOVIES_TMPL) 27 | return tmpl.render(username=username, movies=movies) 28 | 29 | 30 | movies = [ 31 | ('The Shawshank Redemption', '9.3'), 32 | ('The Prestige', '8.5'), 33 | ('Mulan', None), 34 | ] 35 | 36 | print(render_movies('piglei', movies)) 37 | print(render_movies_j2('piglei', movies)) 38 | -------------------------------------------------------------------------------- /02_number_string/literal_mystery.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | def add_daily_points(user): 5 | """当用户每天第一次登录后,为其增加积分""" 6 | if user.type == 13: 7 | return 8 | if user.type == 3: 9 | user.points += 120 10 | return 11 | user.points += 100 12 | return 13 | 14 | 15 | DAILY_POINTS_REWARDS = 100 16 | VIP_EXTRA_POINTS = 20 17 | 18 | from enum import Enum 19 | 20 | 21 | class UserType(int, Enum): 22 | # VIP 用户 23 | VIP = 3 24 | # 小黑屋用户 25 | BANNED = 13 26 | 27 | 28 | def add_daily_points(user): 29 | """当用户每天第一次登录后,为其增加积分""" 30 | if user.type == UserType.BANNED: 31 | return 32 | if user.type == UserType.VIP: 33 | user.points += DAILY_POINTS_REWARDS + VIP_EXTRA_POINTS 34 | return 35 | user.points += DAILY_POINTS_REWARDS 36 | return 37 | 38 | 39 | UserType.XX 40 | -------------------------------------------------------------------------------- /02_number_string/long_string.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # A simple logger as print 3 | import textwrap 4 | import logging 5 | import logging.handlers 6 | 7 | hdr = logging.StreamHandler() 8 | hdr.setFormatter(logging.Formatter('[%(asctime)s] %(name)s:%(levelname)s: %(message)s')) 9 | logger = logging.getLogger(__name__) 10 | logger.addHandler(hdr) 11 | logger.setLevel(logging.DEBUG) 12 | 13 | def main(): 14 | logger.info(("There is something really bad happened during the " 15 | "process. Please contact your administrator.")) 16 | 17 | def main(): 18 | # if user.is_active: 19 | if True: 20 | message = textwrap.dedent("""\ 21 | Welcome, here is your movie list: 22 | - Jaw (1975) 23 | - The Shining (1980) 24 | - Saw (2004)""") 25 | print(message) 26 | 27 | 28 | 29 | if __name__ == '__main__': 30 | main() 31 | -------------------------------------------------------------------------------- /02_number_string/output.txt: -------------------------------------------------------------------------------- 1 | Super SunflowerS. -------------------------------------------------------------------------------- /02_number_string/partition_example.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | def extract_value(s): 5 | items = s.split(':') 6 | # 因为 s 不一定会包含 ':',所以需要对结果长度进行判断 7 | if len(items) == 2: 8 | return items[1] 9 | else: 10 | return '' 11 | 12 | 13 | print(repr(extract_value('name:piglei'))) 14 | print(repr(extract_value('name'))) 15 | 16 | 17 | def extract_value_v2(s): 18 | # 当 s 包含分隔符 : 时,元组最后一个成员刚好是 value。 19 | # 若是没有分隔符,最后一个成员默认是空字符串 '' 20 | return s.partition(':')[-1] 21 | 22 | 23 | print(repr(extract_value_v2('name:piglei'))) 24 | print(repr(extract_value_v2('name'))) 25 | 26 | 27 | with open('temp', 'w', encoding='gbk') as fp: 28 | fp.write('你好,世界。') -------------------------------------------------------------------------------- /02_number_string/str_bytes.py: -------------------------------------------------------------------------------- 1 | with open('hello.txt', 'r') as fp: 2 | print(fp.read()) 3 | 4 | # with open('hello.txt', 'r', encoding='gbk') as fp: 5 | # print(fp.read()) 6 | 7 | with open('hello.txt', 'rb') as fp: 8 | print(fp.read()) 9 | 10 | 11 | with open('output.txt', 'w', encoding='gbk') as fp: 12 | fp.write('你好,中国') 13 | # fp.write('你好,中国'.encode('utf-8')) 14 | 15 | 16 | def upper_s(s): 17 | """把输入字符串里的所有 "s" 都转为大写""" 18 | return s.replace('s', 'S') 19 | 20 | 21 | bin_obj = b'super sunflowers.' 22 | str_obj = bin_obj.decode('utf-8') 23 | print(upper_s(str_obj)) 24 | 25 | 26 | with open('output.txt', 'wb') as fp: 27 | str_obj = upper_s('super sunflowers.') 28 | bin_obj = str_obj.encode('utf-8') 29 | fp.write(bin_obj) 30 | -------------------------------------------------------------------------------- /02_number_string/str_format.py: -------------------------------------------------------------------------------- 1 | username, score = 'piglei', 100 2 | 3 | # f-string 4 | print(f'Welcome {username}, your score is {score:d}') 5 | 6 | # str.format 7 | print('Welcome {}, your score is {:d}'.format(username, score)) 8 | # C 风格格式化 9 | print('Welcome %s, your score is %d' % (username, score)) 10 | 11 | 12 | print('{:>20}'.format(username)) 13 | print(f'{username:>20}') 14 | 15 | 16 | print('{0}: name={0} score={1}'.format(username, score)) 17 | -------------------------------------------------------------------------------- /02_number_string/str_is_iterable.py: -------------------------------------------------------------------------------- 1 | usernames = ['piglei', 'raymondzhu'] 2 | 3 | for username in usernames: 4 | print(username) 5 | 6 | 7 | chars = 'abc' 8 | 9 | # 展开赋值为多个变量,就好像 a, b, c = ['a', 'b', 'c'] 一样 10 | a, b, c = chars 11 | 12 | # 循环整个字符串 13 | for ch in chars: 14 | print(ch) 15 | 16 | # 对字符串做切片 17 | print(chars[:2]) 18 | # 输出: 19 | # ab 20 | 21 | # 调用 join 方法拼接 22 | print('_'.join(chars)) 23 | # 输出: 24 | # a_b_c 25 | 26 | # 用星号表达式展开 27 | print('{}{}{}'.format(*chars)) 28 | # 输出: 29 | # abc 30 | -------------------------------------------------------------------------------- /02_number_string/temp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piglei/the-python-craftsman-book/98fa4909ac71e345df37332c8ccfed7393df7ee7/02_number_string/temp -------------------------------------------------------------------------------- /02_number_string/text_vs_orm.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import datetime 3 | from sqlalchemy import create_engine 4 | from sqlalchemy import ( 5 | Table, 6 | Column, 7 | Integer, 8 | String, 9 | MetaData, 10 | ForeignKey, 11 | DateTime, 12 | Boolean, 13 | ) 14 | from sqlalchemy.sql import select 15 | 16 | 17 | metadata = MetaData() 18 | 19 | users = Table( 20 | 'users', 21 | metadata, 22 | Column('id', Integer, primary_key=True), 23 | Column('name', String(32)), 24 | Column('gender', Integer, default=0), 25 | Column('level', Integer, default=1), 26 | Column('has_membership', Boolean, default=False), 27 | Column('updated', DateTime, default=datetime.datetime.now), 28 | Column('created', DateTime, default=datetime.datetime.now), 29 | ) 30 | 31 | 32 | GENDER_FEMALE = 0 33 | GENDER_MALE = 1 34 | 35 | 36 | def fetch_users( 37 | conn, 38 | min_level=None, 39 | gender=None, 40 | has_membership=False, 41 | sort_field="created", 42 | ): 43 | """获取用户列表 44 | 45 | :param min_level: 要求的最低用户级别,默认为所有级别 46 | :type min_level: int, optional 47 | :param gender: 筛选用户性别,默认为所有性别 48 | :type gender: int, optional 49 | :param has_membership: 筛选会员或非会员用户,默认为 False,代表非会员 50 | :type has_membership: bool, optional 51 | :param sort_field: 排序字段,默认为 "created",代表按用户创建日期排序 52 | :type sort_field: str, optional 53 | :return: 一个包含用户信息的列表:[(User ID, User Name), ...] 54 | """ 55 | # 一种古老的 SQL 拼接技巧,使用 "WHERE 1=1" 来简化字符串拼接操作 56 | statement = "SELECT id, name FROM users WHERE 1=1" 57 | params = [] 58 | if min_level is not None: 59 | statement += " AND level >= ?" 60 | params.append(min_level) 61 | if gender is not None: 62 | statement += " AND gender >= ?" 63 | params.append(gender) 64 | if has_membership: 65 | statement += " AND has_membership = true" 66 | else: 67 | statement += " AND has_membership = false" 68 | 69 | statement += " ORDER BY ?" 70 | params.append(sort_field) 71 | # 将查询参数 params 作为位置参数传递,避免 SQL 注入问题 72 | return list(conn.execute(statement, params)) 73 | 74 | 75 | def fetch_users_v2( 76 | conn, 77 | min_level=None, 78 | gender=None, 79 | has_membership=False, 80 | sort_field="created", 81 | ): 82 | """获取用户列表""" 83 | query = select([users.c.id, users.c.name]) 84 | if min_level != None: 85 | query = query.where(users.c.level >= min_level) 86 | if gender != None: 87 | query = query.where(users.c.gender == gender) 88 | query = query.where(users.c.has_membership == has_membership).order_by(users.c[sort_field]) 89 | return list(conn.execute(query)) 90 | 91 | 92 | def main(): 93 | engine = create_engine('sqlite:///:memory:', echo=True) 94 | metadata.create_all(engine) 95 | 96 | conn = engine.connect() 97 | conn.execute( 98 | users.insert(), 99 | [ 100 | { 101 | "name": "piglei", 102 | "gender": 1, 103 | "level": 2, 104 | "has_membership": False, 105 | }, 106 | { 107 | "name": "cotton", 108 | "gender": 0, 109 | "level": 4, 110 | "has_membership": True, 111 | }, 112 | {"name": "zyx", "gender": 0, "level": 1, "has_membership": True}, 113 | ], 114 | ) 115 | 116 | for func in (fetch_users, fetch_users_v2): 117 | print(func(conn)) 118 | print(func(conn, min_level=2, has_membership=True)) 119 | print(func(conn, min_level=2, gender=1)) 120 | 121 | 122 | if __name__ == '__main__': 123 | main() 124 | -------------------------------------------------------------------------------- /02_number_string/time_str_cat.py: -------------------------------------------------------------------------------- 1 | # 定义一个 100 长度的词汇列表 2 | WORDS = ['Hello', 'string', 'performance', 'test'] * 25 3 | 4 | 5 | def str_cat(): 6 | """使用字符串拼接""" 7 | s = '' 8 | for word in WORDS: 9 | s += word 10 | return s 11 | 12 | 13 | def str_join(): 14 | """使用列表配合 join 产生字符串""" 15 | l = [] 16 | for word in WORDS: 17 | l.append(word) 18 | return ''.join(l) 19 | 20 | 21 | print(str_cat()) 22 | print(str_join()) 23 | 24 | import timeit 25 | 26 | 27 | # 默认执行 100 万次 28 | cat_spent = timeit.timeit(setup='from __main__ import str_cat', stmt='str_cat()') 29 | print("cat_spent:", cat_spent) 30 | 31 | join_spent = timeit.timeit(setup='from __main__ import str_join', stmt='str_join()') 32 | print("join_spent", join_spent) 33 | -------------------------------------------------------------------------------- /03_containers/ap_af.py: -------------------------------------------------------------------------------- 1 | # AF: Ask for Forgiveness 2 | # 要做就做,如果抛出异常了,再处理异常 3 | def counter_af(l): 4 | result = {} 5 | for key in l: 6 | try: 7 | result[key] += 1 8 | except KeyError: 9 | result[key] = 1 10 | return result 11 | 12 | 13 | # AP: Ask for Permission 14 | # 做之前,先问问能不能做,可以做再做 15 | def counter_ap(l): 16 | result = {} 17 | for key in l: 18 | if key in result: 19 | result[key] += 1 20 | else: 21 | result[key] = 1 22 | return result 23 | -------------------------------------------------------------------------------- /03_containers/comments: -------------------------------------------------------------------------------- 1 | Implementation note 2 | Changed 3 | ABC for generator 4 | -------------------------------------------------------------------------------- /03_containers/dequeue_append_appendleft.py: -------------------------------------------------------------------------------- 1 | from collections import deque 2 | 3 | 4 | def deque_append(): 5 | """不断往尾部追加""" 6 | l = deque() 7 | for i in range(5000): 8 | l.append(i) 9 | 10 | 11 | def deque_appendleft(): 12 | """不断往头部插入""" 13 | l = deque() 14 | for i in range(5000): 15 | l.appendleft(i) 16 | 17 | 18 | import timeit 19 | 20 | 21 | # 默认执行 1 万次 22 | append_spent = timeit.timeit( 23 | setup='from __main__ import deque_append', 24 | stmt='deque_append()', 25 | number=10000, 26 | ) 27 | print("deque_append:", append_spent) 28 | 29 | appendleft_spent = timeit.timeit( 30 | setup='from __main__ import deque_appendleft', 31 | stmt='deque_appendleft()', 32 | number=10000, 33 | ) 34 | print("deque_appendleft", appendleft_spent) 35 | -------------------------------------------------------------------------------- /03_containers/dict_basic.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | movie = {'name': 'Burning', 'type': 'movie', 'year': 2018} 4 | 5 | # 通过 key 来获取某个 value,如果 Key 不存在会抛出 KeyError 6 | print(movie['year']) 7 | 8 | # 字典是一种可变类型,所以你可以给它增加新的 key 9 | movie['rating'] = '10' 10 | 11 | # 最常用的两种遍历方式: 12 | # 13 | # 遍历字典的所有 key 14 | for key in movie: 15 | print(key) 16 | # 遍历字典的所有 key/value 键值对: 17 | for key, value in movie.items(): 18 | print(key, value) 19 | 20 | 21 | # 判断某个 key 是否存在 22 | # 返回:True / False 23 | 'some_key' in movie 24 | 25 | # 尝试获取某个值,如不存在时返回 default 默认值 26 | movie.get('some_key', default='DEFAULT') 27 | 28 | # 批量修改字典内容 29 | movie.update(year=2020, rating=1) 30 | 31 | 32 | from collections import OrderedDict 33 | 34 | d = OrderedDict() 35 | # d = {} 36 | d['FIRST_KEY'] = 1 37 | d['SECOND_KEY'] = 2 38 | 39 | for key in d: 40 | print(key) 41 | -------------------------------------------------------------------------------- /03_containers/generator_basic.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | def simple_gen(): 5 | """一个简单生成器,返回数字 123""" 6 | yield 1 7 | yield 2 8 | yield 3 9 | 10 | 11 | def generate_even(max_number): 12 | """一个简单生成器,返回 0 到 max_number 之间的所有偶数""" 13 | for i in range(0, max_number): 14 | # yield 和 return 最大的不同之处在于,return 会直接中断整个函数执行, 15 | # 返回结果,而 yield 会由循环语句触发,一步一步的往外生成多个结果 16 | if i % 2 == 0: 17 | yield i 18 | 19 | 20 | for i in generate_even(10): 21 | print(i) 22 | -------------------------------------------------------------------------------- /03_containers/intro.py: -------------------------------------------------------------------------------- 1 | class Foo: 2 | def __init__(self, value): 3 | self.value = value 4 | 5 | 6 | foo = Foo('bar') 7 | print(foo.__dict__, type(foo.__dict__)) 8 | 9 | 10 | names = ('foo', 'bar') 11 | names[1] = 'x' 12 | -------------------------------------------------------------------------------- /03_containers/list_append_insert.py: -------------------------------------------------------------------------------- 1 | def list_append(): 2 | """不断往尾部追加""" 3 | l = [] 4 | for i in range(5000): 5 | l.append(i) 6 | 7 | 8 | def list_insert(): 9 | """不断往头部插入""" 10 | l = [] 11 | for i in range(5000): 12 | l.insert(0, i) 13 | 14 | 15 | import timeit 16 | 17 | 18 | # 默认执行 1 万次 19 | append_spent = timeit.timeit( 20 | setup='from __main__ import list_append', 21 | stmt='list_append()', 22 | number=1000, 23 | ) 24 | print("list_append:", append_spent) 25 | 26 | insert_spent = timeit.timeit( 27 | setup='from __main__ import list_insert', 28 | stmt='list_insert()', 29 | number=1000, 30 | ) 31 | print("list_insert", insert_spent) 32 | -------------------------------------------------------------------------------- /03_containers/list_comp.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | def remove_odd_mul_100(numbers): 5 | """剔除奇数并乘 100""" 6 | results = [] 7 | for number in numbers: 8 | if number % 2 == 1: 9 | continue 10 | results.append(number * 100) 11 | return results 12 | 13 | 14 | numbers = [3, 4, 12, 17] 15 | 16 | print(remove_odd_mul_100(numbers)) 17 | 18 | 19 | results = [n * 100 for n in numbers if n % 2 == 0] 20 | print(results) 21 | 22 | results = [ 23 | n * 100 if str(n).startswith('3') else n * 1000 24 | for n in numbers 25 | if n % 2 == 0 26 | ] 27 | print(results) 28 | 29 | 30 | results = [ 31 | task.result if task.result_version == VERSION_2 else get_legacy_result(task) 32 | for tasks_group in tasks 33 | for task in in tasks_group 34 | if task.is_active() and task.has_completed() 35 | ] 36 | 37 | results = [] 38 | for tasks_group in tasks: 39 | for task in tasks_group: 40 | if not (task.is_active() and task.has_completed()): 41 | continue 42 | 43 | if task.result_version == VERSION_2: 44 | result = task.result 45 | else: 46 | result = get_legacy_result(task) 47 | results.append(result) 48 | 49 | 50 | 51 | print([(i, j) for i in range(2) for j in range(10)]) 52 | -------------------------------------------------------------------------------- /03_containers/merge_dict.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | def merge_dict(d1, d2): 5 | # 因为字典是可被修改的对象,为了避免修改原对象,此处需要复制一个 d1 的备份 6 | result = d1.copy() 7 | result.update(d2) 8 | return result 9 | 10 | 11 | print(merge_dict({"name": "piglei"}, {"movies": ["Fight Club"]})) 12 | user = {**{"name": "piglei"}, **{"movies": ["Fight Club"]}} 13 | print(user) 14 | -------------------------------------------------------------------------------- /03_containers/mutable.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | def add_str(in_func_obj): 5 | print(f'In add [before]: in_func_obj="{in_func_obj}"') 6 | in_func_obj += ' suffix' 7 | print(f'In add [after]: in_func_obj="{in_func_obj}"') 8 | 9 | 10 | orig_obj = 'foo' 11 | print(f'Outside [before]: orig_obj="{orig_obj}"') 12 | add_str(orig_obj) 13 | print(f'Outside [after]: orig_obj="{orig_obj}"') 14 | 15 | 16 | def add_list(in_func_obj): 17 | print(f'In add [before]: in_func_obj="{in_func_obj}"') 18 | in_func_obj += ['baz'] 19 | print(f'In add [after]: in_func_obj="{in_func_obj}"') 20 | 21 | 22 | orig_obj = ['foo', 'bar'] 23 | print(f'Outside [before]: orig_obj="{orig_obj}"') 24 | add_list(orig_obj) 25 | print(f'Outside [after]: orig_obj="{orig_obj}"') 26 | -------------------------------------------------------------------------------- /03_containers/parse_access_log/analyzer_v1.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from enum import Enum 3 | 4 | 5 | class PagePerfLevel(str, Enum): 6 | LT_100 = 'Less than 100 ms' 7 | LT_300 = 'Between 100 and 300 ms' 8 | LT_1000 = 'Between 300 ms and 1 s' 9 | GT_1000 = 'Greater than 1 s' 10 | 11 | 12 | def analyze_v1(): 13 | path_groups = {} 14 | with open("logs.txt", "r") as fp: 15 | for line in fp: 16 | path, time_cost_str = line.strip().split() 17 | 18 | # 根据页面耗时计算性能级别 19 | time_cost = int(time_cost_str) 20 | if time_cost < 100: 21 | level = PagePerfLevel.LT_100 22 | elif time_cost < 300: 23 | level = PagePerfLevel.LT_300 24 | elif time_cost < 1000: 25 | level = PagePerfLevel.LT_1000 26 | else: 27 | level = PagePerfLevel.GT_1000 28 | 29 | # 如果路径第一次出现,存入初始值 30 | if path not in path_groups: 31 | path_groups[path] = {} 32 | 33 | # 如果性能 level 第一次出现,存入初始值 1 34 | try: 35 | path_groups[path][level] += 1 36 | except KeyError: 37 | path_groups[path][level] = 1 38 | 39 | for path, result in path_groups.items(): 40 | print(f'== Path: {path}') 41 | total = sum(result.values()) 42 | print(f' Total requests: {total}') 43 | print(f' Performance:') 44 | 45 | # 在输出结果前,按照“性能级别”在 PagePerfLevel 里面的顺序排列,小于 100 毫秒 46 | # 的在最前面 47 | sorted_items = sorted(result.items(), key=lambda pair: list(PagePerfLevel).index(pair[0])) 48 | for level_name, count in sorted_items: 49 | print(f' - {level_name}: {count}') 50 | 51 | 52 | if __name__ == "__main__": 53 | analyze_v1() -------------------------------------------------------------------------------- /03_containers/parse_access_log/analyzer_v2.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from enum import Enum 3 | from collections import defaultdict 4 | from collections.abc import MutableMapping 5 | 6 | 7 | class PagePerfLevel(str, Enum): 8 | LT_100 = 'Less than 100 ms' 9 | LT_300 = 'Between 100 and 300 ms' 10 | LT_1000 = 'Between 300 ms and 1 s' 11 | GT_1000 = 'Greater than 1 s' 12 | 13 | 14 | class PerfLevelDict(MutableMapping): 15 | """存储响应时间性能级别的字典""" 16 | 17 | def __init__(self): 18 | self.data = defaultdict(int) 19 | 20 | def __getitem__(self, key): 21 | """当某个级别不存在时,默认返回 0""" 22 | return self.data[self.compute_level(key)] 23 | 24 | def __setitem__(self, key, value): 25 | """将 key 转换为对应的性能级别,然后设置值""" 26 | self.data[self.compute_level(key)] = value 27 | 28 | def __delitem__(self, key): 29 | del self.data[key] 30 | 31 | def __iter__(self): 32 | return iter(self.data) 33 | 34 | def __len__(self): 35 | return len(self.data) 36 | 37 | def items(self): 38 | """按照顺序返回性能级别数据""" 39 | return sorted( 40 | self.data.items(), 41 | key=lambda pair: list(PagePerfLevel).index(pair[0]), 42 | ) 43 | 44 | def total_requests(self): 45 | """返回总请求数""" 46 | return sum(self.values()) 47 | 48 | @staticmethod 49 | def compute_level(time_cost_str): 50 | """根据响应时间计算性能级别""" 51 | # 假如已经是性能等级,不做转换直接返回 52 | if time_cost_str in list(PagePerfLevel): 53 | return time_cost_str 54 | 55 | time_cost = int(time_cost_str) 56 | if time_cost < 100: 57 | return PagePerfLevel.LT_100 58 | elif time_cost < 300: 59 | return PagePerfLevel.LT_300 60 | elif time_cost < 1000: 61 | return PagePerfLevel.LT_1000 62 | return PagePerfLevel.GT_1000 63 | 64 | 65 | def analyze_v2(): 66 | path_groups = defaultdict(PerfLevelDict) 67 | with open("logs.txt", "r") as fp: 68 | for line in fp: 69 | path, time_cost = line.strip().split() 70 | path_groups[path][time_cost] += 1 71 | 72 | for path, result in path_groups.items(): 73 | print(f'== Path: {path}') 74 | print(f' Total requests: {result.total_requests()}') 75 | print(f' Performance:') 76 | for level_name, count in result.items(): 77 | print(f' - {level_name}: {count}') 78 | 79 | 80 | if __name__ == '__main__': 81 | analyze_v2() -------------------------------------------------------------------------------- /03_containers/parse_access_log/generate_file.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import random 3 | 4 | PATHS = ( 5 | "/", 6 | "/", 7 | "/", 8 | "/", 9 | "/about/", 10 | "/about/", 11 | "/articles/15-thinking-in-edge-cases/", 12 | "/articles/15-thinking-in-edge-cases/", 13 | "/articles/what-celeste-teaches-me-about-programming/", 14 | "/articles/three-tips-on-writing-file-related-codes/", 15 | "/articles/write-solid-python-codes-part-1/", 16 | "/admin/", 17 | ) 18 | 19 | for i in range(10000): 20 | path = random.choice(PATHS) 21 | time_cost = random.randint(10, 2000) 22 | print(f"{path} {time_cost}") 23 | -------------------------------------------------------------------------------- /03_containers/remove_even.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | def remove_even(numbers): 5 | """去掉列表里所有的偶数""" 6 | # for i, number in enumerate(numbers): 7 | # if number % 2 == 0: 8 | # # 有问题的代码 9 | # del numbers[i] 10 | for number in numbers: 11 | if number % 2 == 0: 12 | numbers.remove(number) 13 | 14 | 15 | numbers = [1, 2, 7, 4, 8, 11] 16 | remove_even(numbers) 17 | print(numbers) 18 | # OUTPUT: [1, 7, 8, 11] 19 | -------------------------------------------------------------------------------- /03_containers/set_basic.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 集合字面量的语法和字典很像,都是使用大括号,但是不是 "key: value" 的格式 4 | fruits = {'apple', 'orange', 'apple', 'pineapple'} 5 | print(fruits) 6 | # >>> 7 | # 集合的效果:重复的 'apple' 消失了,顺序也被打乱了。 8 | # {'pineapple', 'orange', 'apple'} 9 | 10 | # 注意:{} 表示的是一个空字典,而不是一个空集合 11 | # empty_set = {} 12 | 13 | # 正确初始化一个空集合 14 | empty_set = set() 15 | 16 | # 通过可迭代对象创建一个新集合 17 | new_set = set(['foo', 'foo', 'bar']) 18 | 19 | 20 | fruits_1 = {'apple', 'orange', 'pineapple'} 21 | fruits_2 = {'tomato', 'orange', 'grapes', 'mango'} 22 | 23 | # 求交集:两个集合中都有的东西 24 | print(fruits_1 & fruits_2) 25 | print(fruits_1.intersection(fruits_2)) 26 | # >>> 27 | # {'orange'} 28 | 29 | # 求并集:两个集合的东西合起来 30 | print(fruits_1 | fruits_2) 31 | print(fruits_1.union(fruits_2)) 32 | # >>> 33 | # {'mango', 'orange', 'grapes', 'pineapple', 'apple', 'tomato'} 34 | 35 | # 求差集:一个有,另一个没有的东西 36 | print(fruits_1 - fruits_2) 37 | print(fruits_1.difference(fruits_2)) 38 | # >>> 39 | # {'apple', 'pineapple'} 40 | 41 | 42 | valid_set = set(['apple', 30, 1.3, ('foo')]) 43 | # 可以成功初始化 44 | 45 | invalid_set = set(['foo', [1, 2, 3]]) 46 | # >>> 47 | # 报错:TypeError: unhashable type: 'list' -------------------------------------------------------------------------------- /03_containers/set_instead_of_list.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 这个例子不是特别恰当,因为当目标集合特别小时,使用集合还是列表对效率的影响微乎其微 3 | # 但这不是重点 :) 4 | 5 | VALID_NAMES = ["piglei", "raymond", "jack", "bojack"] 6 | 7 | # 转换为集合类型专门用于成员判断 8 | VALID_NAMES_SET = set(VALID_NAMES) 9 | 10 | 11 | def validate_name(name): 12 | if name not in VALID_NAMES_SET: 13 | raise ValueError(f"{name} is not a valid name!") 14 | -------------------------------------------------------------------------------- /03_containers/tuple_basic.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | def get_rectangle(): 5 | """返回长方形的宽和高""" 6 | width = 100 7 | height = 20 8 | return width, height 9 | 10 | 11 | result = get_rectangle() 12 | print(result, type(result)) 13 | 14 | rectangle = (100, 20) 15 | print(rectangle[0]) 16 | print(rectangle[1]) 17 | 18 | 19 | from collections import namedtuple 20 | 21 | Rectangle = namedtuple('Rectangle', 'width,height') 22 | 23 | from typing import NamedTuple 24 | 25 | 26 | class Rectangle1(NamedTuple): 27 | width: int 28 | height: int 29 | 30 | 31 | # rect = rectangle(100, 20) 32 | rect = Rectangle(width=100, height=20) 33 | # 可以像普通元组一样,通过数字索引访问成员 34 | print(rect[0]) 35 | 36 | # 也能通过字段名称来访问 37 | print(rect.width) 38 | print(rect.height) 39 | 40 | rect_str = Rectangle('string', 'not_a_number') 41 | 42 | 43 | # rect.width += 1 44 | # namedtuple 同样不允许被修改,执行后报错: 45 | # attributeerror: can't set attribute -------------------------------------------------------------------------------- /03_containers/why_not_dict.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | class UpperDict(dict): 3 | """总是把 key 转为大写""" 4 | 5 | def __setitem__(self, key, value): 6 | super().__setitem__(key.upper(), value) -------------------------------------------------------------------------------- /04_if_else/all_any.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | numbers = [3, 5, 7, 13, 7] 4 | 5 | 6 | def all_numbers_gt_10(numbers): 7 | """仅当序列中所有数字大于 10 时,返回 True""" 8 | if not numbers: 9 | return False 10 | 11 | for n in numbers: 12 | if n <= 10: 13 | return False 14 | return True 15 | 16 | 17 | def all_numbers_gt_10_2(numbers): 18 | return bool(numbers) and all(n > 10 for n in numbers) 19 | 20 | 21 | def any_numbers_gt_10(numbers): 22 | """只要序列中有任意数字大于 10 ,返回 True""" 23 | if not numbers: 24 | return False 25 | 26 | for n in numbers: 27 | if n < 10: 28 | return True 29 | return False 30 | -------------------------------------------------------------------------------- /04_if_else/define_bool.py: -------------------------------------------------------------------------------- 1 | class UserCollection: 2 | """用户保存多个用户的集合工具类""" 3 | 4 | def __init__(self, users): 5 | self.items = users 6 | 7 | def __len__(self): 8 | return len(self.items) 9 | 10 | 11 | users = UserCollection(['piglei', 'raymond']) 12 | 13 | # 仅当用户列表里面有数据时,打印语句 14 | if len(users.items) > 0: 15 | print("There's some users in collection!") 16 | 17 | # 定义了 __len__ 方法后,UserCollection 对象本身就可以被用于布尔判断了 18 | if users: 19 | print("There's some users in collection!") 20 | 21 | 22 | class ScoreJudger: 23 | """仅当分数大于 60 时为真""" 24 | 25 | def __init__(self, score): 26 | self.score = score 27 | 28 | def __bool__(self): 29 | return self.score >= 60 30 | -------------------------------------------------------------------------------- /04_if_else/define_eq.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | class EqualWithAnything: 5 | """与任何对象相等""" 6 | 7 | def __eq__(self, other): 8 | # 方法里的 other 方法代表 == 操作时右边的对象,比如 9 | # x == y 会调用 x 的 __eq__ 方法,other 参数为 y 10 | return True -------------------------------------------------------------------------------- /04_if_else/method_instead_of_expression.py: -------------------------------------------------------------------------------- 1 | # 活动:如果活动还在开放,并且活动剩余名额大于 10,为所有性别为女性,或者级别大于 2 | # 3 的活跃用户发放 10000 个金币 3 | if ( 4 | activity.is_active 5 | and activity.remaining > 10 6 | and user.is_active 7 | and (user.sex == 'female' or user.level > 3) 8 | ): 9 | user.add_coins(10000) 10 | return 11 | 12 | 13 | if activity.allow_new_user() and user.match_activity_condition(): 14 | user.add_coins(10000) 15 | return 16 | -------------------------------------------------------------------------------- /04_if_else/movies_ranker.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import random 3 | 4 | movies = [ 5 | {'name': 'The Dark Knight', 'year': 2008, 'rating': '9'}, 6 | {'name': 'Kaili Blues', 'year': 2015, 'rating': '7.3'}, 7 | {'name': 'Citizen Kane', 'year': 1941, 'rating': '8.3'}, 8 | {'name': 'Project Gutenberg', 'year': 2018, 'rating': '6.9'}, 9 | {'name': 'Burning', 'year': 2018, 'rating': '7.5'}, 10 | {'name': 'The Shawshank Redemption ', 'year': 1994, 'rating': '9.3'}, 11 | ] 12 | 13 | 14 | class Movie: 15 | """电影对象数据类""" 16 | 17 | def __init__(self, name, year, rating): 18 | self.name = name 19 | self.year = year 20 | self.rating = rating 21 | 22 | @property 23 | def rank(self): 24 | """按照评分对电影分级: 25 | 26 | - S: 8.5 分及以上 27 | - A:8 - 8.5 分 28 | - B:7 - 8 分 29 | - C:6 - 7 分 30 | - D:6 分以下 31 | """ 32 | rating_num = float(self.rating) 33 | if rating_num >= 8.5: 34 | return 'S' 35 | elif rating_num >= 8: 36 | return 'A' 37 | elif rating_num >= 7: 38 | return 'B' 39 | elif rating_num >= 6: 40 | return 'C' 41 | else: 42 | return 'D' 43 | 44 | 45 | def get_sorted_movies(movies, sorting_type): 46 | """对电影列表进行排序并返回 47 | 48 | :param movies: Movie 对象列表 49 | :param sorting_type: 排序选项,可选值 50 | name(名称)、rating(评分)、year(年份)、random(随机乱序) 51 | """ 52 | if sorting_type == 'name': 53 | sorted_movies = sorted(movies, key=lambda movie: movie.name.lower()) 54 | elif sorting_type == 'rating': 55 | sorted_movies = sorted(movies, key=lambda movie: float(movie.rating), reverse=True) 56 | elif sorting_type == 'year': 57 | sorted_movies = sorted(movies, key=lambda movie: movie.year, reverse=True) 58 | elif sorting_type == 'random': 59 | sorted_movies = sorted(movies, key=lambda movie: random.random()) 60 | else: 61 | raise RuntimeError(f'Unknown sorting type: {sorting_type}') 62 | return sorted_movies 63 | 64 | 65 | all_sorting_types = ('name', 'rating', 'year', 'random') 66 | 67 | 68 | def main(): 69 | # 接收用户输入的排序选项 70 | sorting_type = input('Please input sorting type: ') 71 | if sorting_type not in all_sorting_types: 72 | print( 73 | 'Sorry, "{}" is not a valid sorting type, please choose from ' 74 | '"{}", exit now'.format( 75 | sorting_type, 76 | '/'.join(all_sorting_types), 77 | ) 78 | ) 79 | return 80 | 81 | # 初始化电影数据对象 82 | movie_items = [] 83 | for movie_json in movies: 84 | movie = Movie(**movie_json) 85 | movie_items.append(movie) 86 | 87 | # 排序并输出电影列表 88 | sorted_movies = get_sorted_movies(movie_items, sorting_type) 89 | for movie in sorted_movies: 90 | print(f'- [{movie.rank}] {movie.name}({movie.year}) | rating: {movie.rating}') 91 | 92 | 93 | if __name__ == '__main__': 94 | main() 95 | -------------------------------------------------------------------------------- /04_if_else/movies_ranker_v2.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import bisect 3 | import random 4 | 5 | movies = [ 6 | {'name': 'The Dark Knight', 'year': 2008, 'rating': '9'}, 7 | {'name': 'Kaili Blues', 'year': 2015, 'rating': '7.3'}, 8 | {'name': 'Citizen Kane', 'year': 1941, 'rating': '8.3'}, 9 | {'name': 'Project Gutenberg', 'year': 2018, 'rating': '6.9'}, 10 | {'name': 'Burning', 'year': 2018, 'rating': '7.5'}, 11 | {'name': 'The Shawshank Redemption ', 'year': 1994, 'rating': '9.3'}, 12 | ] 13 | 14 | 15 | class Movie: 16 | """电影对象数据类""" 17 | 18 | def __init__(self, name, year, rating): 19 | self.name = name 20 | self.year = year 21 | self.rating = rating 22 | 23 | @property 24 | def rank(self): 25 | """ 26 | 按照评分对电影分级 27 | """ 28 | # 已经排好序的评级分界点 29 | breakpoints = (6, 7, 8, 8.5) 30 | # 各评分区间级别名 31 | grades = ('D', 'C', 'B', 'A', 'S') 32 | 33 | index = bisect.bisect(breakpoints, float(self.rating)) 34 | return grades[index] 35 | 36 | 37 | def get_sorted_movies(movies, sorting_type): 38 | """对电影列表进行排序并返回 39 | 40 | :param movies: Movie 对象列表 41 | :param sorting_type: 排序选项,可选值 42 | name(名称)、rating(评分)、year(年份)、random(随机乱序) 43 | """ 44 | sorting_algos = { 45 | # sorting_type: (key_func, reverse) 46 | 'name': (lambda movie: movie.name.lower(), False), 47 | 'rating': (lambda movie: float(movie.rating), True), 48 | 'year': (lambda movie: movie.year, True), 49 | 'random': (lambda movie: random.random(), False), 50 | } 51 | try: 52 | key_func, reverse = sorting_algos[sorting_type] 53 | except KeyError: 54 | raise RuntimeError(f'Unknown sorting type: {sorting_type}') 55 | 56 | sorted_movies = sorted(movies, key=key_func, reverse=reverse) 57 | return sorted_movies 58 | 59 | 60 | all_sorting_types = ('name', 'rating', 'year', 'random') 61 | 62 | 63 | def main(): 64 | # 接收用户输入的排序选项 65 | sorting_type = input('Please input sorting type: ') 66 | if sorting_type not in all_sorting_types: 67 | print( 68 | 'Sorry, "{}" is not a valid sorting type, please choose from ' 69 | '"{}", exit now'.format( 70 | sorting_type, 71 | '/'.join(all_sorting_types), 72 | ) 73 | ) 74 | return 75 | 76 | # 初始化电影数据对象 77 | movie_items = [] 78 | for movie_json in movies: 79 | movie = Movie(**movie_json) 80 | movie_items.append(movie) 81 | 82 | # 排序并输出电影列表 83 | sorted_movies = get_sorted_movies(movie_items, sorting_type) 84 | for movie in sorted_movies: 85 | print(f'- [{movie.rank}] {movie.name}({movie.year}) | rating: {movie.rating}') 86 | 87 | 88 | if __name__ == '__main__': 89 | main() 90 | -------------------------------------------------------------------------------- /04_if_else/nested_vs_flatten.py: -------------------------------------------------------------------------------- 1 | def buy_fruit(nerd, store): 2 | """去水果店买苹果 3 | 4 | - 先得看看店是不是在营业 5 | - 如果有苹果的话,就买 1 个 6 | - 如果钱不够,就回家取钱再来 7 | """ 8 | if store.is_open(): 9 | if store.has_stocks("apple"): 10 | if nerd.can_afford(store.price("apple", amount=1)): 11 | nerd.buy(store, "apple", amount=1) 12 | return 13 | else: 14 | nerd.go_home_and_get_money() 15 | return buy_fruit(nerd, store) 16 | else: 17 | raise MadAtNoFruit("no apple in store!") 18 | else: 19 | raise MadAtNoFruit("store is closed!") 20 | 21 | 22 | def buy_fruit_version2(nerd, store): 23 | if not store.is_open(): 24 | raise MadAtNoFruit("store is closed!") 25 | 26 | if not store.has_stocks("apple"): 27 | raise MadAtNoFruit("no apple in store!") 28 | 29 | if nerd.can_afford(store.price("apple", amount=1)): 30 | nerd.buy(store, "apple", amount=1) 31 | return 32 | else: 33 | nerd.go_home_and_get_money() 34 | return buy_fruit(nerd, store) 35 | -------------------------------------------------------------------------------- /04_if_else/not_a_and_not_b.py: -------------------------------------------------------------------------------- 1 | if not user.has_logged_in or not user.is_from_chrome: 2 | return "our service is only open for chrome logged in user" 3 | 4 | if not (user.has_logged_in and user.is_from_chrome): 5 | return "our service is only open for chrome logged in user" 6 | -------------------------------------------------------------------------------- /04_if_else/repetitive_codes.py: -------------------------------------------------------------------------------- 1 | # 仅当分组处于活跃状态时,允许用户加入分组并记录操作日志 2 | if group.is_active: 3 | user = get_user_by_id(request.user_id) 4 | user.join(group) 5 | log_user_activiry(user, target=group, type=ActivityType.JOINED_GROUP) 6 | else: 7 | user = get_user_by_id(request.user_id) 8 | log_user_activiry(user, target=group, type=ActivityType.JOIN_GROUP_FAILED) 9 | 10 | 11 | user = get_user_by_id(request.user_id) 12 | 13 | if group.is_active: 14 | user.join(group) 15 | activity_type = UserActivityType.JOINED_GROUP 16 | else: 17 | activity_type = UserActivityType.JOIN_GROUP_FAILED 18 | 19 | log_user_activiry(user, target=group, type=activity_type) 20 | 21 | 22 | # 创建或更新用户资料数据 23 | # 如果是新用户,创建新 Profile 数据,否则更新已有数据 24 | if user.no_profile_exists: 25 | create_user_profile( 26 | username=data.username, 27 | gender=data.gender, 28 | email=data.email, 29 | age=data.age, 30 | address=data.address, 31 | points=0, 32 | created=now(), 33 | ) 34 | else: 35 | update_user_profile( 36 | username=data.username, 37 | gender=data.gender, 38 | email=data.email, 39 | age=data.age, 40 | address=data.address, 41 | updated=now(), 42 | ) 43 | 44 | 45 | if user.no_profile_exists: 46 | _update_or_create = create_user_profile 47 | extra_args = {'points': 0, 'created': now()} 48 | else: 49 | _update_or_create = update_user_profile 50 | extra_args = {'updated': now()} 51 | 52 | _update_or_create( 53 | username=user.username, 54 | gender=user.gender, 55 | email=user.email, 56 | age=user.age, 57 | address=user.address, 58 | **extra_args, 59 | ) -------------------------------------------------------------------------------- /05_exceptions/assert.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | def print_string(s): 5 | assert isinstance(s, str), 's must be string' 6 | print(s) 7 | 8 | 9 | print_string(3) 10 | print_string('foo') 11 | 12 | 13 | def print_string(s): 14 | if not isinstance(s, str): 15 | raise TypeError('s must be string') 16 | print(s) 17 | -------------------------------------------------------------------------------- /05_exceptions/context_manager.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import random 3 | 4 | 5 | class ignore_closed: 6 | """忽略已经关闭的连接""" 7 | 8 | def __enter__(self): 9 | pass 10 | 11 | def __exit__(self, exc_type, exc_value, traceback): 12 | if exc_type == AlreadyClosedError: 13 | return True 14 | return False 15 | 16 | 17 | with ignore_closed(): 18 | close_conn(conn) 19 | 20 | 21 | class DummyContext: 22 | def __init__(self, name): 23 | self.name = name 24 | 25 | def __enter__(self): 26 | # __enter__ 会在进入管理器时被调用,同时可以返回结果 27 | # 返回一个增加了随机后缀的 name 28 | return f'{self.name}-{random.random()}' 29 | 30 | def __exit__(self, exc_type, exc_value, traceback): 31 | # __exit__ 会在退出管理器时被调用 32 | print('Exiting DummyContext') 33 | return False 34 | 35 | 36 | with DummyContext('foo') as name: 37 | print(f'Name: {name}') 38 | 39 | 40 | conn = create_conn(host, port, timeout=None) 41 | try: 42 | conn.send_text('Hello, world!') 43 | except Exception as e: 44 | print(f'Unable to use connection: {e}') 45 | finally: 46 | conn.close() 47 | 48 | 49 | class create_conn_obj: 50 | """创建连接对象,并在退出上下文时自动关闭""" 51 | 52 | def __init__(self, host, port, timeout=None): 53 | self.conn = create_conn(host, port, timeout=timeout) 54 | 55 | def __enter__(self): 56 | return self.conn 57 | 58 | def __exit__(self, exc_type, exc_val, exc_tb): 59 | # 退出管理器时关闭连接 60 | self.conn.close() 61 | return False 62 | 63 | 64 | with create_conn_obj(host, port, timeout=None) as conn: 65 | try: 66 | conn.send_text('Hello, world!') 67 | except Exception as e: 68 | print(f'Unable to use connection: {e}') 69 | 70 | 71 | try: 72 | close_conn(conn) 73 | except AlreadyClosedError: 74 | pass 75 | 76 | from contextlib import contextmanager 77 | 78 | 79 | @contextmanager 80 | def create_conn_obj(host, port, timeout=None): 81 | """创建连接对象,并在退出上下文时自动关闭""" 82 | conn = create_conn(host, port, timeout=timeout) 83 | try: 84 | yield conn 85 | finally: 86 | conn.close() 87 | -------------------------------------------------------------------------------- /05_exceptions/else_block.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | def sync_user_profile(user): 5 | # 同步用户资料到外部系统,仅当同步成功时发送通知消息 6 | sync_succeeded = False 7 | try: 8 | sync_profile(user.profile, to_external=True) 9 | sync_succeeded = True 10 | except Exception as e: 11 | print("Error while syncing user profile") 12 | 13 | if sync_succeeded: 14 | send_notification(user, 'profile sync succeeded') 15 | 16 | 17 | def sync_user_profile(user): 18 | try: 19 | sync_profile(user.profile, to_external=True) 20 | except Exception as e: 21 | print("Error while syncing user profile") 22 | else: 23 | send_notification(user, 'profile sync succeeded') -------------------------------------------------------------------------------- /05_exceptions/keyboard_int.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | while True: 4 | s = input('Input a string: ') 5 | print(s) -------------------------------------------------------------------------------- /05_exceptions/min_try.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import requests 3 | import re 4 | 5 | 6 | def save_website_title(url, filename): 7 | """获取某个地址的网页标题,然后将其写入到文件中 8 | 9 | :return: 如果成功保存,返回 True,否则打印错误,返回 False 10 | """ 11 | try: 12 | resp = requests.get(url) 13 | obj = re.search(r'(.*)', resp.text) 14 | if not obj: 15 | print('save failed: title tag not found in page content') 16 | return False 17 | 18 | title = obj.grop(1) 19 | with open(filename, 'w') as fp: 20 | fp.write(title) 21 | return True 22 | except Exception: 23 | print(f'save failed: unable to save title of {url} to {filename}') 24 | return False 25 | 26 | 27 | from requests.exceptions import RequestException 28 | 29 | 30 | def save_website_title(url, filename): 31 | """获取某个地址的网页标题,然后将其写入到文件中""" 32 | try: 33 | resp = requests.get(url) 34 | except RequestException as e: 35 | print(f'save failed: unable to get page content: {e}') 36 | return False 37 | 38 | # 这段正则操作本身就是不应该抛出异常的,所以我们没必要使用 try 语句块 39 | # 假如 group 被误打成了 grop 也没关系,程序马上就会通过 AttributeError 来 40 | # 告诉我们。 41 | obj = re.search(r'(.*)', resp.text) 42 | if not obj: 43 | print('save failed: title tag not found in page content') 44 | return False 45 | title = obj.group(1) 46 | 47 | try: 48 | with open(filename, 'w') as fp: 49 | fp.write(title) 50 | except IOError as e: 51 | print(f'save failed: unable to write to file {filename}: {e}') 52 | return False 53 | else: 54 | return True 55 | 56 | 57 | def main(): 58 | save_website_title('https://www.qq.com', 'qq_title.txt') 59 | save_website_title('https://localha23r232l3kj4l2j34.com', 'qq_title.txt') 60 | save_website_title('https://www.qq.com', '/qq_title.txt') 61 | 62 | 63 | if __name__ == '__main__': 64 | main() 65 | -------------------------------------------------------------------------------- /05_exceptions/null_obj_after.py: -------------------------------------------------------------------------------- 1 | QUALIFIED_POINTS = 80 2 | 3 | 4 | class UserPoint: 5 | """用户得分记录""" 6 | 7 | def __init__(self, username, points): 8 | self.username = username 9 | self.points = points 10 | 11 | def is_qualified(self): 12 | """返回得分是否合格""" 13 | return self.points >= QUALIFIED_POINTS 14 | 15 | 16 | class NullUserPoint: 17 | """一个空的用户得分记录""" 18 | 19 | username = '' 20 | points = 0 21 | 22 | def is_qualified(self): 23 | return False 24 | 25 | 26 | def make_userpoint(point_string): 27 | """从字符串初始化一条得分记录 28 | 29 | :param point_string: 形如 "piglei 1" 的表示得分记录的字符串 30 | :return: 如果输入合法,返回 UserPoint 对象,否则返回 NullUserPoint 31 | """ 32 | try: 33 | username, points = point_string.split() 34 | points = int(points) 35 | except ValueError: 36 | return NullUserPoint() 37 | 38 | if points < 0: 39 | return NullUserPoint() 40 | return UserPoint(username=username, points=points) 41 | 42 | 43 | def calc_qualified_count(points_data): 44 | """计算得分合格的总人数 45 | 46 | :param points_data: 字符串格式的用户得分列表 47 | """ 48 | return sum(make_userpoint(s).is_qualified() for s in points_data) 49 | 50 | 51 | data = [ 52 | 'piglei 96', 53 | 'nobody 61', 54 | 'cotton 83', 55 | 'invalid_data', 56 | 'roland $invalid_points', 57 | 'alfred -3', 58 | ] 59 | 60 | print(calc_qualified_count(data)) 61 | -------------------------------------------------------------------------------- /05_exceptions/null_obj_before.py: -------------------------------------------------------------------------------- 1 | QUALIFIED_POINTS = 80 2 | 3 | 4 | class CreateUserPointError(Exception): 5 | """创建得分纪录失败时抛出""" 6 | 7 | 8 | class UserPoint: 9 | """用户得分记录""" 10 | 11 | def __init__(self, username, points): 12 | self.username = username 13 | self.points = points 14 | 15 | def is_qualified(self): 16 | """返回得分是否合格""" 17 | return self.points >= QUALIFIED_POINTS 18 | 19 | 20 | def make_userpoint(point_string): 21 | """从字符串初始化一条得分记录 22 | 23 | :param point_string: 形如 "piglei 1" 的表示得分记录的字符串 24 | :return: UserPoint 对象 25 | :raises: 当输入数据不合法时返回 CreateUserPointError 26 | """ 27 | try: 28 | username, points = point_string.split() 29 | points = int(points) 30 | except ValueError: 31 | raise CreateUserPointError('input must follow pattern "{username} {points}"') 32 | 33 | if points < 0: 34 | raise CreateUserPointError('points can not be negative') 35 | return UserPoint(username=username, points=points) 36 | 37 | 38 | def calc_qualified_count(points_data): 39 | """计算得分合格的总人数 40 | 41 | :param points_data: 字符串格式的用户得分列表 42 | """ 43 | result = 0 44 | for point_string in points_data: 45 | try: 46 | point_obj = make_userpoint(point_string) 47 | except CreateUserPointError: 48 | pass 49 | else: 50 | result += point_obj.is_qualified() 51 | return result 52 | 53 | 54 | data = [ 55 | 'piglei 96', 56 | 'nobody 61', 57 | 'cotton 83', 58 | 'invalid_data', 59 | 'roland $invalid_points', 60 | 'alfred -3', 61 | ] 62 | 63 | print(calc_qualified_count(data)) 64 | -------------------------------------------------------------------------------- /05_exceptions/return_bool_and_errmsg.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | MAX_LENGTH_OF_NAME = 12 4 | MAX_ITEMS_QUOTA = 10 5 | 6 | 7 | def get_current_items(): 8 | return [] 9 | 10 | 11 | class Item: 12 | def __init__(self, name): 13 | self.name = name 14 | 15 | 16 | def create_item(name): 17 | """接收名称,创建 Item 对象 18 | 19 | :return: (对象,错误信息),成功时错误信息为 '' 20 | """ 21 | if len(name) > MAX_LENGTH_OF_NAME: 22 | return None, 'name of item is too long' 23 | if len(get_current_items()) > MAX_ITEMS_QUOTA: 24 | return None, 'items is full' 25 | return Item(name=name), '' 26 | 27 | 28 | def create_from_input(): 29 | name = input() 30 | item, err_msg = create_item(name) 31 | if err_msg: 32 | print(f'create item failed: {err_msg}') 33 | else: 34 | print('item<{name}> created') 35 | -------------------------------------------------------------------------------- /05_exceptions/return_bool_and_errmsg_v2.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | MAX_LENGTH_OF_NAME = 12 4 | MAX_ITEMS_QUOTA = 10 5 | 6 | 7 | def get_current_items(): 8 | return [] 9 | 10 | 11 | class Item: 12 | def __init__(self, name): 13 | self.name = name 14 | 15 | 16 | class CreateItemError(Exception): 17 | """创建 Item 失败""" 18 | 19 | 20 | def create_item(name): 21 | """创建一个新的 Item 22 | 23 | :raises: 当无法创建时抛出 CreateItemError 24 | """ 25 | if len(name) > MAX_LENGTH_OF_NAME: 26 | raise CreateItemError('name of item is too long') 27 | if len(get_current_items()) > MAX_ITEMS_QUOTA: 28 | raise CreateItemError('items is full') 29 | return Item(name=name) 30 | 31 | 32 | def create_from_input(): 33 | name = input() 34 | try: 35 | item = create_item(name) 36 | except CreateItemError as e: 37 | print(f'create item failed: {e}') 38 | else: 39 | print(f'item<{name}> created') 40 | 41 | 42 | class CreateItemError(Exception): 43 | """创建 Item 失败""" 44 | 45 | 46 | class CreateErrorItemsFull(CreateItemError): 47 | """当前的 Item 容器已满""" 48 | 49 | 50 | class CreateItemError(Exception): 51 | """创建 Item 失败 52 | 53 | :param error_code: 错误代码 54 | :param message: 错误信息 55 | """ 56 | 57 | def __init__(self, error_code, message): 58 | self.error_code = error_code 59 | self.message = message 60 | super().__init__(f'{self.error_code} - {self.message}') 61 | 62 | 63 | raise CreateItemError('name_too_long', 'name of item is too long') 64 | raise CreateItemError('items_full', 'items is full') 65 | -------------------------------------------------------------------------------- /05_exceptions/try_basics.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | def safe_int(value): 5 | """尝试把输入转换为整数""" 6 | try: 7 | return int(value) 8 | except TypeError: 9 | # 当某类异常被抛出时,将会执行对应 except 下的语句 10 | print(f'type error: {type(value)} is invalid') 11 | except ValueError: 12 | # 你可以在一个 try 语句块下写多个 except 13 | print(f'value error: {value} is invalid') 14 | else: 15 | print('====================') 16 | finally: 17 | # finally 里的语句,无论如何都会被执行,哪怕 try 里直接 return 18 | print('function completed') 19 | 20 | 21 | def incr_by_key(d, key): 22 | try: 23 | d[key] += 1 24 | except KeyError: 25 | print(f'key {key} does not exists, re-raise the exception') 26 | raise 27 | 28 | 29 | safe_int(3) 30 | safe_int(None) 31 | safe_int('asdf') 32 | 33 | incr_by_key({'foo': 1}, 'bar') -------------------------------------------------------------------------------- /05_exceptions/try_vs_if.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | def incr_by_one(value): 5 | """对输入整数 + 1 ,返回新的值 6 | 7 | :param value: 整型,或者可以转成整型的字符串 8 | :return: 整型结果 9 | """ 10 | if isinstance(value, int): 11 | return value + 1 12 | elif isinstance(value, str) and value.isdigit(): 13 | return int(value) + 1 14 | else: 15 | print(f'Unable to perform incr for value: "{value}"') 16 | 17 | 18 | def incr_by_one(value): 19 | """对输入整数 + 1 ,返回新的值 20 | 21 | :param value: 整型,或者可以转成整型的字符串 22 | :return: 整型结果 23 | """ 24 | try: 25 | return int(value) + 1 26 | except (TypeError, ValueError) as e: 27 | print(f'Unable to perform incr for value: "{value}", error: {e}') 28 | 29 | 30 | def main(): 31 | print(incr_by_one(5)) 32 | print(incr_by_one('73')) 33 | print(incr_by_one('not_a_number')) 34 | print(incr_by_one(object())) 35 | 36 | 37 | if __name__ == '__main__': 38 | main() 39 | -------------------------------------------------------------------------------- /05_exceptions/wrap_exc.py: -------------------------------------------------------------------------------- 1 | class ImageOpenError(Exception): 2 | """图像打开错误异常类 3 | 4 | :param exc: 原始异常 5 | """ 6 | 7 | def __init__(self, exc): 8 | self.exc = exc 9 | # 调用异常父类方法,初始化错误信息 10 | super().__init__(f'Image open error: {self.exc}') 11 | -------------------------------------------------------------------------------- /06_loop/else_block.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | def process_tasks(tasks): 5 | """批量处理任务,如遇到状态不为 Pending 的任务则中止本次处理。""" 6 | non_pending_found = False 7 | for task in tasks: 8 | if not task.is_pending(): 9 | non_pending_found = True 10 | break 11 | process(task) 12 | 13 | if non_pending_found: 14 | notify_admin('Found non-pending task, processing aborted.') 15 | else: 16 | notify_admin('All tasks was processed.') 17 | 18 | 19 | def process_tasks(tasks): 20 | """批量处理任务,如遇到状态不为 Pending 的任务则中止本次处理。""" 21 | for task in tasks: 22 | if not task.is_pending(): 23 | notify_admin('Found non-pending task, processing aborted.') 24 | break 25 | process(task) 26 | else: 27 | notify_admin('All tasks was processed.') 28 | 29 | 30 | def process_tasks(tasks): 31 | """批量处理任务并将结果通知管理员。""" 32 | if _process_tasks(tasks): 33 | notify_admin('All tasks was processed.') 34 | else: 35 | notify_admin('Found non-pending task, processing aborted.') 36 | 37 | 38 | def _process_tasks(tasks): 39 | """批量处理任务,如遇到状态不为 Pending 的任务则中止本次处理。 40 | 41 | :return: 是否完全处理所有任务 42 | :rtype: bool 43 | """ 44 | for task in tasks: 45 | if not task.is_pending(): 46 | return False 47 | process(task) 48 | return True 49 | -------------------------------------------------------------------------------- /06_loop/intro.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | names = ['foo', 'bar', 'foobar'] 4 | 5 | for name in names: 6 | print(len(name)) 7 | 8 | i = 0 9 | 10 | while i < len(names): 11 | print(len(names[i])) 12 | i += 1 -------------------------------------------------------------------------------- /06_loop/islice_read_data.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from itertools import islice 3 | 4 | 5 | def parse_titles(filename): 6 | """从隔行数据文件中读取 reddit 主题名称""" 7 | with open(filename, 'r') as fp: 8 | for i, line in enumerate(fp): 9 | # 跳过无意义的 '---' 分隔符 10 | if i % 2 == 0: 11 | yield line.strip() 12 | 13 | 14 | def parse_titles_v2(filename): 15 | with open(filename, 'r') as fp: 16 | # 设置 step=2,跳过无意义的 '---' 分隔符 17 | for line in islice(fp, 0, None, 2): 18 | yield line.strip() 19 | 20 | 21 | print(list(parse_titles('reddit_titles.txt'))) 22 | print(list(parse_titles_v2('reddit_titles.txt'))) 23 | -------------------------------------------------------------------------------- /06_loop/iter_product.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | numbers_1 = [1, 0, 5, 13] 3 | numbers_2 = [3, 4, -1, 2] 4 | numbers_3 = [9, 0, 3, -4] 5 | 6 | 7 | def find_twelve(num_list1, num_list2, num_list3): 8 | """从 3 个数字列表中,寻找是否存在和为 12 的 3 个数""" 9 | for num1 in num_list1: 10 | for num2 in num_list2: 11 | for num3 in num_list3: 12 | if num1 + num2 + num3 == 12: 13 | return num1, num2, num3 14 | 15 | 16 | print(find_eight(numbers_1, numbers_2, numbers_3)) 17 | 18 | 19 | from itertools import product 20 | 21 | 22 | def find_twelve_v2(num_list1, num_list2, num_list3): 23 | for num1, num2, num3 in product(num_list1, num_list2, num_list3): 24 | if num1 + num2 + num3 == 12: 25 | return num1, num2, num3 26 | 27 | 28 | print(find_eight_v2(numbers_1, numbers_2, numbers_3)) 29 | -------------------------------------------------------------------------------- /06_loop/iter_read.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | def count_digits(fname): 5 | """计算文件里包含多少个数字字符""" 6 | count = 0 7 | with open(fname) as file: 8 | for line in file: 9 | for s in line: 10 | if s.isdigit(): 11 | count += 1 12 | return count 13 | 14 | 15 | def count_digits_v2(fname): 16 | """计算文件里包含多少个数字字符,每次读取 8kb""" 17 | count = 0 18 | block_size = 1024 * 8 19 | with open(fname) as file: 20 | while True: 21 | chunk = file.read(block_size) 22 | # 当文件没有更多内容时,read 调用将会返回空字符串 '' 23 | if not chunk: 24 | break 25 | for s in chunk: 26 | if s.isdigit(): 27 | count += 1 28 | return count 29 | 30 | 31 | from functools import partial 32 | 33 | 34 | def count_digits_v3(fname): 35 | count = 0 36 | block_size = 1024 * 8 37 | with open(fname) as fp: 38 | # 使用 functools.partial 构造一个新的无需参数的函数 39 | _read = partial(fp.read, block_size) 40 | 41 | # 利用 iter() 构造一个不断调用 _read 的迭代器 42 | for chunk in iter(_read, ''): 43 | for s in chunk: 44 | if s.isdigit(): 45 | count += 1 46 | return count 47 | 48 | 49 | def read_file_digits(fp, block_size=1024 * 8): 50 | """生成器函数:分块读取文件内容,返回其中的数字字符""" 51 | _read = partial(fp.read, block_size) 52 | for chunk in iter(_read, ''): 53 | for s in chunk: 54 | if s.isdigit(): 55 | yield s 56 | 57 | 58 | def count_digits_v4(fname): 59 | count = 0 60 | with open(fname) as file: 61 | for num in read_file_digits(file): 62 | count += 1 63 | return count 64 | 65 | 66 | from collections import defaultdict 67 | 68 | 69 | def count_even_groups(fname): 70 | """分别统计文件里每个偶数字符出现的个数""" 71 | counter = defaultdict(int) 72 | with open(fname) as file: 73 | for num in read_file_digits(file): 74 | if int(num) % 2 == 0: 75 | counter[int(num)] += 1 76 | return counter 77 | 78 | 79 | print(count_digits('small_file.txt')) 80 | print(count_digits_v2('small_file.txt')) 81 | print(count_digits_v3('small_file.txt')) 82 | print(count_digits_v4('small_file.txt')) 83 | print(count_even_groups('small_file.txt')) -------------------------------------------------------------------------------- /06_loop/labeled_break.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | def print_first_word(fp, prefix): 5 | """找到文件里,第一个以指定前缀开头的单词,并打印出来 6 | 7 | :param fp: 可读文件对象 8 | :param prefix: 需要寻找的单词前缀 9 | """ 10 | first_word = None 11 | for line in fp: 12 | for word in line.split(): 13 | if word.startswith(prefix): 14 | first_word = word 15 | # 注意:此处的 break 只能跳出最内层循环 16 | break 17 | # 一定要在外层加一个额外的 break 语句来判断是否结束循环 18 | if first_word: 19 | break 20 | 21 | if first_word: 22 | print(f'Found the first word startswith "{prefix}": "{first_word}"') 23 | else: 24 | print(f'Word starts with "{prefix}" was not found.') 25 | 26 | 27 | def find_first_word(fp, prefix): 28 | """找到文件里,第一个以指定前缀开头的单词,并打印出来 29 | 30 | :param fp: 可读文件对象 31 | :param prefix: 需要寻找的单词前缀 32 | """ 33 | for line in fp: 34 | for word in line.split(): 35 | if word.startswith(prefix): 36 | return word 37 | return None 38 | 39 | 40 | def print_first_word(fp, prefix): 41 | first_word = find_first_word(fp, prefix) 42 | if first_word: 43 | print(f'Found the first word startswith "{prefix}": "{first_word}"') 44 | else: 45 | print(f'Word starts with "{prefix}" was not found.') 46 | 47 | 48 | with open('python_doc.txt') as fp: 49 | print_first_word(fp, 're') 50 | -------------------------------------------------------------------------------- /06_loop/python_doc.txt: -------------------------------------------------------------------------------- 1 | 1. Whetting Your Appetite 2 | If you do much work on computers, eventually you find that there’s some task you’d like to automate. For example, you may wish to perform a search-and-replace over a large number of text files, or rename and rearrange a bunch of photo files in a complicated way. Perhaps you’d like to write a small custom database, or a specialized GUI application, or a simple game. 3 | 4 | If you’re a professional software developer, you may have to work with several C/C++/Java libraries but find the usual write/compile/test/re-compile cycle is too slow. Perhaps you’re writing a test suite for such a library and find writing the testing code a tedious task. Or maybe you’ve written a program that could use an extension language, and you don’t want to design and implement a whole new language for your application. 5 | 6 | Python is just the language for you. 7 | 8 | You could write a Unix shell script or Windows batch files for some of these tasks, but shell scripts are best at moving around files and changing text data, not well-suited for GUI applications or games. You could write a C/C++/Java program, but it can take a lot of development time to get even a first-draft program. Python is simpler to use, available on Windows, Mac OS X, and Unix operating systems, and will help you get the job done more quickly. 9 | 10 | Python is simple to use, but it is a real programming language, offering much more structure and support for large programs than shell scripts or batch files can offer. On the other hand, Python also offers much more error checking than C, and, being a very-high-level language, it has high-level data types built in, such as flexible arrays and dictionaries. Because of its more general data types Python is applicable to a much larger problem domain than Awk or even Perl, yet many things are at least as easy in Python as in those languages. 11 | 12 | Python allows you to split your program into modules that can be reused in other Python programs. It comes with a large collection of standard modules that you can use as the basis of your programs — or as examples to start learning to program in Python. Some of these modules provide things like file I/O, system calls, sockets, and even interfaces to graphical user interface toolkits like Tk. 13 | 14 | Python is an interpreted language, which can save you considerable time during program development because no compilation and linking is necessary. The interpreter can be used interactively, which makes it easy to experiment with features of the language, to write throw-away programs, or to test functions during bottom-up program development. It is also a handy desk calculator. 15 | 16 | Python enables programs to be written compactly and readably. Programs written in Python are typically much shorter than equivalent C, C++, or Java programs, for several reasons: 17 | 18 | the high-level data types allow you to express complex operations in a single statement; 19 | 20 | statement grouping is done by indentation instead of beginning and ending brackets; 21 | 22 | no variable or argument declarations are necessary. 23 | 24 | Python is extensible: if you know how to program in C it is easy to add a new built-in function or module to the interpreter, either to perform critical operations at maximum speed, or to link Python programs to libraries that may only be available in binary form (such as a vendor-specific graphics library). Once you are really hooked, you can link the Python interpreter into an application written in C and use it as an extension or command language for that application. 25 | 26 | By the way, the language is named after the BBC show “Monty Python’s Flying Circus” and has nothing to do with reptiles. Making references to Monty Python skits in documentation is not only allowed, it is encouraged! 27 | 28 | Now that you are all excited about Python, you’ll want to examine it in some more detail. Since the best way to learn a language is to use it, the tutorial invites you to play with the Python interpreter as you read. 29 | 30 | In the next chapter, the mechanics of using the interpreter are explained. This is rather mundane information, but essential for trying out the examples shown later. 31 | 32 | The rest of the tutorial introduces various features of the Python language and system through examples, beginning with simple expressions, statements and data types, through functions and modules, and finally touching upon advanced concepts like exceptions and user-defined classes. 33 | -------------------------------------------------------------------------------- /06_loop/range7_gen.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | def range_7_gen(start, end): 5 | """生成器版本的 Range7Iterator""" 6 | num = start 7 | while num < end: 8 | if num != 0 and (num % 7 == 0 or '7' in str(num)): 9 | yield num 10 | num += 1 11 | 12 | 13 | for i in range_7_gen(0, 20): 14 | print(i) -------------------------------------------------------------------------------- /06_loop/range7_iterator.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | names = ['foo', 'bar', 'foobar'] 5 | 6 | for name in names: 7 | print(name) 8 | 9 | iterator = iter(names) 10 | while True: 11 | try: 12 | name = next(iterator) 13 | print(name) 14 | except StopIteration: 15 | break 16 | 17 | 18 | class Range7: 19 | """生成一段范围内的可被 7 整除或包含 7 的整数 20 | 21 | :param start: 开始数字 22 | :param end: 结束数字 23 | """ 24 | 25 | def __init__(self, start, end): 26 | self.start = start 27 | self.end = end 28 | # 使用 current 保存当前所处的位置 29 | self.current = start 30 | 31 | def __iter__(self): 32 | return self 33 | 34 | def __next__(self): 35 | while True: 36 | # 当已经到达边界时,抛出异常终止迭代 37 | if self.current >= self.end: 38 | raise StopIteration 39 | 40 | if self.num_is_valid(self.current): 41 | ret = self.current 42 | self.current += 1 43 | return ret 44 | self.current += 1 45 | 46 | def num_is_valid(self, num): 47 | """判断数字是否满足要求""" 48 | if num == 0: 49 | return False 50 | return num % 7 == 0 or '7' in str(num) 51 | 52 | 53 | class _Range7: 54 | """生成一段范围内的可被 7 整除,或包含 7 的数字""" 55 | 56 | def __init__(self, start, end): 57 | self.start = start 58 | self.end = end 59 | 60 | def __iter__(self): 61 | # 返回一个新的迭代器对象 62 | return Range7Iterator(self) 63 | 64 | 65 | class Range7Iterator: 66 | def __init__(self, range_obj): 67 | self.range_obj = range_obj 68 | self.current = range_obj.start 69 | 70 | def __iter__(self): 71 | return self 72 | 73 | def __next__(self): 74 | while True: 75 | if self.current >= self.range_obj.end: 76 | raise StopIteration 77 | 78 | if self.num_is_valid(self.current): 79 | ret = self.current 80 | self.current += 1 81 | return ret 82 | self.current += 1 83 | 84 | def num_is_valid(self, num): 85 | if num == 0: 86 | return False 87 | return num % 7 == 0 or '7' in str(num) 88 | 89 | 90 | numbers = Range7(0, 20) 91 | # print(next(numbers)) 92 | # print(list(numbers)) 93 | # print(list(numbers)) 94 | 95 | for n in numbers: 96 | print(n) 97 | -------------------------------------------------------------------------------- /06_loop/reddit_titles.txt: -------------------------------------------------------------------------------- 1 | python-guide: Python best practices guidebook, written for humans. 2 | --- 3 | Python 2 Death Clock 4 | --- 5 | Run any Python Script with an Alexa Voice Command 6 | --- 7 | PyCon Latam 2019 8 | --- 9 | -------------------------------------------------------------------------------- /06_loop/small_file.txt: -------------------------------------------------------------------------------- 1 | feiowe9322nasd9233rl 2 | aoeijfiowejf8322kaf9a 3 | -------------------------------------------------------------------------------- /06_loop/yield_func.py: -------------------------------------------------------------------------------- 1 | def sum_even_only(numbers): 2 | """对 numbers 里面所有的偶数求和""" 3 | result = 0 4 | for num in numbers: 5 | if num % 2 == 0: 6 | result += num 7 | return result 8 | 9 | 10 | def even_only(numbers): 11 | for num in numbers: 12 | if num % 2 == 0: 13 | yield num 14 | 15 | 16 | def sum_even_only_v2(numbers): 17 | """对 numbers 里面所有的偶数求和""" 18 | result = 0 19 | for num in even_only(numbers): 20 | result += num 21 | return result 22 | 23 | # return sum(only_even(numbers)) 24 | 25 | 26 | numbers = [1, 2, 8, 9, 13, -1] 27 | print(sum_even_only(numbers)) 28 | print(sum_even_only_v2(numbers)) 29 | -------------------------------------------------------------------------------- /07_function/arguments_patterns.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | def append_value(value, items=[]): 5 | """往 items 列表中追加内容,并返回列表""" 6 | items.append(value) 7 | return items 8 | 9 | 10 | def append_value(value, items=None): 11 | # 在函数内部进行判断,保证参数默认每次都使用一个新的空列表 12 | if items is None: 13 | items = [] 14 | items.append(value) 15 | return items 16 | 17 | 18 | def query_users(limit, offset, *, min_followers_count, include_profile): 19 | """查询用户 20 | 21 | :param min_followers_count: 最小关注者数量 22 | :param include_profile: 结果包含用户详细档案 23 | """ 24 | ... 25 | 26 | 27 | query_users(20, 0, 100, True) 28 | query_users(limit=20, offset=0, min_followers_count=100, include_profile=True) 29 | -------------------------------------------------------------------------------- /07_function/closure_demo.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | def counter(): 4 | value = 0 5 | def _counter(): 6 | # nonlocal 用来标注变量来自上层作用域 7 | nonlocal value 8 | 9 | value += 1 10 | return value 11 | return _counter 12 | -------------------------------------------------------------------------------- /07_function/fib.py: -------------------------------------------------------------------------------- 1 | from functools import lru_cache 2 | 3 | 4 | @lru_cache 5 | def fib(n): 6 | if n < 2: 7 | return n 8 | return fib(n - 1) + fib(n - 2) 9 | 10 | 11 | def fib_loop(n): 12 | a, b = 0, 1 13 | for i in range(n): 14 | a, b = b, a + b 15 | return a 16 | 17 | 18 | for i in range(0, 10): 19 | print(fib(i)) 20 | 21 | for i in range(0, 10): 22 | print(fib_loop(i)) 23 | -------------------------------------------------------------------------------- /07_function/first_album.py: -------------------------------------------------------------------------------- 1 | """通过 iTunes API 搜索歌手发布过的第一张专辑""" 2 | import sys 3 | from json.decoder import JSONDecodeError 4 | 5 | import requests 6 | from requests.exceptions import HTTPError 7 | 8 | ITUNES_API_ENDPOINT = 'https://itunes.apple.com/search' 9 | 10 | 11 | def command_first_album(): 12 | """通过脚本输入查找并打印歌手第一张专辑信息""" 13 | if not len(sys.argv) == 2: 14 | print(f'usage: python {sys.argv[0]} {{SEARCH_TERM}}') 15 | sys.exit(1) 16 | 17 | term = sys.argv[1] 18 | resp = requests.get( 19 | ITUNES_API_ENDPOINT, 20 | { 21 | 'term': term, 22 | 'media': 'music', 23 | 'entity': 'album', 24 | 'attribute': 'artistTerm', 25 | 'limit': 200, 26 | }, 27 | ) 28 | try: 29 | resp.raise_for_status() 30 | except HTTPError as e: 31 | print(f'Error: failed to call iTunes API, {e}') 32 | sys.exit(2) 33 | try: 34 | albums = resp.json()['results'] 35 | except JSONDecodeError: 36 | print(f'Error: response is not valid JSON format') 37 | sys.exit(2) 38 | if not albums: 39 | print(f'Error: no albums found for artist "{term}"') 40 | sys.exit(1) 41 | 42 | sorted_albums = sorted(albums, key=lambda item: item['releaseDate']) 43 | first_album = sorted_albums[0] 44 | # 去除发布日期里的小时与分钟信息 45 | release_date = first_album['releaseDate'].split('T')[0] 46 | 47 | # 打印结果 48 | print(f"{term}'s first album: ") 49 | print(f" * Name: {first_album['collectionName']}") 50 | print(f" * Genre: {first_album['primaryGenreName']}") 51 | print(f" * Released at: {release_date}") 52 | 53 | 54 | if __name__ == '__main__': 55 | command_first_album() 56 | -------------------------------------------------------------------------------- /07_function/first_album_new.py: -------------------------------------------------------------------------------- 1 | """通过 iTunes API 搜索歌手发布过的第一张专辑""" 2 | import sys 3 | from json.decoder import JSONDecodeError 4 | 5 | import requests 6 | from requests.exceptions import HTTPError 7 | 8 | ITUNES_API_ENDPOINT = 'https://itunes.apple.com/search' 9 | 10 | 11 | class GetFirstAlbumError(Exception): 12 | """获取第一张专辑失败""" 13 | 14 | 15 | class QueryAlbumsError(Exception): 16 | """获取专辑列表失败""" 17 | 18 | 19 | def command_first_album(): 20 | """通过输入参数查找并打印歌手第一张专辑信息""" 21 | if not len(sys.argv) == 2: 22 | print(f'usage: python {sys.argv[0]} {{SEARCH_TERM}}') 23 | sys.exit(1) 24 | 25 | artist = sys.argv[1] 26 | try: 27 | album = get_first_album(artist) 28 | except GetFirstAlbumError as e: 29 | print(f"error: {e}", file=sys.stderr) 30 | sys.exit(2) 31 | 32 | print(f"{artist}'s first album: ") 33 | print(f" * Name: {album['name']}") 34 | print(f" * Genre: {album['genre_name']}") 35 | print(f" * Released at: {album['release_date']}") 36 | 37 | 38 | def get_first_album(artist): 39 | """根据专辑列表获取第一张专辑 40 | 41 | :param artist: 歌手名称 42 | :return: 第一张专辑 43 | :raises: 获取失败时抛出 GetFirstAlbumError 44 | """ 45 | try: 46 | albums = query_all_albums(artist) 47 | except QueryAlbumsError as e: 48 | raise GetFirstAlbumError(str(e)) 49 | 50 | sorted_albums = sorted(albums, key=lambda item: item['releaseDate']) 51 | first_album = sorted_albums[0] 52 | # 去除发布日期里的小时与分钟信息 53 | release_date = first_album['releaseDate'].split('T')[0] 54 | return { 55 | 'name': first_album['collectionName'], 56 | 'genre_name': first_album['primaryGenreName'], 57 | 'release_date': release_date, 58 | } 59 | 60 | 61 | def query_all_albums(artist): 62 | """根据歌手名称搜索所有专辑列表 63 | 64 | :param artist: 歌手名称 65 | :return: 专辑列表,List[Dict] 66 | :raises: 获取专辑失败时抛出 QueryAlbumsError 67 | """ 68 | resp = requests.get( 69 | ITUNES_API_ENDPOINT, 70 | { 71 | 'term': artist, 72 | 'media': 'music', 73 | 'entity': 'album', 74 | 'attribute': 'artistTerm', 75 | 'limit': 200, 76 | }, 77 | ) 78 | try: 79 | resp.raise_for_status() 80 | except HTTPError as e: 81 | raise QueryAlbumsError(f'failed to call iTunes API, {e}') 82 | try: 83 | albums = resp.json()['results'] 84 | except JSONDecodeError: 85 | raise QueryAlbumsError('response is not valid JSON format') 86 | if not albums: 87 | raise QueryAlbumsError(f'no albums found for artist "{artist}"') 88 | return albums 89 | 90 | 91 | if __name__ == '__main__': 92 | command_first_album() 93 | -------------------------------------------------------------------------------- /07_function/func_states.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import re 3 | 4 | 5 | def mosaic(s): 6 | """把输入字符串替换为等长的星号字符""" 7 | return '*' * len(s) 8 | 9 | 10 | import re 11 | 12 | 13 | def mosaic_string(s): 14 | """用 * 替换输入字符串里面所有的连续数字""" 15 | return re.sub(r'\d+', '*', s) 16 | 17 | 18 | def mosaic_matchobj(matchobj): 19 | """将匹配到的模式替换为等长星号字符串""" 20 | length = len(matchobj.group()) 21 | return '*' * length 22 | 23 | 24 | def mosaic_string(s): 25 | """用等长的 * 替换输入字符串里面所有的连续数字""" 26 | return re.sub(r'\d+', mosaic_matchobj, s) 27 | 28 | 29 | _mosaic_char_index = 0 30 | 31 | 32 | def mosaic_global_var(matchobj): 33 | """ 34 | 将匹配到的模式替换为其他字符,使用全局变量实现轮换字符效果 35 | """ 36 | global _mosaic_char_index 37 | mosaic_chars = ['*', 'x'] 38 | 39 | char = mosaic_chars[_mosaic_char_index] 40 | # 递增马赛克字符索引值 41 | _mosaic_char_index = (_mosaic_char_index + 1) % len(mosaic_chars) 42 | 43 | length = len(matchobj.group()) 44 | return char * length 45 | 46 | 47 | def make_cyclic_mosaic(): 48 | """ 49 | 将匹配到的模式替换为其他字符,使用闭包实现轮换字符效果 50 | """ 51 | char_index = 0 52 | mosaic_chars = ['*', 'x'] 53 | 54 | def _mosaic(matchobj): 55 | nonlocal char_index 56 | char = mosaic_chars[char_index] 57 | char_index = (char_index + 1) % len(mosaic_chars) 58 | 59 | length = len(matchobj.group()) 60 | return char * length 61 | 62 | return _mosaic 63 | 64 | 65 | class CyclicMosaic: 66 | """使用会轮换的屏蔽字符,基于类实现""" 67 | 68 | _chars = ['*', 'x'] 69 | 70 | def __init__(self): 71 | self._char_index = 0 72 | 73 | def generate(self, matchobj): 74 | char = self._chars[self._char_index] 75 | self._char_index = (self._char_index + 1) % len(self._chars) 76 | length = len(matchobj.group()) 77 | return char * length 78 | 79 | 80 | # print(re.sub(r'\d+', mosaic, 'reference 20, not group 2')) 81 | 82 | print(re.sub(r'\d+', mosaic_global_var, '商店共 100 个苹果,小明用 12 块的价格买走了 8 个')) 83 | print(re.sub(r'\d+', make_cyclic_mosaic(), '商店共 100 个苹果,小明用 12 块的价格买走了 8 个')) 84 | print(re.sub(r'\d+', CyclicMosaic().generate, '商店共 100 个苹果,小明用 12 块的价格买走了 8 个')) 85 | -------------------------------------------------------------------------------- /07_function/functools_demo.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from functools import lru_cache 3 | 4 | 5 | @lru_cache(maxsize=None) 6 | def calculate_score(class_id): 7 | print(f'Calculating score for class: {class_id}...') 8 | return 42 9 | 10 | 11 | calculate_score(100) 12 | calculate_score(100) 13 | -------------------------------------------------------------------------------- /07_function/intro.py: -------------------------------------------------------------------------------- 1 | # 定义函数 2 | def add(x, y): 3 | return x + y 4 | 5 | 6 | # 调用函数 7 | add(3, 4) 8 | 9 | add = lambda x, y: x + y 10 | 11 | add(3, 4) -------------------------------------------------------------------------------- /07_function/more_returns.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | def user_get_tweets(user): 5 | """获取用户已发布状态 6 | 7 | - 如配置“展示随机状态”,获取随机状态 8 | - 如配置“不展示任何状态”,返回空的占位符状态 9 | - 默认返回最新状态 10 | """ 11 | tweets = [] 12 | if user.profile.show_random_tweets: 13 | tweets.extend(get_random_tweets(user)) 14 | elif user.profile.hide_tweets: 15 | tweets.append(NULL_TWEET_PLACEHOLDER) 16 | else: 17 | # 最新状态需要用 token 从其他服务获取,并进行格式转换 18 | token = user.get_token() 19 | latest_tweets = get_latest_tweets(token) 20 | tweets.extend([transorm_tweet(item) for item in latest_tweets]) 21 | return tweets 22 | 23 | 24 | def user_get_tweets(user): 25 | """获取用户已发布状态""" 26 | if user.profile.show_random_tweets: 27 | return get_random_tweets(user) 28 | if user.profile.hide_tweets: 29 | return [NULL_TWEET_PLACEHOLDER] 30 | 31 | # 最新状态需要用 token 从其他服务获取,并进行格式转换 32 | token = user.get_token() 33 | latest_tweets = get_latest_tweets(token) 34 | return [transorm_tweet(item) for item in latest_tweets] -------------------------------------------------------------------------------- /07_function/none.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | def safe_close(fp): 5 | # 操作类函数,默认返回 None 6 | try: 7 | fp.close() 8 | except IOError: 9 | logger.warning('error closing file, ignore.') -------------------------------------------------------------------------------- /08_decorators/basic.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | 4 | def timer(print_args=False): 5 | """装饰器:打印函数耗时 6 | 7 | :param print_args: 是否打印被方法名和参数,默认为 False 8 | """ 9 | 10 | def decorator(func): 11 | def wrapper(*args, **kwargs): 12 | st = time.perf_counter() 13 | ret = func(*args, **kwargs) 14 | if print_args: 15 | print(f'"{func.__name__}", args: {args}, kwargs: {kwargs}') 16 | print('time cost: {} seconds'.format(time.perf_counter() - st)) 17 | return ret 18 | 19 | return wrapper 20 | 21 | return decorator -------------------------------------------------------------------------------- /08_decorators/class_as_deco.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import time 3 | from functools import update_wrapper 4 | 5 | 6 | class DelayedStart: 7 | """在执行被装饰函数前,等待 1 秒钟""" 8 | 9 | def __init__(self, func): 10 | update_wrapper(self, func) 11 | self.func = func 12 | 13 | def __call__(self, *args, **kwargs): 14 | print('Wait for 1 second before starting...') 15 | time.sleep(1) 16 | return self.func(*args, **kwargs) 17 | 18 | def eager_call(self, *args, **kwargs): 19 | """跳过等待,立刻执行被装饰函数""" 20 | print('Call without delay') 21 | return self.func(*args, **kwargs) 22 | 23 | 24 | @DelayedStart 25 | def hello(): 26 | print("Hello, World.") 27 | 28 | 29 | # hello() 30 | 31 | import functools 32 | 33 | 34 | class DelayedStart: 35 | """在执行被装饰函数前,等待一段时间 36 | 37 | :param func: 被装饰的函数 38 | :param duration: 需要等待的秒数 39 | """ 40 | 41 | def __init__(self, func, *, duration=1): 42 | update_wrapper(self, func) 43 | self.func = func 44 | self.duration = duration 45 | 46 | def __call__(self, *args, **kwargs): 47 | print(f'Wait for {self.duration} second before starting...') 48 | time.sleep(self.duration) 49 | return self.func(*args, **kwargs) 50 | 51 | def eager_call(self, *args, **kwargs): 52 | """跳过等待,立刻执行被装饰函数""" 53 | print('Call without delay') 54 | return self.func(*args, **kwargs) 55 | 56 | 57 | def delayed_start(**kwargs): 58 | """装饰器:推迟某个函数的执行。同时提供 .eager_call 方法立即执行""" 59 | return functools.partial(DelayedStart, **kwargs) 60 | 61 | 62 | @delayed_start(duration=2) 63 | def hello(): 64 | print("Hello, World.") 65 | 66 | 67 | hello() 68 | -------------------------------------------------------------------------------- /08_decorators/class_decorator.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | _validators = {} 4 | 5 | 6 | class ValidatorMeta(type): 7 | """元类:将所有校验器类统一注册起来,方便后续使用""" 8 | 9 | def __new__(cls, name, bases, attrs): 10 | ret = super().__new__(cls, name, bases, attrs) 11 | _validators[attrs['name']] = ret 12 | return ret 13 | 14 | 15 | class StringValidator(metaclass=ValidatorMeta): 16 | name = 'string' 17 | 18 | 19 | class IntegerValidator(metaclass=ValidatorMeta): 20 | name = 'int' 21 | 22 | 23 | print(_validators) 24 | 25 | _validators = {} 26 | 27 | 28 | def register(cls): 29 | """装饰器:将所有校验器类统一注册起来,方便后续使用""" 30 | _validators[cls.name] = cls 31 | return cls 32 | 33 | 34 | @register 35 | class StringValidator: 36 | name = 'string' 37 | 38 | 39 | @register 40 | class IntegerValidator: 41 | name = 'int' 42 | 43 | 44 | print(_validators) -------------------------------------------------------------------------------- /08_decorators/deco_pattern.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | class Numbers: 5 | """一个包含多个数字的简单类""" 6 | 7 | def __init__(self, numbers): 8 | self.numbers = numbers 9 | 10 | def get(self): 11 | return self.numbers 12 | 13 | 14 | class EvenOnlyDecorator: 15 | """装饰器类:过滤所有偶数""" 16 | 17 | def __init__(self, decorated): 18 | self.decorated = decorated 19 | 20 | def get(self): 21 | return [num for num in self.decorated.get() if num % 2 == 0] 22 | 23 | 24 | class GreaterThanDecorator: 25 | """装饰器类:过滤大于某个数""" 26 | 27 | def __init__(self, decorated, min_value): 28 | self.decorated = decorated 29 | self.min_value = min_value 30 | 31 | def get(self): 32 | return [num for num in self.decorated.get() if num > self.min_value] 33 | 34 | 35 | obj = Numbers([42, 12, 13, 17, 18, 41, 32]) 36 | even_obj = EvenOnlyDecorator(obj) 37 | gt_obj = GreaterThanDecorator(even_obj, min_value=30) 38 | print(gt_obj.get()) -------------------------------------------------------------------------------- /08_decorators/optional_arguments.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import time 3 | 4 | 5 | def delayed_start(func=None, *, duration=1): 6 | """装饰器:在执行被装饰函数前,等待一段时间 7 | 8 | :param duration: 需要等待的秒数 9 | """ 10 | 11 | def decorator(_func): 12 | def wrapper(*args, **kwargs): 13 | print(f'Wait for {duration} second before starting...') 14 | time.sleep(duration) 15 | return _func(*args, **kwargs) 16 | 17 | return wrapper 18 | 19 | if func is None: 20 | return decorator 21 | else: 22 | return decorator(func) 23 | 24 | 25 | @delayed_start 26 | def hello(): 27 | print('Hello, World!') 28 | 29 | 30 | hello() 31 | 32 | 33 | @delayed_start(duration=2) 34 | def hello(): 35 | print('Hello, World!') 36 | 37 | 38 | hello() 39 | 40 | 41 | @delayed_start() 42 | def hello(): 43 | print('Hello, World!') 44 | 45 | 46 | hello() -------------------------------------------------------------------------------- /08_decorators/wraps.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import time 4 | import random 5 | from functools import wraps 6 | 7 | 8 | def timer(func): 9 | """装饰器:记录并打印函数耗时""" 10 | 11 | @wraps(func) 12 | def decorated(*args, **kwargs): 13 | st = time.perf_counter() 14 | ret = func(*args, **kwargs) 15 | print('function took: {} seconds'.format(time.perf_counter() - st)) 16 | return ret 17 | 18 | return decorated 19 | 20 | 21 | def calls_counter(func): 22 | """装饰器:记录函数被调用了多少次 23 | 24 | 使用 `func.print_counter()` 可以打印统计到的信息 25 | """ 26 | counter = 0 27 | 28 | @wraps(func) 29 | def decorated(*args, **kwargs): 30 | nonlocal counter 31 | counter += 1 32 | return func(*args, **kwargs) 33 | 34 | def print_counter(): 35 | print(f'Counter: {counter}') 36 | 37 | decorated.print_counter = print_counter 38 | return decorated 39 | 40 | 41 | @calls_counter 42 | @timer 43 | def random_sleep(): 44 | """随机睡眠一小会""" 45 | time.sleep(random.random()) 46 | 47 | 48 | random_sleep() -------------------------------------------------------------------------------- /08_decorators/wrapt_exam.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | 4 | def provide_number(min_num, max_num): 5 | """ 6 | 装饰器:随机生成一个在 [min_num, max_num] 范围的整数, 7 | 并追加其为函数的第一个位置参数 8 | """ 9 | 10 | def wrapper(func): 11 | def decorated(*args, **kwargs): 12 | num = random.randint(min_num, max_num) 13 | # 将 num 作为第一个参数追加后调用函数 14 | return func(num, *args, **kwargs) 15 | 16 | return decorated 17 | 18 | return wrapper 19 | 20 | 21 | import wrapt 22 | 23 | 24 | def provide_number(min_num, max_num): 25 | @wrapt.decorator 26 | def wrapper(wrapped, instance, args, kwargs): 27 | # 参数含义: 28 | # 29 | # - wrapped:被装饰的函数或类方法 30 | # - instance: 31 | # - 如果被装饰者为普通类方法,该值为类实例 32 | # - 如果被装饰者为 classmethod 类方法,该值为类 33 | # - 如果被装饰者为类/函数/静态方法,该值为 None 34 | # 35 | # - args:调用时的位置参数(注意没有 * 符号) 36 | # - kwargs:调用时的关键字参数(注意没有 ** 符号) 37 | # 38 | num = random.randint(min_num, max_num) 39 | # 无需关注 wrapped 是类方法或普通函数,直接在头部追加参数 40 | args = (num,) + args 41 | return wrapped(*args, **kwargs) 42 | 43 | return wrapper 44 | 45 | 46 | @provide_number(1, 100) 47 | def print_random_number(num): 48 | print(num) 49 | 50 | 51 | # 输出 1-100 的随机整数 52 | # OUTPUT: 72 53 | print_random_number() 54 | -------------------------------------------------------------------------------- /09_oop/abc_validator.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | class Validator: 5 | """校验器基类,校验不同种类的数据是否符合要求""" 6 | 7 | def validate(self, value): 8 | raise NotImplementedError 9 | 10 | 11 | class NumberValidator(Validator): 12 | """校验输入值是否是合法数字""" 13 | 14 | def validate(self, value): 15 | ... 16 | 17 | 18 | from abc import ABC, abstractmethod 19 | 20 | 21 | class Validator(ABC): 22 | """校验器抽象类""" 23 | 24 | @classmethod 25 | def __subclasshook__(cls, C): 26 | """任何提供了 validate 方法的类,都被当做是 Validator 的子类""" 27 | if any("validate" in B.__dict__ for B in C.__mro__): 28 | return True 29 | return NotImplemented 30 | 31 | @abstractmethod 32 | def validate(self, value): 33 | raise NotImplementedError 34 | 35 | 36 | class StringValidator: 37 | def validate(self, value): 38 | ... 39 | 40 | 41 | class InvalidValidator(Validator): 42 | ... 43 | 44 | 45 | print(isinstance(StringValidator(), Validator)) -------------------------------------------------------------------------------- /09_oop/basic.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | class Person: 3 | def __init__(self, name, age): 4 | self.name = name 5 | self.age = age 6 | 7 | def __setattr__(self, name, value): 8 | # 不允许设置年龄小于 0 9 | if name == 'age' and value < 0: 10 | raise ValueError(f'Invalid age value: {value}') 11 | super().__setattr__(name, value) 12 | 13 | def say(self): 14 | print(f"Hi, My name is {self.name}, I'm {self.age}") 15 | -------------------------------------------------------------------------------- /09_oop/composition.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import re 3 | 4 | 5 | class UniqueVisitorAnalyzer: 6 | """统计某日的独立访客数 7 | 8 | :param date: 需要统计的日期 9 | """ 10 | 11 | def __init__(self, date): 12 | self.date = date 13 | 14 | def analyze(self): 15 | """通过解析与分析 API 访问日志,返回独立访客数量 16 | 17 | :return: 独立访客数 18 | """ 19 | for entry in self.get_log_entries(): 20 | ... # 省略:根据 entry.user_id 统计 UV 并返回结果 21 | 22 | def match_news_pattern(self, path): 23 | """判断 API 路径是不是在访问新闻 24 | 25 | :param path: API 访问路径 26 | :return: bool 27 | """ 28 | return re.match(r'^/news/[^/]*?/$', path) 29 | 30 | def get_log_entries(self): 31 | """获取当天所有日志记录""" 32 | for line in self.read_log_lines(): 33 | entry = self.parse_log(line) 34 | if not self.match_news_pattern(entry.path): 35 | continue 36 | yield entry 37 | 38 | def read_log_lines(self): 39 | """逐行获取访问日志""" 40 | ... # 省略:根据日志 self.date 读取日志文件并返回结果 41 | 42 | def parse_log(self, line): 43 | """将纯文本格式的日志解析为结构化对象 44 | 45 | :param line: 纯文本格式日志 46 | :return: 结构化的日志条目 LogEntry 对象 47 | """ 48 | ... # 省略:复杂的日志解析过程 49 | return LogEntry( 50 | time=..., 51 | ip=..., 52 | path=..., 53 | user_agent=..., 54 | user_id=..., 55 | ) 56 | 57 | 58 | import re 59 | 60 | 61 | class Top10CommentsAnalyzer(UniqueVisitorAnalyzer): 62 | """获取某日点赞量最高的 10 条评论 63 | 64 | :param date: 需要统计的日期 65 | """ 66 | 67 | limit = 10 68 | 69 | def analyze(self): 70 | """通过解析与统计 API 访问日志,返回点赞量最高的评论 71 | 72 | :return: 评论 ID 列表 73 | """ 74 | for entry in self.get_log_entries(): 75 | comment_id = self.extract_comment_id(entry.path) 76 | ... # 省略:统计过程与返回结果 77 | 78 | def extract_comment_id(self, path): 79 | """ 80 | 根据日志访问路径,获取评论 ID。 81 | 有效的评论点赞 API 路径格式:"/comments//up_votes/" 82 | 83 | :return: 仅当路径是评论点赞 API 时,返回 ID,否则返回 None 84 | """ 85 | matched_obj = re.match('/comments/(.*)/up_votes/', path) 86 | return matched_obj and matched_obj.group(1) 87 | -------------------------------------------------------------------------------- /09_oop/composition_v2.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import re 3 | 4 | 5 | class UniqueVisitorAnalyzer: 6 | """统计某日的独立访客数""" 7 | 8 | def __init__(self, date): 9 | self.date = date 10 | self.log_reader = LogReader(self.date) 11 | self.log_parser = LogParser() 12 | 13 | def analyze(self): 14 | """通过解析与分析 API 访问日志,返回独立访客数量 15 | 16 | :return: 独立访客数 17 | """ 18 | for entry in self.get_log_entries(): 19 | ... # 省略:根据 entry.user_id 统计 UV 并返回结果 20 | 21 | def get_log_entries(self): 22 | """获取当天所有日志记录""" 23 | for line in self.log_reader.read_lines(): 24 | entry = self.log_parser.parse(line) 25 | if not self.match_news_pattern(entry.path): 26 | continue 27 | yield entry 28 | 29 | def match_news_pattern(self, path): 30 | """判断 API 路径是不是在访问新闻 31 | 32 | :param path: API 访问路径 33 | :return: bool 34 | """ 35 | return re.match(r'^/news/[^/]*?/$', path) 36 | 37 | 38 | class LogReader: 39 | """根据日期读取特定日志文件""" 40 | 41 | def __init__(self, date): 42 | self.date = date 43 | 44 | def read_lines(self): 45 | """逐行获取访问日志""" 46 | ... # 省略:根据日志 self.date 读取日志文件并返回结果 47 | 48 | 49 | class LogParser: 50 | """将文本日志解析为结构化对象""" 51 | 52 | def parse(self, line): 53 | """将纯文本格式的日志解析为结构化对象 54 | 55 | :param line: 纯文本格式日志 56 | :return: 结构化的日志条目 LogEntry 对象 57 | """ 58 | ... # 省略:复杂的日志解析过程 59 | return LogEntry( 60 | time=..., 61 | ip=..., 62 | path=..., 63 | user_agent=..., 64 | user_id=..., 65 | ) 66 | 67 | 68 | import re 69 | 70 | 71 | class Top10CommentsAnalyzer(UniqueVisitorAnalyzer): 72 | """获取某日点赞量最高的 10 条评论""" 73 | 74 | limit = 10 75 | 76 | def __init__(self, date): 77 | self.log_reader = LogReader(self.date) 78 | self.log_parser = LogParser() 79 | 80 | def analyze(self): 81 | """通过解析与统计 API 访问日志,返回点赞量最高的评论 82 | 83 | :return: 评论 ID 列表 84 | """ 85 | for entry in self.get_log_entries(): 86 | comment_id = self.extract_comment_id(entry.path) 87 | ... # 省略:统计过程与返回结果 88 | 89 | def get_log_entries(self): 90 | """获取当天所有日志记录""" 91 | for line in self.log_reader.read_lines(): 92 | entry = self.log_parser.parse(line) 93 | yield entry 94 | 95 | def extract_comment_id(self, path): 96 | """ 97 | 根据日志访问路径,获取评论 ID。 98 | 有效的评论点赞 API 路径格式:"/comments//up_votes/" 99 | 100 | :return: 仅当路径是评论点赞 API 时,返回 ID,否则返回 None 101 | """ 102 | matched_obj = re.match('/comments/(.*)/up_votes/', path) 103 | return matched_obj and matched_obj.group(1) 104 | -------------------------------------------------------------------------------- /09_oop/decorators.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import random 4 | 5 | 6 | class Duck: 7 | def __init__(self, color): 8 | self.color = color 9 | 10 | def quack(self): 11 | # print(f"Hi, I'm a {self.color} duck!") 12 | pass 13 | 14 | @classmethod 15 | def create_random(cls): 16 | """创建一只随机颜色鸭子""" 17 | color = random.choice(['yellow', 'white', 'gray']) 18 | return cls(color=color) 19 | 20 | 21 | class Cat: 22 | def __init__(self, name): 23 | self.name = name 24 | 25 | def say(self): 26 | sound = self.get_sound() 27 | print(f'{self.name}: {sound}...') 28 | 29 | @staticmethod 30 | def get_sound(): 31 | repeats = random.randrange(1, 10) 32 | return ' '.join(['Meow'] * repeats) 33 | 34 | 35 | import os 36 | 37 | 38 | class FilePath: 39 | def __init__(self, path): 40 | self.path = path 41 | 42 | @property 43 | def basename(self): 44 | """获取文件名""" 45 | return self.path.rsplit(os.sep, 1)[-1] 46 | 47 | @basename.setter 48 | def basename(self, name): 49 | """修改当前路径里的文件名部分""" 50 | new_path = self.path.rsplit(os.sep, 1)[:-1] + [name] 51 | self.path = os.sep.join(new_path) 52 | 53 | @basename.deleter 54 | def basename(self): 55 | raise RuntimeError('Can not delete basename!') 56 | -------------------------------------------------------------------------------- /09_oop/duck_typing.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | class StringList: 5 | """用于保存多个字符串的数据类,实现了 read() 和可迭代接口""" 6 | 7 | def __init__(self, strings): 8 | self.strings = strings 9 | 10 | def read(self): 11 | return ''.join(self.strings) 12 | 13 | def __iter__(self): 14 | for s in self.strings: 15 | yield s 16 | 17 | 18 | def count_vowels(fp): 19 | """统计某个文件中,包含元音字母(aeiou)的数量""" 20 | if not hasattr(fp, 'read'): 21 | raise TypeError('must provide a valid file object') 22 | 23 | VOWELS_LETTERS = {'a', 'e', 'i', 'o', 'u'} 24 | count = 0 25 | for line in fp: 26 | for char in line: 27 | if char.lower() in VOWELS_LETTERS: 28 | count += 1 29 | return count 30 | 31 | 32 | from io import StringIO 33 | 34 | print(count_vowels(StringIO('Hello, world!'))) 35 | -------------------------------------------------------------------------------- /09_oop/func_oop.py: -------------------------------------------------------------------------------- 1 | class AppConfig: 2 | """程序配置类,使用单例模式""" 3 | 4 | _instance = None 5 | 6 | def __new__(cls): 7 | if cls._instance is None: 8 | inst = super().__new__(cls) 9 | # 已省略:从外部配置文件读取配置 10 | ... 11 | cls._instance = inst 12 | return cls._instance 13 | 14 | def get_database(self): 15 | """读取数据库配置""" 16 | ... 17 | 18 | def reload(self): 19 | """重新读取配置文件,刷新配置""" 20 | ... 21 | 22 | 23 | class AppConfig: 24 | """程序配置类,使用单例模式""" 25 | 26 | def __init__(self): 27 | # 已省略:从外部配置文件读取配置 28 | ... 29 | 30 | def get_database(self): 31 | """读取数据库配置""" 32 | ... 33 | 34 | def reload(self): 35 | """重新读取配置文件,刷新配置""" 36 | ... -------------------------------------------------------------------------------- /09_oop/iter_demo.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | class ThreeFactory: 5 | """在被迭代时不断生产 3 6 | 7 | :param repeat: 重复次数 8 | """ 9 | 10 | def __init__(self, repeat): 11 | self.repeat = repeat 12 | 13 | def __iter__(self): 14 | for _ in range(self.repeat): 15 | yield 3 -------------------------------------------------------------------------------- /09_oop/mixin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import sys 3 | 4 | 5 | class InfoDumperMixin: 6 | """Mixin:输出当前实例信息""" 7 | 8 | def dump_info(self): 9 | d = self.__dict__ 10 | print("Number of members: {}".format(len(d))) 11 | print("Details:") 12 | for key, value in d.items(): 13 | print(f' - {key}: {value}') 14 | 15 | 16 | class Person(InfoDumperMixin): 17 | def __init__(self, name, age): 18 | self.name = name 19 | self.age = age -------------------------------------------------------------------------------- /09_oop/mro.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | class A: 5 | def say(self): 6 | print("I'm A") 7 | 8 | 9 | class B(A): 10 | pass 11 | 12 | 13 | class C(A): 14 | def say(self): 15 | print("I'm C") 16 | 17 | 18 | class D(B, C): 19 | pass -------------------------------------------------------------------------------- /09_oop/polymorphism.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from enum import Enum, auto 3 | 4 | 5 | class OutputType(int, Enum): 6 | FILE = auto() 7 | REDIS = auto() 8 | ES = auto() 9 | 10 | 11 | class FancyLogger: 12 | """日志类:支持往文件、Redis、ES 等服务输出日志""" 13 | 14 | _redis_max_length = 1024 15 | 16 | def __init__(self, output_type=OutputType.FILE): 17 | self.output_type = output_type 18 | ... 19 | 20 | def log(self, message): 21 | """打印日志""" 22 | if self.output_type == OutputType.FILE: 23 | ... 24 | elif self.output_type == OutputType.REDIS: 25 | ... 26 | elif self.output_type == OutputType.ES: 27 | ... 28 | else: 29 | raise TypeError('output type invalid') 30 | 31 | def pre_process(self, message): 32 | """预处理日志""" 33 | # REDIS 对日志最大长度有限制,需要进行裁剪 34 | if self.output_type == OutputType.REDIS: 35 | return message[: self._redis_max_length] 36 | 37 | 38 | class FileWriter: 39 | def write(self, message): 40 | ... 41 | 42 | 43 | class RedisWriter: 44 | max_length = 1024 45 | 46 | def _pre_process(self, message): 47 | # REDIS 对日志最大长度有限制,需要进行裁剪 48 | return message[: self.max_length] 49 | 50 | def write(self, message): 51 | message = self._pre_process(message) 52 | ... 53 | 54 | 55 | class EsWriter: 56 | def write(self, message): 57 | ... 58 | 59 | 60 | class FancyLogger: 61 | """日志类:支持往文件、Redis、ES 等服务输出日志""" 62 | 63 | def __init__(self, output_writer=None): 64 | self._writer = output_writer or FileWriter() 65 | ... 66 | 67 | def log(self, message): 68 | self._writer.write(message) 69 | -------------------------------------------------------------------------------- /10_solid_p1/news_digester.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import sys 3 | from typing import Iterable, TextIO 4 | 5 | import requests 6 | from lxml import etree 7 | 8 | 9 | class Post: 10 | """HackerNew 上的条目 11 | 12 | :param title: 标题 13 | :param link: 链接 14 | :param points: 当前得分 15 | :param comments_cnt: 评论数 16 | """ 17 | 18 | def __init__(self, title: str, link: str, points: str, comments_cnt: str): 19 | self.title = title 20 | self.link = link 21 | self.points = int(points) 22 | self.comments_cnt = int(comments_cnt) 23 | 24 | 25 | class HNTopPostsSpider: 26 | """抓取 HackerNews Top 内容条目 27 | 28 | :param fp: 存储抓取结果的目标文件对象 29 | :param limit: 限制条目数,默认为 5 30 | """ 31 | 32 | items_url = 'https://news.ycombinator.com/' 33 | file_title = 'Top news on HN' 34 | 35 | def __init__(self, fp: TextIO, limit: int = 5): 36 | self.fp = fp 37 | self.limit = limit 38 | 39 | def write_to_file(self): 40 | """以纯文本格式将 Top 内容写入文件""" 41 | self.fp.write(f'# {self.file_title}\n\n') 42 | # enumerate 接收第二个参数,表示从这个数开始计数(默认为 0) 43 | for i, post in enumerate(self.fetch(), 1): 44 | self.fp.write(f'> TOP {i}: {post.title}\n') 45 | self.fp.write(f'> 分数:{post.points} 评论数:{post.comments_cnt}\n') 46 | self.fp.write(f'> 地址:{post.link}\n') 47 | self.fp.write('------\n') 48 | 49 | def fetch(self) -> Iterable[Post]: 50 | """从 HN 抓取 Top 内容 51 | 52 | :return: 可迭代的 Post 对象 53 | """ 54 | resp = requests.get(self.items_url) 55 | 56 | # 使用 XPath 可以方便的从页面解析出你需要的内容,以下均为页面解析代码 57 | # 如果你对 xpath 不熟悉,可以忽略这些代码,直接跳到 yield Post() 部分 58 | html = etree.HTML(resp.text) 59 | items = html.xpath('//table[@class="itemlist"]/tr[@class="athing"]') 60 | for item in items[: self.limit]: 61 | node_title = item.xpath('./td[@class="title"]/a')[0] 62 | node_detail = item.getnext() 63 | points_text = node_detail.xpath('.//span[@class="score"]/text()') 64 | comments_text = node_detail.xpath('.//td/a[last()]/text()')[0] 65 | 66 | yield Post( 67 | title=node_title.text, 68 | link=node_title.get('href'), 69 | # 条目可能会没有评分 70 | points=points_text[0].split()[0] if points_text else '0', 71 | comments_cnt=comments_text.split()[0], 72 | ) 73 | 74 | 75 | def main(): 76 | 77 | # with open('/tmp/hn_top5.txt') as fp: 78 | # crawler = HNTopPostsSpider(fp) 79 | # crawler.write_to_file() 80 | 81 | # 因为 HNTopPostsSpider 接收任何 file-like 的对象,所以我们可以把 sys.stdout 传进去 82 | # 实现往控制台标准输出打印的功能 83 | crawler = HNTopPostsSpider(sys.stdout) 84 | crawler.write_to_file() 85 | 86 | 87 | if __name__ == '__main__': 88 | main() 89 | -------------------------------------------------------------------------------- /10_solid_p1/news_digester_O1.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import io 3 | import sys 4 | from typing import Iterable, List, TextIO, Optional 5 | from urllib import parse 6 | 7 | import requests 8 | from lxml import etree 9 | 10 | 11 | class Post: 12 | """HN(https://news.ycombinator.com/) 上的条目 13 | 14 | :param title: 标题 15 | :param link: 链接 16 | :param points: 当前得分 17 | :param comments_cnt: 评论数 18 | """ 19 | 20 | def __init__(self, title: str, link: str, points: str, comments_cnt: str): 21 | self.title = title 22 | self.link = link 23 | self.points = int(points) 24 | self.comments_cnt = int(comments_cnt) 25 | 26 | 27 | class HNTopPostsSpider: 28 | """抓取 HackerNews Top 内容条目 29 | 30 | :param limit: 限制条目数,默认为 5 31 | """ 32 | 33 | items_url = 'https://news.ycombinator.com/' 34 | 35 | def __init__(self, limit: int = 5): 36 | self.limit = limit 37 | 38 | def fetch(self) -> Iterable[Post]: 39 | resp = requests.get(self.items_url) 40 | 41 | # 使用 XPath 可以方便的从页面解析出你需要的内容,以下均为页面解析代码 42 | # 如果你对 xpath 不熟悉,可以忽略这些代码,直接跳到 yield Post() 部分 43 | html = etree.HTML(resp.text) 44 | items = html.xpath('//table[@class="itemlist"]/tr[@class="athing"]') 45 | counter = 0 46 | for item in items: 47 | if counter >= self.limit: 48 | break 49 | 50 | node_title = item.xpath('./td[@class="title"]/a')[0] 51 | node_detail = item.getnext() 52 | points_text = node_detail.xpath('.//span[@class="score"]/text()') 53 | comments_text = node_detail.xpath('.//td/a[last()]/text()')[0] 54 | link = node_title.get('href') 55 | 56 | post = Post( 57 | title=node_title.text, 58 | link=link, 59 | # 条目可能会没有评分 60 | points=points_text[0].split()[0] if points_text else '0', 61 | comments_cnt=comments_text.split()[0] if comments_text.endswith('comments') else '0', 62 | ) 63 | # 使用测试方法来判断是否返回该帖子 64 | if self.interested_in_post(post): 65 | counter += 1 66 | yield post 67 | 68 | def interested_in_post(self, post: Post) -> bool: 69 | """判断是否应该将帖子加入结果中""" 70 | return True 71 | 72 | 73 | class GithubOnlyHNTopPostsSpider(HNTopPostsSpider): 74 | """只关心来自 Github 的内容""" 75 | 76 | def interested_in_post(self, post: Post) -> bool: 77 | parsed_link = parse.urlparse(post.link) 78 | return parsed_link.netloc == 'github.com' 79 | 80 | 81 | class GithubNBloomBergHNTopPostsSpider(HNTopPostsSpider): 82 | """只关心来自 Github/BloomBerg 的内容""" 83 | 84 | def interested_in_post(self, post: Post) -> bool: 85 | parsed_link = parse.urlparse(post.link) 86 | return parsed_link.netloc in ('github.com', 'bloomberg.com') 87 | 88 | 89 | def write_posts_to_file(posts: List[Post], fp: TextIO, title: str): 90 | """负责将帖子列表写入文件""" 91 | fp.write(f'# {title}\n\n') 92 | for i, post in enumerate(posts, 1): 93 | fp.write(f'> TOP {i}: {post.title}\n') 94 | fp.write(f'> 分数:{post.points} 评论数:{post.comments_cnt}\n') 95 | fp.write(f'> 地址:{post.link}\n') 96 | fp.write('------\n') 97 | 98 | 99 | def get_hn_top_posts(fp: Optional[TextIO] = None): 100 | """获取 HackerNews 的 Top 内容,并将其写入文件中 101 | 102 | :param fp: 需要写入的文件,如未提供,将往标准输出打印 103 | """ 104 | dest_fp = fp or sys.stdout 105 | # crawler = HNTopPostsSpider() 106 | # crawler = GithubOnlyHNTopPostsSpider() 107 | crawler = GithubNBloomBergHNTopPostsSpider() 108 | write_posts_to_file(list(crawler.fetch()), dest_fp, title='Top news on HN') 109 | 110 | 111 | def main(): 112 | get_hn_top_posts() 113 | 114 | 115 | if __name__ == '__main__': 116 | main() 117 | -------------------------------------------------------------------------------- /10_solid_p1/news_digester_O2.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import io 3 | import sys 4 | from typing import TextIO, Iterable, List, Optional 5 | from urllib import parse 6 | 7 | import requests 8 | from lxml import etree 9 | 10 | 11 | class Post: 12 | """HN(https://news.ycombinator.com/) 上的条目 13 | 14 | :param title: 标题 15 | :param link: 链接 16 | :param points: 当前得分 17 | :param comments_cnt: 评论数 18 | """ 19 | 20 | def __init__(self, title: str, link: str, points: str, comments_cnt: str): 21 | self.title = title 22 | self.link = link 23 | self.points = int(points) 24 | self.comments_cnt = int(comments_cnt) 25 | 26 | 27 | from abc import ABC, abstractmethod 28 | 29 | 30 | class PostFilter(ABC): 31 | """抽象类:定义如何过滤帖子结果""" 32 | 33 | @abstractmethod 34 | def validate(self, post: Post) -> bool: 35 | """判断帖子是否应该被保留""" 36 | 37 | 38 | class DefaultPostFilter(PostFilter): 39 | """保留所有帖子""" 40 | 41 | def validate(self, post: Post) -> bool: 42 | return True 43 | 44 | 45 | class GithubPostFilter(PostFilter): 46 | def validate(self, post: Post) -> bool: 47 | parsed_link = parse.urlparse(post.link) 48 | return parsed_link.netloc == 'github.com' 49 | 50 | 51 | class GithubNBloomPostFilter(PostFilter): 52 | def validate(self, post: Post) -> bool: 53 | parsed_link = parse.urlparse(post.link) 54 | return parsed_link.netloc in ('github.com', 'bloomberg.com') 55 | 56 | 57 | class HNTopPostsSpider: 58 | """抓取 HackerNews Top 内容条目 59 | 60 | :param limit: 限制条目数,默认为 5 61 | :param post_filter: 过滤结果条目的算法,默认为保留所有 62 | """ 63 | 64 | items_url = 'https://news.ycombinator.com/' 65 | 66 | def __init__(self, limit: int = 5, post_filter: Optional[PostFilter] = None): 67 | self.limit = limit 68 | self.post_filter = post_filter or DefaultPostFilter() 69 | 70 | def fetch(self) -> Iterable[Post]: 71 | resp = requests.get(self.items_url) 72 | 73 | # 使用 XPath 可以方便的从页面解析出你需要的内容,以下均为页面解析代码 74 | # 如果你对 xpath 不熟悉,可以忽略这些代码,直接跳到 yield Post() 部分 75 | html = etree.HTML(resp.text) 76 | items = html.xpath('//table[@class="itemlist"]/tr[@class="athing"]') 77 | counter = 0 78 | for item in items: 79 | if counter >= self.limit: 80 | break 81 | 82 | node_title = item.xpath('./td[@class="title"]/a')[0] 83 | node_detail = item.getnext() 84 | points_text = node_detail.xpath('.//span[@class="score"]/text()') 85 | comments_text = node_detail.xpath('.//td/a[last()]/text()')[0] 86 | link = node_title.get('href') 87 | 88 | post = Post( 89 | title=node_title.text, 90 | link=link, 91 | # 条目可能会没有评分 92 | points=points_text[0].split()[0] if points_text else '0', 93 | comments_cnt=comments_text.split()[0] if comments_text.endswith('comments') else '0', 94 | ) 95 | # 使用测试方法来判断是否返回该帖子 96 | if self.post_filter.validate(post): 97 | counter += 1 98 | yield post 99 | 100 | 101 | def write_posts_to_file(posts: List[Post], fp: TextIO, title: str): 102 | """负责将帖子列表写入文件""" 103 | fp.write(f'# {title}\n\n') 104 | for i, post in enumerate(posts, 1): 105 | fp.write(f'> TOP {i}: {post.title}\n') 106 | fp.write(f'> 分数:{post.points} 评论数:{post.comments_cnt}\n') 107 | fp.write(f'> 地址:{post.link}\n') 108 | fp.write('------\n') 109 | 110 | 111 | def get_hn_top_posts(fp: Optional[TextIO] = None): 112 | """获取 HackerNews 的 Top 内容,并将其写入文件中 113 | 114 | :param fp: 需要写入的文件,如未提供,将往标准输出打印 115 | """ 116 | dest_fp = fp or sys.stdout 117 | # crawler = HNTopPostsSpider() 118 | # crawler = HNTopPostsSpider(post_filter=GithubPostFilter()) 119 | crawler = HNTopPostsSpider(post_filter=GithubNBloomPostFilter()) 120 | write_posts_to_file(list(crawler.fetch()), dest_fp, title='Top news on HN') 121 | 122 | 123 | def main(): 124 | get_hn_top_posts() 125 | 126 | 127 | if __name__ == '__main__': 128 | main() 129 | -------------------------------------------------------------------------------- /10_solid_p1/news_digester_O3.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import io 3 | import sys 4 | from urllib import parse 5 | from typing import Iterable, TextIO, List, Optional 6 | 7 | import requests 8 | from lxml import etree 9 | 10 | 11 | class Post: 12 | """HN(https://news.ycombinator.com/) 上的条目 13 | 14 | :param title: 标题 15 | :param link: 链接 16 | :param points: 当前得分 17 | :param comments_cnt: 评论数 18 | """ 19 | 20 | def __init__(self, title: str, link: str, points: str, comments_cnt: str): 21 | self.title = title 22 | self.link = link 23 | self.points = int(points) 24 | self.comments_cnt = int(comments_cnt) 25 | 26 | 27 | class HNTopPostsSpider: 28 | """抓取 HackerNews Top 内容条目 29 | 30 | :param limit: 限制条目数,默认为 5 31 | :param filter_by_hosts: 过滤结果的站点列表,默认为 None,代表不过滤 32 | """ 33 | 34 | items_url = 'https://news.ycombinator.com/' 35 | 36 | def __init__(self, limit: int = 5, filter_by_hosts: Optional[List[str]] = None): 37 | self.limit = limit 38 | self.filter_by_hosts = filter_by_hosts 39 | 40 | def fetch(self) -> Iterable[Post]: 41 | resp = requests.get(self.items_url) 42 | 43 | # 使用 XPath 可以方便的从页面解析出你需要的内容,以下均为页面解析代码 44 | # 如果你对 xpath 不熟悉,可以忽略这些代码,直接跳到 yield Post() 部分 45 | html = etree.HTML(resp.text) 46 | items = html.xpath('//table[@class="itemlist"]/tr[@class="athing"]') 47 | counter = 0 48 | for item in items: 49 | if counter >= self.limit: 50 | break 51 | 52 | node_title = item.xpath('./td[@class="title"]/a')[0] 53 | node_detail = item.getnext() 54 | points_text = node_detail.xpath('.//span[@class="score"]/text()') 55 | comments_text = node_detail.xpath('.//td/a[last()]/text()')[0] 56 | link = node_title.get('href') 57 | 58 | post = Post( 59 | title=node_title.text, 60 | link=link, 61 | # 条目可能会没有评分 62 | points=points_text[0].split()[0] if points_text else '0', 63 | comments_cnt=comments_text.split()[0] if comments_text.endswith('comments') else '0', 64 | ) 65 | # 判断链接是否符合过滤条件 66 | if self._check_link_from_hosts(post.link): 67 | counter += 1 68 | yield post 69 | 70 | def _check_link_from_hosts(self, link: str) -> True: 71 | """检查某链接是否属于所定义的站点""" 72 | if self.filter_by_hosts is None: 73 | return True 74 | parsed_link = parse.urlparse(link) 75 | return parsed_link.netloc in self.filter_by_hosts 76 | 77 | 78 | def write_posts_to_file(posts: List[Post], fp: TextIO, title: str): 79 | """负责将帖子列表写入文件""" 80 | fp.write(f'# {title}\n\n') 81 | for i, post in enumerate(posts, 1): 82 | fp.write(f'> TOP {i}: {post.title}\n') 83 | fp.write(f'> 分数:{post.points} 评论数:{post.comments_cnt}\n') 84 | fp.write(f'> 地址:{post.link}\n') 85 | fp.write('------\n') 86 | 87 | 88 | def main(): 89 | # hosts = None 90 | hosts = ['github.com', 'bloomberg.com'] 91 | crawler = HNTopPostsSpider(filter_by_hosts=hosts) 92 | 93 | posts = list(crawler.fetch()) 94 | file_title = 'Top news on HN' 95 | write_posts_to_file(posts, sys.stdout, file_title) 96 | 97 | 98 | if __name__ == '__main__': 99 | main() 100 | -------------------------------------------------------------------------------- /10_solid_p1/news_digester_O_before.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import io 3 | import sys 4 | from typing import List, Iterable 5 | 6 | from urllib import parse 7 | import requests 8 | from lxml import etree 9 | 10 | 11 | class Post: 12 | """HN(https://news.ycombinator.com/) 上的条目 13 | 14 | :param title: 标题 15 | :param link: 链接 16 | :param points: 当前得分 17 | :param comments_cnt: 评论数 18 | """ 19 | 20 | def __init__(self, title: str, link: str, points: str, comments_cnt: str): 21 | self.title = title 22 | self.link = link 23 | self.points = int(points) 24 | try: 25 | self.comments_cnt = int(comments_cnt) 26 | except ValueError: 27 | self.comments_cnt = 0 28 | 29 | 30 | class HNTopPostsSpider: 31 | """抓取 HackerNews Top 内容条目 32 | 33 | :param limit: 限制条目数,默认为 5 34 | """ 35 | 36 | items_url = 'https://news.ycombinator.com/' 37 | 38 | def __init__(self, limit: int = 5): 39 | self.limit = limit 40 | 41 | def fetch(self) -> Iterable[Post]: 42 | """从 HN 抓取 Top 内容""" 43 | resp = requests.get(self.items_url) 44 | 45 | # 使用 XPath 可以方便的从页面解析出你需要的内容,以下均为页面解析代码 46 | # 如果你对 xpath 不熟悉,可以忽略这些代码,直接跳到 yield Post() 部分 47 | html = etree.HTML(resp.text) 48 | items = html.xpath('//table[@class="itemlist"]/tr[@class="athing"]') 49 | counter = 0 50 | for item in items: 51 | if counter >= self.limit: 52 | break 53 | 54 | node_title = item.xpath('./td[@class="title"]/a')[0] 55 | node_detail = item.getnext() 56 | points_text = node_detail.xpath('.//span[@class="score"]/text()') 57 | comments_text = node_detail.xpath('.//td/a[last()]/text()')[0] 58 | link = node_title.get('href') 59 | 60 | # 只关注来自 github.com 的内容 61 | parsed_link = parse.urlparse(link) 62 | if parsed_link.netloc == 'github.com': 63 | counter += 1 64 | yield Post( 65 | title=node_title.text, 66 | link=link, 67 | # 条目可能会没有评分 68 | points=points_text[0].split()[0] if points_text else '0', 69 | comments_cnt=comments_text.split()[0], 70 | ) 71 | 72 | 73 | def write_posts_to_file(posts: List[Post], fp: TextIO, title: str): 74 | """负责将帖子列表写入文件""" 75 | fp.write(f'# {title}\n\n') 76 | for i, post in enumerate(posts, 1): 77 | fp.write(f'> TOP {i}: {post.title}\n') 78 | fp.write(f'> 分数:{post.points} 评论数:{post.comments_cnt}\n') 79 | fp.write(f'> 地址:{post.link}\n') 80 | fp.write('------\n') 81 | 82 | 83 | def main(): 84 | crawler = HNTopPostsSpider() 85 | 86 | posts = list(crawler.fetch()) 87 | file_title = 'Top news on HN' 88 | write_posts_to_file(posts, sys.stdout, file_title) 89 | 90 | 91 | if __name__ == '__main__': 92 | main() 93 | -------------------------------------------------------------------------------- /10_solid_p1/news_digester_S1.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import sys 3 | from typing import List, Optional, TextIO 4 | 5 | import requests 6 | from lxml import etree 7 | 8 | 9 | class Post: 10 | """HackerNew 上的条目 11 | 12 | :param title: 标题 13 | :param link: 链接 14 | :param points: 当前得分 15 | :param comments_cnt: 评论数 16 | """ 17 | 18 | def __init__(self, title: str, link: str, points: str, comments_cnt: str): 19 | self.title = title 20 | self.link = link 21 | self.points = int(points) 22 | self.comments_cnt = int(comments_cnt) 23 | 24 | 25 | class HNTopPostsSpider: 26 | """抓取 HackerNews Top 内容条目 27 | 28 | :param limit: 限制条目数,默认为 5 29 | """ 30 | 31 | items_url = 'https://news.ycombinator.com/' 32 | 33 | def __init__(self, limit: int = 5): 34 | self.limit = limit 35 | 36 | def fetch(self) -> Iterable[Post]: 37 | resp = requests.get(self.items_url) 38 | 39 | # 使用 XPath 可以方便的从页面解析出你需要的内容,以下均为页面解析代码 40 | # 如果你对 xpath 不熟悉,可以忽略这些代码,直接跳到 yield Post() 部分 41 | html = etree.HTML(resp.text) 42 | items = html.xpath('//table[@class="itemlist"]/tr[@class="athing"]') 43 | for item in items[: self.limit]: 44 | node_title = item.xpath('./td[@class="title"]/a')[0] 45 | node_detail = item.getnext() 46 | points_text = node_detail.xpath('.//span[@class="score"]/text()') 47 | comments_text = node_detail.xpath('.//td/a[last()]/text()')[0] 48 | 49 | yield Post( 50 | title=node_title.text, 51 | link=node_title.get('href'), 52 | # 条目可能会没有评分 53 | points=points_text[0].split()[0] if points_text else '0', 54 | comments_cnt=comments_text.split()[0], 55 | ) 56 | 57 | 58 | class PostsWriter: 59 | """负责将帖子列表写入到文件""" 60 | 61 | def __init__(self, fp: TextIO, title: str): 62 | self.fp = fp 63 | self.title = title 64 | 65 | def write(self, posts: List[Post]): 66 | self.fp.write(f'# {self.title}\n\n') 67 | # enumerate 接收第二个参数,表示从这个数开始计数(默认为 0) 68 | for i, post in enumerate(posts, 1): 69 | self.fp.write(f'> TOP {i}: {post.title}\n') 70 | self.fp.write(f'> 分数:{post.points} 评论数:{post.comments_cnt}\n') 71 | self.fp.write(f'> 地址:{post.link}\n') 72 | self.fp.write('------\n') 73 | 74 | 75 | def get_hn_top_posts(fp: Optional[TextIO] = None): 76 | """获取 HackerNews 的 Top 内容,并将其写入文件中 77 | 78 | :param fp: 需要写入的文件,如未提供,将往标准输出打印 79 | """ 80 | dest_fp = fp or sys.stdout 81 | crawler = HNTopPostsSpider() 82 | writer = PostsWriter(dest_fp, title='Top news on HN') 83 | writer.write(list(crawler.fetch())) 84 | 85 | 86 | def main(): 87 | get_hn_top_posts() 88 | 89 | 90 | if __name__ == '__main__': 91 | main() 92 | -------------------------------------------------------------------------------- /10_solid_p1/news_digester_S2.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import sys 3 | from typing import List, Optional, TextIO 4 | 5 | import requests 6 | from lxml import etree 7 | 8 | 9 | class Post: 10 | """HackerNew 上的条目 11 | 12 | :param title: 标题 13 | :param link: 链接 14 | :param points: 当前得分 15 | :param comments_cnt: 评论数 16 | """ 17 | 18 | def __init__(self, title: str, link: str, points: str, comments_cnt: str): 19 | self.title = title 20 | self.link = link 21 | self.points = int(points) 22 | self.comments_cnt = int(comments_cnt) 23 | 24 | 25 | class HNTopPostsSpider: 26 | """抓取 HackerNews Top 内容条目 27 | 28 | :param limit: 限制条目数,默认为 5 29 | """ 30 | 31 | items_url = 'https://news.ycombinator.com/' 32 | 33 | def __init__(self, limit: int = 5): 34 | self.limit = limit 35 | 36 | def fetch(self) -> Iterable[Post]: 37 | resp = requests.get(self.items_url) 38 | 39 | # 使用 XPath 可以方便的从页面解析出你需要的内容,以下均为页面解析代码 40 | # 如果你对 xpath 不熟悉,可以忽略这些代码,直接跳到 yield Post() 部分 41 | html = etree.HTML(resp.text) 42 | items = html.xpath('//table[@class="itemlist"]/tr[@class="athing"]') 43 | for item in items[: self.limit]: 44 | node_title = item.xpath('./td[@class="title"]/a')[0] 45 | node_detail = item.getnext() 46 | points_text = node_detail.xpath('.//span[@class="score"]/text()') 47 | comments_text = node_detail.xpath('.//td/a[last()]/text()')[0] 48 | 49 | yield Post( 50 | title=node_title.text, 51 | link=node_title.get('href'), 52 | # 条目可能会没有评分 53 | points=points_text[0].split()[0] if points_text else '0', 54 | comments_cnt=comments_text.split()[0], 55 | ) 56 | 57 | 58 | def write_posts_to_file(posts: List[Post], fp: TextIO, title: str): 59 | """负责将帖子列表写入文件""" 60 | fp.write(f'# {title}\n\n') 61 | for i, post in enumerate(posts, 1): 62 | fp.write(f'> TOP {i}: {post.title}\n') 63 | fp.write(f'> 分数:{post.points} 评论数:{post.comments_cnt}\n') 64 | fp.write(f'> 地址:{post.link}\n') 65 | fp.write('------\n') 66 | 67 | 68 | def get_hn_top_posts(fp: Optional[TextIO] = None): 69 | """获取 HackerNews 的 Top 内容,并将其写入文件中 70 | 71 | :param fp: 需要写入的文件,如未提供,将往标准输出打印 72 | """ 73 | dest_fp = fp or sys.stdout 74 | crawler = HNTopPostsSpider() 75 | write_posts_to_file(list(crawler.fetch()), dest_fp, title='Top news on HN') 76 | 77 | 78 | def main(): 79 | get_hn_top_posts() 80 | 81 | 82 | if __name__ == '__main__': 83 | main() 84 | -------------------------------------------------------------------------------- /10_solid_p1/type_hints.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | 4 | class Duck: 5 | """鸭子类 6 | 7 | :param color: 鸭子颜色 8 | """ 9 | 10 | def __init__(self, color): 11 | self.color = color 12 | 13 | def quack(self): 14 | print(f"Hi, I'm a {self.color} duck!") 15 | 16 | 17 | def create_random_ducks(number): 18 | """创建一批随机颜色鸭子 19 | 20 | :param number: 需要创建的鸭子数量 21 | """ 22 | ducks = [] 23 | for _ in number: 24 | color = random.choice(['yellow', 'white', 'gray']) 25 | ducks.append(Duck(color=color)) 26 | return ducks 27 | 28 | 29 | # type hints 30 | from typing import List 31 | 32 | 33 | class Duck: 34 | def __init__(self, color: str): 35 | self.color = color 36 | 37 | def quack(self) -> None: 38 | print(f"Hi, I'm a {self.color} duck!") 39 | 40 | 41 | def create_random_ducks(number: int) -> List[Duck]: 42 | """创建一批随机颜色鸭子 43 | 44 | :param number: 需要创建的鸭子数量 45 | """ 46 | ducks: List[Duck] = [] 47 | for _ in number: 48 | color = random.choice(['yellow', 'white', 'gray']) 49 | ducks.append(Duck(color=color)) 50 | return ducks 51 | -------------------------------------------------------------------------------- /11_solid_p2/hn_site_grouper.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from lxml import etree 3 | from typing import Dict 4 | from collections import Counter 5 | 6 | 7 | class SiteSourceGrouper: 8 | """对 HackerNews 新闻来源站点进行分组统计 9 | 10 | :param url: HackerNews 首页地址 11 | """ 12 | 13 | def __init__(self, url: str): 14 | self.url = url 15 | 16 | def get_groups(self) -> Dict[str, int]: 17 | """获取 (域名, 个数) 分组""" 18 | resp = requests.get(self.url) 19 | html = etree.HTML(resp.text) 20 | # 通过 xpath 语法筛选新闻域名标签 21 | elems = html.xpath('//table[@class="itemlist"]//span[@class="sitestr"]') 22 | 23 | groups = Counter() 24 | for elem in elems: 25 | groups.update([elem.text]) 26 | return groups 27 | 28 | 29 | def main(): 30 | groups = SiteSourceGrouper("https://news.ycombinator.com/").get_groups() 31 | # 打印最常见的 3 个域名 32 | for key, value in groups.most_common(3): 33 | print(f'Site: {key} | Count: {value}') 34 | 35 | 36 | if __name__ == '__main__': 37 | main() 38 | -------------------------------------------------------------------------------- /11_solid_p2/hn_site_grouper_D1.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from lxml import etree 3 | from typing import Dict 4 | from collections import Counter 5 | from abc import ABC, abstractmethod 6 | from typing import Protocol 7 | 8 | 9 | class HNWebPage(ABC): 10 | """抽象类:Hacker New 站点页面""" 11 | 12 | @abstractmethod 13 | def get_text(self) -> str: 14 | raise NotImplementedError() 15 | 16 | 17 | class HNWebPage(Protocol): 18 | """协议:Hacker News 站点页面""" 19 | 20 | def get_text(self) -> str: 21 | ... 22 | 23 | 24 | class RemoteHNWebPage(HNWebPage): 25 | """远程页面,通过请求 HN 站点返回内容""" 26 | 27 | def __init__(self, url: str): 28 | self.url = url 29 | 30 | def get_text(self) -> str: 31 | resp = requests.get(self.url) 32 | return resp.text 33 | 34 | 35 | class LocalHNWebPage(HNWebPage): 36 | """本地页面,根据本地文件返回页面内容 37 | 38 | :param path: 本地文件路径 39 | """ 40 | 41 | def __init__(self, path: str): 42 | self.path = path 43 | 44 | def get_text(self) -> str: 45 | with open(self.path, 'r') as fp: 46 | return fp.read() 47 | 48 | 49 | class SiteSourceGrouper: 50 | """对 HN 页面的新闻来源站点进行分组统计""" 51 | 52 | def __init__(self, page: HNWebPage): 53 | self.page = page 54 | 55 | def get_groups(self) -> Dict[str, int]: 56 | """获取 (域名, 个数) 分组""" 57 | html = etree.HTML(self.page.get_text()) 58 | # 通过 xpath 语法筛选新闻域名标签 59 | elems = html.xpath('//table[@class="itemlist"]//span[@class="sitestr"]') 60 | 61 | groups = Counter() 62 | for elem in elems: 63 | groups.update([elem.text]) 64 | return groups 65 | 66 | 67 | def main(): 68 | # 实例化 page,传入 SiteSourceGrouper 69 | page = RemoteHNWebPage(url="https://news.ycombinator.com/") 70 | grouper = SiteSourceGrouper(page).get_groups() 71 | for key, value in grouper.most_common(3): 72 | print(f'Site: {key} | Count: {value}') 73 | 74 | page = LocalHNWebPage(path="./static_hn.html") 75 | grouper = SiteSourceGrouper(page).get_groups() 76 | for key, value in grouper.most_common(3): 77 | print(f'Site: {key} | Count: {value}') 78 | 79 | 80 | if __name__ == '__main__': 81 | main() 82 | -------------------------------------------------------------------------------- /11_solid_p2/hn_site_grouper_D2.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import datetime 3 | from lxml import etree 4 | from typing import Dict 5 | from collections import Counter 6 | from abc import abstractmethod, ABC 7 | 8 | 9 | class ContentOnlyHNWebPage(ABC): 10 | """抽象类:Hacker New 站点页面(仅提供内容)""" 11 | 12 | @abstractmethod 13 | def get_text(self) -> str: 14 | raise NotImplementedError() 15 | 16 | 17 | class HNWebPage(ABC): 18 | """抽象类:Hacker New 站点页面(含元数据)""" 19 | 20 | @abstractmethod 21 | def get_text(self) -> str: 22 | raise NotImplementedError() 23 | 24 | @abstractmethod 25 | def get_size(self) -> int: 26 | """获取页面大小""" 27 | raise NotImplementedError() 28 | 29 | @abstractmethod 30 | def get_generated_at(self) -> datetime.datetime: 31 | """获取页面生成时间""" 32 | raise NotImplementedError() 33 | 34 | 35 | class RemoteHNWebPage(HNWebPage): 36 | """远程页面,通过请求 HN 站点返回内容""" 37 | 38 | def __init__(self, url: str): 39 | self.url = url 40 | # 保存当前请求结果 41 | self._resp = None 42 | self._generated_at = None 43 | 44 | def get_text(self) -> str: 45 | """获取页面内容""" 46 | self._request_on_demand() 47 | return self._resp.text 48 | 49 | def get_size(self) -> int: 50 | """获取页面大小""" 51 | return len(self.get_text()) 52 | 53 | def get_generated_at(self) -> datetime.datetime: 54 | """获取页面生成时间""" 55 | self._request_on_demand() 56 | return self._generated_at 57 | 58 | def _request_on_demand(self): 59 | """请求远程地址,并避免重复""" 60 | if self._resp is None: 61 | self._resp = requests.get(self.url) 62 | self._generated_at = datetime.datetime.now() 63 | 64 | 65 | class LocalHNWebPage(HNWebPage): 66 | """本地页面,根据本地文件返回页面内容""" 67 | 68 | def __init__(self, path: str): 69 | self.path = path 70 | 71 | def get_text(self) -> str: 72 | with open(self.path, 'r') as fp: 73 | return fp.read() 74 | 75 | def get_size(self) -> int: 76 | return 0 77 | 78 | def get_generated_at(self) -> datetime.datetime: 79 | raise NotImplementedError("local web page can not provide generate_at info") 80 | 81 | 82 | class SiteSourceGrouper: 83 | """对 HN 页面的新闻来源站点进行分组统计""" 84 | 85 | def __init__(self, page: HNWebPage): 86 | self.page = page 87 | 88 | def get_groups(self) -> Dict[str, int]: 89 | """获取 (域名, 个数) 分组""" 90 | html = etree.HTML(self.page.get_text()) 91 | # 通过 xpath 语法筛选新闻域名标签 92 | elems = html.xpath('//table[@class="itemlist"]//span[@class="sitestr"]') 93 | 94 | groups = Counter() 95 | for elem in elems: 96 | groups.update([elem.text]) 97 | return groups 98 | 99 | 100 | class SiteAchiever: 101 | """将不同时间点的 HN 页面归档""" 102 | 103 | def save_page(self, page: HNWebPage): 104 | """将页面保存到后端数据库""" 105 | data = { 106 | "content": page.get_text(), 107 | "generated_at": page.get_generated_at(), 108 | "size": page.get_size(), 109 | } 110 | # 将 data 保存到数据库中 111 | # ... 112 | 113 | 114 | def main(): 115 | # 实例化 page,传入 SiteSourceGrouper 116 | page = RemoteHNWebPage(url="https://news.ycombinator.com/") 117 | grouper = SiteSourceGrouper(page).get_groups() 118 | for key, value in grouper.most_common(3): 119 | print(f'Site: {key} | Count: {value}') 120 | 121 | page = LocalHNWebPage(path="./static_hn.html") 122 | grouper = SiteSourceGrouper(page).get_groups() 123 | for key, value in grouper.most_common(3): 124 | print(f'Site: {key} | Count: {value}') 125 | 126 | 127 | if __name__ == '__main__': 128 | main() 129 | -------------------------------------------------------------------------------- /11_solid_p2/lsp_1.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from typing import Iterable, List 3 | import logging 4 | 5 | 6 | class GitRepository: 7 | """Git 仓库对象""" 8 | 9 | def __init__(self, repo_url: str): 10 | self.repo_url = repo_url 11 | 12 | def clone(self, local_dir: str): 13 | """将 Git 仓库内容 Clone 到本地目录 14 | 15 | :param local_dir: 本地目录 16 | """ 17 | ... 18 | 19 | def push(self): 20 | """将本地改动推送到远程地址""" 21 | ... 22 | 23 | 24 | class ReadOnlyGitRepository(GitRepository): 25 | """只读 Git 仓库对象""" 26 | 27 | def push(self): 28 | ... 29 | 30 | 31 | logger = logging.getLogger(__name__) 32 | 33 | 34 | class User(Model): 35 | """用户类,包含普通用户的相关操作""" 36 | 37 | ... 38 | 39 | def deactivate(self): 40 | """停用当前用户""" 41 | self.is_active = False 42 | self.save() 43 | 44 | 45 | class Admin(User): 46 | """管理员用户类""" 47 | 48 | ... 49 | 50 | def deactivate(self): 51 | # 管理员用户不允许被停用 52 | raise RuntimeError('admin can not be deactivated!') 53 | 54 | 55 | def deactivate_users(users: Iterable[User]): 56 | """批量停用多个用户 57 | 58 | :param users: 可迭代的用户对象 User 59 | """ 60 | for user in users: 61 | user.deactivate() 62 | 63 | 64 | def deactivate_users(users: Iterable[User]): 65 | """批量停用多个用户""" 66 | for user in users: 67 | # 管理员用户不支持 deactivate 方法,跳过 68 | if isinstance(user, Admin): 69 | logger.info(f'skip deactivating admin user {user.username}') 70 | continue 71 | 72 | user.deactivate() 73 | -------------------------------------------------------------------------------- /11_solid_p2/lsp_2.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from typing import Iterable, List 3 | import logging 4 | 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | 9 | class DeactivationNotSupported(Exception): 10 | """当用户不支持停用时抛出""" 11 | 12 | 13 | class User(Model): 14 | """普通用户模型类""" 15 | 16 | def __init__(self, username: str): 17 | self.username = username 18 | 19 | def allow_deactivate(self) -> bool: 20 | """否允许被停用""" 21 | return True 22 | 23 | def deactivate(self): 24 | """停用当前用户 25 | 26 | :raises: 当用户不支持被停用时,抛出 DeactivationNotSupported 异常 27 | """ 28 | self.is_active = True 29 | self.save() 30 | 31 | 32 | class Admin(User): 33 | """管理员用户类""" 34 | 35 | def deactivate(self): 36 | """停用当前用户 37 | 38 | :raises: 当用户不支持被停用时,抛出 DeactivationNotSupported 异常 39 | """ 40 | raise DeactivationNotSupported('admin can not be deactivated') 41 | 42 | def allow_deactivate(self) -> bool: 43 | # 管理员用户不允许被停用 44 | return False 45 | 46 | 47 | def deactivate_users(users: Iterable[User]): 48 | """批量停用多个用户""" 49 | for user in users: 50 | if not user.allow_deactivate(): 51 | logger.info( 52 | f'user {user.username} does not allow deactivating, skip.' 53 | ) 54 | continue 55 | 56 | user.deactivate() 57 | 58 | 59 | def deactivate_users(users: Iterable[User]): 60 | """批量停用多个用户""" 61 | for user in users: 62 | try: 63 | user.deactivate() 64 | except DeactivationNotSupported: 65 | logger.info( 66 | f'user {user.username} does not allow deactivating, skip.' 67 | ) 68 | -------------------------------------------------------------------------------- /11_solid_p2/lsp_3.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from typing import Iterable, List 3 | import logging 4 | 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | 9 | class User(Model): 10 | """普通用户类""" 11 | 12 | ... 13 | 14 | def list_related_posts(self) -> List[int]: 15 | """查询所有与之相关的帖子 ID""" 16 | return [ 17 | post.id 18 | for post in session.query(Post).filter(username=self.username) 19 | ] 20 | 21 | 22 | class Admin(User): 23 | """管理员用户类""" 24 | 25 | ... 26 | 27 | def list_related_posts(self) -> Iterable[int]: 28 | # 管理员与所有的帖子都有关,为了节约内存,使用生成器返回结果 29 | for post in session.query(Post).all(): 30 | yield post.id 31 | 32 | 33 | def list_user_post_titles(user: User) -> Iterable[str]: 34 | """获取与用户有关的所有帖子标题""" 35 | for post_id in user.list_related_posts(): 36 | yield session.query(Post).get(post_id).title 37 | 38 | 39 | def get_user_posts_count(user: User) -> int: 40 | """获取与用户相关的帖子个数""" 41 | return len(user.list_related_posts()) 42 | 43 | 44 | # EDITED 45 | 46 | 47 | class User(Model): 48 | """普通用户模型类""" 49 | 50 | def __init__(self, username: str): 51 | self.username = username 52 | 53 | def list_related_posts(self) -> Iterable[int]: 54 | """查询所有与之相关的帖子 ID""" 55 | for post in session.query(Post).filter(username=self.username): 56 | yield post.id 57 | 58 | def get_related_posts_count(self) -> int: 59 | """获取与用户有关的帖子总数""" 60 | value = 0 61 | for _ in self.list_related_posts(): 62 | value += 1 63 | return value 64 | 65 | 66 | class Admin(User): 67 | """管理员用户类""" 68 | 69 | def list_related_posts(self) -> Iterable[int]: 70 | # 管理员与所有的帖子都有关,为了节约内存,使用生成器返回 71 | for post in session.query(Post).all(): 72 | yield post.id 73 | 74 | 75 | # Method parameters 76 | 77 | 78 | class User(Model): 79 | def list_related_posts(self, include_hidden: bool = False) -> List[int]: 80 | # ... ... 81 | pass 82 | 83 | 84 | class Admin(User): 85 | def list_related_posts(self) -> List[int]: 86 | # ... ... 87 | pass 88 | -------------------------------------------------------------------------------- /11_solid_p2/lsp_rect_square.py: -------------------------------------------------------------------------------- 1 | class Rectangle: 2 | """长方形 3 | 4 | :param width: 宽度 5 | :param height: 高度 6 | """ 7 | 8 | def __init__(self, width: int, height: int): 9 | self._width = width 10 | self._height = height 11 | 12 | @property 13 | def width(self): 14 | return self._width 15 | 16 | @width.setter 17 | def width(self, value: int): 18 | self._width = value 19 | 20 | @property 21 | def height(self): 22 | return self._height 23 | 24 | @height.setter 25 | def height(self, value: int): 26 | self._height = value 27 | 28 | def get_area(self) -> int: 29 | """返回当前长方形的面积""" 30 | return self.width * self.height 31 | 32 | 33 | class Square(Rectangle): 34 | """正方形 35 | 36 | :param length: 边长 37 | """ 38 | 39 | def __init__(self, length: int): 40 | self._width = length 41 | self._height = length 42 | 43 | @property 44 | def width(self): 45 | return super().width 46 | 47 | @width.setter 48 | def width(self, value: int): 49 | self._width = value 50 | self._height = value 51 | 52 | @property 53 | def height(self): 54 | return super().height 55 | 56 | @height.setter 57 | def height(self, value: int): 58 | self._width = value 59 | self._height = value 60 | 61 | 62 | def test_rectangle_get_area(r: Rectangle): 63 | r.width = 3 64 | r.height = 5 65 | assert r.get_area() == 15 -------------------------------------------------------------------------------- /11_solid_p2/static_hn.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Hacker News
5 | 11 | 135 | 149 |
6 | 10 |
Hacker News 7 | new | past | comments | ask | show | jobs | submit 8 | login 9 |
12 | 13 | 15 | 16 | 17 | 19 | 20 | 21 | 23 | 24 | 25 | 27 | 28 | 29 | 31 | 32 | 33 | 35 | 36 | 37 | 39 | 40 | 41 | 43 | 44 | 45 | 47 | 48 | 49 | 51 | 52 | 53 | 55 | 56 | 57 | 59 | 60 | 61 | 63 | 64 | 65 | 67 | 68 | 69 | 71 | 72 | 73 | 75 | 76 | 77 | 79 | 80 | 81 | 83 | 84 | 85 | 87 | 88 | 89 | 91 | 92 | 93 | 95 | 96 | 97 | 99 | 100 | 101 | 103 | 104 | 105 | 107 | 108 | 109 | 111 | 112 | 113 | 115 | 116 | 117 | 119 | 120 | 121 | 123 | 124 | 125 | 127 | 128 | 129 | 131 | 132 | 133 |
1. Apple store workers should be paid for time waiting to be searched, court rules (latimes.com)
14 | 80 points by danso 1 hour ago | hide | 30 comments
2. I Add 3-25 Seconds of Latency to Every Site I Visit (howonlee.github.io)
18 | 949 points by curuinor 7 hours ago | hide | 261 comments
3. Lockdown: Open Source firewall that blocks app tracking, ads, snooping and more (lockdownhq.com)
22 | 77 points by tilt 3 hours ago | hide | 31 comments
4. The U.S. is charging Huawei with racketeering (techcrunch.com)
26 | 172 points by crivabene 5 hours ago | hide | 107 comments
5. GitHub Enterprise is now free through Microsoft for Startups (github.blog)
30 | 151 points by i_am_not_elon 5 hours ago | hide | 55 comments
6. Fake Travis Scott Song Created by Artificial Intelligence Sounds Almost Like Him (genius.com)
34 | 12 points by saadalem 56 minutes ago | hide | 11 comments
7. The outsize influence of middle-school friends (theatlantic.com)
38 | 169 points by rainhacker 7 hours ago | hide | 93 comments
8. Facebook quitters report more life satisfaction, less depression and anxiety (boingboing.net)
42 | 537 points by ericdanielski 5 hours ago | hide | 238 comments
9. Open Source Security Tools You Should Know (runpanther.io)
46 | 59 points by jacknagz 4 hours ago | hide | 8 comments
10. Show HN: Bob – A GUI for DNS Record Management and Name Auctions on Handshake (github.com)
50 | 38 points by sdtsui 3 hours ago | hide | 19 comments
11. How Big Technical Changes Happen at Slack (slack.engineering)
54 | 130 points by felixrieseberg 7 hours ago | hide | 78 comments
12. The Navy SEAL and His Doctor: An experimental brain treatment blows up two lives (inewsource.org)
58 | 21 points by tomohawk 2 hours ago | hide | 2 comments
13. The web as a GUI toolkit (arp242.net)
62 | 100 points by pcr910303 6 hours ago | hide | 75 comments
14. Usenet – Let's Return to Public Spaces (october.substack.com)
66 | 366 points by jsmoov 11 hours ago | hide | 324 comments
15. ING open-sources Lion, a library of accessible and flexible Web Components (medium.com)
70 | 134 points by d4kmor 8 hours ago | hide | 25 comments
16. PeerTube 2.1 (joinpeertube.org)
74 | 87 points by jrepinc 2 hours ago | hide | 12 comments
17. Show HN: I published my first website – ShellMagic.xyz (shellmagic.xyz)
78 | 139 points by manjana 8 hours ago | hide | 61 comments
18. Kessler Syndrome (wikipedia.org)
82 | 36 points by misthop 5 hours ago | hide | 22 comments
19. Steve Wozniak Interview (2007) (foundersatwork.com)
86 | 37 points by vo2maxer 5 hours ago | hide | discuss
20. CIA Sabotage Manual [pdf] (cia.gov)
90 | 8 points by Anon84 1 hour ago | hide | 1 comment
21. New math makes scientists more certain about quantum uncertainties (ieee.org)
94 | 77 points by magoghm 9 hours ago | hide | 12 comments
22. Nintendo Play Station Super NES CD-ROM Prototype (ha.com)
98 | 185 points by edent 13 hours ago | hide | 115 comments
23. Report to local authority in the UK if you see a kid using Tor, VMs, Linux etc. (twitter.com)
102 | 269 points by Santosh83 5 hours ago | hide | 108 comments
24. Burnoutindex.org (burnoutindex.org)
106 | 331 points by hernantz 6 hours ago | hide | 165 comments
25. Cambly (YC W14) is hiring. Solve a massıve problem for billions of people (lever.co)
110 | 2 hours ago | hide
26. Judge Halts Work on Microsoft’s JEDI Contract in Victory for Amazon (nytimes.com)
114 | 129 points by jbegley 4 hours ago | hide | 71 comments
27. What happened to Google's effort to scan university library books? (2017) (edsurge.com)
118 | 180 points by RyanShook 8 hours ago | hide | 35 comments
28. Taboo language turned the wolf into a monster (nautil.us)
122 | 8 points by dnetesn 1 hour ago | hide | 4 comments
29. Building data liberation infrastructure (beepb00p.xyz)
126 | 58 points by pcr910303 8 hours ago | hide | 1 comment
30. Time safety is more important than memory safety (halestrom.net)
130 | 17 points by panic 2 hours ago | hide | 21 comments
134 |

136 | Applications are open for YC Summer 2020 137 |

Guidelines 138 | | FAQ 139 | | Support 140 | | API 141 | | Security 142 | | Lists 143 | | Bookmarklet 144 | | Legal 145 | | Apply to YC 146 | | Contact

Search: 147 |
148 |
150 | -------------------------------------------------------------------------------- /11_solid_p2/test_hn_site_groups.py: -------------------------------------------------------------------------------- 1 | from hn_site_grouper import SiteSourceGrouper as SiteSourceGrouperO 2 | 3 | # from hn_site_grouper_D1 import SiteSourceGrouper, LocalHNWebPage 4 | from collections import Counter 5 | 6 | 7 | from unittest import mock 8 | 9 | 10 | # def test_grouper_returning_valid_type(): 11 | # """测试 get_groups 是否返回了正确类型""" 12 | # grouper = SiteSourceGrouperO('https://news.ycombinator.com/') 13 | # result = grouper.get_groups() 14 | # assert isinstance(result, Counter), "groups should be Counter instance" 15 | 16 | 17 | @mock.patch('hn_site_grouper.requests.get') 18 | def test_grouper_returning_valid_type(mocked_get): 19 | """测试 get_groups 是否返回了正确类型""" 20 | with open('static_hn.html', 'r') as fp: 21 | mocked_get.return_value.text = fp.read() 22 | 23 | grouper = SiteSourceGrouperO('https://news.ycombinator.com/') 24 | result = grouper.get_groups() 25 | assert isinstance(result, Counter), "groups should be Counter instance" 26 | 27 | 28 | def test_grouper_from_local(): 29 | page = LocalHNWebPage(path="./static_hn.html") 30 | grouper = SiteSourceGrouper(page) 31 | result = grouper.get_groups() 32 | assert isinstance(result, Counter), "groups should be Counter instance" 33 | -------------------------------------------------------------------------------- /12_data_model/com_op_override.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | class Square: 5 | """正方形 6 | 7 | :param length: 边长 8 | """ 9 | 10 | def __init__(self, length): 11 | self.length = length 12 | 13 | def area(self): 14 | return self.length ** 2 15 | 16 | def __eq__(self, other): 17 | # 在判断两对象是否相等时,先检验 other 是否同为当前类型 18 | if isinstance(other, self.__class__): 19 | return self.length == other.length 20 | return False 21 | 22 | def __ne__(self, other): 23 | # “不等”运算的结果一般会直接对“等于”取反 24 | return not (self == other) 25 | 26 | def __lt__(self, other): 27 | if isinstance(other, self.__class__): 28 | return self.length < other.length 29 | # 当对象不支持某种运算时,可以返回 NotImplemented 值 30 | return NotImplemented 31 | 32 | def __le__(self, other): 33 | return self.__lt__(other) or self.__eq__(other) 34 | 35 | def __gt__(self, other): 36 | if isinstance(other, self.__class__): 37 | return self.length > other.length 38 | return NotImplemented 39 | 40 | def __ge__(self, other): 41 | return self.__gt__(other) or self.__eq__(other) 42 | 43 | 44 | from functools import total_ordering 45 | 46 | 47 | @total_ordering 48 | class Square: 49 | """正方形 50 | 51 | :param length: 边长 52 | """ 53 | 54 | def __init__(self, length): 55 | self.length = length 56 | 57 | def area(self): 58 | return self.length ** 2 59 | 60 | def __eq__(self, other): 61 | if isinstance(other, self.__class__): 62 | return self.length == other.length 63 | return False 64 | 65 | def __lt__(self, other): 66 | if isinstance(other, self.__class__): 67 | return self.length < other.length 68 | return NotImplemented -------------------------------------------------------------------------------- /12_data_model/dangerous_hash.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | class HashByValue: 5 | """根据 value 属性计算哈希值""" 6 | 7 | def __init__(self, value): 8 | self.value = value 9 | 10 | def __hash__(self): 11 | return hash(self.value) -------------------------------------------------------------------------------- /12_data_model/data_non_data.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | class DuckWithProperty: 5 | @property 6 | def color(self): 7 | return 'gray' 8 | 9 | 10 | class DuckWithStaticMethod: 11 | @staticmethod 12 | def color(): 13 | return 'gray' -------------------------------------------------------------------------------- /12_data_model/del_demo.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | class Foo: 5 | def __del__(self): 6 | print(f'cleaning up {self}...') -------------------------------------------------------------------------------- /12_data_model/descriptor.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | class Person: 5 | """ 6 | 人 7 | 8 | :name: 姓名 9 | :age: 年龄 10 | """ 11 | 12 | def __init__(self, name, age): 13 | self.name = name 14 | self.age = age 15 | 16 | @property 17 | def age(self): 18 | return self._age 19 | 20 | @age.setter 21 | def age(self, value): 22 | """设置年龄,只允许 0-150 之间的数值""" 23 | try: 24 | value = int(value) 25 | except (TypeError, ValueError): 26 | raise ValueError('value is not a valid integer!') 27 | 28 | if not (0 < value < 150): 29 | raise ValueError('value must between 0 and 150!') 30 | self._age = value 31 | 32 | 33 | class IntegerField: 34 | """整型字段,只允许一定范围的整型值 35 | 36 | :param min_value: 允许的最小值 37 | :param max_value: 允许的最大值 38 | """ 39 | 40 | def __init__(self, min_value, max_value): 41 | self.min_value = min_value 42 | self.max_value = max_value 43 | 44 | def __get__(self, instance, owner=None): 45 | # 当不是通过实例访问时,直接返回描述符对象,这是最常见的做法 46 | if not instance: 47 | return self 48 | # 返回保存在实例字典里的值 49 | return instance.__dict__['_integer_field'] 50 | 51 | def __set__(self, instance, value): 52 | # 校验后将值保存在实例字典里 53 | value = self._validate_value(value) 54 | instance.__dict__['_integer_field'] = value 55 | 56 | def _validate_value(self, value): 57 | """校验值是否为符合要求的整数""" 58 | try: 59 | value = int(value) 60 | except (TypeError, ValueError): 61 | raise ValueError('value is not a valid integer!') 62 | 63 | if not (self.min_value <= value <= self.max_value): 64 | raise ValueError( 65 | f'value must between {self.min_value} and {self.max_value}!' 66 | ) 67 | return value 68 | 69 | 70 | class IntegerField: 71 | """整型字段,只允许一定范围的整型值 72 | 73 | :param min_value: 允许的最小值 74 | :param max_value: 允许的最大值 75 | """ 76 | 77 | def __init__(self, min_value, max_value): 78 | self.min_value = min_value 79 | self.max_value = max_value 80 | 81 | def __set_name__(self, owner, name): 82 | # 将绑定属性名保存在描述符对象中 83 | # 对于 age = IntegerField(...) 来说,此处的 name 就是 "age" 84 | # self._name = name 85 | pass 86 | 87 | def __get__(self, instance, owner=None): 88 | if not instance: 89 | return self 90 | # 在数据存取时,使用动态的 self._name 91 | return instance.width 92 | # return instance.__dict__[self._name] 93 | 94 | def __set__(self, instance, value): 95 | value = self._validate_value(value) 96 | instance.width = value 97 | # instance.__dict__[self._name] = value 98 | 99 | def _validate_value(self, value): 100 | """校验值是否为符合要求的整数""" 101 | try: 102 | value = int(value) 103 | except (TypeError, ValueError): 104 | raise ValueError(f'{self._name} is not a valid integer!') 105 | 106 | if not (self.min_value <= value <= self.max_value): 107 | raise ValueError( 108 | f'{self._name} must between {self.min_value} and {self.max_value}!' 109 | ) 110 | return value 111 | 112 | 113 | class Person: 114 | age = IntegerField(min_value=0, max_value=150) 115 | 116 | def __init__(self, name, age): 117 | self.name = name 118 | self.age = age 119 | 120 | 121 | class Rectangle: 122 | width = IntegerField(min_value=1, max_value=10) 123 | height = IntegerField(min_value=1, max_value=5) 124 | 125 | def __init__(self, width, height): 126 | self.width = width 127 | self.height = height 128 | -------------------------------------------------------------------------------- /12_data_model/dig_users.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | users_visited_puket = [ 4 | { 5 | "first_name": "Sirena", 6 | "last_name": "Gross", 7 | "phone_number": "650-568-0388", 8 | "date_visited": "2018-03-14", 9 | }, 10 | { 11 | "first_name": "James", 12 | "last_name": "Ashcraft", 13 | "phone_number": "412-334-4380", 14 | "date_visited": "2014-09-16", 15 | }, 16 | { 17 | "first_name": "Melissa", 18 | "last_name": "Dubois", 19 | "phone_number": "630-225-8829", 20 | "date_visited": "2019-01-04", 21 | }, 22 | { 23 | "first_name": "Albert", 24 | "last_name": "Potter", 25 | "phone_number": "702-249-3714", 26 | "date_visited": "2014-03-18", 27 | }, 28 | { 29 | "first_name": "Marcel", 30 | "last_name": "May", 31 | "phone_number": "315-794-3895", 32 | "date_visited": "2012-12-12", 33 | }, 34 | ] 35 | 36 | users_visited_nz = [ 37 | { 38 | "first_name": "Justin", 39 | "last_name": "Malcom", 40 | "phone_number": "267-282-1964", 41 | "date_visited": "2011-03-13", 42 | }, 43 | { 44 | "first_name": "Albert", 45 | "last_name": "Potter", 46 | "phone_number": "702-249-3714", 47 | "date_visited": "2013-09-11", 48 | }, 49 | { 50 | "first_name": "James", 51 | "last_name": "Ashcraft", 52 | "phone_number": "412-334-4380", 53 | "date_visited": "2009-04-18", 54 | }, 55 | { 56 | "first_name": "Marcel", 57 | "last_name": "May", 58 | "phone_number": "938-121-9321", 59 | "date_visited": "2016-07-12", 60 | }, 61 | { 62 | "first_name": "Barbara", 63 | "last_name": "Davis", 64 | "phone_number": "716-801-3922", 65 | "date_visited": "2018-03-12", 66 | }, 67 | ] 68 | 69 | 70 | def find_potential_customers_v1(): 71 | """找到去过普吉岛但是没去过新西兰的人 72 | 73 | :return: 通过 Generator 返回符合条件的旅客记录 74 | """ 75 | for puket_record in users_visited_puket: 76 | is_potential = True 77 | for nz_record in users_visited_nz: 78 | if ( 79 | puket_record['first_name'] == nz_record['first_name'] 80 | and puket_record['last_name'] == nz_record['last_name'] 81 | and puket_record['phone_number'] == nz_record['phone_number'] 82 | ): 83 | is_potential = False 84 | break 85 | 86 | if is_potential: 87 | yield puket_record 88 | 89 | 90 | for record in find_potential_customers_v1(): 91 | print(record['first_name'], record['last_name'], record['phone_number']) 92 | 93 | 94 | def find_potential_customers_v2(): 95 | """找到去过普吉岛但是没去过新西兰的人,性能改进版""" 96 | # 首先,遍历所有新西兰访问记录,创建查找索引 97 | nz_records_idx = { 98 | (rec['first_name'], rec['last_name'], rec['phone_number']) 99 | for rec in users_visited_nz 100 | } 101 | 102 | for rec in users_visited_puket: 103 | key = (rec['first_name'], rec['last_name'], rec['phone_number']) 104 | if key not in nz_records_idx: 105 | yield rec 106 | 107 | 108 | for record in find_potential_customers_v2(): 109 | print(record['first_name'], record['last_name'], record['phone_number']) 110 | 111 | 112 | class VisitRecord: 113 | """旅游记录 114 | 115 | :param first_name: 名 116 | :param last_name: 姓 117 | :param phone_number: 联系电话 118 | :param date_visited: 旅游时间 119 | 120 | - 当两条访问记录的名字与电话号相等时,判定二者相等。 121 | """ 122 | 123 | def __init__(self, first_name, last_name, phone_number, date_visited): 124 | self.first_name = first_name 125 | self.last_name = last_name 126 | self.phone_number = phone_number 127 | self.date_visited = date_visited 128 | 129 | def __hash__(self): 130 | return hash(self.comparable_fields) 131 | 132 | def __eq__(self, other): 133 | if isinstance(other, self.__class__): 134 | return self.comparable_fields == other.comparable_fields 135 | return False 136 | 137 | @property 138 | def comparable_fields(self): 139 | """获取用于对比对象的字段值""" 140 | return (self.first_name, self.last_name, self.phone_number) 141 | 142 | 143 | def find_potential_customers_v3(): 144 | # 转换为 VisitRecord 对象后,计算集合差值 145 | return set(VisitRecord(**r) for r in users_visited_puket) - set( 146 | VisitRecord(**r) for r in users_visited_nz 147 | ) 148 | 149 | 150 | for record in find_potential_customers_v3(): 151 | print(record.first_name, record.last_name, record.phone_number) 152 | 153 | 154 | from dataclasses import dataclass, field 155 | 156 | 157 | @dataclass(frozen=True) 158 | class VisitRecordDC: 159 | first_name: str 160 | last_name: str 161 | phone_number: str 162 | date_visited: str = field(compare=False) 163 | 164 | 165 | def find_potential_customers_v4(): 166 | return set(VisitRecordDC(**r) for r in users_visited_puket) - set( 167 | VisitRecordDC(**r) for r in users_visited_nz 168 | ) 169 | 170 | 171 | print('---') 172 | for record in find_potential_customers_v4(): 173 | print(record.first_name, record.last_name, record.phone_number) 174 | -------------------------------------------------------------------------------- /12_data_model/info_descriptor.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | class InfoDescriptor: 5 | """打印帮助信息的描述符""" 6 | 7 | def __get__(self, instance, owner=None): 8 | print(f'Calling __get__, instance: {instance}, owner: {owner}') 9 | if not instance: 10 | print('Calling without instance...') 11 | return self 12 | return 'informative descriptor' 13 | 14 | def __set__(self, instance, value): 15 | print(f'Calling __set__, instance: {instance}, value: {value}') 16 | 17 | def __delete__(self, instance): 18 | raise RuntimeError('Deletion not supported!') 19 | 20 | 21 | class Foo: 22 | bar = InfoDescriptor() 23 | -------------------------------------------------------------------------------- /12_data_model/obj_getitem.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | class Events: 5 | def __init__(self, events): 6 | self.events = events 7 | 8 | def is_empty(self): 9 | return not bool(self.events) 10 | 11 | def list_events_by_range(self, start, end): 12 | return self.events[start:end] 13 | 14 | def __len__(self): 15 | """自定义长度,将会被用来做布尔判断""" 16 | return len(self.events) 17 | 18 | def __getitem__(self, index): 19 | """自定义切片方法""" 20 | # 直接将 slice 切片对象透传给 events 处理 21 | return self.events[index] 22 | 23 | 24 | events = Events( 25 | [ 26 | 'computer started', 27 | 'os launched', 28 | 'docker started', 29 | 'os stopped', 30 | ] 31 | ) 32 | 33 | if not events.is_empty(): 34 | print(events.list_events_by_range(1, 3)) 35 | 36 | 37 | if events: 38 | print(events[1:3]) 39 | -------------------------------------------------------------------------------- /12_data_model/str_demo.py: -------------------------------------------------------------------------------- 1 | from typing import AsyncGenerator 2 | 3 | 4 | class Duck: 5 | """一只鸭子 6 | 7 | :param name: 鸭子的名字 8 | """ 9 | 10 | def __init__(self, name): 11 | self.name = name 12 | 13 | def __str__(self): 14 | return f'' 15 | 16 | 17 | class Person: 18 | """人 19 | 20 | :param name: 姓名 21 | :param age: 年龄 22 | :param favorite_color: 最喜欢的颜色 23 | """ 24 | 25 | def __init__(self, name, age, favorite_color): 26 | self.name = name 27 | self.age = age 28 | self.favorite_color = favorite_color 29 | 30 | def get_simple_display(self): 31 | return f'{self.name}({self.age})' 32 | 33 | def get_long_display(self): 34 | return f'{self.name} is {self.age} years old.' 35 | 36 | 37 | class Person: 38 | """人 39 | 40 | :param name: 姓名 41 | :param age: 年龄 42 | :param favorite_color: 最喜欢的颜色 43 | """ 44 | 45 | def __init__(self, name, age, favorite_color): 46 | self.name = name 47 | self.age = age 48 | self.favorite_color = favorite_color 49 | 50 | def __str__(self): 51 | return self.name 52 | 53 | def __repr__(self): 54 | return '{cls_name}(name={name!r}, age={age!r}, favorite_color={color!r})'.format( 55 | cls_name=self.__class__.__name__, 56 | name=self.name, 57 | age=self.age, 58 | color=self.favorite_color, 59 | ) 60 | 61 | def __format__(self, format_spec): 62 | """定义对象在字符串格式化时的行为 63 | 64 | :param format_spec: 需要的格式,默认为 '' 65 | """ 66 | if format_spec == 'verbose': 67 | return f'{self.name}({self.age})[{self.favorite_color}]' 68 | elif format_spec == 'simple': 69 | return f'{self.name}({self.age})' 70 | return self.name 71 | -------------------------------------------------------------------------------- /13_engineering/black_demo.py: -------------------------------------------------------------------------------- 1 | User.objects.create(name="piglei", gender="M", lang="Python", status="active") 2 | 3 | User.objects.create(name="piglei", gender="M", language="Python", status="active") 4 | 5 | User.objects.create(name="piglei", gender="M", language="Python", status="active") 6 | 7 | User.objects.create( 8 | name="piglei", gender="M", language="Python", status="active", points=100 9 | ) 10 | 11 | User.objects.create( 12 | name="piglei", 13 | gender="M", 14 | language="Python", 15 | status="active", 16 | points=100, 17 | location="Shenzhen", 18 | ) 19 | -------------------------------------------------------------------------------- /13_engineering/flake8_demo.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | class Duck: 3 | """鸭子类 4 | 5 | :param color: 鸭子颜色 6 | """ 7 | 8 | def __init__(self,color): 9 | self.color= color 10 | -------------------------------------------------------------------------------- /13_engineering/flake8_error.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | 4 | 5 | def find_number(input_string): 6 | """找到字符串里的第一个整数""" 7 | matched_obj = re.search(r'\d+', input_sstring) 8 | if matched_obj: 9 | return int(matched_obj.group()) 10 | return None 11 | -------------------------------------------------------------------------------- /13_engineering/isort_demo.py: -------------------------------------------------------------------------------- 1 | import os 2 | from urllib import parse 3 | 4 | import django 5 | import requests 6 | 7 | import myweb.models 8 | from myweb.views import menu 9 | -------------------------------------------------------------------------------- /13_engineering/string_utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | def string_upper(s: str) -> str: 3 | """将某个字符串里的所有英文字母,由小写转换为大写""" 4 | chars = [] 5 | for ch in s: 6 | if ch >= 'a' and ch <= 'z': 7 | # 32 是小写字母与大写字母在 ASCII 码表中相差的距离 8 | chars.append(chr(ord(ch) - 32)) 9 | else: 10 | chars.append(ch) 11 | return ''.join(chars) 12 | -------------------------------------------------------------------------------- /13_engineering/test_string_utils.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from string_utils import string_upper 3 | 4 | 5 | # def test_string_upper(): 6 | # assert string_upper('foo') == 'FOO' 7 | # 8 | # def test_string_empty(): <.> 9 | # assert string_upper('') == '' 10 | # 11 | # def test_string_mixed_cases(): 12 | # assert string_upper('foo BAR') == 'FOO BAR' 13 | # 14 | import pytest 15 | import string 16 | import random 17 | 18 | 19 | @pytest.fixture(scope='session') 20 | def random_token() -> str: 21 | """生成随机 token""" 22 | token_l = [] 23 | char_pool = string.ascii_lowercase + string.digits 24 | for _ in range(32): 25 | token_l.append(random.choice(char_pool)) 26 | return ''.join(token_l) 27 | 28 | 29 | @pytest.fixture 30 | def db_connection(): 31 | """创建并返回一个数据库连接""" 32 | conn = create_db_conn() 33 | yield conn 34 | conn.close() 35 | 36 | 37 | # @pytest.fixture(autouse=True) 38 | # def prepare_data(): 39 | # # 在测试开始前,创建两个用户 40 | # User.objects.create(...) 41 | # User.objects.create(...) 42 | # yield 43 | # # 在测试结束时,创建两个用户 44 | # User.objects.all().delete() 45 | 46 | 47 | @pytest.mark.parametrize( 48 | 's,expected', 49 | [ 50 | ('foo', 'FOO'), 51 | ('', ''), 52 | ('foo BAR', 'FOO BAR'), 53 | ], 54 | ) 55 | def test_string_upper(s, expected, random_token): 56 | print(random_token) 57 | assert string_upper(s) == expected 58 | 59 | 60 | def test_foo(random_token): 61 | print(random_token) 62 | -------------------------------------------------------------------------------- /13_engineering/test_upper.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import unittest 3 | 4 | 5 | class TestStringUpper(unittest.TestCase): 6 | def test_normal(self): 7 | self.assertTrue(1) 8 | self.assertEqual('foo'.upper(), 'FOO') 9 | self.assertGreaterEqual 10 | 11 | def test_normal2(self): 12 | self.assertEqual('foo'.upper(), 'FOO333') 13 | self.assertGreaterEqual 14 | 15 | 16 | if __name__ == '__main__': 17 | unittest.main() -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2023, piglei 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean 2 | clean: 3 | find . -name .mypy_cache -or -name .vscode -or -name __pycache__ -or -name .DS_Store | xargs rm -rf 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 《Python 工匠》源码合集 2 | 3 | 此处收录了《Python 工匠:案例、技巧与工程实践》一书中的所有 Python 源代码文件(不含部分过短的代码片段)。 4 | 5 | > 更多信息请前往图书主页(图灵社区):https://www.ituring.com.cn/book/3007 6 | 7 | ## 说明 8 | 9 | 每个子目录对应书中的一章,里面放着与本章内容相关的源码文件和数据文件(部分程序比如“日志分析脚本”需要)。目录名的前两位数字代表章数,比如 `07_function` 对应“第七章:函数”。 10 | 11 | 如何执行源码: 12 | 13 | - 安装 Python 3.8 版本(更高的版本也行,但部分行为可能有差异) 14 | - 执行源码文件:`python 01_variables/bubble_sort.py` 15 | - 查看执行结果 16 | 17 | ### 小技巧:使用 -i 选项来执行程序 18 | 19 | 分享一个方便调试的小技巧。执行源码时,你可以使用 `python -i {FILE_NAME}` 选项进入交互模式。比如,执行以下命令: 20 | 21 | ```python 22 | $ python -i 01_variables/bubble_sort.py 23 | [1, 3, 19, 23, 2, 4, 4, 20, 32] 24 | [1, 3, 19, 23, 2, 4, 4, 20, 32] 25 | 26 | # 程序不会马上退出,而是会进入交互模式 27 | # 你可以调用源码中的函数或类,做些实验 28 | >>> magic_bubble_sort([74, 28, 113, 3, 13]) 29 | [3, 13, 113, 28, 74] 30 | ``` 31 | 32 | ## 常见问题 33 | 34 | ### 为什么有的文件无法正常运行? 35 | 36 | 部分源码文件(比如 `01_variables/complex_variable.py`)在执行时,可能会抛出 `SyntaxError: 'return' outside function` 之类的异常。这是正常的,因为这些代码本身只是些零碎的小片段,仅用来描述某个概念或知识点,并非完整程序。 37 | 38 | ## 作者信息 39 | 40 | 作者:piglei \ 41 | 主页:https://www.piglei.com/ 42 | 43 | ## 修改记录 44 | 45 | - 2021.12.26:整理第一个版本 46 | --------------------------------------------------------------------------------