40のおっさんのPython学習記録

20年以上前、学部の必修のC言語が全く理解できずに同級生に放り投げ、その後コーディングから遠ざかったガチ文系のおっさんが、ふと思い立ってPythonに挑戦しています。

生存報告

おっさんです。生きています。挫折したわけではありません。

おっさんは先週1週間海外出張に行っていました。海外出張、手間がかかるんですよ。出張の準備をやって、帰ってきたら報告書をまとめなきゃならんのです。転職前の会社は出張報告なんか必要ない、実務で表現できている限り問題ない、という会社だったんですけどね、おっさんの今の会社はいちいち報告会をやらないかんのですよ。報告書を書いた上で報告会ですよ。おっさんの仕事的には海外出張しなきゃならんのですけど、おっさんの仕事に関係のない人たちにとっては海外出張なんかただ金のかかるイベントでしかなくて、報告を通じて会社に還元しなければならないとかいう文化なんだそうですよ。おっさんの出張とあんたらの業務関係ないじゃん。遊びにいってるわけじゃないんだよ。40越えてエコノミークラスに乗って、1日4〜5件ミーティングするってどれだけ苦痛だと思ってるの。めんどくせえ。転職してえ。ということで、1週間出張に行くと、準備に一月(これは以前の会社と変わらず)、報告に2週間(これは以前の会社に比べてまるまる追加負担)というかんじで、Pythonの勉強はまるまる滞ってます。

今日ももくもく会にいってましたけどね、もくもくと出張報告を書いてましたよ。もう一体何なんだろうね。

あー、さっさと今の会社辞めたい。

ということでおっさんの生存報告でした。

今週末のもくもく会からはPythonの勉強再開できると思います。

新たな気づき(Chapter 11-2)

おっさんです。最近明らかにアクティビティが低下しています。Pythonに飽きているというわけではありません。2月は旅行に行って1週末つぶしたり、別の週末は金曜日に飲みすぎたせいで週末をひどい二日酔いで潰したりしているせいです。おっさんなのでそもそも二日酔いになりやすい上に、ほんの少し疲れているだけで、この程度で酔わへんやろと言う量で泥酔するようになりましあt。最近、おっさんは酒は毒だなと言う思いを日に日に強くしています。今週末と来週末も長期海外出張で潰すので、また停滞しそうです。海外出張中に現地オサレカフェにMacBook持ち込んでやればいいだろとも思いますが、海外出張中って基本的には疲れ果ててるので、ホテルでだらけるしか僕にはもう選択肢はありません。それと、もう一つマジな話としては、前回のみんpyでも激しく躓いた関数とクラスのあたりに入っているので、どうしても理解に苦しんでいるので進捗が遅くならざるをえない局面というのはありますな。さて、今週末も張り切っていきましょう。

  • イテラブルとは、要素を順に取り出せるオブジェクト。リスト、タプル、辞書、文字列。セット・集合は順番がないからイテラブルではない。
  • イテレータは、要素を1個取り出すごとにどこまで取り出したかという情報を保持。
  • iter(iterable)でイテラブルからイテレータを作ることができる。
  • next(iterator)で要素を取り出す。1度目で1要素目、2度目で2要素目、3度目で3要素目が取り出される。全ての要素を取り出し終えると繰り返さず、その後はエラーになる。以下の様に3つしか要素がない場合4度目をやるとエラーになる。
>>> colors = ['red','yellow','green']
>>> color_iter=iter(colors)
>>> next(color_iter)
'red'
>>> next(color_iter)
'yellow'
>>> next(color_iter)
'green'
>>> next(color_iter)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
  • ジェネレータ関数を定義してイテレータを作成することも可能。ジェネレーター関数はyieldで値を返す。
def signal_generator():
    yield 'red'
    yield 'green'
    yield 'yellow'

signal = signal_generator()

print(next(signal))
print(next(signal))
print(next(signal))
print(next(signal))

for-inの利用も可能なので、全ての要素を一発で取り出すことも可能。

  • returnもyieldも関数の定義の中で値を返すのは同じ役割ではあるが、returnの場合は関数の処理を全て終了し値を返すが、yieldは関数の処理を一旦停止し値を返し、次のきっかけがあるまでは何の処理も値を返したりもしないので、メモリ処理量が少なく住む。
  • ジェネレーター関数を用いることで、文字通り数列を順に生成することができる。
def num_generator():
    n=0
    while True:
        num=n**2
        yield num
        n+=1

def do_something(num):
    return (num,num%2,num%3)

gen=num_generator()
for i in range(1,10):
    num=next(gen)
    result=do_something(num)
    print(result) #1回目〜10回目の計算結果が表示される

print(result) #10回目の計算結果が再度表示される

num=next(gen)
result=do_something(num)
print(result) #11回目の計算結果が表示される

print(result) #11回目の計算結果が再度表示される
  • 一番外の囲み記号を()とすることで、ジェネレータ式を書くことができる。リストにするとまとめて全部出せる。こんな感じ。
>>> square_gen = (i**2 for i in range(0,10))
>>> next(square_gen)
0
>>> next(square_gen)
1
>>> next(square_gen)
4
>>> next(square_gen)
9
>>> list(square_gen)
[16, 25, 36, 49, 64, 81]
  • ジェネレータは値を取り出すだけではなく、コルーチンという仕組みを使って外部から値を送信することも可能。
def coro():
    hello = yield "Hello" #yieldで値を返すだけではなく、sendされた値が代入される
    yield hello
    yield hello

c = coro()
print(next(c))         #Hello
print(c.send("World")) #World
print(next(c))         #World
print(next(c))         #StopIterationエラー

上の例だと、hello〜の行含めて3行しかないので、4回目のprintはエラーになる。

  • サブジェネレーターというか、ジェネレーターの中にジェネレーターをぶら下げることもできる。
def main_gen(n):
    yield from range(n,0,-1)
    yield from "abc"
    yield from [10,20,30]
    yield from sub_gen()

def sub_gen():
    yield 'x'
    yield 'y'
    yield 'z'

gen=main_gen(3)

print(next(gen)) #3
print(next(gen)) #2
print(next(gen)) #1
print(next(gen)) #a
print(next(gen)) #b
print(next(gen)) #c
print(next(gen)) #10
print(next(gen)) #20
print(next(gen)) #30
print(next(gen)) #x
print(next(gen)) #y
print(next(gen)) #z
print(next(gen)) #StopIteration

新たな気づき(Chapter 11-1)

久しぶりですね。行きています。週末って意味では先々週は旅行に行ってたのと、旅行帰りの週に飲み会が続き、ヘロヘロになっているところに金曜の飲み会で泥酔し、二日酔いがひどく、週末はひたすら休養を取らざるをえないと言う実におっさんらしい情けない状況でした。只今、ウィークデーの夜のもくもく会の会場からお送りしています。尚おっさん、再来週は1週間海外出張なので、週末はほぼ移動する必要があり、ここで2週末はほぼ何もできないことが確定。そして出張中はどうせ疲れ果てて何もする気が起きないに決まっている。おっさんはおっさんなので、海外出張に行くと疲れ果てて、1日の予定が終わるともうホテルでくたばるしかすることがない。本当にダメだ。

  • 関数は数値や文字列と同じように扱うことができる。既定義の関数func1()がある場合、func2=func1と書けば(カッコは書かない)、以後func2()はfunc1()と同じ動きをする。
  • また、以下のように関数を関数の引数にすることもできる。
def calc(func,arg=1):
    price = func(arg)      #funcに代入されるchild(arg)もしくはadult(arg)が実行される。
    return price

def child(arg):
    return 400 * arg

def adult(arg):
    return 1200 * arg

age=input("年齢は?:")
#age = 8
num=input("何人?")
#num =3

if int(age)<12:
    print(f"{age}歳が{num}人なので{calc(child, num):,}円を払え")
else:
    print(f"{age}歳が{num}人なので{calc(adult, num):,}円を払え")

Python Tips: Python でクロージャを使いたい - Life with Python
上のサイトを参考にさせていただいたのだけれど、

def gen_circle_area_func(pi):
    """円の面積を求める関数を返す"""
    def circle_area(radius):
        # ここの pi は circle_area_func に渡された pi が使われる
        return pi * radius ** 2

    return circle_area

# 円周率を 3.14 として円の面積を計算する関数を作成
circle_area1 = gen_circle_area_func(3.14)
print(circle_area1(1))  # => 3.14
print(circle_area1(2))  # => 12.56

# 次は、円周率の精度をぐっと上げて
# 円周率 3.141592 で円の面積を計算する関数を作成
circle_area2 = gen_circle_area_func(3.141592)
print(circle_area2(1))  # => 3.141592
print(circle_area2(2))  # => 12.56638

僕にはcircle_area1(1)で半径1がgen_circle_area_func(3.14)の中のradiusに引き渡される理屈が全くわからない。本当に直感で理解できない。こういうもんなの?

  • ラムダ式は lambda 引数:処理内容 と書く、例えば
lambda x,y:x+y

であれば、xとyが渡されるとx+yが実行される。

(lambda x,y:x+y)(1,2)

の様に書くことでx+y=1+2=3が実行される。要は、

def func(x,y):
    return x+y

を1行で書いたようなもん。じゃあ、だから何、って感じなのだけれど、lambda式でよく利用されるのがソートへの応用。

list = [[10,4],[3,6],[4,6],[5,0],[4,9],[2,0]]
list.sort(key=lambda x:(x[0],x[1])) #1番目でソートし、2番めでソート
print(list)

上の事例の場合はx:(x[0],x[1])と書くことで、まずは1列目x[0]でソートした上で、2列めx[1]をソートする。とりあえず、x:x[1]と書くことで、xがリストを指し、x[i]がリストのi+1番目の要素と認識される模様。この辺の間隔がマジで意味がわかんねえよ。マヂ無理。

  • map関数もラムダ式を利用する事のできる関数。map(関数, イテラブル)と書く。単純言うと、map関数を使うと、イテラブルとして与えられたリスト等の中の要素の全てに関数で記述した処理を行うことができる。例えば数のリストに対してその数をすべて2倍にする等。

>>||
>>> nums = [4,3,8,6,2]
>>> nums2= list(map(lambda x:x*2,nums))
>>> print(nums2)
[8, 6, 16, 12, 4]
|

新たな気づき(Chapter 10-2、10-3)

  • 関数の使用時に引数を明示的に書かない場合は定義の順序どおりに書く必要があるが、明示する限りにおいてはこの限りではない(キーワード引数という)。関数定義で以下のようにデフォルトの値を置いておくことも可能。
def calc(v=100,p=500):
    revenue = v*p
    return revenue
print(calc())           #50000
print(calc(200,500))    #100000
print(calc(p=100,v=10)) #1000
print(calc(p=1000))     #100000
  • def function(*args)の*argsは引数が複数存在し得るが何個かわからない場合に使う。argsでなくても*varsとかでも別に構わない。*がついてると引数の存在が不特定多数と意味する。定義の最中は*argsではなく*をとってargsになる。
def nozomi(start, end, *vias):
    stop_list =  [start]    #起点だけのリストを作成
    stop_list += list(vias) #経由地をリストに追加。ここは*viasではなくvias
    stop_list += [end]      #終点をリストに追加
    return stop_list
print(nozomi("Tokyo", "Shin-Osaka", "Shin-Yokohama","Nagoya", "Kyoto"))
# ['Tokyo', 'Shin-Yokohama', 'Nagoya', 'Kyoto', 'Shin-Osaka']
  • def function(**kwargs)の**kwargsは引数が辞書となる場合に使う。これもkwargsでなくても**dictとかでもかまわない。**がついてると引数が辞書であることを意味する。
def toyoko(length, stations, **dict):
    line_data = {"路線長":length, "駅数":stations}
    line_data.update(dict)
    print(line_data)
toyoko(24.2, 21, 起点="渋谷", 終点="横浜")
{'路線長': 24.2, '駅数': 21, '起点': '渋谷', '終点': '横浜'}
  • 自分で作成した関数やクラスを別のファイルに保存してある場合(これをモジュールと呼ぶ)、import filenameで読み込める。filename.pyのように拡張子を付ける必要はない。読み込んだ上でfilename.function()で関数を呼び出す。
  • モジュールを書き換えた場合それが反映されない場合は以下のコマンドで再読込がなされる。
>>> import importlib
>>> importlib.reload(filename)
from directory_name import module_name
module_name.function()

以下のように特定の関数だけを呼び出すことも可能で、この場合いちいちモジュール名を関数の前に追記する必要がない。

from directory_name.module_name import function
function()

新たな気づき(Chapter 10-1)

  • 関数の定義の章だけど、写経をしながら思いつきで幾つか拡張を加えてみたのが下のコード。数字を入力してもそれをいちいちコードで明示的に数字に変換するのが面倒なんだけど、どうにかならないのか…
from random import randint

def dice():
    num = randint(1,6)
    return num

def dicegame(num):
    cnt_even=0
    cnt_odd=0
    cnt_total=1
    try:
        num=int(num)
    except:
        print("普通整数を入力するだろ…どアホが…")
        return None
    for i in range(num):
        dice1 = dice()
        dice2 = dice()
        sum = dice1 + dice2
        if sum%2 == 0:
            print(f"試行{cnt_total}回目:1投目は{dice1}で、2投目は{dice2}で、合計は{sum}で偶数")
            cnt_even+=1
            cnt_total+=1
        else:
            print(f"試行{cnt_total}回目:1投目は{dice1}で、2投目は{dice2}で、合計は{sum}で奇数")
            cnt_odd+=1
            cnt_total+=1
    print(f"試行回数{num}回中、偶数は{cnt_even}回、奇数は{cnt_odd}回になりました。")

n=input("試行回数は?:")
dicegame(n)
  • 関数内で定義したローカル変数と同じ文字列を使った、関数外のグローバル変数は一致しない。
def calc(v,p):
    revenue = v*p
    print(v)
    return revenue

print(calc(100,50))
print(v)

を実行すると以下のような結果になる。

100                #calc関数内のprint(v)が実行されたもの
5000
Traceback (most recent call last):
  File "20180211_local_global.py", line 7, in <module>
    print(v)
NameError: name 'v' is not defined        #関数の定義の外のグローバルではvは定義されてないのでエラーになる。

一方で、

v=10
p=50

def calc():
    p=500
    revenue = v*p
    return revenue

print(calc())       #5000
print(v)            #10

となるが、また一方で、

v=10
p=50

def calc():
    v=100
    p=500
    revenue = v*p
    return revenue

print(calc())          #50000
print(v)               #10

となる。要するに、グローバルでしか定義のされてない変数はローカルにもそれが援用されるが、ローカルで定義されている場合はそれが優先される

  • 別の話では、
v = 2
def calc():
    v = v*110
    ans = v * 10
    print(ans)
calc()

を実行すると、以下のエラーが出る。

Traceback (most recent call last):
  File "20180211_local_global2.py", line 6, in <module>
    calc()
  File "20180211_local_global2.py", line 3, in calc
    v=v*100
UnboundLocalError: local variable 'v' referenced before assignment

直訳のとおりで、ローカル変数vが宣言される前に使われてしまっていることがエラーの原因。以下は通る:

v = 2
def calc():
    v_local = v*100
    ans = v_local * 10
    print(ans)
calc()

おっさん改めて考えている

スタートブックを放り投げ、みんPyを放り投げ、3冊目の入門書に手を出そうとしているが、これで先が見えない感じならもうスパッと諦めようと思う。2ヶ月弱やっているわけだが、おっさん的に進歩が感じられないというのがその理由。とりあえずはみんPyにつづいて、入門ノートは全部読みつくそうとは思うが。

新たな気づき(Chapter 9-2)

  • key in dict_aで要素が辞書にあるかどうかわかるし、dict_a.get(key)だと要素があれば値が返り、何もなければ何も返らず、エラーにはならない。
  • 東急線の路線名のリストを用意します。
>>> tokyu_lines=["toyoko","meguro","dento","oimachi","ikegami","tamagawa","kodomo"]

各路線の路線長のデータを用意します。

>>> tokyu_line_length=[24.2,11.9,31.5,12.4,10.9,5.6,3.4]

それをzipでつないで辞書にします。

>>> tokyu=dict(zip(tokyu_lines,tokyu_line_length))
>>> print(tokyu)
{'toyoko': 24.2, 'meguro': 11.9, 'dento': 31.5, 'oimachi': 12.4, 'ikegami': 10.9, 'tamagawa': 5.6, 'kodomo': 3.4}

上に作った辞書の値だけをvalue()メソッドで取り出し、最長路線の路線長を出します。他にもminも適用可能。

>>> tokyu.values()
dict_values([24.2, 11.9, 31.5, 12.4, 10.9, 5.6, 3.4])
>>> max(tokyu.values())
31.5

路線名だけをkeys()メソッドで抜き出し、世田谷線を入れ忘れていたのに気づいて加えます。

>>> tokyu.keys()
dict_keys(['toyoko', 'meguro', 'dento', 'oimachi', 'ikegami', 'tamagawa', 'kodomo'])
>>> tokyu["setagaya"]=5.0
>>> print(tokyu)
{'toyoko': 24.2, 'meguro': 11.9, 'dento': 31.5, 'oimachi': 12.4, 'ikegami': 10.9, 'tamagawa': 5.6, 'kodomo': 3.4, 'setagaya': 5.0}

路線名を全て小文字にしてしまっていたので内包表記を利用して大文字にします。

>>> tokyu_keys=[key.capitalize() for key in tokyu]
>>> tokyu_keys
['Toyoko', 'Meguro', 'Dento', 'Oimachi', 'Ikegami', 'Tamagawa', 'Kodomo', 'Setagaya']

items()メソッドを利用するとキーと値で構成されるタプルで構成されるdict_items型データに変換でき、これをlist()関数でリストに変換するとその後の利用がし易い。

>>> tokyu.items()
dict_items([('toyoko', 24.2), ('meguro', 11.9), ('dento', 31.5), ('oimachi', 12.4), ('ikegami', 10.9), ('tamagawa', 5.6), ('kodomo', 3.4), ('setagaya', 5.0)])
>>> list(tokyu.items())
[('toyoko', 24.2), ('meguro', 11.9), ('dento', 31.5), ('oimachi', 12.4), ('ikegami', 10.9), ('tamagawa', 5.6), ('kodomo', 3.4), ('setagaya', 5.0)]

dict_items型でfor-inを利用するとキーと値を取り出して利用しやすい。

>> for key,value in tokyu.items():
...     print(f"{key} line's length is {value}km")
... 
toyoko line's length is 24.2km
meguro line's length is 11.9km
dento line's length is 31.5km
oimachi line's length is 12.4km
ikegami line's length is 10.9km
tamagawa line's length is 5.6km
kodomo line's length is 3.4km
setagaya line's length is 5.0km

やっぱり鉄道線に限ったリストにしたいのでpop()メソッドを利用して世田谷線は削除します。

>>> tokyu.pop("setagaya")
5.0
>>> tokyu
{'toyoko': 24.2, 'meguro': 11.9, 'dento': 31.5, 'oimachi': 12.4, 'ikegami': 10.9, 'tamagawa': 5.6, 'kodomo': 3.4}

popitem()はランダムに要素を取り出し、元の辞書からは削除します。何に使うの?