20200114のPythonに関する記事は30件です。

YouTubeで指定したチャンネルに投稿された動画の情報を自動で集める

やったこと(概要)

  1. 指定したチャンネルに投稿された動画リストを取得し、API経由で動画情報を取得してS3に保存するスクリプトの作成
  2. 上記を毎日0時(UTC)に定期実行するようにlambdaを設定

やったこと(詳細)

1. 動画情報を取得するスクリプトの作成

大まかに、下記の流れで動画情報を取得します
1. チャンネルIDから、そのチャンネルに投稿された動画が全て入っているプレイリスト(「アップロード動画」)のIDを取得
2. プレイリストIDから、そのプレイリストに含まれる動画のIDを取得
3. 2.で得られた動画IDのリストについて、順次APIから動画情報を取得して保存

なお、今回利用するYouTube Data API v3について、最初の登録に当たっては下記を参考にしました
(一年前にやったので状況が変わっている可能性もあります)
https://qiita.com/moshisora/items/4ea23d5abd7b4d852955

1.1 「アップロード動画」のIDを取得

YouTube Data API v3を使う際は、基本的に下記の3つを指定することで必要な情報を取得します

  • APIの種別 (チャンネル、プレイリスト、動画 など)
  • データの指定方法 (各種IDなど)
  • データの種類(part) (snippet, contentDetailsなど)

今回はchannel IDからアップロード動画(uploads)のIDが必要なので下記を選択しました

  • APIの種別: channels
  • データの指定方法: id (channel_id)
  • データの種類(part): contentDetails

これらのパラメータをGETで渡せばuploadsのIDを取得することが可能です。
どのAPIに何を渡すと何が得られるかは公式ドキュメントを参考にしてください。
各APIについて、「概要」にpartで何を指定すると何が得られるか、「list」にデータの指定をどうするかが記載してあります。
channels API: https://developers.google.com/youtube/v3/docs/channels?hl=ja

なお、APIのrate limitに至るまでの利用可能回数は指定したpartによっても変動するため、不要なpartは記載しない方がrate limit上は望ましいです。

上記指定について、requestsモジュールを使うと下記のようにかけます

import requests

def get_channel_info(channel_id):
    channel_url = 'https://www.googleapis.com/youtube/v3/channels'
    param = {
        'key': Browser Key
        , 'id': channel_id
        , 'part': 'contentDetails'
        # チャンネル名がとりたい場合はsnippet, 登録数がとりたい場合はstatisticsも指定する
        #, 'part': 'snippet, contentDetails, statistics'
    }

    req = requests.get(channel_url, params=param)
    return req.json()

channel_info = get_channel_info(channel_id)

# これを次で使う
playlist_id = channel_info["items"][0]["contentDetails"]["relatedPlaylists"]["uploads"]

channel IDはそのチャンネルのURLに含まれる文字列です。
例えば、 https://www.youtube.com/channel/UCllKI7VjyANuS1RXatizfLQ の場合、UCllKI7VjyANuS1RXatizfLQがchannel IDになります。

また、参考までに上記チャンネルIDの場合、APIの戻り値としてどのような結果になるかを下記に示します。
partとしてはcontentDetailsだけではなく、snippetやstatisticsも含まれています。
kind, etag, idはpartに記載しなくても入ってきます。

UCllKI7VjyANuS1RXatizfLQ.json
{
  "kind": "youtube#channelListResponse",
  "etag": "\"OOFf3Zw2jDbxxHsjJ3l8u1U8dz4/H539w7-hq_pSS8ne7t58G7iIBbc\"",
  "pageInfo": {
    "totalResults": 1,
    "resultsPerPage": 1
  },
  "items": [
    {
      "kind": "youtube#channel",
      "etag": "\"OOFf3Zw2jDbxxHsjJ3l8u1U8dz4/7lsjQIBo8ElaiiNhg9LX5OAT0Qw\"",
      "id": "UCllKI7VjyANuS1RXatizfLQ",
      "snippet": {
        "title": "山神 カルタ / Karuta Yamagami",
        "description": "みなさまはじめまして\nにじさんじ所属バーチャルライバー 山神カルタと申します。\n普段は修行をしているので山の中にいることが多いです。\n\n⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨\nにじさんじ所属バーチャルライバー 山神 カルタ\n\n見習いの烏天狗。\n立派な天狗となるべく、日々山で修業に勤しむ。\n休みの日は、内緒で人里まで降りたりしているとか、いないとか…\n翼は自由に出し入れできる(万能)\n\n⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨\n\n【Twitter】\n https://twitter.com/Karuta_Yamagami\n\n【お問い合わせやプレゼントはこちら】\nhttps://nijisanji.ichikara.co.jp/contact/\n\n【公式オンラインショップ】\nhttps://nijisanji.booth.pm/\n\n【Pixiv fanbox】\nhttps://www.pixiv.net/fanbox/creator/24113985\n\n【にじさんじ公式Twitter】\nhttps://twitter.com/nijisanji_app\n《@nijisanji_app 》\n\n【にじさんじ公式HP】\nhttps://nijisanji.ichikara.co.jp/",
        "publishedAt": "2019-08-26T10:03:13.000Z",
        "thumbnails": {
          "default": {
            "url": "https://yt3.ggpht.com/a/AGF-l7-9QUEt-T7wZaw_pYIUYckwhdVXbAkHcuNZHw=s88-c-k-c0xffffffff-no-rj-mo",
            "width": 88,
            "height": 88
          },
          "medium": {
            "url": "https://yt3.ggpht.com/a/AGF-l7-9QUEt-T7wZaw_pYIUYckwhdVXbAkHcuNZHw=s240-c-k-c0xffffffff-no-rj-mo",
            "width": 240,
            "height": 240
          },
          "high": {
            "url": "https://yt3.ggpht.com/a/AGF-l7-9QUEt-T7wZaw_pYIUYckwhdVXbAkHcuNZHw=s800-c-k-c0xffffffff-no-rj-mo",
            "width": 800,
            "height": 800
          }
        },
        "localized": {
          "title": "山神 カルタ / Karuta Yamagami",
          "description": "みなさまはじめまして\nにじさんじ所属バーチャルライバー 山神カルタと申します。\n普段は修行をしているので山の中にいることが多いです。\n\n⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨\nにじさんじ所属バーチャルライバー 山神 カルタ\n\n見習いの烏天狗。\n立派な天狗となるべく、日々山で修業に勤しむ。\n休みの日は、内緒で人里まで降りたりしているとか、いないとか…\n翼は自由に出し入れできる(万能)\n\n⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨\n\n【Twitter】\n https://twitter.com/Karuta_Yamagami\n\n【お問い合わせやプレゼントはこちら】\nhttps://nijisanji.ichikara.co.jp/contact/\n\n【公式オンラインショップ】\nhttps://nijisanji.booth.pm/\n\n【Pixiv fanbox】\nhttps://www.pixiv.net/fanbox/creator/24113985\n\n【にじさんじ公式Twitter】\nhttps://twitter.com/nijisanji_app\n《@nijisanji_app 》\n\n【にじさんじ公式HP】\nhttps://nijisanji.ichikara.co.jp/"
        },
        "country": "JP"
      },
      "contentDetails": {
        "relatedPlaylists": {
          "uploads": "UUllKI7VjyANuS1RXatizfLQ",
          "watchHistory": "HL",
          "watchLater": "WL"
        }
      },
      "statistics": {
        "viewCount": "979371",
        "commentCount": "0",
        "subscriberCount": "44300",
        "hiddenSubscriberCount": false,
        "videoCount": "70"
      }
    }
  ]
}

1.2 プレイリストIDから、動画のIDを取得

今回はplaylist IDから、そこに含まれる動画のリストが必要なので下記を選択しました

  • APIの種別: playlistItems
  • データの指定方法: playlistId
  • データの種類(part): snippet, contentDetails

また、指定したチャンネルが多数の動画をアップロードしていた場合に備えて下記のパラメータを指定します

  • maxResults: 50 (一度のAPI呼び出しで得られる結果(今回は動画)の上限、50が最大値)
  • pageToken: 一度のAPI呼び出しで結果を全て得られなかった場合に、続きからデータを取得する際に指定
def get_playlist_info(playlist_id, pageToken):
    playlist_url = 'https://www.googleapis.com/youtube/v3/playlistItems'
    param = {
        'key': Browser Key
        , 'playlistId': playlist_id
        , 'part': 'contentDetails'
        # 動画のタイトルとかもとりたい場合はsnippetも指定する
        #, 'part': 'snippet, contentDetails'
        , 'maxResults': '50'
        , 'pageToken': pageToken
    }

    req = requests.get(playlist_url, params=param)
    return req.json()


# 動画IDのリスト格納用の変数
uploaded_video_id_list = []

# pageTokenに空文字列を渡すと何も指定していないのと同じになる
pageToken = ""
while True:
    playlist_result = get_playlist_info(playlist_id, pageToken)

    # 今までの結果と今回の結果をマージする
    uploaded_video_id_list += [ item["contentDetails"]["videoId"] for item in playlist_result["items"] ]

    # 残りのアイテム数がmaxResultsを超えている場合はnextPageTokenが帰ってくる
    if "nextPageToken" in playlist_result:
        pageToken = playlist_result["nextPageToken"]
    else:
        break

1.1で取得したuploadsに入っている動画を取得すると下記のようになります

{
  "kind": "youtube#playlistItemListResponse",
  "etag": "\"OOFf3Zw2jDbxxHsjJ3l8u1U8dz4/rZQivmgvfKhGrH-c9M2Vvk9d2G8\"",
  "nextPageToken": "CDIQAA",
  "pageInfo": {
    "totalResults": 71,
    "resultsPerPage": 50
  },
  "items": [
    {
      "kind": "youtube#playlistItem",
      "etag": "\"OOFf3Zw2jDbxxHsjJ3l8u1U8dz4/LlKDbwqU0P3jM4uaTlSNqAe1RcA\"",
      "id": "VVVsbEtJN1ZqeUFOdVMxUlhhdGl6ZkxRLm95NVIxc0IycmRN",
      "snippet": {
        "publishedAt": "2020-01-13T10:21:23.000Z",
        "channelId": "UCllKI7VjyANuS1RXatizfLQ",
        "title": "【Splatoon2】ウデマエC-からの脱出【にじさんじ/山神カルタ】",
        "description": "脱出劇\n\nサムネ→椛ウム(@momizi_umu)さま\n____\n\nにじさんじ所属バーチャルライバー \n見習い烏天狗の山神カルタです。\n普段は山で修行をしています。\nいろんな時間をみんなと一緒に過ごせたら嬉しいなあ。\n____\n\n【Twitter】\n https://twitter.com/Karuta_Yamagami\n\n【お問い合わせやプレゼントはこちら】\nhttps://nijisanji.ichikara.co.jp/contact/\n\n【公式オンラインショップ】\nhttps://nijisanji.booth.pm/\n\nおてがみなど!?\n〒175-0082\n東京都板橋区高島平6-2-1 ネットデポ高島平内\nいちから株式会社 山神カルタ宛\n\n#山神カルタ",
        "thumbnails": {
          "default": {
            "url": "https://i.ytimg.com/vi/oy5R1sB2rdM/default.jpg",
            "width": 120,
            "height": 90
          },
          "medium": {
            "url": "https://i.ytimg.com/vi/oy5R1sB2rdM/mqdefault.jpg",
            "width": 320,
            "height": 180
          },
          "high": {
            "url": "https://i.ytimg.com/vi/oy5R1sB2rdM/hqdefault.jpg",
            "width": 480,
            "height": 360
          }
        },
        "channelTitle": "山神 カルタ / Karuta Yamagami",
        "playlistId": "UUllKI7VjyANuS1RXatizfLQ",
        "position": 0,
        "resourceId": {
          "kind": "youtube#video",
          "videoId": "oy5R1sB2rdM"
        }
      },
      "contentDetails": {
        "videoId": "oy5R1sB2rdM",
        "videoPublishedAt": "2020-01-13T10:21:23.000Z"
      }
    },
    : (略)
    {
      "kind": "youtube#playlistItem",
      "etag": "\"OOFf3Zw2jDbxxHsjJ3l8u1U8dz4/YpEY5UumudZmpMS82RezGcBMidg\"",
      "id": "VVVsbEtJN1ZqeUFOdVMxUlhhdGl6ZkxRLjBTY3Zzalhnbmo0",
      "snippet": {
        "publishedAt": "2019-11-13T13:57:12.000Z",
        "channelId": "UCllKI7VjyANuS1RXatizfLQ",
        "title": "【練習】うたのれんしゅう【山神カルタ】",
        "description": "練習つきあってくれ~~~\n\n____\n\nにじさんじ所属バーチャルライバー \n見習い烏天狗の山神カルタです。\n普段は山で修行をしています。\nいろんな時間をみんなと一緒に過ごせたら嬉しいなあ。\n\n配信タグ→#山神カタル\n____\n\n【Twitter】\n https://twitter.com/Karuta_Yamagami\n\n【お問い合わせやプレゼントはこちら】\nhttps://nijisanji.ichikara.co.jp/contact/\n\n【公式オンラインショップ】\nhttps://nijisanji.booth.pm/\n\n【Pixiv fanbox】\nhttps://www.pixiv.net/fanbox/creator/24113985\n\n【にじさんじ公式Twitter】\nhttps://twitter.com/nijisanji_app\n《@nijisanji_app 》\n\n【にじさんじ公式HP】\nhttps://nijisanji.ichikara.co.jp/\n\n#にじさんじ #山神カルタ",
        "thumbnails": {
          "default": {
            "url": "https://i.ytimg.com/vi/0ScvsjXgnj4/default.jpg",
            "width": 120,
            "height": 90
          },
          "medium": {
            "url": "https://i.ytimg.com/vi/0ScvsjXgnj4/mqdefault.jpg",
            "width": 320,
            "height": 180
          },
          "high": {
            "url": "https://i.ytimg.com/vi/0ScvsjXgnj4/hqdefault.jpg",
            "width": 480,
            "height": 360
          }
        },
        "channelTitle": "山神 カルタ / Karuta Yamagami",
        "playlistId": "UUllKI7VjyANuS1RXatizfLQ",
        "position": 49,
        "resourceId": {
          "kind": "youtube#video",
          "videoId": "0ScvsjXgnj4"
        }
      },
      "contentDetails": {
        "videoId": "0ScvsjXgnj4",
        "videoPublishedAt": "2019-11-13T13:57:12.000Z"
      }
    }
  ]
}

1.3 動画IDのリストから、動画情報を取得し、保存する

今回はvideo IDから、その動画の情報を取得したいので下記を選択しました

  • APIの種別: playlistItems
  • データの指定方法: playlistId
  • データの種類(part): snippet
def get_video_list(video_id_list):
    video_url = 'https://www.googleapis.com/youtube/v3/videos'
    param = {
        'key': Browser Key
        , 'id': ','.join(video_id_list)
        , 'part': 'snippet'
    }

    req = requests.get(video_url, params=param)
    return req.json()

# 取得する動画について順番にAPIを叩いていく
# uploaded_video_id_listにvideo IDが格納されている前提
# maxResultsに合わせて50単位でループを回していく
for start_index in range(0, len(uploaded_video_id_list), 50):
    end_index = min(start_index + 50, len(new_video_id_list))
    video_list_result = get_video_list(uploaded_video_id_list[start_index:end_index])

    # APIの戻り値の["items"]の各要素がそれぞれの動画を表すので、ループ処理する
    for item in video_list_result["items"]:
       # ここでitemを保存したりする
       print(item)

実際に取得できるjsonは下記のようになります

{
  "kind": "youtube#video",
  "etag": "\"OOFf3Zw2jDbxxHsjJ3l8u1U8dz4/kPHQ3DgOEmVwpAmO1NtWiI3J1t0\"",
  "id": "oy5R1sB2rdM",
  "snippet": {
    "publishedAt": "2020-01-13T10:21:23.000Z",
    "channelId": "UCllKI7VjyANuS1RXatizfLQ",
    "title": "【Splatoon2】ウデマエC-からの脱出【にじさんじ/山神カルタ】",
    "description": "脱出劇\n\nサムネ→椛ウム(@momizi_umu)さま\n____\n\nにじさんじ所属バーチャルライバー \n見習い烏天狗の山神カルタです。\n普段は山で修行をしています。\nいろんな時間をみんなと一緒に過ごせたら嬉しいなあ。\n____\n\n【Twitter】\n https://twitter.com/Karuta_Yamagami\n\n【お問い合わせやプレゼントはこちら】\nhttps://nijisanji.ichikara.co.jp/contact/\n\n【公式オンラインショップ】\nhttps://nijisanji.booth.pm/\n\nおてがみなど!?\n〒175-0082\n東京都板橋区高島平6-2-1 ネットデポ高島平内\nいちから株式会社 山神カルタ宛\n\n#山神カルタ",
    "thumbnails": {
      "default": {
        "url": "https://i.ytimg.com/vi/oy5R1sB2rdM/default.jpg",
        "width": 120,
        "height": 90
      },
      "medium": {
        "url": "https://i.ytimg.com/vi/oy5R1sB2rdM/mqdefault.jpg",
        "width": 320,
        "height": 180
      },
      "high": {
        "url": "https://i.ytimg.com/vi/oy5R1sB2rdM/hqdefault.jpg",
        "width": 480,
        "height": 360
      }
    },
    "channelTitle": "山神 カルタ / Karuta Yamagami",
    "categoryId": "20",
    "liveBroadcastContent": "none",
    "localized": {
      "title": "【Splatoon2】ウデマエC-からの脱出【にじさんじ/山神カルタ】",
      "description": "脱出劇\n\nサムネ→椛ウム(@momizi_umu)さま\n____\n\nにじさんじ所属バーチャルライバー \n見習い烏天狗の山神カルタです。\n普段は山で修行をしています。\nいろんな時間をみんなと一緒に過ごせたら嬉しいなあ。\n____\n\n【Twitter】\n https://twitter.com/Karuta_Yamagami\n\n【お問い合わせやプレゼントはこちら】\nhttps://nijisanji.ichikara.co.jp/contact/\n\n【公式オンラインショップ】\nhttps://nijisanji.booth.pm/\n\nおてがみなど!?\n〒175-0082\n東京都板橋区高島平6-2-1 ネットデポ高島平内\nいちから株式会社 山神カルタ宛\n\n#山神カルタ"
    }
  }
}

ここまででYouTube Data APIからのデータ取得は完了なのですが、定期実行をlambdaで実現する都合上、データの保存先をS3にすると取り回しやすいので、S3保存用のスクリプトも作成します。
参考: https://qiita.com/sokutou-metsu/items/5ba7531117224ee5e8af

import boto3
import json


s3 = boto3.resource('s3')
bucket_name = 【バケット名】
bucket = s3.Bucket(bucket_name)


def save_to_s3(bucket, path_string, contents):

    obj = bucket.Object(path_string)
    body = json.dumps(contents, ensure_ascii=False, indent=2)

    response = obj.put(
        Body=body.encode('utf-8'),
        ContentEncoding='utf-8',
        ContentType='text/plane'
    )

    return response

save_to_s3(bucket, 【保存するパス】, 【保存したい変数】)

上で作ったスクリプトと合わせると下記のようになります。

import boto3
import json

s3 = boto3.resource('s3')
bucket_name = 【バケット名】
bucket = s3.Bucket(bucket_name)

def save_to_s3(bucket, path_string, contents):

    obj = bucket.Object(path_string)
    body = json.dumps(contents, ensure_ascii=False, indent=2)

    response = obj.put(
        Body=body.encode('utf-8'),
        ContentEncoding='utf-8',
        ContentType='text/plane'
    )

    return response

def get_video_list(video_id_list):
    video_url = 'https://www.googleapis.com/youtube/v3/videos'
    param = {
        'key': Browser Key
        , 'id': ','.join(video_id_list)
        , 'part': 'snippet'
    }

    req = requests.get(video_url, params=param)
    return req.json()

# 取得する動画について順番にAPIを叩いていく
# uploaded_video_id_listにvideo IDが格納されている前提
# maxResultsに合わせて50単位でループを回していく
for start_index in range(0, len(uploaded_video_id_list), 50):
    end_index = min(start_index + 50, len(new_video_id_list))
    video_list_result = get_video_list(uploaded_video_id_list[start_index:end_index])

    # APIの戻り値の["items"]の各要素がそれぞれの動画を表すので、ループ処理する
    for item in video_list_result["items"]:
        # videoID.jsonという名前で保存する
        video_id = item["id"]
        save_to_s3(bucket, video_id + ".json", item)

2. 1.で作ったスクリプトをlambda経由で定期実行させる

1.で作ったスクリプトをlambda経由で定期実行させます。
各種サービスの連携にあたってはAWS SAMを使用しました。

AWS SAMを使うのが初めてだったので、下記の流れで目標に近づけていきました
1. インストール/チュートリアルをやる
2. 自動でlambdaが起動するように設定
3. 動画情報取得スクリプトをlambdaに組み込む

2.1 インストール/チュートリアル

https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/serverless-getting-started.html のHello Worldのサンプルまでやっていきました。

余談ですが、途中、awsコマンドが入ってないことに気づき pip3 install awscli したのですが、brewでインストールしたpython3でpipしたモジュールにPATHを通すのに苦戦しました。結果、下記設定で通せました。

export PATH=/Users/xuser/Library/Python/3.6/bin/:$PATH

参考: https://stackoverflow.com/questions/26574232/aws-cli-path-settings
(最も評価が高い回答ではないので注意)

2.2 自動でlambdaが起動するように設定

https://qiita.com/hf7777hi/items/e68948c948eb303b1900 を参考にして、Hello Worldが定期実行できるようにtemplate.yamlを修正しました。チュートリアルだとAPI Gatewayとの連携が入っているのですが、それは今回不要なのでこのタイミングで削除しました

2.3 動画情報取得スクリプトをlambdaに組み込む

下記3箇所を修正します
1. モジュール名(チュートリアルのままならhello_world)/app.py
2. モジュール名(チュートリアルのままならhello_world)/requirements.txt
3. template.yaml

その上で、下記コマンドを叩けばlambda(と他に必要なものあればそこにも)デプロイされるはずです

$ sam build
$ sam sam deploy --guided

2.3.1 app.pyの修正

ベタで書いていた部分(もしくは、main等に書いていた部分)を lambda_handler という関数の中に入れれば終わりです。
チュートリアルのままだとimport文が過不足していると思われるので適宜追加してください

2.3.2 requirements.txt

pip freeze > requirements.txt とかで使うrequirements.txtと同じです。boto3など必要なライブラリを記載してください。チュートリアルのままであれば requests が最初から入っているかと思いますので、その感じで記載すればokです。

lambdaで外部ライブラリを使用する際はライブラリをzipで固めてアップロード。。。みたいな作業が必要なのですが、aws-sam-cliを使うとそのあたりをよしなにやってくれるので、あまり気にしなくても動きます

2.3.3 template.yaml

こんな感じにします。
※Descriptionは消しました

template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31

Globals:
  Function:
    Timeout: 900

Resources:
  HelloWorldFunction:
    Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
    Properties:
      CodeUri: hello_world/
      Handler: app.lambda_handler
      Runtime: python3.7
      Policies:
        - AWSLambdaBasicExecutionRole
        - Version: '2012-10-17' # Policy Document
          Statement:
            - Effect: Allow
              Action:
                - s3:GetObject # 保存だけなら不要
                - s3:GetObjectACL # 保存だけなら不要
                - s3:PutObject
                - s3:PutObjectACL
              Resource: 'arn:aws:s3:::【バケット名】/*'
        # 保存だけなら次のPolicyは不要
        - Version: '2012-10-17' # Policy Document 
          Statement:
            - Effect: Allow
              Action:
                - s3:ListBucket
              Resource: 'arn:aws:s3:::*'
      Events:
        Run:
          Type: Schedule
          Properties:
            Schedule: cron(0 0 * * ? *)
            Name: run-schedule
            Description: trigger for collecting videos
            Enabled: True

Outputs:
  HelloWorldFunction:
    Description: "Hello World Lambda Function ARN"
    Value: !GetAtt HelloWorldFunction.Arn
  HelloWorldFunctionIamRole:
    Description: "Implicit IAM Role created for Hello World function"
    Value: !GetAtt HelloWorldFunctionRole.Arn

ポイントはPoliciesで必要な権限を付与するところかと思います。
チュートリアルだとPoliciesの設定を特にしていないのですが、その場合デフォルトでAWSLambdaBasicExecutionRole が付与されるのでそれは残しつつ、S3関連のものを追加しました。

保存だけであればPutObject関連だけで良いと思われますが、実際に動かしているスクリプトではデータの取得やバケットに入ったファイルのリストも行なっているので追加でGetObjectやListBucketの権限も付与しています。

また、cronに関してはUTCでの時刻になるので日本時間基準で設定する場合は時差を考慮する必要があります。

この修正に当たっては下記を参考にしました

まとめと所感

YouTube Data APIを使用したデータ取得の流れから、それを定期実行させるところまでを説明しました。
YouTube Data APIに関しては公式ドキュメントが比較的わかりやすいのと、OAuth等も不要で取得方法がシンプルなので比較的とっつきやすいAPIかと思います。とっつきやすさの割に動画の再生数やコメントなど使いたくなりそうなものは概ね取得できるので、使っていて楽しいAPIだなと感じています。

また、分析用データを収集しようとすると何らかの定期実行システムが必要になることもありますが、lambdaやS3を活用することでサーバを実際に借りるよりも安価な情報の取得・保持が可能となります。lambdaの設定をコンソールでぽちぽちやるとだるい部分も多いのですが、SAM(というかcloudformation)がいい感じにラップしてくれるのでその部分の負担も下がってきています。データの取得はやりたいことの本質じゃないことが多いので、その部分がサクッと組めると分析が楽しくできて良いのかなと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Python3 List/辞書型メモ

忘れないようにメモ。

型について

# List型
  sampleA = [[name,"Sato"],[age,1]], [[name,"Mori"],[age,20]]

# Dictionary型
  sampleB = {"apple":1, "orange":2}

# 型を調べる
  type(sampleA)
  type(sampleB)

#List型からKey/Valueを取り出す
  for index, value in enumerate(sampleA)

#辞書型からKey/Valueを取り出す
  for index, value in items(sampleA)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ortoolpy.logistics_networkの使用時に、日本語の列名を使いたくない場合の対処

はじめに

「Python実践データ分析100本ノック」をやっていくなかで、ortoolpyというモジュールのなかの、logistics_networkという機能を使う内容がありました。

生産計画の最適化、というようなことに使える機能なのですけれども、動きを追ってみると、引数として与えるDataFrameのcolumn名に"工場"などの日本語を使うことがデフォルトとなっています。

ただ、列名などに日本語を使うのはちょっと気持ち悪い、と感じてしまうこともあります。

そんな時に、どうしたらいいか対応を考えてみました。

対策

ortoolpyのソースコードのlogistics_netrworkの宣言の部分を見ると、以下のように書いてあります。
image.png
これを見ると、"需要地"や"工場"、といったcolumn名は、logistics_network関数のデフォルト引数で指定されていることが分かります。
ですので、たとえば"需要地"であれば、引数depを指定してlogistics_networkを実行すれば、"需要地"という日本語でのcolumn名を使わずに実行することができます。

コード例

_, tbdi2, _ = logistics_network(
                    tbde, tbdi, tbfa,
                    dep = "demand_place", # 需要地
                    dem = "demand" , # 需要
                    fac = "factory", # 工場
                    prd = "product", # 製品
                    tcs = "trans_cost", # 輸送費
                    pcs = "product_cost", # 生産費
                    lwb = "lower_limit", # 下限
                    upb = "upper_limit" # 上限
                    )

参考

http://pydoc.net/ortoolpy/0.2.10/ortoolpy.etc/

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

自己紹介

自己紹介

勉強しようと思ったらXcodeのアプデが必要で結構時間かかりそうだったのとカレー2杯食べて腹一杯なので今日は自己紹介をします。

高校時代

高校までは四国のある県で過ごしてました。自転車であちこち行ってました。
割とうるさいやつでしたね。

大学時代

大学は情報系でC言語を学習して実習でマイコンを動かしたりしてました。
研究は半導体の研究室だったのでプログラミングはほとんど使いませんでしたね。

社会人

新卒で半導体の会社に入社しました。
配属が明るくてちゃんとコミュニケーションが取れるからと生産管理配属になり、業務を行っておりました。
しかし、土日の出勤や1年目から毎月40時間付近まで残業があったりで休日が欲しいなと思って2年目で転職しましたね。
今の会社も最初は生産管理で入社したんですが、1年後に物流課に配属が変わり現在となります。
今はフォークリフト乗って、製品の入出庫作業やピッキングしたりたまに物流のスキーム構築のプロジェクト参加したりとルーティンワーク多めです。

きっかけ

今の部署で、資料作成の時に過去1年間のデータを他のエクセルから引っ張ってこなければならない時があり、その時に自動でしてくれたら楽なのにと思ったのがプログラミングをまた勉強しようと思った理由です。

感想

pythonはC言語に比べるとまだ学びやすいような気がします。引き続き勉強していき1,2月で基礎を固めます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

django oscar 簡易チュートリアル

ecサイトを一人でパパッと作る必要性が生じたので、パッケージやフレームワークをフル活用すればパパッとECサイトが完成する(はず)。てことで、ウェブフレームワークにDjango。そしてECパッケージにOscarを使用。

django-oscarのdjango-oscar/Building your own shopを基本的になぞっているだけですが参考に。

最終的に決済機能のないECサイトを構築します。また、これはあくまでもチュートリアルです。

開発環境

  • Mac OS X
  • Python==3.8.0
  • Django==2.2.9
  • django-oscar==2.0.4

Setup

ドキュメントでは、mkvirtualenvvirtualenvを使ってますが、今回は自分のパソコンに元から入っていたAnacondaを活用します。

Anacondaで仮想環境を構築する。まず、python 3.8環境を構築。

conda create -n py38 python=3.8

仮想環境を有効化。

conda info -e #環境確認
conda activate py38

django-oscarをpipでインストール。

pip install django-oscar

一部足りないパッケージがあるので、それもインストール。

pip install sorl-thumbnail
pip install pysolr

なお、2020年1月時点だと、バージョンは以下の通り

  • pysolr==3.8.1
  • sorl-thumbnail==12.5.0

Djangoプロジェクトを作成する

django-admin startproject frobshop

Django設定(settings.py)

frobshop.frobshop.settings.pyの行頭に以下を追記。Oscarのデフォルト設定がインポートされる。

frobshop.frobshop.settings.py
from oscar.defaults import *

Oscarのcontext processorsをテンプレートに追加。

frobshop.frobshop.settings.py
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
                'oscar.apps.search.context_processors.search_form',
                'oscar.apps.checkout.context_processors.checkout',
                'oscar.apps.customer.notifications.context_processors.notifications',
                'oscar.core.context_processors.metadata',
            ],
        },
    },
]

INSTALLED_APPSを以下のように書き換え、SITE_IDも設定。

frobshop.frobshop.settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',

    'django.contrib.sites',
    'django.contrib.flatpages',

    'oscar',
    'oscar.apps.analytics',
    'oscar.apps.checkout',
    'oscar.apps.address',
    'oscar.apps.shipping',
    'oscar.apps.catalogue',
    'oscar.apps.catalogue.reviews',
    'oscar.apps.partner',
    'oscar.apps.basket',
    'oscar.apps.payment',
    'oscar.apps.offer',
    'oscar.apps.order',
    'oscar.apps.customer',
    'oscar.apps.search',
    'oscar.apps.voucher',
    'oscar.apps.wishlists',
    'oscar.apps.dashboard',
    'oscar.apps.dashboard.reports',
    'oscar.apps.dashboard.users',
    'oscar.apps.dashboard.orders',
    'oscar.apps.dashboard.catalogue',
    'oscar.apps.dashboard.offers',
    'oscar.apps.dashboard.partners',
    'oscar.apps.dashboard.pages',
    'oscar.apps.dashboard.ranges',
    'oscar.apps.dashboard.reviews',
    'oscar.apps.dashboard.vouchers',
    'oscar.apps.dashboard.communications',
    'oscar.apps.dashboard.shipping',

    # 3rd-party apps that oscar depends on
    'widget_tweaks',
    'haystack',
    'treebeard',
    'sorl.thumbnail',
    'django_tables2',
]

SITE_ID = 1

oscar.apps.basket.middleware.BasketMiddlewaredjango.contrib.flatpages.middleware.FlatpageFallbackMiddlewareMIDDLEWAREの設定に追加。

frobshop.frobshop.settings.py
MIDDLEWARE = (
    ...
    'oscar.apps.basket.middleware.BasketMiddleware',
    'django.contrib.flatpages.middleware.FlatpageFallbackMiddleware',
)

メールアドレスで認証できるようにAUTHENTICATION_BACKENDSの設定を追加。

frobshop.frobshop.settings.py
AUTHENTICATION_BACKENDS = (
    'oscar.apps.customer.auth_backends.EmailBackend',
    'django.contrib.auth.backends.ModelBackend',
)

日本対応

日本向け対応するには以下のように、言語コードと時間帯を書き換える。

frobshop.frobshop.settings.py
LANGUAGE_CODE = 'ja'
TIME_ZONE = 'Asia/Tokyo'

通貨設定と通貨のフォーマットを設定(oscarのデフォルトのフォーマットだと小数点第二位まで表示してしまうため。)

frobshop.frobshop.settings.py
OSCAR_DEFAULT_CURRENCY = 'JPY'
OSCAR_CURRENCY_FORMAT = '¤#,##0'

URLs

frobshop.frobshop.urls.pyを以下のように書き換える。

frobshop.frobshop.urls.py
from django.apps import apps
from django.urls import include, path  # > Django-2.0
from django.contrib import admin

urlpatterns = [
    path('i18n/', include('django.conf.urls.i18n')),  # > Django-2.0

    # The Django admin is not officially supported; expect breakage.
    # Nonetheless, it's often useful for debugging.

    path('admin/', admin.site.urls),  # > Django-2.0

    path('', include(apps.get_app_config('oscar').urls[0])),  # > Django-2.0
]

Search backend

検索システムを設定する。とりあえず試したい場合は以下の設定をする。

frobshop.frobshop.settings.py
HAYSTACK_CONNECTIONS = {
    'default': {
        'ENGINE': 'haystack.backends.simple_backend.SimpleEngine',
    },
}

Apache Solrを設定する場合は事前にApache Solrをセットアップした上で、以下をfrobshop.frobshop.settings.pyに追記。

frobshop.frobshop.settings.py
HAYSTACK_CONNECTIONS = {
    'default': {
        'ENGINE': 'haystack.backends.solr_backend.SolrEngine',
        'URL': 'http://127.0.0.1:8983/solr',
        'INCLUDE_SPELLING': True,
    },
}

データベース

すぐにでも動作確認したい場合はデータベースにsqliteを使う。frobshop.frobshop.settings.pyを編集してDATABASESの設定を以下のように書き換える。

frobshop.frobshop.settings.py
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
        'USER': '',
        'PASSWORD': '',
        'HOST': '',
        'PORT': '',
        'ATOMIC_REQUESTS': True,
    }
}

サイトの起動確認

マイグレーションを実施。

python manage.py migrate

サーバーを起動してサイトが起動できるかどうかを確認。

python manage.py runserver

初期データ

ここまでだと、商品がないECサイトが構築される。諸々の設定を行なって、ECサイトっぽくしていきます。

国モデル

国モデル ( address_country ) に249カ国の初期データを入れる。

$ pip install pycountry
$ python manage.py oscar_populate_countries

249カ国は多すぎるので、登録国を日本のみにする。まず、DBへの接続してから、is_shipping_country を Japan のみ 1に、他は 0 にする。住所登録時に国名選択が出てこなくなる(日本がデフォルトになる)

$ python manage.py dbshell
SQLite version 3.30.1 2019-10-10 20:19:45
Enter ".help" for usage hints.
sqlite> UPDATE address_country SET is_shipping_country = 0 WHERE printable_name != 'Japan';

ユーザーデータ

モデルに対する初期データを投入するを参考にデータを投入する。

まずはユーザーデータを投入する。Sandboxに登録されているユーザー情報を投入。

$ python manage.py loaddata frobshop/fixtures/auth.json
Installed 2 object(s) from 1 fixture(s)

管理者ユーザーは以下の設定になっています。

username: superuser
email: superuser@example.com
password: testing

スタッフアカウントは以下の通り。

username: staff
email: staff@example.com
password: testing

商品データ

商品データを投入する。公式のSandbox siteには二種類商品データが存在する。multi-stockrecord-product.jsonchild_products.json

それぞれの違いを説明する。child_products.jsonは一つの商品でも種類が違う場合(例えば、Tシャツだとサイズとか色とか)に対応したい場合の設定の仕方を示している。例えば、Tシャツの場合は、AttributesをサイズL,M,Sと設定する。

multi-stockrecord-product.jsonはecサイトを運営している場合、在庫を管理している倉庫あるいは発注先が複数存在する場合がある。複数の発注先をoscarではpartner.stockrecordで管理している。

商品データをとりあえず投入したい場合は、いずれかのjsonをデータベースに投入する。

$ python manage.py loaddata frobshop/fixtures/multi-stockrecord-product.json

oscarがどのように商品管理するのか、考え方を示したドキュメントを以下にリンクを貼っておきます。

オーダーパイプライン

ecサイト経由で注文が来た場合、どのように注文を処理するかをオーダーパイプライン(order pipeline)で設定する。例えば、settings.pyに以下を追記する。以下の例だとPendingが最初のステータスで、PendingBeing processedProcessedの順に進んでいく。途中でキャンセルされるとCancelledのステータスになることを想定している。

frobshop.frobshop.settings.py
OSCAR_INITIAL_ORDER_STATUS = 'Pending'
OSCAR_INITIAL_LINE_STATUS = 'Pending'
OSCAR_ORDER_STATUS_PIPELINE = {
    'Pending': ('Being processed', 'Cancelled',),
    'Being processed': ('Processed', 'Cancelled',),
    'Cancelled': (),
}

メール設定

上記の設定のままだと注文確定時(Preview orderの画面のPlace orderを押す時)にConnectionRefusedError at /checkout/preview/が出る。

Building a full ecommerce site part 5: testing directing customers to Oscar's thank-you page after paymentに理由が書いているけれど、読んでもよく分からなかった。

対処法はsettings.pyに以下のようにEMAIL_BACKENDを追記する。

frobshop.frobshop.settings.py
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'

まとめ

とりあえず、ここまで設定すればECサイトの機能が一通り網羅されます(決済機能以外)。

さらにECサイトを構築する (例えば商品に画像を追加するなど)にはGithub上のSandboxサイトの設定を参考に構築してみてください。

気が向いたら決済機能の実装を書きたいと思います。

参考サイト

以下のサイトを参考にしました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

python リストから重複している要素を取得する。

Django のregistry.pyより

counts = Counter(
    app_config.name for app_config in self.app_configs.values())
duplicates = [
    name for name, count in counts.most_common() if count > 1]

Counterはコンストラクタの引数にリストを渡すとそれらの要素とその回数のデータを取得する。
duplicatesの部分では、そのままではforで繰り返しできないのでCounterのmost_common()で出現回数の多い順に並べられたタプルのリストに変換し、ifで出現回数が1より大きい、つまり重複しているもののみのリストを作成している。

from collections import Counter

names = ["Jhon", "Thomas", "Alice", "Jhon", "Alice", "Jhon"]

counts = Counter(names)
dupulicates = [name for name, count in counts.most_common() if count > 1]
print(dupulicates) # >>> ['Jhon', 'Alice']
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

python メモ リストから重複している要素を取得する。

Django のregistry.pyより

counts = Counter(
    app_config.name for app_config in self.app_configs.values())
duplicates = [
    name for name, count in counts.most_common() if count > 1]

Counterはコンストラクタの引数にリストを渡すとそれらの要素とその回数のデータを取得する。
duplicatesの部分では、そのままではforで繰り返しできないのでCounterのmost_common()で出現回数の多い順に並べられたタプルのリストに変換し、ifで出現回数が1より大きい、つまり重複しているもののみのリストを作成している。

from collections import Counter

names = ["Jhon", "Thomas", "Alice", "Jhon", "Alice", "Jhon"]

counts = Counter(names)
dupulicates = [name for name, count in counts.most_common() if count > 1]
print(dupulicates) # >>> ['Jhon', 'Alice']
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ブロックチェーンの改ざん検知を実装する

はじめに

以前こちらの記事で書いたPythonのブロックチェーンプログラムに改ざん検知のテストを追加してみました。

改ざん検知の仕組み

ブロックチェーンでは各ブロックが一つ前のブロックのハッシュ値を保持しており、改ざんが行われるとハッシュ値の整合性が取れなくなります。早速実装してみましょう。
動作としては単純で、各ブロックが持っているprevious_hash、nonce、transactionの値を使用してハッシュ値を取り、特定の文字列になっているかを確認しています。nonceを探すのは試行回数が必要なので時間がかかりますが、検算は一瞬で行えるのがいいところですね。
関数内で使用されているproof_of_work()は渡されたデータから作られるハッシュ値が特定の文字列となっているかを返す関数です。

    def check_falsification(self):
        print('='*30 + 'Check Falsification')
        for chain_index, block in enumerate(self.chain):
            if chain_index == 0:
                continue
            else:
                if not self.proof_of_work(
                        previous_hash=block['previous_hash'],
                        nonce=block['nonce'],
                        transaction=block['transaction']
                ):
                    print(f'[Block {chain_index}]Falsification is detected!!')
                    return
        print('There is no falsification')

まずは改ざんなしで実行

さて、完成したプログラムがこちらになります。
以下のプログラム内ではまだ改ざんが行われておらず、check_falsification()を実行しても改ざんは検知されません。

ソースコード

import hashlib
import time
import json


class BlockChain(object):
    def __init__(self):
        self.chain = []
        self.transaction_pool = []
        self.create_block(previous_hash='Initialize')

    def make_hash(self, block):
        json_block = json.dumps(block)
        return hashlib.sha256(json_block.encode()).hexdigest()

    def create_block(self, previous_hash=None, nonce=0, transaction=None, timestamp=time.time()):
        block = {
            'previous_hash': previous_hash,
            'nonce': nonce,
            'transaction': transaction,
            'timestamp': timestamp
        }
        self.add_block_to_chain(block)

    def add_block_to_chain(self, block):
        self.chain.append(block)

    def mining(self):
        previous_hash = self.make_hash(self.chain[-1])
        nonce = 0
        transaction = self.transaction_pool
        self.transaction_pool = []
        while True:
            if self.proof_of_work(previous_hash, nonce, transaction):
                break
            else:
                nonce += 1
        timestamp = time.time()
        self.create_block(previous_hash, nonce, transaction, timestamp)

    def proof_of_work(self, previous_hash, nonce, transaction):
        guess_block = {
            'previous_hash': previous_hash,
            'nonce': nonce,
            'transaction': transaction
        }
        guess_block_hash = self.make_hash(guess_block)
        return guess_block_hash.startswith('0' * 3)

    def add_transaction(self, sender_name, reciever_name, value):
        transaction = {
            'sender_name': sender_name,
            'reciever_name': reciever_name,
            'value': value
        }
        self.transaction_pool.append(transaction)

    def print_chain(self):
        for chain_index, block in enumerate(self.chain):
            print(f'{"=" * 30}Block {chain_index:3}')
            if block['transaction'] is None:
                print('Initialize')
                continue
            else:
                for key, value in block.items():
                    if key == 'transaction':
                        for transaction in value:
                            print(f'{"transaction":15}:')
                            for kk, vv in transaction.items():
                                print(f'\t{kk:15}:{vv}')
                    else:
                        print(f'{key:15}:{value}')

    def check_falsification(self):
        print('='*30 + 'Check Falsification')
        for chain_index, block in enumerate(self.chain):
            if chain_index == 0:
                continue
            else:
                if not self.proof_of_work(
                        previous_hash=block['previous_hash'],
                        nonce=block['nonce'],
                        transaction=block['transaction']
                ):
                    print(f'[Block {chain_index}]Falsification is detected!!')
                    return
        print('There is no falsification')


if __name__ == '__main__':
    # print('='*30 + 'Start' + '='*30)
    blockchain = BlockChain()
    blockchain.add_transaction(sender_name='Alice', reciever_name='Bob', value=100)
    blockchain.add_transaction(sender_name='Alice', reciever_name='Chris', value=1)
    blockchain.mining()
    blockchain.add_transaction(sender_name='Bob', reciever_name='Dave', value=100)
    blockchain.mining()
    blockchain.add_transaction(sender_name='Chris', reciever_name='Dave', value=5)
    blockchain.mining()
    blockchain.check_falsification()

    # Make Falsification
    blockchain.chain[2]['transaction'][0]['value'] = 10000
    blockchain.print_chain()
    blockchain.check_falsification()

実行結果

==============================Block   0
Initialize
==============================Block   1
previous_hash  :c9fe668feb5a3661762f088253b12e12d782b33437569403f905e5f99d6cf001
nonce          :96
transaction    :
    sender_name    :Alice
    reciever_name  :Bob
    value          :100
transaction    :
    sender_name    :Alice
    reciever_name  :Chris
    value          :1
timestamp      :1579000533.0407777
==============================Block   2
previous_hash  :052ebe44ca81cdeacddf8f04ccfc7bd0523f6846ec23cd513e7afc4535575365
nonce          :2298
transaction    :
    sender_name    :Bob
    reciever_name  :Dave
    value          :100
timestamp      :1579000533.0647144
==============================Block   3
previous_hash  :425a90af9c398e5cd7a2fd2acb349d684c1be2f02a1dc497642a7597b009ce63
nonce          :73
transaction    :
    sender_name    :Chris
    reciever_name  :Dave
    value          :5
timestamp      :1579000533.0657504
==============================Check Falsification
There is no falsification ←改ざんなし

改ざんと検知

では次に、二番目のブロック内のトランザクションを改ざんしてみましょう。Daveさんが自分の残高を増やそうと改ざんを行ったとして、プログラムの末尾に以下を追記します。

    blockchain.chain[2]['transaction'][0]['value'] = 10000

さらに再度改ざん検知を実行するため、以下も追記します。

    blockchain.check_falsification()

ここでプログラムを実行してみます。

実行結果

==============================Block   0
Initialize
==============================Block   1
previous_hash  :a7f827462b7bdfeacf2fb8706cbc2808e11d41df22536b45054890939a78ff7a
nonce          :541
transaction    :
    sender_name    :Alice
    reciever_name  :Bob
    value          :100
transaction    :
    sender_name    :Alice
    reciever_name  :Chris
    value          :1
timestamp      :1579003434.094443
==============================Block   2
previous_hash  :5f5eeb0fa58f186b8200d4ac85c5ec6822213913a4e541fe41ecb227b0b737a2
nonce          :1231
transaction    :
    sender_name    :Bob
    reciever_name  :Dave
    value          :10000 ←改ざんされた部分
timestamp      :1579003434.101424
==============================Block   3
previous_hash  :7a72053a43fe0c7a8919cdcb999099fa2daf648b95c8cc3b50d40be7b66dff64
nonce          :8453
transaction    :
    sender_name    :Chris
    reciever_name  :Dave
    value          :5
timestamp      :1579003434.1502934
==============================Check Falsification
[Block 2]Falsification is detected!! ←改ざん検知

ということで[Block 2]において改ざんが検知されました。最終的なソースコードは以下になります。

ソースコード

import hashlib
import time
import json


class BlockChain(object):
    def __init__(self):
        self.chain = []
        self.transaction_pool = []
        self.create_block(previous_hash='Initialize')

    def make_hash(self, block):
        json_block = json.dumps(block)
        return hashlib.sha256(json_block.encode()).hexdigest()

    def create_block(self, previous_hash=None, nonce=0, transaction=None, timestamp=time.time()):
        block = {
            'previous_hash': previous_hash,
            'nonce': nonce,
            'transaction': transaction,
            'timestamp': timestamp
        }
        self.add_block_to_chain(block)

    def add_block_to_chain(self, block):
        self.chain.append(block)

    def mining(self):
        previous_hash = self.make_hash(self.chain[-1])
        nonce = 0
        transaction = self.transaction_pool
        self.transaction_pool = []
        while True:
            if self.proof_of_work(previous_hash, nonce, transaction):
                break
            else:
                nonce += 1
        timestamp = time.time()
        self.create_block(previous_hash, nonce, transaction, timestamp)

    def proof_of_work(self, previous_hash, nonce, transaction):
        guess_block = {
            'previous_hash': previous_hash,
            'nonce': nonce,
            'transaction': transaction
        }
        guess_block_hash = self.make_hash(guess_block)
        return guess_block_hash.startswith('0' * 3)

    def add_transaction(self, sender_name, reciever_name, value):
        transaction = {
            'sender_name': sender_name,
            'reciever_name': reciever_name,
            'value': value
        }
        self.transaction_pool.append(transaction)

    def print_chain(self):
        for chain_index, block in enumerate(self.chain):
            print(f'{"=" * 30}Block {chain_index:3}')
            if block['transaction'] is None:
                print('Initialize')
                continue
            else:
                for key, value in block.items():
                    if key == 'transaction':
                        for transaction in value:
                            print(f'{"transaction":15}:')
                            for kk, vv in transaction.items():
                                print(f'\t{kk:15}:{vv}')
                    else:
                        print(f'{key:15}:{value}')

    def check_falsification(self):
        print('='*30 + 'Check Falsification')
        for chain_index, block in enumerate(self.chain):
            if chain_index == 0:
                continue
            else:
                if not self.proof_of_work(
                        previous_hash=block['previous_hash'],
                        nonce=block['nonce'],
                        transaction=block['transaction']
                ):
                    print(f'[Block {chain_index}]Falsification is detected!!')
                    return
        print('There is no falsification')


if __name__ == '__main__':
    # print('='*30 + 'Start' + '='*30)
    blockchain = BlockChain()
    blockchain.add_transaction(sender_name='Alice', reciever_name='Bob', value=100)
    blockchain.add_transaction(sender_name='Alice', reciever_name='Chris', value=1)
    blockchain.mining()
    blockchain.add_transaction(sender_name='Bob', reciever_name='Dave', value=100)
    blockchain.mining()
    blockchain.add_transaction(sender_name='Chris', reciever_name='Dave', value=5)
    blockchain.mining()
    blockchain.check_falsification()

    # Make Falsification
    blockchain.chain[2]['transaction'][0]['value'] = 10000
    blockchain.print_chain()
    blockchain.check_falsification()

まとめ

今回は改ざんから検知までを実装してみました。本来であればBlockChainの変数はカプセル化して直接の編集を行えないようにすべきですね。今回の実装は会社からの帰宅途中の電車でPythonista3というアプリを使って行ったのですが、浮かんだ考えをすぐ実行できるという意味でとてもよかったです。1,200円しましたがいい買い物でした。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

python isinstance

isinstance(オブジェクト,クラス)はそのオブジェクトがクラスのインスタンスかをboolで返してくれる。
サブクラスのオブジェクトと親クラスの組み合わせでもtrueが帰ってくる。

class Parent:
   def __init__(self):
       pass

class Child(Parent):
    def __init__(self):
        pass

p = Parent()
c = Child()

isinstance(p, Parent) # >>> true
isinstance(c, Parent) # >>> true

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

リスト内包表記

1
t = (1, 2, 3, 4, 5)
l = []

for i in t:
    l.append(i)

print(l)
1の実行結果
[1, 2, 3, 4, 5]

これをリスト内包表記で書くと

1をリスト内包表記で
t = (1, 2, 3, 4, 5)
l = [i for i in t]
print(l)
1をリスト内包表記での実行結果
[1, 2, 3, 4, 5]

3で割った余りが0の数字だけリストに入れる場合を考える。

2
t = (1, 2, 3, 4, 5, 6)
l = []

for i in t:
    if i % 3 == 0:
        l.append(i)

print(l)
2の実行結果
[3, 6]

これをリスト内包表記で書くと

2をリスト内包表記で
t = (1, 2, 3, 4, 5, 6)
l = [i for i in t if i % 3 == 0]
print(l)
2をリスト内包表記での実行結果
[3, 6]

二つのタプルがあり、
それらを操作した数をリストに入れる場合

3
t = (1, 2, 3)
t2 = (4, 5, 6, 7, 8, 9)
l = []

for i in t:
    for j in t2:
        l.append(i * j)

print(l)
3の実行結果
[4, 5, 6, 7, 8, 9, 8, 10, 12, 14, 16, 18, 12, 15, 18, 21, 24, 27]

これをリスト内包表記で書くと

3をリスト内包表記で
t = (1, 2, 3)
t2 = (4, 5, 6, 7, 8, 9)

l = [i * j for i in t for j in t2]
print(l)
3をリスト内包表記での実行結果
[4, 5, 6, 7, 8, 9, 8, 10, 12, 14, 16, 18, 12, 15, 18, 21, 24, 27]

リスト内包表記で書けるからといって、
forループを2つも3つも内包させたりして、
長くなるとコードが読みにくくなるので避けるべき。

上記の「2をリスト内包表記で」位までにする事が好ましい。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

pybitflyerでsendparentorderが出せずに無駄に悩んだ件

はじめに

bitflyerのAPIでは4つの特殊注文が出せるとのことです。

order_method: 注文方法です。以下の値のいずれかを指定してください。省略した場合の値は "SIMPLE" です。
"SIMPLE": 1 つの注文を出す特殊注文です。
"IFD": IFD 注文を行います。一度に 2 つの注文を出し、最初の注文が約定したら 2 つめの注文が自動的に発注される注文方法です。
"OCO": OCO 注文を行います。2 つの注文を同時に出し、一方の注文が成立した際にもう一方の注文が自動的にキャンセルされる注文方法です。
"IFDOCO": IFD-OCO 注文を行います。最初の注文が約定した後に自動的に OCO 注文が発注される注文方法です。

以下より引用
https://lightning.bitflyer.com/docs

bitflyerのAPIをpythonで呼び出す際に便利なpybitflyerというパッケージがあります。(pipでインストールできます。)
このパッケージを使って上記の特殊注文を出したい、というお話です。
https://github.com/yagays/pybitflyer

準備

必要なパッケージをimportしておきます。API_KEYとAPI_SECRETは別ファイルで用意します。

import time 
import pybitflyer
import configparser
import json 
import requests

config = configparser.ConfigParser()
config.read('./config.txt')
API_KEY = config['bf']['api_key']
API_SECRET = config['bf']['api_secret']

api = pybitflyer.API(api_key=API_KEY,api_secret=API_SECRET)

※注意事項

以下、API_KEYとAPI_SECRETが正しく設定されていた場合注文が通る可能性があります。
損失が発生しても当方は一切責任を負いませんのでご了承ください。

失敗例

特殊注文を出す関数は「sendparentorder」です。パラメータは以下のサンプルをそのまま使います。
https://lightning.bitflyer.com/docs
特殊注文の種類は「IFDOCO」とします。

api.sendparentorder(
    {
  "order_method": "IFDOCO",
  "minute_to_expire": 10000,
  "time_in_force": "GTC",
  "parameters": [{
    "product_code": "BTC_JPY",
    "condition_type": "LIMIT",
    "side": "BUY",
    "price": 30000,
    "size": 0.1
  },
  {
    "product_code": "BTC_JPY",
    "condition_type": "LIMIT",
    "side": "SELL",
    "price": 32000,
    "size": 0.1
  },
  {
    "product_code": "BTC_JPY",
    "condition_type": "STOP_LIMIT",
    "side": "SELL",
    "price": 28800,
    "trigger_price": 29000,
    "size": 0.1
  }]
})
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-19-2b96f6a935f1> in <module>
     24     "price": 28800,
     25     "trigger_price": 29000,
---> 26     "size": 0.1
     27   }]
     28 })

TypeError: sendparentorder() takes 1 positional argument but 2 were given

→引数がおかしいと怒られる

成功例

parametersより上の部分から辞書に含んでいたのが原因だとわかりました。bitflyerのドキュメントのサンプルはpybitflyerを使わず直にAPIを叩くときのサンプルです。その証拠に引数はbitflyerのドキュメントのサンプルそのままにAPIを直打ちしたら注文が通ります。

api.sendparentorder(
  order_method =  "IFDOCO",
  minute_to_expire = 10000,
  time_in_force = "GTC",
  parameters = [{
    "product_code": "FX_BTC_JPY",
    "condition_type": "LIMIT",
    "side": "BUY",
    "price": 900000,
    "size": 0.01
  },
  {
    "product_code": "FX_BTC_JPY",
    "condition_type": "LIMIT",
    "side": "SELL",
    "price": 950000,
    "size": 0.01
  },
  {
    "product_code": "FX_BTC_JPY",
    "condition_type": "STOP_LIMIT",
    "side": "SELL",
    "price": 800000,
    "trigger_price": 810000,
    "size": 0.01
  }]
)
{'parent_order_acceptance_id': 'JRF20200114-110537-710593'}

API直打ち注文だとサンプルそのまま貼り付けでOK(資金がないのでFX_BTC_JPYに変えていますが。)

def order(side,price,size):
    base_url = "https://api.bitflyer.jp"
    path_url = "/v1/me/sendparentorder"
    method = "POST"
    api_key = API_KEY
    api_secret = API_SECRET

    timestamp = str(datetime.datetime.today())

    params = {
      "order_method": "IFDOCO",
      "minute_to_expire": 10000,
      "time_in_force": "GTC",
      "parameters": [{
        "product_code": "FX_BTC_JPY",
        "condition_type": "LIMIT",
        "side": "BUY",
        "price": 920000,
        "size": 0.1
      },
      {
        "product_code": "FX_BTC_JPY",
        "condition_type": "LIMIT",
        "side": "SELL",
        "price": 1000000,
        "size": 0.1
      },
      {
        "product_code": "FX_BTC_JPY",
        "condition_type": "STOP_LIMIT",
        "side": "SELL",
        "price": 900000,
        "trigger_price": 890000,
        "size": 0.1
      }]
    }

    body = json.dumps(params)

    message = timestamp + method + path_url + body
    signature = hmac.new(bytearray(api_secret.encode('utf-8')), message.encode('utf-8') , digestmod = hashlib.sha256 ).hexdigest()

    headers = {
        'ACCESS-KEY' : api_key,
        'ACCESS-TIMESTAMP' : timestamp,
        'ACCESS-SIGN' : signature,
        'Content-Type' : 'application/json'
    }

    response = requests.post( base_url + path_url , data = body , headers = headers)
    print( response.status_code )
    print( response.json() )

orderprice = decideOrderPrice("BUY")
order("BUY",orderprice,0.01)
{'parent_order_acceptance_id': 'JRF20200114-110537-710593'}

なお、未解決なこと

特殊注文「SIMPLE」はsendchildorderと特に変わらない理解ですが、sendparentorderで「SIMPLE」をしようとすると怒られます。sendchildorderは機嫌よく通るので代用していますが。

api.sendparentorder(
    order_method = "SIMPLE",
    minute_to_expire = 10000,
    time_in_force =  "GTC",
    parameters = [
        {
            #IFD:指値で買い注文、約定したらOCO注文
            "product_code": "FX_BTC_JPY",
            "condition_type": "LIMIT",
            "side": "BUY",
            "price": 900000,
            "size": 0.1
        }]
)
{'status': -101, 'error_message': 'Invalid type of order', 'data': None}

所感

冷静になって考えてみるとAPI直打ちのサンプルBODYをラッパーにそのまま入力しているので通るわけがないです。が、同じようにsendparentorderが出せずに困っている人の助けになれば良いと思い投稿しました。

今後の展望

注文部分にRESTAPI、約定状況確認や価格取得にWEBSOCKETを使うBOTを作成しAWS上でテスト中です。
買いと売りのエントリをどちらにするかは乱数で決めています。
オーダの仕方と継続的にオーダを出し続ける方法が、わかってきたのでこれからはエントリを決めるロジックを考えることに集中します。
またsendparentorderを出せるようになったのでRESTAPIの数を減らせると思っています。RESTAPIが遅いのがボトルネックに感じていますので。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

pythonのスクレイピング(requests)でエラーが出た時

[Errno 11001]について

社内でpythonを使ってスクレイピングしようとしたとき、[Errno 11001]が出た。

プロキシ指定を行うと解決。

import requests
from bs4 import BeautifulSoup

proxies = {
'http': 'http://123.45.678.000:0000',
'https': 'http://123.45.678.000:0000',
}

url = 'https://ja.wikipedia.org/wiki/Python'
res = requests.get(url, verify=False, proxies=proxies)
soup = BeautifulSoup(res.text)

soup
<!DOCTYPE html>
<html class="client-nojs" dir="ltr" lang="ja">
<head>
<meta charset="utf-8"/>
<title>Python - Wikipedia</title>
<script>document.documentElement.className="client-js";RLCONF={"wgBreakFrames":!1,"wgSeparatorTransformTable":["",""],"wgDigitTransformTable":["",""],"wgDefaultDateFormat":"ja","wgMonthNames":["","1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月"],"wgMonthNamesShort":["","1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月"],"wgRequestId":"XhwEyQpAICIAABeLnhMAAADL","wgCSPNonce":!1,"wgCanonicalNamespace":"","wgCanonicalSpecialPageName":!1,"wgNamespaceNumber":0,"wgPageName":"Python","wgTitle":"Python","wgCurRevisionId":75653560,"wgRevisionId":75653560,"wgArticleId":993,"wgIsArticle":!0,"wgIsRedirect":!1,"wgAction":"view","wgUserName":null,"wgUserGroups":["*"],"wgCategories":["無効な出典が含まれている記事/2018年","オブジェクト指向言語","スクリプト言語","オープンソースソフトウェア","Python","基本情報技術者試験"],"wgPageContentLanguage":"ja","wgPageContentModel":

・・・
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

pandas.DataFrame内を変数で検索し、該当する行を取得する。

pandas.DataFrameから、該当する文字列がある行を取得する方法として、query()メソッドがある。

df.query('name == "Suzuki"')

上記のnameに対象の列名、そして==の後に""で囲った検索する文字列を入れればOK。

ただ、for in で検索する文字列をループさせる、という処理をしたかったので、変数を入れる必要があった。

当然、

search_name = 'Suzuki'
df.query('name == search_name')

をやったらエラーになる。

調べると、query()で変数を使うには、@をつければいいらしいので、

search_name = 'Suzuki'
df.query('name == @search_name')

これでできました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AtCoder Beginner Contest 125 過去問復習

所要時間

スクリーンショット 2020-01-14 16.40.05.png

感想

数日前に解いた過去問
流石に典型なので解けなくてだいぶ萎えた

A問題

何回ビスケットの製造が行われるかを考えれば容易い。

answerA.py
a,b,t=map(int,input().split())
print(b*(t//a))

B問題

インデックスの小さい方から順にアクセスしていって0と比較して大きい方をカウントしていけば良い。

answerB.py
n=int(input())
v=input().split()
c=input().split()
co=0
for i in range(n):
    co+=max(0,int(v[i])-int(c[i]))
print(co)

C問題

四問時代にC問題の方がD問題よりも難しいの地雷すぎますね。まあ、典型問題と言われればそれはそうなんですが…。
一つの整数を選んで好きな数へと書き換えることができるので、残りのN-1の整数の最大公約数を求めてその最大値を考えれば良いことがわかります。しかし、純粋に実装した場合、O($N^2 \times log(maxA)$)になるので制約を余裕で超えてしまうことになります。ここで、なぜか(頭がバグっていたとしか思えない)方針もなく実験をしたり適当なアルゴリズム(二分探索など)を使おうとしていました。
このできない理由を考察する中で以下の重要な発見をしました。(基本中の基本…)

計算量を落とすためにアルゴリズムはある!

つまり、この問題で初めに考察すべきは計算量が多くなる原因であり、gcdの計算において複数回計算している部分があることに気づくのは容易だと思います。例えば、k個目の整数を選ぶ場合とk+1個目の整数を選ぶ場合で比較すると、それぞれgcdはgcd(gcd($A_1$~$A_{k-1}$),gcd($A_{k+1}$~$A_{N}$))とgcd(gcd($A_1$~$A_{k}$),gcd($A_{k+2}$~$A_{N}$))になるので、$A_1$~$A_{k-1}$と$A_{k+2}$~$A_{N}$の部分が共通してることがわかります。同様にして$A_1$~$A_{N}$をそれぞれ選んでいくことを考えると、左右からそれぞれ累積gcdを考えれば複数回計算することが必要なくなり、O($N \times log(maxA)$)までオーダーを落として計算することができます。
以上より、メモのフェーズと計算のフェーズを分けて素直に実装することで簡単にプログラムを書くことができました。(変に複雑に考える癖があるので直していきたいです。)

answerC.py
from fractions import gcd

n=int(input())
a=[int(i) for i in input().split()]
a1=[a[0]]
for i in range(1,n-1):
    a1.append(gcd(a1[-1],a[i]))
a2=[a[-1]]
for i in range(n-2,0,-1):
    a2.append(gcd(a2[-1],a[i]))
m=[]
for i in range(n-2):
    m.append(gcd(a1[i],a2[-i-2]))
m.append(a1[-1])
m.append(a2[-1])
print(max(m))

D問題

まずはこれも実験をします。この問題では合計を最大にしたいので、出来るだけ整数列の中に正の数を増やしたいという発想が生まれます。
ここで、実験を繰り返すとほとんどの要素は正にできることがわかると思います。さらに、ほとんどの要素が正になることから、この問題で定義される操作により任意のi,jを選んで$A_i$と$A_j$に-1をかける操作を作り出せるのではと考えました。
この仮説は正しく、$A_i$と$A_{i+1}$、$A_{i+1}$と$A_{i+2}$、…、$A_{j-2}$と$A_{j-1}$、$A_{j-1}$と$A_{j}$の順でこの問題で定義される操作を行うことで実現することができます。
以上のことが示せれば、偶数個の負の要素のみ正にできることから、負の要素が偶数個ある時は全てを正にできるので(全ての数の絶対値の)SUMを求め、負の要素が奇数個ある時は絶対値の最小の要素をのぞいたSUMを求めれば良いことがわかります。

answerD.py
n=int(input())
a=[int(i) for i in input().split()]
a.sort()
j=n
for i in range(n):
    if a[i]>=0:
        j=i
        break
a=list(map(abs,a))
a.sort()
if j%2==0:
    print(sum(a))
else:
    print(sum(a)-2*a[0])
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Pandasでcsvの複数カラムの変更方法(Unixtime -> Japan Time)

import glob
import os
import pandas as pd
import pandas as pd
from datetime import datetime
from pytz import timezone


# CSVデータを変えるスクリプト (カラム 'starttime', 'endtime' をUnixtimeから日本時刻に変更)

# プログラム実行場所の直下に目的のcsvファイルがある
csv_list = os.listdir()

# !!適時 .remove()などでcsvじゃないファイルを抜いてあげてください。
# !!今時間がないのですが、修正してくれる人いたら大歓迎です!
# (↑csv以外をlistから削除 or listに入れない部分)



tz = timezone('Asia/Tokyo')


for csv_file in csv_list:



    japan_starttime_list = []
    japan_endtime_list = []


    print(csv_file, "を読み込む...")

    df = pd.read_csv(csv_file)

    starttime_series = df.starttime
    endtime_series = df.endtime


    for starttime in starttime_series:
        starttime_str = str(starttime)[:10]
        starttime_int = int(starttime_str)

        # Unixtime -> Japan time(str) -> listに格納
        utc_time = datetime.fromtimestamp(starttime_int)
        japan_time = utc_time.astimezone(tz)
        japan_time_str = japan_time.strftime('%H:%M:%S')
        japan_starttime_list.append(japan_time_str)


    # endtimeでも同じことを
    for endtime in endtime_series:
        endtime_str = str(endtime)[:10]
        endtime_int = int(endtime_str)

        # Unixtime -> Japan time(str) -> listに格納
        utc_time = datetime.fromtimestamp(endtime_int)
        japan_time = utc_time.astimezone(tz)
        japan_time_str = japan_time.strftime('%H:%M:%S')
        japan_endtime_list.append(japan_time_str)


    # dataframe の2つのカラムのデータ変更
    df.starttime = japan_starttime_list
    df.endtime = japan_endtime_list


    # 新たな csv を作成
    df.to_csv('rev_' + csv_file)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

たぶんニシキヘビの中にいる (原題: Maybe in Python)

(翻訳記事じゃ) ないです。

Maybe はいいですよね。 値がある、という状態と値がない、という状態を非常に扱いやすくしてくれます。 リリンが生み出した文化の極みです。

なので、Python で似たようなことができないかと思い実装してみました。

本体はこちらにあげてあります。 ぜひ遊んでみてマサカリを投げるといいと思います。

仕様

一部は “null安全でない言語は、もはやレガシー言語だ” を露骨に意識した構成になっておりますので、合わせてお読みいただくと良いと思います。

基本

この Maybe は抽象クラスMaybeを継承した具象クラスSomethingNothingとして実装されています。 Somethingは値があることを示し、Nothingは値が欠落していることを示します。

just関数はSomethingのインスタンスを作成します。 nothingNothingの (唯一の) インスタンスです。

from maybe import just, nothing

a = just(42)
b = nothing

print(a)
print(b)
Just 42
Nothing

Maybe はプロパティhas_valueを持ち、SomethingTrueを、NothingFalseを返します。 またSomethingはそれ自体 Truthy であり、Nothingはそれ自体 Falsy です。

print(a.has_value)
print(b.has_value)

if a:
    print('a is Truthy')
else:
    print('a is Falsy')

print('-----')

if b:
    print('b is Truthy')
else:
    print('b is Falsy')
True
False
-----
a is Truthy
b is Falsy

Maybe は値を入れる箱と考えることが出来ます。 Maybe 自体も値ですから、Maybe は入れ子構造になりえます。 Maybe は “深さ” という概念をもちます。 裸の値の深さは 0 です。 Nothingの深さは 1 です。 Somethingの深さは “中身の深さ + 1” です。 深さはdep関数に渡すことで得られます。

from maybe import dep

a2 = just(a)
b2 = just(b)
print(a2)
print(b2)

print('-----')

print(dep(42))
print(dep(a))
print(dep(b))
print(dep(a2))
print(dep(b2))
Just Just 42
Just Nothing
-----
0
1
1
2
2

Maybe は等値比較ができます。 Somethingは互いがSomethingであり、その中身が等値であれば等しいとします。 入れ子構造ならば再帰しながら掘っていくことになります。 Nothingは互いがNothingであれば等しいとします。

print(a == 42)
print(a == just(42))
print(a == a2)
print(a == b)
print(b == nothing)
print(b == b2)
print(b2 == just(nothing))
False
True
False
False
True
False
True

Maybe バインディング (Null チェックに伴うキャスト)

Maybe[T]Tとして使うには、

  1. その Maybe が値を持つことを確認し、
  2. Maybe[T]Tに変換しなければいけません。

if 文と後述する強制アンラップ演算子を使用することで以下のように使うことは可能です。

m = just(913)
if m.has_value:
    print(m.i + 555)
1488

しかし、このふたつの工程が分離していると、以下のような思わぬハプニングが発生しえます。

m = just(913)
if m.has_value:
    print(m + 555) # アンラップ忘れ、Maybe[int] と int の加算はできない
TypeError: unsupported operand type(s) for +: 'Something' and 'int'
m = nothing
print(m.i + 555) # Nullチェック忘れ、m が Nothing の場合ぬるぽが飛ぶ
maybe.NullpoError: it GOT no value.

実は Maybe はイテレーション可能で、Somethingは中身の値を一回だけ生成し、Nothingはなにも生成せずにStopIteration()例外を発生させます。 これを利用すると、for 文を使用して以下のように Null チェックとキャストを同時に行えます。

m = just(913)
for v in m:
    print(v + 555) # ここは m が Something のときだけ実行される
1468

ちょうど Swift の Optional バインディングと似た使い心地になるので、私はこれを “Maybe バインディング” と呼んでいます。 Optional バインディングとの違いは、シャドーイング (ブロック内で同じ変数名を使用すること) ができない点と、値が欠落している場合との分岐が少々煩雑になる点です。

順に見ていきましょう。 もしシャドーイングのようなことをする場合、Python の変数スコープの関係上、コメントの行でmMaybe[int]からintに変わってしまいます。

m = just(913)
print(m) # ここでは Maybe[int]
for m in m:
    print(m + 555)
print(m) # ここでは int
Just 913
1468
913

すでに Null チェックをしているのだからこのほうが便利だと思いますか? しかしそれは罠です。 もしmの値が欠落している場合Maybe[int]のままです。 あなたはこの先 “Maybe” という同じコンテキストでmを扱うことは許されず、intのように見えるmnothingである可能性に怯えながら過ごさなければならないのです。 これではなんのための Maybe かわかりません。 単にNoneを使用しているのと大差がない (どころかより面倒になっている) じゃないか。

つづいての相違点です。 値があるときだけ実行したいのではなく、値があるときとないときで処理を分岐したい場合があるでしょう。 Swift の Optional では次のように書けます。

let m: Int? = nil
if let v = m {
    print(v + 555)
} else {
    print("ひょっとして君は俺が嫌いなのかな?")
}

ifelseの対応は美しく、他の追随を許しません。 そしてこのふたつのブロックは文法上対になっています。

しかし Maybe ではこのように書かざるを得ません。

m = nothing
for v in m:
    print(v + 555)
if not m.has_value:
    print("ひょっとして君は俺が嫌いなのかな?")

forifの対応というのはおそらく生まれてこの方誰も見たことがないでしょう。 それにこのふたつのブロックは隣接して書いてあるだけで文法上の対でもありません。 さらに言えば、if not m.has_valueとは随分と長いです。 これについてはもともとNothingが Falsy ですからif not mでもよいのですが、すこし文意が読み取りづらい欠点もあります。

とはいえ、Null チェックとキャストが分離されているよりも遥かに安全であり、書きやすさもあります。 基本的には.iの使用は禁止し、Maybe バインディングを使用するのがよいでしょう。

強制アンラップ演算子 (from !, !!)

strからintへの変換を試みて、失敗したらnothingを返す関数safe_intを考えます。

def safe_int(s):
    try:
        return just(int(s))
    except ValueError:
        return nothing

ところで、この関数にとあるサイトで利用する “年齢” テキストボックスから飛んできた文字列が入るとします。 普通は HTML 側で数字しか入力できないように制限しますし、前段の JavaScript で前処理なども行うでしょう。

そういう場合、事実上この関数がnothingを返す可能性はないと言えます。 それでもMaybe[int]intとして使えないから長々と Maybe バインディングを書かなければいけないのでしょうか? それではNoneを使うほうがマシということになります。

そういうときにこそ強制アンラップ演算子.iを使用すればよいのです。 ちなみに演算子とか言っていますが実態はプロパティです。

s = get_textbox('年齢')
age = safe_int(s).i # .i によって age には整数が代入される
if age >= 20:
    ...

手間がなく便利に思えますが、もしnothingに対して.iを呼び出すと古き良きアレが元ネタのNullpoErrorが飛びます。 繰り返しになりますが、強制アンラップ演算子はあくまでも “危険な (Unsafe)” 操作であると考え、ロジック上絶対にnothingになりえない箇所でのみ使用するべきです。

Null 条件演算子 (from ?., ?->)

ここにMaybe[list]があるとします。 あなたはこの中にいくつ0があるか調べたいと思いました。 Maybe ということはnothingな可能性もあると思いますが、大抵の場合リスト自体が存在しないとしたら、期待するのはやはりnothingでしょう(いやいや0が欲しいって人もいる? そういう方は次の “Null 癒合演算子” も見てください)。

愚直に書けば次のようになります。

l = get_foobar_list('hogehoge')

for l0 in l:
    num_0 = just(l0.count(0))
if not l:
    num_0 = nothing

確かに書けますが……はっきり言って煩雑です。 もしllistであればnum_0 = l.count(0)ですむ処理を、4 行もかけて書きたくありませんよね。

Null 条件演算子.q.を使えば簡潔に書けます (例によってホントはプロパティです)。

l = get_foobar_list('hogehoge')
num_0 = l.q.count(0)

また、添字に対しても同様に.q[]が利用できます。

l = get_foobar_list('hogehoge')
print(l.q[3])

# 以下の代わりに使える
# for l0 in l:
#     print(just(l0[3]))
# if not l:
#     print(nothing)

なぜこのようなことができるのでしょう? .qは、SafeNavigationOperatorという抽象クラスを継承したオブジェクトを返します。 SafeNavigationOperator__getattr____getitem__をオーバーライドしています。値をもつ場合SomethingSNOが返されます。 こちらは値の情報を保持していて、__getattr____getitem__は中身の値のそれをjustにくるんで返します。 値が欠落している場合NothingSNOが返され、__getattr____getitem__はただnothingを返します。

ここで勘のいい方は疑問に思うかもしれません。 some_list.countはメソッドであり、呼び出し可能オブジェクトです。 maybe_list.q.countは関数をさらに Maybe でくるんで返されるわけです。 それをl.q.count(0)のように扱えるということは、Maybe 自体呼び出し可能オブジェクトなの?

実はそのとおりです。 Somethingを呼び出すと中身を呼び出し、その結果をjustにくるんで返します。 Nothingを呼び出すと単にnothingを返します。 この仕様によって先に挙げたようなことが可能になっています。 この仕様はあくまでも.q.と同時に使用するためのものです。 それ以外で使用するのは、個人的には魔術的に思うので控えたほうがよいと思います (後述する Maybe 同士の四則演算などを実装していない理由に繋がります)。

ところで、?.を実装している言語の中でも、いわゆる “オプショナルチェーン” を採用している場合では Null が見つかった時点で終了してしまいます (いわば短絡評価) が、.q.の場合はnothingのまま処理を続けます。 nothingに対して.q.をしてもnothingが出るだけで、伝播した結果として得られるのが Null 相当物であることにかわりはありませんが、この違いについてはしっかり認識しておく必要があるかもしれません。

ちなみに、__setattr__はオーバーライドしていないのでfoo.q.bar = bazみたいなことはできません。 これは私の技術的な限界であり、__setattr__をオーバーライドしたときに生じる無限ループをどうしても解決できなかったためです。 助力願いてぇ……。 foo.q[bar] = bazのほうはできると思いますが対称性が崩れるのでいまのところ実装していません。

Null 癒合演算子 (from ?:, ??)

Null の場合にデフォルト値で埋めたいというのは Null を扱う上で最も多い要求でしょう。

もちろん Maybe バインディングで書くこともできますが……

l = get_foobar_list('hogehoge')
num_0 = l.q.count(0) # ちなみにここでは num_0 は Maybe[int] で……

for v in num_0:
    num_0 = v
if not num_0:
    num_0 = 0

# ここまで抜けると num_0 は int になります。

ちゃんと Null 癒合演算子1>>が用意されています。

l = get_foobar_list('hogehoge')
num_0 = l.q.count(0) >> 0 # num_0 は int

Null 癒合演算子の動作は一見シンプルに見えます (し、実際シンプルです) が、少々注意が必要です。

左辺の値が欠落しているときは話はカンタンで、右辺をそのまま返します。

一方で左辺が値を持つときは興味深い (Interesting の訳語的な意味で) 動作をします。 左辺が右辺より深いときは、左辺の中身と右辺を再度癒合します。 右辺が左辺より深いときは、左辺をjustでくるんでから再度癒合します。 同じならば左辺を返します。 ただし、右辺が裸の値だった (深さ 0 の) ときは、右辺をjustでくるんで再度癒合し、その中身を返します。

外から見ると似たような動きをするor演算子の方は左辺が Truthy ならそのまま返すのにくらべてずいぶんと複雑な動きです。 なぜこんなことをしているのかというと、再帰呼び出しの結果として必ず右辺と同じ深さの値を返すためです。

“デフォルト値を設定する” という文脈において期待するのは、値があろうとなかろうと同じように扱えることでしょう。 Maybe は入れ子可能なため、いくぶんか煩雑な動作が必須になります。 ただしそれは内部の話であり、使う側は “常に同じ構造の値が返る” というシンプルな扱いができるでしょう。

たとえば、複数の Maybe があったとき、それらの構造にとらわれず、以下のようにして “最右辺と同じ構造 (この場合裸の値) の、一番早く値が現れる Maybe” を得ることができます。

x = foo >> bar >> baz >> qux >> 999

一方、短絡評価ではないので右辺が評価されてしまう点には注意してください。

ちなみに、この演算子は右ビットシフト演算子をオーバーロードして実装されています。 そのため、演算の優先順位も同一となります。 つまり、加法演算子よりも低く、比較演算子よりも高いです2。 これは C#3、Swift4、Kotlin5 の Null 癒合演算子の優先順位に近いですが、一方で PEP 505 で提案されている None-aware 演算子の??がほとんどの二項演算子よりも高い優先順位を持つ6のとは大きく異なります。 PEP 505 が正式採用される日が来たら (来ない気もしますが) 注意してください。 また、自動的に>>=も定義されています。

a = just(42)
b = nothing

print(a >> 0 + 12) # 42 ?? 0 + 12 なら 54 となる(ハズ)

print('-----')

a >>= 999
b >>= 999

print(a)
print(b)
42
-----
42
999

余談も余談ですが、オーバーロード先として>>を選んだのは、

  1. 手書きの “?” が “>” から下に伸ばして打点するように書く人がいる (形状の類似)
  2. キーボードにおいて “?” と “>” は隣り合っている (入力操作の類似)
  3. 複数並べて書いたとき、左から順に値をチェックしていくのを視覚的にも理解しやすい (処理ロジックの可視化)
  4. 他の言語における Null 癒合演算子の優先順位に近い (混乱しにくい)

などが理由です。 下に行くほど合理的な理由に見えますが、恐ろしいことに下に行くほど後付けの理由であり、筆者が適当に Maybe を作っていたのがよくわかります。

mapメソッド (from map)

Null 条件演算子には実は弱点があります。 それは、Maybe オブジェクトにしか使用できない点です。 SafeNavigationOperatorを引数に与えることもできません。

どういうことかというと、こういうことはできません。

l = just([1, 2, 3, ])

s = .q.sum(l) # 文法エラー
s = sum(l.q) # 弾け飛ぶ例外

# どうしてくれんのこれ
# オアァァァアァァアァーーーーーー♥

Maybe バインディングで書くことはできますがやっぱり煩雑です。

l = just([1, 2, 3, ])

for l0 in l:
    s = just(sum(l0))
if not l:
    s = nothing

これを Maybe オブジェクト側から何とかするために、mapメソッドが存在しています。 mapメソッドを使用すると以下のように書けます。

l = just([1, 2, 3, ])

s = l.map(sum)

mapメソッドに渡された関数は結果をjustでくるんで返します。 値が欠落していれば当然nothingが返ります。 関数にはラムダ式も使えるので、以下のようなこともできます。

a = just(42)
print(a.map(lambda x: x / 2))
Just 21.0

mapメソッドに関しては語るべきことはあまりありません (とくにモナドをご存じの方にとっては)。

bindメソッド (from flatMap)

さっき作ったsafe_int関数を思い出してください。 strを引数にとってMaybe[int]を返すやつです。 mapを使ってこれをMaybe[str]に適用してみましょう。

s1 = just('12')
s2 = just('hogehoge')
s3 = nothing

print(s1.map(safe_int))
print(s2.map(safe_int))
print(s3.map(safe_int))
Just Just 12
Just Nothing
Nothing

safe_intが Maybe を返し、さらにmapメソッドがjustでくるみ直すので入れ子になってしまいました。 たぶんこのプログラムを書いた人はこの結末は望んでいなかったでしょう (入れ子に “できる” こと自体は表現力を高めます。 たとえば JSON のパースの結果、nullが入っていたならjust(nothing)、そもそもそのキーが存在していなかったならnothingと表現できたりするので)。

いままで秘密にしていましたがjoin関数を使うと入れ子を 1 段潰すことができます。 自然変換 μ ってやつです。

from maybe import join

print(join(s1.map(safe_int)))
print(join(s2.map(safe_int)))
print(join(s3.map(safe_int)))
Just 12
Nothing
Nothing

そして、なにより望まれているのは、“mapしてjoin” をひとつにすることです。 bindメソッドがあればそれが使えます。

print(s1.bind(safe_int))
print(s2.bind(safe_int))
print(s3.bind(safe_int))
Just 12
Nothing
Nothing

Haskell を使いこなす変態方にはmapメソッドが<$>演算子でbindメソッドが>>=演算子といえばすぐに分かるでしょう。 裸の値を返す関数にはmap、Maybe を返す関数にはbindと覚えるといいでしょう。

do関数 (from do記法)

mapbindは当然ひとつの Maybe オブジェクトから呼び出すので、2 引数以上をとる関数に使うのには少々骨が折れます。

lhs = just(6)
rhs = just(9)

ans = lhs.bind(lambda x: rhs.map(lambda y: x * y))
print(ans)
Just 54

ちなみに内側がmapなのはx * yで裸の値を返す関数のため、外側がbindなのはmapメソッドの返り値が Maybe だからといえます。 覚えていますか?

do関数を使用するとこれを簡単に書けます。

from maybe import just, nothing, do

lhs = just(6)
rhs = just(9)

ans = do(lhs, rhs)(lambda x, y: x * y)
print(ans)
Just 54

do関数は引数がすべて値を持つときだけ中身を取り出して関数を実行します。 ひとつでも値が欠落しているときはnothingを返します。 doという名前の由来は Haskell ですが、アルゴリズムは極めて反モナド的です。 許し亭ゆるして。

ちなみにdo関数の引数は Maybe のみしか取ることができません。 一部の引数として裸の値を使用したいときはjustでくるみましょう。

そのほかこまごましたこと

コンテナとしての Maybe

Maybe はコンテナでもあり、len関数とin演算子が使用できます。

len関数はSomethingは常に1を、Nothingは常に0を返します。

a = just(42)
b = nothing
a2 = just(a)
b2 = just(b)

print(len(a))
print(len(b))
print(len(a2))
print(len(b2))
1
0
1
1

in演算子は、右辺の中身が左辺に等しいとき、Trueを返します。 それ以外のときはFalseを返します。 Nothingは常にFalseを返します。

print(42 in a)
print(42 in b)
print(42 in a2)
print(42 in b2)
True
False
False
False

さきほど述べたように、Maybe はイテレート可能です。 そのため、たとえばリストへの変換が可能です。 @koher 氏は以下のように述べましたが、まさにその通りの結果になります。

Optional は中身が空かもしれない箱でした。別の見方をすれば、 Optional を最大で一つしか要素を入れられない Array と考えることができます。

   — SwiftのOptional型を極める

al = list(a)
bl = list(b)

print(al)
print(bl)
[42]
[]

この辺の仕様を使う機会があるのかは謎です。

ネイティブの世界とつながる

Maybe には一対になった関数とメソッド、maybe_fromto_nativeがあります。 関数とメソッドですが一対です。 これらは Python のネイティブな型と Maybe をつなげる働きをします。

maybe_fromは、ネイティブな値をとって Maybe を返します。 justとの違いは、Noneが与えられたときにjust(None)ではなくnothingが返ること、Maybe が与えられたときにそのまま返すことです。 この関数は返り値の欠損をNoneで示す既存の関数やメソッドの返り値を、まとめて Maybe という共通のコンテキストで利用するために使用できます。

# 辞書になかったとき価格の代わりに None を返す既存の関数
def get_price(d, name, num, tax_rate=0.1):
    if name in d:
        return -(-d[name] * num * (1 + tax_rate)) // 1
    else:
        return None

# ...

from maybe import maybe_from
p = get_price(unit_prices, 'apple', 5) # int が来るかもしれないし None が来るかもしれない
p = maybe_from(p) # p は unit_prices に 'apple' があるかどうかに関わらず Maybe[int]

to_nativeメソッドは逆に、Maybe を ネイティブな値にします (ただし、ネイティブなといっても中身がそもそも何らかのライブラリで用意されているオブジェクトの場合は単にそれが出ます)。 .iと違い、nothingならNoneを返します (だからといって “安全な” アンラップ演算子として濫用してはいけません) し、ネストしていても再帰的に掘っていき必ず裸の値を返します。 mapは引数が欠落しているとき全体を欠落とみなしますが、こちらは値がないという意味としてNoneを与えることを期待する関数の引数に使用できます。 また、ネイティブな値が返ることを利用して JSON のシリアライズ方式として指定できます (ただしネイティブなといっても略)。

import json

from maybe import Maybe

def json_serial(obj):
    if isinstance(obj, Maybe):
        return obj.to_native()
    raise TypeError ("Type {type(obj)} not serializable".format(type(obj)))

item = {
    'a': just(42),
    'b': nothing
}

jsonstr = json.dumps(item, default=json_serial)
print(jsonstr)
{"a": 42, "b": null}

PyMaybe との違いとか “ただ面倒なだけじゃん?” に対する応答とか

Python で Maybe というと、既存のライブラリとして “PyMaybe” というものがあります。 PEP 505 でも言及されている有名所さんのようです。

こちらの大きな特徴は、Maybe をまるで裸の値のように取り回せるというところです。 具体的には README.rst にある以下のコードが参考になるでしょう。 maybeはこちらでいうところのmaybe_fromor_elseはこちらでいうところの>>演算子にあたると思ってください。

>>> maybe('VALUE').lower()
'value'

>>> maybe(None).invalid().method().or_else('unknwon')
'unknwon'

Maybe でくるんである値に対して、裸の値に対するメソッドをコールしています。 また Null に対するメソッドコールは無視されて Null が伝播しているのがわかります。 私の Maybe のように一旦取り出してとか、.q.とかする必要がありません。 四則演算などの Dunder メソッドも実装されているようです。

なんだ、こちらのほうが便利じゃないか。 余計なエラーも出ないし……とお思いの方も多いと思います。

しかし、私がそのような仕様で Maybe を実装したのには理由があります。 それは、まさに “エラーを発生させるため” なのです。

Maybe[T]Tの違いを正しく意識しないとすぐエラーが出るというのは、逆に言えばその違いを意識する手助けになります。 混同するとすぐエラーが出るし、文字列に変換すると余計なJustという接頭辞がつきます。 それは、“正しく書かないと正しく動かない” ということを保証するためのものです。 間違えていたらすぐ気づくのです (参照: マジレスすると『Optional(2018)年』を恐れる必要はない)。 これが Maybe を直接呼び出してほしくない理由でもあります (違いを隠蔽してしまうので。 ホントは文字列変換も消してしまいたいけど……)。

Null 安全とは、ぬるぽを起こさない仕組みであると一般的には理解されますし、究極的な目的がそこであることは間違いないでしょう (と、いきなり Null 安全について語り始めましたが、私の Maybe の型ヒントはダメダメなので Null 安全のために利用することは事実上不可能です)。 しかし、本当に Null 安全に必要なものは “Nullable と Non-null が区別されること” です。 なぜ Java がだめだったのでしょう? それはあらゆる参照型にnullを代入でき、逆の立場から言えばどんな参照型の値もnullである可能性があることでした。 Tが Null かもしれないということは、つまりTと書いているのにTとして使えないことを意味します。 すべきです。 そこへきて Nullable と Non-null の垣根をさらに取り払うことは—TでないものをTのように扱えるようにするということは—むしろ、危険なことなのです。

Objective-Cには、 nil に対するメソッドコールはエラーにならずに無視されるという悪しき仕様があります。

(中略)

しかし、この便利さと引き換えにObjective-Cは恐ろしいリスクも背負っています。本来、 nil のチェックをしなければならない箇所で nil チェックを忘れてしまったけれども、たまたまうまく動いてしまうことがあります。そしてある日、特定の条件が重なったり、ちょっとした修正を行ったときにそのバグが顕在化するのです。

   — 今すぐObjective-CをやめてSwiftを使おう

Objective-C の nil の扱いは一見安全なように見えて、問題の発覚を遅らせ、解決をより難しくしているだけです。潜在バグを生みやすいという点で、僕は Objective-C 方式は最悪だと考えています。かつて、 Objective-C で書いたプログラムを Java に移植したときに、 Java で NullPointerException が発生して初めて Objective-C 側の nil ハンドリングが不適切だったことに気付くということが何度もありました。

   — null安全でない言語は、もはやレガシー言語だ

もちろん、それが Null 安全のために生産性を落とさなければいけないというわけではありません。 Nullable な変数に対する Null チェックは “かつての言語だって” 必要なことでしたし7、この Maybe ではむしろそれを簡略化するための様々な方法を実装しました。 もし Null チェックなしにぶっつけ本番したければ、そのための Unsafe な操作さえあります。 この辺は数年前に語り尽くされた話ではありますが。

とはいえ、Python を好んで使用する方はそういったガバガバ性は百も承知でとにかく “書きたいことを素直に書ける” ことを重んじているとは私も思います。 ですので、PyMaybe の思想が間違っているとは口が裂けても言えませんっつーか私こそ世間に反逆するMaybe[T]erroristであると言えます。 ですが、まあ半分お遊びというか、自分が書きたいものを書ききったということでどうかお目こぼしください。


  1. 一般的に Null Coalescing Operator の定訳は “Null 合体演算子” ですが、“合体” だと “欠けた部分を埋める” というニュアンスに乏しいので、“傷が治る” という意味の “癒合” を使用しています。 

  2. https://docs.python.org/ja/3/reference/expressions.html#operator-precedence 

  3. https://docs.microsoft.com/ja-jp/dotnet/csharp/language-reference/operators/ 

  4. https://developer.apple.com/documentation/swift/swift_standard_library/operator_declarations 

  5. https://kotlinlang.org/docs/reference/grammar.html#expressions 

  6. https://www.python.org/dev/peps/pep-0505/#the-coalesce-rule 

  7. こんな一個人製の Maybe では非現実的な話ですが、もしもnothingのみを使い、組み込みのNoneを使用しないように (発生しうるときはすぐにmaybe_fromでくるむように) できれば、裸の値には一切 Null チェックをする必要がなくなります。 実はこれが Null 安全な言語の最も大きな利点で、言語仕様にこのような仕組みが組み込まれている Null 安全な言語は “必要な Null チェック” さえすればよくなります。 一般的な “Null 安全な言語では Null チェックを強制される” という認識は正しくはあるのですが、感覚としては真逆です。 参考: 【アンチパターン】全部nil(null)かもしれない症候群 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Pythonの勉強

python

勉強中なので学んだことをまとめていく。随時追加。

リスト

辞書を宣言
ListName = [Value1,Value2, ・・・]
0番目の値を出力。[-1]で一番後ろの値、[-2]で後ろから2番目の値。
print(DictioaryName[0])
リストの末尾に要素を追加
team.append("いぬ")
リストの要素を上書き
team[0] = ("ねこ")
リストの要素を削除
team.pop(0)

辞書

辞書を宣言
DictioaryName = {Key:Value, ・・・}
出力
print(DictionaryName)
Keyに対応するValueを出力
print(DictioaryName[Key])

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

複数の種類の例外を捕捉する

1
import sys, traceback

number1 = 100
number2 = 'a'

print('strat')

try:
    answer = number1 / number2
    print(answer)
except ZeroDivisionError as e:
    print('0では割れません。')
    sys.stderr.write(traceback.format_exc())
except NameError as e:
    print('未定義の変数を呼び出しています')
    sys.stderr.write(traceback.format_exc())
except Exception as e:
    print('予期せぬエラーが発生しました。')
    sys.stderr.write(traceback.format_exc())
finally:
    print('end')
1の実行結果
strat
予期せぬエラーが発生しました。
end
1の実行時エラー
Traceback (most recent call last):
  File "Main.py", line 9, in <module>
    answer = number1 / number2
TypeError: unsupported operand type(s) for /: 'int' and 'str'

number2 = 'a'となっているので、
TypeErrorが発生している。

TypeErrorは
except ZeroDivisionError、
except NameErrorでは捕捉出来ないが、
except Exceptionで捕捉できる。

Exceptionは ZeroDivisionErrorやNameErrorのスーパークラスである。
逆を言うと、
ZeroDivisionErrorやNameErrorのサブクラスである。

Exceptionは全ての例外を捕捉できるが、、
サブクラスの方がより具体的な例外になっている。

複数の例外を捕捉する場合、
上から順番に該当するexceptブロックがないかチェックされる。
その為、
より具体的なサブクラスから記述する必要がある。

さもないと、
全ての例外を捕捉するexcept Exceptionブロックが、
より具体的に例外を捕捉できるサブクラスが動く前に、
例外を捕捉してしまう。

2
import sys,traceback

number1 = 100
number2 = 0

print('start')

try:
    answer = number1 / number2
    print(answer)
except Exception as e:
    print('予期せぬ例外が発生しました。')
    sys.stderr.write(traceback.format_exc())
except ZeroDivisionError as e:
    print('0では割れません。')
    sys.stderr.write(traceback.format_exc())
except NameError as e:
    print('定義されていない変数を呼び出しました。')
    sys.stderr.write(traceback.format_exc())
finally:
    print('end')
2の実行結果
strat
予期せぬエラーが発生しました。
end
2の実行時エラー
Traceback (most recent call last):
  File "Main.py", line 9, in <module>
    answer = number1 / number2
ZeroDivisionError: division by zero

「0では割れません。」と出力させたかったが、
スーパークラスであるExceptionがどんな例外でも捕捉しまうので、
except Exceptionブロックが例外を捕捉してしまった。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

約60行でブロックチェーンを実装

はじめに

Udemyの講座でざっくりブロックチェーンの中身を理解したので実装してみました。講座の内容を基に自力で実装しています。
本講座ではシリコンバレーの現役エンジニアである酒井潤さんが教鞭をとっており、受講することで以下のようなプログラムであれば何も見ずとも作れるようになります。本投稿では実装していないwalletやproof_of_workの実装も行われていますので、興味を持たれた方は是非受講してみてください。同じく酒井潤さんのPython基礎講座もおすすめです。

ブロックチェーンの要素

各ブロック内の要素は以下のようになっています。

  • ブロック
    • previous_hash:前のブロックのハッシュ値
    • nonce:マイニングによって見つける値(未実装)
    • transaction:ブロック内で処理したトランザクション
    • timestamp:ブロックを作成した時間

チェーン上の全てのブロックはprevious_hashでつながっているため、チェーンの途中のブロックを改ざんしたい場合には、それ以降のすべてのハッシュ値を再計算しなければならず改ざんの防止につながります。

コード

nonceは雰囲気のために一応ついていますが、最小限の要素のみの構成としたかったため今回は機能していません。実際はhash値の先頭*文字(任意の文字数)が0になるようにnonceを計算します。

import hashlib
import time
import json


class BlockChain(object):
    def __init__(self):
        self.chain = []
        self.transaction_pool = []
        self.create_block(previous_hash='Initialize')

    def hash(self, block):
        json_block = json.dumps(block)
        return hashlib.sha256(json_block.encode()).hexdigest()

    def create_block(self, previous_hash=None, nonce=0, transaction=None, timestamp=time.time()):
        block = {
            'previous_hash': previous_hash,
            'nonce': nonce,
            'transaction': transaction,
            'timestamp': timestamp
          }
        self.add_block_to_chain(block)

    def add_block_to_chain(self, block):
        self.chain.append(block)

    def mining(self):
        previous_hash = self.hash(self.chain[-1])
        nonce = 0
        transaction = self.transaction_pool
        self.transaction_pool = []
        timestamp = time.time()
        self.create_block(previous_hash, nonce, transaction, timestamp)

    def add_transaction(self, sender_name, reciever_name, value):
        transaction = {
            'Sender_name': sender_name,
            'Reciever_name': reciever_name,
            'Value': value
        }
        self.transaction_pool.append(transaction)

    def print_chain(self):
        for chain_index, block in enumerate(self.chain):
            print(f'{"="*40}Block {chain_index:3}')
            if block['transaction'] is None:
                print('Initialize')
                continue
            else:
                for key, value in block.items():
                    if key == 'transaction':
                        for transaction in value:
                            print(f'{"transaction":15}:')
                            for kk, vv in transaction.items():
                                print(f'\t{kk:15}:{vv}')
                    else:
                        print(f'{key:15}:{value}')

if __name__ == '__main__':
    # print('='*30 + 'Start' + '='*30)
    blockchain = BlockChain()
    blockchain.add_transaction(sender_name='Alice', reciever_name='Bob', value=100)
    blockchain.add_transaction(sender_name='Alice', reciever_name='Chris', value=1)
    blockchain.mining()
    blockchain.add_transaction(sender_name='Bob', reciever_name='Dave', value=100)
    blockchain.mining()
    blockchain.print_chain()

実行結果

========================================Block   0
Initialize
========================================Block   1
previous_hash  :54c72dc7390c09a6d2c00037c381057a7bd069e8d9c427585ce31bed16dfd0d8
nonce          :0
transaction    :
        Sender_name    :Alice
        Reciever_name  :Bob
        Value          :100
transaction    :
        Sender_name    :Alice
        Reciever_name  :Chris
        Value          :1
timestamp      :1578989130.9111161
========================================Block   2
previous_hash  :d4bb210d2e3ad304db53756e87a7513b2fca8672c6e757bef6db3fff8ff26bb1
nonce          :0
transaction    :
        Sender_name    :Bob
        Reciever_name  :Dave
        Value          :100
timestamp      :1578989132.9128711

最後に

少し前に流行って以降あまり名前を聞かなくなったブロックチェーンですが、ソニーが著作権の管理に使用していたりマイクロソフトが個人IDに使用していたりと活用事例はまだまだ増えていますので勉強しておいて損はないかと思います。
今後はdjangoと連携させたものを実装する予定です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AtCoder Beginner Contest 055 過去問復習

所要時間

スクリーンショット 2020-01-14 15.04.06.png

A問題

普通に計算、割引きは別で考える

answerA.py
n=int(input())
print(n*800-(n//15)*200)

B問題

i回目のトレーニングでi倍を素直に書く、modで割った余りを考えることを忘れずに

answerB.py
power=1
n=int(input())
mod=10**9+7
for i in range(1,n+1):
    power*=i
    power%=mod
print(power)

C問題

S→cにすることはできないので、Sを使って作れるだけSccを作っていく。
もし、Sを使い切ってもcが残っていたらcだけでSを作れば良い。

answerC.py
n,m=map(int,input().split())
if 2*n>=m:
    print(m//2)
else:
    x=m-2*n
    print(n+x//4)

D問題

見た目が難しいので億劫になりながら解いていました。
とりあえず、このような問題はまずは実験をして挙動を確認することが大切です。
実験をする前に両隣が同じが違うのかなので順に決めていけば決まるのかもという発想を持ちつつ実験をしてみます。例えば以下のように回答がooとなっている時を考えます。

o o
S S S S
W S W W
W W S W
S W W S

このパターンを考えた時、二つの回答とその二人がわかる時、その両隣りがわかるということがわかります。
したがって、これを同様にして続けて行けばn個の回答とそのn人がわかるのではないかと考えました。また、この際、両端を順に考えていくのは面倒なので片側を順に考えていき、考えていってできたn人に対しその両端についても条件を満たせているかを確認しようという方針に至りました。
片側を順に考えていく際には、今注目している人が羊か狼か、その注目している人の回答はoとxのどちらなのか、注目してる人の手前にいる(番号が一つ小さい)人が羊か狼かにより8通り存在します。しかし、この8通りを正確に書くのが面倒で複数回ミス(インデックスのミスと"W"と"S"を反対にするミス)をしてしまいました。これらは起こりうるミスだと思うので、見直しを頑張っていくしかないのかなと思います。もちろんコードをわかりやすく書くのも大切だと思います。
また、以下の二つのコードはどちらも長くなってはいますが、ここで書いたことを3番目の動物から(1,2番目は決まっているので)順に決めていくことで愚直に実装しています。(一つ目のコードは見にくすぎるので二つ目のコードを見ることをオススメします。)

answerD.py
n=int(input())
s=input()
x=["SS","SW","WS","WW"]

def check0(i):
    if x[i][0]=="S":
        if s[0]=="o":
            if x[i][-1]=="S":
                return x[i][1]=="S"
            else:
                return x[i][1]=="W"
        else:
            if x[i][-1]=="S":
                return x[i][1]=="W"
            else:
                return x[i][1]=="S"
    else:
        if s[0]=="o":
            if x[i][-1]=="S":
                return x[i][1]=="W"
            else:
                return x[i][1]=="S"
        else:
            if x[i][-1]=="S":
                return x[i][1]=="S"
            else:
                return x[i][1]=="W"
def check1(i):
    if x[i][n-1]=="S":
        if s[n-1]=="o":
            if x[i][n-2]=="S":
                return x[i][0]=="S"
            else:
                return x[i][0]=="W"
        else:
            if x[i][n-2]=="S":
                return x[i][0]=="W"
            else:
                return x[i][0]=="S"
    else:
        if s[n-1]=="o":
            if x[i][n-2]=="S":
                return x[i][0]=="W"
            else:
                return x[i][0]=="S"
        else:
            if x[i][n-2]=="S":
                return x[i][0]=="S"
            else:
                return x[i][0]=="W"


for i in range(4):
    for j in range(n-2):
        if x[i][j+1]=="S":
            if s[j+1]=="o":
                if x[i][j]=="S":
                    x[i]+="S"
                else:
                    x[i]+="W"
            else:
                if x[i][j]=="S":
                    x[i]+="W"
                else:
                    x[i]+="S"
        else:
            if s[j+1]=="o":
                if x[i][j]=="S":
                    x[i]+="W"
                else:
                    x[i]+="S"
            else:
                if x[i][j]=="S":
                    x[i]+="S"
                else:
                    x[i]+="W"
    #print(x[i])
    if check0(i) and check1(i):
        print(x[i])
        break
else:
    print(-1)

冗長な部分を短くしたコード

answerD_better.py
n=int(input())
s=input()
x=["SS","SW","WS","WW"]

def check(i,j):
    if x[i][j]=="S":
        if s[j]=="o":
            if x[i][j-1]=="S":
                return x[i][j+1]=="S"
            else:
                return x[i][j+1]=="W"
        else:
            if x[i][j-1]=="S":
                return x[i][j+1]=="W"
            else:
                return x[i][j+1]=="S"
    else:
        if s[j]=="o":
            if x[i][j-1]=="S":
                return x[i][j+1]=="W"
            else:
                return x[i][j+1]=="S"
        else:
            if x[i][j-1]=="S":
                return x[i][j+1]=="S"
            else:
                return x[i][j+1]=="W"

a=["S","W"]
for i in range(4):
    for j in range(n-2):
        if x[i][j+1]=="S":
            if s[j+1]=="o":
                x[i]+=a[x[i][j]!="S"]
            else:
                x[i]+=a[x[i][j]=="S"]
        else:
            if s[j+1]=="o":
                x[i]+=a[x[i][j]=="S"]
            else:
                x[i]+=a[x[i][j]!="S"]
    if check(i,0) and check(i,-1):
        print(x[i])
        break
else:
    print(-1)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[GCP]Cloud ShellでDataFlowをデプロイするまでの手順(Python利用)

はじめに

DataFlowのデプロイ方法がパッと見つからなかったため、また単純に動かせるプログラムが見当たらなかったため、備忘録としてまとめました。

手順

1. apache_beamインストール

Cloud Shellで以下のコマンドを実行。

sudo pip3 install apache_beam[gcp]

以下のインストール方法ではbeam.io.ReadFromTextでエラーがでるのでダメです。

sudo pip install apache_beam

2. プログラム作成

今回は以下のような単純なものを作成しました。
指定したバケットの直下にあるread.txtというファイルを読み込んで、write.txtというファイルに出力するだけです。

実際に試したい方はPROJECTID, JOB_NAME, BUCKET_NAME に適当な内容を入力してください。

gcs_readwrite.py
# coding:utf-8
import apache_beam as beam

# ジョブ名、プロジェクトID、バケット名を指定
PROJECTID = '<PROJECTID>'
JOB_NAME = '<JOB_NAME>'  # DataFlowのジョブ名を入力
BUCKET_NAME = '<BUCKET_NAME>'

# ジョブ名、プロジェクトID、一時ファイルの置き場を設定
options = beam.options.pipeline_options.PipelineOptions()
gcloud_options = options.view_as(
    beam.options.pipeline_options.GoogleCloudOptions)
gcloud_options.job_name = JOB_NAME
gcloud_options.project = PROJECTID
gcloud_options.staging_location = 'gs://{}/staging'.format(BUCKET_NAME)
gcloud_options.temp_location = 'gs://{}/tmp'.format(BUCKET_NAME)

# Workerの最大数や、マシンタイプ等を指定
worker_options = options.view_as(beam.options.pipeline_options.WorkerOptions)
# worker_options.disk_size_gb = 100
# worker_options.max_num_workers = 2
# worker_options.num_workers = 2
# worker_options.machine_type = 'n1-standard-8'
# worker_options.zone = 'asia-northeast1-a'

# 実行環境の切替
# options.view_as(beam.options.pipeline_options.StandardOptions).runner = 'DirectRunner'  # ローカルマシンで実行
options.view_as(beam.options.pipeline_options.StandardOptions).runner = 'DataflowRunner'  # Dataflow上で実行

# パイプライン
p = beam.Pipeline(options=options)

(p | 'read' >> beam.io.ReadFromText('gs://{}/read.txt'.format(BUCKET_NAME))
    | 'write' >> beam.io.WriteToText('gs://{}/write.txt'.format(BUCKET_NAME))
 )
p.run().wait_until_finish()

3. GCSの準備

  1. 上記プログラム内のBUCKET_NAMEで指定したバケット名を作成してください
  2. 作成したバケット直下にstaging, tmp というフォルダを作成してください
  3. ローカルでread.txtというファイルを作成してください。中身はなんでも良いです
  4. 作成したバケット直下にread.txtをアップロードしてください

4. ローカルで実行

まず上記プログラムの「実行環境の切替」で、以下のようにコメントアウトを切り替えます。

options.view_as(beam.options.pipeline_options.StandardOptions).runner = 'DirectRunner'  # ローカルマシンで実行
# options.view_as(beam.options.pipeline_options.StandardOptions).runner = 'DataflowRunner'  # Dataflow上で実行

次に以下のコマンドを実行します。

python gcs_readwrite.py

これでバケット内にwrite.txt-00000-of-00001というファイルが作成されます。

5. デプロイ

まず上記プログラムの「実行環境の切替」で、以下のようにコメントアウトを切り替えます。

# options.view_as(beam.options.pipeline_options.StandardOptions).runner = 'DirectRunner'  # ローカルマシンで実行
options.view_as(beam.options.pipeline_options.StandardOptions).runner = 'DataflowRunner'  # Dataflow上で実行

次に以下のコマンドを実行します。

python gcs_readwrite.py

これでバケット内にwrite.txt-00000-of-00001というファイルが作成されます。
DataFlowのGUIで作成したジョブを選択すると、readwriteが「完了しました」になっていることがわかります。

image.png

参考

Python を使用したクイックスタート
https://cloud.google.com/dataflow/docs/quickstarts/quickstart-python?hl=ja

パイプラインの実行パラメータを指定する
https://cloud.google.com/dataflow/docs/guides/specifying-exec-params

Cloud Dataflow 超入門
https://qiita.com/hayatoy/items/987658490a69c7d24635

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Pythonで、デザインパターン「Builder」を学ぶ

GoFのデザインパターンを学習する素材として、書籍「増補改訂版Java言語で学ぶデザインパターン入門」が参考になるみたいですね。

ただ、取り上げられている実例は、JAVAベースのため、自分の理解を深めるためにも、Pythonで同等のプラクティスに挑んでみました。

■ Builder(ビルダー)

Builderパターン(ビルダー・パターン)とは、GoF(Gang of Four; 4人のギャングたち)によって定義されたデザインパターンの1つである。 オブジェクトの生成過程を抽象化することによって、動的なオブジェクトの生成を可能にする。
2880px-Builder_UML_class_diagram.svg.png
(以上、ウィキペディア(Wikipedia)より引用)

■ "Builder"のサンプルプログラム

Builderパターンとは、複雑な構造のものを一気に組み立てるのは難しいので、事前に全体を構成する各部分をつくって
段階を踏んで構造を持ったインスタンスを組み上げていくものらしいです。
実際に、Builderパターンを使って、「文書」を作成するサンプルプログラムを動かしてみて、動作の様子を確認したいと思います。

  • plainモードで、動作させると、プレーンテキスト形式の文書が出力されます。
  • htmlモードで、動作させると、テーブル形式のリンク集のHTMLファイルが生成されます。

(1) plainモードで動かしてみる

まずは、プレーンテキスト形式の文書が出力するコードを動かしてみます。

$ python Main.py plain
======================
Greeting

*** From the morning to the afternoon ***
- Good morning
- Hello
*** In the evening ***
- Good evening
- Good night
- Good bye
======================

所謂、プレーンテキストの文書が出力されました。

(2) htmlモードを動かしてみる

つづいて、TableベースのWebページを作成するコードを動かしてみます。

$ python Main.py html
[Greeting.html] was created.

Greeting.htmlというファイルが生成されました。
Webブラウザで、見た目を確認してみると、こんな感じでした。
tablehtml.png

■ サンプルプログラムの詳細

Gitリポジトリにも、同様のコードをアップしています。
https://github.com/ttsubo/study_of_design_pattern/tree/master/Builder

  • ディレクトリ構成
.
├── Main.py
└── builder
    ├── __init__.py
    ├── builder.py
    ├── director.py
    ├── htmlbuilder
    │   ├── __init__.py
    │   └── html_builder.py
    └── textbuilder
        ├── __init__.py
        └── text_builder.py

(1) Builder(建築者)の役

Builder役は、インスタンスを作成するためのインタフェースを定めます。
Builder役には、インスタンスの各部分を作るためのメソッドが用意されます。
サンプルプログラムでは、Builderクラスが、この役を努めます。

builder/builder.py
from abc import ABCMeta, abstractmethod

class Builder(metaclass=ABCMeta):
    @abstractmethod
    def makeTitle(self, title):
        pass

    @abstractmethod
    def makeString(self, str):
        pass

    @abstractmethod
    def makeItems(self, items):
        pass

    @abstractmethod
    def close(self):
        pass

(2) ConcreteBuilder(具体的建築者)の役

ConcreteBuilder役は、Builder役のインタフェースを実装しているクラスです。実際のインスタンス作成で呼び出されるメソッドが、ここで定義されます。また、最終的にできた結果を得るためのメソッドが用意されます。
サンプルプログラムでは、TextBuilderクラスや、HTMLBuilderクラスが、この役を努めます。

builder/text_builder.py
from builder.builder import Builder

class TextBuilder(Builder):
    def __init__(self):
        self.buffer = []

    def makeTitle(self, title):
        self.buffer.append("======================\n")
        self.buffer.append(title + "\n")
        self.buffer.append("\n")

    def makeString(self, str):
        self.buffer.append("*** " + str + " ***" + "\n")

    def makeItems(self, items):
        for i in items:
            self.buffer.append("- " + i + "\n")

    def close(self):
        self.buffer.append("======================\n")

    def getResult(self):
        return ''.join(self.buffer)
builder/html_builder.py
from builder.builder import Builder

class HTMLBuilder(Builder):
    def __init__(self):
        self.buffer = []
        self.filename = ""
        self.f = None
        self.makeTitleCalled = False

    def makeTitle(self, title):
        self.filename = title+".html"
        self.f = open(self.filename, "w")
        self.f.write("<html><head><title>"+title+"</title></head></html>")
        self.f.write("<h1>"+title+"</h1>")
        self.makeTitleCalled = True

    def makeString(self, str):
        if not self.makeTitleCalled:
            raise RuntimeError
        self.f.write("<p>"+str+"</p>")

    def makeItems(self, items):
        if not self.makeTitleCalled:
            raise RuntimeError
        self.f.write("<ul>")
        for i in items:
            self.f.write("<li>"+i+"</li>")
        self.f.write("</ul>")

    def close(self):
        if not self.makeTitleCalled:
            raise RuntimeError
        self.f.write("</body></html>")
        self.f.close()

    def getResult(self):
        return self.filename

(3) Director(監督者)の役

Director役は、Builder役のインタフェースを使って、インスタンスを生成します。
ConcreteBuilder役に依存したプログラミングは行いません。ConcreteBuilder役が何であってもうまく動作するように、Builder役のメソッドのみを使います
サンプルプログラムでは、Directorクラスが、この役を努めます。

builder/director.py
class Director(object):
    def __init__(self, builder):
        self.__builder = builder

    def construct(self):
        self.__builder.makeTitle("Greeting")
        self.__builder.makeString("From the morning to the afternoon")
        self.__builder.makeItems(["Good morning", "Hello"])
        self.__builder.makeString("In the evening")
        self.__builder.makeItems(["Good evening", "Good night", "Good bye"])
        self.__builder.close()

(4) Client(依頼人)の役

Builder役を利用する役です。
サンプルプログラムでは、startMainメソッドが、この役を努めます。

Main.py
import sys

from builder.director import Director
from builder.textbuilder.text_builder import TextBuilder
from builder.htmlbuilder.html_builder import HTMLBuilder

def startMain(opt):
    if opt == "plain":
        builder = TextBuilder()
        director = Director(builder)
        director.construct()
        result = builder.getResult()
        print(result)
    elif opt == "html":
        builder = HTMLBuilder()
        director = Director(builder)
        director.construct()
        result = builder.getResult()
        print("[" + result + "]" + " was created.")


if __name__ == "__main__":
    startMain(sys.argv[1])

■ 参考URL

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Python asyncio と ContextVar

Python 3.7 で contextvars モジュールが追加され、asyncio に対応した ContextVar クラスが導入されました。

スレッド毎に固有のデータを持てる Thread Local (Python では threading.local()) のように、コルーチン毎に固有のデータを持てるようになっているのが ContextVar の特徴です。

実際に ContextVar を使ってみて、ContextVar にセットしたデータが消えてしまうような現象に見舞われ、改めて検証コードを書いて挙動を調べてみました。

以下のコードでは、parent_await_coroutine()parent_create_new_task() の二つのコルーチン関数を実行しており、それぞれの関数内では ContextVar に値をセットし、child() 関数を呼び出しています。
この child() 関数では ContextVar から取り出した値を変更しています。

二つの parent 関数では child 関数の呼び出し方が異なります。前者はコルーチンを await し、後者はコルーチンをラップした新しい Task を生成して実行しています。

後者の新しい Task として実行した場合には、child 関数で行われた ContextVar への変更の一部が parent 関数には反映されていません。具体的に言うと、child 関数で number_var ContextVar の値を加算して再セットしていますが、parent 関数ではその変更が読み取れていません(child 関数を呼ぶ前の状態のまま)。
一方で、msg_var ContextVar の Msg オブジェクトへの変更は parent 関数からも見えています。

これは新しいタスクを生成した際に contextvars の内容がコピーされているためです。PEP 567 からそのことが読み取れます。
このコピー処理で number_var の int の場合は値コピーが行われ、msg_var の Msg オブジェクトは参照コピーが行われているため(つまり Shallow Copy)、上記の挙動になっていると考えられます。

import asyncio
import contextvars


class Msg:
    """単なるテキストの入れものクラス。
    contextvars の Shallow copy を確認するために使います。
    """

    def __init__(self, text: str):
        self._text = text

    @property
    def text(self) -> str:
        return self._text

    @text.setter
    def text(self, val):
        self._text = val


msg_var: contextvars.ContextVar[Msg] = contextvars.ContextVar('msg_var')
number_var: contextvars.ContextVar[int] = contextvars.ContextVar('number_var')


async def child():
    # ContextVar から数値を取得して 1 加算する
    n = number_var.get()
    print(f'child: number={n}')  # child: number=1
    n += 1
    number_var.set(n)

    # ContextVar から Msg オブジェクトを取得してテキストを変更する
    msg = msg_var.get()
    print(f'child: msg="{msg.text}"')  # child: msg="msg created by parent"
    msg.text = 'msg changed by child'

    # この child() を async 関数にするための処理
    await asyncio.sleep(0.1)


async def parent_await_coroutine():
    n = 1
    number_var.set(n)
    m = Msg('msg created by parent')
    msg_var.set(m)
    print(f'parent: number={n}')  # parent: number=1
    print(f'parent: msg="{m.text}"')  # parent: msg="msg created by parent"

    await child()

    n = number_var.get()
    m = msg_var.get()
    print(f'parent: number={n}')  # parent: number=2
    print(f'parent: msg="{m.text}"')  # parent: msg="msg changed by child"


async def parent_create_new_task():
    n = 1
    number_var.set(n)
    m = Msg('msg created by parent')
    msg_var.set(m)
    print(f'parent: number={n}')  # parent: number=1
    print(f'parent: msg="{m.text}"')  # parent: msg="msg created by parent"

    await asyncio.create_task(child())

    n = number_var.get()
    m = msg_var.get()
    print(f'parent: number={n}')  # parent: number=1
    print(f'parent: msg="{m.text}"')  # parent: msg="msg changed by child"


if __name__ == '__main__':
    asyncio.run(parent_create_new_task())
    asyncio.run(parent_await_coroutine())
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

can't pickle annoy.Annoy objectsの対処法

機械学習系のmodelは基本pickleで管理していたんですが,Annoyについてはpickleでの保存時に以下のエラーがでます.

can't pickle annoy.Annoy objects

とりあえずググってHitしたissueをみてみます.

https://github.com/spotify/annoy/issues/367

image.png

とのことです..

ちゃんとSampleコードをみたら,loadメソッドとsaveメソッドが用意されていたんですね.

ということで,このサンプルコードの通り

from annoy import AnnoyIndex
import random

f = 40
t = AnnoyIndex(f, 'angular')  # Length of item vector that will be indexed
for i in range(1000):
    v = [random.gauss(0, 1) for z in range(f)]
    t.add_item(i, v)

t.build(10) # 10 trees
t.save('test.ann') #保存

# ...

u = AnnoyIndex(f, 'angular')
u.load('test.ann') # super fast, will just mmap the file #読込
print(u.get_nns_by_item(0, 1000)) # will find the 1000 nearest neighbors

にすれば大丈夫そうです.

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Redis の WebAPI を作成

Redis の WebAPI をさまざまな言語、フレームワークで作成します。
まずは、仕様です。

1)サーバーで Redis が動いていること。

key に対して、JSON 文字列を保存するという運用をします。

$ sudo systemctl status redis
● redis-server.service - Advanced key-value store
   Loaded: loaded (/lib/systemd/system/redis-server.service; enabled; vendor pre
   Active: active (running) since Tue 2020-01-14 12:03:10 JST; 1h 30min ago
     Docs: http://redis.io/documentation,
           man:redis-server(1)

2)作成する API は次の3つです。method は POST です。

key を与えて値の読み出し。
key と value (JSON 文字列)を与えて書き込み。
key の一覧

3) curl によるテストスクリプト

test_get.sh
#
URL="http://localhost/test_dir/api/redis_read.py"
#
curl -X POST -d key=t1855 $URL
#
test_insert.sh
#
URL="http://localhost/test_dir/api/redis_insert.py"
#
curl -X POST -d key=t1855 \
-d value='{"name": "宇都宮","population": 87516,"date_mod": "2001-3-16"}' $URL
#
test_list.sh
#
URL="http://localhost/test_dir/api/redis_list.py"
#
curl -X POST $URL
#

3) python によるテストスクリプト

test_get.py
#! /usr/bin/python3
# -*- coding: utf-8 -*-
#
#   test_get.py
#
#                   Jan/14/2020
#
# ------------------------------------------------------------------
import  sys
import  requests
# ------------------------------------------------------------------
sys.stderr.write("*** 開始 ***\n")
#
url="http://localhost/test_dir/api/redis_read.py"
args={}
args['key'] = 't1855'
#
rr=requests.post(url,args)
print(rr.text)
#
sys.stderr.write("*** 終了 ***\n")
# ------------------------------------------------------------------
test_insert.py
#! /usr/bin/python3
# -*- coding: utf-8 -*-
#
#   test_insert.py
#
#                   Jan/14/2020
#
# ------------------------------------------------------------------
import  sys
import  requests
# ------------------------------------------------------------------
sys.stderr.write("*** 開始 ***\n")
#
url="http://localhost/test_dir/api/redis_insert.py"
args={}
args['key'] = 't1855'
args['value'] = '{"name": "宇都宮","population": 84516,"date_mod": "2001-3-16"}'
#
rr=requests.post(url,args)
print(rr.text)
#
sys.stderr.write("*** 終了 ***\n")
# ------------------------------------------------------------------
test_list.py
#! /usr/bin/python3
# -*- coding: utf-8 -*-
#
#   test_list.py
#
#                   Jan/14/2020
#
# ------------------------------------------------------------------
import  sys
import  requests
# ------------------------------------------------------------------
sys.stderr.write("*** 開始 ***\n")
#
url="http://localhost/test_dir/api/redis_list.py"
args={}
#
rr=requests.post(url,args)
print(rr.text)
#
sys.stderr.write("*** 終了 ***\n")
# ------------------------------------------------------------------
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AtCoder Beginner Contest 054 過去問復習

所要時間

スクリーンショット 2020-01-14 11.29.53.png

A問題

1の時は通常の整数の大小と異なるので別に分けて考えます。(普通のA問題より少しややこしい?)

answerA.py
a,b=map(int,input().split())
if a==1 or b==1:
    print("Alice" if a==1 else "Bob" if b==1 else "Draw")
else:
    print("Alice" if a>b else "Bob" if a<b else "Draw")

B問題

Bの左上をAのどの画素に対応させて比較するかを決め、それぞれについてall_true関数(全ての画素が一致してるかを判定する)を呼べば良いです。(若干これもB問題の中では難しめ?)

answerB.py
n,m=map(int,input().split())
a=[list(input()) for i in range(n)]
b=[list(input()) for i in range(m)]
def all_true(i,j):
    global a,b
    for k in range(i,i+m):
        for l in range(j,j+m):
            if a[k][l]!=b[k-i][l-j]:
                return False
    return True
f=0
for i in range(n-m+1):
    for j in range(n-m+1):
        if all_true(i,j):
            f=1
            break
    if f==1:
        print("Yes")
        break
else:
    print("No")

C問題

見たことないパターンの問題なので難しそうだと感じたのですが、制約を見ると最大でもNは8なので実行時間にかなり余裕があることをまずは確認します。ここで、頂点を訪問した順番に書いた時全ての頂点を訪れるのは一回のみなのでそのパスに含まれる頂点数はちょうどN個で全て異なることがわかります。また、N個のの異なる数の並べ方はN!であり、8!が$4\times10^4$程度なので全探索できそうなことがわかります。
全探索の際にそれぞれのパスが存在するかを確かめる方法はいくつかあると思いますが、ここでは繰り返しチェックすることから隣接行列を用います。つまり、初めに隣接行列を用意して、それぞれの頂点の並びを前からチェックしそれぞれの二頂点間にパスが存在するのかをチェックすれば良いです。

answerC.py
import itertools
n,m=map(int,input().split())
p=[[0]*n for i in range(n)]#隣接行列
for i in range(m):
    a,b=map(int,input().split())
    p[a-1][b-1]=1
    p[b-1][a-1]=1
seq=(i for i in range(n))
x=list(itertools.permutations(seq))
#print(x[0])
l=len(x)
ans=0
for i in range(l):
    k=x[i]
    if k[0]==0:
        for j in range(n-1):
            if p[k[j]][k[j+1]]!=1:
                break
        else:
            ans+=1
            #print(k)
    else:
        break
print(ans)

D問題

初めて見た時はわけわからん無理だろこれと思ったのですが、DPを使ったら思った以上に簡単に解くことができました。
DPであると判断した根拠は、順番など関係ないので貪欲にとっていくことができなく、薬品の選び方がO($2^n$)であるからです。(それに加えO(N*sum(a)sum(b))なので制限時間に収まることも確認しました。)
しかし、今回は薬品を選ぶ際にAとBの二つの物質を含んでいるので、
DPを二次元でする必要*があります。
つまり、N個の薬品を選ぶ際にAの量とBの量をそれぞれメモする行列を作りその行列を更新をしていくDPを考えます。この後はナップザックのDPの二次元版をやるだけなのでコードを見た方が早いと思います。

answerD.py
n,ma,mb=map(int,input().split())
el=[list(map(int,input().split())) for i in range(n)]
sa=sum(i[0] for i in el)
sb=sum(i[1] for i in el)

inf=1000000
#infで初期化
x=[[inf for j in range(sb+1)] for i in range(sa+1)]
#最初の要素は0にするのを忘れずに
x[0][0]=0
for i in range(n):
    now=el[i]
    #deepcopy使うとめちゃめちゃ遅くなった、普通の配列は毎回作った方が早い
    x_sub=[[0 for j in range(sb+1)] for i in range(sa+1)]
    #x_subへの更新(複数個ないので)
    for k in range(sa+1):
        for l in range(sb+1):
            if x[k][l]!=inf and k+now[0]<sa+1 and l+now[1]<sb+1:
                x_sub[k+now[0]][l+now[1]]=x[k][l]+now[2]
    #x_sub→xへ値を移す
    for k in range(sa+1):
        for l in range(sb+1):
                if x_sub[k][l]!=0:
                    x[k][l]=min(x[k][l],x_sub[k][l])
mi=min(sa//ma,sb//mb)
ans=inf
#小さいものから順に考えてありうる一番小さいものを考えれば良い
for i in range(1,mi+1):
    ans=min(ans,x[ma*i][mb*i])
if ans==inf:
    print(-1)
else:
    print(ans)

定数倍高速化をしたコード

answerD_better.py
n,ma,mb=map(int,input().split())
el=[list(map(int,input().split())) for i in range(n)]
sa=sum(i[0] for i in el)
sb=sum(i[1] for i in el)
sa1=sa+1
sb1=sb+1

inf=1000000
x=[[inf]*(sb1) for i in range(sa1)]
x[0][0]=0
for i in range(n):
    now=el[i]
    x_sub=[[0]*(sb1) for i in range(sa1)]
    for k in range(sa1):
        for l in range(sb1):
            if x[k][l]!=inf:
                if k+now[0]<sa1:
                    if l+now[1]<sb1:
                        x_sub[k+now[0]][l+now[1]]=x[k][l]+now[2]
    for k in range(sa1):
        for l in range(sb1):
                if x_sub[k][l]!=0:
                    x[k][l]=min(x[k][l],x_sub[k][l])
mi=min(sa//ma,sb//mb)
ans=inf
for i in range(1,mi+1):
    ans=min(ans,x[ma*i][mb*i])
if ans==inf:
    print(-1)
else:
    print(ans)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

HarmonyEngineっていうサイトを創っていく日記(不定期) 1

HarmonyEngineってなんやねん

まあ端的に言うと、うちの兄貴が作ろうとしている新しいサービスの実装です。
以下HEと呼称していきますね。人生で初めてこういった記事を書くので下手糞ですがお付き合いください。その際使った技術などのアウトプットのために記事を作成していこうと思います。ブログに書くかQiitaに書くか迷ったのですが、結局Qiitaにしました。

HEの内容

HarmonyEngine、通称HE普段埋もれてしまっている才能あるクリエイター達を発掘するための新しいサービスのプロトタイプネームです。と兄貴は言っていますが今のところ概要はあんまし掴めてません。でもサービスの内容的にちょっと面白そうなので制作に加わりました。あと、私のTwitterを見ている方ならわかるとは思いますが、将来やりたいことのためのポートフォリオとして有効活用していきたいなと。

ちなみに

このサービスの名前は某小説家の中に出てくる名前からとったそうです。まあsteins;gateみたいでカッコいいと思うけどね。

仕様

一応今後のポートフォリオにするために、やりたい言語を詰め込んでいく

フロント

  • HTML
    • HTML5
  • CSS
    • CSS3
  • Javascript
    • vue.js
    • jquery

なお現在js系は頑張って勉強中です・・・

サーバーサイド

  • PHP
  • python
    • django

今のところこれ以外に思いつきません・・・

んじゃ

2020/01/15現在で何も情報がないので書きようがないのですが、これからサービスを構築していこうと思います。がんばるお(^ω^)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

pyusbでオーディオインターフェースコントローラーを作る (2)

大分間が空いてしまったが、前回の続き。今回は

  • Linuxでの動作確認
  • Pythonコードからのデバイス状態の変更

この2つ。ただしLinuxではFocusriteのソフトウェアが当然対応していないので、Windowsでは勝手にやってくれた初期化シーケンスを自前で行う必要がある。まずはその解析から。

Scarlett初期化シーケンスの解析

まずはRatatoskrを起動した上でScarlett 18i20の電源をON/OFFしてみる。

VirtualBox_Windows 10_07_01_2020_00_40_15.png

かなりの量の通信が行われているが要点となるのは通番のリセットなので、通番の位置に着目して当たりをつける。通番の位置は15..16バイトのようなので、普通に考えれば通番が0か1になっており、また9..14バイトのメッセージ種別の部分にも特徴が表れそうに思える。(※画像ではフィルターでいろいろサプレスしている。)

結果として以下の2つのコントロール転送がそれっぽいと当たりが付いた。

SEND 21 02 00 00 03 00 10 00 00 00 00 00 00 00 01 00
SEND 21 02 00 00 03 00 10 00 02 00 00 00 00 00 01 00

通番1のメッセージを2回送っているが、多分最初の方が通番リセット処理であり、2回目は普通に通番1でメッセージを出しているのだと思われる。まずはこの通りのコードを書いて、前回書いたデバイス状態の取得コードをリセット後の通番で動作させてみる。(下記のコードは抜粋。詳細は前回のコードを参照。)

reset_seq.py
device.ctrl_transfer(0x21, 0x02, 0x00, 0x03,
    [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00,
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
device.ctrl_transfer(0xA1, 0x03, 0x00, 0x03, 0x0010)
device.ctrl_transfer(0x21, 0x02, 0x00, 0x03,
    [0x00, 0x00, 0x80, 0x00, 0x08, 0x00, 0x01, 0x00,
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
     0x7C, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00])

ちなみに通番リセット命令の通番部分の値は何でもよいようだ。

Linuxでの動作

LinuxでUSBデバイスとの通信を見る場合、wiresharkを使う方法が結構メジャーなのかと思うが、私は/sys/kernel/debug/usbmon以下をcatして不要な出力をgrep -vで弾いている。(いずれにせよusbmonモジュールを組み込む必要がある。)

またLinuxでは初期化処理もWindowsと少々異なるようで、例えばset_configurationを下手に呼ぶとエラーになったりする。

analogstat4linux.py
import usb.core
import usb.backend.libusb1
from ctypes import c_void_p, c_int
backend = usb.backend.libusb1.get_backend()

from usb.util import CTRL_IN, CTRL_OUT, CTRL_TYPE_CLASS, CTRL_RECIPIENT_INTERFACE, build_request_type
from usb.control import get_status

VENDOR_ID = 0x1235
PRODUCT_ID = 0x8215
device = usb.core.find(idVendor=VENDOR_ID, idProduct=PRODUCT_ID, backend=backend)
device.ctrl_transfer(0x21, 0x02, 0x00, 0x03,
    [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00,
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
device.ctrl_transfer(0xA1, 0x03, 0x00, 0x03, 0x0010)
device.ctrl_transfer(0x21, 0x02, 0x00, 0x03,
    [0x00, 0x00, 0x80, 0x00, 0x08, 0x00, 0x01, 0x00,
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
     0x7C, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00])
device.read(0x83, 8, 100)
ret = bytearray(device.ctrl_transfer(0xA1, 0x03, 0x00, 0x03, 0x0028))
print(' '.join(map(lambda x: '{0:0{1}x}'.format(x, 2), ret)))
$ sudo cat /sys/kernel/debug/usbmon/3t > dump.txt
※別のターミナルでスクリプト実行
Ctrl-C
$ grep -v 'Z[io]:' dump.txt 
ffff95a78595f500 560234266 S Co:013:00 s 21 02 0000 0003 0010 16 = 00000000 00000100 00000000 00000000
ffff95a78595f500 560234401 C Co:013:00 0 16 >
ffff95a78595f500 560234429 S Ci:013:00 s a1 03 0000 0003 0010 16 <
ffff95a78595f500 560234646 C Ci:013:00 0 16 = 00000000 00000000 00000000 00000000
ffff95a78595f500 560234667 S Co:013:00 s 21 02 0000 0003 0018 24 = 00008000 08000100 00000000 00000000 7c000000 18000000
ffff95a78595f500 560234893 C Co:013:00 0 24 >
ffff95a78595f500 560235404 S Ii:013:03 -115 8 <
ffff95a78595f500 560235649 C Ii:013:03 0 8 = 01000000 00000000
ffff95a78595f500 560235693 S Ci:013:00 s a1 03 0000 0003 0028 40 <
ffff95a78595f500 560235776 C Ci:013:00 0 40 = 00008000 18000100 00000000 00000000 00000000 00000000 00000000 00000000

※ダンプ結果のフォーマットについてはusbmonのドキュメントを参照。

どうやらこちらも期待通りの結果になっているようだ。(※一部バイトオーダーの変換が入っているのでWindows環境の結果と一致しない部分がある)

尚いろいろコードを書いて実験しているうちにOperation Timeoutになってデバイスがハングした事が何回かあったが、現状そうなった場合の復帰方法がハードウェアの電源OFF以外に見つかっていない。これは今後の課題。

Pythonコードでデバイスの状態を変更

いよいよ本題。Scarlett 18i20の機能のうち、本体のスイッチで対応できず、またLinuxの汎用ドライバーで操作できない機能は以下の通り。

  • AIR機能の切り替え1
  • Scarlett内蔵ミキサーの操作

後者は結構ややこしいというか面倒くさい割に無くても私が使う分にはさほど困らないので2、前者とついでにLINE/INSTのIMPEDANCE3切り替えや入力のPAD4にも対応してみる。

AIR切り替えなどの解析

手順は今までと概ね同じ。

  • Focusrite Controlを起動
  • Focusrite Control上で適当な入力チャンネルのAIRを変更
  • Focusrite Controlを終了
  • Ratatoskrで該当箇所っぽい部分を気合で探す5

結果、以下の通信がAIR切り替えのようだ。

シーケンス(1)
SEND: 21 02 00 00 00 03 00 19 01 00 80 00 09 00 01 00 00 00 00 00 00 00 00 00 8c 00 00 00 01 00 00 00 01
SEND: 03 00 00 00 03 00 10 
RECV: 01 00 80 00 00 00 01 00 00 00 00 00 00 00 00 00

シーケンス(2)
SEND: 21 02 00 00 00 03 00 14 02 00 80 00 04 00 02 00 00 00 00 00 00 00 00 00 08 00 00 00
SEND: 03 00 00 00 03 00 10 
RECV: 01 00 80 00 00 00 02 00 00 00 00 00 00 00 00 00

他にIMPEDANCEなども切り替えた結果、アナログ入力の状態変更のシーケンス(1)での出力コントロール転送は以下のフォーマットになっているようだ。

  • 1..8バイト: 転送ヘッダ。
  • 9..14バイト:メッセージ種別。
  • 15..16バイト:通番。(ここまでは大抵の出力コントロール転送共通のようだ)
  • 17..24バイト: 0埋めの固定値。
  • 25バイト: アナログ入力とそのモードの番号
  • 26..32バイト: 固定値
  • 33バイト: 0 or 1。モード切り替え

25バイト目の詳細は以下。

  • 0x7C~0x7D: アナログ入力1~2のIMPEDANCE
  • 0x83~0x8B: アナログ入力1~8のPAD
  • 0x8C~0x93: アナログ入力1~8のAIR

またシーケンス(2)の出力コントロール転送は何の意味があるのかと思っていたが、どうもこれは本体のインジケーターの表示切替命令のようだ。このメッセージを送信しないと、本体のINST/PAD/AIRのインジケーターが切り替わらない。

ここまでのまとめ的なコード

以下は実行する度にアナログ入力1のAIRの状態を切り替えるコード。

switch_air.py
import usb.core
import usb.backend.libusb1
from ctypes import c_void_p, c_int
backend = usb.backend.libusb1.get_backend()

from usb.util import CTRL_IN, CTRL_OUT, CTRL_TYPE_CLASS, CTRL_RECIPIENT_INTERFACE, build_request_type
from usb.control import get_status

VENDOR_ID = 0x1235
PRODUCT_ID = 0x8215
device = usb.core.find(idVendor=VENDOR_ID, idProduct=PRODUCT_ID, backend=backend)
device.ctrl_transfer(0x21, 0x02, 0x00, 0x03,
    [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00,
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
device.ctrl_transfer(0xA1, 0x03, 0x00, 0x03, 0x0010)

#状態の問い合わせ
device.ctrl_transfer(0x21, 0x02, 0x00, 0x03,
    [0x00, 0x00, 0x80, 0x00, 0x08, 0x00, 0x01, 0x00,
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
     0x7C, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00])
device.read(0x83, 8, 100)
ret = bytearray(device.ctrl_transfer(0xA1, 0x03, 0x00, 0x03, 0x0028))
stat = ret[32] #アナログ入力1のAIR

device.ctrl_transfer(0x21, 0x02, 0x00, 0x03, [0x01, 0x00, 0x80, 0x00, 0x09, 0x00, 0x02, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x8C, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
    stat ^1]) # XORで状態をスイッチ
device.ctrl_transfer(0xA1, 0x03, 0x00, 0x03, 0x0010)

# インジケーターの切り替え
device.ctrl_transfer(0x21, 0x02, 0x00, 0x03, [0x02, 0x00, 0x80, 0x00, 0x04, 0x00, 0x03, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00])
device.ctrl_transfer(0xA1, 0x03, 0x00, 0x03, 0x0010)



実際に私が開発しているコードは下記にて公開中。

私が現状多忙につきまったくもって手が足りないので、

  • Pythonが書ける
  • Linuxで開発作業ができる
  • Scarlettシリーズのgen3を持っている/購入予定がある

上記に該当する方は、是非とも協力してください。6


  1. マイク録音時の音質を、同社のベストセラー商品のマイクプリアンプをエミュレートしたものに変更する機能。これが使えないと56,000円で買った甲斐がない。 

  2. レコーディング作業時には入力信号をPCで録音しつつ、レイテンシーなしにモニタリングするために出力に直に繋ぐなどを行う事があるので、内蔵ミキサーを弄れるに越したことはない。 

  3. 機材によって流れる信号のインピーダンスが異なるのでそれの切り替え。 

  4. 過大入力に対してゲインを下げる。 

  5. 起動シーケンスが終わった後にF4でログを一旦クリアする、前回述べたようにフィルターで絞るなど状況に応じて工夫が必要。余計な操作を行わないというのも重要か。 

  6. 日本に何人いるか知らんけど。海外では似たようなことをやってる人を2人発見した。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AtCoder ABC151 問題D C++ / Python での速度比較

概要

AtCoder ABC151 に参加した。問題Dをこの問題にはおそらく最適ではない方法(ワーシャル-フロイド法)でC++/Pytho3で解いて実行時間を測定・比較してみた。

結果は問題サイズがある程ど大きくなるとC++の方が安定して60倍-70倍程度高速だった。TLEの不安が少しでもある場合はPythonで実装するのはやめたほうが良さそうである。

経緯

AtCode ABC151 に参加した。問題Dみたとき以下のように考えた。

「蟻本にのってた方法(ワーシャル-フロイド法)でとけるなぁ。でもノード(マス)は上下左右のとしかつながっていないし、隣り合うノード間の距離はいつも1だからもっと効率的な方法ありそう。

でもO(N^3)でN=400だから400^3 = 64,000,000かまぁいけるんじゃない?他の方法考えるのも面倒だしこれでやれば解けるのわかっているしワーシャル-フロイド法でいこう。」

とあまり深く考えずPythonで実装を実施。制限時間ぎりぎり(3分前)までしこしこデバッグして提出(20-01-12 22:37:01)したけど結果はTLE。悲しかった。

後日C++で実装して提出したところ実行時間は71msecと余裕。今後の参考にしたい。

速度比較結果

速度比率

image.png

上の図は

  • 自宅のNUC(CentOS)上のubuntu KVMノードで実行する。
  • 問題サイズを1から20まで変化させる。
  • C++/Pythonで一つのサイズをそれぞれ5回繰り返し実行。
  • 実行経過時間をtimeコマンドで測定する。
  • 5回の実行の平均値を計算する。
  • 縦軸を(Pythonでの平均実行時間)/(C++での平均実行時間)横軸を問題サイズ(H,W(H=W))としてプロットする。

して得られた結果である。問題サイズが大きい時実行速度の比率は60倍以上になることがわかる。使用した問題ケースについては後述。

実行経過時間
実行経過時間C++
image.png

実行経過時間Python
image.png

グラフでは5回実施したときの経過時間の最大値(MAX)、平均値(AV)、最小値(MIN)を示してある

サンプルケース(参考)

問題サイズを変えた時のサンプルケースはH=WとしてH(=W)を1から20まで変化させて以下のような感じで作成した。

sampleSquare-10.in
myoden@ubuntu002:~/AtCoder/ABC151/D$ cat sampleSquare-10.in 
10 10
.#.#.#.#.#
..........
.#.#.#.#.#
..........
.#.#.#.#.#
..........
.#.#.#.#.#
..........
.#.#.#.#.#
..........
sampleSquare-20.in
myoden@ubuntu002:~/AtCoder/ABC151/D$ cat sampleSquare-20.in 
20 20
.#.#.#.#.#.#.#.#.#.#
....................
.#.#.#.#.#.#.#.#.#.#
....................
.#.#.#.#.#.#.#.#.#.#
....................
.#.#.#.#.#.#.#.#.#.#
....................
.#.#.#.#.#.#.#.#.#.#
....................
.#.#.#.#.#.#.#.#.#.#
....................
.#.#.#.#.#.#.#.#.#.#
....................
.#.#.#.#.#.#.#.#.#.#
....................
.#.#.#.#.#.#.#.#.#.#
....................
.#.#.#.#.#.#.#.#.#.#
....................
myoden@ubuntu002:~/AtCoder/ABC151/D$ 
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

インクリメント・デクリメントの書き方のまとめ(Scala、Java、Rust、C言語、C++、Go言語、PHP、Perl、Python、Ruby、JavaScript)

いろんな言語を触っていると、言語の細かい仕様がだんだんごっちゃになってきてしまいますので、メモです。

インクリメント・デクリメントの有無

あり: Java、C言語、C++、Go言語△、PHP、Perl、JavaScript
なし: Scala、Rust、Python、Ruby

Go言語は式を構成する演算子ではなく文(statement)という扱いにすることで、インクリメントの演算子としての問題を回避していて、個人的にはちょうどいい仕様に感じます。

ついでに代入演算子も確認しましたが、こちらはだいたいの言語にあるようです。

Scala

  • インクリメント・デクリメント演算子はない
  • 代入演算子はある
i += 1
i -= 1

i += 1 などは i = i + 1 などのシンタックスシュガー。

参考

Assignment Operators - Expressions | Scala 2.13

Scalaでは、なぜインクリメントやデクリメントができないのか?

Java

  • インクリメント・デクリメント演算子は前置・後置ともにある
  • 式であり値を返す
  • 代入演算子もある
++i;
--i;
i++;
i--;
i += 1;
i -= 1;

参考

Prefix Increment Operator ++ - Java Language Specification

Rust

  • インクリメント・デクリメント演算子はない
  • 代入演算子はある
i += 1;
i -= 1;

参考

Compound assignment expressions - Operator expressions - The Rust Reference

なぜインクリメント演算子がないのか?
Why doesn't Rust have increment and decrement operators?

C言語、C++

  • インクリメント・デクリメント演算子は前置・後置ともにある
  • 式であり値を返す
  • 代入演算子もある
++i;
--i;
i++;
i--;
i += 1;
i -= 1;

Go言語

  • C言語でいうインクリメント・デクリメント演算子は後置のみ
  • 式ではなく文の扱いなので、式の中には埋め込めない
  • 代入演算子もある
i++
i--
i += 1
i -= 1

参考

IncDec statements - The Go Programming Language Specification

++--が演算子ではない件
演算子とステートメント — プログラミング言語 Go | text.Baldanders.info

PHP

  • インクリメント・デクリメント演算子は前置・後置ともにある
  • 式であり値を返す
  • 代入演算子もある
++$i;
--$i;
$i++;
$i--;
$i += 1;
$i -= 1;

参考

加算子/減算子 | PHP Manual

Perl

  • インクリメント・デクリメント演算子は前置・後置ともにある
  • 式であり値を返す
  • 代入演算子もある
++$i;
--$i;
$i++;
$i--;
$i += 1;
$i -= 1;

参考

インクリメントとデクリメント - perlop - Perl の演算子と優先順位 - perldoc.jp

Python

  • インクリメント・デクリメント演算子はない
  • 代入演算子はある
i += 1
i -= 1

累算代入文というらしい。

参考

累算代入文 (augmented assignment statement) - 単純文 (simple statement) — Python 3.8.0 ドキュメント

Ruby

  • インクリメント・デクリメント演算子はない
  • 代入演算子はある
i += 1
i -= 1

自己代入というらしい。

参考

演算子式 (Ruby 2.6.0)

Ruby にインクリメント演算子のようなものが無い理由 - fugafuga.write

Rubyのインクリメント速度のバージョンごとの比較 - Qiita

JavaScript

  • インクリメント・デクリメント演算子は前置・後置ともにある
  • 式であり値を返す
  • 代入演算子もある
++i;
--i;
i++;
i--;
i += 1;
i -= 1;

参考

Update Expressions - ECMAScript® 2019 Language Specification

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む