https://m.toutiao.com/is/Jwaod7L/
我們先探討在Python中如何將參數(shù)傳遞給函數(shù)的相關(guān)細(xì)節(jié),然后回顧與這些概念相關(guān)的良好軟件工程實踐的一般理論。
通過了解Python提供的處理參數(shù)的多種方式,我們能夠更輕松地掌握通用規(guī)則,進(jìn)而可以輕松地得出結(jié)論,即什么是好的模式或習(xí)慣用法。然后,我們可以確定在哪些情況下Python方法是正確的,以及在哪些情況下可能濫用了該語言的特性。
Python中的第一條規(guī)則是所有參數(shù)都由一個值傳遞——總是這樣。這意味著,當(dāng)把值傳遞給函數(shù)時,它們被分配給稍后將在其上使用的函數(shù)簽名定義上的變量。你將注意到,函數(shù)更改參數(shù)可能依賴于類型參數(shù)——如果我們傳遞可變對象,而函數(shù)體修改了這一點,那么,這當(dāng)然是有副作用的,當(dāng)函數(shù)返回時,它們已經(jīng)更改了。
通過如下示例,我們可以看到其中的區(qū)別:
>>> def function(argument):... argument += ' in function'... print(argument)...>>> immutable = 'hello'>>> function(immutable)hello in function>>> mutable = list('hello')>>> immutable'hello'>>> function(mutable)['h', 'e', 'l', 'l', 'o', ' ', 'i', 'n', ' ', 'f', 'u', 'n', 'c', 't', 'i','o', 'n']>>> mutable['h', 'e', 'l', 'l', 'o', ' ', 'i', 'n', ' ', 'f', 'u', 'n', 'c', 't', 'i','o', 'n']>>>
這看起來可能不一致,但事實并非如此。當(dāng)我們傳遞第一個參數(shù)(一個字符串)時,它被分配給函數(shù)上的參數(shù)。由于string對象是不可變的,因此像“argument += <expression>”這樣的語句實際上會創(chuàng)建新對象“argument + <expression>”,并將其賦值給參數(shù)。此時,argument只是函數(shù)范圍內(nèi)的一個局部變量,與調(diào)用方中的原始變量無關(guān)。
此外,當(dāng)我們傳遞list時,它是一個可變對象,那么這個語句就有了不同的含義(它實際上等價于在那個list上調(diào)用.extend())。該操作符的作用是對一個包含原始list對象引用的變量就地修改list,從而修改它。
在處理這些類型的參數(shù)時,我們必須小心,因為它們可能導(dǎo)致意想不到的副作用。除非你絕對確定以這種方式操縱可變參數(shù)是正確的,否則應(yīng)避免使用它,并尋找沒有這些問題的替代方法。
不要改變函數(shù)參數(shù)。一般來說,應(yīng)盡量避免函數(shù)中的副作用。
與許多其他編程語言一樣,Python中的參數(shù)可以通過位置傳遞,也可以通過關(guān)鍵字傳遞。這意味著我們可以明確地告訴函數(shù)我們想要為它的哪個參數(shù)設(shè)置哪個值。唯一需要注意的是,在通過關(guān)鍵字傳遞參數(shù)之后,后面的其他參數(shù)也必須以這種方式傳遞,否則會引發(fā)SyntaxError異常。
Python和其他語言一樣,具有內(nèi)置的函數(shù)和結(jié)構(gòu),這些函數(shù)和結(jié)構(gòu)可以接收可變數(shù)量的參數(shù)??紤]這樣一種假設(shè),遵循類似C語言中printf函數(shù)結(jié)構(gòu)的字符串插值函數(shù)(無論是通過使用%運(yùn)算符還是字符串的格式化方法),第一個位置放置字符串類型參數(shù),緊隨其后的是任意數(shù)量的參數(shù),這些參數(shù)將被放置在標(biāo)記了的格式化字符串中。
除了使用Python中提供的函數(shù)外,我們還可以自己創(chuàng)建函數(shù),這兩種函數(shù)的使用方式類似。在本節(jié)中,我們將介紹可變參數(shù)函數(shù)的基本原理,同時給出一些建議。在下一節(jié)中,我們將探討當(dāng)函數(shù)參數(shù)過多時如何利用這些特征來處理常見的問題和約束。
對于位置參數(shù)的可變數(shù)量,在包裝這些參數(shù)的變量名之前,使用星號(*)。這是通過Python的打包機(jī)制實現(xiàn)的。
假設(shè)有一個函數(shù)有3個位置參數(shù)。在某段代碼中,我們恰好可以很方便地將傳遞給函數(shù)的參數(shù)存儲到一個列表中,列表的元素和函數(shù)的參數(shù)順序一致。我們可以使用打包機(jī)制,通過一條指令的方式一起傳遞這些參數(shù),而不是一個一個傳遞它們(就是說,將列表索引0中的元素傳遞給第一個參數(shù),將列表索引1中的元素傳遞給第二個參數(shù),并以此類推),一個一個傳遞參數(shù)的方式非常不符合Python的風(fēng)格。
>>> def f(first, second, third):... print(first)... print(second)... print(third)...>>> l = [1, 2, 3]>>> f(*l)123
打包機(jī)制的好處是它也可以反過來工作。如果我們想將一個列表的值按其各自的位置提取到變量中,就可以這樣分配它們:
>>> a, b, c = [1, 2, 3]>>> a1>>> b2>>> c3
部分解包也是可能的。假設(shè)我們只對序列的第一個值感興趣(可以是列表、元組或其他東西),在某個點之后,我們只希望其余的值放在一起。我們可以分配所需要的變量,把其余的放在一個打包列表中。解包的順序是不受限制的。如果在解包的部分沒有任何內(nèi)容可以放置,那么結(jié)果是一個空列表。我們鼓勵你在Python終端上嘗試一些示例,如下面的清單所示,并探索解包與生成器的關(guān)系:
>>> def show(e, rest):... print('Element: {0} - Rest: {1}'.format(e, rest))...>>> first, *rest = [1, 2, 3, 4, 5]>>> show(first, rest)Element: 1 - Rest: [2, 3, 4, 5]>>> *rest, last = range(6)>>> show(last, rest)Element: 5 - Rest: [0, 1, 2, 3, 4]>>> first, *middle, last = range(6)>>> first0>>> middle[1, 2, 3, 4]>>> last5>>> first, last, *empty = (1, 2)>>> first1>>> last2>>> empty[]
在迭代中可以找到解包變量的最佳用途之一。當(dāng)我們必須遍歷一組元素,而每個元素又依次是一個序列時,最好是在遍歷每個元素的同時解包。為了實際查看這樣的示例,我們將假設(shè)有一個函數(shù)用來接收數(shù)據(jù)庫行的列表,并負(fù)責(zé)從該數(shù)據(jù)創(chuàng)建用戶。第一個要實現(xiàn)的是,從行中每一列的位置獲取要構(gòu)造用戶的值,這根本不是習(xí)慣用法。第二個要實現(xiàn)的是,在迭代時進(jìn)行解包:
USERS = [(i, f'first_name_{i}', 'last_name_{i}') for i in range(1_000)]class User: def __init__(self, user_id, first_name, last_name): self.user_id = user_id self.first_name = first_name self.last_name = last_namedef bad_users_from_rows(dbrows) -> list: '''A bad case (non-pythonic) of creating ``User``s from DB rows.''' return [User(row[0], row[1], row[2]) for row in dbrows]def users_from_rows(dbrows) -> list: '''Create ``User``s from DB rows.''' return [ User(user_id, first_name, last_name) for (user_id, first_name, last_name) in dbrows ]
可以注意到,第二個版本更容易閱讀。在第一個版本的函數(shù)(bad_users_from_rows)中,有以row[0]、row[1]和row[2]的形式表示的數(shù)據(jù),這些數(shù)據(jù)并沒有說明它們是什么。換句話說,像user_id、first_name和last_name這樣的變量代表了它們自己。
在設(shè)計函數(shù)時,我們可以利用這種功能。
我們在標(biāo)準(zhǔn)庫中可以找到的一個示例是max函數(shù),它的定義如下:
max(...) max(iterable, *[, default=obj, key=func]) -> value max(arg1, arg2, *args, *[, key=func]) -> value With a single iterable argument, return its biggest item. The default keyword-only argument specifies an object to return if the provided iterable is empty. With two or more arguments, return the largest argument.
其中有一個類似的表示法,關(guān)鍵字參數(shù)用兩個星號(**)表示。如果有一個字典,我們用兩個星號把它傳遞給一個函數(shù),那么該函數(shù)會選擇鍵作為參數(shù)的名稱,然后把鍵的值作為函數(shù)中那個參數(shù)的值進(jìn)行傳遞。
例如,下面這行代碼:
function(**{'key': 'value'})
等同于:
function(key='value')
相反,如果我們定義一個參數(shù)以兩個星號開頭的函數(shù),則將發(fā)生相反的情況——關(guān)鍵字提供的參數(shù)將被打包到字典中:
>>> def function(**kwargs):... print(kwargs)...>>> function(key='value'){'key': 'value'}
我們認(rèn)為,如果函數(shù)或方法中的參數(shù)太多,就意味著代碼設(shè)計很糟糕(“代碼異味”)。鑒于此,我們將給出這個問題的解決方案。
一種解決方案是軟件設(shè)計的一個更通用的原則——具體化(為傳遞的所有參數(shù)創(chuàng)建一個新對象,這可能是我們?nèi)鄙俚某橄螅?。將多個參數(shù)壓縮到一個新對象中并不是Python特有的解決方案,而是可以應(yīng)用到任何編程語言中。
另一種解決方案是使用我們在上一節(jié)中看到的特定于Python的特性,利用變量位置參數(shù)和關(guān)鍵字參數(shù)創(chuàng)建具有動態(tài)簽名的函數(shù)。雖然這可能是一種Python式的處理方式,但我們必須小心,不要濫用該特性,因為可能創(chuàng)建了一些太過于動態(tài)的東西,以至于難以維護(hù)。在這種情況下,我們應(yīng)該看一下函數(shù)的主體。不管簽名或者參數(shù)是否似乎是正確的,如果函數(shù)使用參數(shù)的值做了太多不同的事情,那么這是一個信號——它必須被分解成多個更小的函數(shù)。(記住,函數(shù)應(yīng)該做一件事,而且僅做一件事?。?/span>
函數(shù)簽名的參數(shù)越多,這個參數(shù)就越有可能與調(diào)用方函數(shù)緊密耦合。
假設(shè)有兩個函數(shù)f1和f2,函數(shù)f2有5個參數(shù)。f2接收的參數(shù)越多,對于任何試圖調(diào)用該函數(shù)的人來說,收集所有信息并將其傳遞下去以便使其正常工作的難度就越大。
現(xiàn)在,f1似乎有所有這些信息,因為這些信息能正確調(diào)用f1,由此我們可以得出兩個結(jié)論。首先,f2可能是一個有漏洞的抽象概念,這意味著,當(dāng)f1知道f2所需要的所有東西時,它幾乎可以知道自己在做什么,并且能夠自行完成??偠灾琭2抽象得沒那么多。其次,f2看起來只對f1有用,很難想象在不同的上下文中使用這個函數(shù),這使得重用變得更加困難。
當(dāng)函數(shù)具有更通用的接口并且能夠處理更高級別的抽象時,它們就變得更加可重用。
這適用于所有類型的函數(shù)和對象方法,包括類的__init__方法。這種方法的出現(xiàn)通常(但并不總是)意味著應(yīng)該傳遞一個新的更高層級的抽象,或者存在一個缺失的對象。
如果一個函數(shù)需要太多的參數(shù)才能正常工作,就可以將其看作“代碼異味”。
事實上,這是一個設(shè)計問題——靜態(tài)分析工具,如Pylint(見第1章),在遇到這種情況時,默認(rèn)會發(fā)出警告。如果發(fā)生這種情況,不要抑制警告,而應(yīng)該重構(gòu)它。
假設(shè)我們找到一個需要太多參數(shù)的函數(shù),并且知道不能就這樣把它放置在代碼庫中,必須重構(gòu)它。但是,用什么方法呢?
根據(jù)具體情況,我們可以應(yīng)用以下一些規(guī)則。這些規(guī)則雖然不是廣泛適用的,但可以為我們解決那些常見問題提供思路。
有時,如果看到大多數(shù)參數(shù)屬于一個公共對象,就可以用一種簡單的方法更改參數(shù)。例如,考慮這樣一個函數(shù)調(diào)用:
track_request(request.headers, request.ip_addr, request.request_id)
現(xiàn)在,函數(shù)可能接收或不接收其他參數(shù),但這里有一點非常明顯:所有參數(shù)都依賴于request,那么為什么不直接傳遞request對象呢?這是一個簡單的更改,但是它顯著地改進(jìn)了代碼。正確的函數(shù)調(diào)用應(yīng)該是track_request(request)方法。進(jìn)一步來說,從語義上講,調(diào)用track_request(request)方法也更有意義。
雖然鼓勵傳遞這樣的參數(shù),但是在所有將可變對象傳遞給函數(shù)的情況下,我們必須非常小心副作用。我們調(diào)用的函數(shù)不應(yīng)該對傳遞的對象做任何修改,因為這會使對象發(fā)生變化,產(chǎn)生不希望出現(xiàn)的副作用。除非這實際上是想要的效果(在這種情況下,必須明確說明),否則不鼓勵這種行為。即使當(dāng)我們實際上想要更改正在處理的對象上的某些內(nèi)容時,更好的替代方法是復(fù)制它并返回(新的)修改后的版本。
處理不可變對象,并盡可能避免副作用。
這給我們帶來了一個類似的主題:分組參數(shù)。在前面的示例中,我們已經(jīng)對參數(shù)進(jìn)行了分組,但是沒有使用組(在本示例中是請求對象)。但是其他情況沒有這種情況那么明顯,我們可能希望將參數(shù)中的所有數(shù)據(jù)分組到能充當(dāng)容器的單個對象中。不用說,這種分組必須有意義。這里的想法是具體化:創(chuàng)建設(shè)計中缺少的抽象。
如果前面的策略不起作用,作為最后的手段,我們可以更改函數(shù)的簽名,以接收可變數(shù)量的參數(shù)。如果參數(shù)的數(shù)量太多,使用*args或**kwargs會使事情更加難以理解,所以我們必須確保接口被正確地記錄和使用,但在某些情況下,這是值得做的。
的確,用*args和**kwargs定義的函數(shù)非常靈活且適應(yīng)性強(qiáng),但缺點是失去了它的簽名,以及它的部分含義和幾乎所有易讀性。我們已經(jīng)看到了變量名(包括函數(shù)參數(shù))如何使代碼更容易閱讀的示例。如果一個函數(shù)將獲取任意數(shù)量的參數(shù)(位置或關(guān)鍵字),當(dāng)我們想要看看這個函數(shù)在未來可以做什么時,我們可能無法通過這些參數(shù)了解到這一點,除非有一個非常好的文檔說明。
本文摘自《編寫整潔的Python代碼》
[西] 馬里亞諾·阿那亞(Mariano Anaya) 著,包永帥,周立 譯
本書介紹Python軟件工程的主要實踐和原則,旨在幫助讀者編寫更易于維護(hù)和更整潔的代碼。全書共10章:第1章介紹Python語言的基礎(chǔ)知識和搭建Python開發(fā)環(huán)境所需的主要工具;第2章描述Python風(fēng)格代碼,介紹Python中的第一個習(xí)慣用法;第3章總結(jié)好代碼的一般特征,回顧軟件工程中的一般原則;第4章介紹一套面向?qū)ο筌浖O(shè)計的原則,即SOLID原則;第5章介紹裝飾器,它是Python的**特性之一;第6章探討描述符,介紹如何通過描述符從對象中獲取更多的信息;第7章和第8章介紹生成器以及單元測試和重構(gòu)的相關(guān)內(nèi)容;第9章回顧Python中最常見的設(shè)計模式;第10章再次強(qiáng)調(diào)代碼整潔是實現(xiàn)良好架構(gòu)的基礎(chǔ)。
本書適合所有Python編程愛好者、對程序設(shè)計感興趣的人,以及其他想學(xué)習(xí)更多Python知識的軟件工程的從業(yè)人員。