(FLINTERS設立10周年ブログリレー84日目)
Summary
PythonでCDKを書くと絶対に型が合わない箇所が生じる可能性があります。型安全愛好家の皆さんはある程度はあきらめましょう。
概要
例えばCDKを使ってLambdaとAPI Gatewayの構成を作るとします。Typescriptで書くと以下のような感じになると思います。
import * as cdk from "aws-cdk-lib";
import * as lambda from "aws-cdk-lib/aws-lambda";
import * as apigateway from "aws-cdk-lib/aws-apigateway";
import { Construct } from "constructs";
class TmpStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const lambdaFunction = new lambda.Function(this, "tmp-function", {
runtime: lambda.Runtime.NODEJS_18_X,
code: lambda.Code.fromAsset("./tmp"),
handler: "handler",
});
const api = new apigateway.RestApi(this, "tmp-api");
const resource = api.root.addResource("tmp");
resource.addMethod("GET", new apigateway.LambdaIntegration(lambdaFunction));
}
}
Pythonの場合は以下のようになりますが、
from aws_cdk import Stack, aws_lambda as _lambda, aws_apigateway as apigateway
from constructs import Construct
class TmpStack(Stack):
def __init__(
self, scope: Construct, construct_id: str, deploy_env: str, **kwargs
) -> None:
lambda_function = _lambda.Function(
self,
"tmp-function",
runtime=_lambda.Runtime.NODEJS_18_X,
code=_lambda.Code.from_asset("./tmp"),
handler="handler",
)
api = apigateway.RestApi(self, "tmp-api")
resource = api.root.add_resource("tmp")
resource.add_method("GET", apigateway.LambdaIntegration(lambda_function))
Pyright等で型チェックをしているとエラーが発生します。
ちなみにTypescriptで書いた場合は型エラーは発生しません。
エラーの内容を見てみると
"Function" is incompatible with protocol "IFunction"
Function
がIFunction
を実装していない???そんなことあるか?????
原因
このエラーの原因を解明するにはCDKの多言語対応の仕組みを理解する必要があります。
CDKはTypescript, Python, Java, Goなど様々な言語で書くことができますが、大元のコード自体はTypescriptで書かれていて、jsiiという仕組みを用いて他の言語からも呼べるようになっています。また、jsiiは各言語間の差異を吸収する働きも担っているようです。例えばGo言語はOptional
を表現する手段に乏しいため、Typescriptでいうところの
type SomeProps = {
name?: string
}
のような必須でないパラメータを表現することが難しくなっています。そこでjsiiはjsii.Number
やjsii.String
といった構造体を提供することで、Go言語でもOptional
なパラメータを指定しやすくしています。1
本題に戻ります。さきほどの型エラーの原因は、TypescriptとPythonの言語機能の差異によるものです。TypescriptでInterfaceを実装する場合、以下のようになります。
interface TmpInterface {
doSomething(someParam: string): void;
}
class TmpImplementation implements TmpInterface {
doSomething(differentName: string): void {
console.log(differentName);
}
}
ここで重要なのは、Typescriptには名前付き引数の機能がないため、メソッドの引数に割り当てる変数名が異なっていたとしても継承関係とみなされるということです。上の例ではInterface上はメソッドの引数はsomeParam
になっていますが、実装したクラス側ではdifferentName
になっています。変数名は異なるものの、メソッド全体のシグネチャは一致しているので、上の例は継承関係とみなされます。
ではPythonの場合はどうでしょう。上記のコードはjsiiを介したPython側では以下のように解釈されるようです。
class TmpInterface(abc.ABCMeta):
def do_something(self, some_param: str) -> None:
raise NotImplementedError()
class TmpImplementation(TmpInterface):
def do_something(self, different_name: str) -> None:
print(different_name)
Pythonには名前付き引数の機能があるため、TmpInterface
のインスタンスに対して
tmp_instance.do_something(some_param = "hoge")
と呼ぶことができます。逆にTmpInterface
を実装するクラスは上記のような呼び方をされても機能するために、引数の名前がTmpInterface
と一致している必要があります。よって、上記のPythonの例は継承関係とみなされません。
Typescript上では継承関係にあるはずのクラスがPython上では継承関係ではなくなってしまう、これが今回の問題の根本的な原因です。2
冒頭のエラーメッセージを詳しく見てみましょう。
Argument of type "Function" cannot be assigned to parameter "handler" of type "IFunction" in function "init"
"Function" is incompatible with protocol "IFunction"
"grant_invoke" is an incompatible type
Type "(grantee: IGrantable) -> Grant" cannot be assigned to type "(identity: IGrantable) -> Grant"
Parameter name mismatch: "identity" versus "grantee"
"grant_invoke_url" is an incompatible type
Type "(grantee: IGrantable) -> Grant" cannot be assigned to type "(identity: IGrantable) -> Grant"
Parameter name mismatch: "identity" versus "grantee"PylancereportGeneralTypeIssues
IFunction
のgrant_invoke
というメソッドは(identity: IGrantable)
という引数ですが、Function
はこのメソッドを(grantee: IGrantable)
という引数で実装してしまっているために、Python上では継承関係にない、即ち型に互換性がないとみなされてしまっています。
Workaround
血反吐を吐きながら# type: ignore
で型チェックを無視します。CDKv3とかで改善されると嬉しいですね。