First GUI
完成一个 GUI 程序,运行在图形界面,实现以下功能:
- 输入城市名,获取对应的天气情况;
- 点击 Help,获取帮助信息;
- 点击 History,获取历史查询信息;
- 点击 Quit,退出程序。
Tk 环境爬坑
Mac 和 Python 联合挖了个坑,想也没想就跳进去了。
首先这是 Mac OSX 内置 Tk Toolkit 版本自己的 bug,不能用输入法写入中文字符了,也有记载此 bug 的文档。那按理说更新了就好了。
如果你和我一样只是下载好 8.5.18 版本安装完,就会发现完全不起作用。
排查原因吧!首先要看看新版本是不是真的安装成功了,在 tkinter library 里发现有 TkVersion
这个值,可惜只是 8.5 没有小版本号。这时了解到原来 python 自带的 IDLE 就是 Tk 写的,打开就有 warning。
>>> WARNING: The version of Tcl/Tk (8.5.9) in use may be unstable.
Visit http://www.python.org/download/mac/tcltk/ for current information.
可以确定至少 python 目前引用到的依然是老版本的 Tk。那么了解下 Tk 安装的目标路径吧。
搜索的结果是 Tk 会装在两个路径下
- /Library/Frameworks
- /System/Library/Frameworks
而 Python 的引用行为描述是这样的
In either case, the dynamically linking occurs when tkinter (Python 3) or Tkinter (Python 2) is first imported (specifically, the internal _tkinter C extension module). By default, the macOS dynamic linker looks first in /Library/Frameworks for Tcl and Tk frameworks with the proper major version. This is the standard location for third-party or built from source frameworks, including the ActiveTcl releases. If frameworks of the proper major version are not found there, the dynamic linker looks for the same version in /System/Library/Frameworks, the location for Apple-supplied frameworks shipped with macOS. (Note, you should normally not modify or delete files in /System/Library.)
我也搞不清楚这个 first imported 是什么时候 ... 总之暴力一点的解决方案就是替换 System/Library/Frameworks 路径下的相应文件,上文表示不推荐,我也不愿意,这有点让人想起曾经在 Windows 下试图补充单个 DLL 文件来满足驱动环境的挫折,而且现在版本的 OSX 的权限问题也很麻烦。
最终通过重新安装 Python 的方式来绕过这个问题。由于之前不希望考虑太多环境问题,Python3 也只是简单用 brew 来安装的,这次直接上 pyenv 吧,可以指定路径使用特定 Python 版本确实很方便。
Tk Widget 和 OOP
Tkinter Library 使用了 OOP 风格组织代码和暴露接口,这貌似是 Python 编程范式的主流。
比如 Tk
是一个 Class
>>> import tkinter
>>> tkinter.Tk
<class 'tkinter.Tk'>
使用 REPL 中使用 help
函数可以传入一个 Class 来查看其规格
>>> help(tkinter.Tk)
Help on class Tk in module tkinter:
class Tk(Misc, Wm)
| Toplevel widget of Tk which represents mostly the main window
| of an application. It has an associated Tcl interpreter.
|
| Method resolution order:
| Tk
| Misc
| Wm
| builtins.object
|
| Methods defined here:
...
如上信息,可以了解到 Tk 继承了 Misc 和 Wm,即除了自己的方法和属性外,也可让 Tk 生成的对象拥有 Misc 和 Wm 对象的方法和属性。
我们选择 Text 部件(Widget)来显示命令输出的日志,Text 的继承信息是这样的:
class Text(Widget, XView, YView)
| Text widget which can display text in various forms.
|
| Method resolution order:
| Text
| Widget
| BaseWidget
| Misc
| Pack
| Place
| Grid
| XView
| YView
| builtins.object
我们希望 Text 对用户来说是只读的,不允许显示光标并随意书写内容,可以通过切换 text.option(state=DISABLED)
来实现。输出日志函数的代码如下:
def print_log(textarea, text):
textarea.config(state=NORMAL)
textarea.insert(END, '%s\n' % text)
textarea.config(state=DISABLED)
传入参数 textarea 要求是一个 Text 实例,是这样生成的
t = Text()
在生成的实例 t 上我们可以调用 config 函数来改变 t 的状态,config 函数是从哪来的呢?
>>> t = Text()
>>> t.config
<bound method Misc.configure of <tkinter.Text object .!text>>
config 函数其实是在 Text 父类 Misc 上实现的,继承关系是 Misc -> BaseWidget -> Widget -> Text。这样做的好处是 Misc 的子类都会获得 config 来改变状态的能力(甚至连状态本身可能都是 Misc 所给予的),这就是类继承的能力。
为了验证,特意去看了 tkinter 的源码(这种源码都好长 ...)
https://github.com/python-git/python/blob/master/Lib/lib-tk/Tkinter.py#L1177-L1203
其实没有指望看懂,不过有几个奇怪的问题,比如 _configure
函数备注指明是 internal function,却没有用 __
来把它写成一个真正的私有方法。实际上,继承 Misc 的类也会使用这个函数,大概是希望设计成只对调用类库的外部使用者屏蔽吧。
另外 Python 的私有方法也不是真私有啊,对外暴露的私有方法加了 _A
前缀,但并不完全阻止用户访问。这也是 Python 语言设计中为了其使用灵活性所设计的吧。
>>> class A:
... def __haha():
... print('a')
...
>>> a = A()
>>> a.__haha
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'A' object has no attribute '__haha'
>>> dir(a)
['_A__haha', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']
>>> a._A__haha
<bound method A.__haha of <__main__.A object at 0x10f32ab00>>
想这些早了点,还是完成功能为主。
「最小步骤调试」 for GUI
另外和 w1 时一样我们还是可以用「最小步骤调试」来快速验证函数是不是满足要求
>>> from tkinter import *
>>> def print_log(textarea, text):
... textarea.config(state=NORMAL)
... textarea.insert(END, '%s\n' % text)
... textarea.config(state=DISABLED)
...
我们需要先准备好参数 textarea
,Disabled 状态的 Text 部件
>>> t = Text()
>>> t.config(state=DISABLED)
>>> t.pack()
此时显示一个空白界面,鼠标在上面显示成编辑文本的图标,但是点击没反应,也不能编辑
>>> print_log(t, 'Don\'t Panic.')
就会看到界面上出现了这些字。
module 逻辑复用
鉴于 w1 的程序维持了最小步骤拆分,逻辑很容易复用,简单修改一下就可以拿来用了。
为了真正的不用复制粘贴使用 w1 的源码,这里(为了偷懒)暂时选择了 linux 软链接方式。
$ ln -s ../../Chap1/project/main.py weather.py
这样我在修改 w1 的 main.py 文件时,w2 引用到的就总是最新了。
不用硬链接是因为 git 不能保存硬链接的状态,别人 clone 仓库时会变成两个不相干的文件。
Python 的模块似乎并不建议从相对路径引入的方式,虽然我觉得那样很灵活。
为什么这个程序没有写成 OOP 风格
OOP 是如今大多数设计模式和软件架构的基础范式,无论如何也是尽快需要掌握的(不然大多源码都看不懂),不过反向的声音其实也不少。
OOP 对实现程序自身作用是集中管理复杂的变量,使之变成一个个具体可扩展的实例的属性,而这些属性和方法互相牵连,和现实世界的关系一样变得不够简单透明了。这是我认为 OOP 亲直觉,反思维和容易令程序变得复杂难懂的原因。(啊,也或许是现阶段我还没掌握到精髓吧,之后思路说不定会变)
w2 的程序没有写成 OOP 最主要的原因是,OOP 把状态和方法绑在一起,就不容易执行「最小步骤调试」了。以后碰到更需要面向对象的场景再来考虑吧。
不过 Python 的 class 还可以做更多有趣的事情,比如可以重载运算符,下面的例子把减法变成了加法
>>> class Number:
... def __init__(self, start):
... self.data = start
... def __sub__(self, other):
... return self.data + other
...
>>> n = Number(20)
>>> n - 10
30
感觉很危险,不过用法得当的话会写出风格不错的代码吧(?)。