要旨
Excel VBAのWorkbook.Worksheets
プロパティのドキュメントを見ると、このように書かれています。
指定したブック内のすべてのワークシートを表す Worksheets コレクションを返します。 1
しかし、そうではない事実がコードを実行すると確認できます。
VBAでの再現
Worksheets
プロパティが返す型を確認するため、下記のVBAコードを実行してみます。
Public Sub VBASample()
Dim app As Excel.Application
Dim wb As Excel.Workbook
Set app = New Excel.Application
app.Visible = True
Set wb = app.Workbooks.Add
Debug.Print "Type of Worksheets: ", TypeName(wb.Worksheets)
app.Quit
End Sub
すると、Worksheets
プロパティは実際にはSheets
型を返していることがわかります。
Type of Worksheets: Sheets
Sheets
オブジェクト2はWorksheets
オブジェクト3のエイリアスというわけではなく、別のオブジェクトです。
Sheets
コレクションはWorksheet
4とChart
5 (そしてシートを右クリックしたメニューの「挿入」から追加できるExcel5.0ダイアログシートやExcel4.0マクロシートも含む)をアイテムとして扱えますが、Worksheets
コレクションはWorksheet
しかアイテムとして扱えません。
ワークシートとチャートをひっくるめて扱っているSheets
プロパティ6はSheets
型を返して型名とプロパティ名が一致しています。
一方でワークシートだけ扱うWorksheets
プロパティがWorksheets
型ではなくSheets
型なのはなかなかにややこしいです。
Debug.Print "Type of Sheets: ", TypeName(wb.Sheets)
Type of Sheets: Sheets
備考: Workbook.Charts
プロパティの場合
ちなみにCharts
プロパティがどうかというと、これもSheets
型です。
Debug.Print "Type of Charts: ", TypeName(wb.Charts)
Type of Charts: Sheets
しかしこちらについてCharts
プロパティのドキュメント7にはSheets
コレクションを返すと書いてある分、まだ親切です。
指定したブック内のすべてのグラフ シートを表す Sheets コレクションを返します。
Pythonとcomtypes
での再現
COM型ライブラリ情報に基づいて、COMインターフェースをPythonのクラスとして定義したモジュールを生成できるcomtypes
8でもこの挙動は再現されます。
>>> from comtypes.client import CreateObject
>>> app = CreateObject("Excel.Application")
>>> app.Visible = True
>>> wb = app.Workbooks.Add()
>>> print(wb.Worksheets) # doctest: +ELLIPSIS
<POINTER(Sheets) ptr=... at ...>
>>> app.Quit()
0
>>> exit()
実行時の振る舞いについて
VBAにしてもPython+comtypes
にしても、実行時にはたとえ返り値がSheets
型であってもWorkbook.Worksheets
プロパティが返すコレクションの中身はWorksheet
のみであり、Chart
が紛れているということはありません。
これは下記のVBAコードを実行することで確認できます。
Sub VBASample2()
' Sheet1とChart1があるワークブックで実行
Dim wb As Excel.Workbook
Dim sh As Variant
Set wb = ThisWorkbook
For Each sh In wb.Sheets
Debug.Print "Sheets:", sh.Name, TypeName(sh)
Next sh
Debug.Print "========"
For Each sh In wb.Worksheets
Debug.Print "Worksheets:", sh.Name, TypeName(sh)
Next sh
Debug.Print "========"
For Each sh In wb.Charts
Debug.Print "Charts:", sh.Name, TypeName(sh)
Next sh
End Sub
Sheets: Sheet1 Worksheet
Sheets: Chart1 Chart
========
Worksheets: Sheet1 Worksheet
========
Charts: Chart1 Chart
この仕様が記載されているドキュメント
Worksheets
プロパティがSheets
型を返す記載が全くないわけではありません。
なんとApplication.Worksheets
のドキュメント9にあります。
Workbook オブジェクトの場合は、指定したブック内のすべてのワークシートを表す Sheets コレクションを返します。
なぜこんなところに記載されているのかは想像にすぎませんがWorksheets
プロパティの仕様を書いた他のドキュメントから転用するときに紛れてしまったのかもしれません。
英語ドキュメントの構文部分には
Syntax
expression.Worksheets
があり、元になったドキュメントは汎用的にWorksheets
プロパティを説明していただろうことが推測できます。
これで困ることは何なのか
Sheets
型を返すメソッドやプロパティを呼び出す際、ワークシートのコレクションなのかチャートが入るコレクションなのかを区別する必要があります。
特に、ワークシートにはセルの概念がありますが、チャートはセルを持ちません。
なので、不用意にコレクションアイテムのセルにアクセスしようとしたときに、それがチャートだとエラーを起こします。
Sheets
とWorksheets
は紛らわしいので、コードレビューをすり抜けてバグを生みかねません。
どう混同を防止するか
私が参画しているPythonプロジェクトでは、コア機能提供ライブラリ内にワークブックCOMインターフェースの振る舞いをラップするオブジェクトを作り、それにワークシートだけがアイテムとなるコレクションを返すプロパティを実装しています。
これによって、プロジェクトに参画した開発者がSheets
プロパティかWorksheets
プロパティどちらを使えばいいか迷うことなくワークシートを簡単に取得でき、バグが入り込みにくいようにしています。
最後に
ExcelのCOMライブラリ内にはSheets
やWorksheets
のように混同しやすいものの他にも、メソッドの返り値の型について遅延評価を前提に作られている部分があり、型安全なプログラミングをやりづらい部分があります。
私はcomtypes
をラップしたExcel操作オープンソースライブラリを作ろうとしていますが、それらが原因で仕様を決めかねています。
ランタイムコードを定義するのも、型スタブを定義するのも、コード量が膨大になってしまうアイディアしかまだ浮かばない状況です。
いいアイディアがあれば、この記事のコメントなどで頂けると幸いです。
-
https://learn.microsoft.com/ja-jp/office/vba/api/excel.workbook.worksheets ↩
-
https://learn.microsoft.com/ja-jp/office/vba/api/excel.sheets ↩
-
https://learn.microsoft.com/ja-jp/office/vba/api/excel.worksheets ↩
-
https://learn.microsoft.com/ja-jp/office/vba/api/excel.worksheet ↩
-
https://learn.microsoft.com/ja-jp/office/vba/api/excel.chart(object) ↩
-
https://learn.microsoft.com/ja-jp/office/vba/api/excel.workbook.sheets ↩
-
https://learn.microsoft.com/ja-jp/office/vba/api/excel.workbook.charts ↩
-
https://learn.microsoft.com/ja-jp/office/vba/api/excel.application.worksheets ↩