Python 基础笔记
了解
Python语言的概念
Python是一门强大的多范式的计算机编程语言,它以程序员的多产、代码的可读性、软件的高质量开发为目标而不断优化
Python着眼于开发者的生产效率以及软件质量(战略性优势)
2.x版本和3.x版本的选择
选用3的情况:新的特性,演化,越来越好
选用2的情况:已有代码,稳定性,做出改动的代价大
同时选用2和3:版本中立的代码
总结:如果你不需要去维护旧的2.x的代码,而且自己具有一定的自学能力,那么最新的python版本就是最好的python版本
为什么使用Python
软件质量
Python更注重可读性、一致性、软件质量,Python代码在设计之初就具有良好的可读性,因此具备了比传统语言更优秀的可重用性和可维护性
从设计之初,Python就秉承了一种独特的简洁而极具可读性的语法,以及一种高度一致的编程模型
Python是一个设计风格始终如一的开发平台,可以保证开发出具有一致性的代码
Python采取了一种所谓极简主义的设计理念,这意味着尽管实现某一编程任务通常有多种方法,往往只有一种方法是显而易见的(明确胜于隐晦,简单胜于复杂)
开发者生产效率
Python的开发者效率比其他语言提高了数倍,Python的代码更短,调试更少,更易于维护,解释型语言可以立即运行
Python致力于开发速度的最优化
程序的可移植性
绝大多数的Python程序不作任何改变即可在所有主流计算机平台上运行
标准库的支持
Python内置了众多构建并可移植的功能模块,称为标准库,涵盖了很多方面
组件集成
Python可以通过多种集成机制轻松地与应用程序的其他部分进行通信,这种集成使Python称为实现产品定制和扩展的工具
Python可以调用各种语言各种软件的功能,Python绝不仅仅是一个孤立的工具
与其他语言相比
- 开源,Python开源的本质意味着它不可能被某一个公司所掌控
- 比C、C++、Java、C Sharp 更简单,更易于使用
- 比 Perl、Ruby 更具有可读性,语法简洁连贯
- 比PHP用途更广,更具有跨平台性
Python是不是“脚本语言”?
Python是一门通用型的编程语言,而它时常扮演脚本语言的角色
何谓脚本,其实并没有官方的定义
一般来说,我们把描述简单的顶层代码文件叫做 脚本,而那些相对复杂的多文件应用叫做 程序
python也常常用来替代Shell作为操作系统的工具
可以直接运行的文件往往也叫做脚本(一个顶层程序文件的非正式说法)
import this
运行这个导入,会打印出一个“菜单”,即python的设计理念
Python的缺点是什么
Python的执行速度还不够快
目前Python的标准实现方式是将源代码的语句编译(翻译)为字节码(byte code)的形式,之后再将字节码解释出来
由于字节码是一种与平台无关的格式,因而它具有可移植性
因为python并没有将代码编译为底层的二进制代码,所以一些python程序会比C这样的完全编译型语言要慢
但是记住:
- 程序的类型决定了是否需要关注程序的执行速度
- 开发速度带来的效益往往比执行速度带来的损失更为重要
Python的特性
易于学习
Python编码简单,运行简单,部署也简单
并且提供了众多编译语言才有的高级软件工程工具
Python曾经被称为“可以执行的伪代码”
面向对象和函数式
从根本上讲,Python是一种面向对象的语言。
但OOP只是Python的一种选择而已,Python既支持面向对象编程也支持面向过程编程的模式。
动态类型
Python在程序运行的过程中跟踪对象的类型,不需要代码中进行复杂的类型和大小的声明
Python中没有类型或变量声明这种做法,因为Python代码不约束数据的类型,它往往自动地应用了一种广义上的对象
自动内存管理
Python自动为对象分配空间,并且当对象不再使用时将自动撤销空间(“垃圾回收”),当需要时自动扩展或收缩,
即Python能够帮你完成底层的内存管理
在标准Python中(也叫CPython),一旦一个对象的最后一次引用被移除,空间将会立即回收
当需要是自动扩展或收缩
即:Python能够帮助你完成底层的内存管理
内置对象类型、内置工具、标准库
列表、元组、字典、集合
模块、类、异常
以及众多的标准库
Python预置的众多工具可以让我们编程更加轻松
超级丰富的第三方库
互联网上的各个都是人才!
可混合编程(“胶水语言”)
python可以调用许多语言的功能,也可以被许多语言去调用(本质在于python的解释器可以被其他语言实现)
这让python的功能丰富了很多,在python解决不了的时候,可以将其他语言的功能拿过来使用
Python的应用现状
暂略
Python命名的由来
英国喜剧组“Monty Python”是python命名的由来,python的作者是他们的粉丝
Python是工程,不是艺术
对比Perl,你可以用Python做到一切用Perl能做到的事情,但是,做好之后,还可以阅读自己的程序代码
Python更专注于产生可读性的代码。就大多数人而言,Python强化了可读性,转换为了代码的可重用性和可维护性,使得Python更适合用于那些不是写一次就丢掉的程序
Perl程序代码很容易写,但是可能会很难读
Python出自训练有素的数学家之手,他似乎自然而然地创造出来一门具有高度一致性和连续性的正交语言。
Perl语言由一位语言学家孕育,他创建了一种接近自然语言的编程工具,并拥有着上下文敏感性和广泛变化性。
就像著名的Perl所说的格言:完成的方法不止一种。有了这种思维,Perl语言及其用户社群在编写代码时,已经历史性地被鼓励脱缰式的表达式自由化,一个人的Perl代码可能和另一个人的完全不同。事实上,编写独特、充满技巧性的代码,常常是Perl用户引以为豪的事。
表达式自由度会造成维护的噩梦
人们在绘画或者雕塑时,他们主要是为了自己做,其他人日后去修改他们的作品的可能性很低。这是艺术和工程之间关键的差异。
当人们在编写软件时,他们不是为了自己写。事实上,他们甚至不是专门为计算机编写的。
实际上,优秀的程序员都知道,代码是为了下一个阅读它而进行维护或重用的人编写的。如果那个人无法理解代码,在现实的开发场景中,就毫无用处了。
换句话说,编程并不事关聪明与深奥,它的关键是让你的程序能够清晰地表达它的意图。
Python的语法模型几乎会迫使用户编写具有可读性的代码。
此外,Python强调了诸如有限互动、代码统一性,以及特征一致性等理念,它会更进一步促进首次编写后能够长期使用的代码。
编码质量其实是脆弱的,与其依赖与技术,不如更多地依靠人
Python 程序执行过程(解释器)
解释器简介
从Python目前的实现来讲,Python也是一个名为解释器的软件包,解释器是一种让其他程序运行起来的程序。
你写了Python的代码,要运行,就必须找一个Python的解释器
Python解释器是一个程序或软件,可以让Python的代码运行起来(就是你去下载安装的Python)
具体运行一个程序的流程是这样的:
- 你写了一段Python代码
- Python的解释器读取这段代码
- Python解释器翻译你写的代码,变成计算机可以执行的二进制代码
- Python解释器将翻译后的二进制代码交给计算机执行
实际上,解释器是代码与机器的计算机硬件之间的软件逻辑层
当Python安装在电脑上之后,它会生成一些组件:至少包括一个解释器、一套支持库
Python解释器可能采取可执行程序的形式,或是作为链接到另一个程序的一系列支持库
根据实现Python解释器的方式的不同,具体情况会有所不同。目前最常用的是C语言实现的Python解释器,即 “CPython”
无论采用何种形式,编写的Python代码必须在解释器中运行,也就是要先安装Python解释器才能运行Python代码
程序如何执行
程序员的视角
考虑最简单的形式,一个Python程序是一个包含Python语句的文本文件(后缀名 .py 是可选的,从技术上来讲,.py后缀名只要在被“导入”时才是必须的)
当你把Python语句输入到文本文件之后,你必须告诉Python解释器来执行这个文件,也就是解释这个文件中的Python代码(你不说要执行,Python解释器是不会自动帮你执行的哈)
告诉解释器执行一般有两种方式:
- 以 Python.exe 的打开方式打开运行这个文本文件
- 在命令行使用 python 文件名 的方式来解释这个文件中的代码
Python的视角
除了,编写代码,交给解释器,解释器执行代码,这个过程之外,透过表面,在Python内部还有一些事情发生
对Python运行时结构有一些基本的了解,可以帮助你从宏观上把握程序的执行
当Python运行脚本时,在代码开始执行之前,Python还会执行一些步骤:
- 编译代码为所谓的“字节码”
- 将“字节码”转发到所谓的“虚拟机”中
字节码编译
执行程序时,Python内部会先将源代码编译成字节码的形式,这里的编译只是一个简单的翻译步骤
字节码:一种低级的、与平台无关的表现形式
即:Python通过把你的代码每一句话分解为单一步骤的一组字节码指令
字节码可以提高执行速度,比直接翻译原始的代码要快得多
.pyc 文件,就是编译过的源代码
如果Python进程在机器上拥有写入权限,那么它会把程序的字节码保存为一个 .pyc 为扩展名的文件,下一次运行程序时,如果上次保存字节码之后没有修改过源代码,并且运行使用的是同一个Python解释器版本,那么Python将会加载.pyc文件并且跳过编译字节码这个步骤
- python检查源文件是否有改变:通过源文件和字节码文件最后一次修改的时间戳,确认它是否必须重新编译,如果修改了源代码,那么下次运行时,字节码将自动重新创建
- Python的版本:导入机制同时检查是否需要因为使用了不同的Python版本而重新编译,这些版本信息在3.2之前保存在字节码文件中,3.2之后保存在字节码文件名中
如果python无法在机器中写入字节码,那么字节码将会在内存中生成,并且在程序结束时被丢弃
字节码只会针对被导入的文件而生成,而不是顶层的执行脚本,严格来说,字节码是一种对导入的优化
文件仅在程序运行时才会被导入
在交互式命令行中输入的命令不会生成字节码
Python虚拟机 PVM
PVM就是迭代运行字节码指令的一个大循环,一个接一个完成操作
PVM 是 Python 的运行时引擎,时常表现为 Python 系统的一部分,并且是实际运行脚本的组件,从技术上讲,它是 Python 解释器的最后一步
PVM 循环仍需要解释字节码!内部仍有编译的步骤
最终导致的效果就是,纯 Python 代码的运行速度介于传统的编译语言和传统的解释语言之间
Python 中真正拥有的只有运行时:完全不需要初始的编译阶段,所有的事情都是在程序运行时发生的
甚至包括建立函数和类的操作以及模块之间的链接(这在静态语言中往往是在执行之前发生的,而在 Python 中是与程序的执行同时进行的)
Python的各种实现
CPython:标准的Python
Jython:基于Java的Python
IronPython:基于 .NET 的Python
Stackless:注重并发的Python
PyPy:注重速度的Python
执行优化工具
Cython:Python和C的混合体
Shed Skin:Python带C++的转换器
Psyco:原先的即时编译器
冻结二进制文件(转换为可以执行的文件)
运行程序的方式
进入和退出交互式命令行
在系统命令行下输入python,即可进入交互式命令行模式(如果不在python目录则需要先配置环境变量)
退出交互式命令行:
- windows 中,
Ctrl + Z
- UNIX 中,
Ctrl + D
PATH 和启动器
在命令行中运行程序
python 代码文件
也可以将输出定向到另一个文件中:python 代码文件 > a.txt
在比较新的 windows 版本上,我们也可以仅仅输入脚本的名字,由于新的 windows 系统使用注册表(文件名关联)找到用哪个程序来运行一个文件,所以我们不需要在命令行上显式地使用 python 来运行一个 .py
文件
指定解释器
在 Python 脚本的第一行,可以用一种特殊的注释来指定使用某个解释器运行这个脚本
#!/...../python
这样,就可以直接运行这个文件了(如果没有可执行权限,需要使用 chmod + x
来赋予权限)
UNIX env 查找技巧
自动寻找Python解释器的路径
env 程序可以通过系统的搜索路径的设置定位 Python 解释器,这种方法可以使代码具有可移植性
#!/usr/bin/env python
为什么会使用交互式命令行模式
代码立即执行,便于实验和测试一些语法特性
这种体验方式不容易带来破坏,要进行真正的破坏,必须显式地导入模块,直接的Python代码总是可以安全运行的
交互式命令行使用
- 只能输入 Python 命令
- 不需要 print 语句,直接输入变量然后回车就可以打印出其值
- 在交互式命令行模式下不需要缩进(目前还不需要)
- 复合语句下(由于不需要缩进),提示符会有变化 ...
- 交互式命令行模式中,用一个空行结束复合语句
- 交互式命令行一次运行一条语句
- 不能在交互式命令行模式中复制并粘贴多行代码,除非这段代码每条复合语句的后面都包含空行!
模块导入与重载
导入同时也是一种启动程序的方法
每一个以扩展名 .py
结尾的 Python 源代码文件都是一个模块
其他文件可以通过导入一个模块读取这个模块定义的内容---导入操作从本质上来讲就是载入另一个文件,并给予读取那个文件内容的权限
一个模块的内容通过其属性从而被外部世界使用
Python 程序架构的一个核心思想,更大的程序往往以多个模块文件的形式出现,并且从其他模块文件导入工具
其中的一个模块文件指定文主文件,或叫做顶层文件,或“脚本”(一个顶层程序文件的非正式说法),也就是启动之后能运行整个程序的文件,逐行运行
在这一层之下,全部是模块导入模块
导入操作运行文件中的代码,这个文件作为最后一步正在被加载,正因为如此,导入文件是另一种运行文件的方法
每次会话中,只有第一次导入时会运行模块中的代码,后面的导入不会反映出模块的修改(后面的导入都不起作用),因为导入是一个开销很大的操作
from 语句直接从一个模块中复制出一个名字,属性成为了接收者的直接变量
from 复制出的名字会产生覆盖的问题,并且覆盖的时候不会发出警告!
使用reload函数:
from imp import reload
# 在使用reload重新载入模块之前,需要先成功导入模块
import xxx
reload(xxx)
relaod 函数载入并运行了文件当前版本的代码,如果模块已经修改,那么 reload 会反映出修改的变化
reload 函数必须在成功导入之后才能使用(即 reload 之前,请确保已经成功地导入了这个模块)
reload 是一个函数,import 是一个语句
reload 的返回值:<module 模块名 from 模块路径 >
加载的模块
用一个 from 载入的名字不会通过一个 reload 直接更新,但是用一条 import 语句访问的名字会
reload 不可传递,重载一个模块文件不会重载模块所导入的任何模块,所以有时候必须重载多个文件
Python2.x中,reload可以作为内置函数使用,因此不需要导入
模块的属性:模块可以看做是一个属性(绑定在特定对象上的变量名)的包(命名空间)
导入操作将会在最后一步执行文件,导入者得到了模块文件中在顶层所定义的所有变量名的访问权限
dir(模块名)
:返回一个包含模块内部全部属性的列表
模块中通常会有一些预置的变量名:通常是 __xxx__
形式,这些变量名对于解释器来说有特殊的意义
模块也是 Python 程序最大的程序结构,Python 程序往往由多个模块文件构成,通过 import 语句连接在一起
每个模块都是自包含的命名空间,一个模块文件不能看到其它文件中定义的变量名,除非显式导入了那个文件
模块文件在代码文件中起到了最小化命名冲突的作用,因为每个文件都是一个独立完备的命名空间,即使拼写相同,一个文件的变量名也不会和另一个文件的变量名冲突
使用exec运行模块文件:exec(open('xxx').read())
exec 调用有着类似 import 的效果,但它并不会真的导入模块,它的操作就好像把模块中的代码 “粘贴” 进来一样
注意这种操作有可能默默地覆盖掉正在使用的变量,而且不会发出提示
Python自带的IDLE环境
IDLE 是 Python 自带的一个 IDE,用来做 Python开发的图形界面程序,可以编辑、运行、浏览、调试 Python程序
IDLE 使用Python内置的 TKinter库进行开发,所以 IDLE也是可移植的
IDLE 只是标准库中模块搜索路径上的一个Python脚本,所以也可以通过Python命令来运行它
python -m idlelib.idle
IDLE 的常用技巧:
- 前一条:Alt + P(Mac:Ctrl + P)
- 后一条:Alt + N(Mac:Ctrl + N)
注意:
- Tkinter GUI 和 线程程序 可能并不适合用 IDLE 来开发,因为IDLE本身使用tkinter开发的,如果用它来开发tkinter程序,可能会没有响应
- 如果发生了连接错误,可以尝试使用单进程模式启动IDLE
基本准则
- 访问不存在的东西,是一种错误
Python对象类型概述
在Python中,数据以对象的形式出现
从本质上看,对象无非就是内存的一部分,包含数值和相关操作的集合
Python脚本中的一切都是对象
Python的程序结构
Python 的程序可以分解为 模块、语句、表达式、对象
- 程序由模块组成
- 模块包含语句
- 语句包含表达式
- 表达式创建并处理对象
为什么要使用内置类型
懒,而且没有必要重复实现常用的类型
- 内置对象使程序更容易编写,不必从零开始
- 内置对象是可以进一步扩展的组件
- 内置对象往往比自己写的数据结构更有效率
- 内置对象是语言标准的一部分
内置函数 type(x)
内置函数type接收一个对象,返回一个type对象,这个对象能告知其他对象的类型
在python3.x中,类型与类已经合并起来了
Python的核心数据类型
数字、字符串、列表、字典、元组、文件、集合
类型、None、布尔
函数、模块、类
已编译的代码、调用栈跟踪
字面量的解释:
- python中,字面量一般理解为那些产生Python对象的表达式,有时候也称为常量
- 注意:常量一次并不意味着对象或者变量具有不可变性
python中没有类型声明,运行的表达式的语法决定了创建和使用的对象的类型
一旦创建了一个对象,它就和操作集合绑定了
数字
数字类型包括:整数、浮点数、复数、固定精度的十进制数、有理分数、等等
python3.x的整数类型会在需要的时候自动提供额外的精度(而在python2.x中,有一个处理大数值的单独的长整型)
字符串
字符串属于序列类型
序列中的元素包含了一个从左到右的顺序,序列中的元素根据它们的相对位置进行存储和读取
序列:一个包含其他对象的有序集合
字符串:有单个字符的字符串所组成的序列
序列的长度:内置函数 len(S)
可以获取序列的长度
fan序列的索引和切片
索引操作:S[index]
,通过索引可以取出序列中的元素
索引的计算规则:
- 从左到右:
0, 1, 2, 3, ......, len(S)-1
- 从右到左:
-1, -2, -3, ......, -len(S)
即:[len(S)+k]
如果k是负数
切片操作:S[start:end:step]
- 从start开始,到end位置,以step为间隔取值,组成一个新的序列
- 包含start,不包含end
step>0
从左到右,step<0
则从右到左
注:切片返回新的对象
序列的操作
序列的运算:
- 加法:序列连接
- 乘法:序列和整数相乘,表示序列重复
- 这里的加法和乘法并不是数学意义上的功能,体现了对象的多态性(即:操作的意义取决于被操作的对象)
in:判断元素是否存在
not in:判断元素不存在(注意是 not in,不是 in not)
其他:
len(s)
:获取序列的长度
max(s)
:序列中的最大值
min(s)
:序列中的最小值
range(start, end, step)
:生成start到end的数字,不包括end,步长为step
enumerate()
:将一个可遍历的数据对象组合为一个索引序列,同时列出数据和数据下标
sorted()
:排序,cpm比较函数,key用来比较的元素,reverse:排序规则(False升序,True降序)
all()
:都为True才返回True(空元组、空列表返回的是True)
any()
:有一个True就返回True
reverse()
:翻转,没有返回值,对列表元素本身进行翻转
zip()
:接收一个或多个可迭代对象,将对应的元素打包成一个元组,如果元素个数不一致,则以最短的长度为准,就算只有一个序列,也会把一个元素压缩成元组
字符串的不可变性
Python中的每一个对象都可以归为:可变的、不可变的
在核心类型中:数字、字符串、元组 等是不可变的,字典、列表、集合等是可变的
在原位置改变文本的数据,
- 一种方法是将它扩展为一个有独立字符构成的列表,然后将字符重新拼接起来。
- 另一种方法是使用 bytearray 类型(bytearray类型支持文本的原位置转换,但仅仅适用于字符编码至多8位宽(如ASCII)的文本,其他的所有字符串依然是不可变的)
字符串的方法学习
dir(S)
:列出了在调用者的作用域内,形式参数的默认值,返回了一个包含对象的所有属性的列表
help(S.方法名)
:查询具体方法的帮助信息
这里的S可以是实际的对象,也可以是 str 这样的类型
和字符串编码有关的内置函数
ascii(x)
:返回一个可打印的对象字符串表示方式,如果是非ascii字符则以 \x,\u等表示bin(x)
:将一个整数转换为二进制字符串ord(x)
:返回Unicode字符对应的整数(ascii、unicode)oct(x)
:整数转换为八进制字符串chr(x)
:返回数值对应的字符(ascii、unicode)
Unicode字符串
Python3.x的str类型可以处理Unicode字符串,并且使用一种独特的bytes字符串类型表示原始字节值
Unicode字符串能够分辨数字和字符,但在编码到文件或存储到内存时不一定要将文本字符映射成单字节,事实上,字节的概念并不适用于Unicode(一些Unicode码的一个字符大于一个字节,而且即使是7位的ascii字符,也不是存储为一个字符一字节的形式)
python3.x有一个严格的模型,禁止在没有显式转型的情况下,将普通字符串和字节串混合使用
- 存入文件:字节
- 读入内存:字符
文本在存入文件时被编码成字节,在读入内存时被解码成字符
列表
列表也是一个序列
列表是一个任意类型对象的位置相关的集合,没有固定的大小(尽管没有固定的大小,但仍然不允许引用不存在的元素)
列表是可
列表支持任意的嵌套
可以通过列表推导创建列表,如:[i for i in range(100)]
列表的格式:[x1, x2, x3, x4, x5]
字典
字典不是一种序列,而是一种映射
映射也是其他对象的集合,映射通过键(key)来存储对象
映射没有维护任何可靠的从左到右的顺序,只是简单地把键映射到相应的值上
对一个不存在的键赋值,会创建这个键
数值类型
在python中,数字并不是一种实际的对象类型,而是一些相似类型的分组
数值字面量
整数
整数直接写成十进制数字的串
Python2中的整数有两种类型,一般整数(32位)和长整数(无限精度),并且一个整数可以以 l 或 L 结尾,从而强制它转换为长整数。由于当整数的值超过为其分配的位数的时候会自动转换为长整数,所以一般不需要手动输入 L
Python3中整数变为了一种单独的类型,一般整数和长整数合二为一了,自动支持无限的精度(内存允许的情况下)
可以在整数字面量中间添加一些下划线,方便整数的阅读,而不影响这个字面量的值
a = 12_456_423
十六进制整数字面量:以 0x 或 0X 开头
八进制整数字面量:以 0o 或 0O 开头
二进制整数字面量:以 0b 或 0B 开头
浮点数
浮点数带一个小数点,也可以使用 e 或 E 进行科学计数法的表示
浮点数在标准CPython实现中是采用C语言的双精度来实现,因此其精度与用来构建Python解释器的C编译器所给定的双精度一样
复数
复数写成 实部 + 虚部,python中的虚部默认以 j 或 J 结尾
虚部可以独立于实部单独存在
混合基本数值类型的表达式 与 向上转换
如果一个表达式中有多种数值类型,Python会首先将被操作的对象转换成其中最复杂的操作数的类型,然后再对相同类型的操作数进行运算
整数比浮点数简单,浮点数比复数简单
这种混合类型的转换只适用于数值类型
一般来说,Python不会再其他的类型之间进行转换
非数值类型的比较
Python3不允许非数值类型的比较,会引发异常
Python2中,非数值类型也可以被比较,但是不会执行任何的转换(比较的是对象类型名称的字符串)
除法
Python3中的 /
是真除法,
python2和Python3中, //
是向下取整除法
Python2中的 /
是经典除法,会省去小数部分,对浮点数会保持余项(小数部分)
向下取整与截断
//
有一个别名,叫做截断除法,也叫做向下取整除法
在执行除法操作的时候,其实只是截断了正的结果,所以对于正数,截断除法和向下取整除法是相同的
对于负数,触发操作就是一个向下取整的结果
整数的精度
Python2中有一个专门用于长整数的类型,并且会自动把任何太长了的整数转变为这种长整数,而Python3中自动支持了无限精度的整数
由于Python需要为支持扩展精度而进行额外的工作,因此大精度的运算通常要比正常的整数运算更慢
当你要求精度的时候,你应该庆幸python内置了长整数的支持,而不是抱怨性能上的不足
进制字面量的转换
Python默认使用十进制显示整数数值,并且提供了内置函数将整数转换为其他进制的数字字符串
- oct:将十进制数转换为八进制字面量字符串(带前缀0o)
- hex:将十进制数转换为十六进制字面量字符串(带前缀0x)
- bin:将十进制数转换为二进制字面量字符串(带前缀0b)
按位操作整数
复数
内置函数 pow、abs
pow:用于计算幂
abs:用于计算绝对值,可以用于复数
字符串基础
字符串对象概述:"字符串"
字符串可以用来表示能够编码为文本或者字节的任何事物
字符串的概念
Unicode简介
在Python3.x中,有三种字符串类型:
- str 用于 Unicode 文本(包括 ASCII)
- bytes 用于二进制数据(包括已编码的文本)
- bytearray 是bytes的一个可修改的变体
文件在两种模式下工作:
- 文本:将内容表示为str类型并实现Unicode编码
- 二进制:以原始的bytes形式处理,且不做任何数据转换
在Python2.x中,unicode字符串表示Unicode文本,str字符串同时处理8位普通文本和二进制数据
bytearray从3.x向后移植,python2.6以后的2.x版本中可用
普通文件内容是由str直接表示的字节,但是codecs模块打开Unicode文件,处理编码,并将内容作为unicode对象表示
在Unicode世界中,“字节”没有任何意义
更为严格地讲,在Unicode语境下,字符串内容和长度都反应了码点(识别数字)值;无论在文件中编码或在内存中存储,Unicode中的单个字符不一定直接映射为单个字节。一一映射对于简单的7位ASCII文本可能有效,但即便是ASCII也依赖于外部编码类型和内部使用的存储方案,这取决于Python是如何为它们分配空间的
实际上,Python3.x形式上将str字符串定义为Unicode码点序列,而不是字节序列,以明确这一点。
为了安全起见,请在字符串中思考字符而不是字节
字符串字面量
一对单引号、一对双引号:可以表示普通的字符串
一对三引号(三个单引号或三个双引号):表示的字符串内部可以直接换行,即跨行表示。三引号字符串也叫 块字符串
两个字符串字面量直接放在一起,中间不加其他东西,会自动拼接为一个新的字符串
转义序列(略)
\xhh
:十六进制hh的字符(两位)
\ooo
:八进制ooo的字符(三位)
\N{id}
:Unicode数据库ID
\uhhhh
:16位十六进制值的Unicode字符
\uhhhhhhhh
:32位十六进制值的Unicode字符
注:不管如何指定不可打印字符,Python都会以十六进制显示
字符串的结束?
Python中,如果使用了转义字符的空字符(\0
),并不会像C语言一样会结束一个字符串
Python在内存中保存整个字符串的长度和文本
Python中没有任何一个字符会结束一个字符串
不管如何指定不可打印字符,Python都以十六进制显示
byetarray类型
Python3.0和Python2.6后引入的新字符串类型,它是可变的,因此可以原位置修改其值
bytearray对象并不是真正的文本字符串,它们是8位小整数的序列
然而,它们支持和普通字符串相同的大多数操作,并且显示的时候打印为ASCII字符
相应地,他们为必须频繁修改的大量简单8位文本提供了另一个选项(Unicode文本的更为丰富的类型隐含不同的技巧)
......
字符串的前缀
r:原始字符串
r前缀,或者 R前缀
阻止转义:原始字符串中的转义字符不会生效
注意:即使是原始字符串也不能以奇数个反斜杠结束(同样会转义结尾的引号)
如果确实需要一个反斜杠结束,那么可以分成两个字符串写,如 r'xxxxxx' '\\'
b:字节串
u:2.x和3.3+的Unicode字符串
f:模板字符串
字符串与运算符
字符串 + 字符串:拼接操作
字符串 * 整数:重复操作
字符串格式化
%:字符串格式化表达式
- 在 % 的左侧放一个需要进行格式化的字符串,这个字符串带有一个或多个内嵌的转换目标,以 % 开头
- 在 % 的右侧放一个对象或多个对象(内嵌在元组中),这些多项将会插入到你想让Python进行格式化的左侧的字符串中,并替换那些转换目标
转换目标的类型码:
s
:字符串r
:与s相同,但是使用 repr,而不是 strc
:字符(int或str)d
:十进制整数(以10为底)i
:整数u
:与d相同(已废弃,不再是无符号整数)o
:八进制整数(以8为底)x
:十六进制整数(以16为底)X
:与x相同,但是使用大写字母e
:带有指数的浮点数,小写E
:与e相同,但是使用大写字母f
:十进制浮点数F
:与f相同,但是使用大写字母g
:浮点数 e 或 fG
:浮点数 E 或 F%
:%字符
转换目标的写法:
%[(keyname)][flags][width][.precision]typecode
%[(键名)][标志][域宽].[精度]类型
- flag:
-
表示左对齐,+
表示显示数值的符号,0
表示零填充 - width 和 percision 都可以写成一个
*
,以指定它们应该从表达式右侧的输入值中下一项取值
基于字典的格式化表达式:
'$(key1)d ----- %(key2)s' % {'key1': 123, 'key2': 'HelloWorld'}
s.format 格式化
替代目标写法:它的四部分都是可选的,中间必须不带有空格,下面写了空格只是为了区分开四部分
{filedname component !conversionflag :formatspec}
- filedname:一个可选的数字或关键字
- component:大于等于零个
.name
或[index]
引用的字符串,它可以被省略以使用完整的参数值。其中的引用用来获取参数的属性或索引值 - conversionflag:如果出现则以
!
开始,后面跟着r
、s
、a
的一个,分别表示在这个值上调用repr
、str
、ascii
- formatspec:如果出现则以
:
开始,后面跟着文本,指定了如何表示该值,包括字段宽度、对齐方式、补零、小数精度等细节,并且以一个可选的数据类型码结束
formatspec的形式:[[fill]algin][sign][#][0][width][,][.precision][typecode]
- fill 可以是除了大括号以外的任意填充字符
- align 可以是:
<
(左对齐)>
(右对齐)^
(居中对齐)=
(符号字符后的填充) - sign 可以是 +、-、空格
......
内置函数 format:对象格式化为字符串
索引
字符串是一种序列,所以字符串可以用序列的索引方式( "f序列的索引和切片")
从左到右:0 到 字符串长度-1
从右到左:-1 到 -字符串长度
索引如果为负数,就将负数与字符串长度相加,就得到了对应的正数索引值
分片(切片)
S[i:j]
:获取序列中的连续部分,从 i
到 j
(不包括 j
)
如果省略 i
,则默认从 0 开始
如果省略 j
,则默认到最后,j=len(S)
S[i:j:k]
:另外接收一个步长(步幅)k
,默认值为 +1
允许跳过元素和反转顺序(k
为负数时)
注:切片创建新的对象
分片对象 slice
分片的冒号写法只是一个语法糖,实际上直接传入一个分片对象也是可以的
slice 函数接收三个参数,分别对应了分片的三个数字,返回一个和分片一样的参数
统计
获取字符串的长度(字符数量):len(s)
count
统计子字符串的个数:s.count(sub, start=None, end=None)
查找
find、rfind
index、rindex
判断
存在性判断
in
、not in
:判断一个字符串是否在另一个字符串中包含
字符串类别判断
s.isalnum()
:是否是字母或数字
s.isalpha()
:是否是字母
s.isascii()
:是否是ASCII字符
s.isdecimal()
:
s.isdigit()
:
s.isnumeric()
:
s.isidentifier()
:
s.isprintable()
:是否是可打印字符
s.isspace()
:是否是空字符
s.istitle()
:是否是 title 字符串
s.isupper()
:是否是大写字符串
s.islower()
:是否是小写字符串
字符串组成判断
s.startswith(self, prefix, start=None, end=None)
:判断是否以某个字符串开头
s.endswith(self, suffix, start=None, end=None)
:判断是否以某个字符串结尾
转换
字符串代码转换
ord(s)
:将字符转换为编码
chr(n)
:将编码转换为字符
大小写转换
s.upper()
:将字符串中的字母转换为大写
s.lower()
:将字符串中的字母转换为小写
s.title()
:将字符串中字母大小写,转换为标题格式(首字母大写)
s.capitalize()
:返回字符串的大写版本
s.casefold()
:返回适合于无大小写比较的字符串版本
s.swapcase()
:交换字母的大小写
替换
s.replace
s.replace("要替换的部分", "替换使用的内容")
如果不得不对一个超长的字符串进行多处修改,为了优化脚本性能,可以将字符串转换为一个支持原位置修改的对象,如list
expandtabs
maketrans、translate
format、format_map 格式化也是一种替换
填充
center、ljust、rjust
s.center()
:返回指定长度的居中对齐字符串
s.ljust()
:返回指定长度的左对齐字符串
s.rjust()
:返回指定长度的右对齐字符串
zfill
s.zfill()
:在数字字符串的左边填充零,以填充给定宽度的字段
切除
strip、lstrip、rstrip
removeprefix、removesuffix
分割
split、rsplit
partition、rpartition
s.partition()
:使用指定的分隔符将字符串分为三部分
s.rpartition()
:从右边开始,只用指定的分隔符将字符串分为三部分
splitlines
连接
join
编码解码
encode
decode(这个不是 str 的方法)
string模块在3.x和2.x中的区别
在2.x中早期,字符串对象上没有很多处理方法,都需要使用 string 模块中的方法,后来在字符串对象上也能调用这些方法了,即在2.x中有两种使用字符串方法的方式,对象方法调用和模块调用
在3.x中,删去了string模块中的那些方法,都需要通过字符串对象调用,但是在string中,增加了许多其他的工具
列表
列表对象概述:"列表"
列表可以看做是 对象引用的数组
列表是可变对象,支持在原位置修改的操作
将其他类型转换为列表
将序列转换为列表:list(s)
适用于所有类型的序列
列表信息统计
列表的长度:len(L)
列表中指定数据出现的次数:L.count(x)
在列表中查找
L.index(x)
:查找元素第一次出现的位置,如果不存在则报错(注:列表没有 find 方法)
x in L
:判断元素是否存在
x not in L
:判断元素是否不存在
增加列表中的对象
L.append(x)
:在列表末尾增加一个对象
L[len(L):] = [x]
:在列表末尾增加一个对象
L[:0] = [x]
:在列表头部增加一个对象
L.extend(序列)
:将序列拆开,将其元素挨个添加到列表末尾
L.insert(i, x)
:在索引i之前插入对象
删除列表中的对象
del L[index]
:删除指定的元素(也可以删除整个列表 del L
)
del L[i:j]
:删除一个片段
L[i:j] = []
:删除一个片段
L.pop()
:移除列表中最后一个对象,并返回这个对象
L.pop(x)
:移除指定索引的对象,并返回这个对象
L.remove(x)
:删除指定值的对象
L.clear()
:清空列表
修改列表中的对象
L[index] = 值
:修改指定索引处的对象,如果索引处的对象不存在,则会报错
L[i:j] = [...]
:分片赋值(拆分为两步理解:删除 + 插入),注:被赋值的长度与原分片的长度可不同
其他操作
列表逆置(反转):L.reverse()
,没有返回值,改变列表本身
列表排序:L.sort(key=None, reverse=False)
,改变列表本身,默认升序(Python3.x也不再支持传入一个任意的比较函数来进行排序以实现不同的顺序)
- python中的sort子程序是使用C语言进行编写的,并使用了一个高度优化的算法,该算法将利用序列中元素部分有序的性质来进行排序。被称为 timsort,来源于创始人 Tim Peters,同时这个算法的文档中声称它有着“超自然的性能”(对于排序很好)
列表的复制:
L.copy()
,浅复制,得到的列表是一个新的对象,但其中的元素引用是相同的L[:]
:没有参数的分片表达式可以复制序列list(L)
:使用内置函数复制(dict、set等同理)
列表元素拼接-转字符串:''.join(L)
切片(分片)赋值
会将整个分片的片段都替换掉,可以分成两步理解(只是理解方式-有助于理解为插入元素数目不需要等于删除元素的数目,实际情况并非下面这样):
- 删除:删除等号左边指定的分片
- 插入:将包含在等号右边可迭代对象中的片段插入到旧分片被删除的位置
赋值的序列长度不一定要与被赋值的分片长度相匹配,所以分片赋值能够用来 替换(覆盖)、增长(插入)、缩短(删除)
range函数
range函数常用在for循环中,可以得到一系列的连续数值
range(start, end, step)
,从start开始(包括start),到end结束(不包括end),每隔step产生一个数字
返回的是一个range对象,不是一个列表,如果需要一个列表,要使用 list(range(...))
来转换
注:python2.x中的range则直接生成了一个列表
列表推导语法
列表推导可以创建新的列表对象
基本写法:[与i有关的表达式 for i in 一个可迭代对象]
可以添加if筛选:[i表达式 for i in 迭代对象 if 筛选条件]
可以使用多个for,例如:[(i, j) for i in range(10) for j in range(10)]
使用列表推导的好处:更快,内部使用了C的实现
zip:列表(序列)压缩
zip可以压缩多个列表,例如 zip(a, b, c)
如果zip压缩的列表长度不相等,最终的个数以短的为准
zip返回的是专门的 zip 类型,可以使用 list 转换为列表类型,结果是 一个包含多个元组的列表
字典
字典可以看作是 对象引用表(散列表),Python采取优化过的三列算法来寻找键,所以搜索是很快速的
保存在字典中的项并没有特定的顺序
键提供了字典中项的象征性(非物理性)的位置
创建字典
创建一个空字典:
D = {}
,注意这个是空字典,不是空集合D = dict()
D = dict(k1=v1, k2=v2)
这种形式,键需要是字符串才行
D = dict([(k1, v1), (k2, v2)])
D = dict(zip(键列表, 值列表))
直接通过字面量创建字典:D = {k1: v1, k2: v2, ...}
通过fromkeys创建字典,默认value是None,可以传入一个默认值
D = dict.fromkeys(seq) # 以seq中的元素为键创建字典,默认值为None
D = dict.fromkeys(seq, value) # 以seq中的元素为键创建字典,默认值为 value
D = dict.fromkeys(seq, (v1, v2, ...)) # 以seq中的元素为键创建字典,对应键的值通过查找元素对应位置的值确定
字典推导式
D = {k:v for k in [.....]}
添加字典元素
D[key] = value
:如果字典中没有key这个键,则会添加这个映射关系
修改字典元素
D[key] = value
:如果字典中有key这个键,则会修改key对应的对象
D.update(D2)
:用另一个字典更新当前字典,相当于把D2中的键值对向D中填充,如果遇到相同的键,则覆盖D中原本的值
查找字典元素
D[key]
:直接访问的话,如果key不存在,则会报错,KeyError
key in D
、key not in D
:判断键是否存在于字典中
D.get(key, 默认值=None)
:如果存在key这个键,则返回对应的value,如果不存在,则返回默认值(默认值不设置时,为None)
setdefault(key, 默认值=None)
:返回指定key对应的value,如果不存在则返回设置的默认值,同时将这个值设置为这个默认值
删除字典元素
del D[key]
:删除字典中key对应的对象
D.pop(key)
:移除这个key-value关系,并返回value
D.popitem()
:随机删除一个键值对,并返回
D.clear()
:清空字典
字典视图:keys()、values()、items()
D.keys()
:包含字典的键的可迭代对象
D.values()
:包含字典的值的可迭代对象
D.items()
:包含字典的键值对的可迭代对象
Python2.x中需要通过特殊的 D.viewkeys、D.viewvalues、D.viewitems 方法名来实现,他们的非视图方法一样返回列表
字典视图与集合
与Python2.x中viewkeys视图方法返回的列表不同,Python3的 keys 方法返回的结果类似于集合,并支持交集、并集等常见的集合操作
D1 = {'a': 1, 'b': 2, 'c': 3}
D2 = {'c': 4, 'd': 5, 'e': 6}
print(D1.keys() & D2.keys())
print(D1.keys() | D2.keys())
values 视图则不像集合,因为它们不是唯一性的
items 视图也像集合,因为 (key, value)
键值对是唯一的,并且是可散列的(具有不可变性的)
有因为集合的行为像是无值的字典,所以这是一种符合逻辑的对称。集合的项是无序的、唯一的、不可变的,就像字典的键一样
其他方法
D.copy()
:复制字典
len(D)
:内置函数len也可以用于字典,返回字典中键的数目,或者说 keys 列表的长度
字典的遍历
for 遍历
简单的可以先通过字典收集方法获得字典的键、值、或者键值对,然后进行遍历
for k in D.keys():
pass
for v in D.values():
pass
for k, v in D.items():
pass
迭代遍历
因为字典是可迭代对象,所以可以通过 next 不断地返回后续的键,从而进行遍历
通过 map、filter 等进行遍历
因为字典是可迭代对象,所以也可以被 map 之类的函数所处理
元组
元组的概念
元组是一个序列,不可变,与列表类似,用来表示确定元素的有序集合
元组的关键在于它的不可变性,元组提供了一种 完整性约束
因为不可变,所以元组的长度是固定的
元组的不可变性只适用于元组本身的顶层,而非其内容
为什么有了列表还需要元组?
Python的创造者是一个训练有素的数学家,他曾说过:元组是一种对象的简单组合,而列表是一种随时间改变的数据结构
元组一词其实来自数学领域,同时它也被用来指代关系数据库表的一行
元组的不可变性提供了某种一致性,这样就可以确保元组在程序中不会被另一个引用修改(类似于其他语言中的“常量”声明)
因为元组的不可变性,所以元组可以作为字典的键,让字典可以用多个组合的数据来存储键,很好用
创建元组
(a, b, c)
、a, b, c
、(a, b, c,)
()
空元组
注意:在创建的元组只有一个元素的时候,这个元素要加上逗号结尾!
在不引起歧义的情况下,允许忽略元组的圆括号
没有元组推导式!
(i for i in range(10))
这种写法创建的是一个生成器,不是元组!
可以用 tuple(列表推导式)
代替
逗号 与 圆括号
圆括号除了元组的作用,也表示将表达式括起来,所以如果想要圆括号里面的单一对象是元组对象而不是一个简单的表达式,就需要进行特别说明
方法就是在这个单一的元素之后,右圆括号之前,加一个逗号即可
在不会引起二义性的情况下,允许忽略元组的圆括号
圆括号不可省略的情况
- 出现在一个函数调用中
- 嵌套在一个更大的表达式中
逗号不可省略的情况
- 嵌套在一个更大的列表、字典
- python2.x的print语句中
元组解包赋值
集合
集合不是映射也不是序列,它是不可变的对象的无序集合
集合更像是一个无值的字典
创建集合
创建一个空集合:S = set()
不能用 {}
,因为字典占了
通过字面量创建集合:S = {1, 2, 3, 4, 5}
通过其他可迭代对象创建集合:S = set(xxx)
集合推导:S = { x*x for x in [1, 2, 3, 4]}
冻结集合(不可变性的限制)
可以像调用 set
一样调用内置函数 frozenset
frozenset
会创建一个不可变的集合
集合的操作
判断元素是否存在
in
、not in
:判断元素在或者不在集合中
子集
s.issubset(t)
:子集测试(允许不严格意义上的子集):s 中所有的元素都是 t 的成员
符号表示:s <= t
、s < t
超集
s.issuperset(t)
:超集测试(允许不严格意义上的超集):t 中所有的元素都是 s 的成员
符号表示:s >= t
、s > t
并集
s.union(t)
:合并操作:s "或" t 中的元素
符号表示: s | t
交集
s.intersection(t)
:交集操作:s "与" t 中的元素
符号表示:s & t
差分
s.difference(t)
:差分操作:在 s 中存在,在 t 中不存在的元素
符号表示:s - t
对称差分
s.symmetric_difference(t)
:对称差分操作:s "或" t 中的元素,但不是 s 和 t 共有的元素
符号表示:s ^ t
并集修改
s.update(t)
:将 t 中的元素添加到 s 中
符号表示: s |= t
交集修改
s.intersection_update(t)
:交集修改操作:s 中仅包括 s 和 t 中共有的成员
符号表示: s &= t
差分修改
s.difference_update(t)
:差修改操作:s 中包括仅属于 s 但不属于 t 的成员
符号表示:s -= t
对称差分修改
s.symmetric_difference_update(t)
:对称差分修改操作:s 中包括仅属于 s 或仅属于 t 的成员
符号表示:s ^= t
添加元素
s.add(obj)
:加操作:将 obj 添加到 s
移除元素
s.remove(obj)
:从集合s中去掉obj,如果不存在,则会报错
丢弃元素
s.discard(obj)
:从集合s中去掉obj,如果不存在,没事
弹出
s.pop()
:从集合中随机弹出一个元素
清空
s.clear()
:清空集合中所有元素
集合的应用
过滤重复
分离差异
非顺序的等价判断
文件
文件对象是Python代码调用电脑上存放的外部文件的主要接口
open 内置函数
文件对象没有直接创建的字面量语法,需要通过内置函数 open 来获取文件对象
open(file, mode='r', buffering=None, encoding=None, errors=None, newline=None, closefd=True)
第一个是文件名,第二个是打开模式(通常不用写 mode=,但也可以写),其他参数一般都指定为关键字参数
按位置传参的顺序依次为:文件名,模式,缓冲
close
关闭文件,close一般是可选的,对象被回收时会自动关闭文件
但是有时候为了性能,最好养成手动关闭的习惯
严格来讲,文件这种回收后自动关闭的特性并不是语言定义的一部分,以后可能会变,所以能关就关,累不死
文件的打开
文件打开模式
'r' open for reading (default)
'w' open for writing, truncating the file first
'x' create a new file and open it for writing
'a' open for writing, appending to the end of the file if it exists
'b' binary mode
't' text mode (default)
'+' open a disk file for updating (reading and writing)
'U' universal newline mode (deprecated)
文件打开模式默认为 'r'
,即以只读模式打开文本文件(文本默认用 t
表示,但是一般默认不写)
w
表示已输出模式打开文件
a
表示在文件尾部追加内容并打开文件
在面的基础上:
- 加上
b
可以进行二进制数据的处理 - 加上
+
意味着被打开文件同时支持输入输出
文件的编码
在使用open方法时,可以传入一个编码名参数(encoding
),python会自动在读取和写入的时候采用这个编码来进行编码和解码
二进制字节文件
文本文件在python3.x中通常以字符串形式编码,而在2.x中则默认地写入字符串的内容
python3.x中,文本文件把内容显示为正常的str字符串,并且在写入和读取数据时,自动执行Unicode编码和解码而二进制文件把内容显示为一个特定的字节字符串,并且允许你不修改地访问文件内容
读取并还原二进制数据实际上是一个对称的过程
二进制文件不会对文本进行任何换行符转换
读取文件内容
F.read() 方法
F.read()
:读取文件中的所有内容
F.read(N)
:读取文件中接下来的N个字符到一个字符串
F.readline()
:读取下一行
F.readlines()
:将整个文件读取到一个字符串列表,每行一个元素,包含每行结尾的换行
文件是可迭代对象
读取一个文件的最佳方式就是不读它,而是通过文件提供的一个迭代器在for循环或者其他上下文中自动地逐行读取
向文件写入内容
F.write(string)
:向文件中写入字符串(或字节串)
F.writelines(stringList)
:将一个字符串列表挨个元素写入到文件中
随机访问文件
F.seek(offset, from)
:把文件指针位置从from移动到偏移量offset处以便进行下一个操作
f.tell()
:返回当前在文件中的位置
文件截取
f.truncate([size=file.tell()])
:截取文件到size个字节,默认是截取到文件指针当前位置
文件的上下文管理器
我们可以把文件处理的代码包装到一个逻辑层中,以确保退出后一定会自动关闭文件,而不是依赖于垃圾回收时的自动关闭
with open(.....) as f:
代码......
with上下文管理器方案在所有的python版本中都能保证操作系统资源的释放,而且对于确保刷新输出文件缓冲区更加有用
与try语句不通,with值局限于能够支持它的协议的对象
缓冲
文件是被缓冲的以及可定位的,默认情况下,输出文件总是被缓冲的,这意味着写入的文本可能不会立即从内存转移到硬盘,
除非关闭一个文件或者运行其flush方法,才能强制被缓冲的数据进入硬盘
Python文件也是在字节偏移量的基础上随机访问的,他们的seek方法允许脚本跳转到指定的位置进行读取或写入
变量、表达式、语句、注释
变量
变量也叫名称,变量就是名称,为了可以方便的使用对象才存在的,一般是维护在一个表中记录的(解释器层面)
Python中的变量不代表内存空间,不是内存空间的别名,地址无关
- 变量在第一次赋值的时候被创建
- 变量在表达式中使用时,会被替换成他们所代表的对象
- 变量在表达式中使用之前,必须已经被赋值(创建)
- 变量引用对象,并且不需要事先声明
- 变量更像一个标签
名称没有类型,但是对象有类型,它们只是在特定的时刻碰巧引用了一个对象
变量的命名规则
在Python中,当给名称赋值后,名称就会存在
命名规则
下划线或字母 + 任意数目的字母、数字、下划线
区分大小写
禁止使用保留字(无法通过赋值来重新定义保留字)
注:exec 是一个保留字,它是一个语句,而不是一个内置函数
import 语句中的模块名会成为脚本中的变量,所以不符合上面规则的模块名的模块将无法被导入
命名惯例
- 前后都有双下划线:对 python 解释器有特殊的意义
- 单一下划线开头:不会被
from 模块 import *
语句导入 - 双下划线开头,结尾没有双下划线:是外围类的本地变量
- 只有单个下划线
_
:在交互式命令行中,这个变量会保存最后一个表达式的结果;平时的编程中也会把它当做哑元变量
,用来接收那些不得不接收却用不到的对象 - 更多规范参考 "PEP8","谷歌python编码风格" 等
表达式
表达式的定义:对象 与 运算符 的组合
运算符具有优先级,在表达式中,按照优先级依次计算,最后得到表达式的值
括号可以对表达式进行分组,子表达式不会按照运算符的优先级规则,总是优先计算
语句
语句的分类
赋值语句:=
函数调用语句:xxx()
选择:if、else
循环:while、else
迭代:for、else
空占位符:pass
循环退出:break
循环继续:continue
函数方法定义:def
函数结果:return
生成器函数:yield
命名空间:global、nolocal(3.x)
获取模块:import
构建对象:class
触发异常:raise
调试检查:assert
上下文管理器:with、as (3.x、2.6+)
删除引用:del
表达式语句
一个表达式独占一行,作为一个语句,忽视这个表达式的值
因为这样不会用到表达式的结果,所以一般只有当表达式存在其他附加的工作效果时才有意义
分号
python中,行的终止就是语句的终止,所以可以省略分号
如果想在一行中书写多个语句,则需要用分号将多个语句分开
语句折行
- 当一行的语句过长的时候,可以考虑将语句拆成多行,在一行的最后写一个反斜杠,下一行接着写上一行的语句
- 如果是 圆括号、方括号、花括号这种,在内部可以直接换行书写,也会被视为一条语句
缩进
缩进表示代码块
缩进的结束就是代码块的结束
嵌套的代码块,缩进可以完全不同,只要保证缩进的部分,缩进的长度是相同的即可
建议使用4个空格缩进
冒号
如果代码块的内容可以写成一句,那么可以不需要缩进,直接和上一句写在冒号后面即可
只有当复合语句本身不包含其他任何复合语句的时候,才能这么做
注释
单行注释
用多行字符串注释
文档字符串
动态类型
动态类型是Python语言灵活性的根源
Python没有声明语句
如果学习过 C、C++、Java 等语言,就会有一个疑问,我们使用变量的时候,为什么没有声明变量的类型,但是变量仍然可以工作呢
实际上,python的变量只是名字,真正使用的是对象,对象有类型,但是变量是没有类型的
Python 中的类型是与对象相关联的,而不是和变量关联,变量没有类型,变量是通用的
如果说变量的类型就是其引用的对象的类型的话,那么也可以说:变量的类型是在运行时自动决定的,而不是通过代码声明,即变量是动态类型的
变量、对象、引用
Python在代码运行之前先检测变量名
变量永远不会拥有任何和它关联的类型信息和约束。类型的概念存在于对象而不是变量
变量是通用的,它只是在一个特定的时间点,简单地引用了一个特定的对象而已(变量无类型)
当变量出现在表达式中时,它会马上被当前引用的对象所替代,无论这个对象是什么类型
变量就是名字,名字不需要声明,但是名字使用前你必须说清楚这是谁的名字,即你必须先初始化变量的引用才能去更新它
a = 3
当你写了这样一句话的时候,会发生以下的一些事情:
- 创建一个对象,整数数字3
- 创建一个变量 a,如果他还没有创建的话
- 将变量 a 与上面创建的整数对象3的引用(连接)起来
变量总是连接到对象,并且绝不会连接到其他变量
对象可能连接到其他对象(对象中也可以有名字(变量/属性))
引用:一种关系,通过内存中的指针的形式来实现
一旦使用(引用)变量,Python自动跟踪这个变量到对象的连接:
- 变量是一个系统表的入口,包含了指向对象的连接
- 对象是被分配到的一块内存,有控件去表示它们自己的数据
- 引用是自动形成的从变量到对象的指针
对象的垃圾收集
每当一个变量名被赋予了一个新的对象,如果原来的对象没有被其他变量名或对象所引用的话,那么这个对象占用的控件就会被回收(自动的)
引用计数器
在内部,Python在每个对象中保留了一个计数器,计数器记录当前指向该对象的引用的数目。一旦这个计数器被设置为零,这个对象的内存空间就会自动回收
共享引用
a = 3
b = a
这里的第二行,发生的效果并不是将变量b连接到变量a,而是a和b都引用了相同的对象,指向了相同的内存空间
Python中不可能发生两个变量的相互关联
在Python中变量总是一个指向对象的指针,而不是可改变的内存区域的标签
给一个变量赋一个值,并不是替换原始的对象,而是让这个对象去引用完全不同的一个对象
所有的赋值都是基于引用,包括函数传参
当两个变量引用同一个对象的时候,使用一个变量对这个对象进行修改,用另一个变量访问的时候也会反映出修改
内置引用
Python即使一行代码都不写,本身就已经有很多名字可以使用
通过 sys 模块的 getrefcount 函数可以得到对象的引用次数
数值和字符串缓存
Python缓存并复用了小的整数和小的字符串
这意味着,即使你不再使用这些对象,他们也不会被回收(因为在内部被引用)
通用类型分类
数字
序列
映射
集合
不可变类别
不可变类别的对象类型都不支持原位置修改
可变类别
可变类型总是可以通过不生成新对象的操作在原位置修改
等价性
==
:测试值的等价性,会递归的比较所有内置对象
is
:测试对象的同一性,测试两者是否是同一个对象(在存储器中是否是相同的地址)
类型层次
Python系统中的一切东西都是对象类型
类型本身也是一种类型的对象:对象本身的类型,也属于type类型的对象
循环数据结构
如果一个复合对象包含指向自身的引用,就称之为循环对象
无论何时Python在对象中检测到循环,都会打印成 [...]
,而不会陷入无限循环
赋值语句
赋值操作总是存储对象的引用,而不是这些对象的副本
赋值语句的特性
- 赋值语句创建对象引用
- 变量在首次赋值时会被创建
- 变量在引用前必须先赋值
- 某些操作会隐式的进行赋值
基础赋值形式
A = obj
:一个名称绑定到一个对象
元组与列表的解包赋值(基于位置)
当在 = 左边编写元组或列表是,python会按照位置把右侧的对象和左侧的目标从左到右相配对
python在内部会为右侧的各项创建一个元组,这也是 “元组解包赋值“ 名字的由来
因为语句执行时,python会给右侧变量原本的值创建一个临时元组,所以解包赋值也能够用于交换两个变量的值,而不需要另外创建一个临时变量
即:右侧的元组会自动记住变量原先的值
a, b = b, a
Python中原本的元素和列表赋值形式已经得到了推广,能在右侧接收任何类型的序列(也可以是可迭代对象),只要长度等于左侧序列即可
扩展的序列赋值
在配对完之后,如果嵌套的元素中仍然是一个序列,那么序列赋值会接着进行!
((a, b), c) = ('SP', 'AM')
扩展序列解包
任何值的序列都可以赋值给任何名称的序列,而python会按照位置一次性完成所有的赋值
在序列赋值中,如果左右长度不相等,我们会得到错误
我们可以使用带单个星号的名称来更通用的匹配
当使用一个带星号的名称时,左侧的数目不彼等于右侧主体对象的数目,实际上,带星号的名称可以出现在目标中的任何地方
当带星号的名称出现在中间时,它将收集其他列出的名称之间的所有内容
无论带星号的名称出现在哪里,这个名称都会被赋值一个列表,而这个列表会收集起所有在这个位置上没有被分配给其他名称的待分配对象
- 带星号的名称有可能只匹配到单个项,但即使这个时候,它仍然是一个列表(虽然只有一个元素)
- 即使带星号的名称一个项都匹配不到,那它也是个空列表
多目标赋值
a = b = c = d = e = obj
增量赋值
A += B
相当于 A = A + B
,其他类似的运算同理
优点:
- 减少输入
- 减少表达式求值次数,执行的更快
- 增量赋值有着自动选择的优化技术,对于支持原位置修改的对象而言,增量形式会自动选择执行原位置的修改运算,而不是更慢的复制运算
L = L + [3, 4]
当我们调用增量赋值来扩展列表是,python会自动调用比较快的 extend 方法,而不是使用较慢的 + 运算
+=
并不是所有情况下都精确地等同于 +
和 =
,+=
和 extend
接收任何序列,但是 +
不是
L = []
L += 'abcd' # 正确
L = L + 'efgh' # 错误
+=
等于列表来说是原位置修改,而 +
对于列表来说是产生新的列表 !
打印、输入
打印
打印将一个或多个对象转换成相应的文本表示,然后发送给标准输出或其他类似文件的流
Python中的打印操作,与文件、流的概念,有很大关系
print 输出数据
print函数
print(*args..., sep=' ', end='\n', file=None, flush=False)
print(value, ..., sep=' ', end='\n', file=sys.stdout, flush=False)
- args:可以向print传入一堆值,它会挨个打印
- sep:不同的值之间,用sep分隔,默认是空格
- end:所有值打印完之后,最后打印一个 end,默认是换行
- file:打印的目标,默认是控制台
- flush:刷新缓冲区
python2.x 中的 print 语句
print在2.x中不是一个内置函数,而是一个语句,拥有自己的特定语法
缓冲区的概念
input 获取输入的数据
input函数
python2.x 中的 input
python2.x 中的 raw_input
默认的输入输出设备 stdin、stdout
打印流重定向
手动重定向输出流
import sys
sys.stdout.write(.....)
-------------------------
import sys
sys.stdout = open('log.txt', 'a') # 会丢失 sys.stdout 原本的值
print(......) # 这时会输出到文件中
自动流重定向
因为 sys.stdout
只不过是一个普通的文件对象,所以你可以存储它,并在需要的时候恢复它
import sys
temp = sys.stdout
......
print(......)
......
sys.stdout = temp
print函数的file选项,可以自动做这件事情
log = open('log.txt', 'w')
print(......., file=log)
file 选项更深层理解
print 语句只是把文本传送给 sys.stdout.write 方法,或者说把文本传递给 file 选项指定的对象的 write 方法!
class FileFaker:
def write(self, string):
# ......
import sys
sys.stdout = FileFaker()
print(......)
或者:
class FileFaker:
def write(self, string):
# ......
myobj = FileFaker()
print(......, file=myobj)
真假、if判断
bool 类型
bool类型的本质是数值的,其包含的两个值 True 和 False,是整数1和0的定制版,只不过在打印的时候有所不同。
bool 实际上只是内置类型 int 的子类(从面向对象的角度来看)
bool 为 True 和 False 重新定义了 str 和 repr 的字符串格式
真假的判断
Python中,整数0代表假,整数1代表真(非0也代表真)
空数据结构视为假,非空数据结构视为真
真与假是每个对象的固有属性,每个对象,非真即假
比较或相等测试会被递归的应用到数据结构中
逻辑运算符
与或非
and
:与
or
:或
not
:非
逻辑短路
布尔运算会在结果确定的时候立即停止计算,即不会做多余的计算
布尔运算的一些其他的用法
通过 or 从一组对象中做选择
# 会将 X 设为 A,B, C 中第一个非空的对象,如果都为空,就设为 None
X = A or B or C or None
# 如果 A 非空,则设置为 A,否则设置为 default
X = A or defaut
None 对象
这个对象总是被认为是假,它是一种特殊的数据类型的唯一值,表示空
if 语句的基本用法
基本判断
if 条件:
语句
------
if 条件:
语句1
else:
语句2
多路分支
if 条件1:
语句1
elif 条件2:
语句2
elif 条件3:
语句3
......
else:
语句x
三元表达式的替代语法:A if X else B
相当于:
# 假设 R 是表达式结果
if X:
R = A
else:
R = B
在没有这种写法之前,经常使用:
R = (X and A) or B
没有 switch,使用字典来处理大规模任务
branch = {
"任务1": fun1,
"任务2": fun2,
"任务3": fun3,
......
}
branch.get(choice, default_fun)(......)
循环 while、for
while
只要条件表达式的测试一直为真,就会重复地执行一个语句块
while 条件表达式:
语句块
else:
语句块(没有碰到break时会被执行,一次循环都没执行过,也会执行!)
break、continue
break:跳出最近所在的外围循环(跳过整个循环语句)
continue:跳到最近所在外围循环的头部(来到循环的头部行)
for
一般形式
for target in obj:
语句
else:
语句
for 头部的赋值操作
循环目标本身可以是一组目标,循环对象的每一个元素可以看作是对目标进行赋值,在这里赋值的各种操作都适用
for + range
返回一系列连续增加的整数,可作为for中的索引
for i in range(x):
......
range(len(L))
的方式生成索引
for i in range(len(L)):
......
for + zip
返回一些列并行元素的数组,可以用在for中遍历多个序列的对应值
# x 是 L1 的元素,y 是 L2 的元素
# x 和 y 在它们各自的结构中偏移量相同
for x, y in zip(L1, L2):
......
for + enumerate
同时生成可迭代对象中的元素的值和索引,即同时生成偏移量和元素
for offset, obj in enumerate(L):
......
迭代
可迭代对象
如果对象是世纪保存的序列或是可以在迭代工具上下文中一次产生一个结果的对象,那么就看作是可迭代的
总之,迭代对象包括实际序列,以及能按照需求计算的虚拟序列
可迭代对象:
- 内存中物理存储的序列
- 在迭代操作情况下每次产生一个元素的对象(一种“虚拟的”序列)。在响应next之前先用一个对象对iter内置函数做出响应,并在结束时触发一个异常
生成器就是这种对象,它的值并非立即存储在内存中,而是通过迭代工具在被访问时生成
任何一个从左到右扫描一个对象的Python工具都使用迭代协议
基本迭代协议
所有带 __next__
方法的对象会前进道下一个结果,而当到达一系列结果的末尾时,会引发 StopIteration
异常,这种对象成为迭代器
任何这类对象也能以for循环或其他工具遍历,因为所有的迭代工具内部工作起来都是在每次迭代中调用 __next__
,并且通过捕捉 StopIteration
异常来确定何时离开
对于文件而言,上面的就足够了
手动迭代 iter 和 next
内置函数 next,它会自动调用对象的 __next__
方法
内置函数 iter,拿到一个迭代器,而 iter 返回的迭代器对象有需要的 next 方法,iter 同样也在内部调用了 __iter__
方法
完整的迭代协议
- 可迭代对象:迭代的被调对象,其
__iter_
方法被iter
函数所调用 - 迭代器对象:可迭代对象的返回结果,在迭代过程中实际提供值的对象。它的
__next__
方法被next
运行,并在结束时触发StopIteration
异常
迭代工具在多数情况下自动执行这个扫描步骤,但知道这两类对象的角色有助于理解迭代运行过程
当只需支持单遍扫描时,这两种对象是相同的,而迭代器对象通常是临时的,他们在迭代工具内部被使用
有一些对象既是迭代上下文的工具(可以主动发起迭代),也是可迭代对象(它们的返回值可迭代)
文件对象自身就是迭代器
Python的内置工具集中所有能够从左到右扫描一个对象的工具,都被定义为在主体对象上使用了迭代协议
万物皆对象,无处不迭代,
将一些大的序列转换为可迭代对象以节省空间
例如,range、map、filter、等等都是的
函数
函数的概念
函数就是将一些语句集合在一起的组件,从而让它们能够不止一次地在程序中运行(可以通过它们的名字来使用他们的打包的过程)
函数还能计算出一个返回值,并允许我们指明作为输入的实际参数,而这些参数在每次使用函数的时候可能都不相同,从而让函数变成了一个可以被广泛应用的工具
具体的说,函数是在编程过程中交替地赋值、粘贴 -- 我们不再有一个操作的多个冗余副本,而是将代码包含到一个单独的函数中,这样可以大大减少我们的工作量
函数是为了达到代码重用最大化而提供的最基本的程序结构,它使我们能够更宏观地把握程序设计
总结为何使用函数:
- 最大化代码重用
- 最小化代码冗余
- 过程的分解,代码结构更清晰
编写函数
- def 是可执行的代码:在运行def之前,函数不存在
- def 创建了一个对象并将其赋值给一个变量名:这个对象就是函数对象,这个变量名一般叫做函数名(其实保存的是函数对象的引用)
- 万物皆对象,函数也是对象,函数对象也可以携带任意的用户自定义属性
- lambda 创建一个函数对象并将其作为结果返回
- return 写在函数中,将数据返回给函数的调用者,并且停止函数的工作
- yield 向调用者发回一个结果对象,但是会记住它离开的位置
- global 声明了一个模块级的可被赋值的变量
- nolocal 声明了一个需要被赋值的外层函数变量
- 参数是通过赋值(对象引用)传递给函数的
- 除非显式指明 形参与实参的对应,否则实际参数按位置赋值给形式参数
- 参数、返回值、变量 不需要被声明
def 语句
def 头部定义了被赋值函数对象的函数名,并且在圆括号中包含了 0 个或多个参数,也叫形参
在函数调用的时候,括号中传入的对象将赋值给头部的形式参数
def 语句执行于运行时,它运行的时候会创建一个新的函数对象,并将其赋值给一个变量名
注:python中的一切都存在于运行时,没有独立的编译时这样的过程
函数名并没有什么特别之处,其实就是普通的变量名,关键在于函数名所引用的那个对象
函数参数
参数传递基础
给函数传递参数时的一些关键点:
- 参数的传递是通过自动将对象赋值给局部变量名实现的
- 函数参数(调用者传递的共享对象的引用(可能))在实际中只不过是python赋值的又一个例子而已,对参数名的赋值是自动并且隐式的
- 引用是以指针的形式实现的,所有的参数实际上都是通过指针传入的
- 作为参数被传递的对象从来不会自动复制
- 在函数内部赋值参数名不会影响调用者
- 函数头部的参数名是一个新的局部的变量名,这个变量名是在函数的局部作用域内的
- 函数参数名和调用者作用域中的变量名是没有关联的
- 改变函数的可变对象参数的值也许会对调用者有影响
- 因为是直接传入的对象赋值给参数,所以函数能够原位置改变传入的可变对象,所以结果可能会影响调用者
- 可变参数可以作为函数的输入输出
参数传递只是把对象赋值给变量名,无论是可变对象还是不可变对象都是如此:
- 不可变参数可以当成传入了 “值”(可以这么理解,但传递的不是值,也是指针)
- 其实也是传递的指针,但因为你不能通过更改参数改变调用者的值,所以看起来像是传值
- 可变对象本质上传入了 “指针”
参数默认值可变
可变默认值参数允许状态记忆
如果将一个默认值写成一个可变对象,同一个可变对象会在函数的所有调用中被反复地使用(即使在它在函数中被原位置修改了),即不会重置
如果我们不希望函数内部的原位置修改影响传入的对象,我们可以创建一个显式的可变对象的副本
在调用时创建副本:当遇到没有预期的修改并且发现是某个函数修改的时候,可以传入副本防止修改
L = [1, 2]
fun(L[:])
在函数中创建副本:
def fun(a, b):
b = b[:]
......
以上两种都不阻止函数修改对象,而是避免函数的修改影响到调用者的对象
将可迭代对象转换成元组来防止改变:fun(tuple(L))
‘
参数匹配
参数匹配的很多特殊模式都是可选的,并且它们解决的是匹配问题,匹配完成后在传递机制的底层依然是赋值
位置次序:从左只有进行匹配
关键字参数:通过参数名进行匹配
默认值参数:为没有传入值的可选参数指定参数值
可变长参数收集:收集任意多的基于位置或关键字的参数
可变长参数解包:传入任意多的基于位置或关键字的参数
keyword-only参数:必须按照名称传递的参数
形参、实参写法
调用者:(实参)
func(value)
:通过位置传递func(name=value)
:通过关键字传递func(*iterable)
:解包,相当于基于位置传递func(**dic)
:解包,相当于基于关键字传递
函数:(形参)
def func(name)
:常规参数,通过位置或名称匹配def func(name=value)
:默认值参数,如果没有在调用时传入的话def func(*name)
:将剩下的基于位置的参数匹配并收集到一个元组中def func(**name)
:将剩下的基于关键字的参数匹配并收集到一个字典中def func(*other, name)
:在调用中必须通过关键字传入的参数def func(*, name=value)
:在调用中必须通过关键字传入的参数
两个阶段:函数调用和函数定义
- 在函数调用中,简单值按照位置匹配。如果使用
name=value
的形式,则会告诉python按照参数名匹配,称为关键字参数。一个星号表示将序列拆成基于位置的实参,两个星号表示将字典拆成基于参数名的实参 - 在函数定义时,一个简单的
name
是按照位置或者变量名匹配的(取决于调用者传入参数的方式),但是name=value
的形式指定了一个默认值。一个星号把多出来的不能匹配到基于位置的参数收集到一个元组中,两个星号把多出来的不能匹配到关键字参数的收集到一个字典中- 在Python3中,所有在星号形参后面的普通参数,或者带默认值的参数,都是 keyword-only 参数,必须在调用时通过关键字传入
要遵守的规则:
- 在函数调用时,实际参数必须按照此顺序出现:先是所有基于位置的参数,之后是所有关键字参数,之后是一个星号,之后是两个星号
- 在函数定义时,形式参数必须按照此顺序出现:所有一般参数,带有默认值的参数,一个星号,keyword-only参数(一般的或者带默认值的),两个星号
- 总之:避免出现二义性
函数注解
python3.x中,函数的头部可以写一些注解值:
def func(a: xxx, b: xxx, ) -> xxxx:
当注解出现的时候,python会将他们收集到字典中并将他们附加给函数对象本身(__annotations__
),参数名变为键,如果写了返回值注解,那么它的键是 return
注解提供了一个特殊的语法,但是他自身不作任何事情,注解完全是可选的
但是我们可以去读取和处理注解
匿名函数 lambda
与 def 一样,这个表达式创建了一个之后能够调用的函数,但是它返回函数本身而不是把函数赋值给一个变量名,这也是lambda被称为匿名函数的原因
lambda 是一个表达式,而不是一个语句
lambda 的主体是一个单独的表达式,而不是一个代码块
lambda 中的代码与def中的代码一样遵循作用域规则
lambda 可以嵌套
递归相关
函数自省
函数的属性:
dir(函数对象)
函数属性
除了上面的系统定义的属性,python2.1之后可以向函数附加任意的用户定义的属性
函数是 “一等” 对象
Python函数是对象,可以赋值给其他名称,传递给其他函数,嵌入到数据结构中,从一个函数返回等等
这通常被称为一等对象模型,在Python中是普遍存在的
def 语句中的名称并没有什么特殊的含义,它只是当前作用域中的一个变量赋值,就好像出现在等号左侧一样,def 运行之后,这个名称仅仅是一个对象(函数对象)的引用
增强了函数的灵活性
函数的设计
- 耦合性:在输入时使用参数,输出时使用return语句
- 耦合性:只在真正必要的情况下使用全局变量
- 耦合性:不要改变可变类型的参数,除非调用者希望这么做
- 内聚性:每一个函数都应该有一个单一的,统一的目标
- 大小:每一个函数应该相对较小
- 耦合性:避免直接改变其他模块文件中的变量
作用域
作用域基础
变量(名称)存在于作用域中,作用域决定了名称可以在何处使用
在代码中一个变量名定义的位置很大程度上决定了它的含义
当你在一个程序中使用变量名时,python创建、改变、查找 变量名都是在所谓的命名空间(变量存在的地方)中进行的
我们在讨论代码中查找变量名时,作用域这个术语指的就是命名空间
也就是说,在源代码中变量名被赋值的位置决定了这个变量名能被访问到的范围(即它在哪里可见)
python中一切与变量名有关的事件(包括作用域的分类),都发生在赋值的时候
变量名在第一次赋值时才能存在,并且必须经过赋值才能使用
由于变量名最初没有声明,python将变量名被赋值的地点关联为(绑定为)一个特定的命名空间,换句话说,在代码中给一个变量赋值的地方决定了这个变量将存在于哪个命名空间,也就是它的可见范围
函数也有一个命名空间,为了最小化变量名之间的冲突
在默认情况下,一个函数内赋值的所有变量名都与该函数的命名空间相关联,这意味着:
- 在def内赋予的变量名只能够被def内的代码使用,你不能再函数的外部引用该变量名
- 在def内赋值的变量名与在def外赋值的变量名并不冲突,即使它们相同的名称,它们也是两个完全不同的变量
在任何情况下,一个变量的作用域总是由它在代码中被赋值的位置决定的,而与函数调用完全无关
变量可以在3个不同的地方被赋值,分别对应三种不同的作用域:
- 如果一个变量在def内赋值,它对于函数而言是局部的
- 如果一个变量在一个外层def中赋值,它对于内层函数来说,它是非局部的
- 如果一个变量在所有def外赋值,它对整个文件来说是全局的
上面的称为语义作用域,因为变量的作用域完全由变量在程序文件中源代码的位置决定,而不是由函数调用决定
最终,函数的作用域有助于防止程序内变量名的冲突,并且有助于使函数称为更加独立的程序单元(它们的代码将不再和其他地方使用的名称有关)
作用域细节
我们要使用的名称要么存在于模块文件本身,要么就是python预先定义好的内置名称
从技术上讲,交互式命令行是一个名为 __main__
的模块,它可以打印结果,但不会保存代码,不过在其他方面,它都与模块文件的顶层相同
函数提供了嵌套的命名空间(作用域),使其内部使用的变量名局部化,以便函数内部使用的变量名不会与函数外的变量名产生冲突
- 局部变量是作为临时的变量名,只有在函数运行时才需要它们
- 当函数调用结束时,局部变量会从内存中移除,它们引用的对象如果没有在别的地方引用则会被当成垃圾回收掉,这是一个自动的,内部的步骤,但是它能帮助最小化内存的需求
- 局部变量、全局变量的区别也使得函数变的容易理解,因为一个函数使用的绝大多数变量名只会在函数自身内部出现,而不是这个模块文件的任意其他地方。此外,因为局部变量名不会被程序中某些其他函数改变,这会让程序调试和修改起来更加容易。函数是软件中的自包含单元
函数定义了局部作用域,而模块定义了具有如下属性的全局作用域:
- 外围模块是全局作用域,每个模块都是一个全局作用域:在模块被导入后,相对于外部世界而言全局变量就成了模块对象的属性,但是在这个模块文件自身中也能像简单变量一样使用全局变量
- 全局作用域的作用范围仅限于单个文件:这里的全局指的是在一个文件的顶层变量名仅对于这个文件内部的代码而言是全局的。在python中是每有一个跨文件的单一且无所不包的全局作用域的概念的。名称总是被划分到一个个模块中,你必须明确地导入一个模块文件才能使用这个文件中定义的变量名。当你听到全局时,你就应该联想到 “模块”
- 赋值的变量名除非被声明为 global 或 nolocal,否则均为局部变量
- 所有其他的变量名都是外层函数的局部变量、全局变量、或内置变量
- 函数的每次调用都会创建一个新的局部作用域
- 一个函数内部任何赋值都会把一个名称划定为局部的:包括 赋值,import的模块名,def的函数名,函数的形参名 等等
变量名解析:LEGB 规则
- 在默认情况下,变量名赋值会创建或者改变局部变量
- 变量名引用至多可以再四种作用域中进行查找:首先是局部,其次是外层的函数,之后是全局,最后是内置
- 使用global和nolocal语句声明的名称将赋值的变量名分别映射到外围的模块和函数的作用域
LEGB规则,由作用域的命名而来:
- 当你在函数中使用未限定的变量名时,Python将查找4个作用域并在第一次找到该变量名的地方停下来:首先是局部作用域(L),其次是向外一层的def或lambda的局部作用域(E),之后是全局作用域(G),最后是内置作用域(B)。如果变量名在这一查找过程中没被找到,python会报错
- 当你在函数中给一个变量名赋值时,python会创建或改变局部作用域的变量名,除非该变量名已经在函数中被声明为全局变量
- 当你在左右函数外面给一个变量名赋值时(也就是,在一个模块文件的顶层,或者是交互式命令行下),此时的局部作用域就是全局作用域(即该模块的命名空间)
因为名称在使用前必须被赋值,所以这一模型中没有任何自动化组件:赋值语句总会无二义性地决定名称的作用域
局部变量是被静态检测的
python默认把在一个函数中赋值的变量名归为局部变量,他们存在于函数的作用域中并且只在函数运行时存在
python在编译def代码的时候静态检测python的局部变量,而不是在运行时通过发现赋值语句进行检测的
如果X在局部变量中直接赋值了,且没有声明为global或者nonlocal,Python会在函数中所有的地方都把X作为局部变量
推导式中的变量
在3.x中可能会和其他名称冲突,并可能反映生成器内部的状态,所以推导变量在所有的推到形式中对于表达式自身是局部的
在2.x中,他们对于生成器表达式,集合和字典推导是局部的,但对于将它们的名称映射到表达式外部作用域的列表推导不是局部的
相比之下,for循环语句在任何时候都不会在其语句块内部局部化它们的变量
异常变量
在3.x中异常变量可能会推迟垃圾回收的内存恢复,所以这样的变量对那个except块是局部的,并且事实上当这个异常快退出时会被移除(即使使用过它们)
在2.x中,这些变量在try语句之后一直存在
class 语句
class语句也为其语句块顶层中被赋值的变量名创建局部作用域,在一个 class 内部赋值的变量名不会同其他地方的名称相冲突
这些变量名遵循LEGB查找原则,而class块属于其中的 L 这一等级
同模块导入一样,这些名称在class语句结束后也会转变为类对象的属性
与函数不同,class名称不是通过调用创建的:类对象调用生成实例,后者继承了class中赋值的变量名,并把每个对象的状态记录为属性
尽管LEGB规则既被用来解析类自身顶层中的变量名,又被用来解析类中嵌套方法顶层中的变量名,类自身则会被作用域查找跳过(它们的变量名必须作为对象属性来获取)
因为Python会在外层函数中查找引用变量名,而不是在外层类中查找,所以LEGB规则仍然适用于OOP编程
内置作用域
内置作用域仅仅是一个名为builtins 的内置模块,但是必须要导入 builtins 才能使用内置作用域,因为builtins 本身没有预先内置 X
python3的内置作用域通过一个名为builtins的标准库模块来实现的,但是这个变量名自身并没有放入内置作用于内,所以必须导入这个文件才能使用它
因为Python的LEGB查找规则,通常你可以不引入builtins就可以使用其中的变量名
global
global 是命名空间的声明,global告诉函数,打算生成一个或多个全局变量,即存在于整个模块内部作用域(命名空间)的变量名
- 全局变量是在外层模块文件的顶层被赋值的变量名
- 全局变量如果是在函数内被赋值的话,必须经过声明
- 全局变量名在函数内部不经过声明也可以被引用
即:global允许我们修改一个def外的模块文件顶层的名称
对全局名称的赋值总是在模块的作用域中创建或修改它们
程序设计:最少化全局变量
普通的函数和特别的全局变量会引发一些很大的设计问题
我们的函数之间如何通信?
函数应该依赖形参与返回值而不是全局变量!
在默认情况下,函数内部赋予的变量名是局部变量,如果希望在函数外对变量进行改变,必须编写额外的代码(global语句)。
这是有意为之,即:如果想做些错误的事情,就得多写点代码。反过来,代码写的太多,是不是写的有问题呢
在函数内部定义的变量名默认为局部变量,通常这是最好的约定(自包含,到哪都能用,不依赖额外的全局变量实现)
但是如果你是有目的的使用全局变量,比如多个程序委托一个模块来收集所有的全局变量,就没什么问题
在python中使用多线程进行并行计算的程序实际上也是要依靠全局变量,因为全局变量在并行线程中在不同的函数之间成为共享内存,所以扮演了通信工具的角色
总结:不要轻易用
程序设计:最小化跨文件修改
修改其它模块中的变量,很有可能影响它们的运行,也容易让两个模块之间产生强耦合
在文件之间进行通信最好的办法就是通过调用函数,传递参数,然后得到其返回值
其他访问全局变量的方式
thismod.py
a = 99
def local():
a = 0
def glob1():
global a
a += 1
def glob2():
a = 1
import thismod
thismod.a = a
def glob3():
a = 1
import sys
m = sys.modules['thismod']
m.a += 1
作用域和嵌套函数
E:包括任意外层函数局部作用域的形式
嵌套作用域有时也叫做静态嵌套作用域
实际上,嵌套是一种代码写法上的表述:嵌套的作用域对应于程序源码文本中物理上和语法上的嵌套代码结构
增加了嵌套作用域之后的变量查找规则,在一个函数内:
- 一个引用:首先在局部(函数内)作用域查找变量名,之后会在代码句法上的外层函数中的局部作用域,从内到外查找,之后查找当前的全局作用域(模块文件),最后查找内置作用域(builtins模块)。然而,global声明会直接从全局作用域进行查找
- 在默认情况下,一个赋值会创建或改变当前作用域中的变量名X,如果X在函数内部声明为全局变量,它会创建或改变模块的作用域中变量名X。另一方面,如果X仅在3.x版本中的函数内声明为非局部变量,赋值会修改最近的嵌套函数的局部作用域中的名称X
作用域可以任意嵌套,当名称被引用时只会查找外层函数def语句(而不是类)
工厂函数:闭包
这种代码行为的称呼因人而异,可以叫做闭包(closure),也可以叫做工厂函数(factory fuction)
闭包把它描述成一种函数式编程技巧
工厂函数把它认为是一种设计模式
不论叫法如何,这里讨论的函数对象能够记忆外层作用域里的值,不管那些嵌套作用域是否还在内存中存在。
从结果上看,他们附加了内存包(又称为状态记忆),它对于每个被创建的嵌套函数副本而言都是局部的,从这一作用来看,他们经常提供了一种类的简单替代方法
当记忆状态是唯一的目标时,闭包经常提供一个轻量级的可行的替代方案
它们为每一次调用提供局部化存储空间,来存储一个单独的内嵌函数所需的数据
当一个class嵌套在一个def中时,闭包也可以被创建:外层函数的局部名称的值会被类中的引用,或类中的一个方法函数所保存。它成了一个类工厂,并为嵌套类提供状态记忆
使用默认值参数保存外层作用域的状态:
def f1():
x = 88
def f2(x=x):
print(x)
f2()
f1()
在循环中定义函数,是否能记住循环变量
外层作用域中的变量在嵌套的函数被调用时才进行查找,所以它们实际上记住的是同样的值,也就是在最后一次循环迭代中循环变量的值
nonlocal
即使外层函数已经返回,内嵌函数仍可以引用其外层函数作用域中的变量
如果我们还要修改外层作用域中的名称,则需要使用 nonlocal 声明
nonlocal作用域外层函数的作用域中的一个名称,而不是所有def之外的全局模块作用域
在声明nonlocal时,它必须已经存在于该外层函数的作用域中,即它们只能现成存在外层函数中,而不能由内嵌def中的第一次赋值来创建
nonlocal即允许对外层函数的作用域中的名称赋值,又限制该名称的作用域查找只能在外层def中进行,最终效果是为那些不想要或不需要带有属性、继承和多态行为的类的使用场景
nonlocal 只有在函数内部定义才有意义
nonlocal允许内嵌函数修改定义在语法上位于外层的函数的作用域中的名称(3.x)
这使得外层函数能够提供可写的状态信息,以便在随后调用嵌套函数时能记住这些信息
可以修改状态对嵌套函数而言更加有用(例如外层作用域中的一个计数器)
nonlocal的查找直接从def的外层开始,而不是从函数的局部作用域开始,即 nonlocal 完全略过局部作用域
执行 nonlocal 时,列出的名称必须在一个外层的def中被定义过,否则会引发一个错误
nonlocal 的引入并没有改变通用的名称引用作用域规则
nonlocal 不会一直深入到全局作用域、内置作用域
通过可变的参数默认值记忆状态
默认参数的实现:附加于函数上的单独的对象
可变的默认参数在调用之间记忆状态,而不是每一次调用都重新初始化
函数属性的状态
函数属性有时可以达到和 nonlocal 相同的效果
当给外层函数工厂生成的内嵌函数附加用户自定义的属性时,它们也可以作为基于调用、多副本、可写 的状态,同 nonlocal 作用域闭包和类属性一样。
与nonlocal一样,只对于必须改变的状态变量时才有必要使用,其他作用域引用会正常的保留工作
可变对象的状态
原位置对象修改不会将一个名称归类为局部
def tester(start):
def nested(label):
print(label, state[0])
state[0] += 1
state = [start]
return nested
推导
列表推导
列表推导把任意一个表达式应用于一个迭代对象中的元素,然后生成一个新的列表
列表推导往往速度会比循环更快
列表推导产生一个新的列表对象
列表推导的语法源于集合理论表示法中的一个结构,该结构对集合中的每个元素应用一个操作
列表推导看上去就像一个反着写的for循环
列表推导写在一个方括号中
简单的列表推导由一个 表达式 和一个 for in 表达式组成,最终生成由表达式的结果组成的值,表达式中使用的数据来自 后面的 for in 语句的 taget
# 基本形式
# [表达式 for 目标 in 可迭代对象]
L = [i*i for i in range(100)]
# 可以增加判断
L = [i*i for i in range(100) if i % 2 == 0]
# 多个for
"""
[表达式 for 目标1 in 可迭代对象1 if ......
for 目标2 in 可迭代对象2 if ......
for 目标3 in 可迭代对象3 if ......
......]
在表达式中可以使用所有的目标
"""
L = [(i, j) for i in range(100) for j in range(100)]
生成
python提供了很多对延迟的支持:在需要的时候才产生结果的工具,而不是立即产生结果
如文件对象按行读取,map和zip等按需产生元素
生成器的概念
生成器是单遍迭代对象
生成器函数和生成器表达式自身都是迭代器,并因此只支持一次活跃迭代,一个生成器的迭代器就是生成器自身(实际上在一个生成器上调用 iter 没有任何实际效果)
生成器函数
生成器函数:使用常规的 def 语句进行编写,但是使用 yield 语句一次返回一个结果,在每次结果产生之间挂起和恢复它们的状态
生成器函数也可以有一个 return,不过总是出现在def语句块的结尾,用于终止值的生成
最终效果:生成器函数,被编写为包含 yield 的def语句,能自动支持迭代器协议,并且可以用在任何迭代上下文以随着时间根据需要产生结果
生成器表达式
类似列表推导,不过外面是圆括号不是方括号,它一次产生一个对象,而不是直接创建一整个列表
生成 与迭代协议
生成器函数协议
python2.5后,生成器函数协议增加了一个新的 send 方法,send方法生成一些列结果的下一个元素,提供了一种调用者与生成器之间进行通信的方式,从而影响生成器的操作
从技术上将,yield是一个表达式的形式,它会返回发送给send函数的元素
表达式必须被包含在括号中,除非它是右边的唯一一项
生成器与多线程
生成器函数也是一种 “穷人的多线程机制”:他们通过把操作拆分到每一次的yield之间,从而将一个函数的执行交错的插入它的调用者的工作中
然后生成器并不是线程:程序的运行仍旧在一个单线程的控制内,被显式地交给函数或从函数收走
从某种意义上来说,线程更加通用(新线程中的程序可以真正独立地运行并将结果提交到一个队列中),但是生成器更加容易编写
yield from
函数式编程:map、filter、reduce......
模块、包
模块
什么是模块?
一个以 .py 结尾的,命名符合标识符规范的python代码文件,往往就是一个python模块(并不一定哦)
模块是最高级别的程序组织单元,它将代码和数据封装起来以便再利用,同时提供自包含的命名空间从而避免程序出现变量名冲突
从实际的角度来看,模块往往对应python程序文件,每一个文件都是一个模块,并且模块在导入其他模块之后就可以使用被导入模块中定义的名称
模块也可以是使用其他语言编写的拓展包,认知还可以是在包导入时的文件路径
模块的处理:
- import:使用户程序(导入者)以一个整体获取一个模块
- from:允许用户程序从一个模块文件中获取特定的名称
- imp.reload:在2.x中。提供了一种在不终止python程序的情况下重新载入模块文件代码的方法
模块往往是提供自包含的变量的包(也就是所谓的命名空间)从而将部件组织为系统的一种方式
一个模块文件顶层定义的所有变量都变成了被导入的模块对象的属性
导入给予了对模块的全局作用域中名称的访问权,也就是手,在模块导入时,模块文件的全局作用域变成了模块对象的属性命名空间
确切地说,模块至少能扮演以下三个角色:
- 代码重用
- 系统命名空间的划分
- 实现共享的服务和数据
模块可以避免变量名冲突
模块的分类:
- 标准模块、内置模块
- 自定义模块
- 第三方模块
组织一个程序
一个python程序包括了多个含有python语句的文本文件
程序拥有一个主体的顶层文件,辅以零个或多个被称为模块的支持文件
顶层文件包含了程序的主要控制流程,这个文件也是用来启动应用程序的文件
而模块文件是工具库,这些文件中收集了顶层文件要使用的组件
顶层文件使用了模块文件中定义的工具,而这些模块有可能使用了其他模块所定义的工具
python一系列预先编写好的类,第三方程序也有很多很多写好的类
模块的导入
import xxx 的理解:
- 载入文件 xxx.py(除非已经被载入了),并且通过 xxx 这个变量名称获取它所有的属性的权限
- import语句会按需运行并载入其他的文本
- 在python中,跨文件的模块连接在运行时 import 语句执行后才会进行解析
- 实际效果是:import语句将模块名赋值了载入的模块对象
- 一个import在模块名中起到了两个作用:
- 识别加载的外部文件
- 会变成被赋值了被载入模块的变量
import 如何工作
import导入并非只是把一个文本文件插入另一个文件,导入其实是运行的操作
程序运行期间第一次导入指定文件时,会执行三个步骤:
- 找到模块文件
- 编译成字节码(如果需要的话)
- 执行模块的代码来创建其所定义的对象
在这之后导入相同模块时,会跳过这三个步骤,而只提取内存中已加载的模块对象
python把载入的模块存储到一个名为 sys.modules 的表中,并在每次导入操作的时候检查改表,如果模块不存在,则启动这三个步骤的过程
搜索
首先,python必须查找到import语句所引用的模块文件
python使用了标准模块搜索路径来找出 import 语句所对应的模块
编译(可选)
在遍历模块搜索路径找到符合import语句的源代码文件后,如果需要接下来会将其编译为字节码
导入的同时,会姜茶文件最近一次的修改时间和生成的字节码对应的python版本号,从而决定接下来的行为
如果发现字节码文件比源代码文件旧,或者是由不同的python版本编译的,就会在程序运行时自动重新生成字节码代码
如果发现字节码文件不比源代码文件旧,而且是由同一个python版本编译的,那么python就会跳过源代码找到字节码的编译步骤
3.1之前字节码和源代码在一个路径,3.2及之后字节码存放在 __pychche__
子路径下
运行
import 最后的步骤是执行模块的字节码
文件中的所有语句会从头至尾的依次执行
步骤中任何对名称的赋值运算,都会产生所得到的模块对象的属性,这就是创建模块代码所定义的工具的方式
模块搜索路径
- 程序的主目录
- PYTHONPATH 环境变量的目录(如果设置了的话)
- 标准库目录
- 任何 .pth 文件的内容(如果存在的话)
- 第三方扩展应用的 site-packages 主目录
最终,这五个组件组合起来就变成了 sys.path
,它是一个可变的目录名称字符串的列表
sys.path
可以在程序中修改,但是这种修改只会在当前程序中保持
模块源文件
import x 形式的 import 语句可能会加载或解析为:
- 源代码文件 x.py
- 字节码文件 x.pyc
- 优化字节码文件 x.pyo
- 目录 x,针对包导入
- 编译扩展模块(通常用C或C++编写),导入时使用动态链接(例如linux的
x.so
,windows的x.dll
或x.pyd
) - 用C编写好的编译好的内置模块,并被静态链接至Python
- ZIP文件组件,导入时会自动解压缩
- 内存内镜像,对于冻结可执行文件而言
- Java 类,在 Jython版本的Python中
- NET 组件,在IronPython版本的Python中
- ......
如果在搜索的时候遇到两个同名不同类型的文件,那么先找到哪个就用哪个
导入钩子 import hook
对应的查看 __import__
函数
优化的字节码文件
这种文件在创建和执行时要加上 -O 这个标志位,之后便会自动被一些安装过的工具所生成,这些文件会比普通的 pyc 文件稍微快一点点
一般模块的创建
输入python代码,保存为 .py 文件,这样创建出的任何文件都被自动认为是python的模块
在模块顶层指定的所有名称都会变成其属性(与模块对象相关联的名称),并且可以导出供用户程序使用(它们会自动地从变量变为模块对象属性)
因为模块名在导入的时候会变成变量名,所以最好也要符合名称的命名规则,否则可能不方便导入
from
from 扩展了 import ,但是多了一个额外的步骤
from 将从模块中取出(复制)特定的名称到另一个作用域,之后我们可以直接在脚本中使用复制后的名称,而不需要通过模块
这种形式的 from 可以列出一个或多个要复制的名称,以逗号分隔
如果使用 from *
,则会取得模块顶层被赋值的所有名称的副本
from 语句会让变量的位置更隐式和模糊,所以大多数时候更推荐使用 import 而不是 from,除非你特别清楚from导入的是个什么东西
import 和 from 是赋值语句
和def一样,import 和 from 是赋值语句,而不是编译时的声明
它们可以嵌套在 if 测试中,在选项中挑选
出现在函数 def 之中,只有在被调用时才加载
在try语句中使用,提供默认值
等等,直到执行程序时python到达这些语句,它们才会进行解析和运行,即被导入的名称只有在它们所对应的import或from执行后,才可以使用
在模块中改变可变对象
以from复制的名称会变成对共享对象的引用
模块命名空间
在模块顶层被赋值的所有名称都会成为该模块的属性
- 模块语句会在首次导入时执行
- 顶层的赋值语句会创建模块属性
- 模块的命名空间可以通过
__dict__
或者dir(M)
获取 - 模块是一个独立的作用域,其局部变量就是模块的全局变量,模块的作用域在导入之后就持续存在
命名空间字典 dict
内部,模块命名空间被存储为字典对象,只是一个普通的字典,拥有一般字典的方法
python会在模块命名空间中加入一些名称:
__file__
指明模块是从哪个文件加载的__name__
指明导入者的名称- 同样
__dict__
本身也是被加入到模块的一个名称
重新加载模块
reload 函数会强制已加载的模块重新载入并重新执行,文件中新的代码的赋值语句会在原位置修改现有的模块对象
reload 的使用是为了动态定制化,reload允许在整体程序不停止的情况下修改程序的一部分,并可以立即看到对组件修改后的效果
reload 目前貌似只能用于python编写的模块
reload 位于 imp 中,需要导入才能使用
reload 需要一个已经存在的模块对象作为参数,而不是一个名称
重新加载不会覆盖用from复制的名称
重新加载只适用于单一的模块,不能嵌套传递式地重新加载
模块包
在import语句中列举简单文件名的地方,也可以改成以点号相隔的路径名称
这些带点号的路径对应于机器目录层次的路径,通过这个路径可以获取到指定的文件
包导入路径中最左边的部分仍然是相对于 sys.path 模块搜索路径的一个目录
init.py 包文件
包导入需要多遵循一个约束,3.3版本位置,包导入语句指定的路径下的每个目录都必须有 __init__.py
这个文件,否则包导入会失败
这个文件在导入的时候会自动执行,所以它们也被用于执行包导入的初始化步骤的钩子
init 文件 更直接的作用说明就是,声明一个目录是python包
包导入时,会得到一个包模块对象,其直接属性可以在 init 文件中定义
包相对导入
在包自身的内部,包的导入可以相对于包,从而简化导入的写法
相对导入使用一个点号开头来导入位于同一包下面的模块,而不是位于模块导入搜索路径上某处的模块(称为绝对导入)
点号开头的形式只能对 from 语句使用,import 语句总是绝对导入的,并且会跳过内部的包路径
from . import m
:在2.x和3.x中都是仅相对的
import m
,from m import x
:在2.x中是先相对再绝对,在3.x中仅仅是绝对的
相对导入只会搜索包目录,相对导入是相对于CWD的(即当前文件运行的目录)
包外导入、包内导入
命名空间包
除了常规的包,还有一些可选的模型,如命名空间包
该模型允许包横跨多个目录,并且不需要 __init__.py
初始化文件,在 py3.3 被引入
与普通的包没有本质的区别,只是提供了另一种创建包的方式,它们在顶层仍然相对于 sys.path
- 如果找到 init 文件,便会导入一个常规包并返回
- 如果找到 xxx.py 便会导入一个简单模块并返回
- 如果找到文件夹 xxx,便会将其记录下来,而扫描将从搜索路径的下一个目录继续
- 如果上述的所有都没有找到,扫描将从搜索路径的下一个目录继续
如果搜索结束,至少记录了一个路径,那么就会创建一个命名空间包(立即发生,而不是等到一个子层级被导入时才发生)
新的命名空间包有一个 __path__
属性,被设置为扫描中记录的各个路径的字符串组成的列表,但是没有 __file__
属性
模块查找规则总结
使 * 的破坏最小化 _X 和 all
你可以再名称前面加一个单独的下划线,从而防止 from *
语句导入时,把这些加了下划线的名称复制出来
下划线不是私有,你仍然可以使用 import 导入然后通过 点号 获取
如果指定了 __all__
from 局域只会把其中的名称复制出来,但不包括以单个下划线开头的名称
未来的语言特性 future
混合使用模式:__name__和 "main"
模块的属性,可以再模块中直接使用,也可以在其他的调用模块中引入使用
name是一个模块的变量,而main则是一个普通字符串,表示当前模块是以顶层脚本的方式运行的,而不是被引入的
使用name属性可以写一些代码,并保证这些代码在被引入的时候不会执行,但是单独运行模块文件时才会执行
if __name__ == "__main__":
......
修改模块搜索路径
对 sys.path
这个列表进行修改,会对本次程序运行中后来所有的导入产生影响
用名称字符串导入模块
直接使用 import "xxxx"
使用 exec
使用 import
传递性模块重载(递归重载)
类与对象概述
类初识
之前,我们一直在说“对象”这个词,我们一直的代码也都是基于对象的
我们:
- 在模块中传递对象
- 在表达式中使用对象
- 调用对象的方法
类是python中实现的一种新的对象,没错,类也是对象
类是面向对象程序设计的主要工具,利用类,我们将代码分解从而把冗余程度降至最低,并且通过定制已有的代码来编写新的程序而不是在原处进行修改
类的创建提供了一种新的语句:class 语句
概括地讲,类就是一些函数的包,这些函数大量地使用并处理内置对象类型
类的设计是为了创建和管理新的对象,同时他们也支持继承等复杂的代码定制和复用机制
在Python中,OOP完全是可选的,即使不学OOP,也能写出很好的Python程序
使用类通常需要一些预先的规划,因此类更适合于喜欢按战略模式工作的人(做长期产品开发维护升级迭代)
而不用类的代码更适合采用战术模式工作的人(时间有限)
为什么使用类
编程是一种什么样的工作? 用一些东西来做一些事
类,就是一种定义新种类的东西的方式,它在编程中反映了现实中的对象,可以帮助开发人员建立代码与现实世界中结构和关系的模型
类可以继承和组合:
- 继承:对于通用的属性将通用的代码实现一次,就能让所有需要这些操作的类去继承这些操作
- 组合:可以让多种不同的对象协同工作
从编程角度来看,类是Python程序的组成单元,就好像函数和模块一样
类是封装逻辑和数据的一种方式
类也定义了新的命名空间
类有三个重要的独特之处:
- 多重实例:
- 类本质上是生产对象的工厂。每当我们调用一个类的时候,就会产生一个有独立命名空间的新对象。
- 每个由类产生的对象都能读取类的属性,并获得自己的命名空间来存储数据,这些数据是属于每个对象本身的(这和闭包函数可以在不同调用之间保存状态一样,但是在类中显得更加显式和自然)
- 通过继承进行定制:
- 通过编写子类的方式,来重新定义其属性进而扩充这个类
- 类可以建立命名空间的层次结构,而这种层次结构可以定义该结构中类创建的对象所使用的名称
- 运算符重载:
- 通过提供特定的协议方法,类可以定义对象来响应在内置类型上的一些运算
从本质上讲,Python的OOP主要依托于两个基础:
- 特殊的函数第一位参数
- 继承属性搜索
除此之外,这个模型基本上就是处理内置类型的函数
如果已经知道了如何使用python的核心类型,那么就已经知道了类的大部分用法,类其实只是一个较小的结构性扩展
继承属性搜索
比起其他一些语言,C++、Java等,Python中的OOP大大简化了,Python吧那些语言中使OOP隐晦的语法杂质和复杂性都去掉了
在python中,大多数OOP的故事,都可以简化为一个表达式:obj.attribute
当我们对class语句产生的对象使用这种方式时,表达式会在python中启动一次搜索,即搜索对象连接的类树,从而去寻找 attribute 首次出现的类
找到attribute首次出现的地方,先搜索 obj,然后是该对象之上的所有类,由下往上,由左到右
换句话说:属性访问就只是搜索类树。
我们称这种搜索为继承,因为树中位置较低的对象继承了树中位置较高的对象的所有属性
当从下至上进行搜索时,连接至树中的对象就是树中所有父节点定义的所有属性的并集,直到树的根部
我们通常把树中位置较高的类称为父类,位置较低的类称为子类,父类提供了所有子类共享的行为,到时因为搜索过程是从下向上的,所以子类可能会在树中较低的位置重新定义父类中的名称,从而覆盖父类定义的行为
总结:我们通过代码建立连接对象树,每次使用 对象.属性
表达式,python确实会在,运行期间去“爬树”,来搜索属性
类和实例
类是对象,实例也是对象,实例是由它所属的类生成的
- 类
- 类是实例工厂
- 类的属性提供了行为(数据、函数)
- 所有从类产生的实例都继承该类的属性
- 实例
- 实例由类产生
- 代表程序中的具体元素
- 实例的属性记录了每个实例自己的数据
类和实例是两种不同的对象类型,但是在类树中看,它们是完全等价的
两者的主要目的都是作为另一种命名空间(即变量的封装,也就是我们可以附加属性的地方)
即使没有实例,类本身也是对象,事实上,类只是自包含的命名空间,因此只要有类的引用值,就可以在任何时刻设定或修改其属性
类与模块的区别
模块对应于文件,而类对应于class语句,一个文件只能有一个模块的实例,但是类可以定义多个
模块反映了整个文件,而类只是文件中的语句
某种意义上,模块就像是单一实例的类,没有继承,而且模块对应于整个文件的代码
模块:
- 实现了 数据/逻辑 包
- 通过Python文件或其他语言的扩展来创建
- 通过导入来使用
- 成为Python程序结构的顶层
类:
- 实现了新的功能完整的对象
- 通过class语句来创建
- 通过调用来使用
- 总是位于一个模块中
- 类支持模块所不支持的额外功能,但是和模块的层级不同,功能不同,扮演的角色不同
方法调用
如果我们在类中定义了函数,则每当我们通过实例调用函数的时候,总是会隐含着传入这个函数的实例,作为这个函数的第一个参数
Python会把引号的实例传入方法中特殊的第一位参数,习惯上称其为 self。方法通过这个参数来处理调用的主体
方法的绑定和未绑定
通过类点方法来调用,返回的是未绑定的方法对象,无 self
通过实例点方法来调用,返回的是绑定方法对象,python在绑定方法对象中自动把实例和函数打包,所以不需要传递实例去调用该方法
在Python3中,已经删除了未绑定方法这个概念,未绑定的方法是函数(Python3中甚至已经没有了未绑定方法这种说法,完全当做函数看待)
编写类树
- 每个class语句会生成一个新的类对象
- 每次类调用时,就会生成一个新的实例对象
- 实例自动链接到创建它们的类
- 类链接到其父类的方式是,将父类列在class头部的括号中,括号中从左到右的顺序会决定类树中的顺序
增加属性
属性通常是 class 语句的顶层语句块中通过赋值语句添加到类中,而不是嵌入在内部的函数 def 语句中
属性通常是通过对特殊的称为 self 的第一位参数的赋值,从而附加给实例的,而这个self参数也被传入类中编写的方法函数
在类中定义函数,实际上也是在给类添加属性,属性名是函数名,属性值是一个函数对象,定义在类内部的函数通常称为方法
多态的现象
运算的意义取决于运算的对象
代码不应该关心它处理的对象是什么,而只应当关注这个对象应该做什么
多态可用于隐藏(封装)接口差异性
类代码编写
从对底层来看,类基本就是命名空间
但是类和模块不同的是,类还支持生成多个对象,命名空间继承,运算符重载
类生成多个实例对象
Python的OOP模型中有两种对象:类对象、实例对象
类提供默认行为,是实例对象的工厂
实例对象是程序处理的实际对象,各自都有独立的命名空间,但同时继承了(能自动访问)创建该实例的类中的变量名
类对象来自语句,而实例来自调用,每调用一个类,你就会得到这个类的一个新的实例
借助类,每个实例都有了它们自己的、独立的数据,从而支持类所建模型的对象的多个版本
类对象提供默认行为
当我们执行 class 语句的时候,就会得到类对象
- class 语句创建类对象并将其赋值给一个名称:就像def一样,class语句一般是在其所在文件被导入或运行的时候执行的
- class 语句内的赋值语句会创建类的属性:从技术角度看,class语句定义了一个局部作用域,该作用于会变成类对象的属性的命名空间,就像模块的全局作用域一样
- 类属性提供了对象的状态和行为:类对象的属性记录了可以由这个类所创建的所有实例共享的状态信息和行为。类内部的def语句会生成方法,方法可以用于处理实例
实例对象是具体的元素
当调用类对象时,就可以得到实例对象
- 像函数那样调用类对象会创建新的实例对象:每次类被调用时,都会建立并返回新的实例对象,实例代表了程序领域中的具体元素
- 每个实例对象继承了类的属性并获得了自己的命名空间:由类所创建的实例对象是新的命名空间,它们一开始是空的,但是会继承创建该实例的类对象内的属性
- 在方法内对self的属性做赋值运算会产生每个实例自己的属性:在类的方法函数内,第一位参数会引用当前处理的实例对象修改self的属性会创建或修改实例内的属性值,而不是类的数据
总结:
- 类定义了公共的、共享的数据和行为,并生成实例
- 实例反应了具体的应用程序中的实体,并记录了每个实例自己的随对象变化的数据
类通过继承进行定制
类可以引入新的组件(子类)来进行修改,而不对原有的类进行原处的修改,从而形成类的层次结构
通过在较低层次的地方可以覆盖已有的属性从而让行为特定化
我们把在树中较低处发生的通过重新定义取代属性的动作称为重载(重写)
实际上,越深入层次的下端,软件就会变得越特定化
类和模块在这一点上看也是有区别的,因为模块的属性存在于一个单一、扁平的命名空间中(该命名空间不接受定制化)
属性继承:在Python中,实例的属性从类中继承,而类的额属性继承于父类
- 父类列在class语句头部的括号中
- 类从父类中继承属性
- 实例会继承所有可访问类的属性
- 每个
obj.attribute
引用都会启动一个新的独立的搜索 - 逻辑的修改是通过创建子类,而不是修改父类
这种搜索的最终结果和主要目的就是:类支持了程序的分解和定制,这样做可以把程序的冗余度降到最低(从而减少维护成本),也就是把操作分解为单一、共享的实现
继承的类属性只是附加到了类,而没有向下复制到实例
类是模块内的属性
通过 class 语句可以定义一个类,但是注意,class 后面的那个名字,只是一个普通的变量而已,它只是刚好引用了一个类对象!
方法的说法
方法是与特定对象关联,并作用于特定对象的简单函数
他们是附属于对象的属性,而这些属性碰巧引用了可调用的函数罢了,这些函数总是拥有一个隐含的主题
从更精细的角度看,函数就是代码包,而方法调用同时结合了两种操作:一次属性获取和一次函数调用
- 属性获取:
obj.attribute
,获取对象的某个属性的值 - 调用表达式:
function(arguments)
,调用这个函数里的代码,并给给它们访问传入一些可以使用的参数 - 方法调用表达式:
obj.method(arguments)
,方法调用表达式意味着,使用参数调用方法来处理对象
类可以截获Python中的运算符
运算符重载就是让用类创建的对象,可截获并响应用在内置类型上的运算
这其实是一种自动分发机制:表达式和其他内置运算被路由到了类内部的实现
在这点上类和模块也不同:模块可以实现函数调用,但却不能实现表达式的行为
运算符重载让对象和python的内置对象模型更紧密地结合起来,也可以让我们自己的对象拥有内置对象那样的行为
- 运算符重载需要一些以双下划线命名的方法,它们是特殊的钩子
- 当实例出现在内置运算中时,这类方法会自动被调用
- 类可以重载绝大多数内置类型运算
- 默认的运算符重载方法既不存在,也不需要
- 新式类有一些默认的运算符重载方法
- 运算符将类与Python的对象模型结合到一起
class 语句
python中的 class 语句并不是声明式的,就像 def 一样,class语句是对象的创建者并且是一个隐含的赋值运算:当它执行时会产生类对象,并把其引用值存储到前面所使用的的名称中
class语句和def一样,是真正可以执行的代码,直到python抵达并运行class语句之前,类都不存在,这一般都是当类所在的模块被导入或被运行时发生,在此之前都不存在
class 是复合语句,如果有内容块多行,需要缩进写在 class 的下面,在头部行中,父类列在类名称之后的括号中,用逗号分隔
在class语句中,任何赋值语句都会产生类属性,而且还有特殊名称方法重载运算符等
python会从头到尾运行 class 里面的所有语句,从而在类这个作用域中创建名称,从而成为对应的类对象内的属性
- 一般赋值语句会创建数据属性,而def语句则会创建函数属性
- 就像函数一样,由class语句中内嵌的赋值语句创建的名称,位于class的局部作用域中
- 就像模块内的名称,在class语句中赋值的名称会成为对象中的属性
命名空间也是Python中继承的基础
__init__方法
用来初始化对象,可以让类在新产生的实例中立即添加属性
在创建新的实例时被调用,第一位参数 self,就代表创建的实例的引用
属性字典
命名空间对象的属性通常都是以字典的形式实现的,而类继承树(一般化的来看只是)互相连接的字典而已
__dict__
属性是绝大多数基于类的对象的命名空间字典,即实例的命名空间字典
查看命名空间的名称:
list(obj.__dict__.keys())
一个属性通常既可以通过字典索引又可以通过属性记号访问,但是仅当其出现在所需的对象上
属性记号自动启动了继承搜索,但是索引只在单独的该对象中查看
为了完成属性搜索,每个实例都拥有一个Python帮我们创建好的指向其类的连接,这个链接叫做 __class__
,类也有一个 __bases__
属性 它是其父类对象引用的数组
虽然这是python在内存中实际表示树的方式,但是像这样的内部细节初学时不需要知道,你的代码在运行时自动隐含类树,并发起相应的搜索
理解这两个属性可以有助于阐明对象模型
Python的类模型是相当动态的,类和实例只是命名空间对象,它们所携带的属性是通过赋值语句动态创建的,只要你能引用类树中的一个对象,就能在任何地方使用这些属性
类是由class语句填充的,实例的属性则是在类的方法中通过对 self 属性进行赋值运算而创建的,不过重点在于并不是必须如此
Python中的OOP其实就只是在已连接的命名空间的对象内寻找属性而已
名称解析的规则、命名空间的总结
首先需要记住,带点号和不带点号的名称采用的是不同的方式处理
- 无点号运算的名称对应于作用域
- 带点号的属性名使用的是对象的命名空间
- 有些作用域用于初始化对象的命名空间
简单名称:非全局作用域里被赋值就不是全局的
赋值语句:默认情况下让名称称为局部的:在当前作用域内创建或改变名称X,除非使用 global 或者 nonlocal
引用:按照LGEB规则,python对名称的搜索首先在当前作用域中,之后在所有外层函数中,再然后在当前全局作用域中,最后在内置作用域中。注意:外层的类不会被搜索,相反,类名称是作为对象属性被访问
属性名称:对象命名空间
对类对象和实例对象而言,引用规则扩展并包含了继承搜索这个过程
- 赋值语句:在进行点号运算的对象的命名空间内创建或修改属性名,而没有其他作用。继承树的搜索只发生在属性引用时,而不是属性的赋值运算时
- 引用:对基于类的对象(类对象、实例对象)而言,会在实例中搜索属性名,然后是其上可以访问的类(采用继承搜索过程)。对模块这样不是基于类的对象而言,则是直接从对象中访问属性
赋值决定名称
名称被赋值的位置相当重要:这完全决定了名称所在的作用域或对象
局部变量对于def内的其余代码才是可见的,而事实上,也只有在函数调用或者方法执行时,它们才会存在于内存中
class 创建类对象时,会执行里面的语句,里面赋值的属性立即存在
def 创建函数对象时,默认不会执行函数中的语句,必须等到调用时才可以,函数中的作用域在被调用的时候才会存在
访问外层类的作用域的名称
类的作用域最终在class语句运行后变成了一个属性命名空间
尽管在类中可以访问外层函数的作用域,但它们不能作为类中其他代码的外层作用域
python搜索外层函数来访问被引用的名称,但从来不会搜索外层类
也就是说,类是一个可以访问其外层作用域的局部作用域,但其本身却并不能作为一个外层作用域被访问
因为方法函数中对名称的搜索跳过了外层的类,所以类属性必须作为对象属性并使用继承来访问
命名空间链接:一个类树爬升器
对象的显示 __str__
和 __repr__
__str__
通常被 print 和 str 等使用,而 __repr__
被当做 __str__
的一种退路,或用在其他上下文中
__repr__
通常提供了一种对象的底层表示,而 __str__
则通常用于用户友好的显示
名称重整机制
两个下划线开头:伪私有类属性
以下划线开头的属性,Python将自动扩展这样的名称以包含类的名称,从而使它们在继承搜索中变的真正唯一
在类的内部语句,任意开头有双下划线,但结尾没有双下划线的名称,会自动在前面包含外围类的名称从而进行扩展 _类名__X
名称重整并不能阻止来自类外部代码的访问,这种功能主要是为了避免实例内的命名空间冲突,而不是限制名称的访问
伪私有属性解决了实例的属性存储问题(大体上,如果真的有笨蛋这么命名了,那还是会冲突的)
钻石继承问题
MRO 与 C3
MRO 的顺序基本就是:在避免同一类被调用多次的前提下,使用广度优先和从左到右的原则去寻找需要的属性和方法
在继承体系中,C3 算法确保同一个类只会被搜寻一次。例子中,如果一个属性或方法在 D 类中没有被找到,Python 就会搜寻 B 类,然后搜索 C类,如果都没有找到,会继续搜索 B 的基类 A,如果还是没有找到,则抛出“AttributeError”异常
运算符重载
运算符重载,意味着在某个类的方法中拦截内置的操作,但类的实例出现在内置操作中时,python会自动调用你的方法,并且你的方法的返回值会作为相应操作的结果
- 运算符重载让类拦截常规的Python操作
- 类可重载所有Python表达式运算符
- 类也可以重载打印、函数调用、属性访问等内置运算
- 重载使类实例的行为更接近内置类型
- 重载是通过在一个类中提供特殊名称的方法来实现的
__new__
实例创建时触发的第一个方法
在一个实例被创建的过程中,首先触发的是 __new__
方法,
这一方法将创建并返回一个新的实例对象,并传入 __init__
方法供初始化
由于 __new__
方法拥有一个内置的实现,并且自己定义了之后担任的角色非常有限,因此我们一般都是用 __init__
来初始化
__init__
类创建实例时初始化
__del__
对象回收时(析构函数)
__call__
函数调用
定义了这个方法,实例可以当做函数来使用
__repr__
、__str__
对象的字符串表示
点号运算:属性相关
__getattr__
属性访问
拦截属性引用,每当你引用一个未定义的(不存在的)属性名称字符串对一个实例对象做点号运算时,就会被调用
如果Python能够通过其继承树搜索找到这个属性,那么这个方法就不会被调用
所以,__getattr__
可以用作以泛化形式响应属性请求的钩子,它通常用于将代理控制对象的调用委托给内嵌(又称“被包装”)的对象
__setattr__
属性赋值
__setattr__
会拦截所有的属性赋值
注意,在这个内部如果再对属性赋值,就会造成潜在的无限递归
如果想使用这个方法,可以把实例属性的赋值写成对属性字典(__dict__
)的赋值来避免递归
另一个避免递归的方法是,通过一个调用把任意属性赋值传递给一个更高级的父类,从而代替使用属性字典的方法
__delattr__
属性删除
__getattribute__
属性访问
描述符属性
__get__
__set__
___delete___
索引、分片、迭代
__getitem__
索引访问、分片访问、迭代访问
拦截一般索引参数
拦截分片参数
__setitem__
索引赋值、分片赋值
__delitem__
索引删除、分片删除
大小比较 __lt__、__gt__、__le__、__ge__、__eq__、__ne__
有几点需要注意:
- 比较方法没有右侧的变体,相反,仅当一个操作数支持比较的时候,才可以使用对应的方法
- 比较运算符没有隐含关系,例如 == 为真并不意味着 != 为假,因此定义时要确保这两者的关系从而不让使用者产生迷惑
成员关系 __contains__
in 通常实现为一个迭代,使用 __iter__
或者 __getitem__
方法
类还可以编写一个 __contains__
方法来支持更加具体的成员关系,这个方法优先于前两个方法
__contains__
优先于 __iter__
优先于 __getitem__
类型转换
__index__
整数类型转换
加减乘除
__add__
__sub__
逻辑运算
__and__
与
__or__
或
__not__
非
双操作数运算拓展
下面用加法举例,其他的操作方法同理
__radd__
右侧 + 操作
只有当运算符右侧是实例对象而左侧不是实例对象的时候才会调用,否则就会调用 __add__
__iadd__
原位置 += 操作
长度 __len__
真假 __bool__
布尔测试的时候,先尝试使用一个特定的 __bool__
当它不存在的时候使用 __len__
一个非零的长度意味着真
迭代协议
__iter__
Python中的所有迭代上下文都会先尝试 __iter__
方法,再尝试 __getitem__
方法
iter+yield
__next__
单遍扫描 与 多遍扫描
单个对象上的多个迭代器
内省
"""
1. 我是谁
2. 我是什么人
3. 我在哪
4. 我有哪些东西
5. 我在的地方有哪些东西
6. 我要干什么
7. 我能干什么
8. 我能让这个地方的其他人做什么
"""
__class__
instance.__class__
:提供了一个从实例到创建它的类的链接,同时类也有一个 __name__
,还有一个 __bases__
来提供父类的访问
__dict__
object__dict__
:提供了一个字典,将所有命名空间对象(包括模块、类、实例)中的属性都存储为键值对
因为它是字典,因此我们可以获取键列表,按照键来索引,在它的键上进行迭代等,从而泛化的出来所有的属性
__name__
__doc__
文档字符串,将会自动保存在对象的这个属性中,适用于模块文件、函数定义、类、方法等等
反射
程序内部自省的一种方式
可以用字符串来操作对象的属性
hasattr
getattr
setattr
delattr
类的设计
三大思想:
- 继承:以属性查找为基础
- 多态:方法的意义取决于主体对象的类型(类)
- 封装:方法和运算符实现行为,不过默认情况下数据隐藏是一种惯例
多态,它因python中缺少类型声明而出现,因为属性总是在运行时解析,所以实现相同接口的对象可以互相交换;客户端不必知道他们调用的方法是由哪种类型的对象实现的
封装意味着在Python中打包,也就是把实现的细节隐藏在对象接口之后,尽管私有性可通过代码实现,但是封装并不代表强制施行的私有性
封装允许修改对象接口的实现,同时不影响该对象的用户
多态意味着接口
继承是一种 is-a 的关系,组合是一种 has-a 的关系
从程序员的角度来看,继承是由属性点号操作启动的,并由此触发实例、类以及任何父类中的变量名搜索
从类的设计者的角度看,继承是一种指明集合成员关系的方式:类定义了一个属性集合,可由更具体的集合(如子类)继承和定制
组合一般指内嵌对象集合体,组合类一般都提供自己的接口,并通过内嵌的对象来实现接口
从程序员的角度来看,组合涉及把其他对象嵌入容器对象内,并促使其实现容器的方法
对类的设计者来说民族和是在一个问题领域中另一种呈现关系的方式
但是,组合不是集合的成员关系,而是组件,也就是整体的组成部分。
组合也反映了各组成部分之间的关系,通常称为 has-a 关系
组合和继承是互补的工具
委托,通常是指控制器对象内嵌其他对象,并把操作请求传递给那些内嵌的对象。
控制器能够负责管理类活动,添加额外的步骤等
从某种意义上讲,委托是组合的一种特殊形式。它使用包装器(有时叫做代理)类管理单一的内嵌对象,而包装器类则保留了内嵌对象的大多数或全部的接口
代理这一概念有时也适用于函数调用等其他机制;在委托过程中,我们关心对一个对象全部行为的代理,包括方法调用和其他运算
Mixin:
Mixin 编程是一种开发模式,是一种将多个类中的功能单元的进行组合的利用的方式,这听起来就像是有类的继承机制就可以实现,然而这与传统的类继承有所不同。通常 Mixin 并不作为任何类的基类,也不关心与什么类一起使用,而是在运行时动态的同其他零散的类一起组合使用
使用 Mixin 机制有如下好处:
- 可以在不修改任何源代码的情况下,对已有类进行扩展;
- 可以保证组件的划分;
- 可以根据需要,使用已有的功能进行组合,来实现“新”类;
- 很好的避免了类继承的局限性,因为新的业务需要可能就需要创建新的子类。
异常
标准异常总结
异常 | 说明 | 备注 |
---|---|---|
AssertionError | 断言语句(assert)失败 | |
AttributeError | 尝试访问未知的对象属性 | |
EOFError | 用户输入文件末尾标志EOF(Ctrl+d) | |
GeneratorExit | generator.close()方法被调用的时候 | |
ImportError | 导入模块失败的时候 | |
IndexError | 索引超出序列的范围 | |
KeyError | 字典中查找一个不存在的关键字 | |
KeyboardInterrupt | 用户输入中断键(Ctrl+c) | |
MemoryError | 内存溢出(可通过删除对象释放内存) | |
NameError | 尝试访问一个不存在的变量 | |
NotImplementedError | 尚未实现的方法 | |
OSError | 操作系统产生的异常(例如打开一个不存在的文件) | |
OverflowError | 数值运算超出最大限制 | |
ReferenceError | 弱引用(weak reference)试图访问一个已经被垃圾回收机制回收了的对象 | |
RuntimeError | 一般的运行时错误 | |
StopIteration | 迭代器没有更多的值 | |
SyntaxError | Python的语法错误 | |
IndentationError | 缩进错误 | |
TabError | Tab和空格混合使用 | |
SystemError | Python编译器系统错误 | |
SystemExit | Python编译器进程被关闭 | |
TypeError | 不同类型间的无效操作 | |
UnboundLocalError | 访问一个未初始化的本地变量(NameError的子类) | |
UnicodeError | Unicode相关的错误(ValueError的子类) | |
UnicodeEncodeError | Unicode编码时的错误(UnicodeError的子类) | |
UnicodeDecodeError | Unicode解码时的错误(UnicodeError的子类) | |
UnicodeTranslateError | Unicode转换时的错误(UnicodeError的子类) | |
ValueError | 传入无效的参数 | |
ZeroDivisionError | 除数为零 |
层次结构:
BaseException
+-- SystemExit
+-- KeyboardInterrupt
+-- GeneratorExit
+-- Exception
+-- StopIteration
+-- ArithmeticError
| +-- FloatingPointError
| +-- OverflowError
| +-- ZeroDivisionError
+-- AssertionError
+-- AttributeError
+-- BufferError
+-- EOFError
+-- ImportError
+-- LookupError
| +-- IndexError
| +-- KeyError
+-- MemoryError
+-- NameError
| +-- UnboundLocalError
+-- OSError
| +-- BlockingIOError
| +-- ChildProcessError
| +-- ConnectionError
| | +-- BrokenPipeError
| | +-- ConnectionAbortedError
| | +-- ConnectionRefusedError
| | +-- ConnectionResetError
| +-- FileExistsError
| +-- FileNotFoundError
| +-- InterruptedError
| +-- IsADirectoryError
| +-- NotADirectoryError
| +-- PermissionError
| +-- ProcessLookupError
| +-- TimeoutError
+-- ReferenceError
+-- RuntimeError
| +-- NotImplementedError
+-- SyntaxError
| +-- IndentationError
| +-- TabError
+-- SystemError
+-- TypeError
+-- ValueError
| +-- UnicodeError
| +-- UnicodeDecodeError
| +-- UnicodeEncodeError
| +-- UnicodeTranslateError
+-- Warning
+-- DeprecationWarning
+-- PendingDeprecationWarning
+-- RuntimeWarning
+-- SyntaxWarning
+-- UserWarning
+-- FutureWarning
+-- ImportWarning
+-- UnicodeWarning
+-- BytesWarning
+-- ResourceWarning
装饰器
函数装饰器旨在用来扩充特定的函数或方法调用,而不是对象的全部接口
类装饰器则向一个类的所有实例自动添加基于委托的包装器
元类
字符串
早期的编码注释
写法1:
# _*_ coding: utf-8 _*_
写法2:
# coding=utf-8
内置函数总结
数字相关
排序:sorted
排序任何集合,并且返回一个新的列表作为结果(而不是在原位置修改)
eval
eval会将字符串当做python表达式来执行,并且有返回值,返回表达式的结果
exec
exec会将字符串视作一条语句而不是表达式,并且没有返回值
vars():返回包含被调用的地方所存在的所有变量的字典
文档
内置 dir 函数
抓取对象内所有可用属性列表, 能传入任何有属性的对象
如果不传参数,则可以列出调用者作用域内的变量
只提供属性名称的列表,但没有告诉这些名称的意义
文档字符串:doc
文件的顶端
def 语句块的顶端
class 语句块的顶端
等出现的字符串,都会被附加到对象的 __doc__
属性上
PyDoc:help 函数
help(x)
:接收一个引用值,获取帮助信息(文档字符串信息)
PyDoc:HTML报告
pydoc -g
python -m pydoc -g
py -3 -m pydoc -g
超越文档字符串:Sphinx
标准手册集
其他
其他
在代码中检测类型
在代码中检测类型实际上破坏了灵活性,即限制它只能使用一种类型工作
我们编写对象接口(被支持的操作)而不是类型,这意味着,我们关注一个对象能做什么,而非它是什么
不关注于特定的类型意味着代码会自动适应它们中的很多类型:任何具有兼容接口的对象均能工作,而不管它是什么对象类型
杂
一个类中的函数总有一个隐含的对象
一般来说基于类的类型建立在核心类型之上,并使用了核心类型
多态指的是:操作的意义由操作对象来决定,即一个操作的意义取决于被操作的类型
Python的设计座右铭之一就是 拒绝猜测
方法是与特定对象关联,并作用域特定对象的简单函数,它们只是附属于对象的属性,而这些属性碰巧引用了可调用函数罢了。从更精细的角度看,函数就是代码包,而方法调用同时结合了两种操作:一次属性获取 和 一次函数调用
obj.attrname
:属性获取,得到obj对象中 attrname 属性的值
func(arguments)
:函数调用表达式,向函数传入参数调用,得到返回值
obj.method(argumants)
:方法调用表达式
理应只存在一种 唯一的、显然的 解决方法
不是说不可以有多种代码写法,而是同一个程序中,做一件事的代码有一个就够了
只要时间足够,未来的Python倾向于把大多数常规任务自动化