いろいろ備忘録日記

主に .NET とか Java とか Python絡みのメモを公開しています。

Pythonメモ-57 (サブクラスで __init__ を定義する場合の注意点) (class, super, __init__, Never code a method that just delegates to the superclass)

概要

たまに間違えてバグ作ってしまったりしてるので、忘れないようメモメモ。

python で サブクラスの __init__ 定義する場合に

super().__init__()

としていないと、親クラスの__init__() が呼ばれないよって話しです。

他の言語の場合 (例えば C#)

C#の場合

using System;

namespace ConsoleApplication1
{
    internal class BaseClass
    {
        internal int BaseValue { get; }

        public BaseClass()
        {
            BaseValue = 10;
        }
    }

    internal class DerivedClass : BaseClass
    {
        internal int DerivedValue { get; }

        public DerivedClass()
        {
            DerivedValue = 20;
        }
    }

    internal class Program
    {
        public static void Main(string[] args)
        {
            var x = new DerivedClass();
            Console.WriteLine($"Base={x.BaseValue}, Derived={x.DerivedValue}");
        }
    }
}

と記述して、実行すると

Base=10, Derived=20

って表示されます。つまり、サブクラス側で明示的に親クラスのコンストラクタを

呼んでなくても、この場合は暗黙で デフォルトコンストラクタ が呼ばれます。

python の場合

python の場合、__init__メソッドは厳密にはコンストラクタではないのですが

まあ、大体同じものだということで。以下のようなクラスを定義してみて

class B:
    """
    ベースクラス
    """

    def __init__(self) -> None:
        self.x = 10


class C(B):
    """
    サブクラス1

    自分の __init__ を定義していないバージョン
    """
    pass

使ってみると

        obj1 = C()
        pr('C.x', obj1.x)

以下のように表示されます。

C.x=10

予想通りの動作。次に

class D(B):
    """
    サブクラス2

    自分の __init__ を定義しているが
    super().__init__() を呼んでいないバージョン
    """

    # noinspection PyMissingConstructor
    def __init__(self):
        self.y = 20

を定義して

            obj2 = D()
            pr('D.x', obj2.x)
            pr('D.y', obj2.y)

ってすると

AttributeError("'D' object has no attribute 'x'",)

となります。サブクラス側が __init__ を定義しているので

親クラス側の __init__ がオーバーライドされます。 python は素直な言語なので

普通に上書きされます。なので、この場合 親クラスの__init__が呼ばれない となります。

で、以下のようにするのが正しいやり方。

class E(B):
    """
    サブクラス3

    自分の __init__ を定義していて
    super().__init__() も呼んでいるバージョン
    """

    def __init__(self):
        super().__init__()
        self.y = 20

呼び出すと

        obj3 = E()
        pr('E.x', obj3.x)
        pr('E.y', obj3.y)

ちゃんと表示されます。

E.x=10
E.y=20

まとめ

てことで、サブクラスで __init__定義するときは、必ず

super().__init__()

を呼び出しするべしってなります。

PyCharmのような統合環境使っている場合は自動で挿入してくれていたり

呼び出しを書いていなかった場合は警告が出たりしますので、呼び出し忘れることは

ほぼ無いのですが、エディタで書いてたりすると抜けてたりします。(しましたw

実行すると意味の分からないエラーパターンになるので焦ります。。

ちなみに、このパターンは __init__だけじゃなくて、どのメソッドでも当てはまります。

サンプル

"""
Python のクラスについてのサンプルです。

サブクラス側で __init__ を定義した場合の注意点について。
"""
from common.commoncls import SampleBase
from common.commonfunc import pr, hr


class B:
    """
    ベースクラス
    """

    def __init__(self) -> None:
        self.x = 10


class C(B):
    """
    サブクラス1

    自分の __init__ を定義していないバージョン
    """
    pass


class D(B):
    """
    サブクラス2

    自分の __init__ を定義しているが
    super().__init__() を呼んでいないバージョン
    """

    # noinspection PyMissingConstructor
    def __init__(self):
        self.y = 20


class E(B):
    """
    サブクラス3

    自分の __init__ を定義していて
    super().__init__() も呼んでいるバージョン
    """

    def __init__(self):
        super().__init__()
        self.y = 20


class Sample(SampleBase):
    """
    サンプルとなるクラスです。
    """

    def exec(self):
        """
        処理を実行します。
        """
        # ------------------------------------------------------------
        # (1) サブクラス側が __init__ を定義していない場合
        #     既定で、 super().__init__() が呼ばれた状態となる
        # ------------------------------------------------------------
        obj1 = C()
        pr('C.x', obj1.x)

        hr()

        # ------------------------------------------------------------
        # (2) サブクラス側が __init__ を定義しているが super().__init__() を
        #     呼んでいない場合、親クラスの __init__ は呼ばれない。
        #
        # 他の言語に慣れている場合、デフォルトのコンストラクタが呼ばれるのが暗黙的なので
        # よく間違えてしまう。注意が必要。
        #
        # PyCharmで作業している場合、IDE側が警告を出してくれるので気付ける。
        # ------------------------------------------------------------
        try:
            obj2 = D()
            pr('D.x', obj2.x)
            pr('D.y', obj2.y)
        except AttributeError as e:
            pr('D', e)

        hr()

        # ------------------------------------------------------------
        # (3) サブクラス側が __init__ を定義していて super().__init__() を
        #     呼んでいる場合、親クラスの __init__ も呼ばれる。
        # ------------------------------------------------------------
        obj3 = E()
        pr('E.x', obj3.x)
        pr('E.y', obj3.y)


def go() -> None:
    """
    サンプルを実行します。
    """
    obj = Sample()
    obj.exec()


if __name__ == '__main__':
    go()

try-python/cls03.py at master · devlights/try-python · GitHub

補足

この点については、書籍「Python in a nutshell」でも記載されていました。

Python in a Nutshell

Python in a Nutshell

書籍内では、

単純に親クラスの処理をデリゲートするだけのメソッドを書いてはいけません。
(Never code a method that just delegates to the superclass)

つまり、以下のようなものは定義したらダメだよってことですね。

class Derived(Base):
    def __init__(self):
        super().__init__()

過去の記事については、以下のページからご参照下さい。

サンプルコードは、以下の場所で公開しています。

Pythonメモ-56 (pygments で色付き cat コマンドみたいなの作る) (pygments, highlight, get_lexer_for_filename, TerminalFormatter)

概要

前回

devlights.hatenablog.com

ってことで、pygments ってライブラリを初めて使ってみたのですが

面白かったので、もう少し遊んでみました。

ドキュメント見ると、いろいろ関数があって、Lexer を推定して作ってくれる系のものがあります。

その中にget_lexer_for_filename という関数があったので

これで、「色付きのcatコマンドみたいなもの」を作ってみました。といっても大事な部分は

全部pygmentsさんにおまかせしているので、何もしていないのですがw

サンプル

以下、サンプルです。

"""
pygments に関するサンプルです。
pygments に備わっている ファイル名からの予想機能 をつかって 色付きcatコマンド みたいなのを実装しています。

usage:
  $ python -m trypython.extlib.pygments02 ファイルパス

  ファイルのエンコーディングを指定したい場合は以下のようにします。デフォルトは utf-8 です。

  $ python -m trypython.extlib.pygments02 ファイルパス --encoding euc-jp
"""
import argparse
import os

from pygments import highlight
from pygments.formatters.terminal import TerminalFormatter
from pygments.lexers import get_lexer_for_filename

from trypython.common.commoncls import SampleBase


class Sample(SampleBase):
    """
    pygments に関するサンプルです。
    pygments に備わっている ファイル名からの予想機能 をつかって 色付きcatコマンド みたいなのを実装しています。
    """

    def __init__(self, file_path: str, encoding: str) -> None:
        """
        オブジェクトを初期化します。

        :param file_path: ファイルパス
        :param encoding: エンコーディング
        """
        super().__init__()
        self.file_path = file_path
        self.encoding = encoding

    def exec(self) -> None:
        """
        処理を実行します。

        :return: なし
        """
        with open(self.file_path, mode='r', encoding=self.encoding) as in_fp:
            code = in_fp.read()

        lexer = get_lexer_for_filename(self.file_path)
        formatter = TerminalFormatter(bg='dark')
        result = highlight(code, lexer, formatter)

        print(result)


def go(file_path: str, encoding: str) -> None:
    """
    サンプルを実行します。

    :param file_path: ファイルパス
    :param encoding: エンコーディング
    :return: なし
    """
    if not os.path.exists(file_path):
        raise FileNotFoundError(f'target file is not found [{file_path}]')
    if os.path.isdir(file_path):
        raise ValueError(f'file_path should be file, NOT directory [{file_path}]')

    try:
        'helloworld'.encode(encoding)
    except LookupError:
        raise ValueError(f'invalid encoding [{encoding}]')

    obj = Sample(file_path, encoding)
    obj.exec()


if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument('file_path', type=str, help='target file path')
    parser.add_argument('--encoding', type=str, default='utf-8', help='file encoding (default: utf-8)')

    args = parser.parse_args()
    go(args.file_path, args.encoding)

try-python/pygments02.py at master · devlights/try-python · GitHub

動かしてみた

Windowsでcmder上で実行した結果が、こんな感じです。

f:id:gsf_zero1:20180118130053p:plain
python -m trypython.extlib.pygments02 /tmp/pytmp/hello.py

f:id:gsf_zero1:20180118130106p:plain
python -m trypython.extlib.pygments02 trypython/extlib/pygments02.py

いいんじゃないでしょうかね。Styleとか調整したらもうちょいましな色付きできるかも。

参考情報

pygmentsでcatとか調べてみたら、以下の情報がありました。すでに やってらっしゃる方もいらっしゃるんですね。

havelog.ayumusato.com

こちらのページでは less も出来るように調整されています。pygmentizeコマンドを使われています。


過去の記事については、以下のページからご参照下さい。

サンプルコードは、以下の場所で公開しています。

Pythonメモ-55 (pygments でシンタックスハイライトさせて遊ぶ) (pygments, pygmentize, syntax highlight)

概要

何かのライブラリをインストールしようとすると、ちょこちょこ見る名前って結構あります。

私の場合、pygments ってライブラリがよく出てきていたのですが

これが何なのか知りませんでした(恥

Welcome! — Pygments

とっても有名なライブラリなんですね。

何の機能を持っているかというと プログラムコードなどに対して シンタックスハイライト してくれるというものです。

よくあちこちのブログとかで見る、あのハイライト状態な出力を行ってくれます。

300以上の言語に対応しているとのこと。すごい!

出力も HTML画像 とかいろいろ選べる模様。

インストール

conda の場合は、いつものように

$ conda install pygments

で終わりです。Anaconda使っている場合は、最初からインストールされています。

基本的な使い方

とりあえず、ドキュメントをちょっと見てみたら、使い方としては以下のようにするみたい。

  • ハイライト対象のデータを用意する

  • 構文解析器 (Lexer) を選んで生成

  • 出力するもの (Formatter) を選んで生成

  • pygments.highlight 関数に渡して出力してもらう

みたいです。

以下のような感じ。python3の構文でHTMLでシンタックスハイライトする場合の基本的なやり方。

# データ 用意
code = """\
def hello():
    print('hello world')
    for x in range(10):
        print(x)
"""

# Lexer 用意
lexer = pygments.lexers.python.Python3Lexer()

# Formatter 用意
formatter = pygments.formatters.html.HtmlFormatter()

# 出力
html = pygments.highlight(code, lexer, formatter)

上のように直接 具象クラス を使ってもいいですが

ヘルパー関数があるのでそれ使ったほうが良さそう。

# データ 用意
code = """\
def hello():
    print('hello world')
    for x in range(10):
        print(x)
"""

# Lexer 用意
lexer = pygments.lexers.get_lexer_by_name('python3')

# Formatter 用意
formatter = pygments.formatters.get_formatter_by_name('html')

# 出力
html = pygments.highlight(code, lexer, formatter)

サンプル

以下、サンプルです。

"""
pygments に関するサンプルです。
最も基本的な使い方を記載しています。

- pythonからpygmentsの操作方法
- pygmentize コマンドの使い方(コメントで)
"""
import os
import subprocess

from pygments import highlight
from pygments.formatters import get_formatter_by_name
from pygments.lexers import get_lexer_by_name

from common.commoncls import SampleBase
from common.commonfunc import pr


class Sample(SampleBase):
    """
    pygmentsのサンプルです。
    pythonのコードをハイライト付きでhtmlに出力します。
    """

    def exec(self):
        """処理を実行します。"""
        # --------------------------------------------------------------------------------
        # pygments の基本的な使い方
        #   - http://pygments.org/
        #
        # pygments を利用する場合、必要なオブジェクトは以下の3つ
        #   - コード: ハイライト表示対象のコード
        #   - 解析器(lexer): コードを解析するためのオブジェクト
        #   - フォーマッタ: 最終的な出力を担当するオブジェクト
        #
        # lexer と formatter は、各実装クラスを直接つかってもいいが
        # 以下のヘルパー関数を使用しても取得できる
        #   - pygments.lexers.get_lexer_by_name()
        #     - 例えば、言語が python の場合は、"python" or "python3" となる
        #     - http://pygments.org/docs/lexers/
        #   - pygments.formatters.get_formatter_by_name()
        #     - 例えば、HTMLで出力したい場合は "html" となる
        #     - http://pygments.org/docs/formatters/
        #     - HtmlFormatterは便利だが、残念な事にHTML4形式で現状出力されてしまう。
        #
        # 必要なオブジェクトが揃ったら、後は pygments.highlight() に渡すと
        # 整形データが取得できる
        # --------------------------------------------------------------------------------
        code = """\
def hello():
    print('world')
    results = []
    for x in range(10):
        ressults.append(i for i in x)
        """
        lexer = get_lexer_by_name('python3')
        formatter = get_formatter_by_name('html', linenos=True, full=True, encoding='utf-8')

        # formatter に対して, 共通パラメータ [encoding] を指定した場合
        # 結果として受け取るデータの型は、bytes になる。指定していない場合、str となる。
        # HtmlFormatter にて encoding を指定しない場合、 charset の値が None となることに注意。
        html = highlight(code, lexer, formatter)
        if hasattr(html, 'decode'):
            html = html.decode('utf-8')
        pr('pygments.html', html)

        file_path = '/tmp/pygments_test.html'
        with open(file_path, 'w') as out_fp:
            out_fp.write(html)

        preserve_file = True
        try:
            subprocess.check_call(file_path, shell=True)
        finally:
            if not preserve_file:
                os.unlink(file_path)

        # --------------------------------------------------------------------------------
        # pygmentize コマンドの利用
        # pygments をインストールすると pygmentize という専用コマンドもインストールされる。
        # このコマンドを利用すると、pythonスクリプトを書かなくてもハイライト付きのデータが取得できる。
        #
        # 上のコードと同じような出力を得る場合は以下のようにする
        # $ pygmentize -f html -O linenos,full -o /tmp/pygments_test.html hello.py
        # --------------------------------------------------------------------------------


def go():
    """サンプルを実行します。"""
    obj = Sample()
    obj.exec()


if __name__ == '__main__':
    go()

try-python/pygments01.py at master · devlights/try-python · GitHub

pygmentize コマンド

pygmentsをインストールすると、pygmentizeというコマンドもインストールされます。

このコマンドは、いちいちスクリプト書かなくても、上のサンプルの動きと同じようなことをやってくれます。

さくっとハイライトした出力物を生成したい場合は、このコマンドでつくって渡した方が楽ですね。

上のサンプルと同じような出力を得る場合は、以下のようにします。

$ pygmentize -f html -O linenos,full -o /tmp/pygments_test.html hello.py

参考情報


過去の記事については、以下のページからご参照下さい。

サンプルコードは、以下の場所で公開しています。