0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Python初心者の備忘録 #04

Last updated at Posted at 2023-07-09

はじめに

今回私は最近はやりのchatGPTに興味を持ち、深層学習について学んでみたいと思い立ちました!
深層学習といえばPythonということなので、最終的にはPythonを使って深層学習ができるとこまでコツコツと学習していくことにしました。
ただ、勉強するだけではなく少しでもアウトプットをしようということで、備忘録として学習した内容をまとめていこうと思います。
この記事が少しでも誰かの糧になることを願っております!
※投稿主の環境はWindowsなのでMacの方は多少違う部分が出てくると思いますが、ご了承ください。
最初の記事:Python初心者の備忘録 #01
前の記事:Python初心者の備忘録 #03
次の記事:Python初心者の備忘録 #05

今回の記事はオブジェクト指向ついてまとめてあります。

■学習に使用している資料

Udemy:米国AI開発者がゼロから教えるPython入門講座

■オブジェクト指向(Object Oriented Programming)

▶オブジェクト指向とは

プログラミングを行う上でとても重要は考え方や概念で、さまざまな言語がオブジェクト指向を取り入れた言語でPythonもその一つ。
「なんのオブジェクトなのかわかれば、そのオブジェクトで使える機能がわかる」というのが基本的な考え方。
例えば、車というオブジェクトがあったときにその車オブジェクトの詳細が「テスラ」であれ、「プリウス」であれ、それが車と分かれば「アクセルを踏めば前に進む」、「ブレーキを踏めば止まる」ということがわかる(機能が使える)、といった感じ。
オブジェクトはClassと呼ばれる設計図から生成されたインスタンスのことで、ややこしいがオブジェクト=インスタンスである。
オブジェクト指向ではClassという設計図にある程度決まった・共通する機能や属性(パラメーター)を用意しておき、その設計図からそれぞれ違った特徴を持ったインスタンスを作成することで、大幅にコーディングの量や管理のしやすさを向上させるというのが目的。

▶クラス(Class)

クラスとはオブジェクトを生成する際に参照する設計図のようなもの。
クラスにはオブジェクトの属性を表すParamatorや機能(関数)を事前に設定することができる。
クラスを宣言する際は最初にclass <ClassName>:と書くことで<ClassName>というクラスを定義することができる。
classでは最初に初期化メソッドという特殊な関数を定義することができ、定義したclassを使用して、インスタンスを生成する際に最初に実行される関数となる。このメソッドでインスタンス生成時にParamatorを設定することができる。
インスタンスを生成する際は<ClassName>(param1, param2, param3, ...)とコードする。
インスタンス内のParamatorやメソッドにアクセスするにはインスタンスの後に.をつけてParamatorやメソッドを後に続けることでアクセスすることができる。
Classで定義したメソッド内でParamatorを使用したい場合は、メソッドを定義する会にselfを引数に設定してあげる。
そうすることで、self.paramとすることでインスタンスのParamatorをメソッド内で使用することができる。

# ClassNameはキャメルケース
# ()で継承するクラスを設定できる、すべてのClassはobjectクラスを継承しているので省略してもいい
class Person(object):
    # 初期化メソッド
    def __init__(self, name, age, gender):
        # Paramator
        self.name = name
        self.age = age
        self.gender = gender

    def walk(self):
        print(f"{self.name} is walking")


# インスタンスの生成
john = Person("John", 28, 'male')
taro = Person("Taro", 18, 'male')
emma = Person("Emma", 40, 'female')

# インスタンス変数: インスタンスに紐づいている変数
# インスタンスに「.」を続けてアクセス可能
print(john.name)
john.walk()

Challenge

CarクラスとCarオブジェクトのインスタンスを作成
条件
  Carクラスの属性:車名、燃費、製造会社
  Carクラスのメソッド:gas()、breakes() → アクセルとブレーキ
            (いくつかの属性をprint()するようなメソッド)

解答例
class Car(object):

    def __init__(self, model_name, mileage, manufacturer):
        self.model_name = model_name
        self.mileage = mileage
        self.manufacturer = manufacturer

    def gas(self):
        print("{0.manufacturer}の{0.model_name}(燃費:{0.mileage}),アクセル全開!!".format(self))

    def brakes(self):
        print(f"{self.manufacturer}{self.model_name}(燃費:{self.mileage}),ブレーキ!!!")


# 継承の際に実行されないように__name__で条件分岐(Challenge的にはなくてもいい)
if __name__ == "__main__":
    conti_gt = Car("Bentley Continental GT", 4, "Bentley")
    prius = Car("Prius", 20, "TOYOTA")

    conti_gt.gas()
    prius.brakes()

▶インスタンス変数(instance variable)とクラス変数(class variable)

インスタンス変数は前述の通り、それぞれのインスタンスごとに固有の変数で初期化メソッド内で定義されているParamatorのことで、クラス変数はClassに属するParamatorで、そのClassから生成されたインスタンス全体で共有する変数のこと。
インスタンスAでクラス変数が更新された場合、インスタンスB、インスタンスCにもその変更が反映される。
クラス変数はclassのトップレベルに定義する。
クラス変数にアクセスする際は<class>.<class variable>というようにアクセスする。
<instance>.でアクセスすることもできるが基本しない。

class Person(object):

    # クラス変数はclassのトップレベルに定義
    num_legs = 2
    count = 0

    def __init__(self, name, age, gender):
        self.name = name
        self.age = age
        self.gender = gender
        Person.count += 1

    def walk(self):
        print(f"{self.name} is walking.. with {Person.num_legs} legs!")


john = Person("John", 28, 'male')
taro = Person("Taro", 18, 'male')
emma = Person("Emma", 40, 'female')

# インスタンス変数: インスタンスに紐づいている変数
# インスタンスに「.」を続けてアクセス可能
print(john.name)
john.walk()
taro.walk()

# インスタンス変数の上書き
print(f"変更前:{john.age}")
john.age = 29
print(f"変更後:{john.age}")
# 当然他のインスタンスのageには影響はない
print(f"{taro.name}のageは{taro.age}のまま")

# クラス変数はインスタンス間で共有する
print(john.num_legs)
print(taro.num_legs)

# クラス変数は<Class名>.に続けてアクセスできる
print(Person.num_legs)

# クラス変数をクラス経由で変更すると,
print("Person.num_legs = 3 を実行")
Person.num_legs = 3
# 全インスタンスでそれが共有される
print(john.num_legs)
print(taro.num_legs)
# インスタンス経由で更新すると,そのインスタンスの変数だけが更新される
# ※他の言語と挙動が異なるので注意.またクラス変数をインスタンス経由で更新することは普通はしない
# warningもでないので注意.バグのもとになる
john.num_legs = 4
print("john.num_legs = 4を実行")
print(john.num_legs)
print(Person.num_legs)
print(taro.num_legs)

print(f"create {Person.count} persons!!")

Challenge

銀行口座のAccountクラスの作成
条件
  属性:残高、口座名、口座番号
    ・口座番号はAccountが作成された順番に連番を振る(0、1、2、3、...)
  メソッド:預入、引出
    ・預金が引出額より少ない場合は引き出せないようにする
    ・残高が変更されたら楮名、口座番号、残高を表示する

解答例
class Account:

    count = 0

    def __init__(self, name, balance):
        self.name = name
        self.balance = balance
        self.account_number = Account.count
        self.show_balance()
        Account.count += 1

    def withdraw(self, amount):
        if amount <= self.balance:
            self.balance -= amount
            self.show_balance()
        else:
            print(f"残高({self.balance}円)が足りません")

    def deposit(self, amount):
        self.balance += amount
        self.show_balance()

    def show_balance(self):
        print(f"{self.name}(口座番号:{self.account_number})の残高は{self.balance}円です")


myaccount = Account(name='my account', balance=0)
myaccount.deposit(10000)
myaccount.withdraw(5000)

▶スタティックメソッド(static method)

インスタンスに紐づかないメソッドのことで、classに紐づくメソッド。
(後述のクラスメソッドとも少し違うもの)
@staticmethodデコレーターを使用することで定義することができる。
インスタンス内の情報は使用しないので、引数にselfを設定する必要はなく、<Class>.の形でアクセスする。
<instance>.の形でもアクセスすることもできるが、インスタンス内の情報を使用しないのに、インスタンスからcallするのは無駄なので、使用しないようにする。

class MyClass():

    def mymethod(self):
        MyClass.mystaticmethod()
        print("This is normal method!")

    # static methodは引数にselfを取らず,インスタンスには紐づかない
    @staticmethod
    def mystaticmethod():
        print("This is staticmethod")


c = MyClass()
# 普通のmethodはインスタンスに紐づいている
c.mymethod()
# static methodは通常クラスからアクセスする
MyClass.mystaticmethod()
# インスタンスからstatic methodをcallすることもできるが,これは無駄なのでやらない
c.mystaticmethod()

Challenge

前回のChallengeの際に作成したAccountクラスに、取引(transaction)を記録する仕組みを追加する。
  取引に保持する情報
    ・預入/引出の金額
    ・新しい残高
    ・日時
  取引はList型で保持し、日時を取得するメソッドはスタティックメソッドで作成する。

解答例
import time


class Account:

    count = 0

    def __init__(self, name, balance):
        self.name = name
        self.balance = balance
        self.account_number = Account.count
        self.show_balance()
        self.transaction_history = []
        Account.count += 1

    def withdraw(self, price):
        if price <= self.balance:
            self.balance -= price
            self.show_balance()
            self.add_transaction(-price)
        else:
            print(f"残高({self.balance}円)が足りません")

    def deposit(self, price):
        self.balance += price
        self.show_balance()
        self.add_transaction(price)

    def show_balance(self):
        print(f"{self.name}(口座番号:{self.account_number})の残高は{self.balance}円です")

    def add_transaction(self, price):
        transaction = {'withdraw/deposit': price,
                       'new_balance': self.balance,
                       'time': Account.get_time_str()}
        self.transaction_history.append(transaction)

    @staticmethod
    def get_time_str():
        current_time = time.localtime()
        return "{0.tm_year}年{0.tm_mon}月{0.tm_mday}日{0.tm_hour}時{0.tm_min}分".format(current_time)

    def show_transaction_history(self):
        for t in self.transaction_history:
            transaction_str_list = []
            for k, v in t.items():
                transaction_str_list.append(f"{k}: {v}")
            print(", ".join(transaction_str_list))


myaccount = Account(name='my account', balance=0)
myaccount.deposit(10000)
myaccount.withdraw(5000)
myaccount.withdraw(6000)
myaccount.withdraw(3000)
myaccount.deposit(4500)
myaccount.show_transaction_history()

▶クラスメソッド(class method)

インスタンスに紐づかないメソッドで、クラス変数にアクセスしたいときやクラス内で便利関数のように使用する。
引数にはclsを取って、クラスの情報にアクセスする。
@classmethodデコレーターを使用して定義する。
クラスメソッド内でインスタンスを生成して、返すという使い方も可能。
こちらも<instance>.でアクセスできるが、無駄なのでやらないようにする。

class MyClass():

    classmethod_count = 0

    def mymethod(self):
        MyClass.mystaticmethod()
        print("This is normal method!")


    @staticmethod
    def mystaticmethod():
        print("This is staticmethod")


    # classmethodは引数にclsをとり,クラスとインタラクションを取ることができる.
    @classmethod
    def myclassmethod(cls):
        cls.classmethod_count += 1
        print(f"This is classmethod and now the count is {cls.classmethod_count}")



c = MyClass()
# 普通のmethodはインスタンスに紐づいている
c.mymethod()
# static methodは通常クラスからアクセスする
MyClass.mystaticmethod()
# インスタンスからstatic methodをcallすることもできるが,これは無駄なのでやらない
c.mystaticmethod()
# class methodも通常クラスからアクセスする
MyClass.myclassmethod()

Challenge

Personクラスの作成
条件
  属性:名前、年齢
  クラスメソッド:年齢を生年月日から判断してインスタンスを生成する
※ヒント:引数のclsはクラスそのものが入るので、cls()でインスタンスの生成は可能

解答例
import time


class Person:

    def __init__(self, name, age):
        self.name = name
        self.age = age

    @classmethod
    def create_from_dob(cls, name, year, month, date):
        today = time.localtime()
        # ((today.tm...date))はtuple型の比較を使用している
        age = today.tm_year - year - ((today.tm_mon, today.tm_mday) < (month, date))
        return cls(name=name, age=age)  # ここでインスタンスの生成を行っている


john = Person('John', 20)
emma = Person.create_from_dob('Emma', 1989, 4, 3)
print(emma.age)

▶名前修飾(name mangling)

名前修飾は特殊なParamatorの設定方法で、通常の名前の前にアンダースコア(_)を二つつけて定義する。
そうすることで、通常のように.でアクセスできなくなるので、外部から勝手に更新されたくないParamatorに対して名前修飾を行うことが多い。
(例:Accountクラスのbalanceなど)
名前修飾は裏でparam名の先頭に_<Class>が付与されており、定義した際の名前と異なっていることからアクセスを防いでいる。(なので、<instance>._<Class>__<param_name>とするとアクセスすることはできる…)
dir()にインスタンスを渡すことで、本当に名前修飾されているのかどうかを見ることできる。
non public変数が更新しないでほしい変数の目印であるのと同じようにクラスの属性で__がついているものは直接更新しないようにするのが常識となっている。

class Account:

    def __init__(self, balance):
        self.__balance = balance

    def deposit(self, price):
        self.__balance += price

    def withdraw(self, price):
        if self.__balance < price:
            print("残高が足りません")
        else:
            self.__balance -= price

    def show_balance(self):
        print(f"残高は{self.__balance}円です")


myaccount = Account(10000)
# __balanceは名前修飾(name mangling)されて,_Account__balanceになっている
print(dir(myaccount))
myaccount.deposit(2000)
myaccount.withdraw(5000)
myaccount.withdraw(10000)
# __balanceは_Account__balanceになっているので,「__balanceはない」というエラーになる(balanceも同様)
# print(myaccount.__balance)
# print(myaccount.balance)
# __balanceを更新しようとすると,新たに__balanceが作られる
# __は名前修飾されることを想定しているので,普通は__から始まる変数を更新することはしない
myaccount.__balance = 100000000
print(myaccount.__balance)
print(dir(myaccount))
# __balanceを更新してもself.__balanceは_Account__balanceをさす
myaccount.show_balance()
# 名前修飾はprivate変数として扱うことができるが,更新することは可能
# Pythonではprivate変数を作ることはできない(privateを強制することはしない)
myaccount._Account__balance = 100000000
myaccount.show_balance()

▶getterとsetter

getter:インスタンスの属性の情報を取得するためのメソッド
setter:インスタンスの属性の情報を更新するためのメソッド
getterは<instance>.<param_name>、setterは<instance>.<param_name> = <obj>の代わりとなる。
「わざわざ、getterやsetterを作成しなくても上記のコードで値を取得したり、設定したりすればいいのでは?」となるかもしれないが、non publicや名前修飾の属性は直接アクセスされることを想定したものではないので、そのような属性に対してgetterとsetterを作成することで、間接的にアクセスすることで何かの間違いで値が更新されることがないのが強みとなっている。
また、setterに関してはただ属性の値を更新するだけではなく、if文を活用することでvalidationのような動かし方もできる。
(例:年齢Paramatorにマイナスの値が入らないようにif age >= 0:という条件分岐を入れるなど)
property()関数を使用することで、いちいちgetter(<param>)setter(<param>)としなくても、<instance>.<param>とするだけで、getterとsetterが呼ばれるようにすることができる。
property()を使用する際は、属性は必ずnon publicの形で定義するようにする。(無限ループしてしまう)

class Person:

    def __init__(self, name, age):
        self.name = name
        # property()を使用する際は必ずnon publicの形で定義する
        self._age = age

    def get_age(self):
        print("get_age called")
        return self._age

    def set_age(self, age):
        print("set_age called")
        if age < 0:
            print("0以上の値を入れてください")
        else:
            self._age = age
    
    # 作成したgetter、setterをproperty()に設定する
    age = property(get_age, set_age)


john = Person("John", 10)
john.set_age(-10)
# setterが呼ばれる
john.age = -10
print(john.age)
# non publicなだけなので、直接アクセスすることはできる。
john._age = -10
print(john.age)

▶プロパティデコレータ(property decorator)

property()よりもより良い書き方。
getterには@property、setterには@<param>.setterデコレータを設定し、関数名は<param_name>にする。
基本的にはgetter/setterはこの書き方で定義するようにする。

class Person:

    def __init__(self, name, age):
        self.name = name
        self._age = age

    @property
    def age(self):
        print("get_age called")
        return self._age

    @age.setter
    def age(self, age):
        print("set_age called")
        if age < 0:
            print("0以上の値を入れてください")
        else:
            self._age = age


john = Person("John", 10)
print(john.age)
john.age = 10

print(john.age)

▶継承(inheritance)

継承とはクラスを定義する際に、別のクラスの機能を引き継いで定義する方法でオブジェクト指向が大体の言語に採用されている理由の一つ。
class <Class>(<InheritanceClass>):というようにして継承したいクラスを()の中に定義する。
継承される側のクラスは親、基底、superなどと呼ばれ、継承する柄のクラスは子、派生、subなどと呼ばれる。

class Animal(object):
    def __init__(self, name):
        self.name = name
        print('Animal init is called')

    def breath(self):
        print(f"{self.name} is breathing!!")


# Animalクラスを継承
class Dog(Animal):
    pass


class Cat(Animal):
    pass


# subclassのインスタンスを生成するときに,superclassのAnimalのコンストラクタが呼ばれている
dog = Dog("pochi")
cat = Cat("tama")

# superclassで定義されたメソッドが使用できる
dog.breath()
cat.breath()

▶継承時のコンストラクタ

継承したクラスから生成したインスタンス内にアクセスする際は先にsubclass内の情報を精査し、記述がなければsuperclassの情報を精査する。
そのため、superclassで定義されているのと同じメソッド名でメソッドを定義すると機能を上書きすることができる。
この上書きすることをオーバーライド(override)という。
もし、superclassのメソッドに機能を追加したいという程度であれば、__init__()(コンストラクタ)であれば、super.__init__(<param1>, <param2>,...)とすることで、簡単にsuperclassの機能を記述することができる。
もちろんself.<param> = <param>とすることで、subclass特有の属性を追加することもできる。

class Animal(object):
    def __init__(self, name):
        self.name = name
        print('Animal init is called')

    def breath(self):
        print(f"{self.name} is breathing")

class Dog(Animal):
    # superclassのコンストラクタをオーバーライドするために同名のコンストラクタをsubclassで定義
    def __init__(self, name, breed):
        # superclassのコンストラクタを定義
        super().__init__(name=name)
        # Dogクラス特有の属性を定義
        self.breed = breed
        # Dogクラス特有の機能
        print('Dog init is called')


class Cat(Animal):
    pass


# subclassのインスタンスを生成するときに,superclassのAnimalではなくDogのコンストラクタが呼ばれている
# そしてsubclassのinitでsuperclassのAnimalのinitが呼ばれている
dog = Dog("pochi", "akitainu")
print(dog.name)
print(dog.breed)

Challenge

Carクラスを継承したTruckクラスの作成
条件
・追加属性:最大積載量、現在の積載量
・追加メソッド:load() -> 積み荷の上げ下ろしができ、現在の積載量がマイナスにならないようにする。
・現在の積載量が最大積載量を超過した際にprint()するが、積むこと自体はできる。
・gas()をオーバーライドして、最大積載量を超過している状態では走れないようにする。

Car.py
Car.py
class Car(object):

    def __init__(self, model_name, mileage, manufacturer):
        self.model_name = model_name
        self.mileage = mileage
        self.manufacturer = manufacturer

    def gas(self):
        print("{0.manufacturer}の{0.model_name}(燃費:{0.mileage}),アクセル全開!!".format(self))

    def brakes(self):
        print(f"{self.manufacturer}{self.model_name}(燃費:{self.mileage}),ブレーキ!!!")


if __name__ == "__main__":
    conti_gt = Car("Bentley Continental GT", 4, "Bentley")
    prius = Car("Prius", 20, "TOYOTA")

    conti_gt.gas()
    prius.brakes()
解答例
from car import Car


class Truck(Car):
    def __init__(self, model_name, mileage, manufacturer, max_loadings):
        super().__init__(model_name, mileage, manufacturer)
        self._max_loadings = max_loadings
        self._loadings = 0

    def load(self, weight):
        if weight > 0:
            print(f"{weight}tの荷物を積みました")
            self._loadings += weight
        else:
            if self._loadings <= -weight:
                print(f"{self._loadings}t全ての荷物を降ろしました")
                self._loadings = 0
            else:
                print(f"{-weight}tの荷物を降ろしました")
                self._loadings += weight
        print(f"現在の積載量は{self._loadings}tです")
        if self._loadings > self._max_loadings:
            print(f"最大積積載量は{self._max_loadings}tです.重量オーバーです.!!")

    def gas(self):
        if self._loadings > self._max_loadings:
            print("重量オーバーなので走れません")
            print(f"最低でも{self._loadings-self._max_loadings}tの荷物をおろしてください")
        else:
            super().gas()


if __name__ == "__main__":
    isuzu_truck = Truck("トラックA", 6, "いすゞ", 10)
    isuzu_truck.gas()
    isuzu_truck.brakes()
    isuzu_truck.load(5)
    isuzu_truck.load(6)
    isuzu_truck.gas()
    isuzu_truck.load(-1)
    isuzu_truck.gas()

▶継承時のスタティックメソッドとクラスメソッド

クラスメソッドは引数にclsを持っているが、スタティックメソッドは持っていないという違いがある。
その違いが継承の際に大きな違いとなってくる。
subclassでsuperclassのクラスメソッドを呼ぶ際はclsにそのクラスそのものが入るので、Animalクラスを継承したDogクラスで呼び出したとしても、clsにはDogが入るので、Dogクラスのクラスメソッドとしてふるまうことができる。
しかし、スタティックメソッドではclsがないので、subclassから呼び出したとしても、superclassから呼び出されたという振る舞いになってしまう。
なので、継承する予定のクラスではスタティックメソッドは使用せず、クラスメソッドを定義するようにする。

import time


class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    @classmethod
    def create_from_dob(cls, name, year, month, date):
        today = time.localtime()
        age = today.tm_year - year - ((today.tm_mon, today.tm_mday) < (month, date))
        return cls(name=name, age=age)
    
    @staticmethod
    def create_from_dob2(name, year, month, date):
        today = time.localtime()
        age = today.tm_year - year - ((today.tm_mon, today.tm_mday) < (month, date))
        return Person(name=name, age=age)


class Baby(Person):
    pass


john = Baby('John', 20)
print(john.age)
print(type(john))
emma = Baby.create_from_dob('Emma', 1989, 4, 3)
emily = Baby.create_from_dob2('Emily', 1999, 12, 3)
print(emma.age)
print(emily.age)
print(type(emma))
print(type(emily))

▶継承時の名前修飾

名前修飾をうまく活用することで、オーバーライドを回避する書き方ができる。

class Person:
    def __init__(self, name):
        self.name = name
        # 名前修飾していないとsubclassでインスタンスを生成した際に、superclassのmymethod()ではなく、subclassのmymethod()が呼ばれてしまう。
        self.__mymethod()

    def mymethod(self):
        print("Person method is called")

    # superclassのコンストラクタが`__mymethod()`を呼び出すときに、mymethod()を参照しに行く
    __mymethod = mymethod


class Baby(Person):
    def mymethod(self):
        print("Baby method is called")


taro_baby = Baby("Taro")
print(dir(taro_baby))
p = Person("test")
p.mymethod()
taro_baby.mymethod()

▶クラスに書くdocstring

ほとんど関数に書くdocstringと同じ。
クラスの最初に定義する。

class Duck:
    """
    This is a class for Duck.

    Attributes:
        name (str): the name of the duck

    Methods:
        walk: print xxx
        quack: print xxx
        fly: print xxx
    """

    def __init__(self, name):
        """
        The constructor for Duck class.
        :param name: the name of the duck
        """
        self.name = name

    def walk(self):
        """
        print walking,,,
        :return: None
        """
        print("walking,,,")

    def quack(self):
        print("quack quack..!!")

    def fly(self):
        print("Whee!!")


class Penguin:

    def __init__(self, name):
        self.name = name

    def walk(self):
        print("walking,,,")

    def quack(self):
        print("quack quack..??")

    def swim(self):
        print("Swimming!!")


if __name__ == "__main__":
    print(help(Duck))
    print(help(Duck.__init__))
    print(Duck.__doc__)

次の記事

Python初心者の備忘録 #05

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?