入门文章太啰嗦,参考文档太庞大。
总结个精要速查,感觉要更直接些。
如果脑子里记得,直接看快速复习。
如果脑子不记得,这里就当索引吧。
太久没更新,也太久没用Python了,回看自己写的都晕了。慢慢补上示例吧,但精要仍是这篇日志要追求的——2021.3
胡言乱语:Python这个语言有点奇葩,自带Pythoner的文化和约定熟成,其他语言转过来的程序员看Pythonic的代码还真会有点莫名其妙。当这些“约定”比关键字还多时,你会发现其实这东西并不是那么容易上手的,特别是看别人的代码时。明明用人话(应该是标准程序流程)来实现的逻辑,Pythoner可以用更简洁的代码实现(比如一行天书般的代码干了很多事情),其中包含了门外汉不明白的某些“隐喻”,或者被称为语法糖?。无疑,这增加了初学者学习的难度,当然同时增加了入门后的装逼资本。
- 语法与书写风格:缩进风格是Python特有的,每缩进一级相当于一层代码块。注释用#(单行)和''' '''及 """ """(多行)。三引号是定义多行字符串不赋给变量即用于注释。变量、函数命名大小写敏感,__name__之类的变量名是预定义的,如程序是直接被解释器调用执行,则__name__的值为字符串'__main__'。
# This is a test
def foo(p: str):
"""
Description of the function of foo
:param p: Description of parameter a
:return: Description of return value
"""
print(a)
if __name__=='__main__':
foo('Hello world')
- 常用数据类型:Python是动态强类型语言,变量可以保存"单值",对象和函数(实际上Python中万物皆对象,int整型也是,变量名只是对象地址空间的引用,内存是动态管理的,当一个变量被赋新值时并不是在原来地址写入新值,而是原有的地址被放弃交由垃圾回收机制处理,在新地址创建新值并将变量名引用到新地址)
- "单值"数据类型:数值(整,浮,复x+yj),布尔
- 字符串(字符序列,可切片可索引但不可修改元素),字符串定义的前缀修饰:b'123'定义bytes,u'abc'定义unicode字符串(Python3已不需要),r'a\b'不进行转义,f'abc:{abc}'格式化变量到字符串中完成拼接。
- None 表示一个空对象
a = 1
a = a + 1.2 # 升为浮点数
c = a + '-' + b # 错:类型不兼容,必须显式转换
a = '1' # 但可以重新赋值
d = 1 + 2j # 复数,好像一般用不到
# 以上操作本质都是动态内存操作,值或类型变化实际是生成了新值重新绑定到变量上,语法层面显示出动态和强类型特征
- 类型转换:int()/str()/float()/complex(),类型名对应的函数完成转换
- bytes类型与str类型及转换: bytes可以看成是裸字节,这些字节表示什么,需要提供编码信息如encoding='utf-8'
a = b'abc'
b = str(a, encoding='utf-8') # 'abc'
c = bytes(b, encoding='utf-8') # b'abc'
a == c # True
运算符:
算术运算:+-/%,*幂,//整除
逻辑运算:比较类似C语言== > < >= <=,逻辑表达式is/is not,in/not in,not,and,or(优先级递减),当然优先级()走遍天下。比值相同用==,比地址相同用is。相关应用:比类型家谱type().__name__/isinstance()/issubclass()。仿三目运算:a=b if condition else c。
整数位操作:&|^~>><<序列、集合与字典:
- 序列:tuple和list;集合:set;字典:dict。存放多值或键值对
- 序列与集合的特点总结: tuple: ()有序-只读-可重复,list: []有序-读写-可重复,set:{}无序-读写-非重复,有序类型支持索引取值
- 共性:len(O)计数,max(O)/min(O)取最值,O.clear()清空,i in/not in O,for i in O,最值计算可自定义比较的key:
# 示例数据:一个包含字典对象的列表
data = [
{'name': 'Alice', 'score': 85},
{'name': 'Bob', 'score': 92},
]
# 自定义比较函数,按分数找到最高分
def get_score(student):
return student['score']
# 输出最高分的学生信息
print(max(data, key=get_score)["name"]) #Bob
序列可作为参数传入iter()生成迭代器
x = [1, 1, 3, 2]
x = list(set(x)) # 去重
a = (1,2,3)
print(len(a)) # 3
print(1 in a) # True
i = iter(a)
print(next(i)) # 1
print(next(i)) # 2
print(next(i)) # 3
- 索引(仅序列类型支持):
- 支持多维;
- list/tuple有序,0正序起始/-1倒序起始,切片;支持.count(item)对某元素计数;支持+和*运算符累加序列
a = (1,2,3)
print(a[0]) # 1,首位
print(a[-1]) # 3,末位,注意与下面切片的区别
print(a[:-1] # (1,2),切片操作:注意不含末位,这里a[:-1]与a[:2]等价,结果为(1,2)
print(a[:2]) # (1,2)
print(a[:3] # (1,2,3),含末位,大于列表长度不报错,只取到最末位,a[:3]与a[:]等价
- dict无序,O[’keyname‘] 访问元素,字典的key可以是数值序号,更常用是字符串
- 修改(tuple只读除外):
- list通过索引位置O.append(item)/O.extend(seq)/O.insert(idx,item)/O.pop([idx=-1])增/返回并删,索引元素赋值修改,值删除O.remove(),O.sort(key=None, reverse=False)/O.reverse()换序
- dict直接通过['keyname']索引元素增改;O.update(dict)增加,O.pop('keyname',[default])返回并删除元素,迭代用O.keys()/O.values()/O.items()
- set通过O.add(item)/O.update(seq)增加,O.remove(val)/O.discard(val)删除,O.pop()随机删除。
- 通用删除:del var删除变量(对数据的引用)达到删除变量及元素(如能索引到)的目的。
- list,dict,set的赋值=为引用,要拷贝使用O.copy()方法,注意为浅拷贝,元素中的序列类型仍引用原地址。要使用深拷贝,import copy模块,new=copy.deepcopy(old)
- 注意:
- tuple:只读;定义单元素tuple元素后要加逗号结尾,
- set:不重复,可用两个相反类型转换去重list;支持&|集合运算;空集合用set()创建
- [含x表达式 for x in list]生成按各元素按表达式计算的新列表
- 字符串续:
- 编码与表示:python 3内码为unicode,前缀u'字符串'已不必要,'\u十六进制编码'等价于该内码unicode字符。前缀b'字符串'等价于bytes串(ascii)。.encode()/.decode():unicode,utf-8,gb2312,gbk...与对应编码的byte序列互转;前缀r'字符串'忽略串中转义符(raw的意思)
- 转义:\oii八进制数,\xii十六进制数,\000空,行尾\续行
- ’’’ ’’’/“”“ ”“”:生成raw字符串,可用于注释
- 运算:+,*;序列操作(for in,索引,切片,len(),in/not in,iter()参数);格式化(类C):'%s%d%.2f'%('ABC',1,3.1415)
- 方法:.replace(),.split(),.join(),.strip(),.is...系列,.find()/.index()
- 单字符操作:ord(),chr()
- 注意,与C/C++不同Python字符串不能通过下标索引修改其中的字符
- 有时字符串可以看成一种序列类型(没有clear等方法),但可以使用len(),max(),min()函数对其操作
- 与序列类型一样可以用for i in X或内置函数for i, element in enumerate()来遍历,用in和not in检查是否包含字符
- 控制:
- 缩进形成代码块,条件if/else/elif,循环for in/while,break/continue,range(start,stop[,step])常用于生成for in序列
- with context [as var]:(换行) block,语句块结束会关闭上下文,类似try finally,但不捕获异常。常用于文件处理等,这个上下文是一个自定义类,必须实现__enter__和__exit__方法,具体见类。
- pass占位符
- 异常:
try:
except Exception1 as e1: # 捕获特定异常
else: # 无异常的处理
finally: # 无论如何都处理
raise # 抛出异常
异常处理可以层叠,直到最后finally - 函数:def func(para, defpara=default, *args, **args): block return [expression](此处省略换行缩进)
- 参数:str,number,tuple为值复制,其他对象为传引用(事实上作为性能优化,只要不修改值的内容,任何参数传入时都是引用,str/number/tuple会在值被修改时才复制,而函数内创建的值作为返回值,调用者也是直接引用同一地址,直到值被修改而创建新的地址)
- 返回值:return 表达式,可以为任意对象
def foo(param1: int):
print(id(param1))
param1=2
print(id(param1))
return param1
a = 1
print(id(a))
a = foo(a)
print(id(a))
- 调用:指定参数名可无视顺序,不输入的使用默认参数(如有否则报错),不定长参数:从定长位置之后的部分,*声明的会作为tuple传入,**声明的调用时必须以键值对输入参数会作为dict传入
- 函数是一个对象,保存在对应的命名变量中,变量可指向新的函数而“覆盖”系统函数。
- 特殊函数
- 生成器/迭代器:使用yield输出某次结果的函数,函数返回值是一个迭代器对象,使用next(迭代器对象)获得下一次yield输出。yield保留函数作用域结束当前运行输出相应的值,等待下一次被next()调用时继续运行,一般在循环体内实现迭代直至到return语句释放作用域中的变量。只要函数出现了yield,return的返回值就作废了,return仅作为函数终点比如有条件提前结束迭代(函数返回值固定为迭代器对像obj,取下个输出通过next(obj)得到yield返回值)。
def _range(start, end):
while start < end:
yield start # 每次被调用next或for迭代到这里返回start的值,直到最后一个yield结束,局部变量的值仍保持,下次迭代从这里继续往下执行
start += 1
for i in _range(1, 5):
print(i) # 每次迭代得到一个返回值
- 函数参数:函数本身也是对象,可作为参数传给其他函数,函数名也就是一个函数对象的变量引用(Python中你甚至可以覆盖系统函数——函数名也只不过是个指向函数对象的引用,所以,你可以用exit=print这样的语句把exit()函数的功能变成print())。
- 函数返回值/闭包:函数本身可作为返回值,如将函数(母函数)内部嵌套的函数作为返回值返回,从而使母函数作用域中的参数、变量打包入该返回函数以在外部继续访问,该子函数要修改母函数变量在函数定义中要nonlocal声明,注意如子函数中如修改了母函数的变量,该修改会一直保持,再次调用该子函数时可能影响结果——但多次调用母函数返回的闭包子函数相互不影响。
- 高阶函数/装饰器:高阶函数是参数或/和返回值为函数的函数。典型高阶函数:map(func1, list),reduce(func2, list),map中的func1接受一个参数依次计算list元素,reduce中的func2接受两个参数依次从list中取元素计算并将结果作为下次计算的第一个参数。装饰器是一种高阶函数,接收一个函数参数返回一个函数,从而实现了将传入的函数增加功能的能力(返回的函数调用传入函数又运行了自己的额外代码)。
from functools import wraps
def d1(f):
@wraps(f)
def decorated(*args, **kwargs):
print("Before calling")
res = f(*args, **kwargs)
print("After calling")
return res
return decorated
高阶函数是用代码实现的,所以本质上不属于语法层面的规则,但Python规定了用@号加装饰器名放在原函数定义的前一行的语法规则,即覆盖了原函数名,使其指向了装饰器的返回函数。
@d1
@d2
def foo():
pass
# 以上代码等价于代价:
def foo():
pass
foo = d1(d2(foo))
# 所以,@语法 是语法规则,还是语法糖?
需要注意:单改变函数名指向不一定保证正常工作,因包装了旧函数的新函数与旧函数是两个对象,具有不同的属性。完整的装饰器需要用functools模块中的@functools.wraps(传入函数)来装饰返回函数。好吧,本质上wraps装饰器完成了一堆从旧函数到新函数的属性拷贝让返回的新函数“更像”传入的旧函数。装饰器的写法看起来很像C/C++代码中的宏,看到的时候就当是吧,想像一下脑子里展开宏的崩溃状态。。。装饰器似乎更绕人,知道是干什么的就不求甚解了吧。。。
另外,需要知道装饰器可以是一个类的方法,而不一定要是一个独立函数。
@装饰器语法本身可带参数,带有参数,则表示该“装饰器”传入的不是被装饰函数,而是该语句指定的参数,而返回值则是一个真正的装饰器(它的参数才是@语句后面被装饰的函数 ),这个真正的装饰器是一个三层子母函数的中间层。看下面的例子:
# 一个用类封装的不定长参数装饰器的例子
from functools import wraps
class MyAuth(object):
# 此装饰器是某类的方法,使用时用 @Auth.login_required(参数) 修饰其下的函数
# 需要支持参数的装饰需要三层,中间的bridge函数是一个中介,其中使用@warps装饰器完成属性拷贝
@classmethod
def login_requried(cls, *_args, **_kwargs):
def bridge(f):
@wraps(f)
def decorated(*args, **kwargs):
for i in _args: # 打印装饰器参数
print(i)
return f(*args, **kwargs)
return decorated
return bridge
@MyAuth.login_requried(1,2,3,4)
def func(*args):
for i in args: # 打印被装饰函数参数
print(i)
# @MyAuth.login_requried(1,2,3,4)的语法本质是以下两句,b是中介,即上面定义的bridge函数对象。无参数的装饰器无需bridge中介函数。
# b = MyAuth.login_requried(1,2,3,4) # 构建了bridge函数对象b,此时参数被记录在其对应作用域中——所谓闭包。
# func = b(func) # b函数是一个装饰器,返回了新的decorated函数对象
# 再简单粗暴点就是:func = MyAuth.login_requried(1,2,3,4)(func)
if __name__ == '__main__':
func(5,6,7) # 结果是打印 1 - 7
- 匿名函数:lambda 参数表:返回表达式,可作为表达式调用,也可作为函数返回值保留到变量中。
- 内置函数(from www.runoob.com):
_ | _ | |||
---|---|---|---|---|
abs() | divmod() | input() | open() | staticmethod() |
all() | enumerate() | int() | ord() | str() |
any() | eval() | isinstance() | pow() | sum() |
basestring() | execfile() | issubclass() | print() | super() |
bin() | file() | iter() | property() | tuple() |
bool() | filter() | len() | range() | type() |
bytearray() | float() | list() | raw_input() | unichr() |
callable() | format() | locals() | reduce() | unicode() |
chr() | frozenset() | long() | reload() | vars() |
classmethod() | getattr() | map() | repr() | xrange() |
cmp() | globals() | max() | reverse() | zip() |
compile() | hasattr() | memoryview() | round() | __import__() |
complex() | hash() | min() | set() | |
delattr() | help() | next() | setattr() | |
dict() | hex() | object() | slice() | |
dir() | id() | oct() | sorted() | exec 内置表达式 |
- 模块与包
- 一个.py即为一个模块,import 模块名或from 模块 import 函数/变量/类,会导致在sys.path中的路径搜索,可以使用as关键字给导入的模块或对象取别名防止冲突或简化书写
- 一个目录包含__init__.py即为一个包,__init__.py中提供一个__all__列表即可向from 包.模块 import *提供要导入的名称
- import 模块,后面要用模块名.函数名调用;from 模块 import 对象,后面可直接用对象名调用
- __init__.py文件中也可以写代码,定义变量与方法,并可以用包名.变量/方法名来引用或导入,但不建议这么做,更常见的做法一个组件包是把代码写在子包或模块中,但在它的__init__.py中import所有子包中的对象,然后其他代码再通过包名.对象名间接引用或导入,因此不用记住对象所在的子包或模块。
- 关于一个包内模块间的相互引用,如果它们是平级目录的,用import .模块名,或from .模块名 import 对象名,注意点号,但这种模块本身是不能直接通过
if __name__="__main__":
somecode
来直接运行测试的,需要使用另外的方式如用测试框架来测试。这种代码如要运行,要使用import 模块名来引用同级目录模块,即去掉模块名前面的点号。这将导致模块测试时的代码与包被外部代码使用时的不一致,因此不建议模块内部写测试代码(一种不优雅的做法是加路径到sys.path搜索路径)。
基本原则:
1.在同一级包下的两个子module之间, 有引用关系(并采用relative import相对路径方式导入)时, 应当在包的上级目录中运行这些代码, 而不应该在子模块运行,而module之间用相对路径方式引用,如.代表当前目录,..代表上级目录,...代表上上级目录,以此类推;
2.因此当需要在子模块中引用这些代码来作为程序运行时, 需要弃用点号相对引用, 改用模块名或者包名.模块名的方式引用;
- 模块名方式的引用,通俗就是去掉那个.号; 但是这个只适用于自定义的模块名与sys.path路径下模块不能出现重名;
- 包名.模块名的方式需要在sys.path中添加包的上级目录路径; 又因为不推荐在子模块里面引用同一包中的上层模块. 所以99.99%的情况使用sys.path.append(os.path.dirname(os.path.dirname(os.pathabspath(__file__)))). 可以满足需求; 其他修改sys.path的方法当然也是可以的.
总结: 不要直接运行导入了同级别模块的模块. 而应该在包的上层目录导入这个模块运行, 要注意设计模式本身. 从而避免代码杂乱无章.
- 作用域:读查找顺序:局部变量->nonlocal变量如闭包->全局变量->内建变量,修改:局部和内建可改,nonlocal和global要先声明
- 注意:全局变量是模块级的,要在多模块的应用中共享全局变量,需要import module,然后用module.variable引用,建议用一个模块专门管理所有全局变量。from module import variable,会从引用的模块拷贝出一个本地变量,对本地变量的修改,不会影响原模块的值,即import之后,两个变量就无关了。
- _开头的变量约定为模块内私有但不强制
- 关于标准库 https://www.runoob.com/python3/python3-stdlib.html
- 类
- 继承,支持多重继承,语法:class subclass(base1,base2,...): ,如多个父类有相同属性或方法,按MRO顺序即前面的优先继承到子类。
- 方法首参数必须为self/this
- 子类可重写父类方法,并用super()复用父类方法
- 访问权限:
- __开头的成员为私有
- @property装饰器,在命名为属性的方法前加@property,用该方法返回数据成员,同时自动创建了一个@属性名.setter的装饰器,用来修饰setter方法修改数据。没有setter方法的属性为只读。
- __init__:构造方法,其中初始化的self.变量名成为数据成员。单独写的数据成员为类静态成员。
- __str__:支持对象以字符串输出显示
- __call__:支持对象当函数调用
- __contains__:使对象支持in查询
- 运算符重载:重写__add__,__sub__,__mul__,__truediv__等方法
- @classmethod和@staticmethod装饰器,使方法成为类方法或静态方法。静态方法不需要第一个self参数,静态方法只能访问静态成员。
- 迭代器:实现__iter__方法初始化并返回自己和__next__方法返回下个元素,用iter(clsname)实例化obj,用next(obj)迭代
- 能使用with的类:实现__enter__,__exit__方法。
- 作用域
Python 中只有模块(module),类(class)以及函数(def、lambda)才会引入新的作用域,其它的代码块(如 if/elif/else/、try/except、for/while等)是不会引入新的作用域的,也就是说这些语句内定义的变量,外部也可以访问。
基本规则:
- 外层不能访问内层,无嵌套关联的内层之间也不能访问
- 内层能访问外层,但不能修改,要修改必须用global(访问最外层)或nonlocal声明(访问嵌套的上层)要访问的变量。
Last. 吐槽:Python是一种语言层面极灵活极没有规则的语言,然后用了一些语义与约定代替其他语言的特性(所谓语法糖?),比如:
- 没有代码块标识,只有用:号跟随的缩进能标识为代码块:缩进是强制的,不要指望从网上复制粘贴来的混乱代码能reformat
- 命名约定代替了关键字,如:
- __init__.py作为包标识,当然目录名为包名,文件名为模块名的做法Java也一样
- 各种特定的__XXX__变量比关键字多,也就是说起个名字,加个前缀就等于其他语言里的关键字定义
- _前缀与模块私有变量:约定但不强制
- 装饰器的语法,算语法糖,还是语法规则?如果单纯语法上能定义装饰器,为什么还需要额外的functools.wraps模块?通过@property装饰器实现类属性定义,这个@property装饰器算是Python语法的一部分么?感觉上装饰器更像是编码技巧,编程模式层面的东西。。。
- OOP编程里组合优于继承,好的编程思想会形成习惯,但是真的一切都要模式化吗?好的代码没学过设计模式的人看了也会觉得“优雅”吧,但是像Mixin这种感觉真是为模式而模式了,但是你不得不去学去用,因为你要看别人的代码,不得不去接受这种Pythonic风格。
- 太多太多约定和模式,加上标准库,所以学Python可以很精要(学很少的东西就有了一堆零件),又无法很精要(但是要组装成能上战场的武器,你得看一大堆“使用手册”),当然,如果做一个数据工作者,用Jupyter Notebook就够了。