Chap1. 完成首个 MVP

  • 输入城市名,返回该城市的天气数据;
  • 输入指令,打印帮助文档(一般使用 h 或 help)
  • 输入指令,退出程序的交互(一般使用 quit 或 exit)
  • 在退出程序之前,打印查询过的所有城市

STEP.1 获得和读取数据

数据引用本地文档,这里的知识点是 git 操作,如前所述六个月前我的 git 操作也相对熟练了。

TODO: 这里可以有个小插曲,因为我在 pull AIMind/Py103 来更新自己的仓库前更改过根目录的 README.md,和维护者的 commits 有冲突,需要手动去解。涉及到 git 冲突的原因和解决、merge 和 rebase 两种工作习惯,commit 的手动整理。工具上的问题可能不建议过早了解,但如果在日常工作中学习其实是很折磨的,之后会详细写下。

在 Python 程序中获取文档内容,使用内置 function open() 返回 io.TextIOWrapper 文本流封装对象来操作

# built-in function open
open(file, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)

io.TextIOWrapper 继承自 io.TextIOBase,拥有方法

read(size)

读取并返回最多 size 个字符,size 没有指定则读到文件结尾

readline(size=-1)

读取一行,指定 size 参数可以限制读取的字数

io.TextIOBase 又继承自 io.IOBase ,拥有方法

readlines()

读取多行并返回一个 list,当迭代 (iterate) 一个 file 对象,readlines() 调用是可以省略的(有点奇怪)

for line open('file').readlines():
# 等价
for line open('file'):

STEP.2 最小步骤调试

好的学习者,输出的时间多于输入;据说好的程序员,写测试的时间也会多于写程序。现阶段我还不想引入单元测试,但是每一个最小步骤如果能尽早验证总好过程序运行错误了再顺着报错逐行检查。

Python 提供了 REPL 给开发者,就用它了

$ python3
Python 3.5.1 (default, Jan  4 2017, 10:24:37)
[GCC 4.2.1 Compatible Apple LLVM 8.0.0 (clang-800.0.42.1)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>>

可以在这里查文档、验证函数用法

>>> help(open)
# 输出 open 的文档,jk 上下行,ud 上下页,q 结束

>>> file = open('weather_info.txt')
<_io.TextIOWrapper name='weather_info.txt' mode='r' encoding='UTF-8'>
>>> file.readline()
'北京,晴\n'

>>> import io
>>> io.TextIOWrapper
<class '_io.TextIOWrapper'>

创建 main.py 文件,然后先用之前探索到的 open() 函数来实现最简单的 get_weather_raw() 看看吧

# main.py
def get_weather_raw(filename):
    return open(filename).rea()

在 REPL 里试试看

>>> import main
>>> main.get_weather_raw('weather_info.txt')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/gogu/learn-py/Py103/Chap1/project/main.py", line 17, in get_weather
    return open(filename).rea()
AttributeError: '_io.TextIOWrapper' object has no attribute 'rea'

报错,是因为 read 拼错了,到 main.py 去修改一下,再回 REPL。这个时候重新执行 main.get_weather_raw 函数,会发现函数是没有更新的,因为内存里的函数没有更新,这里有个办法可以不必重新开启 REPL 来解决

>>> main.get_weather_raw
<function get_weather_raw at 0x10b8c8378>
>>> from importlib import reload
>>> reload(main)
>>> main.get_weather_raw
<function get_weather_raw at 0x10ba062f0>

可以看到函数的内存地址更新了,再来验证一下

>>> main.get_weather_raw('weather_info.txt')
'北京,晴\n海淀,晴\n朝阳,晴\n顺义,晴\n怀柔,晴\n通州,晴\n昌平,晴\n延庆,晴\n丰台,晴\n石景山,晴\n大兴,晴\n房山,晴\n密云,晴\n门头沟,晴\n平谷,晴\n上海,小雨\n闵行,小雨\n宝山,小雨\n嘉定 ....

STEP3. 转换为字典

这个步骤也抽象成一个便于重用和测试的函数,先来想一下要做成什么样

get_weather_dict(filename)

这个函数被设计成输入文件路径字符串,返回一个形如 { '北京': '雾霾', '上海': '冻狗' } 的字典对象。

然后按这个图纸来做细节的实现

def get_weather_dict(filename):
    # 读取全文,以 \n 为界分割成列表,暂存
    _list = open(filename).read().split('\n')
    # 创建一个空字典对象
    _dict = {}
    # 迭代列表对象
    for pair in _list:
        # 排除空字符串
        if pair != '':
            # 字符串以 , 为界分割成列表,再解构赋值给 city 和 weather
            city, weather = pair.split(',')
            # 写入字典
            _dict[city] = weather
    # 返回字典对象
    return _dict

对应源码位置

没有用上面的 get_weather_raw, 因为该函数功能在这个程序里复用性不强,觉得没必要。不过若维持一个函数做一件事,把构造字典逻辑和读取文件逻辑分开也是很好的实践。

STEP4. 主体控制逻辑

这很像游戏里的 main loop,有了它这个世界的时间就开始流动了。不过我们做的更像 Rogue-like Game,你每行动一次,世界转动一次。

原理是使用 if(True) 创建一个无限循环,每当在循环中执行 input() 时,程序会等待用户输入,执行 break 语句则可终止循环。

if (True):
    command = input('>')
    if command == 'quit':
        break
    elif command in weather_dict:
        # ...

想了一下,把更耗时的判断放到后面了。

对应源码位置

STEP5. 依靠 __name__ 指定程序执行环境

查文档前初步试了一下

>>> __name__
'__main__'
>>> import main
>>> main.__name__
'main'

恩,貌似是一个属性,指向环境或者模块名?

搜了文档,会发现 __name__ 出现在三处

  1. Import-related module attributes 中的一个
  2. Sepcail attribute definition.__name__
  3. types.ModuleType.__name__

第三个貌似涉及动态类型和模块类型的内容,现在不想深入,先无视之。

Import-related module attributes 是在模块被 import 的时候自动填入对象的,其中 __name__ 是用于 import 系统中的唯一标示,如果没有被 import 缺省值就是 __main__

所以可以利用这个特性来指定某段程序执行的环境,比如

if __name__ == '__main__':
    startQuery()

就可以在 直接运行 python3 main.py 时执行 startQuery,而在 REPL 中 import 则不会执行,方便我手动测试模块中的函数。

而其实每个对象都有 __name__ attribute,应该都是被设计用于程序底层系统中做标示的吧。

STEP6. 生成自己的文档

在 REPL 中使用 help() 可以快速查看对象文档,其实不仅止于内建的对象,自己的模块也可以这样生成文档。每定义一个函数记得留下一个描述,这也是不错的实践吧。

def print_history(history = []):
    '''Print history records from the history list.'''
    # ...
>>> import main
>>> help(main)

Help on module main:

NAME
    main - # -*- coding: utf-8 -*-

FUNCTIONS
    get_weather_dict(filename)
        Get weather dict, convered from a text file. filename is a relative/absolute path string.

    hitCity(city, saveAs=[], searchIn={})
        Generate and print a city-weather record from the searchIn dict, and save in the saveAs list.

    print_history(history=[])
        Print history records from the history list.

    startQuery()
        Main loop function and print welcome info.

DATA
    FILENAME_HELP = 'help_info.txt'
    FILENAME_WEATHER = 'weather_info.txt'
    TEXT_DICT = {'city_weather_info': '%s 今日天气: %s', 'goodbye_info': '拜拜',...
    __warningregistry__ = {'version': 0, ("unclosed file <_io.TextIOWrappe...

FILE
    /Users/gogu/learn-py/Py103/Chap1/project/main.py

results matching ""

    No results matching ""