LoginSignup
56
38

Python の Code Golf テクニック紹介 〜 Python のコードをできるだけ短く書いてみよう! 〜

Last updated at Posted at 2023-12-07

こんにちは、totosuki です!
私が通っている N 高で Advent Calendar 2023 が開催されていたため、参加します!

この記事ではコードをできるだけ短く書くため、可読性を無視しています。

この記事の構成

この記事では、以下の構成になっています。

  1. Code Golf について例題を交えて説明
  2. 私が Code Golf をしている理由
  3. Code Golf のテクニックを 5 項目に分けて紹介(結構長いよ)
  4. 上記で紹介したテクニックを使ってもう一度 Code Golf をしてみる

また、記事を夢中で書いていたら、結構長い記事になりました...
ですが、とてもとても面白い内容になってるはずです!是非最後まで見てみてください!

Code Golf って何??

皆さんは「Code Golf」というものをご存知でしょうか?

Code Golf とは「できるだけ短いコードで、与えられた問題を解決する競技」です。

実際に例題を交えて説明した方が伝わると思うので、早速例題を見てみましょう!

例題
以下の文章を出力してください。

Hello World!
Hello World!
Hello World!
Hello World!
Hello World!

まずは、単純明快な解法を考えてみます!

print("Hello World!")
print("Hello World!")
print("Hello World!")
print("Hello World!")
print("Hello World!")

print 関数を 5 回使用して問題を解きました!

ですが Code Golf は、できるだけ短いコードで問題を解決する競技なので、更に短くする必要があります!

今回の場合は、for文を使用することで更に短く出来そうですね。

for i in range(5):
    print("Hello World!")

また、 for文の中身が一行なので、

for i in range(5):print("Hello World!")

と書けます。かなり短くなってきましたね!

また、変数 i は使わない為、裏技的ですがこのように書くと更に短くなります。

for i in[0]*5:print("Hello World!")

4文字短くなりました!

ちなみに、in[0] の間にスペースが無くても動く理由は、記号と文字が隣接している場合、間の空白が省略出来るからです。

こんな感じで短くしていき、おそらくこの例題の最短コードはこれです!!

print("Hello World!\n"*5)

最初と比べてだいぶ短くなりましたね!

上記のコードでは Hello World! に改行文字( \n )を追加した文字列を掛け算して出力しています。

ただ、上記のコードでは最後に改行が生まれてしまい、問題が求めている文章と若干ずれてしまいます。修正した最短コードを下に載せますが、難解な部分があるため、読み飛ばしてもらって構いません!

exec("print('Hello World!');"*5)

では最後に、最初に書いたコードと、出来るだけ短く書いたコードを見比べて見ましょう!

短縮前
print("Hello World!")
print("Hello World!")
print("Hello World!")
print("Hello World!")
print("Hello World!")
短縮後
print("Hello World!\n"*5)

Code Golf がどのようなものか分かりましたかね?

なぜ Code Golf をするのか

めっちゃ楽しいからです。

それだけでも良いんですけど、特定の言語に対して深い理解が出来る手段の一つだと思います。
つまり、特定の言語に対して深い理解が求められるということでもあるんですけどね。

次の章から、「Python の Code Golf テクニック」を紹介しますが、Python の言語仕様や面白い所(とても主観)を感じてくれると嬉しいです!!

PythonのCode Golf テクニック5選

前の前の章で、"Hello World!" と 5 行出力する問題で Code Golf をしました。
「思ってたより短くなるんだな〜!」と思った方も居ると思います。(居て欲しい)

......しかし、そのコードのテクニックは氷山の一角の一角(?)に過ぎません。

そこで!私が思う、Python の Code Golf で「これはおすすめ!」と思うテクニックを5項目に分けて紹介します!

注意書き

この先、Code Golf 特有の書き方が段々と増えてくるため、事前に注意点(?)を箇条書きでまとめました。

  • Python の基本的な知識があると読みやすいです!
  • 後半のテクニックは難しめの内容が多いので、飛ばしちゃって良いです!
  • 私は、主に AtCoder 上で Code Golf をしているため、Code Golf といった Code Golf 用のサイトとはズレがある可能性があります。
  • map(int, input().split()) のような、一般的に競技プログラミングで使われている入力を多めに使用しています。

1. 基本的な短縮をする!

1つめは、テクニックと呼んでいいのか分かりませんが、一番重要でありとても効果的なので、紹介します!

1.1 変数名を一文字にする

まずは、変数名を一文字にしましょう。これをするだけで、Code Golf感が増し、コードの短縮に繋がります!

また、Python ではアルファベットの大文字と小文字を区別出来るため、一文字でも変数名に困ることは Code Golf において無いと思います。

変更前
hello_text = "Hello World!"
for i in range(5):
    print(hello_text)
変更後
T = "Hello World!"
for i in range(5):
    print(T)

1.2 余計な空白を無くす

一般的なコードでは、可読性(コードの読みやすさ)を上げるために、空白や改行を使い見やすくしますが、Code Golf では必要ありません。

また、Pythonで間の空白を省略出来る場合は沢山あるので気になった方は是非色々試してみてください!

変更前
T = "Hello World!"
for i in range(5):
    print(T)
変更後
T="Hello World!"
for i in range(5):
    print(T)

1.3 余計な改行を無くす

if文やfor文などの、中身が一行の場合は、コロン : の隣に記述できます。

変更前
T="Hello World!"
for i in range(5):
    print(T)
変更後
T="Hello World!"
for i in range(5):print(T)

1.4 インデントを 1 にする

実は、インデントは 1 でもプログラムを動かすことが出来ます。
可読性がとてもとても悪くなるので普段はしちゃ駄目ですよ!!

変更前
T="Hello World!"
for i in range(5):
    print(T)
変更後
T="Hello World!"
for i in range(5):
 print(T) # <- 勿論処理が一行なので、for文の横に書けます

2. 変数への代入テクニック

コードを書く上で欠かせないものが変数です。

その中で初期化や代入の部分で一工夫入れることにより、コードを短縮できる可能性があります。

2.1 同じ値を複数の変数に代入したい場合

同じ値を複数の変数に代入することは一行で簡潔に書けます!

変更前
x=0
y=0
z=0
変更後
x=y=z=0

基本イミュータブルの型だったら何でも、このテクニックを使えると思います!

Python のイミュータブルな型は bool int str tuple などです。
詳しくイミュータブルについて知りたい方はこちらの記事がおすすめです!

2.2 アンパック代入

リストや文字列などのイテラブルなオブジェクトは、複数の変数へ一気に代入することが出来ます。

後ほど紹介しますが、open(0) で入力を受け取る方法と相性が良く覚えておいて損はないです!!

変更前
A=list(map(int,input().split()))
print(A[1:])
変更後
_,*A=map(int,input().split())
print(A)

他にもアンパックを利用した短縮はいくつかあるので、活用していきたいですね!!

2.3 セイウチ演算子

セイウチ演算子とは「変数の代入と使用」を同時に行える演算子です。
セイウチ演算子自体の解説についてはこちらの記事が分かりやすいです!

可読性が高くなる(主観)セイウチ演算子ですが、可読性が悪いCodeGolfにも使えるタイミングがあります(?)

以下は、配列の長さが5以上の場合に配列の平均値を出力するプログラムです!

変更前
A=list(map(int,input().split()))
l=len(A)
s=sum(A)
if(l>=5):print(s/l)

# よりCodeGolfっぽく書くと...
A=list(map(int,input().split()))
if len(A)>=5:print(sum(A)/len(A))
変更後
if(l:=len(A:=list(map(int,input().split()))))>=5:print(sum(A)/l)

「変数の代入と使用」を同時に行っている箇所は、:= がある所です。
if文や内包表記などの、普段は代入が行えない場所で、代入が行えるのがとても便利です!

3. input 関数のテクニック

3.1 入力をそのままfor文に使う

「for文を使う入力であり、その入力が最後に来る」時には、そのままfor文に使ったほうが短くなる場合が多いです。

使用例を見せたいので、以下のコードより短い書き方もありますが許して下さい!!

変更前
N,X=map(int,input().split())
A=map(int,input().split())
c=0
for a in A:
 if a<X:c+=1
print(c)
変更後
N,X=map(int,input().split())
c=0
for a in map(int,input().split()):
 if a<X:c+=1
print(c)

3.2 必要の無い入力がある場合

問題によっては、一つの行が完全に必要ない入力の場合があります。
知ってる方もいるかと思いますが、input も関数なので代入などもせずに呼び出すことが出来ます!

変更前
_=input()
A=list(map(int,input().split()))
print(sum(A))
変更後
input()
A=list(map(int,input().split()))
print(sum(A))

また、必要の無い入力の後に二行以上入力が来る場合、セイウチ演算子を使うことで更に短縮に繋がります!
(ここまで入力が多いとopen(0) を使用した場合の方が短い可能性が高いですが...)

(i:=input)()
A=list(map(int,i().split()))
B=list(map(int,i().split()))
print(sum(A+B))

3.3 eval 関数を利用する

入力に使われる、list(map(int, input().split())) ってとても長いですよね。

そこで、入力の文章を文字列にして eval 関数で実行すると短縮することが出来ます!

eval 関数は簡単に言うと「文字列として書いたコードを実行する関数」です

変更前
A=list(map(int,input().split()))
B=list(map(int,input().split()))
変更後
S="list(map(int,input().split()))"
A=eval(S)
B=eval(S)

「短くなります!」とは言いましたが、実際は後ほど紹介する open(0) という方法があるため使うタイミングは全くありません...
ですが、個人的に好きな短縮方法なので紹介しちゃいました!

4. open(0) の入力テクニック

先程、input 関数のテクニックを紹介しましたが、大量の入力を受け取る際には open(0) を使うと更に短く書ける場合が多いです。

少々難解な内容ですが、open(0) を理解できれば、入力系のテクニックはマスターしたと言っても過言では無いので、是非見て欲しいです!!

4.1 open(0) って何???

Pythonでの入力を受け取る方法は、input sys.stdin sys.stdin.buffer io.BytesIO などありますが、open(0) というものでも入力を受け取ることが可能です。

open(0) がなぜCode Golfで良く使われている理由はずばり...
open(0) は、全ての入力を改行区切りで一度に受け取る事が出来るからです!!

4.2 open(0) で入力する際の注意点

では、早速 open(0) で入力した値を受け取ってみましょう!以下のコードを実行してみます。

S = open(0)
print(S)
出力
<_io.TextIOWrapper name=0 mode='r' encoding='UTF-8'>

おかしいですね...入力ができずにこのような出力になりました。

この問題の原因は、open(0) の戻り値が str 型では無く、 TextIOWrapper 型というものが戻り値として設定されている事が理由です。

では、どのようにしたら入力をした値を利用できるのでしょうか?
ここで、TextIOWrapper クラスにはどのような関数が実装されているのか確認してみましょう。

print(dir(open(0)))
出力
[..., __iter__, ..., __next__, ...]

出力結果が長いので、省略しますが、__iter____next__ 関数が含まれていました。

つまり、for文や、アンパックなどで、入力した値を利用できるということです!

なぜ __iter____next__ があるとfor文が出来るかは、こちらの記事
なぜ __iter__ があるとアンパックが出来るかは、こちらの記事
を見ると分かるかもしれません!

余談ですが、map クラスも __iter____next__ 関数が実装されています。そのため、ほぼ map(int, input().split()) と同じような感覚で、open(0) は利用して問題無いと思います!

4.3 open(0) の入力方法その1

一番使用頻度が高いと思う open(0) での入力方法は、「一度に入力を受け取ってアンパック代入」です。
(アンパックを利用した代入方法の呼び方は間違ってるかもしれません、許して!)

また、open(0) を使用する際、 Ctrl+D を押すことで標準入力を終了できます!

変更前
N=int(input())
S=input()
print(S[:N])

# input() でも、もう少し短く出来る
N=int(input())
print(input()[:N])
変更後
N,S=open(0)
print(S[:int(N)])

もちろん、変数に * 演算子をつけることで、残りの要素を全て受け取れます!

N,*S=open(0)
print(S)
入力
5
abcde
vwxyz
出力
['abcde\n', 'vwxyz']

open(0) を使用する上での注意点としては、改行コード \n が残ってしまいます。
対象法は色々ありますが良く使う 3 種類を紹介します!

  • 空白区切りで後々リスト等にする場合:split()
  • ただ文字列として利用したい場合:スライスで末尾だけ除く [:-1]
  • 上記以外で削除したい場合:strip()

もちろん上記の方法が最短になるとは限らないので、色々試してみるのが良いでしょう。

4.4 open(0) の入力方法その2

全ての入力が、空白 か \n で区切られてる時、以下の入力方法が使用できます。

変更前
N=int(input())
A=list(map(int,input().split()))
print(A[:N])
変更後
N,*A=map(int,open(0).read().split())
print(A[:N])
入力
3
1 2 3 4 5
出力
[1, 2, 3]

どのような時に使うかですが、「空白 か \n で区切られていて、全ての入力を整数に変換したい」時に一番使う気がしています!

4.5 open(0) の 0 で初期化する

これは入力のコツでは無いのですが、整数の変数を 0 で初期化したい時、open(0) と セイウチ演算子を組み合わせることで、一文字短縮に繋がります。

変更前
X,*A=map(int,open(0).read().split())
c=0
for a in A:
 if a<X:c+=1
print(c)
変更後
X,*A=map(int,open(c:=0).read().split())
for a in A:
 if a<X:c+=1
print(c)
入力
5
1 2 3 4 5 6 7
出力
4

5. スライス と "YNeos" 関係

実は、Code Golf では条件式を、配列のスライス操作に使う場合があるんです。
この章では、Python のスライス操作というものを利用したテクニックを紹介します!

5.1 YNeos って何?????

まず初めに以下のコードを見てみましょう。

x が 3 未満の場合 Yes を出力し、そうでない場合は No を出力するコードです。

if x < 3:
 print("Yes")
else:
 print("No")

より短く書くと以下の様になります。
(更に空白を削れますが見にくいため、ここではしてません。)

print("Yes" if x<3 else "No")

結構短くなりましたね。
ですが、実はスライス操作を使用すると更に短く書けるんです!

print("YNeos"[x>2::2])

「一体、何をしてるんだ!」ってなる方も居ると思います。

ここで、スライス操作で、文字列 "YNeos" の 0 文字目から 1 つ飛ばしに、文字を取り出した文字列を出力するコードを書いてみます。

print("YNeos"[0::2])
出力
Yes

Yes と出力されましたね!

先程のコードと似てますね...見比べて見ましょう!

print("YNeos"[x>2::2])

print("YNeos"[0::2])

気づいた方も居るのではないでしょうか?

このテクニック( "YNeos" )は...


条件式が True なら 1 、False なら 0 として、スライスの開始位置を変えて、"Yes""No" を変えているテクニックなのです!!


また、なぜ条件式( True False )が 1 や 0 として扱えるかと言うと、「 bool クラスが int クラスのサブクラス」であるからです!
詳しく知りたい方は、こちらの記事公式リファレンスをご覧ください。

5.2 YNeos と NYoe s

"YNeos" の意味について理解できた所で、実際の使用例を見てみましょう。

問題文
与えられた整数が、3 未満なら Yes そうでないなら No と出力してください。
print("YNeos"[int(input())>2::2])

このような問題文には、"YNeos" を使うことが最適である場合が多いです。

また条件式を、int(input()) < 3 としてしまうと、3 未満の場合 True つまり開始位置が 1 となり、出力結果は "No" となってしまいます。

つまり、Yes と No の出力が逆になってしまうわけです!

そのため、上記のコードでは「与えられた整数が 3 以上の場合に、条件式が "True" となる」ようにコードを書いています。

ちなみに、条件式で >=<= を使うと一文字損するので、可能な限り使わない様にしましょう!

では、次に "YNeos" が使えない場合にどうするのか、問題を交えて考えてみましょう!

問題文
与えられた整数が、奇数なら Yes 偶数なら No と出力してください。
print("NYoe s"[int(input())%2::2])

まず、奇数偶数の判定は %2 &1 などで行えて、両方とも奇数の場合 1 となります。
この問題では「奇数の場合に "Yes" 」のため、"YNeos" と書こうとすると少し冗長になります。

そこで、"NYoe s" です。「 "Yes""No" の順番を入れ替えれば良い」のです!

「e と s の間の空白があるじゃないか!」と疑問に思った方も居るかもしれませんが、私が主に Code Golf をしている AtCoder では、末尾に空白があっても問題ない場合が多いため、問題ありません。

ですが、他のサイトではどうなのか分かりません...ごめんなさい!(勢い)

5.3 条件式でインデックス指定をする

応用(こっちが基本?)ですが、インデックス指定に条件式を入れるテクニックもあります!

問題文
「P」と入力したら Python と出力し、それ以外は C# と出力してください。
短縮前
print("CP#y t h o n"[input()=="P"::2])
短縮後
print(["C#","Python"][input()=="P"])

もちろん、 "YNeos" と同じ要領で書くのは凄く Code Golf をしててと思います。
ですが、今回の様に「2つの選択肢の文字列の長さに大きな差がある場合」は、インデックス指定の方が短い場合もあります!

また、変数に値を代入する際にもインデックス指定が最適な場合があります。

x=[10,20][int(input())<20]

上記のコードは、入力した値が 20 未満の場合 10、それ以外は 20 を変数に代入しています。

最後に Code Golf をしてみよう!!!

まずは、記事をここまで読んでくださりありがとうございます。
今まで様々なテクニックを紹介してきましたが、どうでしたか?

最後に、ここまでのテクニックを使って一緒に Code Golf をしてみましょう!!

(自作しましたが、何かのコンテストの問題と被ってたらごめんなさい!)


問題文

$ N $ 個の整数 $ A_1,A_2,...,A_N $ が与えられます。
これらの整数を $ X $ 未満と $ X $ 以上に分類し、それぞれのグループの総和を求めてください。

入力

$ N X$
$ A_1 A_2 ... A_N$

出力

$ X $ 未満の総和、$ X $ 以上の総和の順に、空白区切りで出力してください。

入力例

6 5
1 2 3 4 5 6

出力例

10 11

Code Golf

まずは、一般的な解法を考えてみます。

初めに、入力を全て受け取った後に、総和を取る用の変数を2つ用意します。
次に、for文で配列の要素を一つ一つ見ていき、「要素が X 未満か」で条件分岐をして、事前に作成した変数に値を足していきます。
最後に、2つの変数を出力すれば良いですね。

179文字
N, X = map(int, input().split())
A = list(map(int, input().split()))
under = 0
over = 0
for a in A:
    if a < X:
        under += a
    else:
        over += a
print(under, over)

実際に実装してみました!
ここからが、Code Golf のスタートですね!!

コードブロックの左上に文字数を記載してます!(改行は一文字判定)

まず初めに、「変数名を一文字にする」「余計な空白無くす」をしたほうが良さそうですね。

140文字
N,X=map(int,input().split())
A=list(map(int,input().split()))
u=0
o=0
for a in A:
    if a<X:
        u+=a
    else:
        o+=a
print(u,o)

次に、条件分岐の処理が一文字のため、隣に書けますね!
また、インデントも 1 に出来るため、変更してしまいましょう。

116文字
N,X=map(int,input().split())
A=list(map(int,input().split()))
u=0
o=0
for a in A:
 if a<X:u+=a
 else:o+=a
print(u,o)

だんだん Code Golf っぽくなってきましたね!
また、2つの変数が同じ値で初期化しているため、以下のようにするとより短くなります!

114文字
N,X=map(int,input().split())
A=list(map(int,input().split()))
u=o=0
for a in A:
 if a<X:u+=a
 else:o+=a
print(u,o)

次は、入力関係を短縮していきます!

最後の入力は、for文 に使うのみの入力ですね。
なので、入力をそのまま for文 に使っちゃいましょう!

104文字
N,X=map(int,input().split())
u=o=0
for a in map(int,input().split()):
 if a<X:u+=a
 else:o+=a
print(u,o)

入力をそのまま for文 に使いましたが、open(0) を使うと更に短く出来そうですね。

また、全ての入力が、空白 か 改行 で区切られていて、整数に変換する必要があることから、以下の方法が入力で最適です。

91文字
# 変数 N は使っていないので、アンダーバーに変えました。
_,X,*A=map(int,open(0).read().split())
u=o=0
for a in A:
 if a<X:u+=a
 else:o+=a
print(u,o)

因みに、open(0) の 0 を使用した初期化は、上記のような初期化の状態だと 1 文字増えてしまいます。

ついに、100文字を切りました!!

そして、今まで紹介したテクニックを使用した短縮は全てしました!!お疲れ様でした!


......ですが!更にまだ短縮が出来ます。

ここからは、紹介したテクニックを応用したものを使います。解説しながら進めますね!

まず初めに、文字数は変わりませんが、exec 関数の文字列の中に、 "YNeos" と同じ考え方で、文字列を条件式によって分ける方法を使います。

exec 関数を簡単に説明すると「文字列のコードを実行する関数」です。
詳しく知りたい方は、こちらの記事がおすすめです!

91文字
_,X,*A=map(int,open(0).read().split())
u=o=0
for a in A:exec("ou++==aa"[a<X::2])
print(u,o)

"YNeos" っぽいコードになってますね。中身を2つに分割すると、o+=au+=a になります!

次は、条件分岐の処理のうち、+=a の部分が同じ事を利用して短縮をします。
具体的には、f-string を使用して、文字列のうち、"ou" の部分のみを変更するようにします。

f-string を簡単に説明すると「文字列の中に変数等を使える書き方」です。
詳しく知りたい方は、こちらの記事がおすすめです!

90文字
_,X,*A=map(int,open(0).read().split())
u=o=0
for a in A:exec(f"{'ou'[a<X]}+=a")
print(u,o)

この様に exec 関数の中を書くことで、1 文字短縮することが出来ました!

また、exec 関数の中に % を使った、昔ながらの書き方でも同じ文字数になります。

exec("%c+=a"%'ou'[a<X])

では、これ以上短くすることは可能なのでしょうか?
おそらく、上記のコードの書き方から更に短縮することは不可能です。

ですが、解法を少し変えることで更に短縮することが可能です!
問題文を振り返ってみます。

問題文
N 個の整数 A1, A2, ..., AN が与えられます。
これらの整数を X 未満と X 以上に分類し、それぞれのグループの総和を求めてください。

よくよく考えてみると、X 未満のグループの総和が求まれば、 X 以上のグループの総和が求まることが分かります。

では、先程までのコードをこの解法に合わせて修正してみます。

85文字
_,X,*A=map(int,open(0).read().split())
u=0
for a in A:
 if a<X:u+=a
print(u,sum(A)-u)

条件分岐の else と変数 o が必要無くなり、85文字まで短縮されました!!

また、sum 関数を使用して、X 以上のグループの総和を以下のように求めました。

X 以上のグループの総和 = (全ての整数の総和) - (X 未満のグループの総和)

だいぶ短くなりましたが、まだまだ改善点はあります。

まず初めに、if文 の部分を、条件式の True False1 0 として扱える事を利用することで更に短縮が可能です!

82文字
_,X,*A=map(int,open(0).read().split())
u=0
for a in A:u+=(a<X)*a
print(u,sum(A)-u)

上記のコードの u+=(a<X)*a についてですが、

  1. 条件式 (a<X)True なら 1、False なら 0 となる。
  2. (a<X) が 1 なら a を掛けると、実質 u+=a をしてることになり、0 の場合は u+=0 となる。

という流れになっていて、X 未満の総和を求めることに成功しています!

そして、次が最後の短縮になります!
最後は、sum 関数とセイウチ演算子 := を利用した短縮をします。

80文字
_,X,*A=map(int,open(0).read().split())
print(u:=sum(a*(a<X)for a in A),sum(A)-u)

ここでは、変数 u にセイウチ演算子で、sum 関数で作成した X 未満のグループの総和を代入し、使用(出力)しています。

その後、変数 u に総和は代入されたので、sum(A)-u で X 以上のグループの総和を出力しています。

肝心の sum 関数の内容ですが、a*(a<X) とすることで、for 句との間の空白を省略した以外は、for文の時と変更点はありません!


短縮し終えたので、最後に短縮前と短縮後を比べてみます!!
ここまで、お疲れ様でした!!

短縮前179文字
N, X = map(int, input().split())
A = list(map(int, input().split()))
under = 0
over = 0
for a in A:
    if a < X:
        under += a
    else:
        over += a
print(under, over)
短縮後80文字
_,X,*A=map(int,open(0).read().split())
print(u:=sum(a*(a<X)for a in A),sum(A)-u)

終わりに

とても長い記事でしたが、最後まで読んでいただきありがとうございます!
この記事を読んで、少しでも Code Golf の魅力や面白さ、 Python という言語の面白さが伝わっていたらとてもとても嬉しいです。
それでは良き Code Golf ライフを!!

56
38
1

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
56
38