わかったつもりのPython その3

python

 前回の続き。オライリーの読解です。O’Reilly Python 文法詳解

 普通に書いてたら理解できる文法の「きほんのき」は省いて「へぇ~」と思った所をピックアップしていきます。前回の続きで6章。細かい解説はしていません。「へぇ~」と思ったらそのキーワードでググるか本買ってください。

6章 関数とクラス

 ぼーっと生きていてもそれなりに情報が入ってくるこれらですが、やはり細かいところになってくるといろいろ新発見があるものです。避けては通れない関数とクラスに関して解説されています。

関数定義

 defで始まるブロックで関数が記述できますが、ここでまず最初の「へぇ」は定義した行に命令をそのまま続けて書けます。

def stub(): print("stub")

 あれだけコーディングスタイルを強制してくるのに、これは許容するんだなぁ~。と思いました。ちなみに関数は空にできないので、何もしない関数にはpass文を入れる必要があります。

 次の「へぇ」は関数の中でimportできてしまいます。そしてこのimportはその関数内でしか有効ではないローカルなものになります。

def func():
    import xx

 これは使えるかもしれない。

 最後の「へぇ」はreturn文を省略するとNoneが返る。以下3つはすべてNoneが返る空関数です。

def a(): return None
def b(): return
def c(): pass

 上で覚えた関数の1行記述で書いてみました。

可変長引数

 この機能もあるだろうなぁとは思っていましたがやはりありました。タプルで受ける方法と辞書で受ける方法があります。

 タプルで複数受け取るには、*付き引数で受け取ります。前章にあったfor文のイテラブルな値の分解ではリストとして入ってきたのですが、関数ではタプルのようです。なんでやろ?

def d(a, b, *c):
    print(a)
    print(b)
    print(c)

d(1,2,3,4,5,6,7,8)
----------
1
2
(3, 4, 5, 6, 7, 8)

 辞書で複数受け取るには、**付き引数になります。

def d(a, b, **c):
    print(a)
    print(b)
    print(c)

d(1,2,c = 3, e = 4, f = 5)
-----------
1
2
{'c': 3, 'e': 4, 'f': 5}

 ちなみにこの**付き引数は引数リストの最後にのみ指定が許されているようです。*付き引数は以下にあるように最後でなくてもOKです。

キーワード専用引数

 *付き引数の後ろの引数は必ずキーワード形式で値を指定する必要があります。まぁそうですよね。どこまで*で受けていいかわからないので。

def a(*b, c):
    print(b)
    print(c)

a(1,2,3,4,5,c=6)
----------
(1, 2, 3, 4, 5)
6

 どこまで引数bかわからなくなるのでcは明確に指定する必要があります。cを指定しないとエラーです。

a(1,2,3,4,5)
----------
TypeError: a() missing 1 required keyword-only argument: 'c'

 cだけ指定するとエラーはおきません。可変長引数は省略されて呼ばれることも想定しないといけないみたいですね。

a(c=6)
----------
()
6

 ちなみに*だけの、名前のない引数を指定すると、*よりも右側に指定された引数は、必ずキーワード引数として指定せよ。という指示になるようです。

def a(b, *, c):
    print(b)
    print(c)

# a(1,2) #こいつはエラー
a(1,c=2)
----------
1
2

 覚えるしかないやつです。

デフォルト引数

 これ自体はほかの言語でもよく見るので良いのですが、Pythonならではの注意点がありました。これは「へぇ~」です。

 デフォルト引数として変更可能なオブジェクト(リストとか)を指定したときに注意が必要です。デフォルト引数として指定されたオブジェクトは、関数を呼び出したときに新しく生まれる(呼び足した都度生まれる)わけではなく、関数オブジェクトを生成するときに一度だけその引数も生成されます。

 なんだか文章だとよくわかりませんが、コードにすると

def a(b = []):
    b.append(1)
    print(b)
    
a() # [1]
a() # [1, 1]
a() # [1, 1, 1]
a([0,1,2,3]) # [0, 1, 2, 3, 1]
a() # [1, 1, 1, 1]

 こんな感じ。1回目こそ想定通りの動きですが、2回目、3回目でデフォルト空リストじゃないの?って動きをします。4回目で全然違う引数で読んだ後の5回目でもこれまでのデフォルトリストは存命です。

 関数オブジェクトが生まれたときに一度だけ作られているので、こんなことになります。この動きが想定と違う場合にはちゃんと対処しないといけません。

 またfor文の中で複数回同じ関数と生成するような場合は、複数回このデフォルト引数も生まれます。関数オブジェクトの生成の都度デフォルト引数も生成されると理解しておく必要がありそうです。

ドキュメンテーション文字列

 もうこの章そのものが「へぇ~」でした。docstringとか呼ばれているやつです。関数の説明文を入れるものです。個人的にはdoxygen愛好家だったのですが、pythonはこちらが正解な気がしてきました。

 しかも使っているIDE(Spyder)だと関数に続けて「”””」をタイプするだけで自動的に関数のdocstringテンプレートを挿入してくれました。あぁ知らなかった。超便利

def a(b,c):
    """  # 以下はSpyderが自動生成

    Parameters
    ----------
    b : TYPE
        DESCRIPTION.
    c : TYPE
        DESCRIPTION.

    Returns
    -------
    TYPE
        DESCRIPTION.

    """
    return b+c

help(a) # これでdocstringを表示してくれる。

 関数の中に書かないといけないのがいまいち慣れませんが、今度からはこれを使おう。

関数アノテーション

 これもdocstringと似た感じの関数に対する説明文です。anotation:日本語にすると注釈です。一般的には型を書くようです。使ってるエディタでは型以外を書くと警告が出ますが、エラーなく実行できます。引数に続けて : 型 と書いてあげるとよいみたいです。あくまで注釈なので動作には影響しません。まったく違う型を代入しても問題なく動作します。型チェックするわけではないので。

 ちなみにここに型まで書いたうえで、docstringを挿入しようと思い “”” を打ったら、型まで含めたdocstringテンプレートを挿入してくれました。すげーぜSpyder。

def a(b:float, c:int = 1) -> list:
    """ # ここから下はSpyderが関数定義を見て勝手に作ってくれたdocstring
    
    Parameters
    ----------
    b : float
        DESCRIPTION.
    c : int, optional
        DESCRIPTION. The default is 1.

    Returns
    -------
    list
        DESCRIPTION.

    """
    
    return b+c

d = a(1,2)
help(a)

 ちなみにデフォルト引数はアノテーションの後に書くようです。まったくでたらめの型を書いていますが、問題なく動作します。

lambda式

 匿名関数。省略

モジュールとパッケージ

 モジュールとはPythonの関数やクラスをファイルで束ねたもの。この記述に関する解説の章です。

モジュールの内容

 ここでの「へぇ~」は、モジュールにもドキュメンテーション文字列が入れられ、ファイルの冒頭に挿入することでそのモジュールの説明を記述することができること。ファイルの先頭に”””があるのはそういう意味でした。そういえばSpyderさんも新規作成すると頭にこれつけてくれてました。

 ちなみにその記述はimportした先からhelpで教えてくれます。

import cv2
help(cv2)

 関数やら変数やらずらっと教えてくれます。Cでいうところのヘッダを生成してくれるようなイメージ。

 ちなみにその表示される関数の説明では前述のdocstringも表示されます。幸いなことにdoxygenの記述も表示されました。

グローバル変数とローカル変数

 ここの内容は過去はまったことがあるので残しときます。global宣言というやつです。コードを見てもらった方が早いです。

a = 100 # グローバルなa

def get_a():
    return a # これはグローバルなa

def set_la(c):
    a = c # これはローカルなa(結果何もしない関数)

def set_ga(c):
    global a
    a = c # これはグローバルなa

ga = get_a()
print(ga) # 100
    
set_la(200) # aにセットされない
print(a) # 100

set_ga(200) # aにセットされる
print(a) # 200

 関数の中でglobal変数に値を入れようと思ったら global宣言してやらないといけません。c言語な脳みそだとはまります。

import文

 モジュールの読み込み。モジュール名は*.pyの拡張子を除いたファイル名。モジュールの中の関数や変数へ、「モジュール名.オブジェクト名」でアクセスできます。

 モジュール名が長ったらしいときはasで別名がつけられます。numpy as npですね。

 from モジュール名 import オブジェクト名 のスタイルもよく見かけます。こちらだと「モジュール名.オブジェクト名」のうちモジュール名が省略できます。あとは小ネタみたいなものが色々書かれています。「ふ~ん」といった感じです。

 「へぇ~」だったのが、__all__というシーケンスの役割。確かに公開したくない変数や関数はありますものね。

__all__=['a', 'b']

def a(b, c): pass
def b(c, d): pass
def c(d, e): pass
def _d(e, f): pass

 __all__で公開するオブジェクトを限定しています。この場合aとbしか公開していないので、

from mod import *

a(1,2)
b(1,2)
c(1,2)  # NameError: name 'c' is not defined
_d(1,2)  # NameError: name '_d' is not defined

 cと_dは見えません。ただ…

import mod

mod.a(1,2)
mod.b(1,2)
mod.c(1,2)
mod._d(1, 2)

 これは全パスしてしまいました。隠しきれてない…。_で隠したつもりの関数もフツーに呼び出せてしまいます。なんかいまいちな感じだな…。

パッケージ

 パッケージはモジュールを格納するディレクトリ。複数モジュールを階層的に管理できます。__init.py__があるディレクトリはパッケージとしてインポートできます。詳細は割愛。

名前空間パッケージ

 Python3.3以降では__init.py__が存在しないディレクトリでもパッケージとしてインポートできるようになったようです。いきなり上の章を否定するような内容ですが、詳細は割愛。名前空間パッケージでググるとしましょう。

モジュールの実行

 フォルダに__main__.pyという名前のファイルを置いておくと、そのファイルを置いているディレクトリを実行すると、__main__.pyが実行される。「へぇ~」

 ZIPファイルに__main__.pyという名前のファイルを入れとくとZIP内の__main__.pyが実行される。「へぇ~」

__main__モジュール

 Python実行時に指定されたファイルは__main__という名前のモジュールとして読み込まれます。この指定は__name__という名前の変数に格納されているので、この値を見ることでimportされたのか、直で呼ばれたのか切り分けできます。

# modtest.py
print(__name__)
if __name__ == '__main__':
    print("in module")
# test.py
import modtest
print(__name__)

 modtest.pyを実行すると

__main__
in module

 test.pyを実行すると

motdest
__main__

 どうやらモジュールとしてimportされた時に__name__にはそのモジュール名が入るみたいです。

名前空間とスコープ

 変数や関数のスコープに関して記述があります。「ふ~ん」な感じです。読んでも忘れそうだし忘れてもよさそうな気がします。

ローカルスコープのネスト

 関数の中に関数が作れます。「へぇ~」

 関数の中の関数から、関数の変数に対して代入しようと思ったらnonlocal文ってので変数名を宣言します。先ほどのglobalと似た感じですね。外の関数の変数(内の関数的にはglobal)にアクセスする方法ですしね。「へぇ~」

a = 0 # global
def b():
    a = 10 # nonlocal    
    def c():
        nonlocal a # nonlocalなa
        # global a # globalなa
        a += 1
    c()    
    print("nonlocal : " + str(a))

b()
print("global : " + str(a))

 同じ変数名でも違うアクセス方法になります。3重とかしたらどうなるんだろう。

クラス

 やっと出てきました。クラス。C++やjavaのクラシックなクラスに関してなんとなく知ってる前提でPythonならではの「へぇ~」なポイントだけメモします。

属性

 インスタンスの属性値に外からいくらでも追加できちゃいます。「えっ?へぇ~」な感じ。

class a:
    c = 0    
    def b(self, a):
        self.c = a
        print(self.c)

i = a()
i.b(3)
i.d = 100 # 属性が追加できちゃう
print(i.d) # 追加した属性にアクセスできちゃう

 この記述はOKです。追加を抑制する記述__slots__が後で出てきます。

インスタンスの作成と解放

 __new__メソッドと__init__メソッドは別物です。

 どちらもコンストラクタ風に見えますが、__new__()はそのクラスのインスタンスを戻り値として返すための関数で、インスタンスが作られる前に呼ばれるそのクラスのオブジェクトのstaticな関数です。

 __init__()メソッドがどちらかといえばコンストラクタで、そのインスタンスの属性の初期化を行うことを目的としています。インスタンスが作られた直後に呼ばれるメソッドです。

 デストラクタに相当しそうなのが__del__()メソッドなのですが、インスタンスが消えるて呼び出されるタイミングつかみにくいのと、そもそも呼ばれないこともあるらしく、推奨しないそうです。「へぇ~」

継承

 基底クラスにある関数と同じ名前の関数を定義したら上書き。基底クラスの属性へはsuper().でアクセス。多重継承OK。なのでsuper()でアクセスするには複数の基底クラスのうち先に継承した方から順に探しに行きます。

 多重継承OKなのでややこしいですが、複数の基底クラスを継承する際に、基底と基底の基底を継承してしまうようなケースでは矛盾が生じて、エラーになります。あまりびっくりするような記述はなかったかな…。

プライベートメンバ

 これは記述のお約束で識別するようです。アンダーバーが2つ以上で始まり、末尾に2つ以上のアンダーバーがつかないものはプライベートメンバ。好きな名前でprivateなメンバは作れないみたい。

 また頑張ればプライベートメンバに外からアクセスできちゃいます。このあたりあまり厳密じゃないみたいです。

class a:
    def __private(self):
        print("private")
    def _not_private(self):
        print("not private")
    def __not_private__(self):
        print("not private")

b = a()
# b.__private() # 'a' object has no attribute '__private'
b._not_private()
b.__not_private__()
b._a__private() # privateにアクセスできちゃう

クラスブロックとスコープ

 ああややこしい。クラスブロックの中で関数の記述ができちゃいます。だけどスコープが届かないので、

class a:
    b = 100
    def twice(e): # メンバじゃない関数
        return e*2
    def twice2(self, e): # メンバ関数
        return e*2
    def getb(self):
#        self.b = twice(self.b) # これはダメ
#        self.b = self.twice(self.b) # これもダメ
        self.b = self.twice2(self.b) # これはOK
        return self.b
    b = twice(b) # これはOK
#    b = twice2(b) # これもダメ
        
c = a()
d = c.getb()
# d = c.twice(d) # これはダメ
print(d)

 こんなことになります。ややこしい。分かりにくい記述ですがルール的にはOKです。

インスタンスの名前空間

 これもややこしいのであまり多用したくないです。インスタンス毎に名前空間を有しています。なので属性のところにも記述がありましたが、インスタンスに属性を後付けできちゃいます。しかもその際、クラスの関数名と重複してもいい。というなんとも自由なルールになっています。

class a:
    c = 0    
    def b(self, a):
        self.c = a
        print(self.c)

i = a()
i.b(3)
i.d = 100 # 属性が追加できちゃう
print(i.d) # 追加した属性にアクセスできちゃう

i.b = 200 # 関数名と同名の属性を足せる
print(i.b) # そしてアクセスできちゃう。
# i.b() # そして関数は消えてしまう。(エラー)
del i.b # 追加した属性をdelすると
i.b(10) # 関数は復活

 あぁややこしい。この動的な属性追加を止める方法もあるようで、__slots__というキーワードで属性をあらかじめ宣言しておくと追加ができなくなります。

class a:
    __slots__ = {"c"}
    def b(self, a):
        self.c = a
        print(self.c)

i = a()
i.b(3)
# i.d = 100 # 属性が追加できなくなる

 ここまでややこしい記述しないかな…。でも出くわしても恐れないようにしましょう。

メタプログラミング

 何を言っているんだかさっぱりわからないのと、何のご利益があるのかわからないので、キーワードだけ。

デコレータ

 だいぶマニアックになってきました。しかしこのデコレーター。出くわしたことがあるので、ちゃんと読むことにします。

「関数を引数として受け取り、戻り値として関数を返す」関数を、ほかの関数でラップした新しい関数の作成や、関数のフレームワークやサービスへの登録などに利用できます。このような使い方をする関数を、デコレータと呼びます。

6章 6.5 デコレータ

 関数を引数として受け、関数を返す関数だそうです。@マークを付けて、こんな感じでデコレータを付けたbとcの関数を書くと

def a():
    print("in a")
def deco(func):
    print("in deco")
    return a
@deco
def b():
    print("in b")
@deco
def c():
    print("in c")
print("start")
b()
c()

 bとcの関数の中身は無視され、常にaの関数が呼ばれます。関数生成時にdecoの関数が呼ばれていることもわかります。

in deco
in deco
start
in a
in a

 @decoで書かれている関数に関数を渡してあげる。そこでの動作はdeco次第。って記述だと理解しました。

 何やら使い方はわかりましたが、使い道はよくわかりません。次のwrapper関数編を読むと少しわかった気になります。

ラッパ関数を作成するデコレータ

 デコレータのラッパ関数としての使い道が書かれています。なるほど確かにこれは便利かも

import time

def deco2(func):
    print("in deco2")
    def wrapper(t):
        start = time.time()
        ret = func(t)
        print(time.time() - start, "sec")
        return ret
    return wrapper

@deco2
def d(tt):
    time.sleep(tt)
    print("in d")

d(2)
d(1)

 関数の処理時間を計測して表示してくれる関数で呼び出し関数をWrappしています。なるほどこれは便利かも。

 実行結果は

in deco2
in d
2.00571608543396 sec
in d
1.0145235061645508 sec

 これはいいかも。

関数呼び出し形式のデコレータ

 もう何が何だかわかりません。デコレータを返すデコレータ。と理解しました。この関数をデコレータとして使ってね。ってことだと思います。これだとデコレータに引数が指定できるようになります。

def deco(func):
    return func
def decodeco(msg):
    print(msg)
    return deco
@decodeco("hello")
def a():
    print("in a")

print("start")    
a()

 これを実行すると

hello
start
in a

 だそうです。startの前に関数生成時の動作が走っています。

 この後、複数のデコレータ、functools.wrapsデコレータ、クラスデコレータと続きますが、マニアックだなぁ~。な内容だったので割愛。

 このデコレータですが、個人的にはfluskというライブラリを触ったときにお目にかかりました。ルーティングする関数にはデコレータがついていて、今思うとその複雑なルーティングの関数の中にユーザーコードを組み込む仕組みとして使われてたんだなぁ~。と思えるようになりました。

つづく

 7章・8章(最終)に続く。

python
スポンサーリンク
キャンプ工学

コメント

タイトルとURLをコピーしました