1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[CDK][Python]絶対に合わない型

Last updated at Posted at 2023-12-01

(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等で型チェックをしているとエラーが発生します。

スクリーンショット 2023-11-28 18.02.27.png

ちなみにTypescriptで書いた場合は型エラーは発生しません。

エラーの内容を見てみると

"Function" is incompatible with protocol "IFunction"

FunctionIFunctionを実装していない???そんなことあるか?????

原因

このエラーの原因を解明するにはCDKの多言語対応の仕組みを理解する必要があります。

CDKはTypescript, Python, Java, Goなど様々な言語で書くことができますが、大元のコード自体はTypescriptで書かれていて、jsiiという仕組みを用いて他の言語からも呼べるようになっています。また、jsiiは各言語間の差異を吸収する働きも担っているようです。例えばGo言語はOptionalを表現する手段に乏しいため、Typescriptでいうところの

type SomeProps = {
  name?: string
}

のような必須でないパラメータを表現することが難しくなっています。そこでjsiiはjsii.Numberjsii.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

IFunctiongrant_invokeというメソッドは(identity: IGrantable)という引数ですが、Functionはこのメソッドを(grantee: IGrantable)という引数で実装してしまっているために、Python上では継承関係にない、即ち型に互換性がないとみなされてしまっています。

Workaround

血反吐を吐きながら# type: ignoreで型チェックを無視します。CDKv3とかで改善されると嬉しいですね。

  1. AWS CDK Goならではの制約とお作法を考えつつハローワールドしてみた

  2. CDKの開発チームもこの問題を認識しているようで、2021年の時点でIssueになっています。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?