Pythonでメソッドのオーバーロードをする

はじめに

Pythonにはメソッドのオーバーロードが標準にはありません.
これは型がないことに影響です.
型がないので呼び出すメソッドが判断できないということです.

通常の実装だと複数の型を受け取れるように関数内で工夫しているかと思います.
(if文で処理を分けてキャストしたりなど)
本質的な実装ではないので,個人的には避けたいです.

関数のオーバーロード自体はPython3.4から追加されたsingledispatchを使うことで実現できます.
これは型アノテーションを使うことを前提としています.

これは関数なのでメソッドではないです.
メソッドで実現したいことが多いので,singledispatchを拡張して実装しました.

実装

以下のデコレータ関数で実現します.

funcdispatch.py
from pathlib import Path
from typing import get_type_hints, _GenericAlias
from functools import singledispatch, update_wrapper

def funcdispatch(method: bool = True):
    """
    メソッド用のオーバーロード実装
    Args:
        method: メソッドかどうか
    """

    def dispach(func):
        # 通常のsingledispatchを取得
        dispatcher = singledispatch(func)

        # regist関数リスト
        funclist = [func]

        def register(cls, func=None):
            """
            registerのオーバーライド
            registされる関数を保持しておく
            """
            # typingの型ではsingledispatchのregister時にtypeと判定されずエラーになる
            # funcがNoneの際にエラーになるので,予めclsとfuncを取得して変換する
            clses = [cls]
            if func is None:
                ann = getattr(cls, '__annotations__', {})
                if ann:
                    func = cls
                    _, cls = next(iter(get_type_hints(func).items()))

                    clses = []
                    if isinstance(cls, _GenericAlias):
                        clses.extend(cls.__args__)
                    else:
                        clses.append(cls)
            # 関数登録
            for cls in clses:
                func = dispatcher.register(cls, func)   # singledispatchのregisterを呼んでおく
            funclist.append(func)
            return func

        def wrapper(*args, **kwargs):
            """
            ラップ
            registした関数から一致する型を見つける
            """
            # デフォルトは第1引数
            # インスタンスメソッドの場合はseldになる
            dispatch_class = args[0].__class__

            # キーワード引数のみで指定されている場合は,registされた関数からクラスを取得
            args_is_none = (not method and len(args) == 0) or (method and len(args) == 1)
            if args_is_none and len(kwargs.keys()) > 0:
                for func in funclist:
                    # 型ヒントから第2引数名を取得して,キーワード引数から値を取得
                    # selfには型ヒントが付いていない想定なので第2引数
                    argname, _ = next(iter(get_type_hints(func).items()))

                    # 値が存在しない場合は、次の関数を探索
                    if not argname in kwargs:
                        continue

                    # 値のクラスを取得
                    arg = kwargs.get(argname, None)
                    dispatch_class = arg.__class__
                    break

            # 通常の引数が指定されている場合は,メソッド種類に従い処理
            else:
                # インスタンスメソッドの場合は第2引数を取得
                if method:
                    dispatch_class = args[1].__class__

                # 派生クラスでは判定できないので、Pathは基底クラスに変換
                if issubclass(dispatch_class, Path):
                    dispatch_class = Path

            return dispatcher.dispatch(dispatch_class)(*args, **kwargs)

        wrapper.register = register
        update_wrapper(wrapper, func)
        return wrapper
    return dispach

selfを除く第一引数の型で判断しています.
一致するものがない場合は,一番最初に登録されたメソッドが呼び出されます.

テスト

メソッドをオーバーロード

使い方はsingledispatchと同じです.

from funcdispatch import funcdispatch

class Test:

    @funcdispatch()
    def test(self, value: int):
        print("intです", value)

    @test.register
    def _(self, value: str):
        print("strです", value)


if __name__ == "__main__":
    test_obj = Test()

    # 引数がint
    test_obj.test(5)

    # 引数がstr
    test_obj.test("method overload")

intです 5
strです method overload

関数をオーバーロード

funcdispatchの第一引数でメソッドか関数か指定できます(デフォルトはTrue=メソッド).

from funcdispatch import funcdispatch

@funcdispatch(method=False)
def test(value: int):
    print("intです", value)


@test.register
def _(value: str):
    print("strです", value)


if __name__ == "__main__":
    # 引数がint
    test(5)

    # 引数がstr
    test("method overload")

intです 5
strです method overload

まとめ

Pythonでメソッドをオーバーロードする方法を紹介しました.

欠点がいくつかあるので改善中です.

  • 第一引数でしか判断できない
  • インテリセンスで型情報が見えなくなる(overloadとの組み合わせで回避できないか検討中です)

型が違うなら挙動が違うから名前を変えろよ,というのがPythonの思想ですが...
どうしても名前を変えたくない・名前を考えるのが面倒という方にオススメです.

シェアする

  • このエントリーをはてなブックマークに追加

フォローする

ROSのCvBridgeをPython3で使う

はじめに

ROSで画像をMessageとしてPub/Subしたい場合は,CvBridgeを使うのが一般的だと思います.
しかし,Python3からCvBridgeを使おうとするとエラーになります.
前回,ROSノードをPython3で動かす方法を紹介しました)

CvBridge自体はPython2用にインストールされていて,Python3でROSノードを動かすとPython3でCvBridgeを動かすことになるので,エラーになります.
そこでCvBridgeをPython3用にビルドするのですが,ネットの記事だとエラーになったので,解決策を備忘録的に残します.

環境構築

Pythonはビルドイン・仮想環境どちらでも問題ないです.
(どちらかというと仮想環境向けです)

$ mkdir -p ~/cvbridge_build_ws/src
$ cd ~/cvbridge_build_ws/src
$ git clone -b melodic https://github.com/ros-perception/vision_opencv.git
$ cd vision_opencv
$ git checkout 578af4d6c7846876b3fc64512b1cc92a54894483
$ cd ../..
$ source /opt/ros/melodic/setup.bash
$ catkin config \
    -DCMAKE_BUILD_TYPE=Release \
    -DSETUPTOOLS_DEB_LAYOUT=OFF\
    -DPYTHON_EXECUTABLE={Pythonのパス} \
    -DPYTHON_LIBRARY={Pythonのlibパス} \
    -DPYTHON_INCLUDE_DIR={Pythonのincludeパス}
$ catkin config --install
$ catkin build cv_bridge
$ source install/setup.bash --extend

ex)
$ catkin config \
    -DCMAKE_BUILD_TYPE=Release \
    -DSETUPTOOLS_DEB_LAYOUT=OFF\
    -DPYTHON_EXECUTABLE=~/.anyenv/envs/pyenv/versions/3.9.1/bin/python \
    -DPYTHON_LIBRARY=~/.anyenv/envs/pyenv/versions/3.9.1/lib/libpython.3.9.so \
    -DPYTHON_INCLUDE_DIR=~/.anyenv/envs/pyenv/versions/3.9.1/include/python3.9

Checkoutしてバージョンを戻していますが,最新でも問題ないと思います.

開発の際の注意点

元々入っているPython2のCvBridgeを削除しない場合,
PYTHONPATHの順番に注意しないとビルドしたCvBridgeが使われません.

ROS本体の順番が先だとPython2のCvBrdigeが使われます.

この順番だとダメ(root直下にワークスペースを作ってるので以下のようになっているので,順番だけ見てください)
$ echo $PYTHONPATH
/opt/ros/melodic/lib/python2.7/dist-packages:/cvbridge_build_ws/install/lib/python3.9/site-packages

解決策としては,以下.

  1. PYTHONPATHの先頭に明示的に追加する
  2. sourceの順番を変える

1.PYTHONPATHの先頭に明示的に追加する

こちらは2回記述されてすっきりしないのと,site-packagesまで記載しないといけないので面倒です.

$ export PYTHONPATH=~/cvbridge_build_ws/install/lib/python3.9/site-packages/$PYTHONPATH

2.sourceの順番を変える

ROS(関連)のsetup.shの中を見ると,自身のPYTHONPATHをPYTHONPATHの先頭に追加しています.
なので,ROS本体のsetup.bash(sh)より後にCvBridgeのsetup.bash(sh)を実行すれば解決します.

Dockerfileで環境を作っている場合は,
bashrcに記載するかentrypointで順番をコントロールすれば面倒はないかと思います.

まとめ

CvBridgeをPython3から使う方法を紹介しました.
ご参考になれば幸いです.

シェアする

  • このエントリーをはてなブックマークに追加

フォローする

ROSノードをPython3で動かす

はじめに

最近ROS Melodicを触っています.
ROS1ではPython2が標準になっていて,普段Python3を使っている身からするとやりにくいです.
(サポートも切れていて時流に沿ってないですしね)

しかし,ROS1でしかサポートされていないノードも多々あると思うので,
ROS1が避けられないこともあるかと思います.
(OpenVSLAMなど ← develop版ではROS2をサポートとしているようですが)

そこで,ROS本体はPython2で動かしつつ,ROSノードはPython3で動かす方法を取りました.
備忘録的に残しておきます.

ROSをPython3用にビルドする方法も探すと出てきますが,
時間がかかったりうまくいかなかったりするのでこの方法が簡単です.

環境構築

ROSインストール

以下を実行してROSをインストールします.
ROS Wikiの通りです)

ROSのインストール準備
$ apt-get update
$ apt-get install -y dirmngr gnupg2
$ apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C1CF6E31E6BADE8868B172B4F42ED6FBAB17C654
$ echo "deb http://packages.ros.org/ros/ubuntu bionic main" > /etc/apt/sources.list.d/ros1-latest.list

ROSのインストール(Desktop Fullをインストールしています)
$ apt-get install -y ros-melodic-desktop-full
$ apt-get install -y -essential python-rosdep python-rosinstall python-vcstools

rosdep初期化&更新
$ rosdep init
$ rosdep update --rosdistro melodic

Python×ROS

Python自体の環境構築は省きますが,ビルドインのPythonでも問題ありませんし,pyenvなどの仮想環境でもOKです.
PythonにROSのコマンドをインストールします.
catkin_toolsはpipでインストールすると使うに際エラーが出るので,gitから直接インストールします.

$ pip install trollius rosdep rospkg rosinstall_generator rosinstall wstool vcstools catkin_pkg
$ pip install git+https://github.com/catkin/catkin_tools

Catkinワークスペースの作成

Catkinワークスペースを作る際に使用したいPythonを指定します.

$ mkdir -p ~/catkin_ws/src
$ cd ~/catkin_ws
$ catkin_make -DPYTHON_EXECUTABLE={Pythonのパス}
$ source devel/setup.sh

ex)
$ catkin_make -DPYTHON_EXECUTABLE=~/.anyenv/envs/pyenv/versions/3.9.1/bin/python

Dockerfileの場合は,ここはentrypointにしておくと良いです.
(sourceの部分はbashrcに書くようにするなど工夫する)

開発

rosrunで実行すると,Python3で実行したいノードはPython2から起動されますが,
#!(シバン)を書いておくことでPython3でノードを実行することができます.

#!/usr/bin/env python
import rospy
from std_msgs.msg import String

def talker():
    rospy.init_node('talker')
    word = rospy.get_param("~content", "default")
    pub = rospy.Publisher('chatter', String, queue_size=10)

    r = rospy.Rate(1) # 10hz
    while not rospy.is_shutdown():
        str = "send: %s" % word
        rospy.loginfo(str)
        pub.publish(str)
        r.sleep()

if __name__ == '__main__':
    try:
        talker()
    except rospy.ROSInterruptException:
        pass

まとめ

ROSノードをPython3で実行するための環境構築に関して書きました.
特にしがらみがないのであればROS2を選択するのがベターな気がします.

シェアする

  • このエントリーをはてなブックマークに追加

フォローする