怎樣才算學會Python?
Python inside the door
Python 實踐基礎(chǔ)
起源
假如你已經(jīng)有了編程基礎(chǔ),那么學習一門新語言的困難點絕對不在語法、語義和風格等代碼層面上的,而在于語言范式(OO,F(xiàn)P還是Logic),語言的生態(tài)(如:依賴管理和包發(fā)布等)和工具(編輯器,編譯器或者解釋器)這些方面,請參看如何高效地學習編程語言。再假如你已經(jīng)對各類語言范式都有一定的了解,那么***的困難之處就是…細節(jié),它是魔鬼。
我相信真正擁抱一門新語言,花在工具和語言生態(tài)上的時間一定很多。龐大的社區(qū)利用群體智慧構(gòu)筑的生態(tài)圈充滿了各種零碎的知識點,這些知識點可能是前人趟過的陷阱(Common Gotchas),基于局部共識經(jīng)由經(jīng)典項目實踐過之后的約定(Convention)和慣用法(Idioms),也可能是總結(jié)出的概念模式(Pattern),甚至是審美(Aesthetic)和禪(Zen)或道(Dao)。這些知識點作用到了工具和語言生態(tài)之中,意味著你需要使用合適工具、遵循生態(tài)的玩法才能從中受益。
工具
工欲善其事必先利其器,對于程序員而言,這個器是編輯器…嗎?Emacs, Vim, VS Code or PyCharm?
解釋器
當然不是,這個器應當是讓你能立馬運行程序并立刻看到結(jié)果的工具,在Python的上下文中,它是Python的解釋器。一般情況下,我們會選擇***版的解釋器或者編譯器,但是Python有一點點例外,因為Python3和2并不兼容,那么該選擇哪個版本呢?尋找這類問題的答案其實就是融入Python社區(qū)的過程。幸運的是,社區(qū)出版了一本書 The Hitchhiker’s Guide to Python,里面誠懇地給出了建議。所以不出意外,Python3是比較合適的選擇。
因為Python安裝起來很簡單,我們跳過…吧?不過,大俠留步,你可知道Python其實只是一個語言標準,它的實現(xiàn)程序不止一個,其中官方的實現(xiàn)是CPython,還有Jython和IronPython等。不過,CPython作為使用最為廣泛的解釋器當然是開發(fā)之***。
- $ python3
- Python 3.6.5 (default, Jun 17 2018, 12:13:06)
- [GCC 4.2.1 Compatible Apple LLVM 9.1.0 (clang-902.0.39.2)] on darwin
- Type "help", "copyright", "credits" or "license" for more information.
- >>> print("hello world")
- hello world
編輯器
雖然面向REPL編程(Repl-Oriented Programming)是一種比單元測試的反饋速度更快的編程方式,但是在REPL中編寫應用程序并不合適,不合適的地方表現(xiàn)在代碼不易組織(分模塊)和代碼沒法記錄(存盤)。所以我們需要可以編輯的源代碼、目錄和其它相關(guān)文件,這個時候就需要挑選趁手的編輯器。
神之編輯器Emacs中內(nèi)置了python-mode,如果已經(jīng)是Emacs用戶,這款編輯器當是寫Python的不二之選。編輯器之神的Vim排第二,如果你比較喜歡折騰Vim8.0的插件,或者想自己構(gòu)建NeoVim的話。其它的編輯器,我不知道,不想用。不過PyCharm是Jetbrains家的IDE,靠譜。
有功夫在Terminal中裝一個emacsclient,然后下載一個oh-my-zsh的插件emacsclient,就可以很愉悅地在Terminal中使用Emacs編輯文件了。
- $ te hello_world.py # te: aliased to /Users/qianyan/.oh-my-zsh/plugins/emacs/emacsclient.sh -nw
- """
- hello_world.py
- Ctrl+x+c 退出emacs :)
- """
- print("hello world")
- $ python3 hello_world.py
- hello world
- $ python3 -m hello_world #注意沒有.py的后綴
- hello world
生態(tài)
基本工具比較好把握,但是何時選擇什么工具做什么樣的事情就不好拿捏了,而且如何把事情做成Pythonic的模樣也是對經(jīng)驗和能力的考驗。
如果我們不是編程小白的話,就需要充分利用遷移學習的能力了。學習的***方法就是解決問題。不得不承認,在動手實踐的過程,時間走得是最快的,在同一件事上花的時間越多也就越熟悉。
我們嘗試用Python編寫一個tree命令行(Command-Line Application),顧名思義,打印目錄層級結(jié)構(gòu)的程序,詳細描述參看這篇命令行中 tree 的多重實現(xiàn)。
測試模塊
怎么寫測試呢?多年養(yǎng)成的TDD習慣讓我首先想要了解什么是Python中常用的測試工具。答案不難尋找,unittest是Python內(nèi)置的測試模塊,而pytest是比unittest更簡潔和強大的選擇,所以我選擇后者。
這個程序的測試我使用pytest,但是它并不是所有項目測試的唯一選擇,所以***能局部安裝,尤其是限制在當前工程目錄里。搜索查找的結(jié)果是,Python3內(nèi)置的虛擬環(huán)境(Virtual Environment)模塊可以做到這點。
虛擬環(huán)境
在當前創(chuàng)建venv目錄(python3 -m venv venv),然后用tree命令查看該目錄的結(jié)構(gòu)。
- $ python3 -m venv venv
- $ tree -L 4 venv
- venv
- ├── bin
- │ ├── activate
- │ ├── activate.csh
- │ ├── activate.fish
- │ ├── easy_install
- │ ├── easy_install-3.6
- │ ├── pip
- │ ├── pip3
- │ ├── pip3.6
- │ ├── python -> python3
- │ └── python3 -> /usr/local/bin/python3
- ├── include
- ├── lib
- │ └── python3.6
- │ └── site-packages
- │ ├── __pycache__
- │ ├── easy_install.py
- │ ├── pip
- │ ├── pip-9.0.3.dist-info
- │ ├── pkg_resources
- │ ├── setuptools
- │ └── setuptools-39.0.1.dist-info
- └── pyvenv.cfg
進入虛擬環(huán)境,然后使用pip3安裝pytest測試模塊,會發(fā)現(xiàn)venv目錄多了些東西。
- $ . venv/bin/activate
- venv ❯ pip3 install pytest
- Collecting pytest
- ...
- $ tree -L 4 venv
- venv
- ├── bin
- │ ├── py.test
- │ ├── pytest
- ├── include
- ├── lib
- │ └── python3.6
- │ └── site-packages
- │ ├── __pycache__
- │ ├── _pytest
- │ ├── atomicwrites-1.1.5.dist-info
- │ ├── attr
- │ ├── attrs-18.1.0.dist-info
- │ ├── more_itertools
- │ ├── more_itertools-4.2.0.dist-info
- │ ├── pluggy
- │ ├── pluggy-0.6.0.dist-info
- │ ├── py
- │ ├── py-1.5.3.dist-info
- │ ├── pytest-3.6.2.dist-info
- │ ├── pytest.py
- │ ├── six-1.11.0.dist-info
- │ └── six.py
此時,虛擬環(huán)境會在PATH變量中前置./bin目錄,所以可以直接使用pytest命令進行測試。根據(jù)約定,測試文件的名稱必須以test_開頭,如test_pytree.py,測試方法也必須如此,如test_fix_me。遵循約定編寫一個注定失敗的測試如下:
- """
- test_pytree.py
- """
- def test_fix_me():
- assert 1 == 0
- $ pytest
- ...
- def test_fix_me():
- > assert 1 == 0
- E assert 1 == 0
- test_pytree.py:5: AssertionError
測試失敗了,說明測試工具的打開方式是正確的。在進入測試、實現(xiàn)和重構(gòu)(紅-綠-黃)的心流狀態(tài)之前,我們需要考慮測試和實現(xiàn)代碼該放在哪里比較合適。
假設(shè)我們會把pytree作為應用程序分發(fā)出去供別人下載使用,那么標準的目錄結(jié)構(gòu)和構(gòu)建腳本是必不可少的,Python自然有自己的一套解決方案。
目錄結(jié)構(gòu)
在Packaging Python Projects的指導下,我們略作調(diào)整,創(chuàng)建和源代碼平級的測試目錄(tests),得到的完整目錄如下:
- .
- ├── CHANGES
- ├── LICENSE
- ├── README.md
- ├── docs
- ├── pytree
- │ ├── __init__.py
- │ ├── __version__.py
- │ ├── cli.py
- │ └── core.py
- ├── setup.cfg
- ├── setup.py
- ├── tests
- │ ├── fixtures
- │ └── test_pytree.py
- └── venv
這樣的目錄結(jié)構(gòu)不僅可以清晰地模塊化,隔離測試和實現(xiàn),提供使用指導和版本更新記錄,還可以很方便地做到包依賴管理和分發(fā),這得歸功于setup.py,它是Python項目中事實標準(de facto standard)上的依賴和構(gòu)建腳本,pytree下的setup.py內(nèi)容如下:
- # setup.py
- # -*- coding: utf-8 -*-
- from setuptools import setup, find_packages
- from codecs import open
- import os
- here = os.path.abspath(os.path.dirname(__file__))
- about = {}
- with open(os.path.join(here, 'pytree', '__version__.py'), 'r', 'utf-8') as f:
- exec(f.read(), about)
- with open('README.md') as f:
- readme = f.read()
- with open('LICENSE') as f:
- license = f.read()
- setup(
- name='pytree',
- version=about['__version__'],
- description='list contents of directories in a tree-like format.',
- long_description=readme,
- author='Yan Qian',
- author_email='qianyan.lambda@gmail.com',
- url='https://github.com/qianyan/pytree',
- license=license,
- packages=find_packages(exclude=('tests', 'docs')),
- classifiers=(
- "Programming Language :: Python :: 3",
- "License :: OSI Approved :: MIT License",
- "Operating System :: OS Independent",
- ),
- setup_requires=['pytest-runner'],
- tests_require=['pytest'],
- entry_points = {
- 'console_scripts': [
- 'pytree = pytree.cli:main'
- ]
- },
- install_requires=[]
- )
setup.py能幫助我們解決測試中依賴模塊的問題,這樣我們把pytree作為一個package引入到測試代碼中。
- venv ❯ python3
- Python 3.6.5 (default, Jun 17 2018, 12:13:06)
- [GCC 4.2.1 Compatible Apple LLVM 9.1.0 (clang-902.0.39.2)] on darwin
- Type "help", "copyright", "credits" or "license" for more information.
- >>> import sys, pprint
- >>> pprint.pprint(sys.path)
- ['',
- '/usr/local/Cellar/python/3.6.5_1/Frameworks/Python.framework/Versions/3.6/lib/python36.zip',
- '/usr/local/Cellar/python/3.6.5_1/Frameworks/Python.framework/Versions/3.6/lib/python3.6',
- '/usr/local/Cellar/python/3.6.5_1/Frameworks/Python.framework/Versions/3.6/lib/python3.6/lib-dynload',
- '/Users/qianyan/Projects/personal/public/pytree/venv/lib/python3.6/site-packages',
- '/Users/qianyan/Projects/personal/public/pytree/venv/lib/python3.6/site-packages/docopt-0.6.2-py3.6.egg',
- '/Users/qianyan/Projects/personal/public/pytree']
然后運行pytest或者python3 setup.py pytest,此時pytest會把.pytree/tests前置到PATH變量中,驗證如下:
- # test_pytree.py
- import sys
- def test_path():
- assert sys.path == ''
- venv ❯ pytest
- -> AssertionError: assert ['/Users/qianyan/Projects/personal/public/pytree/tests',
- '/Users/qianyan/Projects/personal/public/pytree/venv/bin', ...] == ''
- venv ❯ python3 setup.py pytest
- -> AssertionError: assert ['/Users/qianyan/Projects/personal/public/pytree/tests',
- '/Users/qianyan/Projects/personal/public/pytree', ...] == ''
這里python3 setup.py pytest可以通過setup.cfg設(shè)置別名(alias):
- # setup.cfg
- [aliases]
- test=pytest
python3 setup.py test的效果和前面的命令等同。
使用TDD的方式實現(xiàn)了pytree核心的功能(源代碼),然后考慮如何把它變成真正的命令行程序。首先要解決的問題是如何以用戶友好的方式顯示需要哪些傳入?yún)?shù),我們期待pytree -h能提供一些幫助信息,為了不重復造輪子,挑選現(xiàn)成的Option解析庫比較輕松。Python內(nèi)置的argparse已經(jīng)足夠用了,不過docopt值得嘗試。
依賴管理
setup.py提供了依賴管理功能,聲明依賴及其版本號。
- # setup.py
- ...
- install_requires=[docopt==0.6.2]
然后運行python3 setup.py develop安裝。就緒之后,編寫cli.py作為命令行程序的入口。
- #!/usr/bin env python3
- """list contents of directories in a tree-like format.
- Usage:
- pytree <dir>
- pytree -h | --help | --version
- """
- import pytree.core as pytree
- import pytree.__version__ as version
- def main():
- from docopt import docopt
- arguments = docopt(__doc__, version=version.__version__)
- dir_name = arguments['<dir>']
- print('\n'.join(pytree.render_tree(pytree.tree_format('', dir_name))))
- if __name__ == "__main__":
- main()
通過打印help信息的方式驗證是否符合預期:
- $ python3 pytree/cli.py --help
- list contents of directories in a tree-like format.
- Usage:
- pytree <dir>
- pytree -h | --help | --version
當然理想的結(jié)果是直接可以運行pytree --help,setup.py的console_scripts剛好派上用場。
- # setup.py
- entry_points = {
- 'console_scripts': [
- 'pytree = pytree.cli:main' #以pytree作為命令行程序的調(diào)用名
- ]
- }
此時查看which pytree顯示/Users/qianyan/Projects/personal/public/pytree/venv/bin/pytree,說明pytree已經(jīng)在路徑變量當中,可以直接執(zhí)行:
- $ pytree tests/fixtures
- tests/fixtures
- └── child
完成了命令行程序并通過測試,我們嘗試發(fā)布到測試倉庫(TestPyPI)供其他人下載使用。
包發(fā)布
依照文檔描述,先去TestPyPI注冊用戶,本地打包成發(fā)行版,然后安裝twine工具發(fā)布。
- $ python3 -m pip install --upgrade setuptools wheel
- $ python3 setup.py sdist bdist_wheel
- $ pytree dist # pytree查看dist目錄
- dist
- ├── pytree-1.0.2-py3-none-any.whl
- └── pytree-1.0.2.tar.gz
- $ python3 -m pip install --upgrade twine
- $ twine upload --repository-url https://test.pypi.org/legacy/ dist/* #or twine upload --repository testpypi dist/* 如果你配置了~/.pypirc
上傳成功需要一段時間,等待服務(wù)完成同步才可以下載,我們在另一個虛擬環(huán)境中進行驗證:
- $ python3 -m venv test
- $ . test/bin/activate
- test > python3 -m pip install --index-url https://test.pypi.org/simple/ pytree==1.0.2
- test > ls venv/lib/python3.6/site-packages/
- ...
- pytree
- pytree-1.0.2.dist-info
- ...
確保site-packages目錄下有這兩個目錄:pytree和pytree-1.0.2.dist-info,然后我們就可以完成***的驗證階段了,如下:
- test > pytree tests/fixtures
- tests/fixtures
- └── child
這里版本號之所以是1.0.2,是因為已經(jīng)上傳過了0.0.1, 1.0.0, 1.0.1 等版本,TestPyPI不允許同名同版本的文件重復上傳,即使刪除原來的文件也不行。前面的版本都有一定的錯誤,錯誤的根因在于find_packages以及package_dir的配置項文檔說明很模糊,而且只有到上傳到TestPyPI然后下載下來,才能驗證出來,這種緩慢的反饋是Python的應該詬病的地方。
注意
find_package()也是一個深坑,***個參數(shù)如果寫成find_packages('pytree', exclude=...),那么pytree下的所有Python文件都會被忽略。原因是pytree已經(jīng)是package,所以不應該讓setup去這個目錄找其他的packages.
這個package_dir也是如此,我們?nèi)绻O(shè)置package_dir={'': 'pytree'},setup.py就會將/Users/qianyan/Projects/personal/public/pytree/pytree前置到PATH中,這會導致console_scripts': ['pytree = pytree.cli:main']拋出錯誤 ModuleNotFoundError: no module named ‘pytree’,究其原因是pytree/pytree導致setup嘗試在pytree/pytree這個package里頭找自己(pytree),自然找不到。但是如果改成console_scripts': ['pytree = cli:main'],因為cli在pytree/pytree底下,所以就能成功執(zhí)行。當然這是一種錯誤的寫法。
如果遇到了 ModuleNotFoundError: no module named ‘pytree’ 的錯誤,***的方式就是import sys, pprint然后pprint.pprint(sys.path),很容易發(fā)現(xiàn)Python運行時的執(zhí)行路徑,這有助于排查潛在的配置錯誤。