Python String Format

fstring! fstring! fstring!

Posted by Xiaofei on December 10, 2022

python字符串格式化的几种方案

string是我们写代码绕不开的基础类型,而构造string也是我们经常遇到的场景。

在我刚刚接触python的时候,学到的构造string的方法是C或C++的风格,即通过%d, %s, %f等特殊字符构造,然后通过一个%将参数与占位符一一对应,比如这样

1
2
3
a = 1
b = "hello"
print("a=%d, b=%s" % (a, b))

紧接着,逐渐学习到format的用法,最大的好处是不需要区分类型,例如:

1
2
3
a = 1
b = "hello"
print("a={0}, b={1}".format(a, b))

这种写法在计数的那部分略微冗余,可以简写成这样:

1
2
3
a = 1
b = "hello"
print("a={}, b={}".format(a, b))

但是数量不匹配的时候还是要写明数字,比如这种情况:

1
2
3
a = 1
b = "hello"
print("a={0}, b={1}, a={0}".format(a, b))

format还支持用键值的形式,比如:

1
2
3
a = 1
b = "hello"
print("a={a}, b={b}, a={a}".format(a=a, b=b))

这其实已经和fstring很像了,但是依旧有一个巨大问题没有解决——不够直观,即变量和string整体是分离的,需要程序员人眼去对应。

这时候,fstring出现了。

fstring是formatted string的缩写,也就是格式化的字符串,可以将变量直接嵌入到string中构造新的string。比如上面的代码可以写成:

1
2
3
a = 1
b = "hello"
print(f"a={a}, b={b}, a={a}")

简洁了很多!

而且,fstring还有一个好处——速度!我一开始知道的时候也很吃惊,fstring居然是众多python字符串格式化方案中最快的方式,做到了兼具速度和可读性。该论断来自于:https://www.scivision.dev/python-f-string-speed/ 和https://www.reddit.com/r/Python/comments/pivojb/performance_comparison_of_string_formatting/

fstring与string.format混用

fstring的一大限制是在构造字符串的时候,所有变量的值就已经有了,但是如果我想准备一个模板,然后里面的key是随着程序运行才逐渐赋值的(比如通过for循环或者用户输入拿到不同的值),该怎么办?这时候,比较常见的用法是通过之前提到的键值形式的format方法。

例如:

1
2
3
template = "a={a}, b={b}, a={a}"
for a in range(10):
    print(template.format(a=a, b="hello"))    

但是如果我的原始template中的某个字符串也是变量呢:例如我要针对b=”hello”和b=”world”构造两个template,要手写吗?如果有200个怎么办,其实,fstring也可以保留{} 这种占位符的,例如下面的代码:

1
2
3
b = "hello"
template = f"a=, b={b}, a="
print(template)

会发现template的值是"a={a}, b=hello, a={a}",这样后续的format就顺理成章了。

fstring的格式控制

“格式控制”这里主要包括:

  • 小数位数
  • 左侧填充占位符(对齐)和右侧填充占位符(对齐)
  • 日期

格式控制主要是靠变量后面的冒号和格式来实现的。

小数位数

1
2
3
a = 11.123456789
print(a)
print(f"{a:0.2f}")

输出为

1
2
11.123456789
11.12

左侧填充占位符(对齐)和右侧填充占位符(对齐)

填充占位符的用法可以总结为:

1
f"{变量:占位符>整体长度}"

这里需要注意,占位符应当是单个字符(因为python3是unicode,所以可以是中文),其中>代表左填充,变成<的话,就变成了右填充。

例子:

1
2
3
4
5
6
a = "hello"
print(f"{a:*>10}")
print(f"{a:*<10}")
print(f"{a:0>10}")
a = 1
print(f"{a:0>4}")

输出为:

1
2
3
4
*****hello
hello*****
00000hello
0001

最后一条的效果等价于str(a).zfill(4)

另外,在变量为数字的情况下,且占位符为空格或者0的时候,可以不写>或者<,例如:

1
2
3
a = "hello"
print(f"{a: 10}")
print(f"{a:010}")

但是我个人感觉这样不够清晰,而且适用情况太单一,遇到复杂情况还是需要加上>或者<,莫不如直接只记一种用法。

另外注意上面的写法中的占位符和长度分别是普通字符和数字。填充的占位符或长度也可以是变量,但是需要将上面的用法更改为:

1
f"{变量:{占位符变量}>{整体长度变量}"

比如下面这个例子里,我们一次将占位符设置为空格、星号、井号,输出也相应地以三种字符进行填充

1
2
3
4
5
6
num_list = [10, 100, 1000, 10000]
symbol_list = [" ", "*", "#", "dd"]
length = 10
for symbol in symbol_list:
    for num in num_list:     
		print(f"{num:{symbol}>{length}}")

输出为

1
2
3
4
5
6
7
8
9
10
11
12
        10
       100
      1000
     10000
********10
*******100
******1000
*****10000
########10
#######100
######1000
#####10000

日期

1
2
3
4
from datetime import datetime
date = datetime(2022, 1, 2, 3, 4 ,5)
print(date)
print(f"year and month: {date:%Y/%m}")

输出为

1
2
2022-01-02 03:04:05
year and month: 2022/01