区别于C/C++, java等编程语言,python是一门弱类型语言,如果你接触过perl或者JS之类的弱类型语言,那么对这个特性应该不陌生。
当然即使是动态类型语言,变量也是有类型的,只是定义跟赋值上更加的自由,多数时候我都是把这个特性当成语法糖来用(比如在Python学习笔记(番外)——获取键盘输入事件这一篇里,用同一个变量名把bytes类型的值改成了str类型而不需要修改后续代码),当然动态类型有动态类型的缺点,python为了解决这个问题甚至利用了一些方法去接近静态类型语言的特性(通过Generic Alias
和Union
规定函数参数类型跟返回值类型)。
Python内置类型
参考python官网, python内置类型可以分为以下几类:
数值类型
数值类型的内置类型有:int
, float
, complex
,int跟float就不多做赘述了,一个有趣的点是python的bool
类型是继承自int
的,另外int
类型具有无限精度(内置大数计算),float
类型实现的是C的double
类型,大大方便了我们编程,当然会存在内存开销的问题,但是这个就交给官方优化吧(如果你足够牛逼并且对官方的内存优化不满的话可以自己尝试修改)。除了以上两个常见类型以外,python还内置了复数类型complex
,对于某些领域应该有用吧,单纯的当作二维向量的话,我还是愿意用一些vector库。
常见的运算符(+-*/%
)就不提了,python还额外提供了//
跟**
运算符,其中//
代表除并向下取整,**
代表指数运算,x ** y
跟pow( x , y )
等价,一种语法糖吧。
此外,对于int
类型来说,常见的位运算也是全部支持的(&
|
~
^
<<
>>
)。
序列类型
list
tuple
range
,除了这三个之外,str
bytes
bytearray
memoryview
也属于序列类型,所以它们也具有序列类型的特性。
序列类型可以使用以下运算符跟函数:
运算符 | 作用 |
---|---|
x in s |
判断元素x是否在序列s中 |
s1 + s2 |
返回s1跟s2连接的结果,如[1,2]+[3],结果为[1,2,3] |
s * n n * s
|
注意n为int ,返回s重复n次的结果,[1,2]*2,结果为[1,2,1,2] |
s[i] |
这个不用说了 |
s[i:j] |
序列s从i到j的子序列,包含i,不包含j,即[1,2,3][0:1]结果为[1] |
s[i:j:k] |
同上,但是以k为步长 |
len(s) |
不用说了 |
min(s) |
作用不用说了,注意当s的元素不是数值类型时该函数也是可以使用的,只要元素类型一致,个人猜测跟元素的__ge__ __gt__函数有关 |
max(s) |
同上 |
s.index(x) |
不用说了,注意此处可以使用可选参数s.index(x,i, j),表示从i到j搜索,依然是包含i不包含j |
s.count(x) |
同上 |
list
不用多做介绍了吧,类似C的数组或者java的array,只是不用严格地定义大小并且方便操作,由于python弱类型的特性,一个序列内可以存放不同的类型,类似[ 1, '1' , True]
这样的list都是允许的,当然这样会使min max函数失效。
tuple
即元组类型,跟list的区别在于tuple一经初始化,无法修改,使用()
定义而非[]
。
range
在for循环中比较常见,类似for i in range(0,100)
这样的用法,使用range(i , j , k)
,可以生成一个等差数列,参数k可选,默认为1,同样包含i不包含j,也可以仅传递一个参数,range(10)
等价于range(0 , 10)
,可以强制转换为list类型,如下:
print(list(range(0, 1)))
可以看到输出是[0]
,如果不转换的话,输出的是range(0,1)
。
字符串——str
另外,如前所述,str
类型也是一种序列,可以使用所有序列类型的运算符,所以类似'gg' in 'egg'
或者'egg'.index('gg')
这样的代码都是可以正常运行的,相对于C/C++的strcpy strcat之类的函数,使用+
这样的运算符自然是方便不少,当然这种语法糖也不新鲜了,java都有。另外,str
是一种immutable type
,所以它的内容一经定义也是无法修改的。
str
有4种定义方式:
'This is a string'
"This is a string"
'''
This is a string
'''
"""
This is a string
"""
可以看到,单引号跟双引号是等价的,而三个连续的引号可以定义多行文本,一个有趣的点是,双引号内部用单引号不用转义,反之亦然。另外,在字符串前面加字母r,代表这是一个raw string,内部任何字符都是它本来的意思而不用考虑任何转义,但是一个操蛋的点在于如果raw string是以\
结尾的,那么它将是非法的,且raw string内部不进行转义,所以\\
也无法得到\
:
r'\' # 这是非法的
r'\\' # 合法,但输出结果是\\
实在要用\
结尾的话,要不,还是拼贴一下?
r'test' + '\\' # 得到test\
对于长单行文本,过长的单行代码可读性差并且也不符合PEP8标准,比如为了写单词Preumonoultramicroscopicsilicovolcanoconiosis可以如此定义:
'Preumonoul' \
'tramicrosco' \
'picsilicovol' \
'canoconiosis'
有点像C/C++的写法,以上写法等价于java的:
"Preumonoul" +
"tramicrosco" +
"picsilicovol" +
"canoconiosis"
但是python的换行缩进定义了代码块,所以java写法不可用。但是在作为参数时,由于不存在歧义,python又允许你这么用:
def foo(arg : str):
print(arg)
foo(arg='Preumonoul'
'tramicrosco'
'picsilicovol'
'canoconiosis')
好吧python,你是自由的。
熟悉java的程序员都知道字符串的比较不能简单地使用==
,这会导致严重的问题,python则不会,以下写法在python中是正确的:
s = input('input test') # 输入test
print( s == 'test' ) # True
在java中,你不得不使用String.equals()函数来确保它的正确性,然而在python中,你可以尽情地使用==
和!=
来进行判断,赞美python!
二进制序列类型
二进制序列类型有bytes
bytearray
memoryview
,如前所述,这些类型都属于序列类型,所以序列类型中介绍的运算符依旧适用。
bytes
类型的定义跟str一样,只是在前面加一个b:
b'This is a bytes'
bytearray
则是bytes版的list
,bytearray
之于bytes
,就像list
之于tuple
一样,即bytes
不可写而bytearray
可写,bytearray
用法跟list
大差不差,也不多说了(懒)。
bytes
和bytearray
可以通过decode
函数转化为str
,反之,str
可以通过encode
转化为bytes
。
memoryview
就比较牛逼了,memoryview
使用一个支持buffer protocol
的对象(list
bytes
bytearray
等)作为参数初始化:
momeryview(b'momoryview test')
momeryview(['meomryview',test])
然后可以通过memoryview
查看、修改源对象的内容,注意:当源对象可写时,memoryview
才可以进行修改,总的来说,python封装了一个方便你对对象(该对象必须支持buffer protocol
)进行内存级别操作的类:
ba = bytearray(b'test')
mv = memoryview(ba)
mv[0:5] = b'tttt'
print(ba)
以上代码会输出b'tttt'
。
集合
set
frozenset
,set
就不多说了,都懂,使用{}
定义:
{ 'set' , 'test' , 0 , 1 }
由于python弱类型的特性,set
里面可以有各种不同类型。
frozenset
顾名思义,就是冻结了的set
,一个不可修改版本的set
。
映射集
dict
这个不用介绍了吧,类似java的HaspMap
,使用{}
定义:
{'0' : 0}
同样由于python弱类型的特性,dict
的kv对可以出现任何妖魔鬼怪,以下写法也是可行的:
{
'0':0,
0:'0',
}
当然一般不推荐这么做。
类型注解 Generic Alias,联合体
好吧我不知道怎么翻译这个Generic Alias
,通用别名?总之,如前言所说,python作为弱类型语言,有时候需要一些规则来规定类型,Generic Alias
和Union
就是做这个事情的,常见应用如下:
def average(values: list[float]) -> float:
return sum(values) / len(values)
def send_post_request(url: str, body: dict[str, int]) -> None:
...
对于容器类型,Generic Alias
可以规定它的元素类型,对于dict
,可以分别规定键值的类型。
根据python 3.10.5的文档,以下用法也是可行的:
t = list[int]
t([1, 2, 3])
这就很接近强类型语言了,终究是妥协了,不过我在3.8的环境下是会报错的,应该是比较新的特性。
Union
也是3.9以后的新特性,不过跟C/C++的Union
完全不是一回事,python的Union
类似Generic Alias
,也是一种类型注解:
def square(number: int | float) -> int | float:
return number ** 2
并且具有如下规则:
(int | str) | float == int | str | float # 结合律
int | str | int == int | str # 冗余类型会被删除
int | str == str | int # 交换律
int | str == typing.Union[int, str] # 另一种写法
str | None == typing.Optional[str] # Optional可以表示为 | None
逻辑判断
万物皆对象,python中的类全都继承自内置类型object,继承了几个通用的内置函数,其中__bool__跟__len__会用来做逻辑判断,如果这两个函数都未被定义,那么这个类型在做逻辑判断时永远为真,即在如下代码中:
class test:
pass
if test():
print('True')
else:
print('False')
if test()
中将会把test()的值判断为True。
那么有趣的情况来了,如果我定义__bool__返回False,而__len__返回非零值,或者相反呢?
class test:
def __bool__(self):
return False
def __len__(self):
return 1
if test():
print('True')
else:
print('False')
这种情况下,程序输出False,即__bool__的优先级是比__len__高的。
对于python内置的基本类型判断为False的情况(基本类型也都是类,python重写了它们的__bool__或者__len__函数),官网给出了如下解释:
- constants defined to be false: None and False.
- zero of any numeric type: 0, 0.0, 0j, Decimal(0), Fraction(0, 1)
- empty sequences and collections: '', (), [], {}, set(), range(0)
常量None和False,逻辑判断为False,数值型变量数值为0则判断为False,集合为空,则判断为False,其余情况均为True,当然从可读性角度来说,我个人倾向于除了bool类型的变量,全都使用==和!=判断(PEP8标准下,对于None应该使用is和is not),没必要追求早期程序员那种极致的简化风格。