为天地立心,为生民立命,为往圣继绝学,为万世开太平。
——张载
类的基本注意点
私有变量
1.变量名如果以双下划线__
开头,就变成了一个私有变量(private),只有内部可以访问,外部不能访问。
2.如果外部代码要获取相应的私有变量,可定义get_xxx
方法,如果想修改,定义set_xxx
方法。
3.在Python中,变量名类似__xxx__
的,也就是以双下划线开头,并且以双下划线结尾的,是特殊变量,自己定义时不要使用这种命名方法。
4.看一个例子:
1 | class Student(object): |
表面上看,外部代码“成功”地设置了__name
变量,但实际上这个__name
变量和class内部的__name
变量不是一个变量!内部的__name
变量已经被Python解释器自动改成了_Student__name
,而外部代码给bart新增了一个__name
变量,通过最后一个打印可以看出。
5.注意:不建议这么做,因为不同版本的Python解释器可能会把__name
改成不同的变量名。
python单下划线与双下划线
1._xxx
:不能用于from module import *
以单下划线开头的表示的是保护变量,只能允许其本身与子类进行访问。
2.__xxx
:双下划线的表示的是私有类型的变量(private),只能是允许这个类本身进行访问了,连子类也不可以
3.__xxx__
:定义的是特列方法,如__init__
,__str__
,__call__
等
4.python中不存在protected的概念,上面所说的保护变量,应该把其看作私有变量使用,之所以称为保护变量是为了便于与private类型做区分。
5.一个例子:
1 | class A(object): |
继承和多态
1.继承最大的好处是子类获得了父类的全部功能。
2.当子类和父类都存在相同的run()方法时,子类的run()会覆盖了父类的run(),在代码运行的时候,总是会调用子类的run()。
3.在继承关系中,如果一个实例的数据类型是某个子类,那它的数据类型也可以被看做是父类,可用isinstance(object, class)
查看。
4.多态:对于一个变量,我们只需要知道它是Animal类型,无需确切地知道它的子类型,就可以放心地调用run()方法,而具体调用的run()方法是作用在Animal、Dog、Cat还是Tortoise对象上,由运行时该对象的确切类型决定,这就是多态真正的威力:调用方只管调用,不管细节,而当我们新增一种Animal的子类时,只要确保run()方法编写正确,不用管原来的代码是如何调用的。这就是著名的“开闭”原则:
- 对扩展开放:允许新增Animal子类;
- 对修改封闭:不需要修改依赖Animal类型的run_twice()等函数。
5.动态语言的鸭子类型特点决定了继承不像静态语言那样是必须的。
获取对象信息
1.判断基本数据类型可以直接写int,str等,但如果要判断一个对象是否是函数,可以使用types模块中定义的常量:
1 | 123)==type(456) type( |
2.使用isinstance(),且推荐使用它。
1 | 'a', str) isinstance( |
3.使用dir()
- 要获得一个对象的所有属性和方法,可以使用dir()函数,它返回一个包含字符串的list,比如,获得一个str对象的所有属性和方法:
1 | 'abc') dir( |
- 类似
__xxx__
的属性和方法在Python中都是有特殊用途的,比如__len__
方法返回长度。在Python中,如果你调用len()函数试图获取一个对象的长度,实际上,在len()函数内部,它自动去调用该对象的__len__()
方法,所以,下面的代码是等价的:
1 | 'ABC') len( |
- 剩下的都是普通属性或方法,比如upper()返回小写的字符串:
1 | 'abc'.upper() |
- 仅仅把属性和方法列出来是不够的,配合getattr()、setattr()以及hasattr(),我们可以直接操作一个对象的状态:
1 | class MyObject(object): |
- 如果试图获取不存在的属性,会抛出AttributeError的错误,可以传入一个default参数,如果属性不存在,就返回默认值:
1 | 'z', 404) # 获取属性'z',如果不存在,返回默认值404 getattr(obj, |
- 通过内置的一系列函数,我们可以对任意一个Python对象进行剖析,拿到其内部的数据。要注意的是,只有在不知道对象信息的时候,我们才会去获取对象信息。如果可以直接写:
1 | sum = obj.x + obj.y |
实例属性和类属性
1.由于Python是动态语言,根据类创建的实例可以任意绑定属性。
2.如果Student类本身需要绑定一个属性,可以直接在class中定义属性,这种属性是类属性,归Student类所有:
1 | class Student(object): |
3.从上面的例子可以看出,在编写程序的时候,千万不要对实例属性和类属性使用相同的名字,因为相同名称的实例属性将屏蔽掉类属性,但是当你删除实例属性后,再使用相同的名称,访问到的将是类属性(这里很容易出错,曾在这里困扰半天)
面向对象高级编程
使用__slots__
1.可以给该实例绑定任何属性和方法,这就是动态语言的灵活性。
1 | class Student(object): |
2.给一个实例绑定的方法,对另一个实例不起作用,为了给所有实例都绑定方法,可以给class绑定方法(这里没有讲给类绑定属性,因为方法和给实例绑定属性类似,只需把实例名改成相应的类名即可):
1 | def set_score(self, score): |
3.上面对实例的属性添加十分灵活,如果想限制,为了达到限制的目的,Python允许在定义class的时候,定义一个特殊的__slots__
变量,来限制该class实例能添加的属性
1 | class Student(object): |
由于’score’没有被放到__slots__
中,所以不能绑定score属性,试图绑定score将得到AttributeError的错误。
使用__slots__
要注意,__slots__
定义的属性仅对当前类实例起作用,对继承的子类是不起作用的:
1 | class GraduateStudent(Student): |
除非在子类中也定义__slots__
,这样,子类实例允许定义的属性就是自身的__slots__
加上父类的__slots__
。
使用@property
1.对属性值做相应的限制,通过其可使相应的方法看作属性使用。
把一个getter方法变成属性,只需要加上@property
就可以了,此时,@property
本身又创建了另一个装饰器`@score.setter,负责把一个
setter方法变成
属性赋值`,于是,就拥有一个可控的属性操作:
1 | class Student(object): |
2.若定义只读属性,只定义getter方法,不定义setter方法就是一个只读属性,详情。
3.@property
广泛应用在类的定义中,可以让调用者写出简短的代码,同时保证对参数进行必要的检查,这样,程序运行时就减少了出错的可能性。
多重继承
Python允许使用多重继承,因此,MixIn就是一种常见的设计。
枚举
1.enum 提供了Enum/IntEnum/unique
三个工具,可以通过继承Enum/IntEnum
定义枚举类型。
其中IntEnum
限定枚举成员必须为(或可以转化为)整数类型,而unique
方法可以作为修饰器限定枚举成员的值不可重复。
2.实例
1 | from enum import Enum, IntEnum, unique |
3.可参考:Python 中的枚举类型
元类(metaclass)
动态语言和静态语言最大的不同,就是函数和类的定义,不是编译时定义的,而是运行时动态创建的。
1.这个概念比较深,而且不太好理解,个人感觉未能很好地理解,下面是看到的很好的文章。
2.运用实例:元类的主要用途是创建API,一个典型的例子是Django ORM,下面是源码链接。
3.需要注意的是,Python2和Python3中的用法形式不同,这点可以认真看看原文和上面的源代码例子。
错误、调试和测试
错误处理
1.Python使用try...except...finally...
进行错误处理。需要注意的是如果在except后又else语句,若没有错误则会执行else的语句,反之则不执行。
2.记录错误,Python内置的logging模块可以非常容易地记录错误信息。通过配置,logging可以把错误记录到日志文件里,方便事后排查。
3.抛出错误,捕获错误目的只是记录一下,便于后续追踪。但是,由于当前函数不知道应该怎么处理该错误,所以,最恰当的方式是继续往上抛,让顶层调用者去处理。Python中使用raise语把当前错误抛出。
调试
1.直接用print()
查看结果调试,麻烦且效率不高。
2.使用断言(assert)如果断言失败,assert语句本身就会抛出AssertionError
1 | # 使用断言 |
3.把print()
替换为logging是第3种方式,和assert比,logging不会抛出错误,而且可以输出到文件。
4.第4种方法是使用pdb,启动Python的调试器pdb,让程序以单步方式运行,可以随时查看运行状态。
- 命令行模式下运行某py文件
python -m pdb xxx.py
- 以参数
-m pdb
启动后,pdb定位到下一步要执行的代码,有->
指示,可输入命令l
来查看源码 - 输入命令
n
可以单步执行代码 - 任何时候都可以输入命令
p
变量名来查看变量值,如查看变量s
,则为p s
- 输入命令
q
结束调试,退出程序。
5.方法太笨重了些,可以改进:使用pdb.set_trace()
- 这个方法也是用pdb,但是不需要单步执行,只需要
import pdb
,然后,在可能出错的地方放一个pdb.set_trace()
,就可以设置一个断点。 - 运行代码,程序会自动在
pdb.set_trace()
暂停并进入pdb调试环境,可以用命令p
查看变量,或者用命令c
继续运行
6.使用IDE,效率较高,如Visual Studio Code,pycharm等。
单元测试
1.单元测试是用来对一个模块、一个函数或者一个类来进行正确性检验的测试工作。
2.为了编写单元测试,需要引入Python自带的unittest
模块,编写单元测试时,需要编写一个测试类,从unittest.TestCase
继承。
3.以test
开头的方法就是测试方法,不以test
开头的方法不被认为是测试方法,测试的时候不会被执行,对每一类测试都需要编写一个test_xxx()
方法。
4.unittest.TestCase
提供了很多内置的条件判断,只需要调用这些方法就可以断言输出是否是所期望的。最常用的断言就是assertEqual()
:
1 | self.assertEqual(abs(-1), 1) # 断言函数返回的结果与1相等 |
另一种重要的断言就是期待抛出指定类型的Error,如KeyError,AttributeError等。
5.运行
- 方法一:加上两行代码,然后正常运行。
1 | if __name__ == '__main__': |
- 方法二:在命令行通过参数
-m unittest
直接运行单元测试,如python -m unittest py文件名
6.可以在单元测试中编写两个特殊的setUp()
和tearDown()
方法。这两个方法会分别在每调用一个测试方法的前后分别被执行。
文档测试
1.Python内置的“文档测试”doctest
模块可以直接提取注释中的代码并执行测试。
2.doctest
严格按照Python交互式命令行的输入和输出来判断测试结果是否正确。只有测试异常的时候,可以用...
表示中间一大段烦人的输出。
3.doctest
非常有用,不但可以用来测试,还可以直接作为示例代码。通过某些文档生成工具,就可以自动把包含doctest
的注释提取出来。用户看文档的时候,同时也看到了doctest
。
4.一个例子,对函数fact(n)
编写doctest并执行
1 | def fact(n): |
5.什么输出也没有。这说明编写的doctest运行都是正确的。如果程序有问题,比如fact(10)下的3628800去掉,再运行就会报错。
IO编程
1.IO编程中,Stream(流)是一个很重要的概念,可以把流想象成一个水管,数据就是水管里的水,但是只能单向流动。
- Input Stream就是数据从外面(磁盘、网络)流进内存
- Output Stream就是数据从内存流到外面去。
对于浏览网页来说,浏览器和服务器之间至少需要建立两根水管,才可以既能发数据,又能收数据。
2.由于CPU和内存的速度远远高于外设的速度,所以,在IO编程中,就存在速度严重不匹配的问题,于是出现了两种模式:
- 同步IO
- 异步IO
3.操作IO的能力都是由操作系统提供的,每一种编程语言都会把操作系统提供的低级C接口封装起来方便使用,Python也不例外。
文件读写
1.Python中有open(),write(),read()
等函数用于文件读写,但用完后得close()
2.可以直接使用with open() as f
自动调用close()方法简洁高效。
3.关于读文件内容常用的几个方法:
read()
会一次性读取文件的全部内容,如果文件太大,会出错,所以,要保险起见,可以反复调用read(size)
方法,每次最多读取size
个字节的内容。readline()
可以每次读取一行内容readlines()
一次读取所有内容并按行返回list
4.file-like Object
像open()
函数返回的这种有个read()
方法的对象,在Python中统称为file-like Object
。除了file外,还可以是内存的字节流,网络流,自定义流等等。file-like Object
不要求从特定类继承,只要写个read()
方法就行。StringIO
就是在内存中创建的file-like Object
,常用作临时缓冲。
5.一些例子
1 | # 写 |
6.所有模式的定义及含义可以参考Python的官方文档。
StringIO和BytesIO
1.StringIO顾名思义就是在内存中读写str。
- 要把str写入StringIO,需要先创建一个StringIO,然后,像文件一样写入即可:
1 | from io import StringIO |
- 也可以用一个str初始化StringIO,然后,像读文件一样读取
1 | f = StringIO('abc') |
2.StringIO操作的只能是str,如果要操作二进制数据,就需要使用BytesIO。
- BytesIO实现了在内存中读写bytes,创建一个BytesIO,然后写入一些bytes:
1 | f = BytesIO() |
- 和StringIO类似,可以用一个bytes初始化BytesIO,然后,像读文件一样读取
1 | f = BytesIO(b'abc') |
3.StringIO和BytesIO是在内存中操作str和bytes的方法,使得和读写文件具有一致的接口。
操作文件和目录
1.Python的os
模块封装了操作系统的目录和文件操作,注意这些函数有的在os
模块中,有的在os.path
模块中。
2.记的话,有些难,用时看文档:操作系统接口模块
3.几个小例子:
1 | # 列出当前目录下的所有目录: |
序列化
1.把变量从内存中变成可存储或传输的过程称之为序列化,在Python中叫pickling,序列化之后,就可以把序列化后的内容写入磁盘,或者通过网络传输到别的机器上。反过来,把变量内容从序列化的对象重新读到内存里称之为反序列化,即unpickling。
2.Python提供了pickle模块来实现序列化,相关操作如下:
1 | import pickle |
3.JSON
- Pickle的问题和所有其他编程语言特有的序列化问题一样,就是它只能用于Python,而JSON对象序列化是标准格式,可以被所有语言读取,也可以方便地存储到磁盘或者通过网络传输。
- Python内置的json模块提供了非常完善的Python对象到JSON格式的转换。
- 使用方法:和pickle使用方法类似,如下:
1 | import json |
- 序列化对象:
1 | import json |