测试篇
此部分承接上一篇文章,在发现错误以及调试后,需要对一个模块或是函数或类进行检验
单元测试
为了保证代码质量,方便后续修改和设计,我们采用单元测试:
要测试的Dict类:
python
# mydict.py
class Dict(dict):
"""支持属性访问的字典"""
def __getattr__(self, key):
try:
return self[key]
except KeyError:
raise AttributeError(f"'Dict' object has no attribute '{key}'")
def __setattr__(self, key, value):
self[key] = value
测试代码:(引入 Python 自带的 unittest 模块)
python
# test_mydict.py
import unittest
from mydict import Dict
class TestDict(unittest.TestCase):
def test_init(self):
"""测试初始化"""
d = Dict(a=1, b='test')
self.assertEqual(d.a, 1) # 属性访问
self.assertEqual(d['b'], 'test') # 字典访问
self.assertTrue(isinstance(d, dict))
def test_key(self):
"""测试字典方式设置和获取"""
d = Dict()
d['key'] = 'value'
self.assertEqual(d.key, 'value') # 可以通过属性访问
def test_attr(self):
"""测试属性方式设置和获取"""
d = Dict()
d.key = 'value'
self.assertEqual(d['key'], 'value') # 可以通过字典访问
self.assertIn('key', d) # 确实在字典中
def test_keyerror(self):
"""测试访问不存在的key时抛出KeyError"""
d = Dict()
with self.assertRaises(KeyError): # 期待抛出KeyError
value = d['nonexistent']
def test_attrerror(self):
"""测试访问不存在的属性时抛出AttributeError"""
d = Dict()
with self.assertRaises(AttributeError): # 期待抛出AttributeError
value = d.nonexistent
# 运行测试
if __name__ == '__main__':
unittest.main()
编写时测试类继承自unittest.TestCase,test开头的为测试方法,继承的类提供了内置的条件判断,调用他们通过assert断言输出是否为我们期望输出的值,如code中的assertEquals()。
self.assertEquals(abs(-1), 1) # 断言函数返回的结果与 1 相等
另一种重要的断言就是期待抛出指定类型的 Error,比如通过 d['empty']访问不存在的 key
时,断言会抛出 KeyError:
with self.assertRaises(KeyError):
value = d['empty']
而通过 d.empty 访问不存在的 key 时,我们期待抛出 AttributeError:
with self.assertRaises(AttributeError):
value = d.empty
运行测试
python
# 方法1:直接运行测试文件
python test_mydict.py
# 方法2:使用unittest模块(推荐)
python -m unittest test_mydict
# 输出:
# .....
# ----------------------------------------------------------------------
# Ran 5 tests in 0.001s
# OK
setUp 和 tearDown
这是两个特殊方法,会在每调用一个测试方法的前后分别被执行
这种什么时候可能会用到呢,一般可用于连接:连接数据库,连接通信协议
setUp()里可以用于连接,tearDown()里可以用来释放。这样就省去此类每个测试中重复的代码部分。
python
class TestDict(unittest.TestCase):
def setUp(self):
"""每个测试方法前执行"""
print("准备测试环境...")
self.test_dict = Dict(a=1, b=2)
def tearDown(self):
"""每个测试方法后执行"""
print("清理测试环境...")
del self.test_dict
def test_something(self):
"""测试方法"""
self.assertEqual(self.test_dict.a, 1)
文档测试
把测试写在文档字符串里,既是文档又是测试
python
# mydict_with_doctest.py
class Dict(dict):
'''
支持属性访问的字典类
示例:
>>> d = Dict(a=1, b=2)
>>> d.a
1
>>> d['b']
2
>>> d.c = 3
>>> d['c']
3
>>> d['nonexistent']
Traceback (most recent call last):
...
KeyError: 'nonexistent'
>>> d.nonexistent
Traceback (most recent call last):
...
AttributeError: 'Dict' object has no attribute 'nonexistent'
'''
def __getattr__(self, key):
try:
return self[key]
except KeyError:
raise AttributeError(f"'Dict' object has no attribute '{key}'")
def __setattr__(self, key, value):
self[key] = value
if __name__ == '__main__':
import doctest
doctest.testmod() # 自动运行文档中的测试
python mydict_with_doctest.py
**# 如果测试通过,没有任何输出如果测试失败,会显示详细的错误信息**
测试小结
常见的软件工程项目都有测试环节,做好测试需要考虑很多,输入组合;边界条件;各类异常等等,是推进项目的保证和必要条件。
- 先写测试(测试驱动开发)
def test_add_user():
先写测试,再实现功能
pass
运行测试(应该失败)
实现功能代码
再次运行测试(应该通过)
重构优化(确保测试仍然通过)
IO Py编程
cite 廖老师:
++IO 在计算机中指 Input/Output,也就是输入和输出。由于程序和运行时数据是在内存中驻留,++ ++由 CPU 这个超快的计算核心来执行,涉及到数据交换的地方,通常是磁盘、网络等,就需++ ++要 IO 接口。++
++比如你打开浏览器,访问新浪首页,浏览器这个程序就需要通过网络 IO 获取新浪的网页。++
++浏览器首先会发送数据给新浪服务器,告诉它我想要首页的 HTML,这个动作是往外发数++
++据,叫 Output,随后新浪服务器把网页发过来,这个动作是从外面接收数据,叫 Input。所++
++以,通常,程序完成 IO 操作会有 Input 和 Output 两个数据流。当然也有只用一个的情况,++
++比如,从磁盘读取文件到内存,就只有 Input 操作,反过来,把数据写到磁盘文件里,就++
++只是一个 Output 操作。++
++IO 编程中,Stream(流)是一个很重要的概念,可以把流想象成一个水管,数据就是水管++
++里的水,但是只能单向流动。Input Stream 就是数据从外面(磁盘、网络)流进内存,++
++Output Stream 就是数据从内存流到外面去。对于浏览网页来说,浏览器和新浪服务器之间++
++至少需要建立两根水管,才可以既能发数据,又能收数据。++
++由于 CPU 和内存的速度远远高于外设的速度,所以,在 IO 编程中,就存在速度严重不匹++
++配的问题。举个例子来说,比如要把 100M 的数据写入磁盘,CPU 输出 100M 的数据只需++
++要 0.01 秒,可是磁盘要接收这 100M 数据可能需要 10 秒,怎么办呢?有两种办法:++
++第一种是 CPU 等着,也就是程序暂停执行后续代码,等 100M 的数据在 10 秒后写入磁盘,++
++再接着往下执行,这种模式称为同步 IO;++
++另一种方法是 CPU 不等待,只是告诉磁盘,"您老慢慢写,不着急,我接着干别的事去了",++
++于是,后续代码可以立刻接着执行,这种模式称为异步 IO。同步和异步的区别就在于是否等待 IO 执行的结果。好比你去麦当劳点餐,你说"来个汉堡",++
++服务员告诉你,对不起,汉堡要现做,需要等 5 分钟,于是你站在收银台前面等了 5 分钟,++
++拿到汉堡再去逛商场,这是同步 IO。++
++你说"来个汉堡",服务员告诉你,汉堡需要等 5 分钟,你可以先去逛商场,等做好了,我++
++们再通知你,这样你可以立刻去干别的事情(逛商场),这是异步 IO。++
++很明显,使用异步 IO 来编写程序性能会远远高于同步 IO,但是异步 IO 的缺点是编程模型++
++复杂。想想看,你得知道什么时候通知你"汉堡做好了",而通知你的方法也各不相同。如果++
++是服务员跑过来找到你,这是回调模式,如果服务员发短信通知你,你就得不停地检查手++
++机,这是轮询模式。总之,异步 IO 的复杂度远远高于同步 IO。++
++操作 IO 的能力都是由操作系统提供的,每一种编程语言都会把操作系统提供的低级 C 接++
++口封装起来方便使用,Python 也不例外。++
文件读写
文件读写是最常用的IO操作。python内置的文件读写函数用法兼容c语言,本质是os提供接口打开一个文件对象(文件描述符),从接口中读取数据或写入数据。
读文件
python内置读文件函数open(),'r'默认是读取文本文件且是ASCII编码的文本,若要读取二进制文件(例如常见的图片,视频等)采用'rb'模式
python
f = open('/Users/code/test.txt', 'r')
f = open('D:\图片\OIP-C.jpg', 'rb')
print(f.read())
b'RIFF\x8cf\x00\x00WEBPVP8...'
如果非ASCII码的文本,就必须以二进制模式打开再解码。以GBK编码文件为例:
python
f = open('/Users/michael/gbk.txt', 'rb')
u = f.read().decode('gbk')
print(u)
这样转码很麻烦,py提供了一个codecs模块帮助读文件时自动转换编码,直接读出Unicode:
python
import codecs
with codecs.open('/Users/michael/gbk.txt', 'r', 'gbk') as f:
f.read() # u'\u6d4b\u8bd5'
文件不存在,open()函数会抛出IOError的错误,返回错误码和错误信息。
Traceback (most recent call last):
File "D:\Python Codelib\Code\pylearning.py", line 672, in <module>
f = open('/Users/code/test.txt', 'r')
FileNotFoundError: [Errno 2] No such file or directory: '/Users/code/test.txt
文件打开成功,就可以直接调用read()方法一次读取文件的全部内容,py把内容读到内存中转化成str对象表示:
>>>f.read()
'Hello ,world'
关闭文件
>>> f.close()
文件读写时很容易出现读写错误返回IOError,那么close()就不会调用,所以可以结合学会的try...finally实现
try:
f = open('/Users/code/test.txt', 'r')
print (f.read())
finally:
if f:
f.close()
但Python有更方便的语句来自动调用close()方法,那就是with语句
with open('/path/to/file', 'r') as f:
print f.read()
和上一个try的意思一致还更简洁,不用每次去记得加close。
在前面操作时还有一个问题:调用read()一次性读取文件内容到内存,那如果文件内容几十G,岂不是一下子把内存干爆了?所以保守一下,采用反复调用的方法read(size),每次只会读最多size个字节的内容。这种控制读取的方法在Java等语言也见过。Python调用readline()可以每次读取一行,调用readlines()则可以读取所有内容并按行返回list
总结:文件小,直接read();不确定文件大小,用read(size)试探就行;配置文件(类似于json格式,适用于读取json数据集的情况)就用readlines()
for line in f.readlines():
print(line.strip()) # 把末尾的'\n'删掉
这里还需要补充一点:
只要对一个对象使用open()函数,其返回中有read()方法的对象,我们统称它为File-like Object。其寓意也很简单:"像文件的对象"。
它的核心特征就是必须有read()方法,通常也有write(),seek(),close()等方法
且不要求从特定的类继承。
StringIO就是内存中的创建的File-like Object常作为临时缓冲。让你在内存中操作字符串像操作文件一样,下面只举一个小栗子方便理解
pythonfrom io import StringIO # 创建内存中的"文件" memory_file = StringIO() # 像写入文件一样写入内容 memory_file.write("Hello, World!\n") memory_file.write("这是第二行内容") # 移动到文件开头(就像操作真实文件一样) memory_file.seek(0) # 读取内容 content = memory_file.read() print("读取的内容:") print(content) # 关闭"文件" memory_file.close()
写文件
与读文件很是类似,写默认文本就是'r',二进制文本就是'rb'
python
>>> f = open('/Users/michael/test.txt', 'w')
>>> f.write('Hello, world!')
>>> f.close()
反复写反复调用write即可,但是close()不要忘。os不会把我们写入的东西立刻写入磁盘而是放入内存中缓存,空闲时慢慢写。调用close(),才能保证让os都写好。所以不加的后果就是可能写了一部分剩下的没了。所以with更保险些:
python
with open('/Users/michael/test.txt', 'w') as f:
f.write('Hello, world!')
换码的操作仿照读就可以,这里不再另外举例了。
操作文件和目录
这部分在操作系统(下简称os)课中都会提及,不过本节根据python做适应。
操作文件和目录,可以在命令行里输入os的各种命令,dir或cp等等
python
import os
# 查看操作系统类型
print("操作系统:", os.name) # 'posix' (Linux/Mac) 或 'nt' (Windows)
# 获取详细系统信息(Windows不可用)
if os.name == 'posix':
print("系统详情:", os.uname())
#说明os模块的一些函数和os息息相关

目录操作
操作文件和目录的函数一部分放在 os 模块中,一部分放在 os.path 模块中,这一点要注意
一下
python
import os
# 获取当前工作目录
current_dir = os.getcwd()
print("当前目录:", current_dir)
# 切换目录
os.chdir('/tmp') # 切换到 /tmp 目录
print("切换后目录:", os.getcwd())
# 切换回原目录
os.chdir(current_dir)
# 查看当前目录的绝对路径:
>>> os.path.abspath('.')
'/Users/michael'
# 在某个目录下创建一个新目录,
# 首先把新目录的完整路径表示出来:
>>> os.path.join('/Users/michael', 'testdir')
'/Users/michael/testdir'
# 然后创建一个目录:
>>> os.mkdir('/Users/michael/testdir')
# 删掉一个目录:
>>> os.rmdir('/Users/michael/testdir')
操作 | 方法 | 说明 |
---|---|---|
目录操作 | os.listdir() , os.mkdir() |
列出、创建目录 |
文件操作 | os.rename() , os.remove() |
重命名、删除文件 |
路径处理 | os.path.join() , os.path.exists(), os.path.split(),os.path.splitext() |
路径拼接和检查 |
信息获取 | os.stat() , os.getcwd() |
获取文件和系统信息 |
tips:但是你会发现竟然没有复制操作,os中没提供系统调用,但其实通过读写可以实现,就是麻烦了些。
幸运的是 shutil 模块提供了 copyfile()的函数,你还可以在 shutil 模块中找到很多实用
函数,它们可以看做是 os 模块的补充
环境变量
os中定义的环境变量都保存在os.environ这个dict中
python
environ({'ALLUSERSPROFILE': 'C:\\ProgramData', 'APPDATA': 'C:\\Users\\Administrator\\AppData\\Roaming', 'COMMONPROGRAMFILES': 'C:\\Program Files\\Common Files', 'COMMONPROGRAMFILES(X86)': 'C:\\Program Files (x86)\\Common Files', 'COMMONPROGRAMW6432': 'C:\\Program Files\\Common Files', 'COMPUTERNAME': 'WKTAN', 'COMSPEC': 'C:\\Windows\\system32\\cmd.exe', 'CONDA_DEFAULT_ENV': 'base', 'CONDA_PREFIX': ...})
获得某个环境变量的值,调用os.getenv()
python
>>>print(os.getenv('WINDIR'))
>>>C:\Windows