注冊(cè)報(bào)錯(cuò)解決:NamedStyle機(jī)制與Excel導(dǎo)出實(shí)踐)
我在寫(xiě)批量導(dǎo)出Excel的腳本時(shí)最怕的不是業(yè)務(wù)邏輯復(fù)雜而是撞上樣式這種小但磨人的報(bào)錯(cuò)。前兩天在跑客戶(hù)訂單導(dǎo)出第一次執(zhí)行一切正常第二次一運(yùn)行就崩了控制臺(tái)丟出來(lái)一行干凈利落的錯(cuò)誤Style customer_style exists already。當(dāng)時(shí)我的第一反應(yīng)是同名樣式怎么會(huì)重復(fù)我明明全項(xiàng)目里只定義了一次customer_style。順著調(diào)用鏈一路翻下去才意識(shí)到問(wèn)題根本不在業(yè)務(wù)代碼而在openpyxl的樣式注冊(cè)機(jī)制——命名樣式在一個(gè)工作簿里是按名字全局索引的重復(fù)注冊(cè)就會(huì)被攔截。這篇筆記我就把這個(gè)問(wèn)題從頭到尾拆一遍包括報(bào)錯(cuò)背后的原理、四種不同的解決思路、適合批量導(dǎo)出場(chǎng)景的完整代碼模板以及我踩過(guò)的幾個(gè)坑。如果你正在用openpyxl寫(xiě)Excel導(dǎo)出、報(bào)表生成、表格自動(dòng)化工具這篇應(yīng)該能幫你省下不少排查時(shí)間。1. 先搞清楚這個(gè)報(bào)錯(cuò)到底是怎么來(lái)的1.1 報(bào)錯(cuò)現(xiàn)場(chǎng)與異常特征openpyxl不同版本拋出的異常形式略有差別。我用的是openpyxl 3.1.x實(shí)際輸出長(zhǎng)這樣ValueError: Style customer_style exists already在更早的版本里有人遇到過(guò)KeyError: Style customer_style exists already沒(méi)有被引號(hào)包裹。雖然顯示形式不同但核心信息一致代碼向同一個(gè)Workbook對(duì)象注冊(cè)了兩次同名的NamedStyle。很多剛接觸openpyxl的同學(xué)會(huì)對(duì)這個(gè)報(bào)錯(cuò)感到困惑因?yàn)楣饪创a自己可能只new了一次樣式對(duì)象。這里要先理解Excel和openpyxl對(duì)命名樣式的約束——命名樣式相當(dāng)于工作簿級(jí)別的格式模板里面定義好了字體、邊框、填充色、數(shù)字格式等一堆屬性然后以名字為索引供多個(gè)單元格反復(fù)引用。既然名字是索引那同一本工作簿里樣式名自然必須唯一。這就是customer_style exists already這條錯(cuò)誤信息的本質(zhì)。1.2 NamedStyle的注冊(cè)機(jī)制才是元兇要理解這個(gè)報(bào)錯(cuò)必須清楚openpyxl內(nèi)部是怎么處理命名樣式的。每個(gè)Workbook對(duì)象里都有一個(gè)named_styles屬性它本質(zhì)上是一個(gè)特殊的樣式列表。當(dāng)你做了類(lèi)似下面的操作from openpyxl.styles import NamedStyle style NamedStyle(namecustomer_style) ws[A1].style styleopenpyxl大概率會(huì)自動(dòng)把這個(gè)NamedStyle對(duì)象注冊(cè)到wb.named_styles列表中。注冊(cè)的邏輯是先檢查列表里是否已經(jīng)存在同名的樣式如果存在立即拋異常。這里的關(guān)鍵認(rèn)知是同樣是NamedStyle(namecustomer_style)在業(yè)務(wù)代碼里每new一次都覺(jué)得是新對(duì)象但到了Workbook的樣式注冊(cè)表里名字才是唯一標(biāo)識(shí)。名字相同就等于同一個(gè)Key后面再注冊(cè)就是重復(fù)Key。這個(gè)機(jī)制跟Python字典的行為非常像——同一個(gè)字典里不可能存在兩個(gè)相同的key否則后來(lái)的值無(wú)法區(qū)分。openpyxl之所以設(shè)計(jì)成這樣是為了保證最終保存的.xlsx文件結(jié)構(gòu)是干凈的。如果允許同名樣式重復(fù)注冊(cè)保存出來(lái)的文件里就會(huì)出現(xiàn)兩個(gè)名字一樣但定義不一致的樣式記錄Excel客戶(hù)端一打開(kāi)就會(huì)提示文件已損壞是否修復(fù)。所以這個(gè)報(bào)錯(cuò)其實(shí)是openpyxl在替Excel提前把關(guān)。1.3 觸發(fā)重復(fù)創(chuàng)建的三類(lèi)典型場(chǎng)景根據(jù)我自己的排查經(jīng)驗(yàn)這個(gè)報(bào)錯(cuò)最常見(jiàn)的誘因可以歸納成三類(lèi)。第一類(lèi)是循環(huán)里反復(fù)創(chuàng)建樣式。比如遍歷一行單元格想給每個(gè)格子都套一個(gè)客戶(hù)樣式代碼寫(xiě)成for cell in row: cell.style NamedStyle(namecustomer_style)第一次循環(huán)時(shí)樣式順利注冊(cè)進(jìn)工作簿第二次循環(huán)又新建了一個(gè)同名樣式去賦值于是直接拋異常。這種寫(xiě)法我見(jiàn)過(guò)不少很多人剛開(kāi)始以為每輪循環(huán)都得new一個(gè)樣式對(duì)象才能賦值實(shí)際上完全不需要。第二類(lèi)是多個(gè)導(dǎo)出函數(shù)各自定義同名樣式。項(xiàng)目一大導(dǎo)出邏輯拆成多個(gè)函數(shù)每個(gè)函數(shù)各管一段都順手定義了customer_style。單看每個(gè)函數(shù)都沒(méi)問(wèn)題但運(yùn)行時(shí)第一個(gè)函數(shù)注冊(cè)成功第二個(gè)函數(shù)再注冊(cè)就開(kāi)始撞車(chē)。第三類(lèi)是Jupyter Notebook里反復(fù)執(zhí)行同一個(gè)單元格。同一個(gè)kernel環(huán)境下如果之前的執(zhí)行結(jié)果還留在內(nèi)存里后續(xù)又重復(fù)執(zhí)行創(chuàng)建同名樣式的代碼就會(huì)觸發(fā)同樣的錯(cuò)誤。這個(gè)場(chǎng)景跟普通腳本不太一樣普通腳本每次啟動(dòng)都是全新環(huán)境很少受上次運(yùn)行殘留影響但Notebook的交互式特性會(huì)讓問(wèn)題被放大。注意如果你跑的是普通.py腳本每次運(yùn)行都是全新進(jìn)程基本不存在因?yàn)樯洗芜\(yùn)行殘留而報(bào)錯(cuò)的情況。真正需要排查的一定是同一個(gè)Workbook實(shí)例內(nèi)、同一次進(jìn)程里發(fā)生的重復(fù)注冊(cè)。2. 最常見(jiàn)的解法創(chuàng)建前先檢查樣式是否存在2.1 用 in 判斷的寫(xiě)法最容易想到的方案就是動(dòng)手創(chuàng)建之前先問(wèn)一句這個(gè)工作簿里是不是已經(jīng)有同名樣式了如果有直接用現(xiàn)成的如果沒(méi)有再創(chuàng)建。from openpyxl.styles import NamedStyle, Font if customer_style not in wb.named_styles: style NamedStyle(namecustomer_style) style.font Font(boldTrue) wb.add_named_style(style)注意這里依賴(lài)一個(gè)前提openpyxl新版本里NamedStyle對(duì)象的相等比較是基于名字的所以customer_style in wb.named_styles可以正常判斷。但我必須提醒一句這個(gè)寫(xiě)法在不同版本之間行為并不完全一致。2.2 兼容舊版本的遍歷寫(xiě)法如果你要維護(hù)的代碼需要跑在別人機(jī)器上而對(duì)方環(huán)境里openpyxl版本不可控那最穩(wěn)妥的方式是放棄in判斷改成一個(gè)不依賴(lài)任何版本行為的遍歷existing_names [s.name for s in wb.named_styles] if customer_style not in existing_names: style NamedStyle(namecustomer_style) style.font Font(boldTrue) wb.add_named_style(style)列表推導(dǎo)式直接提取所有已有樣式名然后做一次普通的字符串成員判斷。這段代碼對(duì)任何版本都成立因?yàn)閟.name一定是字符串字符串比較沒(méi)有歧義。我給內(nèi)部工具做兼容處理時(shí)一直用這種寫(xiě)法幾乎沒(méi)有返工過(guò)。2.3 封裝成樣式工廠函數(shù)與其在每個(gè)導(dǎo)出函數(shù)里重復(fù)寫(xiě)判斷邏輯不如直接封裝一個(gè)獲取樣式的工廠函數(shù)。它的職責(zé)很簡(jiǎn)單傳入樣式名如果工作簿里已經(jīng)有同名樣式直接返回現(xiàn)有對(duì)象如果沒(méi)有就創(chuàng)建一個(gè)并注冊(cè)進(jìn)去再返回。def get_named_style(wb, name, make_style): for s in wb.named_styles: if s.name name: return s style make_style() style.name name wb.add_named_style(style) return style調(diào)用方完全不關(guān)心這個(gè)樣式是新建的還是復(fù)用的只負(fù)責(zé)傳名字。這種方式最大的好處是把樣式是否已被注冊(cè)這個(gè)狀態(tài)收斂到一個(gè)函數(shù)里其他所有代碼都不用再擔(dān)心重復(fù)注冊(cè)的問(wèn)題。我在后面的第5節(jié)會(huì)給一個(gè)可以直接抄的完整模板。3. 穩(wěn)妥兜底用異常捕獲處理重復(fù)注冊(cè)3.1 try/except 捕獲同名沖突有時(shí)候你不想在創(chuàng)建前去查詢(xún)列表覺(jué)得多一次遍歷麻煩。那也可以反著來(lái)大膽創(chuàng)建撞車(chē)了再補(bǔ)救。用try/except把賦值過(guò)程包住捕獲到exists already之后改成用字符串方式引用已有樣式。try: ws[A1].style NamedStyle(namecustomer_style) except (ValueError, KeyError) as e: if exists already in str(e): ws[A1].style customer_style else: raise這里有一個(gè)我自己踩過(guò)的坑不同版本的openpyxl拋出的異常類(lèi)型不一樣可能是ValueError也可能是KeyError。所以捕獲時(shí)最好兩種都列上并且用錯(cuò)誤信息里的關(guān)鍵詞做二次判斷。不要圖省事寫(xiě)except Exception否則真正的編碼錯(cuò)誤會(huì)被一起吞掉排查起來(lái)更痛苦。3.2 刪除舊樣式再重建的做法還有一種思路是先把舊的同名樣式刪掉然后重新創(chuàng)建。在openpyxl新版本里按名字刪除是支持的if customer_style in wb.named_styles: del wb.named_styles[customer_style] style NamedStyle(namecustomer_style) # 重新配置樣式... wb.add_named_style(style)但說(shuō)實(shí)話(huà)我在實(shí)際項(xiàng)目中不太推薦刪除后重建除非你真的想徹底改變這個(gè)樣式的定義。原因是如果一個(gè)工作簿里已經(jīng)有單元格引用了customer_style你把舊樣式刪掉再重建同名樣式舊引用和新樣式之間會(huì)出現(xiàn)一個(gè)短暫的懸空期在邊界情況下可能導(dǎo)致保存后樣式失效。能用檢查后復(fù)用解決的場(chǎng)景沒(méi)必要繞道刪除重建。3.3 按名字刪除時(shí)容易踩的兩個(gè)坑第一個(gè)坑是老版本不支持按名字直接刪除。del wb.named_styles[customer_style]在3.x版本里好用但在更老的版本里可能直接拋KeyError。遇到這種情況只能手動(dòng)遍歷后刪除for i, s in enumerate(wb.named_styles): if s.name customer_style: del wb.named_styles[i] break第二個(gè)坑是刪除和重建之間不能夾其他邏輯。如果你刪完舊樣式還沒(méi)來(lái)得及注冊(cè)新樣式中間某個(gè)函數(shù)剛好要讀取這個(gè)樣式就會(huì)命中一個(gè)樣式不存在的窗口期。我在一次重構(gòu)里就因?yàn)檫@個(gè)踩過(guò)坑本來(lái)刪除和新建中間隔了幾行日志打印結(jié)果日志處理函數(shù)恰好也要用樣式程序直接崩了。所以刪除和重建之間盡量保持代碼簡(jiǎn)潔不要摻入其他業(yè)務(wù)邏輯。4. 更優(yōu)雅的思路改造現(xiàn)有樣式而不是新建4.1 直接修改已有樣式的屬性如果你的真實(shí)需求是調(diào)整現(xiàn)有樣式的某個(gè)字段那壓根不需要新建一個(gè)NamedStyle。直接從工作簿里把樣式撈出來(lái)改它的Font、Border、Fill、Alignment等屬性即可for s in wb.named_styles: if s.name customer_style: s.font Font(name微軟雅黑, size11, boldTrue) s.alignment Alignment(horizontalcenter, verticalcenter) break這種做法的好處很直接所有引用customer_style的單元格會(huì)同步更新不需要逐個(gè)單元格重新賦值。但反過(guò)來(lái)也要注意它是全局性的改動(dòng)——如果只有一個(gè)Sheet想要特殊效果改完可能影響其他Sheet的顯示。所以用之前先想清楚作用范圍。4.2 用copy復(fù)制命名樣式再改名為新樣式另一個(gè)常見(jiàn)需求是拿現(xiàn)有樣式做底子微調(diào)幾個(gè)字段生成一個(gè)新樣式。這時(shí)候不需要把字體、邊框、填充全部重新配置一遍用copy.copy淺拷貝然后改名即可from copy import copy base None for s in wb.named_styles: if s.name customer_style: base s break new_style copy(base) new_style.name customer_style_v2 new_style.font Font(name微軟雅黑, size14, boldTrue) wb.add_named_style(new_style)這里有兩個(gè)坑要提醒。第一淺拷貝之后Font、Border這些子對(duì)象有可能是共享的如果你只改new_style.font的對(duì)象屬性而不重新賦值整個(gè)Font對(duì)象可能會(huì)連累原樣式。所以修改前最好直接new一個(gè)新的Font對(duì)象賦上去而不是new_style.font.bold True這種深層修改。第二改名必須在add_named_style之前完成注冊(cè)之后名字就定死了再想改就得走刪除重建的流程。4.3 普通單元格樣式對(duì)象是更輕量的備選很多場(chǎng)景下你其實(shí)并不需要NamedStyle的重量級(jí)能力。比如只想給某個(gè)單元格加粗、填充背景色、加邊框直接用獨(dú)立的Font、PatternFill對(duì)象賦值就行from openpyxl.styles import Font, PatternFill ws[A1].font Font(boldTrue, colorFF0000) ws[A1].fill PatternFill(solid, fgColorFFF2CC)這種方式完全不涉及命名注冊(cè)自然也不存在重名沖突。NamedStyle和普通樣式對(duì)象的區(qū)別可以這樣理解NamedStyle是全局命名模板適合在整個(gè)工作簿里按名字反復(fù)引用直接賦值是一次性樣式每個(gè)單元格的樣式對(duì)象彼此獨(dú)立改一個(gè)不影響其他。如果你的樣式只用在一兩個(gè)單元格上直接用普通對(duì)象賦值代碼更簡(jiǎn)潔也不會(huì)踩到命名沖突的雷。5. 批量導(dǎo)出Excel場(chǎng)景下的完整代碼模板5.1 一個(gè)帶樣式工廠的導(dǎo)出腳本結(jié)合前面幾節(jié)的思路我給出一份可以直接套用在自己項(xiàng)目里的完整模板。核心是一個(gè)ensure_style函數(shù)負(fù)責(zé)在任意時(shí)刻確保指定樣式已注冊(cè)并返回這個(gè)樣式對(duì)象。from openpyxl import Workbook from openpyxl.styles import NamedStyle, Font, PatternFill, Alignment, Border, Side MARGIN Side(stylethin, color999999) def ensure_style(wb, namecustomer_style): 從工作簿中獲取指定命名樣式不存在則創(chuàng)建并注冊(cè) for s in wb.named_styles: if s.name name: return s style NamedStyle(namename) style.font Font(name微軟雅黑, size11, boldTrue) style.fill PatternFill(solid, fgColorFFF2CC) style.border Border( leftMARGIN, rightMARGIN, topMARGIN, bottomMARGIN ) style.alignment Alignment(horizontalcenter, verticalcenter) wb.add_named_style(style) return style def build_report(path): wb Workbook() ws wb.active ws.title 客戶(hù)訂單 style ensure_style(wb, customer_style) ws[A1] 客戶(hù)名稱(chēng) ws[A1].style style ws[A2] 某科技有限公司 ws[A2].style style wb.save(path) if __name__ __main__: build_report(report.xlsx)這段代碼的關(guān)鍵點(diǎn)在于ensure_style每次調(diào)用先遍歷wb.named_styles找到同名樣式就返回找不到才創(chuàng)建。因此無(wú)論build_report被調(diào)用幾次、在什么位置調(diào)用只要傳入同一個(gè)Workbook對(duì)象就不會(huì)觸發(fā)exists already。5.2 多Sheet、多函數(shù)調(diào)用的安全處理如果報(bào)表里有多張Sheet每張Sheet的表頭都要用同一種樣式建議在工作簿創(chuàng)建后第一時(shí)間統(tǒng)一取出樣式對(duì)象后續(xù)所有Sheet共用def build_multi_sheet_report(path, sheets_data): wb Workbook() style ensure_style(wb, customer_style) for sheet_name, rows in sheets_data.items(): ws wb.create_sheet(sheet_name) for row_idx, row_data in enumerate(rows, start1): for col_idx, value in enumerate(row_data, start1): cell ws.cell(rowrow_idx, columncol_idx, valuevalue) cell.style style wb.save(path)同一本工作簿內(nèi)多個(gè)函數(shù)同時(shí)處理不同Sheet時(shí)也都統(tǒng)一走ensure_style(wb, customer_style)獲取樣式對(duì)象不要在函數(shù)內(nèi)部私自new樣式。這樣可以保證全工作簿只有一個(gè)customer_style實(shí)例從源頭規(guī)避沖突。這里還要提醒一點(diǎn)如果項(xiàng)目里多個(gè)導(dǎo)出函數(shù)各自創(chuàng)建不同的Workbook那么每個(gè)Workbook都要單獨(dú)調(diào)用一次ensure_style。因?yàn)槊麡邮绞菕煸赪orkbook對(duì)象上的不是全局單例不要試圖用一個(gè)全局樣式對(duì)象覆蓋所有Workbook。5.3 服務(wù)端常駐進(jìn)程的樣式管理如果你的代碼跑在服務(wù)端常駐進(jìn)程里比如定時(shí)任務(wù)框架或者Web接口每次請(qǐng)求都會(huì)生成新的Workbook。這種場(chǎng)景下每個(gè)Workbook獨(dú)立注冊(cè)樣式通常沒(méi)有問(wèn)題但要注意別把樣式對(duì)象或Workbook對(duì)象緩存成全局變量。我見(jiàn)過(guò)一個(gè)事故有人把樣式對(duì)象放到模塊級(jí)常量為了省事直接復(fù)用結(jié)果同一個(gè)NamedStyle對(duì)象被塞進(jìn)了兩個(gè)不同的Workbook。后一個(gè)先注冊(cè)的還沒(méi)事另一個(gè)在保存或讀取時(shí)因?yàn)闃邮揭缅e(cuò)亂生成了無(wú)法正常打開(kāi)的xlsx文件。正確做法是樣式對(duì)象在函數(shù)內(nèi)部創(chuàng)建通過(guò)wb.add_named_style(style)綁定到當(dāng)次請(qǐng)求的Workbook不要跨Workbook共享同一個(gè)樣式實(shí)例。每次請(qǐng)求都走ensure_style既保證樣式的獨(dú)立性又避免了重復(fù)注冊(cè)的報(bào)錯(cuò)。如果并發(fā)量上來(lái)了多線(xiàn)程同時(shí)調(diào)用ensure_style理論上可能存在兩個(gè)線(xiàn)程同時(shí)發(fā)現(xiàn)樣式不存在、同時(shí)創(chuàng)建注冊(cè)的競(jìng)態(tài)。普通腳本場(chǎng)景可以忽略這個(gè)問(wèn)題但如果你在寫(xiě)Web服務(wù)接口最好在外層加一把互斥鎖把檢查樣式-創(chuàng)建樣式-注冊(cè)樣式整段保護(hù)起來(lái)避免極端并發(fā)下的重復(fù)注冊(cè)。6. 常見(jiàn)問(wèn)題與排查技巧實(shí)錄6.1 現(xiàn)象速查表報(bào)錯(cuò)信息或現(xiàn)象可能原因排查優(yōu)先級(jí)ValueError: Style customer_style exists already同一Workbook中同名NamedStyle被重復(fù)注冊(cè)高KeyError: Style customer_style exists already老版本openpyxl的同名沖突高保存后的文件提示文件已損壞樣式注冊(cè)混亂或刪除重建時(shí)舊引用未清理中改動(dòng)一個(gè)單元格樣式其他單元格跟著變使用了同一個(gè)NamedStyle對(duì)象屬于正常行為低6.2 排查步驟三板斧遇到這個(gè)報(bào)錯(cuò)我一般順著三個(gè)方向定位。第一步全項(xiàng)目搜索創(chuàng)建NamedStyle(name...)的地方看同一個(gè)樣式名出現(xiàn)了幾處。第二步確認(rèn)這些創(chuàng)建點(diǎn)是否共享同一個(gè)Workbook對(duì)象——只有同一個(gè)Workbook內(nèi)重復(fù)注冊(cè)才會(huì)觸發(fā)報(bào)錯(cuò)不同Workbook之間互相不影響。第三步把wb.named_styles構(gòu)建過(guò)程打印出來(lái)看報(bào)錯(cuò)那一刻已有樣式名列表是什么。最簡(jiǎn)單有效的調(diào)試方法是在樣式注冊(cè)前后各打印一次樣式名列表。print(before:, [s.name for s in wb.named_styles]) ws[A1].style NamedStyle(namecustomer_style) print(after:, [s.name for s in wb.named_styles])打印結(jié)果會(huì)直接告訴你撞車(chē)的原因是工作簿里提前存在了customer_style還是別的問(wèn)題。很多時(shí)候輸出一出來(lái)你立刻就能認(rèn)出是哪個(gè)函數(shù)、哪條分支提前注冊(cè)了樣式。6.3 我的幾個(gè)避坑心得最后分享幾個(gè)我在實(shí)際項(xiàng)目里沉淀下來(lái)的個(gè)人習(xí)慣。第一個(gè)習(xí)慣除非需要按名字在整個(gè)工作簿范圍復(fù)用樣式否則不要輕易使用NamedStyle。大部分報(bào)表的表頭加粗、背景填充、邊框線(xiàn)需求用普通Font、PatternFill直接賦值就夠了簡(jiǎn)單且不踩坑。NamedStyle是給那些真正需要在多個(gè)Sheet、多次運(yùn)行之間穩(wěn)定復(fù)用的場(chǎng)景準(zhǔn)備的。第二個(gè)習(xí)慣樣式注冊(cè)邏輯全部收斂到工廠函數(shù)里代碼庫(kù)里不允許出現(xiàn)散落的NamedStyle(name...)調(diào)用。這樣以后排查問(wèn)題只需要看一個(gè)地方不用全項(xiàng)目翻找。第三個(gè)習(xí)慣每次修改完樣式相關(guān)代碼至少連續(xù)運(yùn)行兩次腳本驗(yàn)證。第一次運(yùn)行可能碰巧不報(bào)錯(cuò)第二次才暴露重復(fù)注冊(cè)的問(wèn)題連續(xù)跑兩遍能把這個(gè)隱患提前暴露出來(lái)。第四個(gè)習(xí)慣如果你只是希望某個(gè)單元格啟用工作簿里已經(jīng)定義好的命名樣式直接用字符串賦值是最省事的寫(xiě)法ws[A1].style customer_style這樣既不會(huì)創(chuàng)建新對(duì)象也不會(huì)觸發(fā)注冊(cè)邏輯代碼干凈利落還會(huì)自動(dòng)繞開(kāi)所有重復(fù)注冊(cè)問(wèn)題。寫(xiě)到這里這個(gè)報(bào)錯(cuò)的來(lái)龍去脈就基本捋清了。從報(bào)錯(cuò)現(xiàn)場(chǎng)到源碼機(jī)制從四種解決思路到完整代碼模板中間穿插了幾個(gè)我實(shí)際踩過(guò)的坑。我最深的體會(huì)是openpyxl的NamedStyle設(shè)計(jì)本身沒(méi)毛病問(wèn)題在于它按名字索引的機(jī)制跟平時(shí)寫(xiě)Python對(duì)象時(shí)每次new都是獨(dú)立對(duì)象的直覺(jué)不一樣——在工作簿的世界里同名就是同一個(gè)Key。理解到這一層之后所有繞開(kāi)報(bào)錯(cuò)的做法都變得順理成章。下次再看到Style xxx exists already你可以直接對(duì)癥下藥不用像我當(dāng)初那樣對(duì)著報(bào)錯(cuò)發(fā)呆一中午了。