Python抽象基类的定义与使用
我们写Python基本不需要自己创建抽象基类,而是通过鸭子类型来解决大部分问题。《流畅的Python》作者使用了15年Python,但只在项目中创建过一个抽象基类。我们更多时候是创建现有抽象基类的子类,或者使用现有的抽象基类注册。本文的意义在于,了解抽象基类的定义与使用,可以帮助我们理解抽象基类是如何实现的,为我们以后学习后端语言(比如Java、Golang)打下基础。**毕竟抽象
我们写Python基本不需要自己创建抽象基类,而是通过鸭子类型来解决大部分问题。《流畅的Python》作者使用了15年Python,但只在项目中创建过一个抽象基类。我们更多时候是创建现有抽象基类的子类,或者使用现有的抽象基类注册。本文的意义在于,了解抽象基类的定义与使用,可以帮助我们理解抽象基类是如何实现的,为我们以后学习后端语言(比如Java、Golang)打下基础。**毕竟抽象
本书作者:测试开发刚哥,文章版权归个人所有,未经授权请勿转载。 Python接口与协议 Python语言是没有interface关键字的,这也是动态类型语言的特点之一。Python的接口指的是类实现或继承的公开属性,包括数据或方法。比如Sequence的正式接口如下图所示: 这些都叫做Python接口。事实上,Python每个类都有接口,除了抽象基类。 接口是正式的,它定义了类具有哪些属性,协议是非正式的接口,只由文档和约定定义。比如只要类实现了__getitem__方法,按照序列协议的约定,Python就会支持这个类完成访问元素、迭代和使用in运算符等一系列操作。 猴子补丁 我们可以通过猴子补丁来进一步认识,Python中协议的约定是怎么回事。猴子补丁是这样一种技术:在运行时修改类或模块,而不改动源码。还是以序列协议来举例,比如: >>> from random import shuffle >>> from frenchdeck import FrenchDeck >>> deck = FrenchDeck() >>> shuffle(deck) 假设运行报错了:TypeError: 'FrenchDeck' object does not support item assignment,因为FrenchDeck类只有__getitem__方法,只实现了不可变的序列协议。为了解决这个报错,需要通过__setitem__方法实现可变的序列协议。 代码如下: def set_card(deck, position, card): deck._cards[position] = card FrenchDeck.__setitem__ = set_card shuffle(deck) 这就是一段猴子补丁技术的实现代码,没有修改FrenchDeck类的源码,而是在运行时动态修改。 抽象基类 Python的抽象基类是指必须让继承它的子类去实现它所要求的方法的类。Python的collections.abc模块中就定义了很多抽象基类: 虽然我们在实际编程中并不会自己编写抽象基类(一般也不建议这样做,因为可能会导致意想不到的问题),但是了解抽象基类,可以帮助我们更好理解Python面向对象的继承等概念。 抛开枯燥的理论知识,我在书中找到了一处具有实践价值的片段: 「在一连串if/elif/elif中使用isinstance做检查,然后根据对象的类型执行不同的操作,通常是不好的做法;此时应该使用多态,即采用一定的方式定义类,让解释器把调用分派给正确的方法,而不使用if/elif/elif块硬编码分派逻辑。」 交流群和公司都有人问过我这个问题,写了太多的ifelse有没有更好的实现方式,我想这里已经给出了答案。 鸭子类型和白鹅类型 Python鸭子类型是指对象的类型无关紧要,只要实现了特定的协议即可。它的好处是避免过多的isinstance,如果遵守既定协议,能增加利用现有的标准库和第三方代码的可能性。示例: class Animal(object): def run(self): print("The animal is running...") class Dog(Animal): def run(self): print('The dog is running...') class Cat(Animal): def run(self): print('The cat is running...') def make_run(animal): animal.run() dog = Dog() cat = Cat() make_run(dog) make_run(cat) 对于 make_run() 函数来说,传入的参数并不一定需要是 Animal 类型的,只需要保证传入的对象有一个 run() 方法即可。 ...
本文将在上篇文章二维向量Vector2d类的基础上,定义表示多维向量的Vector类。
Python是一门高级语言,支持面向对象设计,如何设计一个符合Python风格的面向对象的类,是一个比较复杂的问题,本文提供一个参考,表达一种思路,探究一层原理。
本书作者:测试开发刚哥,文章版权归个人所有,未经授权请勿转载。 变量全都是引用 跟其他编程语言不同,Python的变量不是盒子,不会存储数据,它们只是引用,就像标签一样,贴在对象上面。 比如: >>> a = [1, 2, 3] >>> b = a >>> a.append(4) >>> b [1, 2, 3, 4] >>> b is a True a变量和b变量引用的是同一个列表[1, 2, 3]。b可以叫做a的别名。 比较来看: >>> a = [1, 2, 3] >>> c = [1, 2, 3] >>> c == a True >>> c is a False c引用的是另外一个列表,虽然和a引用的列表的值相等,但是它们是不同的对象。 浅复制与深复制 浅复制是指只复制最外层容器,副本中的元素是源容器中元素的引用。如果所有元素都是不可变的,那么这样没有问题,还能节省内容。但是,如果有可变的元素,那么结果可能会出乎意料之外。构造方法或[:]做的都是浅复制。 示例: >>> x1 = [3, [66, 55, 44], (7, 8, 9)] ## x2是x1的浅复制 >>> x2 = list(x1) ## 不可变元素没有影响 >>> x1.append(100) >>> x1 [3, [66, 55, 44], (7, 8, 9), 100] >>> x2 [3, [66, 55, 44], (7, 8, 9)] ## x1[1]是列表,可变元素会影响x2 ## 因为它们引用的是同一个对象 >>> x1[1].remove(55) >>> x1 [3, [66, 44], (7, 8, 9), 100] >>> x2 [3, [66, 44], (7, 8, 9)] ## x2[1]也会反过来影响x1 >>> x2[1] += [33, 22] >>> x1 [3, [66, 44, 33, 22], (7, 8, 9), 100] >>> x2 [3, [66, 44, 33, 22], (7, 8, 9)] ## 不可变元组也不会有影响 ## +=运算符创建了一个新元组 >>> x2[2] += (10, 11) >>> x1 [3, [66, 44, 33, 22], (7, 8, 9), 100] >>> x2 [3, [66, 44, 33, 22], (7, 8, 9, 10, 11)] 深复制是指我们常规理解的复制,副本不共享内部对象的引用,是完全独立的一个副本。这可以借助copy.deepcopy来实现。 ...
本书作者:测试开发刚哥,文章版权归个人所有,未经授权请勿转载。 编解码器 在字符与字节之间的转换过程称为编解码,Python自带了超过100种编解码器,比如: ascii(英文体系) gb2312(中文体系) utf-8(全球通用) latin1 utf-16 编解码器一般有多个别名,比如utf8、utf-8、U8。 这些编解码器可以传给open()、str.encode()、bytes.decode()等函数的encoding参数。 UnicodeEncodeError 多数非UTF编解码器(比如cp437)只能处理Unicode字符的一小部分子集。把字符转换成字节时,如果目标编码中没有定义这个字符,那么就会抛出UnicodeEncodeError异常。 处理方式一:使用utf8编码。 处理方式二:添加errors参数: ## 忽略 如b'So Paulo' city.encode("cp437", errors="ignore") ## 替换为? 如b'S?o Paulo' city.encode("cp437", errors="replace") ## 替换为XML实体 如b'São Paulo' city.encode("cp437", errors="xmlcharrefreplace") UnicodeDecodeError 把字节转换为字符时,遇到无法转换的字节时会抛出UnicodeDecodeError异常。这是因为不是每个字节都包含有效的ASCII字符,也不是每个字符都是有效的UTF-8。 处理方式也有两种,跟上面一样。 SyntaxError Python3默认使用UTF-8编码源码。如果加载的.py模块中包含UTF-8之外的数据,而且没有声明编码,就会抛出SyntaxError异常。 处理方式是在文件顶部添加coding注释: ## coding: cp1252 但是这个办法并不好,最好还是找到这些报错字符,把它们转换为UTF-8。 从网上直接复制代码到IDE中执行经常会报这个错。 处理文本文件 Unicode三明治: 在程序中尽量少接触二进制,把字节解码为字符,只处理字符串对象。比如在Django中,view应该输出Unicode字符串,Django会负责把响应数据编码成字节序列,而且默认使用UTF-8编码。 Python内置的open函数就是采用了这个原则,在读取文件时会做必要的解码,以文本模式写入文件时会做必要的编码。 文件乱码 Windows更容易遇到这个问题,因为Windows并不是统一的UTF-8编码,比如在Windows10中: >>> open("cafe.txt", "w", encoding="utf8").write("café") 4 >>> open("cafe.txt").read() 'caf茅' 写入文件时指定了utf8,但是读取文件没有指定,Python就会使用系统默认编码: >>> import locale ## 打开文件用这个 ## 如果没有设置PYTHONENCODING环境变量,sys.stdout/stdin/stderr也用这个 >>> locale.getpreferredencoding() 'cp936' cp936把最后一个字节解码成了茅而不是é。 >>> import sys # 二进制数据和字符串之间转换用这个 >>> sys.getdefaultencoding() 'utf-8' >>> import sys # 文件名(不是文件内容)用这个 >>> sys.getfilesystemencoding() 'utf-8' GNU/Linux或Mac OS X不会遇到这个问题,因为多年来它们的默认编码都是UTF-8。 ...
本书作者:测试开发刚哥,文章版权归个人所有,未经授权请勿转载。 字符 字符是一个信息单位,简单来讲就是一个字母、数字、标点符号、汉字等。 字符的最佳定义是Unicode字符: 它是一个全球化的标准,能表示世界上所有语言的字符。Unicode字符的标识(码位)是以4~6个十六进制数字表示的,并且加前缀U+。 字节 字节是计算机信息计量单位,一个字节代表八个比特,存储的数值范围为0~255。 字节跳动(ByteDance)互联网公司的字节就是这个字节。 字节是机器的,字符是人类的。 把人类字符转换为机器字节时使用的算法叫做编码,反之叫做解码。 算法不同,字节与字符的关系也不同: bytes和bytearray 字节实际上是个二进制序列。不可变bytes类型和可变bytearray类型是用来存储二进制序列的,它们的示例如下: >>> cafe = bytes("café", encoding="utf_8") >>> cafe b'caf\xc3\xa9' >>> cafe[0] 99 >>> cafe[:1] b'c' >>> cafe_arr = bytearray(cafe) >>> cafe_arr bytearray(b'caf\xc3\xa9') >>> cafe_arr[-1:] bytearray(b'\xa9') 特别的是cafe[0]返回了整数,cafe[:1]返回了二进制序列,这是因为s[0] == s[:1]只对str类型成立,而对于其他类型来说,s[i]返回一个元素,s[i:i+1]返回一个相同类型的序列。 二进制序列实际上是整数序列。它们的字面量表示法包含ASCII字符(ASCII只能表示英文体系的字符),比如cafe的b'caf\xc3\xa9',具体规则是: 从空格到~的字符直接使用ASCII字符 制表符\t、换行符\n、回车符\r、转义符\\ 其他字符用十六进制转义序列,比如\x00空字节 构建bytes和bytearray对象的方式有以下几种: 一个str对象和一个encoding关键字参数 一个可迭代对象,数值在0~255 一个实现了缓冲协议的对象,如bytes、bytearray、memoryview、array.array memoryview和struct memoryview允许在二进制数据结构之间共享内存,struct能从序列中提取结构化信息。 示例如下,提取一个GIF图像的宽度和高度: import struct with open("filter.gif", "rb") as fp: img = memoryview(fp.read()) ## 这里不会复制字节序列,因为用的memoryview header = img[:10] print(bytes(header)) # b'GIF89a+\x02\xe6\x00' ## <是小字节序,3s3s是两个3字节序列,HH是两个16位二进制整数 ## 类型、版本、宽度、高度 struct.unpack("<3s3sHH", header) # (b'GIF', b'89a', 555, 230) ## 删除引用,释放memoryview实例所占的内存 del header del img 小结 本文介绍了字符和字节的概念以及它们之间的关系,一个字符对应一个或多个字节。字符是人类的,字节是机器的,编码就是人类字符转换为机器字节,反之叫做解码。然后分别介绍了二进制序列的类型bytes和bytearray,和二进制序列的工具memoryview和struct。 ...
一句话:前者在报进展,后者在讲思想。同样的素材,后者立意高一个身位。
在了解了Python函数装饰器基础知识和闭包之后,开始正式学习函数装饰器。
本书作者:测试开发刚哥,文章版权归个人所有,未经授权请勿转载。 简介 TestNG是Test Next Generation的缩写,它的灵感来自于JUnit和NUnit,在它们基础上增加了很多很牛的功能,比如说: 注解。 多线程,比如所有方法都在各自线程中,一个测试类一个线程等。 验证代码是否多线程安全。 灵活的测试配置。 支持数据驱动(@DataProvider)。 支持参数化。 强大的执行机制(不需要TestSuite)。 能跟各种工具结合(比如IDEA、Maven等)。 内嵌BeanShell。 提供运行时和日志的JDK函数(不需要添加dependencies)。 提供应用服务器测试依赖的方法。 一句话总结就是,TestNG强大到能覆盖所有测试类型:单元测试、功能测试、端到端测试、集成测试等等等。 一个简单的示例如下: package example1; import org.testng.annotations.*; public class SimpleTest { @BeforeClass public void setUp() { // code that will be invoked when this test is instantiated } @Test(groups = { "fast" }) public void aFastTest() { System.out.println("Fast test"); } @Test(groups = { "slow" }) public void aSlowTest() { System.out.println("Slow test"); } } setUp()会在测试类创建后,测试方法执行前运行。 测试方法名字可以任意的,TestNG通过@Test注解来标识。 可以把测试方法按group分组。 然后使用xml来配置运行策略: ...