🙄

PHPerがはじめてのPythonでつまづいたポイント8点

2022/12/05に公開

PGを10年少々やっているPHPerがこのたび、初めてPythonをさわったところつまづいたポイントがいくつかあったので備忘録のためまとめていく。

筆者(K)の普段使用している言語

  • PHP
  • JavaScript
  • HTML
  • CSS

他、数回だけど作るのにさわったことのある言語

  • Node.js
  • React

開発環境

  • AWS Lambda
  • Python 3.9
  • AWS RDS (PostgreSQL)

AWSコンソールのコードエディターにて開発していく。

開発概要

  • S3にアップされたExcelをGETしてちょっと編集して同一S3に別KeyでPUTする

ただこれだけ...。
以下は仕様としては実装するか保留中だが、一部実践してつまづいたポイントがあるため、記載していく。

  • (Excelの内容はRDSに非正規状態でつっこんでいく→諸事情で保留中)
  • (Excelの内容をCSVへ書き込む→諸事情で保留中)

この記事を読むにあたっての注意

筆者はPHPが好き過ぎるわけではなく、またPythonを否定したいわけでもありません。
いつも使っている言語でならこうできるのにと当たり前に書いた記法でSyntaxエラーが出た...きっと他のPHPerも困ってかもしれない...ということがまとめられているだけです。

つまづきポイントざっと一覧

  • 言語としてのPython 編
    • いーっぱい引数がある関数の引数の指定
    • 関数は先に定義しておかないと使えない(当たり前)
    • 型不一致で文字結合できない
    • インクリメントでSyntaxエラーだと...?
  • PostgreSQLライブラリ psycopg2 編
    • プリペアドステートメントは一体どうやって...?
    • また文字結合問題かよ!
  • Excel操作ライブラリ openpyxl 編
    • セルの指定方法がいっぱい
    • あの...背景色が欲しいだけなんですけど
  • 番外編 Lambda
    • 日本語ファイルをtmpにDLできない?なんで?

言語としてのPython 編

いーっぱい引数がある関数の引数の指定

s3_client = boto3.client('s3')
s3_client.head_object(Bucket=Bucket, Key=Key);
s3_client = boto3.client('s3')
s3_client.head_object(Bucket, Key);

head_object()の1つ目の引数はBucketですが、2つ目はKeyではない。

これはAPIのリファレンスを最初に読まなかった私も悪いのですが、このhead_object()一つをとっても相当な引数があるんですね。諸般の事情でPHP5を長らくやっていた私には(引数と同じ名前なら動くのか?)と思っていました。

関数は先に定義しておかないと使えない(当たり前)

大した話ではないですが、ある関数Aの中で使用する別の関数Bは順序的に先に定義しないとSyntaxエラーになります。
PHPは使えるんだよ、PHPは...。

def fuctionB(hoge):
    return 1

def functionA(fuga):
    value = functionB(hoge)
def functionA(fuga):
    value = functionB(hoge)
    
def fuctionB(hoge):
    return 1

大抵の言語は先に定義しておくものです。了解です。

型不一致で文字結合できない

pythonは使用する変数の事前の宣言や型の定義が不要です。PHPと同じですね(ニッコリ)

ですが、文字列結合したい!となった時にPHPとは少々事情が異なるようです。
結論から言えばキャストしないといけません。

minutes = 1
print('現在設定されている時間は' + str(minutes) + '分です。')
# もしくは
print(f'現在設定されている時間は{minutes}分です。')

後者の書き方であればキャストをする必要はありません。
一般的には恐らくこちらの方が多いのでしょう。
f'文字列'fはformat文字列という意味のfだとか。

minutes = 1
print('現在設定されている時間は' + minutes + '分です。')

こうするとSyntaxエラーがでます。
文字列にintは結合できないよ!って怒られます。
型の定義がないくせにキャストが必要だとは生意気だ_(:3」∠)_

インクリメントでSyntaxエラーだと...?

インクリメント箇所より前までのエラーはつぶしていたので不意打ちをくらいました。

i = 0
i += 1

えっ...あの書き方できないの...?

i = 0
i++

デクリメントも同様に存在しないため、i -= 1と書かないといけないんですね。
個人的にあまり使う機会はないと思っているんですが、++iという使い方はできないと...。
こちらに関しては結構自分で実装する!系の記事があるようです。

PostgreSQLライブラリ psycopg2 編

プリペアドステートメントは一体どうやって...?

PHPではSQLインジェクション対策にプリペアドステートメントで値を式内にバインドして、実行...なんですが、psycopg2においてはこのバインドする、という記述が不要なようです。ライブラリが勝手にやってくれると。便利ですね。

SQL文を記述して、その中にそのまま入れればOK。
そしてこの時、再び文字列結合問題に直面するのでした。

また文字結合問題かよ!

今回の仕様上、DBに格納したい値にはfloat値などちょっとややこしい(?)ものが含まれていました。

id = 1
name = 'やまだたろう'
email = 'hogehoge@example.com'
avg = 130.7 # 適当にボウリングのアベレージ(厳密に計算したもの)と仮定

# 接続などは省略
cursor.execute('\
    insert into users(\
        id, name, email, avg\
    ) values (\
        %s, %s, %s, %s\
    )',
    id, name, email, avg
)

float型どころかint型まで全部%s!?※ご存じとは思いますが通常はstr型の場合にのみ使用

id = 1
name = 'やまだたろう'
email = 'hogehoge@example.com'
avg = 130.7 # 適当にボウリングのアベレージ(厳密に計算したもの)と仮定

# 接続などは省略
cursor.execute('\
    insert into users(\
        id, name, email, avg\
    ) values (\
        %d, %s, %s, %f\
    )',
    id, name, email, avg
)
# こちらもNG
cursor.execute('\
    insert into users(\
        id, name, email, avg\
    ) values (\
        ' + id + ', ' + name + ', ' + email + ', ' + avg\
    )'
)

未検証ですがが、おそらくformat文字列でも書けないのでしょう...。

関係無いですが、SQL文のように文字列で複数行に渡る場合はPythonの特性上、行末に\(バックスラッシュ)を付けなくてはいけないんですね。面倒ですね。ヒアドキュメント的なものは無いようです。

Excel操作ライブラリ openpyxl 編

セルの指定方法がいっぱい

JavaScriptの時にDOMとオブジェクトが微妙に分からない的なアレなのか、とにかく「このセルの値が欲しい!」の時に指定する方法がいっぱいあります。混乱極まる。

# A1のセルに値を格納したいとき
wb = openpyxl.load_workbook(excel_file)
ws = wb.worksheets[0]

# ①
ws['A1'].value = 'hoge'

# ②
ws[f"{get_column_letter(1)}1"].value = 'hoge' # 1列目(A列)の意

# ③
ws.cell(1, 1, 'hoge')

①と②は厳密には同じ書き方なんですが、1列目(A)、という情報しかない時もあると思います。Zまではまあ26までですが、それ以降はどんどん複雑になるし、Kだから11...なんてのも面倒なのでget_column_letter()を使用して数値→アルファベットに変換します。

③の記法は値を格納するにはいいんですが、他の値(文字色や背景色などの修飾情報など)をとるには①か②をしなければいけません。

あの...背景色が欲しいだけなんですけど

結論から言うとこの問題はちゃんと解決していません。

仕様としてはセルに背景色がついてるかを判定し、後に当該セルの値の位置が変わった時に背景色を追従させたい、というのが目的です。

背景色は元データの気まぐれで別の色が指定される可能性を考慮し、「背景色無しではない」を条件にしようとしています。

ググってみると

cell.fill.start_colorcell.fill.start_color.indexで取得できるよ!

と出てくるのですが、
実際に背景色がないセルにこれをやろうとすると

取得できた値

pprint.pprint(cell.fill.start_color)

# 出力内容
# <openpyxl.styles.colors.Color object>
# Parameters:
# rgb='FFFFFF00', indexed=None, auto=None, theme=None, tint=0.0, type='rgb'

これはこっからどうしたらええんや...(圧倒的勉強不足)

ちなみにcell.fill.start_color.indexではきちんと欲しかった背景色の値が取得できました。ただこの検証をしていた時、筆者が色々テンパったらしく結局別の書き方をします。
したがって

# 背景色指定なしのときに00000000になるっぽい...
if cell.fill.fgColor.value != '00000000' :
    # 背景色がついている場合の処理が続く...

fgColorがなんなのかまた調べないといけないなと思いつつとりあえず放っておきます。

ただし、ここでまた落とし穴が...
この時検証のサンプルとして使用していた元データのExcelでは背景色が真っ黄色だったため取得できた値は'FFFFFF00'でした。

待って、カラーコード8桁なんですけど?????

いや6桁までしか知らないよ?
真っ黄色はFFFF00のはずなので最初の2桁はアルファ値か何かですか?(これが後に正解と知る)
ただ、イメージ的に「アルファ値が最後」という頭があったためそもそもFFFFFF(白)なのもおかしいよなあと最初は騙されていました。

番外編 Lambda

日本語ファイルをtmpにDLできない?なんで?

excelファイルがS3に置いてあるため、操作をするにもまずはLambdaのtmpにダウンロードします。
テストの際にはeventの中身を出力するなどし、それをコピペ後、一部ファイル名などはいじりながらテストデータとして使用していたため、いざS3へのPUTをトリガーとして動かした際ダウンロードするところで404でエラーに。

S3からの情報を利用しているだけな以上、プログラム上での加工はなく、なぜ...?と宇宙猫顔に🐱

source_key = urllib.parse.unquote(event['Records'][0]['s3']['object']['key'])

ポイントは日本語のファイルの場合、eventの中身はJSONであるため、URLエンコードされているというところにあります。

例えばあいうえお.xlsxというファイル名(key)であれば%E3%81%82%E3%81%84%E3%81%86%E3%81%88%E3%81%8A.xlsxという値でPython上にはわたってきます。

これをこのままS3に向かってダウンロードして~指定すると%E3%81%82%E3%81%84%E3%81%86%E3%81%88%E3%81%8A.xlsxなんてファイルは知らん=404となるわけです。

S3にはURLデコードしたあいうえお.xlsxダウンロードして~と指定すればきちんとS3からファイルを落としてきてくれるわけですね。

読まなくてもいい所感

以上が、「PHPerがはじめてのPythonでつまづいたポイント8点」でした。
Pythonが「誰が書いても綺麗に書ける言語」というふれ込みをずいぶん昔に聞いていたので、やっと実感に至ったのですが、フォーマットが自動的にできないよなーとか思うところはありました。

LambdaではPHPランタイムが無い(方法はありますが)ため、Node.jsでものを作ったことはあったのですが、APIを利用するにしても非同期通信でのメリットが無い実装が多く、もっと早くにやっていればよかったな、とも思いました。頑張ろう...。

Discussion