Skip to content

929. Unique Email Addresses#17

Open
fhiyo wants to merge 2 commits intomainfrom
929_unique-email-addresses
Open

929. Unique Email Addresses#17
fhiyo wants to merge 2 commits intomainfrom
929_unique-email-addresses

Conversation

@fhiyo
Copy link
Copy Markdown
Owner

@fhiyo fhiyo commented Jun 6, 2024


### ①

正規表現が最初に思いついた。ReDOSが怖いからPythonで正規表現を簡単に選択肢に入れない方がいいのだろうか?
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

読みやすさの観点からは、単純な正規表現であれば使っても良いと思います。一方、複雑な正規表現は、理解に時間がかかるため、あまり読みたくありません…。

normalized_local_name = re.sub('\.|\+.*', '', local_name)
return ''.join([normalized_local_name, '@', domain_name])

return len(set(map(lambda e: normalize(e), emails)))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

パズルを読み解いているような気持ちになりました。あまり読みやすいとは思えませんでした。単純なループで書いたほうが読みやすいと思います。

unique_emails = set()
for email in emails:
    unique_emails.add(normalize(email))
return len(unique_emails)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

うーんそうなんですかね...?自分はパズルと言うほど複雑には感じなかったです。もちろん共通の感覚としてあるなら修正したいですが。

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

許容範囲な気がします。私は、len(set(map(normalize, emails)))の方が、for loopより速く理解できる気がします。

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lambda e: normalize(e) -> normalize
ですか?

パズルというほどではないですが、中間変数を作る選択肢なども様々あるはずで、その中でこれを意識的に選んでいますか。あと、本当はカッコの対応が分かっていないと、シンタックスエラーか分からないはずですが、ひと目見て、シンタックスエラーでないと言えますか。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lambda e: normalize(e) -> normalize

そうです。

中間変数を作る選択肢なども様々あるはずで、その中でこれを意識的に選んでいますか。

様々というほどではないですが、nodchipsさんにご提案いただいたものと比べて選択はしました。どちらがより良いという感覚は無いので明確なものではないんですが。

本当はカッコの対応が分かっていないと、シンタックスエラーか分からないはずですが、ひと目見て、シンタックスエラーでないと言えますか。

ひと目で分かるかというと分からないですね。ただこれって皆さん意識されてるんですか...?そこを気にする感覚が自分には無かったので、意識しないといけないのか気になります。普段は括弧の対応が正しいかはエディタで分かるようにしていて、そういう人も多いと思っていたので。

foo(bar(baz(qux(quux(...))))) と多くなってくると、中間変数を作った方が分かりやすくなる場合も増えるのは分かるんですが。

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

longestCommonPrefix = foldl1 (((.) . (.)) (map fst . takeWhile (uncurry (==))) zip)

昔こういうポイントフリーを投稿しましたが、これは11個の演算子または関数で十分厳しいですね。

return len(set(map(lambda e: normalize(e), emails)))

これ9つです。
私は lambda を書いたら一呼吸入れたほうがいいと思います。

normalized = map(lambda e: normalize(e), emails)
return len(set(normalized))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

一方

return len(set(map(normalize, emails)))

こっちは許容範囲と思いますが、

unique = set(map(normalize, emails))
return len(unique)

一呼吸いれても set にした意図が明確になるように思います。

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

心理学やマーケティングだと、マジカルナンバーといって、短期記憶の限界は7つか4つかというんで、それくらいであふれると思ったほうがいいと思います。

class Solution:
def numUniqueEmails(self, emails: List[str]) -> int:
def normalize(email: str) -> str:
g = re.match('(.*)@(.*)', email)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ で分けるだけの処理であれば、 split() のほうが読みやすいと思います。

local_name, domain_name = email.split('@')

g = re.match('(.*)@(.*)', email)
local_name = g[1]
domain_name = g[2]
normalized_local_name = re.sub('\.|\+.*', '', local_name)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

. または + 以降を空白で置き換える」という処理なのは理解できるのですが、やりたい処理に対して実装がやや複雑に感じました。

normalized_local_name = local_name.split('+')[0]
normalized_local_name = normalized_local_name.replace('.', '')

と平易な書き方をした方が読みやすいと思います。

local_name = g[1]
domain_name = g[2]
normalized_local_name = re.sub('\.|\+.*', '', local_name)
return ''.join([normalized_local_name, '@', domain_name])
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

このくらいの文字列の連結であれば、 '+' を用いたほうが読みやすいと思います。

normalized_local_name + '@' + domain_name

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

あと、f string というのもありますね。

class Solution:
def numUniqueEmails(self, emails: List[str]) -> int:
def normalize(email: str) -> str:
local_name, domain_name = email.rsplit('@', maxsplit=1)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

こちらの方が読みやすく感じます。

def normalize(email: str) -> str:
local_name, domain_name = email.rsplit('@', maxsplit=1)
normalized_local_name = []
for ch in local_name:
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ループで書いても、あまり読みやすくはなっていないように感じました。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

何に対して読みやすくなっていないのでしょうか?

def normalize(email: str) -> str:
local_name, domain_name = email.rsplit('@', maxsplit=1)
local_name = local_name.replace('.', '')
plus_position = local_name.find('+')
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

個人的には local_name = local_name.split('+')[0] のほうが読みやすく感じますが、人によって意見が分かれるかもしれません。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

個人的には local_name = local_name.split('+')[0] のほうが読みやすく感じますが

自分もご提案いただいた方が読みやすい気がします。

local_part = False
ignored_local_part = False
yield ch
continue
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

このcontinue必要でしょうか(上のyieldも同様)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

いらないといえばいらないですね。
自分の意図としては、上からdomainを処理するスイート、'@'を処理するスイート、残りがlocal_partのイメージで書いていて、認知負荷的に下側を見なくても済むように書きたかったのでこうなりました (その旨のコメントはあった方が良かったかも)。

不要では?と考えるのもよく分かるので、どうするのが正解なのかは分からないです...それぞれのトレードオフを踏まえたケースバイケースだろうとは思いますが (そもそももっと良い書き方あればぜひ)。

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@TORUS0818 さんは、early returnと混同しているような気がします。continueがないと同じ文字が2回yieldされませんか?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

local part, ignored local part, domain partと複数のループに分ける方が、分かりやすいと思います。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

上のyieldも同様

なので、2行まとめて不要じゃないかという主張かなと

Copy link
Copy Markdown

@TORUS0818 TORUS0818 Jun 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@liquo-rice さん
確かにここの理解があやふやです。ドキュメント読んできます。

最後の文字が2回出てきちゃう感じでしょうか?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

私は105-106, 110-111行目に対する指摘だと読みました。

                if not local_part:
                    yield ch
                    continue # 不要?
                if ch == '@':
                    local_part = False
                    ignored_local_part = False
                    yield ch
                    continue # 不要?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@liquo-rice さん 確かにここの理解があやふやです。ドキュメント読んできます。

最後の文字が2回出てきちゃう感じでしょうか?

continueしないと、117行目のyieldでもう一度chが出る可能性があります。

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@liquo-rice
理解しました。
今回の場合、たまたま通ってしまいますが、continueがないと@以降が(.以外)ダブって出てきてしまいますね。。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

local part, ignored local part, domain partと複数のループに分ける方が、分かりやすいと思います。

class Solution:
    def numUniqueEmails(self, emails: List[str]) -> int:
        def normalize(email: str) -> Generator[str, None, None]:
            i = 0
            while not (email[i] == '@' or email[i] == '+'):
                if email[i] != '.':
                    yield email[i]
                i += 1
            while email[i] != '@':
                # ignore the substring between '+' and '@' ('@' is excluded)
                i += 1
            while i < len(email):
                yield email[i]
                i += 1

        return len(set(map(lambda e: ''.join(normalize(e)), emails)))

書いてみました (local partに'@'は無い前提にしている)。こちらの方が素直な気がします、ありがとうございます

rsplitで分けてreplace, findで削除。discord上でlocal_partの中で@が含まれる場合について話していた記憶があったので、一応右から検索した (https://discord.com/channels/1084280443945353267/1201211204547383386/1209856265413591140 にあることを確認)。
[RFC5322](https://datatracker.ietf.org/doc/html/rfc5322#section-3.4.1)と[RFC1034](https://www.ietf.org/rfc/rfc1034.txt)を見る限り、domain側に@は多分含まない、と思う。

入力が不正で@が含まれない場合、 `local_name, domain_name = email.rsplit('@', maxsplit=1)` の行でunpackできずにエラーになる。まあいいんじゃないだろうか
Copy link
Copy Markdown

@TORUS0818 TORUS0818 Jun 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ここ想定されているエラーが出ますか?

local_name, domain_name = email.split('@')

だとunpack出来ずにValueErrorを吐くと思いますが。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

自分の環境で試すとエラー出ますね。出なかったですか?

$ ipython
Python 3.11.2 (main, May  3 2023, 18:53:30) [Clang 14.0.0 (clang-1400.0.29.102)]
Type 'copyright', 'credits' or 'license' for more information
IPython 8.13.2 -- An enhanced Interactive Python. Type '?' for help.

In [1]: a, b = 'hoge'.rsplit('@', maxsplit=1)
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[1], line 1
----> 1 a, b = 'hoge'.rsplit('@', maxsplit=1)

ValueError: not enough values to unpack (expected 2, got 1)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

あ、ごめんなさい。

”入力で不正に@が含まれる場合”
と空目していました。

"@"が複数入ってた場合にunpack出来ずにエラー吐く仕様なのかと勘違いしました。。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants