言語処理100本ノック2020(python)備忘録00-09

00

s="stressed"
print(s[::-1])

で出力は
desserts
ここで、最初から最後までという意味を持たせようとあえて

s="stressed"
print(s[-1:0:-1])

と書いたところ、
dessert
と出力されてしまう。
これはs[-1:0:-1]の0が最初の文字までではなく、最初の文字の直前までを意味するためである。
s[-1:-1:-1]と書いて解決できるわけでもなく、コロンに挟まれた真ん中には何も書かない以外の選択肢は見つけられなかった。

s="stressed"
print(s[1000::-1])

のように、極端に大きな数字を書いても自動的に長さが解釈され、dessertsのみが出力される。
s[::-1]で全体を逆順かつステップ-1で「スライス」した。

s[8:0:-1]やs[-1:0:-1]がdを消してしまうことに注意。
 

01

s="パタトクカシーー"
ans = s[1::2]
print(ans)

s[::2]でステップ2でスライスした。
出力は
タクシー
である。
問題文の1文字目とは「パ」なのか「タ」なのか?
自然な日本語とプログラミングの日本語でのインデックスの差に苦しむ。

 

02

s1="パトカー"
s2="タクシー"
s=""
for i in range(len(s1)):
    s=s+s1[i]
    s=s+s2[i]
print(s)

都合のいいmergeとかない?と思ったけどこんな操作することあまりないかも。
とりあえず空の文字列を作って、+演算子で連結した。
appendはstrには使えない。

s1="パトカー"
s2="タクシー"
s = [""]*(len(s1)+len(s2))
s[::2]=s1
s[1::2]=s2
print("".join(s))

のような手法もあると聞いた
https://twitter.com/o_nishy/status/1253138227654877185

最初はzip関数を使って

for i in range(len(s1)):
    s = zip(s1, s2)
    print(s)

のようにするとうまくいく課題なのかと思ったが、zip関数はzip objectを返し、うまくいかなかった。

03

s = "Now I need a drink, alcoholic of course, after the heavy lectures involving quantum mechanics."
import re
s2=s[:-1]
t2 = re.split(r',?\s',s2)

l = list(map(len,t2))
print(l)

出力は
[3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5, 8, 9, 7, 9]
ピリオドを処理した後に正規表現で", "と" "でsplitする例である。
美しくない。(ちなみにre.splitの中のrを書かなくても動くが、警告が出る。)
rstripで文末の記号を削除した上で、「(記号)半角スペース」を区切りとするのがよいだろうか。
とりあえず今回はこれで解ける。
map関数を使っているが、splitで分けられているのだからfor文で処理してもいいと思う。

s = "Now I need a drink, alcoholic of course, after the heavy lectures involving quantum mechanics."
##だめな例。カンマが正しく除外されていない。
t = s.split()
print(t)
l = list(map(len,t))
print(l)

検索したら

s.replace('.', '').replace(',', '')

を使っている人がいた。
mapの使い方を学ばせるのかなと思っていたが、そうではなかったのかもしれない。

04

s = "Hi He Lied Because Boron Could Not Oxidize Fluorine. New Nations Might Also Sign Peace Security Clause. Arthur King Can."
s.replace('.', '').replace(',', '')
t = s.split()
f = [1, 5, 6, 7, 8, 9, 15, 16, 19]
for i in range(len(t)):
    if i in f:
        t[i-1] = t[i-1][0]
    else:
        t[i-1] = t[i-1][0:2]

dic = {}
for i in range(len(t)):
    dic[t[i]]=i+1
print(dic)

# print(dic["He"])#こうすると2になる。

出力は
{'H': 1, 'He': 2, 'Li': 3, 'Be': 4, 'B': 5, 'C': 6, 'N': 7, 'O': 8, 'F': 9, 'Ne': 10, 'Na': 11, 'Mi': 12, 'Al': 13, 'Si': 14, 'P': 15, 'S': 16, 'Cl': 17, 'Ar': 18, 'K': 19, 'Ca': 20}
元素記号の覚え型だとした場合マグネシウムがMiになってしまっているが、問題がこれを求めているのでしょうがない。
あと、配列を作ってから辞書を作っているので効率が悪そう。

ということで

s = "Hi He Lied Because Boron Could Not Oxidize Fluorine. New Nations Might Also Sign Peace Security Clause. Arthur King Can."
s.replace('.', '').replace(',', '')
t = s.split()
f = [1, 5, 6, 7, 8, 9, 15, 16, 19]
dic = {}

for i in range(len(t)):
    if (i+1) in f:
        t[i] = t[i][:1]
    else:
        t[i] = t[i][:2]
    dic[t[i]]=i+1

print(dic)

print(dic["He"])#こうすると2になる。

も実装。
もちろん最初からdic[t[i][:1]]=i+1のようにしてもよい。

05

関数を使わないとこうなる。

s = "I am an NLPer"

c_bigram = []
for i in range(len(s)-1):
    c_bigram.append(s[i:i+2])

print(c_bigram)

w_bigram = []
w = s.split()
for i in range(len(w)-1):
    w_bigram.append(w[i:i+2])
    
print(w_bigram)

出力は
['I ', ' a', 'am', 'm ', ' a', 'an', 'n ', ' N', 'NL', 'LP', 'Pe', 'er']
[['I', 'am'], ['am', 'an'], ['an', 'NLPer']]

関数を使うと、

def c_n_gram(s, n):
    ret = []
    for i in range(len(s)-(n-1)):
        ret.append(s[i:i+n])
    return ret

という関数をまず実装する。これは渡したリストの前後をつなげたものを作る関数である。そしてその関数を含む

def c_n_gram(s, n):
    ret = []
    for i in range(len(s)-(n-1)):
        ret.append(s[i:i+n])
    return ret

s = "I am an NLPer"
c_bigram = c_n_gram(s,2)
print(c_bigram)

w = s.split()
w_bigram = c_n_gram(w,2)
print(w_bigram)

とすると渡したリストの要素が文字であれば文字バイグラム、要素が単語であれば単語バイグラムを出力する。

ここで、
文字バイグラムは['I ', ' a', 'am',...]のように2文字の文字列が配列に並ぶ。
単語バイグラムは[['I', 'am'], ['am', 'an'], ['an', 'NLPer']]のように単語という文字列が2つ入った配列がさらに配列になっている。
sは'I am...'だが実際には['I', ' ', 'a', ...]として保存されていると解釈されている。
つまりstr型をcharのlistであると解釈する性質を用いている。
(後に誤りだと気がついた)
いや、str型はイテレータブルであり、文字ごとにイテレートされているのだろう...

06

集合型は実行するたびに表示順が変わるので出力はあくまで例。

s1 = "paraparaparadise"
s2 = "paragraph"

c1_bigram = set()
for i in range(len(s1)-1):
    c1_bigram.add(s1[i:i+2])

print(c1_bigram)

c2_bigram = set()
for i in range(len(s2)-1):
    c2_bigram.add(s2[i:i+2])

print(c2_bigram)

 ##和集合
union = c1_bigram|c2_bigram
print(union)

 ##積集合
intersection = c1_bigram & c2_bigram
print(intersection)

##差(s1-s2)集合
difference = c1_bigram - c2_bigram
print(difference)

##差(s1^s2)集合
symmetric_difference = c1_bigram ^ c2_bigram
print(symmetric_difference)

##seの有無
print("se" in c1_bigram)
print("se" in c2_bigram)

まともなプログラマは作った関数を使うと思います。
したがって以下のように書くんじゃなかろうか。

s1 = "paraparaparadise"
s2 = "paragraph"

def n_gram(s, n):
    ret = []
    for i in range(len(s)-(n-1)):
        ret.append(s[i:i+n])
    return ret

c1_bigram = set(n_gram(s1,2))
print(c1_bigram)

c2_bigram = set(n_gram(s2,2))
print(c2_bigram)

union = c1_bigram|c2_bigram
print(union)

intersection = c1_bigram & c2_bigram
print(intersection)

difference = c1_bigram - c2_bigram
print(difference)

symmetric_difference = c1_bigram ^ c2_bigram
print(symmetric_difference)

print("se" in c1_bigram)
print("se" in c2_bigram)

関数→定数→処理の順で書くかもしれませんね。
出力は
{'di', 'is', 'ap', 'se', 'pa', 'ra', 'ad', 'ar'}
{'ap', 'gr', 'pa', 'ra', 'ag', 'ar', 'ph'}
{'di', 'is', 'ap', 'se', 'pa', 'gr', 'ra', 'ag', 'ad', 'ar', 'ph'}
{'ar', 'ra', 'ap', 'pa'}
{'ad', 'di', 'is', 'se'}
{'se', 'ag', 'ad', 'gr', 'ph', 'di', 'is'}
True
False

ただ、symmetric_differenceも書いています。

07

def jinoha(x,y,z):
    ret = str(x)+'時の'+str(y)+'は'+str(z)
    return ret

x=12
y="気温"
z=22.4

print(jinoha(x,y,z))

yはstrにする必要があるのかはわかりませんでした。関数名は「時のは」です。

ちなみにf文字列というものがあり、

def jinoha(x,y,z):
    return f'{x}時の{y}は{z}です'

x=12
y="気温"
z=22.4

print(jinoha(x,y,z))

と書けるらしいです。C言語のprintfみたいな感じがした。
型をstrにしなくても動く...

08

文字列に
s[i] = 'a'
と代入できなくて不便。
ということは問題05で「str型をcharのlistであると解釈する。」と思っていたが、これはイテレータと呼ばれる動きをするだけなのかな?
とりあえず実装したのはこちら。追記で(下部に)改良を載せる。

def cipher(s):
    s_list = list(s)
    for i in range(len(s_list)):
        if 'a' <= s_list[i] <= 'z':
            tmp = ord(s_list[i])
            tmp = 219-tmp
            tmp = chr(tmp)
            s_list[i] = tmp
    ans = ''.join(s_list)
    return ans

s = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'

sc = cipher(s)
print(sc)

list(s)と、listだと明言してchar型のlistとして扱い、s_list[i]の代入を可能としている。
if 'a' <= s_list[i] <= 'z':のように文字(列)は辞書順で比較可能。*1
ord()関数は文字のUnicode コードポイントを表す整数を返す関数である。
そして''.joinで配列の要素をつなげてansにしている。

ちなみに、

def cipher2(s):
    ret = []
    for c in s:
        if 'a' <= c <= 'z':
            ret += chr(219-ord(c))
        else:
            ret += c
    ans = "".join(ret)
    return ans

s = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'

sc = cipher2(s)
print(sc)

でも動く。
復習時にこちらで書いた。

比較について、a<=b<=cと書け、a<=b and b<=c と書かなくていいのは楽で見やすい。
a <= b >= c も可能。
参考: Pythonでは範囲条件を「a <= value < b」と書ける - Qiita

09

import random

def rand_perm_mid(s):
    if(len(s)<=3):
        return s
    mid = "".join(random.sample(s[1:-1], len(s)-2))
    return s[0]+mid+s[-1]

s="I couldn’t believe that I could actually understand what I was reading : the phenomenal power of the human mind ."
s_w = s.split(' ')

shuffled_list = list(map(rand_perm_mid, s_w))
print(' '.join(shuffled_list))

出力例は
I cndol’ut bvlieee taht I colud auctally uansertdnd what I was rdinaeg : the peeohnmanl pwoer of the hmuan mnid .

sが1文字の場合、最初と最後の文字を間の文字と連結しようとした場合、上記のように書くと-1個の要素をrandom.sample()しようとしてエラーが生じる。
sが2文字の場合、random.sample()において無の入れ替えが生じ、挙動が不安であった(実はlen(s)<=1でも上記は正しく動作する)。
sが3文字の場合、入れ替えが生じないためそのまま文字列を返した。
したがって、 if(len(s)<=3):は if(len(s)<=1):や if(len(s)<=2):でも同じ出力である。

ちなみに、最後のピリオドの前の空白は原文ままであり、英語表記としては間違っていると思った。
このように両端の文字さえ合っていれば他の並びが不適切でも読める現象はTypoglycemiaと呼ばれる。
現実の言語処理では、mind.に対してmdin.という並び替えではTypoglycemiaは生じにくいと思うので、Typoglycemiaを再現したい場合にはピリオドに対して特殊な処理をすべきだろう。

*1:もしcに'aa'が入ってしまっても真になる