你真的百分之百了解python中的f-string吗

楔子

我们之前在格式化字符串的时候会使用百分号占位符或者format函数,但Python在3.6版本的时候新增了一个格式化字符串的方法,称之为f-string。下面我们就来看看用法。

格式化字符串的方式

我们先来看看之前格式化字符串时,所使用的方式。

name = "古明地觉"
age = 17
where = "东方地灵殿"
 
# 使用百分号占位符格式化字符串
print("姓名: %s, 年龄: %d, 来自: %s" % (name, age, where))  # 姓名: 古明地觉, 年龄: 17, 来自: 东方地灵殿
 
# 使用format函数格式化字符串
print("姓名: {}, 年龄: {}, 来自: {}".format(name, age, where))  # 姓名: 古明地觉, 年龄: 17, 来自: 东方地灵殿
# 或者指定关键字也是可以的,这样format函数里面就可以无视顺序了
print("姓名: {name}, 年龄: {age}, 来自: {where}".format(name=name, where=where, age=age))

下面我们再来看看f-string,这算是格式化字符串的一把"瑞士军刀"。

name = "古明地觉"
age = 17
where = "东方地灵殿"
 
print(f"姓名: {name}, 年龄: {age}, 来自: {where}")  # 姓名: 古明地觉, 年龄: 17, 来自: 东方地灵殿

f-string,所以在字符串的前面加上一个f即可。我们知道如果加上r,那么表示 "raw(原生的)",此时字符串里面的反斜杠不具备转义效果。如果加上f,那么此时{}里面的内容则不再是字符串,而是需要单独计算的值或者表达式、或者一个变量。我们再举个例子:

print(f"1 + 1 = {1 + 1}")  # 1 + 1 = 2
print(f"sum([1, 2, 3, 4]) = {sum([1, 2, 3, 4])}")  # sum([1, 2, 3, 4]) = 10
 
try:
    print(f"{a}")
except Exception as e:
    print(e)  # name 'a' is not defined
# 我们说在f-string中,{}里面的内容是需要单独计算的
# 它可以是一个普通的表达式,比如:{1 + 1}或者{2 > 1}等等
# 或者是一个变量,而我们上面的a显然没有定义,所以报错
 
print(f"{'a'}")  # a
# 而f"{'a'}"这种方式是可以的,因为此时{}里面是一个字符串,完全没问题
print(f"{'--'.join(['a', 'b', 'c', 'd'])}")  # a--b--c--d
 
# 或者定义一个变量
a = lambda x: x + 100
print(f"{a}")  # <function <lambda> at 0x00000218B9A451F0>
print(f"{a(1)}")  # 101

我们看到f-string还是很方便的,关于它的逻辑,我们可以简单地理解为:先将{}里面的内容给它拿出来单独计算,然后再将计算之后的结果放到原来的位置(或者将原来的{xxx}整体替换掉)。所以{}里面可以放任何你想放的内容,只要它可以作为一个右值、或者可以赋值给一个变量,那么它都可以出现在f-string的{}里面。

f-string在功能方面和format类似,都比百分号占位符要丰富的多。但是在性能方面f-string是最优的,因为它不是字符串常量,而是在运行时才会计算的表达式。

支持的格式化

后我们来看看f-string格式化字符串的时候,支持哪些格式吧。

实现repr打印

有时候我们在打印的时候需要带上引号。

name = "古明地觉"
print(name)  # 古明地觉
print("%s" % name)  # 古明地觉
# 如果我们使用%r打印,会带上单引号
print("%r" % name)  # '古明地觉'
 
# 上面类似于str和repr的区别
print(str(name))  # 古明地觉
print(repr(name))  # '古明地觉'
"""
等价于调用__str__和__repr__
当我们在交互式环境下,不使用print,而是直接输入变量name、然后回车,那么会调用__repr__方法
如果使用print(name),那么不管在什么环境,都会调用__str__方法,等价于print(name.__str__())
"""
 
# 那么我们需要这个''有什么意义呢?比如:数据库查询
birthday = "1995-07-05"
print("select name from where birthday > %s" % birthday)  # select name from where birthday > 1995-07-05
print("select name from where birthday > %r" % birthday)  # select name from where birthday > '1995-07-05'
# 看到区别了吗?如果是第一个查询,那么肯定是会报错的。
 
# 重点来了,如何在f-string中实现这种效果呢?
print(f"{birthday!r}")  # '1995-07-05'
print(f"{birthday}")  # 1995-07-05
# 我们只需要在打印的内容后面加上一个!r即可
"""
所以: "{name}" <==> str(name) <==> name.__str__() , "{name!r}" <==> repr(name) <==> name.__repr__()
"""
print(f"{'.'.join(['a', 'b', 'c'])}")  # a.b.c
print(f"{'.'.join(['a', 'b', 'c'])!r}")  # 'a.b.c'
# 注意:!r针对的是字符串,虽然也可以作用于整型、不过没有效果
 
# 另外除了!r还有!s、!a,只有这三种
# !a和!r类似,!s是默认选择、加不加均可
print(f"{birthday}")  # 1995-07-05
print(f"{birthday!s}")  # 1995-07-05
print(f"{birthday!a}")  # '1995-07-05'
print(f"{birthday!r}")  # '1995-07-05'

整型的进制转换

我们在打印整型的时候,需要进制上的转换,这个时候怎么做呢?

i = 123
# 打印2进制
print(f"{i:b}")  # 1111011
# 打印8进制
print(f"{i:o}")  # 173
# 打印10进制,默认是10进制,也可以直接使用{i}
print(f"{i:d}")  # 123
# 打印16进制
print(f"{i:x}")  # 7b
 
# 此外我们还可以使用#b、#o、#d、#x
print(f"{i:#b}, {i:#o}, {i:#d}, {i:#x}")  # 0b1111011, 0o173, 123, 0x7b
# 另外对于十六进制的x,也可以换成大写
print(f"{i:x}, {i:X}, {i:#x}, {i:#X}")  # 7b, 7B, 0x7b, 0X7B
 
# 当然除了#号,我们还可以使用+、-、以及空格。注意:不可以同时出现,或者一个字符出现多次
"""
+: 显示正负号
-: 负数显示符号、正数不显示
空格: 正数显示空格、负数不显示,只能是一个空格
#: 显示前缀,比如0b、0o、0x
"""
print(f"{i:+x}, {-123:-x}")  # +7b, -7b
print(f"{i:-x}, {-123:-x}")  # 7b, -7b
print(f"{i: x}, {-123: x}")  #  7b, -7b
 
# 当然,我们知道python在创建整型的时候,还支持使用_进行分隔
print(10_000_00_00)  # 100000000
# 同理在f-string中也是可以的,并且除了下划线之外,还可以使用逗号
print(f"{10000000:_d}")   # 10_000_000
print(f"{10000000:,d}")   # 10,000,000
print(f"{10000000:+_d}")  # +10_000_000
print(f"{10000000:+,d}")  # +10,000,000

另外需要注意:b、o、d、x这些只用于整型,不能是其它的类型。

print(f"{'aaa':b}")
"""
    print(f"{'aaa':b}")
ValueError: Unknown format code 'b' for object of type 'str'
"""

我们之前提到了!r、!a、!s,这些是作用于字符串的,那么它们和b、o、d、x可不可以混用呢?其实,不用想也知道不行,因为前者作用于字符串,后者作用于整型。

print(f"{123}")  # 123
print(f"{123!r}")  # 123
# 两个都是123,因为我们说!r可以作用于整型,但是会没有效果
 
# 但是
print(f"{123:b}")  # 1111011
print(f"{123!r:b}")
"""
    print(f"{123!r:b}")
ValueError: Unknown format code 'b' for object of type 'str'
"""
# 我们看到print(f"{123!r:b}")报错了,虽然我们说!r作用整型会没有效果,但是已经把它变成字符串了
# 而b针对于整型,不能用于字符串,所以报错

整型的填充和浮点数的小数保留

我们之前使用过这种格式的打印,打印一个整型的时候至少打印3位,比如1的话,就打印001,18则打印018,123则打印本身的123;以及浮点数保留多少位小数等等,这种需求要怎么做呢?

a = 1
# 还记得这个d吗?我们说直接打印的话有它没它无影响
# 但是对于填充的话,它就派上用场了
print(f"{a:03d}")  # 001
print(f"{a:013d}")  # 0000000000001
"""
填充只能用0或者空格来填充,比如:0123d,表示打印出来要占123个字符,够的话不管了,不够则使用0在左边填充
如果是:123d,它代表的可不是占23位、不够用1填充,它代表的还是占123位,但是由于我们没有指定0,所以默认使用空格在左边填充
"""
print(f"{a:23d}")   #                       1
print(f"{a:023d}")  # 00000000000000000000001
# 当然我们同样可以结合+、-、空格、#
print(f"{b:+08d}")  # +0000123
# 可以的话,再将_或者,放进来
print(f"{a:+023_d}")  # +00_000_000_000_000_001
print(f"{a:+023,d}")  # +00,000,000,000,000,001
 
# 因此:d前面的必须是"数字",或者"+ - 空格 #"之一, 或者用于分隔的"_ ,"
# 并且它们出现的顺序是:("+ - 空格 #"之一    "数字"    "_ ,"之一),当然不需要全部同时出现
# 然后填充的时候,如果数字的第一个是0,那么0后面的表示占多少个字符,不够在左边用0填充
# 如果数字的第一个不是0,那么整体表示输出占的字符个数,不够用空格填充
 
# 当然,以上规则除了适用于十进制的d,也同样适用于二进制的b、八进制的o、十六进制的x
b = 123
print(f"{b:x}")  # 7b
print(f"{b:016x}")  # 000000000000007b
 
# 当然我们同样可以结合+、-、空格、#
print(f"{b:+08d}")  # +0000123
print(f"{b:+8d}")   #     +123
print(f"{b:#018b}")  # 0b0000000001111011
print(f"{b:#18b}")   #          0b1111011
print(f"{b:#18_b}")  #         0b111_1011
# 所以如果带上0b或者+、-等前缀的时候,我们看到填充的时候:
# 如果用0填充,那么会填充在0b、+等前缀的后面,如果是空格,填充在前缀的前面。
# 当然这也符合我们正常人的思维,如果是"+       123"或者"00000+123"明显觉得别扭
# 而"     +123"和"+000000123"则明显顺眼多了
 
 
# 下面来看看浮点数的转化
c = 123.13421
# f是保留小数,但是我们没有指定精度,所以默认是小数点后6位,不够6位使用0补齐
print(f"{c:f}")  # 123.134210
# .2f则是保留两位小数
print(f"{c:.2f}")  # 123.13
# 10.2f也是保留两位小数,然后整体占满10个字符长度,不够的话使用空格在左边填充
print(f"{c:10.2f}")    #     123.13
# 如果我们不想使用空格填充的话,那么也可以使用(也只能使用)0来进行填充,规则和整型是类似的
print(f"{c:010.2f}")   # 0000123.13
 
# 当然+、-、空格、#同样可以适用于浮点数,规则也和整型类似
# 如果使用空格填充,那么在+等前缀的前面;如果使用0填充,那么则填充在前缀的后面
print(f"{c:+10.2f}")   #    +123.13
print(f"{c:+010.2f}")  # +000123.13
# 但是#针对于二进制、十进制、十六进制整数的,浮点数没有效果
print(f"{c:#10.2f}")  #     123.13
# 同理,浮点数也支持使用下划线或者逗号进行分隔
print(f"{c:#10_.2f}")  #     123.13
print(f"{c:#10,.2f}")  #     123.13
# 上面由于字符比较少,所以没有分割,我们用0填充一下
print(f"{c:#010_.2f}")  # 000_123.13
print(f"{c:#010,.2f}")  # 000,123.13

任意字符的填充

我们上面介绍的还只是f-string的一部分,下面我们介绍的是f-string的杀手锏。

name = "古明地觉"
print(f"~{name:>10}~")  # ~      古明地觉~
print(f"~{name:^10}~")  # ~   古明地觉   ~
print(f"~{name:<10}~")  # ~古明地觉      ~
"""
>n: 输出的字符串占n个字符,原始的内容右对齐,长度不够则在左边用空格填充
^n: 输出的字符串占n个字符,原始的内容居中对齐,长度不够则在左右两端用空格填充
<n: 输出的字符串占n个字符,原始的内容左对齐,长度不够则在右边用空格填充
"""
# 上面的格式,也适用于整型
print(f"~{1:>3}~")  # ~  1~
 
# 我们看到默认是使用空格填充的,那么可不可以使用指定字符填充呢?
# 答案是可以的, 直接在>、<、^的左边写上用来填充的字符即可,但是只能写一个字符,多了报错
print(f"{'a':1>10}")  # 111111111a
print(f"{'a':1^10}")  # 1111a11111
# 使用空格填充,'a': >10等价于'a':>10
print(f"{'a': >10}")  #          a
 
# 这里我们实现了{1:03d}的效果
print(f"{1:0>3}")  # 001
 
# 所以我们看到这里有没有>、<、^是很关键的
print(f"{123:b}")  # 1111011
print(f"{123:b<}")  # 123
"""
对于f"{123:b}",当中的b表示整型的进制转换,此时只能作用于整型,不能是字符串
但是对于f"{123:b<},由于里面出现了<, 那么此时的b就不再代表进制了,而是代表的填充字符
只不过,<后面没有指定个数,所以python解释器不知道要填充多少个,因此就原本输出了。
所以此时的这个b既可以作用整型、也可以作用于字符串
"""
print(f"{'aaa':b<}")  # aaa
try:
    # 如果不是b<,而是b的话
    print(f"{'aaa':b}")
except Exception as e:
    print(e)  # Unknown format code 'b' for object of type 'str'
 
# 所以很简单,格式就是 变量:填充字符[^><]长度,比如: 123:a>10 表示输出占10位,不够在左边用字符a进行填充
 
# 我们同样可以使用!r、!a、!s,此时符号也是算在内的
print(f"{'abc'!s:x>10}")  # xxxxxxxabc
print(f"{'abc'!r:x>10}")  # xxxxx'abc'
print(f"{'abc'!a:x>10}")  # xxxxx'abc'
 
# 但是此时可不可以和代表进制的b、d、o、x使用呢?
# 想想也知道不可以,因为我们说出现了>、^、<的话,其前面的是填充字符,不再代表进制了
try:
    print(f"{123:08d>10}")
except Exception as e:
    print(e)  # Invalid format specifier
# 报错,提示无效的格式化字符,因为>前面出现了3个字符
# 而且这种写法逻辑上也讲不通啊
 
# 同理浮点数也是,我们这里没有指定精度,这里默认是小数点后6位
print(f"{123.1234:f}")  # 123.123400
print(f"{123.1234:.1f}")  # 123.1
 
 
try:
    # 这里我们想保留一位小数,然后整体占10个字符
    print(f"{123.1234:.1f>10}")
except Exception as e:
    print(e)  # Invalid format specifier
# 显示无效的格式化字符,原因还是我们说的,>前面的代表格式化字符,并且只能出现一个字符
# 如果对于浮点数,真的想占满指定长度,那么就只能使用我们之前介绍的下面这种方式
print(f"{123.1234:10.1f}")  #      123.1
print(f"{123.1234:010.1f}")     # 00000123.1
print(f"{123.1234:+010.1f}")    # +0000123.1
# 此外,刚才在介绍浮点型的时候,没有说
# 其实在使用0或者空格填充的时候,也是可以指定^、>、<的,并且要放在最开始的位置
# 之所以放在这里说,主要想介绍完>、<、^再提
print(f"{123.1234:^+010.1f}")  # 00+123.100
print(f"{123.1234:>+010.1f}")  # 0000+123.1
print(f"{123.1234:<+010.1f}")  # +123.10000
# 但是我们看到一旦指定了>、<、^,那么+等前缀会先和数字结合,然后0再填充
# 虽然我们实现了填充,只不过此时只能用0或者空格来填充了,当然我个人觉得已经足够了。浮点型,谁会搞这么多花里胡哨的

日期的截取

f-string还可以进行日期的操作是我没想到的,算是一大亮点吧。我们在格式化或者截取日期的时候,一般会使用datetime.strftime、datetime.date、time、year、month等等,这些也是可以使用f-string来实现的。

import datetime
 
dt = datetime.datetime(1995, 7, 5, 13, 30, 45, 100000)
print(dt)  # 1995-07-05 13:30:45.100000
print(str(dt))  # 1995-07-05 13:30:45.100000
 
# %F: 返回年月日(使用-连接)
# %D: 返回日月年(使用/连接),但是年是两位的,并且也不符合中国人的日期表达习惯,建议只用%F
print(f"{dt:%F}, {dt:%D}")  # 1995-07-05, 07/05/95
 
# %X: 返回时间,精确到秒(小数点后面的会截断)。这里注意X要大写,如果是%x那么等价于%D
print(f"{dt:%X}")  # 13:30:45
 
# 所以返回字符串格式的完整日期就可以这么写
print(f"{dt:%F} {dt:%X}")  # 1995-07-05 13:30:45
 
# %Y: 返回年(四位) %y: 返回年(两位)
print(f"{dt:%Y}, {dt:%y}")  # 1995, 95
 
# %m: 返回月 %d: 返回天 注意:会占满两位,不够补0
print(f"{dt:%m}, {dt:%d}")  # 07, 05
 
# %H: 返回小时(24小时制度) %I: 返回小时(12小时制度) 注意:会占满两位,不够补0
print(f"{dt:%H}, {dt:%I}")  # 13, 01
 
# %M: 返回分钟 %S: 返回秒 注意:会占满两位,不够补0
print(f"{dt:%M}, {dt:%S}")  # 30, 45
 
# %f: 返回微妙 注意:会占满六位,不够补0
print(f"{dt:%f}")  # 100000
 
# %p: 本地早上还是下午,早上返回AM、下午返回PM
print(f"{dt:%p}")  # PM
 
# %j: 一年中的第几天,从1开始(1月1号就是1) 注意:会占满三位,不够补0
print(f"{dt:%j}")  # 186
 
# %w: 星期几(0是周日、6是周六) %u: 星期几(1是周一、7是周日)
# 可以看到两种格式只有星期天不一样
print(f"{dt:%w}, {dt:%u}")  # 3, 3
 
# %U: 一年中的第几周(以全年首个周日所在的星期为第0周,占满两位,不够补0)
# %W: 一年中的第几周(以全年首个周一所在的星期为第1周,占满两位,不够补0)
# %V: 一年中的第几周(以全年首个包含1月4日的星期为第1周,以 0 补足两位)
print(f"{dt:%U}, {dt:%W}, {dt:%V}")  # 27, 27, 27
"""
所以如果对应的年的第一天恰好是星期一,那么%U会比%W少1。
如果不是星期一,那么两者是相等的
"""
# 比如2007年的1月1号恰好是星期一
dt = datetime.datetime(2007, 10, 13)
print(f"{dt:%U}, {dt:%W}, {dt:%V}")  # 40, 41, 41
 
# %Z: 返回时区名,如果没有则返回空字符串
print(f"'{dt:%Z}'")  # ''
 
# 这里面的符号还可以连用
print(f"{dt:%F %X}")  # 2007-10-13 00:00:00
print(f"{dt:%F %X %y %Y %m}")  # 2007-10-13 00:00:00 07 2007 10

f-string的注意事项

使用f-string需要注意单双引号的问题,如果限定字符串使用的是双引号,那么{}里面出现的必须是单引号,反之亦然。

d = {"a": 1}
print(f"{d['a'] + 1}")  # 2
# 我们限定字符串的时候使用的是双引号,{}里面必须是单引号,不能是{d["a"] + 1}
 
# 可能有人好奇,那我使用反斜杠(\) 对里面引号(") 进行转义的话会怎么样呢?
# 答案是不行的,因为f-string的{}里面不可以出现\
# 注意:{}是不可以出现\,一个都不可以,所以也不要再想是不是可以使用两个\进行转义啥的
try:
    print(f"{\\}")
except Exception as e:
    pass
# 我们即便使用异常捕获,也是无用的,依旧会抛出SyntaxError
# 因为try except是捕捉运行时的错误
# 而{}里面出现反斜杠属于语法上的错误,在编译成字节码阶段就会检测出来
"""
    print(f"{\}")
          ^
SyntaxError: f-string expression part cannot include a backslash
"""

因此:使用f-string是同样需要注意单双引号的问题,并且{}里面不可以出现反斜杠。

如果真的需要反斜杠,那么可以将反斜杠赋值给一个变量,然后将变量传递到{}里面去

a = "\\"
print(f"{a}")  # \

另外,f-string中最好不要出现嵌套的{},可以出现多个{},但是{}里面不要再出现{}。如果需要直接写集合或者字典的话,那么一定要使用括号括起来。

name = "satori"
age = 17
print(f"{({name, age}.pop())}")
# 如果写成f"{{name, age}.pop()}",那么是不符合语法规则的
# 是不会通过编译的

此外,f-string中一定要注意:{}的个数要匹配。

# 下面这段代码如果不使用f-string,那么没有任何问题
print(f"我永远喜欢{古明地觉")
# 但是一旦使用了f-string,那么会报错,因为里面出现了{,但是却没有对应的}。这段代码也不会通过编译
"""
    print(f"我永远喜欢{古明地觉")
          ^
SyntaxError: f-string: expecting '}'
"""

然后是多行的问题了。

a = 17
 
print(f"age: {a} "
      "age: {a}")  # age: 17 age: {a}
"""
如果是多行,那么会自动将多行当成是一个字符串
所以每一行都需要f,否则就会得到上面的结果
"""
print(f"age: {a} "
      f"age: {a}")  # age: 17 age: 17
 
print(f"""
    age: {a},
age: {a}
""")
# 或者使用""",打印如下
"""
 
    age: 17,
age: 17
"""

最后则是lambda表达式的问题。

# 使用lambda表达式的时候一定要使用括号括起来
# 否则会将lambda中的:解释成表达式与格式描述符之间的分隔符
print(f"{(lambda x: x + 123)(123)}")  # 246

总结

个人觉得f-string算是Python3.6新增的一大亮点,虽然有着一些限制:比如{和}的个数要匹配,里面不能出现反斜杠之类的。但是个人觉得这都不是什么问题,毕竟在做分词解析的时候肯定是有一些限制的,但总体来说f-string是非常强大的一个工具了。因此在格式化字符串的时候,推荐使用f-string,相信它一定可以在格式化字符串的时候给你提供很大的帮助。

另外使用f-string,甚至可以无需再使用字符串的一些内置方法,比如:ljust、rjust、center,我们完全可以使用<、>、^进行替换,而且更方便。

posted @ 2018-06-23 16:37  古明地盆  阅读(2041)  评论(0编辑  收藏  举报