【プログラマー新米の"言語処理100本ノック 2020"】第1章を解く

#はじめに
　ネットをさまよっている時にふと、"[言語処理100本ノック 2020](https://nlp100.github.io/ja/)"というサイトに出会いました。自然言語処理を触ってみたいなと思っていた反面、プログラミングは競プロを少しやったプログラマー新米。ちょっと興味もあるのでせっかくなので挑戦してみようと思います。
　この記事を書いている時点では全体の半分しか終わっていませんが、備忘録的な意味で書いて行こうと思います。心が折れたらやめます。先の記事がなかったら察してください。

#環境とスタンス
###環境
+ OS : macOS Catalina 10.15.3
+ Python : 3.7.6

###スタンス
+ 実装はあんまり頑張らない
+ 慣習とか知らない。
+ 安全性もそこまで考えない。
+ 他人が読めるように頑張る。
+ Python初心者にできるだけ優しくかきたい(願望)。

極力解説を書こうと思いますが、気になった方は調べることをお勧めします。

#"第1章: 準備運動"を解く
###00. 文字列の逆順
>文字列”stressed”の文字を逆に（末尾から先頭に向かって）並べた文字列を得よ．

```python:00.py
print("stressed"[::-1])
```

```:端末
desserts
```

Pythonのスライスを生かした処理です。競プロやってるとスライスはよく見ます。
 スライスは`[start:stop:step]`を指定することができます。

###01. 「パタトクカシーー」
>「パタトクカシーー」という文字列の1,3,5,7文字目を取り出して連結した文字列を得よ．

```python:01.py
print("パタトクカシーー"[::2])
```
```:端末
パトカー
```
一文字おきに最初の文字から取り出す、というのもスライスを使えば簡単です。

###02. 「パトカー」＋「タクシー」＝「パタトクカシーー」
>「パトカー」＋「タクシー」の文字を先頭から交互に連結して文字列「パタトクカシーー」を得よ．

```python:02.py
print("".join([ i + j for i, j in zip("パトカー", "タクシー")]))
```
```:端末
パタトクカシーー
```

リストを文字列に変換してくれるjoin()とリスト内包表記、複数のリストの内容を取得するzip()を使ってコード長を縮めています(何故)。

###03. 円周率
>“Now I need a drink, alcoholic of course, after the heavy lectures involving quantum mechanics.”という文を単語に分解し，各単語の（アルファベットの）文字数を先頭から出現順に並べたリストを作成せよ．

意味もなく短くしたver

```python:03.py
print(*(map(lambda x: len(x),"Now I need a drink, alcoholic of course, after the heavy lectures involving quantum mechanics.".translate(str.maketrans({",":"",".":""})).split())))
```
```:端末
3 1 4 1 5 9 2 6 5 3 5 8 9 7 9
```

まだ一行で行ける...(努力の方向音痴)
map関数はlistの要素それぞれに対して関数を実行してmapオブジェクトを返してくれます。
その関数は今回、lambda式によって定義されています。式の中身は与えられた文字列の長さを返すように定義されています。
`translate()`は`str.maketrans()`で作られた変換テーブルに基づき、文字列を置換してくれます。
また、`split()`は空白で区切ってlist化しています。

多分まともなver

```python:03.py
s = "Now I need a drink, alcoholic of course, after the heavy lectures involving quantum mechanics."
l = s.translate(str.maketrans({",": "", ".": ""})).split()
a = []
for i in l:
    a.append(len(i))
print(*a)
```
やっていることは短い方と一緒です。
変わった点はmap関数で行なっていたことをfor文にしていることくらいです。`append()`はlistの最後尾に要素を追加してくれます。

printの際に`*`をつけている理由は、listを展開して表示するためです。

###04. 元素記号
>“Hi He Lied Because Boron Could Not Oxidize Fluorine. New Nations Might Also Sign Peace Security Clause. Arthur King Can.”という文を単語に分解し，1, 5, 6, 7, 8, 9, 15, 16, 19番目の単語は先頭の1文字，それ以外の単語は先頭に2文字を取り出し，取り出した文字列から単語の位置（先頭から何番目の単語か）への連想配列（辞書型もしくはマップ型）を作成せよ．

```python:04.py
s="Hi He Lied Because Boron Could Not Oxidize Fluorine. New Nations Might Also Sign Peace Security Clause. Arthur King Can.".split()
l=[1,5,6,7,8,9,15,16,19]
dic={}
for i in range(len(s)):
    if i in l:
        dic[s[i][0]]=i+1
    else:
        dic[s[i][:2]]=i+1
print(dic)
```
```:端末
{'Hi': 1, 'H': 2, 'Li': 3, 'Be': 4, 'Bo': 5, 'C': 20, 'N': 10, 'O': 8, 'F': 9, 'Na': 11, 'Mi': 12, 'Al': 13, 'Si': 14, 'Pe': 15, 'S': 16, 'Ar': 18, 'Ki': 19}
```

観念して複数行にしました。`i`が`l`の中にあれば冒頭一文字を、そうでなければ二文字目までをKeyとするdictを生成しています。

###05. n-gram
>与えられたシーケンス（文字列やリストなど）からn-gramを作る関数を作成せよ．この関数を用い，”I am an NLPer”という文から単語bi-gram，文字bi-gramを得よ．

n-gramが何かについては[こちら](https://qiita.com/kazmaw/items/4df328cba6429ec210fb)を参照してみてください。

```python:05.py
def N_gram(s, n=1):
    return [s[i:i+n] for i in range(len(s)-n+1)]


s = "I am an NLPer"
print(*(N_gram(s, 2)))
print(*(N_gram(s.split(), 2)))
```

```:端末
I   a am m   a an n   N NL LP Pe er
['I', 'am'] ['am', 'an'] ['an', 'NLPer']
```

N_gramの実装はだいぶコンパクトになりました。
実行結果では空白があるせいで何だこれという感じではありますが...。
`range()`関数は0から指定された数字未満の整数を順に返してくれるジェネレーターです。0の部分も指定することができます。
関数宣言部分の`n=1`はテンプレート引数です。指定がなければnは1になるという意味です。

###06. 集合
>“paraparaparadise”と”paragraph”に含まれる文字bi-gramの集合を，それぞれ, XとYとして求め，XとYの和集合，積集合，差集合を求めよ．さらに，’se’というbi-gramがXおよびYに含まれるかどうかを調べよ．

とりあえず、paraparaparadiseはダンスゲームらしいです。

```python:06.py
def N_gram(s, n=1):
    return {s[i:i + n] for i in range(len(s) - n + 1)}


s1 = "paraparaparadise"
s2 = "paragraph"

X = N_gram(s1, 2)
Y = N_gram(s2, 2)

s_union = X | Y
s_intersection = X & Y
s_difference = X - Y

print(*s_union)
print(*s_intersection)
print(*s_difference)

if "se" in X:
    print("\"se\" is in X")

if "se" not in Y:
    print("\"se\" is not in Y")
```
```:端末
pa ar ad ap is se di ag ph gr ra
ar pa ra ap
is ad se di
"se" is in X
"se" is not in Y
```

未来が見えているかのような書き振りですね。
`set.union`や`set.intersection()`、`set.difference()`を使うことも可能ですが、個人的には`|`、`&`、`-`を使った方がわかりやすいのでこうしました。

###07. テンプレートによる文生成
>引数x, y, zを受け取り「x時のyはz」という文字列を返す関数を実装せよ．さらに，x=12, y=”気温”, z=22.4として，実行結果を確認せよ．

```python:07.py
def temp(x=12, y="気温", z=22.4):
    return str(x) + "時の" + str(y) + "は" + str(z)


print(temp())
```
```:端末
12時の気温は22.4
```

05\. n-gramで触れたテンプレート引数を使っています。
関数宣言の時に書く引数に代入文を書いておくと実行時に引数が与えられなくても、その値で関数が実行されます。

###08. 暗号文
>与えられた文字列の各文字を，以下の仕様で変換する関数cipherを実装せよ．
>    ・英小文字ならば(219 - 文字コード)の文字に置換
>    ・その他の文字はそのまま出力
>この関数を用い，英語のメッセージを暗号化・復号化せよ．

```python:08.py
def cipher(s):
    return "".join(c.islower()*chr(219-ord(c))+(not c.islower())*c for c in s)


print(cipher("The quick brown fox jumps over the lazy dog."))
print(cipher(cipher("The quick brown fox jumps over the lazy dog.")))
```
```:端末
Tsv jfrxp yildm ulc qfnkh levi gsv ozab wlt.
The quick brown fox jumps over the lazy dog.
```

頑張って一行にした。(違う、そうじゃない)
今回はPythonのbool型がint型のサブクラスであることをうまく利用しています。
islower()は小文字かどうかを判定する関数です。
219 - 文字コードなのは二回するとうまく戻ってくるからなんですね。

###09. Typoglycemia
>スペースで区切られた単語列に対して，各単語の先頭と末尾の文字は残し，それ以外の文字の順序をランダムに並び替えるプログラムを作成せよ．ただし，長さが４以下の単語は並び替えないこととする．適当な英語の文（例えば”I couldn’t believe that I could actually understand what I was reading : the phenomenal power of the human mind .”）を与え，その実行結果を確認せよ．

[タイポグリセミア](https://ja.wikipedia.org/wiki/タイポグリセミア)は文章中のいくつかの単語で最初と最後の文字以外の順番が入れ替わっても正しく読めてしまう現象です(ただし都市伝説/ネットミーム)。

```python:09.py
import random


def typoglycemia(s):
    return s if len(s) < 4 else s[0] + "".join(random.sample([i for i in s[1: -1]], len(s)-2)) + s[-1]


s = "I couldn't believe that I could actually understand what I was reading : the phenomenal power of the human mind .".split()
print(" ".join(map(lambda x: typoglycemia(x), s)))

```
また関数を一行に収めてやりました(何故)。
4文字より少なければそのまま、そうでなければ1文字目の最後の文字以外をシャッフルし、連結して返すという仕組みです。
`random.sample()`は`random.shuffle()`と違って、第一引数がイミュータブル(改変不可)でも大丈夫なのが特徴です。また、`random.shuffle()`は返り値がありませんが、`random.sample()`はリストで返してくれます。


#おわりに
第1章の問題を解いていきましたが、いかがだったでしょうか。変な実装が多かったと思いますが、そこはお茶目です。後半になってくると嫌でもまともな実装をしていかないといけないので今のうちは許してください。
次の章からは解説の量を増やせていけたらと思っています。

"こうしたらコードが短くなる"や、"こうしたほうがいい"などあればぜひコメントください。

それでは第2章の記事でお会いしましょう。