Last Updated on 2020年7月20日
这里谈一下个人的学习建议. 首先阅读官方的入门教程, 看完这一部分,对于一个熟练的C++程序员,应该可以凑合写出可堪一用的代码了. 然后是Python in a Nutshell 3rd第七章之前的内容(不含第七章),这一部分看完, 你就能基本了解Python的运行机制, 写出质量合格的代码了(但可能不够Pythonic).
上面两步完成后, 对一个熟悉C++的程序员而言, 基本任何Level的Python资料都可以看了. 如果偏应用,你可以继续看Python in a nutshell中感兴趣的部分(或者任何一本偏应用的书); 如果偏理论,你可以选择Fluent Python.
最后,本文仅仅简单扼要的说明了Python和C++不同的一些特性, 可以作为补充阅读.
书评
Python的受众非常广泛, 导致有非常多的Python书籍是完全面向初级读者,甚至无计算机背景的”程序盲”而写的, 尽管这些书一般风评很好, 但是看起来真的可能让你着急. 我简单说说我看过的书(都没有仔细研读过).
- < Python In a Nutshell>: 面向有其他语言经验的读者, 既言简意赅的把所有特性陈列出来, 也详细的说明了重要的特性, 详略得当. 特别是这本书介绍了近来Python的业界生态,如重要的第三方库,IDE等(从2019年看仍不过时), 非常有助于你迅速找到状态.
- < Learning Python>: 面向首次学习编程的人. 上册主要在说语言特性,下册偏应用. 由于过于啰嗦,没看完就停了
- < Beginning Python: From Novice to Professional>: 面向初级读者, 足够简单,总体内容比较浅,不如官方文档好.
- < Python Cookbook> : 面向进阶Python用户, 大致相当于C++的< Effective C++ > ,条目之间比较独立, 工程价值比较高
- < Core PYTHON Programming (2e)> : 面向进阶Python用户, 但内容太过老旧, 现在(2019)已经没有必要去了解Python2的特性了. 注意,”Python核心编程(第三版)”(Core Python Application Programing) 完全是一本讲应用的书籍.
一般性主题
- Python提供了一个相当开放的运行框架,你可以拦截很多操作,实现各种骚trick, 最好不要干这种事.
- 与C++不同
a
,a.m
,a[key]
使用的方法有区别. 了解这些不同有助于你理解一些看起来奇怪的机制.- 例如
a[0:2]=[1,2,3,4,5]
这样的语法,在C++的惯例中意为a.operator[](0:2).operator=([1,2,3,4,5])
,我们操作的是a.operator[](0:2)
返回的某个对象,且这个对象通常不是a
,在Python中,其实它的意思是a.set(range(0,2),[1,2,3,4,5])
,我们是在直接操作a
. a=expr
这样的语句称为”plain assignment”,只有引用绑定的语义.其中a
必须是一个标识符.a[key]
和a.m
相关的操作总是会触发函数调用,例如:- 作为右值时,
a[key]
相当于a.get(key)
,一般会返回某个对象的引用 - 作为左值时,
a[key]=expr
相当与a.set(key,expr)
- 作为右值时,
- 例如
- 一般而言,Python称呼一个对象为”不可变”时,意思是:尝试修改该对象时,不会inplace原地操作,而是产生一个新的对象.由于Python的限定很弱,当我们使用一些习惯上称为”不可变”的类型时,不要通过任何trick来修改其内部的值.
- 逻辑上,所有的内置容器类型中存储的都是引用,而不是具体的对象.
- 在C++看起来, 相当于存储的是指针.
- 对容器有浅拷贝和深拷贝的概念,浅拷贝只拷贝一层, 深拷贝则保证创建完全独立的两个容器.
- Python的惯例中,
a.foo()
从不返回self
- 如果返回了同类型的对象,一定是一个新实例, 且此时
a
绑定的对象没有任何变化. - 如果返回了
None
,则说明a
绑定的对象被修改了.
- 如果返回了同类型的对象,一定是一个新实例, 且此时
del a
的效果:解除名字与对象的绑定, 也就是是删除名字并将对象的引用计数减1.del a.m
或del a[key]
将触发函数调用.
- 对于各种运算符,解释器可能会解释为不同的函数调用. 如
x+=y
可能是x.__iadd__(y)
,或者x=x.__add__(y)
. - 由于Python的语法非常多变,所以用
()
辅助解释器解析代码非常常见.- 如果不用
()
,for num in list
默认是被解析为一个rangefor语法,这里缺少冒号:
,肯定报错. 而(for num in list)
则将被解析为一个列表生成器对象. ()
被解释为空的tuple,(obj)
被解释为obj
,(obj,)
被解释为仅包含obj
的tuple
- 如果不用
- 逗号表达式做右值时, 整体是tuple (一般用
()
包住来强调这一点) - 逗号表达式做左值时,将对应拆包赋值语法,即
逗号表达式 = iterable
- 列表推导可以展开成for循环,对于复杂的情况,只需要从左到右不断缩进展开即可.本质上列表推导语句创造了一个生成器,并按照
list(iterable)
的形式创建对象..
list_new=[expr for num in list_num if num >8]
# 等价于
num_gen = (expr for num in list_num if num>8)
list_new=list(num_gen)
# 等价于
def num_gen2():
for num in list_num:
if num>8:
yield expr
list_new=list(num_gen2())
- python3的字符串
str
逻辑上是一个UTF-32数组,直接存储原始的代码点.这种设计导致了一些基本的交互问题- 输出至其他环境时,需要指定编码,将其转为字节流.即
bytedata=str.encode()
- 输入字节流时,需要指定解码,将其转换为UTF-32数组.即
str=bytedata.decode()
- 输出至其他环境时,需要指定编码,将其转为字节流.即
函数
- 函数定义语句只会执行
def foo
那一行,它将创建一个函数对象,并绑定到foo
上.- 函数体是在调用时才执行的,只要函数体内没有语法错误就OK,其他错误将推迟到被调用时
- 默认形参是在执行
def
时创建的, 它对应了某个对象,在没有传入实参时,就会传入它.如果这个对象是可变的,就可能引起一些问题.
- 在函数内可以嵌套的定义函数,称为闭包,闭包的
def
是在函数执行时才动态进行的. - 如果函数
foo
的输入只有一个, 那么foo
就可以被当做装饰器使用,支持装饰器语法,装饰器语法常常用于wrap
输入函数,并生成一个新的函数
@foo
def bar(...)
pass
#等价于
def bar(...)
pass
bar=foo(bar)
生成器
Python的生成器最初的设计目的很简单: 节约内存的可迭代对象, 但经过python3的拓展后,生成器还可以用作实现协程coroutine的工具.
* 从语法上说,生成器是带有yield
语句的函数.
* 这种函数的调用语句不会返回值,而是返回一个新的生成器对象.
* 生成器对象会记录自己的函数栈及pc等, 每次外部重新触发生成器对象时,会从上一次yield的地方开始执行.
* 生成器对象执行return
时,会触发generator.close()
def foo(x,end):
while x<end
x*=2
x+=1
yield x
return
generator=foo(0,10)
next(generator)#吐出1
next(generator)#吐出3
- python3扩充了
yield
的功能,以支持”coroutine”- 添加了
yield from
语法,它可以转发yield
- 和”rangefor”类似,
yield from foo(...)
只会执行foo(...)
一次, 之后会直接转发值. yield from
可以用于实现递归, 其开销和递归函数调用是一致的.
- 和”rangefor”类似,
yiled
可以返回值,它返回的值是外部通过generator.send(value)
发送的,外部发送值后,控制流将转给generator,yield
语句处将返回value
,并继续执行.- 如果外部通过
next(generator)
来切换控制流,那么相当于generator.send(None)
- 如果外部通过
- 添加了
generator.throw(obj)
可以发送异常,throw
过去的异常在generator看来仿佛是被yield语句抛出的,所以yield语句也可以放在try-except
中del generator
会触发generator.close()
,close()
会调用generator.throw(GeneratorExit())
- Python规定,在
generator.close()
后,generator
必须rasie
一个GeneratorExit
型的对象,用于通知解释器自己已经正常结束了. 如果你不去catchgenerator.close()
发送过去的异常,那么这个异常就一定会被yield语句抛出, 也就默认满足了这个条件.
- Python规定,在
作用域级别的名字搜索
- 如果域X不含名字,那么依照:Local-Enclosing->Global->Builtin逐层查找
- Local:当前所在作用域就是Local.
- Enclosing: 仅对闭包有这一步, 闭包可以引用外层函数体定义的名字,直到外层不是函数体为止.
- Global:
.py
文件的顶层是该模块的global
- Builtin:内置名字
- 没有特殊处理时,
a=expr
这样的”plain assignment”语句一定会创建一个新名字, hide外部作用域的名字.通过”global”和”nonlocal”可以使得当前作用域的”plain assignment”能重新绑定位于外部的名字,而不是hide.- “nonlocal x”: 按闭包的规则向外查找
x
,不会到达global - “global x”: 在”global”内查找
x
- “nonlocal x”: 按闭包的规则向外查找
Python OOP
Python3的OOP模型和Python2很不一样, 和Cpp也很不一样. 由于我不了解Python2,所以至多仅和Cpp对比
- Python中,
class C
这样的语句实际创建了一个”type object”, 然后把C
绑定到这个对象上. 本节中, class用于指代一个自定义类型, 用C
举例, instance指代一个class的实例, 用x
举例.C
和x
是相当独立的两个对象 - Python没有静态绑定,所有
??.attr
型语句都是通过运行期的名字查找来动态确定的. 这是实现”鸭子类型”和”多态”的基础,一般情况下:- 绑定时
??.attr=expr
:总是执行??.__dict__["attr"]=expr
- 引用时
??.attr
:从??.__dict__["expr"]
开始,顺着继承关系逐步查找所有__dict__
,直到找到为止. - 如果
C
的继承体系中存在名为attr
的property
,那么除C.attr=expr
外, 对C.attr
和x.attr
的访问/绑定/del都将被拦截为property
的fget/fset/fdel
- 绑定时
- 从Cpp的角度看,
C
的attr相当于是static
成员,主要通过class body
定义, 而x
的attr
是对象的普通成员,只能在__init__
时创建- 在通常情况下,
C.foo(args)
的调用是简单直白的,x.foo(args)
则会被解释器替换为C.foo(x,args)
- 在通常情况下,
Atrribute机制
- Python OOP中的诸多特性都是通过其神奇的”attribute”使用机制实现的.主要包括:多态, property, bound method.
- 除了少数内置的特殊attribute可以直接被解释器使用,其余attribute都是通过
__dict__
间接实现的- 每个class和instance都有其独立的的
__dict__
- class的
__dict__
是不能直接写入的,本文中的写入操作仅仅是表达这个意思.
- class的
- 在类定义阶段创建的
attr=expr
,相当于是直接操作C.__dict__["attr"]=expr
- 每个class和instance都有其独立的的
- 绑定
C.attr=expr
- 总是执行
C.__dict__["attr"]=expr
- 总是执行
- 引用
C.attr
- 按继承关系逐层向上确定
v=??.__dict__["attr"]
是否存在,如果找到的对象v
有__get__
,则会触发v.__get__(None,C)
,否则就是v
- 按继承关系逐层向上确定
- 绑定
x.attr=expr
, 直接触发x.__setattr__("attr",expr)
, 这个函数的默认实现为:- 从
C.__dict__
开始,按继承关系逐层向上确定v=??.__dict__["attr"]
, 若v
存在,且v
有__set__
,则触发v.__set__(x,expr)
- 否则, 执行
self.__dict__["attr"]=expr
- 从
- 引用
x.attr
,直接触发x.__getattribute__("attr")
,该函数的默认实现为:- 从
C.__dict__
开始,按继承关系逐层向上确定v=??.__dict__["attr"]
,若v
存在,且v
同时有__get__
和__set__
,则触发v.__get__(x,C)
- 否则,如果
x.__dict__["attr"]
存在, 则就是它. - 否则,再次从
C.__dict__
开始按继承查找,如果最终得到的v
有__get__
,那么触发v.__get__(x,C)
- 如果最终还是找不到,就会触发
x.__getattr__("attr")
,这个函数的默认实现就是报错.
- 从
property
- property就是同时定义了
__set__,__get__,__del__
的特殊类,当class的某个attr是property时,因为attribute的使用规则,仅C.attr=expr
仍是常规语义,C.attr,x.attr=expr,x.attr
都会被拦截为预先设好的函数调用.
bound method
- 首先,所有
def
创建的函数对象都定义了__get__
而没定义__set__
. 本节只讨论def
型函数 - 对于
C.foo(args)
这样的调用,找到foo
后一定会触发某个可调用对象v
的f=v.__get__(None,C)
, 返回一个普通的function对象f
,最终调用f(args)
,再转发为v(args)
. - 对于
x.foo(args)
这样的调用,查找foo
的过程中可能性就多一些.- 如果
x
通过x.foo=callable
绑定了自定义的可调用对象callable
, 语义上就只能是callable(args)
- 如果
x
没有绑定自己的foo
,那么x.foo
最终就肯定会触发某个函数v
的bm=v.__get__(x,C)
,返回一个 bound methodbm
,bm
内部会持有x
和v
,在触发调用bm(args)
时,实际会转发给v(x,args)
- 如果
其他
class C
定义时,”class body”是完整执行的,只有在class body执行完之后,才绑定到C.- 在class body执行完成前,是不能使用
C
这个名字的. - 在 class body 中, 按照LGB的顺序检索名字, 如果嵌套的定义class, 要注意名字检索问题.
- 在class body执行完成前,是不能使用
- 所有自定义class都隐式的继承自
object
,从而获得默认__init__
,__new__
等dunder函数 - 在class body内定义函数时,函数体内的名字查找遵循LEGB的规则, 所以class body的作用域并不在其中, 因此,函数不能直接使用类的attribute,只能通过
C.attr
或self.attr
这样的形式使用- 函数定义时,函数体不会执行,只有在调用时才会执行, 所以
C.attr
和self.attr
是没问题的
- 函数定义时,函数体不会执行,只有在调用时才会执行, 所以
- 可以用
@classmethod
,@staticmethod
来修饰类内的函数,经过修饰后,其调用形式将不按默认的C.foo(args)
或x.foo(args)
流程处理.- 这种带修饰的函数与一般的函数相比,仅仅是
__get__
的行为不一样,名字检索的流程是一致的
- 这种带修饰的函数与一般的函数相比,仅仅是
- instance构造的流程大致相当于C++的”malloc + ctor”
x=C(args)
#等价于
x = C.__new__(C, args) # `__new__`是一个classmethod, 返回的空对象x连__dict__都没有
if isinstance(x, C): x.__init__(args)
- 可以通过拦截
__new__
来管理对象的创建, 下面就是一个典型的单例模式- 拦截
__new__
,仅在需要的时候创建对象. - 拦截
__init__
,以避免重复的初始化同一个对象.如果派生类要添加自己的新成员, 那么就必须写这样逻辑的__init__
- 拦截
class MyClass(BaseClass):
_instance = None
def __new__(cls, name):
if not cls._instance :
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self, name):
if hasattr(self, "_inited") and self._inited:
return
super().__init__()
self.name = name
self._inited = True
foo = MyClass("1")
print(foo.name, id(foo))
foo = MyClass("2")
print(foo.name, id(foo))
- 如果在class body中定义了成员
__slots__=("name1","name2"...)
,那么这个class的所有instance都只能绑定__slots__
提供的name
作为attribute__slots__
将抑制解释器创建__dict__
,以节省空间- 继承类不受影响, 继承类必须在其class body中定义自己的
__slots__
super(cls,obj)
:语义比较复杂,在仅使用单继承的场合中,super()
就是指父类的class
,可以使用super().foo(self,args)
这样的语法
导入系统
- Python的导入系统是基于
Module-模块
实现的.- 模块对象如果有了
__path__
属性,就会被作为Package-包
来处理,也就是说,”包”是特殊的”模块”.后文用P
特指包对象,用M
特指一般的模块对象. - 传统上,包特指包含了
__init__.py
文件的目录(__init__.py
内容可以为空,但必须存在),目录名就是包的名字. - 内置模块
bultin modules
不对应某个独立的文件,其余的模块都对应了一个python模块文件. - 例如,名为
XMod
的模块一般对应了一个XMod.py
,或者名为XMod
的文件夹(该文件内必须包含__init__.py
)
- 模块对象如果有了
简单导入
import X
或from X import item
时,对X
的检索.- 1.优先在
builtin module
中查找名为X
的模块 - 2.在启动脚本
main.py
所在的目录查找名为X
的模块(注意,如果main.py
是符号链接,那么实际是从原始文件位置查找的) - 3.在
sys.path
中的指定目录下查找 - 4.在
PYTHONHOME
内的.pth
指定目录下查找
- 1.优先在
注意,在Python查找模块时,总是不会自动进入子目录.
NOTE: python的实际实现中: 解释器启动后,
sys.path
的初始值是环境变量PYTHONPATH
,会自动把作为main的.py
文件的所在目录插入到sys.path
的头部,把PYTHONHOME
内*.pth
文件的内容加载到sys.path
尾部. 所以对python解释器而言,它只需要在sys.path
中查找即可.
import X
后,X
的可用attr
是由模块开发者通过__all__
设定的,__all__
中存储的名字称为模块的公开属性(public attr).- 没有在
M.py
手动定义__all__
时,M.py
内的所有非_
开头的全局名字都会被添加到__all__
中 - 没有在
__init__.py
中定义__all__
时,__init__.py
内的所有非_
开头的全局名字都会被添加到__all__
中,这个__all__
将作为Package的可用attr列表. - 换言之,对于一个包,空的
__init__.py
将使这个包对象没有任何attr
可用
- 没有在
from X import item
的机制随着item
不同,意义不同.- 优先在
X
的模块属性中查找名为item
的属性(不必是公开属性),如果找不到名为item
的属性,将尝试查找X.__path__
下名为item
的模块(即item.py
或名为item
的文件夹) from X import *
相当于import X
,然后再把X
的所有attr
alias 到当前空间中. 也就是说,只有模块的公开attr会被引入当前空间,不能引入私有的attr.
- 优先在
- 通过
from X import item
进行导入时,模块虽然完整载入了,但是并不会把模块对象绑定到名字X
上,所以当前空间无法使用X
绝对路径导入
import A.B...Q.X
或from A.B...Q.X import item
时,对A.B...Q.X
的导入- 限定: 最后一项
X
必须是一个模块(可以是包,也可以是普通模块) - Python先按简单导入的规则查找名字为
A
的模块(如A.py
或名为A
的目录),载入A
模块,并绑定到名字A
上 - 然后查找
A.B
- 如果
A
不是包,就会直接报错 - 如果A是包,就在
A.__path__
目录下查找名为B
的模块,载入该模块,并绑定到A.B
上- 显然,把模块对象绑定到
A.B
上的操作是在A
载入后执行的,如果A的__init__.py
定义了名为B
的公开属性,则该属性会被覆盖掉.
- 显然,把模块对象绑定到
- 如此循环下去,直到X被载入.
- 限定: 最后一项
- 从上面可以看出,
A.B....Q.X
中A
到Q
必须是包- 中间过程的所有包都被完整载入了,在当前空间中
A
,A.B
,A.B.C
等等都是可以完整使用的名字.
from A.B...Q.X import item
和简单情况一致
相对路径导入
- 只有
from
支持相对路径导入, 规则和from
的绝对路径导入一致,但要求当前目录必须是一个包.
其他
- 优秀的实践风格:
import
导入的总应该是一个模块对象.import X
已经满足from A.B.C... import X
:X
总应该是一个模块
- 每个模块都有它自己的私有符号表,该表用作模块中定义的所有函数的全局符号表
- 每个模块逻辑上都有自己私有的一份
bultins
,可以安全的修改其中的bultins.attr
,而不影响其他模块 - 如果要修改自己模块的builtins,需要先
import builtins
,再直接修改builtins.attr
即可
- 每个模块逻辑上都有自己私有的一份
- 模块载入时,会把载入的模块对象额外添加到
sys.modules
列表中sys.modules
相当于cache,只有未import
过的module才会触发module
对象的创建, 否则import会直接从sys.modules
绑定.importlib.reload(X)
可以用于刷新sys.modules
中已经加载的模块.- 注意,
reload
只是创建了一个新的module
对象,并绑定到sys.modules
中旧对象的位置,如果之前通过foo=M.attr
这样的语句绑定过,那么foo
使用的仍然是旧模块对象的attr
importlib.import_module("modname")
可以手动创建一个模块对象,从而让我们在不刷新sys.modules
的同时使用新的module
Pipenv
- pipenv 是一个相对独立的工具,可以通过pip/apt/brew 安装.
- 官方的venv最初用于替代virtualenv, 而现在的pipenv似乎是要把它们都替代掉.
- pyenv仍然是独立的组件.
- virtualenv的风格是创造一个”虚拟环境”,仿佛你在一个新的shell里执行操作. pipenv的风格则将更像是remote controller, 你通过指令与虚拟环境进行交互,仅在必要的时候才进入虚拟环境的shell.
异常
- Python的异常机制是围绕
raise,try
展开的- 如果后跟了
finally
,那么无论try-except
语句块如何结束,都将执行finlaly
(即使在try中return也会触发finally)
- 如果后跟了
try:
expr
except TypeName [as err]:
pass
finally:
pass
- 尽管
try-except
支持else
语法,但是同样的,这个else引入的复杂性要大于它的便利性. - 最后一个
excpet
一般不带有任何TypeName
,作为default
来接受任何异常. - 由于Python的设计中静态检查很弱, 所以很多检查工作需要放到运行期执行,常常就会涉及异常.
- 例如, 我们希望
foo(obj)
仅处理某一个类型的对象,那么,检查isinstance(T,obj)
失败时,一般就会抛出一个异常. - Python自身大量的使用这种策略, 当检查到不符合预期的情况,就直接抛出异常
- 例如, 我们希望
- RAII异常安全的Python实现(基于
with
)- expr需要返回一个对象
tmp
,该对象支持__enter__
和__exit__
,可选的as可以用于为tmp命名. - block执行前,会自动执行
tmp.__enter__
- 无论block是否被异常中断,都可以保证block结束时,
tmp.__exit__
被执行
- expr需要返回一个对象
with expr [as varname]:
block