原文來自: How to Implement String Interpolation in Python - DZone Web Dev,本文是在自行理解之后的翻譯,粗淺之處,望請諒解。
字符串插值是將字符串中的占位符替換為局域變量的過程。許多編程語言都可以做到,比如 Scala:
// Scale 2.10+
var name = "John";
println(s"My name is $name")
>>> My name is John
Perl:
my $name = "John";
print "My name is $name";
>>> My name is John
CoffeeScript:
name = "John"
console.log "My name is #{name}"
>>> My name is John
… 還有很多。
乍看之下,似乎不大可能使用 Python 實現(xiàn)字符串插值,但實際上,我們只需要兩行代碼就可以實現(xiàn)。
首先,讓我們從基礎(chǔ)開始說起。通常我們構(gòu)建一個復(fù)雜的 Python 字符串時都會使用 format 函數(shù):
print "Hi, I am {} and I am {} years old".format(name, age)
>>> Hi, I am John and I am 26 years old
可以看出,format 的實現(xiàn)比字符串連接看起來整潔許多:
print "Hi, I am " + name + " and I am " + str(age) + " years old"
Hi, I am John and I am 26 years old
但如果通過這種方式使用 format 函數(shù),輸出的內(nèi)容就取決于參數(shù)的位置順序:
print "Hi, I am {} and I am {} years old".format(age, name)
Hi, I am 26 and I am John years old
為了避免這種情況,我們可以構(gòu)造鍵值對形式的參數(shù)序列傳給 format 函數(shù),如下:
print "Hi, I am {name} and I am {age} years old".format(name="John", age=26)
Hi, I am John and I am 26 years old
print "Hi, I am {name} and I am {age} years old".format(age=26, name="John")
Hi, I am John and I am 26 years old
這里,為實現(xiàn)字符串插值,我們不得不傳入將所有變量傳入 format 函數(shù),但是這依然沒有達到我們想要的效果,因為 name 和 age 并不是局域變量。那么,format 函數(shù)可以在某種程度上訪問到局域變量嗎?
答案是可以的,使用 locals 函數(shù)我們能夠獲得存儲著所有局域變量對象的字典:
name = "John"
age = 26
locals()
>>> {
...
'age': 26,
'name': 'John',
...
}
現(xiàn)在,我們可以將這個字典傳給 format 函數(shù)了。不幸的是,我們不能像這樣調(diào)用 s.format(locals()) :
print "Hi, I am {name} and I am {age} years old".format(locals())
---------------------------------------------------------------------------
KeyError Traceback (most recent call last)
<ipython-input-5-0fb983071eb8> in <module>()
----> 1 print "Hi, I am {name} and I am {age} years old".format(locals())
KeyError: 'name'
這是因為 locals 函數(shù)返回的是一個字典,而 format 函數(shù)期望的是鍵值對參數(shù)序列。
幸運的是,我們可以使用 ** 操作符將字典轉(zhuǎn)換為鍵值對參數(shù)序列。如下,假設(shè)我們有一個期望鍵值對序列作為參數(shù)的函數(shù):
def foo(arg1=None, arg2=None):
print "arg1 = " + str(arg1)
print "arg2 = " + str(arg2)
那么,我們就可以將存儲于字典中的參數(shù)進行解包傳入了:
d = {
'arg1': 1,
'arg2': 42
}
foo(**d)
>>> arg1 = 1
arg2 = 42
現(xiàn)在,使用這項技巧,我們就可以完成字符串插值的初版了,它大概長成這樣:
print "Hi, I am {name} and I am {age} years old".format(**locals())
Hi, I am John and I am 26 years old
以上代碼確實可以達到我們的需求,但看起來既笨重又不雅觀。因為在進行字符串插值的時候,我們每次都不得不寫上長長的一串 format(\*\*locals()) 。如果能夠?qū)懸粋€函數(shù)來完成這個過程會好很多,像這樣:
# Can we implement inter() function in Python?
print inter("Hi, I am {name} and I am {age} years old")
>>> Hi, I am John and I am 26 years old
你可能覺得這不科學(xué),因為如果我們將完成字符串插值的代碼移動到另一個函數(shù)中,那么它不就無法訪問原本作用域中的局域變量了嗎:
name = "John"
print inter("My name is {name}")
...
def inter(s):
# How can we access "name" variable from here?
return s.format(...)
然而,這是有可能的。Python 提供了 sys.\_getframe 方法,借用它的便利,我們可以方便地監(jiān)測到用于保存當(dāng)前局域變量的 frame 對象:
import sys
def foo():
foo_var = 'foo'
bar()
def bar():
# sys._getframe(0) would return frame for function "bar"
# so we need to to access 1-st frame
# to get local variables from "foo" function
previous_frame = sys._getframe(1)
previous_frame_locals = previous_frame.f_locals
print previous_frame_locals['foo_var']
foo()
>>> foo
稍作解釋:
f_locals是frame的一個屬性,它用于保存對應(yīng)作用域的局域?qū)ο笞值?,因此可以通過f_locals[‘foo_var’]獲取到函數(shù)foo的局域變量foo_var。
關(guān)于frame和f_locals,可以參考python inspect模塊解析 - 2.9. 棧幀(frame) 或者 Python程序的執(zhí)行原理 - PyFrameObject 部分。
現(xiàn)在的工作就只剩將獲得的 frame 數(shù)據(jù)與函數(shù) format 結(jié)合起來了。下面就給出實現(xiàn) Python 字符串插值的兩行代碼,請盡情使用吧:
def inter(s):
return s.format(**sys._getframe(1).f_locals)
## example
name = "John"
age = 26
print inter("Hi, I am {name} and I am {age} years old")
>>> Hi, I am John and I am 26 years old