Smile Engineering Blog

ジェイエスピーからTipsや技術特集、プロジェクト物語を発信します

リンクを Markdown 形式でクリップボードにコピー

はじめに

Markdown で執筆中、サイトのリンクを貼り付けたいときありますよね、きっと。そんなときは、chrome の拡張 Markdown Linker が便利です。

chrome ウェブストアより:

chrome 拡張なので chrome 限定のお話です。ごめんなさい

続きを読む

画像を ASCII 表示させる方法

はじめに

テストに用いるデータなど、サーバ上にあるイメージ(画像)を確認したいときないですか?このファイルなんだっけ?みたいな。そんなとき、jp2a を使えば画像を ASCII 表示できます。テキストで表示されるので、手軽にコンソールからも確認できるってことです。

Talinx/jp2a: Converts jpg/png images to ASCII

続きを読む

Pythonで読み取り専用コレクションを実装する

今回は、Pythonで読み取り専用コレクションのメンバ変数を実装する方法を解説します。

概要

通常、読み取り専用のメンバ変数を実装するときは、メンバ変数をプライベートにしつつ、getterのみ宣言して、setterを宣言しないようにします。

class Name:
    def __init__(self):
        self._firstname = 'Fugataro'

    @property
    def firstname(self):
        return self._firstname

name = Name()
print(name.firstname)
name.firstname = 'Hogemi'

# Fugataro
# AttributeError: can't set attribute

注意が必要なのは、listdictなどのコレクション型変数をメンバ変数とする場合です。

上記と同様に、getterのみ宣言して、setterを宣言しないことにより、コレクション型のメンバ変数を別のコレクション型変数に置き換えることは出来ません。

class NumbersList:
    def __init__(self):
        self._data = [1, 2, 3]

    @property
    def data(self):
        return self._data

numbers = NumbersList()
print(numbers.data)
numbers.data = [4, 5, 6]

# [1, 2, 3]
# AttributeError: can't set attribute

しかし、append()remove()などでコレクション型メンバ変数の中身を書き換えることが出来てしまっています。これは、getterのみの宣言により、メンバ変数そのものの置き換えは出来ないようになりましたが、コレクション型メンバ変数の関数はいまだ機能しているため、結果としてそれら関数により、中身の書き換えが出来てしまっているのです。

class NumbersList:
    def __init__(self):
        self._data = [1, 2, 3]

    @property
    def data(self):
        return self._data

numbers = NumbersList()
print(numbers.data)
numbers.data.append(4)
numbers.data.extend([5, 6])
numbers.data.remove(1)
print(numbers.data)

# [1, 2, 3]
# [2, 3, 4, 5, 6]

通常、この書き換えを防ぐために、別途用意されている読み取り専用のコレクションクラスを利用します。

Pythonの場合、読み取り専用のコレクションクラスは用意されていません。しかし、基本的なコレクションであるlistdictsetに対して、以下を用いることにより、代用可能です。

通常 読み取り専用
list tuple
dict types.MappingProxyType
set frozenset

実装方法

listdictsetに対して、対応する代用のコレクションにてキャストすればよいです。以下にサンプルソースを記載します。

tuple

class ReadOnlyList:
    def __init__(self):
        self._data = tuple([1, 2, 3])

    @property
    def data(self):
        return self._data

l = ReadOnlyList()
print(l.data)

print(l.data[0])
print(l.data[1])
print(l.data[2])

for i in l.data:
    print(i)

l.data.append(4)

# (1, 2, 3)
# 1
# 2
# 3
# 1
# 2
# 3
# AttributeError: 'tuple' object has no attribute 'append'

types.MappingProxyType

import types

class ReadOnlyDict:
    def __init__(self):
        d = {'msg': 'Hello,World!', 'param': 1}
        self._data = types.MappingProxyType(d)

    @property
    def data(self):
        return self._data

d = ReadOnlyDict()
print(d)

print(f'msg: {d.data["msg"]}')
print(f'param: {d.data["param"]}')

for key in d.data:
    print(key)

for value in d.data.values():
    print(value)

for key, value in d.data.items():
    print(key, value)

d.data['name'] = 'Foge'

# <__main__.ReadOnlyDict object at 0x0000024B239D7D88>
# msg: Hello,World!
# param: 1
# msg
# param
# Hello,World!
# 1
# msg Hello,World!
# param 1
# TypeError: 'mappingproxy' object does not support item assignment

frozenset

class ReadOnlySet:
    def __init__(self):
        s = set((1, 2, 3))
        self._data = frozenset(s)

    @property
    def data(self):
        return self._data

s = ReadOnlySet()
print(s.data)

for i in s.data:
    print(i)

s.data.add(4)

# frozenset({1, 2, 3})
# 1
# 2
# 3
# AttributeError: 'frozenset' object has no attribute 'add'

Pythonで抽象クラスを実装する

今回は、Pythonで抽象クラスを実装する方法を解説します。

抽象クラスとは?

プログラミング言語によりある程度違いはありますが、抽象クラスとは、以下の特徴を持ったクラスになります。

  • 継承されることを前提としています。
  • 空実装のメソッドが定義されています。(これを抽象メソッドと呼びます。)
  • このクラスを継承したクラスは、抽象メソッドを再定義(オーバーライド)しなければなりません。

よって、抽象クラスは子クラスに対してメソッドの定義を強制する機能を持っています。

用途としては、多態性ポリモーフィズム)を実装するときに、メソッドの実装漏れ、引数または戻り値の違いによるエラーの回避、などがあります。

実装方法

ここでは、抽象クラスと抽象メソッドの実装方法を解説します。

まず、抽象クラスの実装方法はABCMetaというメタクラスを用います。抽象クラスにしたいクラスのメタクラスABCMetaを設定することにより、そのクラスが抽象クラスになります。(ちなみに、ABCとはAbstract Base Classの略だそうです。)

from abc import ABCMeta

class vehicle(metaclass = ABCMeta):
    pass

また、PythonにはABCというクラスがあり、このクラスを継承することでも抽象クラスを実現することが可能です。

from abc import ABC

class vehicle(ABC):
    pass

このABCの実態は、ABCMetaメタクラスに設定しているだけのクラスです。よって、初めのコードと実質同じです。

次に抽象メソッドを実装するときは、@abstractmethodデコレータを使用します。このデコレータを付与されたメソッドが抽象メソッドになります。抽象メソッドには実際の処理を記述しないので、passまたはraise NotImplementedError()を実装しておきます。(NotImplementedError()とは、未実装エラーを表す例外です。)

from abc import ABCMeta
from abc import abstractmethod

class vehicle(metaclass = ABCMeta):
    """抽象クラスvehicleの定義。"""
    @abstractmethod
    def start(self):
        raise NotImplementedError()

    @abstractmethod
    def stop(self):
        raise NotImplementedError()

ABCMetaには、抽象メソッドが実装されている場合にインスタンスを生成しようとすると、例外が発生する仕様になっています。よって、上記vehicleインスタンスを生成しようとすると、以下のように例外が発生します。

v = vehicle()

# TypeError: Can't instantiate abstract class vehicle with abstract methods start, stop

なお、ABCMetaメタクラスとしているが抽象メソッドが実装されていないクラスや、抽象メソッドは実装されているがABCMetaメタクラスとしていないクラスのインスタンスは生成出来てしまいます。抽象クラスを実装するときは、ABCMetaメタクラスにすることと抽象メソッドの実装をセットで行ってください。

以下のように、抽象クラスを継承して抽象メソッドをオーバーライドすれば、インスタンスの生成が出来るようになります。

from abc import ABCMeta
from abc import abstractmethod

class vehicle(metaclass = ABCMeta):
    """抽象クラスvehicleの定義。"""
    @abstractmethod
    def start(self):
        raise NotImplementedError()

    @abstractmethod
    def stop(self):
        raise NotImplementedError()

class car(vehicle):
    """vehicleを継承したクラスcarの定義。"""
    def start(self):
        print("car start.")

    def stop(self):
        print("car stop.")

class motorcycle(vehicle):
    """vehicleを継承したクラスmotorcycleの定義。"""
    def start(self):
        print("moto start.")

    def stop(self):
        print("moto stop.")

my_car = car()
my_car.start()
my_car.stop()

my_motorcycle = motorcycle()
my_motorcycle.start()
my_motorcycle.stop()

# car start.
# car stop.
# moto start.
# moto stop.

抽象メソッドを表すデコレータについては、@abstractmethod以外にも存在します。ここでは詳細を省きますが、抽象メソッドを表すデコレータを以下に列挙しておきます。

  • @abstractmethod
  • @abstractclassmethod
  • @abstractstaticmethod
  • @abstractproperty

ちなみに、誤って抽象クラスのインスタンスを生成してしまっているコードをflake8にかけてみましたが、こちらではなにも表示されませんでした。抽象クラスのインスタンス生成エラーは実行時にしかわからないようです。

参考

【Android】コンストラクタに引数があるViewModelを使う

AndroidでViewModelを使用するとき、以下のようなコンストラクタに独自の引数があるViewModelを使用したいときがあります。今回は、その方法を解説します。

class MainViewModel(id: Int) : ViewModel()

独自Factoryの定義

ViewModelインスタンスの生成はViewModelのコンストラクタを直接呼び出すのではなくて、以下のようにFactoryクラスのcreateメソッドを呼び出して生成します。

if (mFactory instanceof KeyedFactory) {
    viewModel = ((KeyedFactory) mFactory).create(key, modelClass);
} else {
    viewModel = mFactory.create(modelClass);
}

よって、まずはコンストラクタに引数があるViewModelに対応したFactoryクラスを定義します。

class MainViewModel(id: Int) : ViewModel() {

    class Factory(
        private val id: Int
    ) : ViewModelProvider.NewInstanceFactory() {

        @Suppress("UNCHECKED_CAST")
        override fun <T : ViewModel> create(modelClass: Class<T>): T {
            return MainViewModel(id) as T
        }
    }
}

独自Factoryの使用

独自に定義したFactoryクラスは、そのインスタンスをViewModelProviderに渡す必要があります。ViewModelProviderのコンストラクタの中に、Factoryインスタンスを引数に持つコンストラクタがオーバーロードされているので、以下のようにそのコンストラクタを使用します。

class MainActivity : AppCompatActivity(R.layout.main_activity) {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val viewModel = ViewModelProvider(
            this, MainViewModel.Factory(0)
        ).get(MainViewModel::class.java)
    }
}

以上のようにして、コンストラクタに引数があるViewModelを使用できます。

キー入力をスクリーンキャストする

はじめに

前回の記事「git worktree と peco」の実演(gif アニメ)にて、キー入力を表示するために [screenkey] というソフトウェアを使いました。使用するにあたり、少しコツ(バグフィックス?)が必要だったので、備忘のためここに記します。

[screenkey] のサイトより:

なお、今現在 [screenkey] は Wayland 環境では動作しないので、使用する場合は Xorg 環境で使用します。

続きを読む

【Android】WorkManagerでバックグラウンド処理を行う

今回は、AndroidWorkManagerでバックグラウンド処理を行う方法を解説します。

概要

WorkManager を使用すると、処理をバックグラウンドで実行できます。WorkManager を使用すると、以下のメリットがあります。

  • 完了に時間がかかる処理でもアプリが止まらない。
  • アプリを終了しても WorkManager の処理は継続する。

よって、WorkManager は以下のような場合に使われます。

  • ファイルのダウンロード/アップロード
  • ログのアップロード

ライブラリを追加する

WorkManager を使用するため、以下のようにライブラリを追加します。

dependencies {
    val work_version = "2.7.1"
    // Kotlin + coroutines
    implementation("androidx.work:work-runtime-ktx:$work_version")
}

作業を定義する

まず、WorkManager に処理させたい作業を定義します。Worker を継承して、doWork をオーバーライドします。そして、オーバーライドした doWork の中で、WorkManager に処理させたい作業を実行します。

class HeavyWorker(
    appContext: Context, workerParams: WorkerParameters
) : Worker(appContext, workerParams) {

   override fun doWork(): Result {

       // 重い処理
       doHeavyWork()

       // 処理結果の返却
       return Result.success()
   }
}

処理の後に処理結果を返します。なお、返せる処理結果を以下の通りです。

  • Result.success() : 作業成功。
  • Result.failure() : 作業失敗。
  • Result.retry() : 作業をリトライする。

WorkRequestを作成する

次に、さきほど定義した作業から WorkRequest を作成します。WorkRequest の作成方法は、以下の 2 通りあります。

val heavyWorkRequest: WorkRequest =
    OneTimeWorkRequestBuilder<HeavyWorker>().build()
val heavyWorkRequest = OneTimeWorkRequest.from(HeavyWorker::class.java)

WorkRequestを登録する

最後に、WorkManager に WorkRequest を登録します。登録されると、WorkRequest の作業がバックグラウンドで実行されます。

WorkManager
    .getInstance(context)
    .enqueue(heavyWorkRequest)

参考

WorkManager の概要