初学Python会遇到很多自己从未见过的语法,这些语法在其它编程语言中都没见过,有些语法是Python独创的,有些语法只是改变了书写形式自己又造一种新形式,这里罗列一下我认为Python中比较特殊的语法。
1. pass占位符
当函数或者代码块中不需要写任何代码时需要显式的写pass, pass表示占位符, 如果不写会报语法错误。
在其它编程语言中如果一个方法是空的给出一对大括号{}就表示方法体是空的,但是Python的方法体不是用大括号表示的而是用冒号+缩进方式表示的,Python为了表示一个空的方法体发明了这个pass占位符。
public void test() {
// java空方法体
}
def func():
pass
if True:
pass
2. Python Package:_ _ init_ _.py
Python中的__init__.py作用:
2.1 标识一个普通的文件夹为包Package而不是一个普通目录。
在编程语言中一般都使用package来管理源文件,用于对源文件分类,防止命名冲突,其实包就是一个普通的文件夹。像Java直接在src目录下New Package就创建了一个包,src表示源文件位置。但是Python创建Package时不但会创建一个文件夹,还会创建一个__init__.py的文件,这个文件的作用用于标识此目录是一个package而不是一个普通的文件夹。Python中区分包和普通目录,普通目录就类似于java中的resources目录。
像Java直接就约定好源文件src目录了,maven约定好了测试类test和资源文件resource的位置。但Python没有任何约定,没有约定的后果就是你创建一个文件夹不知道这个文件夹究竟是一个包还是一个普通的文件夹,Python的解决办法是如果你要创建包那么这个包下必须有__init__.py文件这个文件才被认为是package,如果没有这个固定的文件就认为这是一个普通的文件夹。
在开发中大家普遍认为约定大于配置,如果不约定就会非常的灵活,太灵活也不是什么好事。如果一个项目有很多个包,每个包下都要有个__init__.py文件,个人感觉显得臃肿,不优雅,个人不太理解这种设计。
2.2 配置该包允许被导出的模块__all__
使用通配符*导入包下的所有模块时需要在__init__.py文件中定义一个__all__属性,属性值是可以被允许导入的列表,只有列举出来的模块才允许被导入。
# 在Python3.8中如果models包下__init__.py文件没有定义__all__,那么通过*号不会导入任何模块
test.py
from models import *
print(dir())
# 如果在models.__init__.py文件中定义了__all__, 那么只有被列举的值才允许被导入
__all__ = ['model', 'model2']
views.test.py
# 定义了__all__所以只有model, model2被导入进来了
from models import *
print(dir())
2.3 package每次被导入时都会执行__init__.py文件中的代码。
init.py文件也是一个普通的py源码文件,也可以写任何python代码。可以写个print测试一下。
models.init.py
print('package models imported...')
views.test.py
# 执行该语句就会执行models.__init__.py中的print代码
from models import *
注意:不同文件每次执行from models import *
都会执行models.init.py中的print代码,但是同一个py文件如果写多次只会执行一次, 这样设计不知道什么场景下会被使用到。很多资料都说不建议在.init.py文件中写业务代码。
3. 正索引和负索引
python中支持两种索引,正索引和负索引(负索引语法还是第一次接触到,虽然第一次接触但是并不反感这个语法,反而感觉这种语法很好很赞成),可以用于字符串、列表、元组:
- 头索引(正索引):从左边开始,最左边第一个下标为0,最左边第二个下标为1
- 尾索引(负索引):从右边开始,最右边第一个下标为-1,最右边第二个下标为-2
索引:一个下标称之为索引 [1]
切片:两个下标称之为切片 [1:3]
>>> foo = 'abcdef'
>>> foo[0]
'a'
>>> foo[0:2]
'ab'
>>> foo[-1]
'f'
>>> foo[-3: -1]
'de'
>>> foo[-3: ]
'def'
>>> l = [1, 2, 3, 4]
>>> l[0] = 0
>>> l
[0, 2, 3, 4]
>>> l[1:3] = [22, 33]
>>> l
[0, 22, 33, 4]
>>> l[1:3] = []
>>> l
[0, 4]
>>> del l[1]
>>> l
[0]
4. while else 和 for else
循环代码块中可以有else代码块(我也是第一次见到这种语法),当循环条件为False时执行else。
else表示正常结束循环体才会执行,如果循环体中遇到break时不执行else代码块的。
i = 0
while i < 5:
print(f"{i} 小于5")
i += 1
else:
print(f"{i} 大于等于5")
# 执行结果
0 小于5
1 小于5
2 小于5
3 小于5
4 小于5
5 大于等于5
for i in [1, 2, 3]:
print(f"i={i}")
if i == 2:
break
else:
print(f"for ended, current i = {i}")
# 执行结果
i=1
i=2
5. 三位运算符
Java中的三位运算符是用 ? : 来实现的。Python中是用if else来实现的(突然换种语法感觉不太适应)
int a = 2;
int b = 1;
int result = a >= b ? a : b;
System.out.println(result);
a = 2
b = 1
# if条件成立返回if前面的表达式,if条件不成立返回else对应的表达式
result = a if a >= b else b
# 2
print(result)
6. 文档注释doctest
Python中的文档注释doctest是写在模块的第一行或者函数的第一行使用三个引号写的一段字符串注释,注释中一般包括函数作用的解释以及给出如何使用函数的示例程序。
文档测试的作用:
- 给出一些示例代码,便于快速了解使用
- 通过工具来生成文档,类似于Java中的Javadoc
def abs(n):
"""
Function to get absolute value of number.
usage::
>>> abs(1)
1
>>> abs(-1)
1
>>> abs(0)
0
"""
return n if n >= 0 else -n
if __name__ == '__main__':
import doctest
# 执行文档测试
doctest.testmod()
7. del 关键字
del 关键字用于删除列表、字典中的元素,或者删除变量。
del这种用法即不像面向过程的语法(面向过程一般都是调用函数),也不是面向对象的语法(面向对象一般都是通过对象来调用方法),del这种语法比较像Shell命令行语法。
# 删除序列中的元素
lst = [1, 2, 3]
del lst[0]
print(lst)
# 删除变量
del lst
# NameError: name 'lst' is not defined
print(lst)
有意思的是Python即提供了类似于Shell中的del命令,也提供了del()
函数,也能删除元素。
lst = [1, 2, 3]
del(list[0])
8. 异常 try except else finally
在其它编程语言中一般都是try catch finally, Python用的不是catch而是except
,并且python还支持else代码块。else子句将在try子句没有发生任何异常的时候执行。注意代码块的顺序,先try,在except,然后else最后finally,这就意味着else中的代码发生异常时不会走except
。
Python中的大部分异常类都是以Error作为后缀的,如NameError、IndentationError、AssertionError、ValueError、TypeError、ZeroDivisionError。
try:
print('try')
except Exception as e:
print('except')
else:
print('else')
# 注意:else出现异常不会走except
print(1/0)
finally:
print('finally')
也就是说else中的代码必须成功保证执行, 不能被捕获,如果出错程序将终止sys.exit(1)。
9. 在if、try、while、for代码块中声明的变量是全局变量
Python在代码块中声明的变量是"全局变量"这和Java语言完全相反,Java在代码块中声明的变量都是局部变量当代码块块执行完毕了就释放了局部变量,Python则完全不同。
if True:
a = "this is global variable"
print(a)
try:
b = "try"
except ValueError:
# 局部变量,外面不能访问
c = "except"
else:
d = "else"
finally:
e = "finally"
print(b)
# print(c)
print(d)
print(e)
while True:
f = "while"
break
print(f)
for i in range(10):
g = "for"
print(g)
10. 函数参数默认值类型只能为不可变类型
如果函数参数默认值为可变类型如list列表,那么函数执行的结果可能会和你预期的结果不一致。因为函数参数的默认值只会初始化一次,下次再调用函数参数的默认值的地址引用还是指向上一次初始时的内存地址。
list传参属于引用传递而不是值传递。
lst = [1, 2]
def change_list(l):
l.append(3)
change_list(lst)
# [1, 2, 3]
print(lst)
def append_end(l=[]):
print("id(l)=", id(l))
l.append("END")
return l
# ['END']
print(append_end())
# 第二次调用参数l的内存地址竟然和第一次的完全一样。
# 这说明一个问题:参数默认值只会分配一次内存,如果不传参数所有函数调用都使用同一块内存,这就解释了第二次函数调用为啥是['END', 'END']而不是['END']
# ['END', 'END']
print(append_end())
解决函数参数默认是可变类型带来的异常结果就是将函数参数默认值改为不可变类型None
。
def append_end2(l=None):
print("id(l)=", id(l))
if l is None:
# 如果没有传值,将l显式清空
l = []
l.append("END")
return l
# ['END']
print(append_end2())
# ['END']
print(append_end2())
11. global和nonlocal
当内部作用域想修改外部作用域的变量时,就要显式使用global和nonlocal关键字。原因是如果在局部作用于声明一个和全局作用于相同的名字的变量,Python会认为局部变量和全局变量是两个不同的变量(虽然名字相同),要告诉Python这是同一个变量就要使用关键字来修饰。
- 如果函数内部要修改全局变量,需要使用
global
关键字修饰一下全局变量; - 如果要在嵌套函数内部修改外层函数中的变量,需要使用
nonlocal
关键字来修饰一下;
foobar = 1
def func():
foobar = 10
print(f"func foobar={foobar}")
func()
print(foobar)
foobar = 1
def func():
global foobar
foobar = 10
print(f"func foobar={foobar}")
# func foobar=10
func()
# 10
print(foobar)
def outer():
foobar = 1
def inner():
nonlocal foobar
foobar = 10
print(f"inner foobar={foobar}")
inner()
print(f"outer foobar={foobar}")
outer()
12. 嵌套函数
函数定义可以嵌套函数,这在其它语言中也经常见到,但是可以嵌套多层函数还是很少见到。
def function():
print("function")
def func():
print("func")
def fun():
print("fun")
return fun
return func
function()()()
13. lambda表达式
lambda表达式在很多编程语言中都有类似语法,但是Python中的lambda表达式却和其它语言相差很大。lambda表达式就是一个有参数有返回值的匿名函数
。
- Python中的lambda表达式必须使用
lambda
关键字声明 - Python中的lambda式的方法体只能是一行代码,不能是代码块。
z = 10
sum = lambda x, y: x + y + z
print(sum(1, 2))
print(type(sum))
14. 函数注释(Function Annotations)
Python中的函数定义参数不需要指定参数类型,函数签名中不需要写返回值类型。如果不了解这个方法的作用,只看函数签名是看不出来啥,也不知道传参数传什么类型,也不知道这个函数的返回值是什么类型,就算知道函数的作用也不能完全确定函数的返回值是什么类型,是int类型还是float类型呢?不指定参数类型不指定返回值类型,这使得函数的签名非常模糊,非常的不直观,也不利于快速了解函数的作用。
def foobar(a, b):
pass
像很多服务端语言都会指定参数的类型和返回值类型,这些都作为函数签名的一部分。如下Java方法的定义:
/**
* 除法
* @param a 除数
* @param b 被除数
* @return 除法结果
* @throws ArithmeticException 算术异常
*/
public static long div(long a, long b) throws ArithmeticException {
return a / b;
}
为了解决函数签名不明确的问题,Python3中引入了函数注释。注意此函数注释并不太像我们通常所说的注释:解释函数的作用。Python中的函数注释只能用来描述参数的数据类型和作用以及字段的返回值类型,不能描述整个函数的作用。
函数注释作用是提高代码可读性,暗示传入参数及返回数据的类型, 注意这只是提示参数和返回值的类型,并不校验参数的值是不是这种类型,因为Python中参数名是没有数据类型的,实际上还是可以任意传任意类型的值,这里只是提示要传入的数据类型,作为一种注释来提示你。
函数注释包括:
- 参数注释:以冒号 : 标记,可以是建议传入的参数数据类型type,也可以是一个帮助help字符串注释
- 返回值注释:以箭头 -> 标记,建议函数返回值的数据类型
参数类型和返回值类型注释
def div(a: int, b: int) -> float:
return a / b
帮助字符串注释
def div(a: "除数", b: "被除数") -> float:
return a / b
参数类型注释和帮助字符串注释混用
def div(a: int, b: "被除数") -> float:
return a / b
同时支持参数数据类型和帮助字符串注释
def div(a: dict(type= int, help="除数"), b: dict(type= int, help="被除数")) -> float:
return a / b
# 注释是一种帮助开发人员了解代码的,并不是可执行代码的一部分,
# 函数注释只是一种提示,并不是强制。虽然我们指定了参数的数据类型,
# 但是我们让然虽然传值,我们可以传float类型的值。
print(div(4.4, 2.0))
类型注解,是一种注释,不影响编译,所以说类型注解并没有改变Python弱类型的基本特性。
name = '我就是不报错,我就是能正常运行,类型注解只是一种提示,不影响编译' # type: int
def add(x: int, y: int) -> int:
return x + y
# 正常x,y是int,但是编译不会报错,传入str也能正常运行
add('hello', 'world')
个人觉得把字符串帮助描述写在函数签名里使得函数签名过于冗余不够简洁,还不如在函数体内些文档注释看着清晰。
def div(a: int, b: int) -> float:
r"""除法运算
:param a: 除数
:param b: 被除数
:return: 除法结果
"""
return a / b
15. 动态注释
动态注释就是动态的修改注释的值。如果要修改return中的值,那么函数的返回类型需要给__annotations__[“return”]一个初始值
。
__annotations__["return"]
就像一个函数的全局静态属性一样,函数每次的结果都会被记住。
def div(a: int, b: int) -> 0:
result = a // b
div.__annotations__["return"] += result
return result
# {'a': <class 'int'>, 'b': <class 'int'>, 'return': 0}
print(div.__annotations__)
# 2
print(div(4, 2))
# 5
print(div(10, 2))
# return为7
# {'a': <class 'int'>, 'b': <class 'int'>, 'return': 7}
print(div.__annotations__)
16. 动态添加属性和方法
Python支持动态的添加属性和方法(实例方法、静态方法),Javascript脚本语言也支持此语法。
class Foobar:
pass
foobar = Foobar();
foobar.attr1 = "动态添加属性"
print(foobar.attr1)
from types import MethodType
def test(self, value):
print(f"动态添加{value}方法")
# 给某一个对象动态添加一个实力方法,其它对象是没有该方法的
foobar.func = MethodType(test, foobar)
foobar.func("实例")
# 给类添加静态方法
Foobar.test = MethodType(test, Foobar)
Foobar.test("类")
type() 函数即可用来获取变量的类型,也可用用于动态创建类型。
def foo(self):
print('Hello World')
# 参数1:类名
# 参数2:父类
# 参数3:方法
Hello = type('Hello', (object,), dict(foobar=foo))
obj = Hello()
obj.foobar()
17. with关键字
with关键字的作用自动关闭资源或者自动获取锁和自动释放锁,不需要程序员显式的关闭资源和释放锁了,再也不会由于忘记关闭资源或者忘记释放锁造成bug了,系统会自动做这件事。
# 手动关闭资源
try:
f = open("test.txt")
print(f.readline())
except BaseException as e:
print(e)
finally:
f.close()
# with自动方式
with open("test.txt") as f:
print(f.readline())
with自动获取锁和释放锁
import threading
lock = threading.Lock()
lock.acquire()
print("线程安全代码...")
lock.release()
import threading
lock = threading.Lock()
with lock:
print("线程安全代码...")
18. import 模块 和 from 模块 import 成员 的区别
- import 模块 : 导入之后要想使用模块里的函数、类或者变量,需要通过 “模块.” 语法来调用,而不能直接使用模块内的成员函数或者变量。如果把Python中的
import 模块
理解成Java中的创建对象Sys sys = new Sys();
通过对象点调用就是sys.argv - from 模块 import 成员:导入之后可以直接使用模块内的函数、类或者变量,而不需要在函数或者变量加"模块.",类似于Java中的静态导入。
import sys
print(sys.argv)
# 导入多个用逗号分隔,导入所有用星号*
from sys import argv, path
print(argv)
print(path)
Python允许在方法内部导入模块,这是在其它编程语言中很难看到的语法。
def sleep():
import time
import random
print('睡觉中zZZZ')
time.sleep(random.randint(1, 10))
19. id() 和 ctypes
- id()函数用户获取变量的内存地址。
- ctypes模块可以根据内存地址获取对应的值。
import ctypes
value = 'Hello World'
memory_address = id(value)
# 4485631408
print(memory_address)
raw_value = ctypes.cast(memory_address, ctypes.py_object).value # 根据内存地址获取对应的值
# Hello World
print(raw_value)
20.数学方式比较大小
在其它编程语言中一般是不允许数学方式比较的,原因是一些符号是特殊语法而不是数学用的比较符号,在Python中这些数学比较符号没有被用来作为语法使用,所以支持数学方式的比较
x = 10
# False
result = 1 < x < 10
# True
result = 20>= x >= 10
21.偏函数
偏函数使用原函数指定一些默认值来封装出一个新函数。如果某个函数经常使用某个固定的默认参数,可以通过偏函数语法来简化调用,减少传参个数。
def int16(value):
return int(value, base=16)
int16("123456")
import functools
int16 = functools.partial(int, base=16)
int16("12345")
斐波那契数列(Fibonacci sequence),又称黄金分割数列,因数学家列昂纳多·斐波那契(Leonardoda Fibonacci)以兔子繁殖为例子而引入,故又称为“兔子数列”,指的是这样一个数列:1、1、2、3、5、8、13、21、34、……在数学上,斐波那契数列以如下被以递推的方法定义:F(1)=1,F(2)=1, F(n)=F(n-1)+F(n-2)(n>=3,n∈N*)在现代物理、准晶体结构、化学等领域,斐波纳契数列都有直接的应用。
/**
* f(1) = 1
* f(2) = 1
* f(3) = f(2) + f(1) = 1 + 1 = 2
* f(4) = f(3) + f(2) = 2 + 1 = 3
* f(n) = f(n-1) + f(n-2)
* @param n
* @return
*/
private static long fibonacci(int n) {
if (n < 1) {
return 0;
} else if (n < 3) {
return 1;
}
return fibonacci(n -1) + fibonacci(n - 2);
}
private static long fibonacci(int n) {
if (n < 1) {
return 0;
}
long a = 1;
long b = 1;
long temp = 0;
for(int i = 3; i <=n; i++) {
// 指向最新的前一个值
temp = a;
// 最后一个值变成前一个值
a = b;
// 最新的下一个值 = 原来的b值 + 前一个值
b = b + temp;
}
return b;
}
22. 交换两个变量的值
一般情况下交换两个变量的值需要借助一个临时变量temp来实现,Python元组语法可以一次同时声明多个变量,也可以同时修改多个元祖中的值,使用此语法可以简单的完成两个变量值交换。
x, y = 1, 2
x, y = y, x
23. 变量可以更改数据类型
由于python在声明变量时不需要指定类型,所以变量可以任意改变它的类型。
foo = "foo"
# <class 'str'>
print(type(foo))
foo = 1
# <class 'int'>
print(type(foo))
foo = True
# <class 'bool'>
print(type(foo))
24. _ _ str _ _ 调用次数不确定
在Python中方法名以两个下划线开始和以两个下划线结束的方法叫做魔法函数
,魔法函数一般不是用来主动调用的,而是在特定情形下系统会自动调用。_ _ str _ _就是java语言中toString()方法。
class User:
def __str__(self):
print('__str__')
return '__str__ return'
if __name__ == '__main__':
user = User()
print(user)
如果是以Run方式运行好像只有print时会调用__str__,
如果是Debug方式具体执行次数不确定。
25. 对象之间比较大小和加减乘除
在其它语言中一般使用 == 比较对象的内存地址,如果类重写了hashCode() + equals() 方法则可以通过对象的属性来比较相等,这在Python中通过魔法函数 __eq__
来实现。但是在其它语言中一般都是不能比较两个对象的大小的,但是在Python中可以通过魔法函数__lt__
、__le__
来实现。两个对象也可以进行加__add__
减__sub__
乘__mul__
除__divmod__
运算。
class User:
name = None
age = None
def __init__(self, name, age):
self.name = name
self.age = age
def __lt__(self, other):
return self.age < other.age
def __le__(self, other):
return self.age <= other.age
def __eq__(self, other):
return self.name == other.name
user1 = User('张三', 28)
user2 = User('张三', 28)
user3 = User('张三疯', 29)
print(user1 == user2)
print(user1 <= user3)
26. 导入会调用全局代码
user.py
print("this is user.py global script")
test2.py
import user
if __name__ == '__main__':
print(__name__)
导入module时会执行全局代码。一般情况下全局代码用来定义函数,变量等,如果想部分全局代码在导入时不要被执行到,那么就将这些代码放入到每个文件的if __name__ == '__main__':
里面, 只有执行 python xxx.py 对应的脚本中的全局变量__name__ 才会赋值为__main__,被导入的文件的__name__值被赋值为"script<文件名>"
。
27.多重继承
多重继承只有个别语言(C++)支持,java不支持。多重继承如果子类中有相同的属性或者方法时,按照从左到右的顺序,优先使用左边的
。
class Person:
country = '地球村'
def say_hello(self):
print(f'{self.country}')
class China:
country = '中国'
def say_hello(self):
print(f'{self.country}')
class Chinese:
country = '中国人'
def say_hello(self):
print(f'{self.country}')
class God(Person, China, Chinese):
pass
god = God()
# 地球村
print(god.country)
# 地球村
god.say_hello()
28.Python没有常量
Python中的常量命名规范是所有字母均大写,多个单词之间使用下划线分割,Python只能通过约定,即看到这种格式的变量就是常量,程序员就不要去修改他的值,通过约定来限制而不是通过语法来限制。
AGE_OF_ADULT = 18
AGE_OF_ADULT = 17
29.Python支持复数
a = 1 + 2j
b = 2 + 3j
# (3+5j)
print(a + b)
30.取反not
一般其它开发语言中取反操作使用感叹号!,而Python对bool值取反使用not关键字, 有点类似于SQL语法。
flag = False
# True
print(not flag)
print(1 in [1, 2, 3])
print(4 not in [1, 2, 3])
31. 判断变量是否为None使用关键字is
java判断变量是否为null就是直接判断 foobar == null, 而Python要使用关键字is。
foobar = None
if foobar is None:
print('foobar = None')
32.生成器
生成器是个有意思的功能,调用生成器函数并不执行函数体,而是在调用生成器的__next__()方法时才会真正去执行方法体,而且只有第一次才会执行这个方法体,方法体的for循环只执行一次,第二次再执行也就只执行循环体里面的代码,不会执行循环体外部的代码。
当循环的对象非常占用内存时,或者当真正使用时再去获取对应的值时使用。
def power(x):
print("init code")
for i in range(1, x):
print(f"for {i}")
yield i ** i
gen = power(5)
# <generator object power at 0x10123d0e0>
print(gen)
# init code
# for 1
# 1
print(gen.__next__())
print("*" * 20)
# for 2
# 4
print(gen.__next__())
33. 方法重载
Python中的变量是没有数据类型的,也就是相当于Java中的Object类型,所以同一个参数传值既可以是字符串也可以是其它任意类型,只要程序支持。在Java中方法的参数的数据类型很少是Object类型,一般都是某个具体的类型。而Python中的同一个参数的类型可以是任意的类型,同样返回值的类型也可以是任意类型,这就相当于Java的方法重载了Overload
,神奇的是Python就用了一个方法就达到了方法重载的目的。
header = ('姓名', '年龄')
rows = [('张三', 20), ('李四', 25)]
df = pandas.DataFrame(rows, columns=header)
# 第一个参数类型为str
df.to_excel('test.xlsx', sheet_name='Sheet1', index=False)
writer = pandas.ExcelWriter('test1.xlsx')
# 第一个参数为对象
df.to_excel(writer, sheet_name='Sheet1', index=False)
def foobar(a: int | str | tuple | list | dict = None) -> int | str | tuple | list | dict:
if isinstance(a, int):
print(f'数据类型为int={a}')
elif isinstance(a, str):
print(f'数据类型为str={a}')
elif isinstance(a, tuple):
print(f'数据类型为tuple={a}')
elif isinstance(a, list):
print(f'数据类型为list={a}')
elif isinstance(a, dict):
print(f'数据类型为dict={a}')
return a
print(type(foobar(1)))
print(type(foobar('python')))
print(type(foobar((1, 2, 3))))
print(type(foobar([1, 2, 3])))
print(type(foobar({'k1': 'v1', 'k2': 'v2'})))