├── .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
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 |
--------------------------------------------------------------------------------