前言
本文是笔者作为一个java程序员,对《python基础教程》的笔记。学多种不同的语言,知道达到目的有不同方式,就不会被“相”迷惑
python是类似linux shell的解释型语言(其实与javascript更像),只不过使用shell操作多是为了操作计算机,使用python则是为了实现业务逻辑。感觉上的不同在于,如果要创建一个文件夹
- linux,
mkdir dirname
- python,
import os os.mkdir(path,mode)
java是面向对象的,所以对象是第一元素,对任何实例方法的应用都必须通过对象。python不是面向对象的,对于非复杂业务,python代码基本就是函数 + 函数调用
。
基本语法
对于任何一门编程语言来说,数据都分为基本类型和复合类型。
- 基本类型
- 对于java,基本类型是int,float等,复合类型是list、set等
- 对于python,类型不用声明,基本类型和一般语言差不多,包括整数、浮点数、字符串、布尔值。
- 语言自带的复合类型一般是为数据的存取方便,主要有两种形式:
根据索引存取(数组和list)和根据键值存取
。有以下不同点- 有可修改和不可修改两种。类似于java的数组和list
- 一般不要求元素是同一类型
Python 是动态语言,其显著特点是在声明变量时,你不需要显式声明它的类型。除此之外,已经确定类型的变量,可以随时更改其类型。
# 虽然代码里没有明确指定 age 的类型,但是程序运行时隐式推断出它是 int 类型,因此可以顺利执行 age + 1 的动作。
age = 20
print('The age is: ', age + 1)
# Output:
# The age is: 21
age = 20
print(type(age))
# Output: <class 'int'>
age = '20'
print(type(age))
# Output: <class 'str'>
类型转换
- 对于java,
String a = 1 + ""
,float a = (float)1
- 对于python,类型转换与 go 类似,比如
int('8')
将字符串8 转为8,a = str(1)
,注意此处str是一个类型,不是一个函数。
解释型语言一般写起来比较简便,(),[],{}
就可以创建元组(Tuple)、列表和字典。内置类型几乎所有需要用到的方法 库函数(运算符)都有提供,不像java还要搞一个apache collections 库。
- 字符串的基本操作:in(对应java 的List.contains),序列加序列,序列*整数(重复几次),序列按下标取数,格式化
string.formt
,python比较早的版本 也用% 符号来进行格式化 - 元组tuple 是静态的,小括号。元组可以比较 大小 ,比如
(1, 5) < (2, 3) # True
。除了作为不可变列表使用之外,还可用作没有字段名字的记录。 解密 Python 元组的实现原理- 元组存在的意义是什么呢?首先元组可以作为字典的 key 以及集合的元素,因为字典和集合使用的数据结构是哈希表,它存储的元素一定是可哈希的,而列表可以动态改变,所以列表不支持哈希。因此当我们希望字典的 key 是一个序列时,显然元组再适合不过了。比如要根据年龄和身高统计人数,那么就可以将年龄和身高组成元组作为字典的 key,人数作为字典的 value。元组如果可哈希,那么元组存储的元素必须都是可哈希的。只要有一个元素不可哈希,那么元组就会不可哈希。比如元组里面存储了一个列表,由于列表不可哈希,导致存储了列表的元组也会变得不可哈希。
- 从底层对应的结构体来看,与list 相比,元组没有 allocated、也就是容量的概念,这是因为它是不可变的,只支持查询操作,不支持 resize 操作。其次而列表对应的指针数组是定义在结构体外面的,可变对象的具体元素不会保存在结构体内部,而是会维护一个指针,指针指向的内存区域负责存储元素。当发生扩容时,只需改变指针指向即可,从而方便内存管理。
- 列表 list是动态的,
[xx,xx]
,可以append和remove。 - 集合包括set 和 frozenset(不可变), 集合的标准字符串表示形式始终使用
{...}
表示法,{'zhangshan','lisi'}
,唯有空集例外。 - 字典 dict,赋值就像是 json 字符串,大括号。
for xx in xx.keys()
。dict 必须是可哈希的(基于__hash__
,__eq__
)
列表和元组,都是一个可以放置任意数据类型的有序集合,都支持负数索引,都支持切片操作(l[1:3]
返回列表中索引从1到2的子列表),都可以随意嵌套(列表的元素可以是列表),内部实现都是array的形式,两者也可以通过 list() 和 tuple() 函数相互转换。
- 步长切片,
list[start:stop:step]
,如果step为正数,切片将从start索引开始,向列表末尾方向提取元素。如果step为负数,切片将从start索引开始,向列表开头方向提取元素。 list[start:stop:step]
背后的原理def __getitem__(self, key): if isinstance(key, slice): # 此时key = slice(start,stop,step) cls = type(self) return cls(self._components[key]) index = operator.index(key) return self._components[index]
字典本身只有键是可迭代的,如果我们要遍历它的值或者是键值对,就需要通过其内置的函数 values() 或者 items() 实现。其中,values() 返回字典的值的集合,items() 返回键值对的集合。
d = {'name': 'jason', 'dob': '2000-01-01', 'gender': 'male'}
for k in d: # 遍历字典的键
print(k)
name
dob
gender
for v in d.values(): # 遍历字典的值
print(v)
jason
2000-01-01
male
for k, v in d.items(): # 遍历字典的键值对
print('key: {}, value: {}'.format(k, v))
key: name, value: jason
key: dob, value: 2000-01-01
key: gender, value: male
控制语句
- python 不支持 switch 语句,所以多个条件判断,只能用 elif 来实现。
if a
会首先去调用a的__nonzero__()
去判断a是否为空,并返回True/False,若一个对象没有定义__nonzero__()
,就去调用它的__len__()
来进行判断(这里返回值为0代表空),若某一对象没有定义以上两种方法,则if a
的结果永远为True。在实际写代码时,我们鼓励,除了 boolean 类型的数据,条件判断最好是显性的,少来if a
这种形式 。- for 和while 都能实现循环处理逻辑,for 更多用于遍历 集合 使用
for ... in ..
,如果纯粹是循环次数的话,可以for .. in range(1,10)
,死循环while True
,条件循环可以while 条件
。通常来说,如果你只是遍历一个已知的集合,找出满足条件的元素,并进行相应的操作,那么使用 for 循环更加简洁。但如果你需要在满足某个条件前,不停地重复某些操作,并且没有特定的集合需要去遍历,那么一般则会使用 while 循环。PS:也就是while 之后通常是条件,for 之后通常是集合。 - 在 Python 中一切皆对象,对象的抽象就是类,而对象的集合就是容器。所有的容器都是可迭代的(iterable)。而可迭代对象(实现了
__iter__
方法即可被视为可迭代对象),通过 iter() 函数返回一个迭代器(iterator,实现了__next__
方法的对象),再通过 iterator.next() 函数就可以实现遍历。for in 语句将这个过程隐式化。- 为何有了可迭代对象,还要有iterator呢?iterator为了支持next() 要能维护内部状态(比如index),可迭代对象 一般是业务对象,不必要为支持迭代再专门维护成员变量。
- 使用 for 语句的时候,会涉及到两个字节码,分别是 GET_ITER 和 FOR_ITER,GET_ITER 的作用是对一个可迭代对象(iter()),取得它的迭代器,FOR_ITER 的作用是针对迭代器进行迭代(next() )。
- 推导式是Python的一种独有特性。推导式(for后置)是可以从一个数据序列构建另一个新的数据序列的结构体。
variable = [out_exp_res for out_exp in input_list if out_exp == 2]
。包含列表推导式、字典推导式,集合推导式。 - if-else的后置,类似于三目运算符/三元表达式:
c = a if a > b else b
,if中条件满足则返回a,否则返回b。
函数
- 不用声明参数类型(其实就是多态了,必要时请在函数开头加入数据类型的检查)、返回值类型。 python3.5 支持类型提示
def func(变量名:类型) -> 返回类型
。 - 传参可以带名字,此时传参不用按顺序。
- 调用一个函数时,最普通的传参方式是只提供参数值,这种方式就是位置参数。也可以通过参数名称来指定参数值,这种传参方式被称为键参数。对于不定项参数,Python 使用一个list来存储它们,列表使用一个星号
*
修饰,这种方式就是扩展位置参数。如果键参数的名称不在函数定义的形参之内,它们就会被存储在一个dict中,字典使用两个星号**
修饰,这种方式就是扩展键参数。*
和**
表示将list和dict当中的所有值展开,如果不加的话,list和dict会被当成是整体传入。 - 给函数加注释也有一个规范,之后即可使用
foo.__doc__
查看函数文档def foo(): '''这个函数的用途、用法、注意事项等等''' pass
- lambda函数没有名字,是一种简单的、在同一行中定义函数的方法。lambda表达式只允许包含一个表达式,不能包含复杂语句,该表达式的运算结果就是函数的返回值。lambda 的主体是只有一行的简单表达式,并不能扩展成一个多行的代码块。将lambda函数赋值给一个变量,通过这个变量间接调用该lambda函数。lambda 可以用在一些常规函数 def 不能用的地方,比如,lambda 可以用在列表内部,可以被用作某些函数的参数。
f=lambda a,b,c,d:a*b*c*d print(f(1,2,3,4)) #相当于下面这个函数 list1 = [1,2,3,4,5,6,7] list(filter(lambda i:i>2, list1))
- 内建函数,当你定义指定对象,python就会为指定对象赋予特定的一些能力/自省能力,可以使用dir() 函数查看其内部定义的属性和方法。
- filter(funcion,iterator), 过滤得到 iterator 中符合funcion 的 元素
- map(funciton,iterator), 对iterator 每个元素 进行function 转换
- 函数嵌套
- 函数的嵌套能够保证内部函数的隐私。内部函数只能被外部函数所调用和访问,不会暴露在全局作用域,因此,如果你的函数内部有一些隐私数据(比如数据库的用户、密码等),不想暴露在外,那你就可以使用函数的的嵌套,将其封装在内部函数中,只通过外部函数来访问
- 合理的使用函数嵌套,能够提高程序的运行效率。比如 我们使用递归的方式计算一个数的阶乘。因为在计算之前,需要检查输入是否合法,所以我写成了函数嵌套的形式,这样一来,输入是否合法就只用检查一次。而如果我们不使用函数嵌套,那么每调用一次递归便会检查一次,这是没有必要的,也会降低程序的运行效率。
def factorial(input): # validation check if not isinstance(input, int): raise Exception('input must be an integer.') if input < 0: raise Exception('input must be greater or equal to 0' ) ... def inner_factorial(input): if input <= 1: return 1 return input * inner_factorial(input-1) return inner_factorial(input) print(factorial(5))
- 装饰器本质上是一个 Python 函数或类,它可以让其他函数或类在不需要做任何代码修改的前提下增加额外功能,装饰器的返回值也是一个函数/类对象。它经常用于有切面需求的场景,比如:插入日志、性能测试、事务处理、缓存、权限校验等场景。 有了装饰器,我们就可以抽离出大量与函数功能本身无关的雷同代码到装饰器中并继续重用,调用的时候还是和以前一样。@ 符号是装饰器的语法糖。
- 编译语言,需要将代码编译成可执行的二进制文件。为了让操作系统/引导程序找到程序的开头,需要定义这样一个函数。
- Python是一种解释语言,即脚本语言。运行过程是从上到下,逐行进行的,这意味着它的起点是已知的。
- 每个
.py
文件都是一个可执行文件,可作为整个程序的入口文件,意味着该程序的入口很灵活,而且无需遵循任何约定。 - 有时运行Python项目时不需要有指定入口文件(命令行比较常见,例如
python -m http.server 8000
),可能是因为该项目中有main.py文件,在软件包中作为“文件”来执行。
- 每个
- 经常看到或编写以下代码,除了函数名是“main”之外,这段代码与我们前面介绍的main函数没有半点关系,这个函数既不是必须的,也不能确定程序的执行顺序。他们之所以要编写
__name__ =='__main__'
,可能是因为想表明main()
只在直接执行当前脚本时才运行,而在将其导入到其他模块时不要运行。PS: 以go 来类比,就相当于在这个地方写xx_test.go# main file def main(): …… if __name__ == '__main__': main()
面向对象
传统的命令式语言有无数重复性代码,虽然函数的诞生减缓了许多重复性,但随着计算机的发展,只有函数依然不够,需要把更加抽象的概念引入计算机才能缓解(而不是解决)这个问题,于是 OOP 应运而生。
fluent python:面向对象编程全靠接口,支撑一个类型的是它提供的方法,也就是接口。对象协议指明为了履行某个角色,对象必须实现哪些方法,协议是非正式接口,比如只要实现 __getitem__
方法,就可以按索引获取项,以及支持迭代和in运算符。PS:python 支持鸭子类型
- 动态协议,动态协议是隐含的,按约定定义,在文档中描述。Python 大多数重要的动态协议由解释器支持。
- 静态协议,使用typing.Protocol 子类显式定义。如果想满足静态协议,对象必须提供协议类中声明的每一个方法,即使程序用不到。
class Player():
id # 类变量
def __init__(self,name,hp): # 类函数和普通函数的差别就是有一个 self 参数
self.name = name # 成员变量
self.hp = hp # 成员变量
def print_role(self):
print('%s %s' %{self.name,self.hp})
def play(self): # 利用异常机制抛出错误,强制子类实现该方法
raise NotImplemetnedError('play')
user1 = Player('tom',100)
user1.print_role()
# MalePlayer 集成 Player
class MalePlayer(Player):
...
mp = MalePlayer("zhangsan",100)
print('mp的类型 %s' %type(mp))
print(isinstance(mp,Player))
- id 为类变量,类和类的实例都可以访问类变量,但只有类可以修改类变量;如果使用类的实例来修改类变量,那么python会自动给生成一个与类变量同名的成员变量,之后所有通过类的实例来访问和修改类变量,实际上访问和修改的是同名的成员变量。
- name 和hp 是成员变量,一般成员变量都是
self.xxx
,但并不是有的self.xxx
都是成员变量。只有类__init__
内包含的self.xxx
变量,还有__init__
所调用函数中的self.xxx
变量,以上两类才是真正意义上的成员变量。除此之外的类内其它self.xxx
变量只能看作是类内的全局变量。成员变量如果不想被直接访问,需要使用__
前缀修饰 - 所有的类 都继承自 object。 PS:难道python 一切皆对象,对象皆继承object,魔术方法都在object 里。 魔术方法可以理解为数据类型的接口?
- 如果要让内部属性不被外部访问,可以把属性的名称前加上两个下划线,在Python中,实例的变量名如果以
__
开头,就变成了一个私有变量(private)。变量名类似__xxx__
的,也就是以双下划线开头,并且以双下划线结尾的,是特殊变量,特殊变量是可以直接访问的。有些时候,你会看到以一个下划线开头的实例变量名,比如_name
,这样的实例变量外部是可以访问的,但是,按照约定俗成的规定,当你看到这样的变量时,意思就是,“虽然我可以被访问,但是,请把我视为私有变量,不要随意访问”。 - 可以用
@dataclass
注解 修饰class,类似java lombok 的@Data
- 每个类都有构造函数,继承类在生成对象的时候,是不会自动调用父类的构造函数的,因此你必须在 init() 函数中显式调用父类的构造函数。
- 在Python当中没有对public和private的字段做区分,所有的字段都是public的,也就是说用户可以拿到类当中所有的字段和方法。为了规范,程序员们约定俗成,决定所有加了下划线的方法和变量都看成是private的,即使我们能调用,但是一般情况下我们也不这么干。一个下划线允许子类覆盖,Python禁止加了两个下划线的方法被子类覆盖。
- python 没有interface关键字,使用抽象基类定义接口。抽象基类是用于封装框架所引入的一般性概念和抽象的,要抑制创建抽象基类的冲动。 标准库的抽象基类大多在 collections.abc和abc.abc 中定义。
- 多重继承。Mixin 即 Mix-in,常被译为“混入”,是一种编程模式,在 Python 等面向对象语言中,通常它是实现了某种功能单元的类,用于被其他子类继承,将功能组合到子类中。但 Mixin 终归不属于语言的语法,为了代码的可读性和可维护性,定义和使用 Mixin 类应该遵循几个原则:每个 Mixin 只实现一种功能,可按需继承;Mixin 只用于拓展子类的功能,不能影响子类的主要功能,子类也不能依赖 Mixin,如果是依赖关系,则是真正的基类,不应该用 Mixin 命名。
异常
- 异常一种特殊对象,在 Python 中并不区分错误和异常,所有内置异常的基类是BaseException,所有内置的非系统退出类异常都派生自Exception。
- Python 异常处理完整逻辑是 try…except…else…finally
try: 可能产生异常的代码 except 异常: 捕获指定异常后运行的代码 else: try部分的代码没有抛出异常,执行此部分代码
模块
层次从小到大
- 语句
- 函数 def
- 类 class
- 模块 module, 物理上是一个python文件
- 包 package, 物理上是一个文件夹, 包中可以含有模块和包
不可能所有java代码都写到一个xx.java
文件里,自然也不可能所有的python代码(主要是函数和对象)都写到xx.py
里,所以要分模块/文件。这就有一个如何引用其它 模块/文件 对象/函数 的问题。
- 在python里面,导入一个模块使用的是
import 文件名
,python会在sys.path(可以在python里面打印sys.path是些什么目录)里面寻找匹配名称的文件。import 模块名称 import 模块名称 as 别名 from 模块名称 import 方法名 from 包 import 模块 from your_file import function_name, class_name
- 模块本质就是一个
*.py
文件,在模块的内部,可以通过一个全局变量__name__
来获得模块名。 - Python 的 import 语句和 Java 的大不相同,Java 的 import 只是用于编译时引入符号,而 Python 中却会执行要加载的模块。模块可以包含可执行的语句,这些语句在模块初始化的时候执行——当所在模块被import导入时,它们有且只有执行一次。
- 当一个模块首次被导入时,Python 会搜索该模块,如果找到就创建一个 module 对象并初始化它。python加载后的模块都会保存在sys.modules里面。
- 给python文件xx.py起名时,尽量不要用一些公共的包名,比如json等。假设有一个json.py,那么同一个包下的另一个python文件
import json
时就会找到自己写的json.py。(类似java classpath的搜索优先级问题) - Python 是脚本语言,和 C++、Java 最大的不同在于,不需要显式提供
main()
函数入口。import 在导入文件的时候,会自动把所有暴露在外面的代码全都执行一遍。因此,如果你要把一个东西封装成模块,又想让它可以执行的话,你必须将要执行的代码放在if __name__ == '__main__'
下面(避开import 时执行)。其实,__name__
作为 Python 的魔术内置参数,本质上是模块对象的一个属性。我们使用 import 语句时,__name__
就会被赋值为该模块的名字,自然就不等于__main__
了 - 安装第三方模块使用pip 命令
包
- 为了帮助组织模块并提供名称层次结构,Python 还引入了包的概念。包通过层次结构进行组织,在包之内除了一般的模块,还可以有子包。
- Python 定义了两种类型的包,常规包 和 命名空间包。
- 目录和包的区别在于,包会比目录多一个__init__.py的空文件,最初,它是为了将目录识别为Python包而创建的。PS:否则你建个json目录,再import json,python 还以为你在import 官方json库。
- 常规包通常以一个包含
__init__.py
文件的目录形式实现,用来表述包对外暴露的模块接口。我们在导入一个包时,实际上是执行它的__init__.py
文件,这样进行一些包级别的变量或进行一些初始化操作。当一个包包含多个子模块时,可以在__init__.py
文件中批量导入我们所需要的模块,而不再需要一个一个的导入。不过,事实上,这是 Python 2 的规范。在 Python 3 规范中,__init__.py
并不是必须的
其它
编译语言,需要将代码编译成可执行的二进制文件。为了让操作系统/引导程序找到程序的开头,需要定义这样一个函数。简而言之,需要在大量可执行的代码中定义一个至关重要的的开头。Python是一种解释语言,即脚本语言。运行过程是从上到下,逐行进行的,这意味着它的起点是已知的。
- 每个.py文件都是一个可执行文件,可作为整个程序的入口文件,意味着该程序的入口很灵活,而且无需遵循任何约定。
- 有时运行Python项目时不需要有指定入口文件(命令行比较常见,例如“ python -m http.server 8000”),可能是因为该项目中有main.py文件,在软件包中作为“文件”来执行。