[Python] 実例を見て分かるSet型の使い方

実例を見ながらPythonのSet型の使い方を理解しましょう。
2024.05.27

こんにちは。サービス開発室の武田です。

前回、集合型便利だよ!というお話をしました。

「便利そうなのはわかったけど、実際どう使うの?」という声が聞こえてきそうですね。そんなわけで、実例を交え紹介します。

Set型を利用した実例

boto3を使用した例に偏っているのは最近書いているコードがこういうものだからです。ご承知おきください。

指定のサービスが使用できるリージョンを求める

boto3にはget_available_regions()という、指定したサービスがサポートされているリージョンの一覧を取得するメソッドがあります。たとえばdetectiveであれば次のような結果が得られます。

>>> import boto3
>>> session = boto3.session.Session()
>>> session.get_available_regions("detective")
['af-south-1', 'ap-east-1', 'ap-northeast-1', 'ap-northeast-2', 'ap-south-1', 'ap-southeast-1', 'ap-southeast-2', 'ca-central-1', 'eu-central-1', 'eu-north-1', 'eu-south-1', 'eu-west-1', 'eu-west-2', 'eu-west-3', 'il-central-1', 'me-south-1', 'sa-east-1', 'us-east-1', 'us-east-2', 'us-west-1', 'us-west-2']

またdescribe_regions()という、アカウントで利用できるリージョンの一覧を取得するメソッドもあります。

>>> ec2 = boto3.client("ec2")
>>> ec2.describe_regions()["Regions"]
[{'Endpoint': 'ec2.ap-south-1.amazonaws.com', 'RegionName': 'ap-south-1', 'OptInStatus': 'opt-in-not-required'}, {'Endpoint': 'ec2.eu-north-1.amazonaws.com', 'RegionName': 'eu-north-1', 'OptInStatus': 'opt-in-not-required'}, {'Endpoint': 'ec2.eu-west-3.amazonaws.com', 'RegionName': 'eu-west-3', 'OptInStatus': 'opt-in-not-required'}, {'Endpoint': 'ec2.eu-west-2.amazonaws.com', 'RegionName': 'eu-west-2', 'OptInStatus': 'opt-in-not-required'}, {'Endpoint': 'ec2.eu-west-1.amazonaws.com', 'RegionName': 'eu-west-1', 'OptInStatus': 'opt-in-not-required'}, {'Endpoint': 'ec2.ap-northeast-3.amazonaws.com', 'RegionName': 'ap-northeast-3', 'OptInStatus': 'opt-in-not-required'}, {'Endpoint': 'ec2.ap-northeast-2.amazonaws.com', 'RegionName': 'ap-northeast-2', 'OptInStatus': 'opt-in-not-required'}, {'Endpoint': 'ec2.ap-northeast-1.amazonaws.com', 'RegionName': 'ap-northeast-1', 'OptInStatus': 'opt-in-not-required'}, {'Endpoint': 'ec2.ca-central-1.amazonaws.com', 'RegionName': 'ca-central-1', 'OptInStatus': 'opt-in-not-required'}, {'Endpoint': 'ec2.sa-east-1.amazonaws.com', 'RegionName': 'sa-east-1', 'OptInStatus': 'opt-in-not-required'}, {'Endpoint': 'ec2.ap-southeast-1.amazonaws.com', 'RegionName': 'ap-southeast-1', 'OptInStatus': 'opt-in-not-required'}, {'Endpoint': 'ec2.ap-southeast-2.amazonaws.com', 'RegionName': 'ap-southeast-2', 'OptInStatus': 'opt-in-not-required'}, {'Endpoint': 'ec2.eu-central-1.amazonaws.com', 'RegionName': 'eu-central-1', 'OptInStatus': 'opt-in-not-required'}, {'Endpoint': 'ec2.us-east-1.amazonaws.com', 'RegionName': 'us-east-1', 'OptInStatus': 'opt-in-not-required'}, {'Endpoint': 'ec2.us-east-2.amazonaws.com', 'RegionName': 'us-east-2', 'OptInStatus': 'opt-in-not-required'}, {'Endpoint': 'ec2.us-west-1.amazonaws.com', 'RegionName': 'us-west-1', 'OptInStatus': 'opt-in-not-required'}, {'Endpoint': 'ec2.us-west-2.amazonaws.com', 'RegionName': 'us-west-2', 'OptInStatus': 'opt-in-not-required'}]

これらを利用すれば「指定のアカウントで指定のサービス(ここではDetective)がサポートされているリージョン」を求められます。

>>> {r["RegionName"] for r in ec2.describe_regions()["Regions"]} & set(session.get_available_regions("detective"))
{'eu-west-3', 'sa-east-1', 'us-east-1', 'ap-southeast-2', 'ap-southeast-1', 'eu-west-2', 'us-west-2', 'eu-north-1', 'us-east-2', 'us-west-1', 'ap-northeast-1', 'ap-northeast-2', 'ap-south-1', 'eu-west-1', 'eu-central-1', 'ca-central-1'}

反対に、「有効なリージョンのうち、Detectiveがサポートされていないリージョン」も求められます。

>>> {r["RegionName"] for r in ec2.describe_regions()["Regions"]} - set(session.get_available_regions("detective"))
{'ap-northeast-3'}

ちなみに{n for n in ns}という表記は集合内包表記というもので、コレクションから新しい集合を生成できます。リスト内包表記の集合版です。

うーん、便利!

重複のない一覧を作成する

そもそもの話として、他のコレクションにないSet型の特徴は重複のないコレクションということです。たとえばS3バケットに次のようなデータが保存されているとします。JSON Linesという形式です。

{"id":"aaa","email":"takeda1@example.com"}
{"id":"bbb","email":"takeda2@example.com"}
{"id":"ccc","email":"takeda3@example.com"}
{"id":"ddd","email":"takeda1@example.com"}

このファイルをもとに、重複のないメールアドレスの一覧は次のように作成できます。

res = boto3.client("s3").select_object_content(
    Bucket="xxx",
    Key="yyy",
    InputSerialization={
        "JSON": {
            "Type": "LINES",
        }
    },
    OutputSerialization={"CSV": {"RecordDelimiter": "\n", "FieldDelimiter": ","}},
    ExpressionType="SQL",
    Expression="SELECT s.email FROM s3object s",
)

content = b"".join(
    event["Records"]["Payload"] for event in res["Payload"] if "Records" in event
).decode("UTF-8")

email_set = {c for c in content.strip().split("\n")}

SQLに詳しい方はDISTINCT使えばいいじゃん、と考えるでしょうか。しかし、残念ながらS3 SelectではDISTINCTは使えません。そんなわけで、取得側で工夫する必要があります。

うーん、便利!

ロールに所定のポリシーがアタッチされているかの確認

利用しているロールにアタッチされているべきポリシーがあるとします。Set型を使えば簡単にチェックできます。

>>> regulation_policyes = {"AAA", "BBB", "CCC"}
>>> iam = boto3.client("iam")
>>> set(iam.list_role_policies(RoleName="test-role")["PolicyNames"]) >= regulation_policyes

アタッチされていないポリシーも簡単に出せます。

>>> regulation_policyes - set(iam.list_role_policies(RoleName="test-role")["PolicyNames"])

うーん、便利!

権限の強いポリシーがアタッチされているかの確認

先ほどの例と似ていますが、IAMユーザーにアタッチされているポリシーのうち、権限の高いポリシーがあるかのチェックも可能です。ユーザーに直接アタッチされているポリシーだけでなく、属しているグループの権限もチェックが必要です。簡略化のためlist_xxxx()メソッドはそのまま書いています。実際には一度にすべての結果が返ってこない可能性もあるため、プロダクトコードではIsTruncatedフラグのチェックを必ずしてください。

>>> strong_authority = {"AdministratorAccess", "PowerUserAccess", "IAMFullAccess"}
>>> iam = boto3.client("iam")
>>> group_attached_policies = {p["PolicyName"] for g in iam.list_groups_for_user(UserName="test-user")["Groups"] for p in iam.list_attached_group_policies(GroupName=g["GroupName"])["AttachedPolicies"]}
>>> user_attached_policies = {p["PolicyName"] for p in iam.list_attached_user_policies(UserName="test-user")["AttachedPolicies"]}
>>> (user_attached_policies | group_attached_policies).isdisjoint(strong_authority)

うーん、便利!

まとめ

Set型による操作のイメージがちょっとずつでもついたでしょうか。複数の値どうしで何かしらの操作をする際には、集合でうまいことできないかな?と考えてみてください。