PythonでFizzBuzz を色々と書いてみた

twitterのタイムラインがFizzBuzzの話題で盛り上がっていたので、やってみた。
普通のFizzBuzzだと面白さが足りないので、4つの実装方法で書いた。

仕様

1から100までの数をプリントするプログラムを書け。ただし3の倍数のときは数の代わりに「Fizz」と、5の倍数のときは「Buzz」とプリントし、3と5両方の倍数の場合には「FizzBuzz」とプリントすること。

実装 1 〜 シンプルなif...elif...

FizzBuzz で最初に挙がる回答だと思う。

#!/usr/bin/env python
# -*- coding: utf-8 -*-


class FizzBuzz(object):
    @classmethod
    def get_results(self, limit=100):
        results = []
        for n in range(1, limit + 1):
            if n % 3 == 0 and n % 5 == 0:
                results.append('FizzBuzz')
            elif n % 3 == 0:
                results.append('Fizz')
            elif n % 5 == 0:
                results.append('Buzz')
            else:
                results.append(str(n))
        return results


if __name__ == '__main__':
    for x in FizzBuzz.get_results():
        print x

実装 2 〜 リスト内包表記

Python三項演算子を使って、こんな感じ。
改行が多いのは、pep8に怒られないようにするためです。

    @classmethod
    def get_results(self, limit=100):
        return ['FizzBuzz' if x % 3 == 0 and x % 5 == 0 \ 
                else 'Fizz' if x % 3 == 0 \ 
                else 'Buzz' if x % 5 == 0 \ 
                else str(x) for x in range(1, limit + 1)] 
参考にしたページ

実装 3 〜 ジェネレータ式

ジェネレータ式を使ってみた。
仕様は100までだけど、大きな数字まで出力する場合はジェネレータ式がいいかも。

    @classmethod
    def get_results(self):
        n = 0
        while n < 100:
            n += 1
            if n % 3 == 0 and n % 5 == 0:
                yield 'FizzBuzz'
            elif n % 3 == 0:
                yield 'Fizz'
            elif n % 5 == 0:
                yield 'Buzz'
            else:
                yield str(n)

実装 4 〜 デコレータ式

数値型をfizzbuzzでラップしてみた・・・というネタ。

#!/usr/bin/env python
# -*- coding: utf-8 -*-


def fizzbuzz(num):
    class Wrapper(object):
        def __init__(self, num):
            self.__num = num
            if num % 3 == 0 and num % 5 == 0:
                self.fizzbuzz = 'FizzBuzz'
            elif num % 3 == 0:
                self.fizzbuzz = 'Fizz'
            elif num % 5 == 0:
                self.fizzbuzz = 'Buzz'
            else:
                self.fizzbuzz = str(num)

        def __getattr__(self, name):
            return getattr(self.__num, name)

        def __dir__(self):
            return dir(self.__num) + ['fizzbuzz']

    return Wrapper(num)


if __name__ == '__main__':
    for x in range(1, 101):
        num = fizzbuzz(x)
        print num.fizzbuzz

git-rebase を多用した開発の流れ

git-rebase を使った開発の流れが固まってきたので、ブログで晒してみます。

この呟きから日数が経っている理由は察してください。
とりあえず、マグナ・ゼロは2週して、黄金魔剣士は2回撃破しました。

まず初めに

  • git-rebase に不慣れな方は真似しない方がいいです
  • reflog でgit-rebase の失敗を戻せない人も真似しない方がいいです
  • 無名ブランチに移動しても泣かないように

開発の流れ

前提
  • git-pull は使わず、git-fetch を使う
  • 追跡ブランチでは作業をしない(必ずトピックブランチを作る)
  • bashにgitのブランチ名を表示しておく(rebaseでコンフリクト起きるのが見えないと危険なので)
0. 作業準備

プロジェクトのディレクトリに移動する。

$ cd ~/Projects/FizzBuzz

作業前にリモートリポジトリの変更を取得する。

$ git fetch

その後、gitk を起動しておく

$ gitk --all &
1. 作業着手

作業するために、トピックブランチを作ります。
トピックブランチは必ずリモートのブランチから作ります。

$ git checkout -b feature/foo origin/develop
2. 作業中

作業は必ずトピックブランチで行います。
トピックブランチのコミットログは下書きなので、綺麗に書いても、適当に書いても、git-now(tmp コミットのための独自サブコマンド git-now)でも構いません。

2.1. 作業中にリモートの変更を取得する

作業中にリモートの変更が気になった場合、git-fetchでリモートの変更を取得する。
git-pull と違い、マージが発生しないため、気軽にfetchできる。

2.2. 今の作業に必要な変更がpushされていた場合

git-fetch 後にログを確認し、変更が必要であればgit-rebase で変更を取り込みます。

$ git fetch
$ git log --oneline -n 10 origin/develop
$ git rebase origin/develop
3. 作業終了

トピックブランチでの作業が終わったら、コミットを整理する。
整理するポイントは2点です。

  • プロジェクトの他メンバとコミットの粒度を合わせる
  • コミットログの文章を整形する(変更の意図が分かるか、チケット番号があるか、誤字・脱字チェック...etc)

まずは、作業中の他の人の変更内容を取得します。

$ git fetch

その後、コミットを整理します。

$ git rebase -i origin/develop
4. 作業終了の確認

rebase で失敗していないか、gitk で確認します。
を押して、意図したコミットログになっているか確認する。

4.1. git-rebase で失敗した場合

もし意図したコミットログになっていない場合、reflog(もしくはgitk)とgit-resetを使って戻します。

5. 変更内容を追跡ブランチに反映する

「3. 作業終了」のgit-rebase が終わると、今いるブランチはトピックブランチになるはずです。
ここでdevelopブランチにトピックブランチの内容を反映させます。

$ git rebase HEAD develop
6. 追跡ブランチに反映した内容の確認

gitk で確認する。
基本的に、git-rebase の後はgitk で意図したログになっているか確認する。

7. 作業内容をpushする

「5. 変更内容を追跡ブランチに反映する」が終わると、今いるブランチはdevelopになるはずです。
ここで、git-push をします。

$ git push

まとめ

メリット
  • git-fetch だと気軽にリモートの変更を取得できる
  • トピックブランチのコミットログを必ず書き換えるので、作業中はログに拘らなくて良い
  • git-rebase を多用するとgit-checkout が減って、タイプ数が減る
  • git rebase -i, git rebase HEAD, git pushのテンポが良い
デメリット
  • git-rebase はWindowsだと遅い
  • git-rebase に不慣れだと手順ミスった時に泣きたくなる

まとめ(140文字)

エイリアスを設定していると、こんな手順です。

エイリアス コマンド
ft fetch
co checkout
rb rebase
rbh rebase HEAD
ps push

bashにgitとmercurialのブランチ名を表示する

環境

変更箇所

.bashrc の「PS1=....」みたいな所を下記のような感じで変更する。

  • .bashrc
# prompt command
hg_branch() {
    hg branch 2> /dev/null | awk '{printf "(hg:" $1 ")"}'
}

git_branch() {
    __git_ps1 '(git:%s)'
}

if [ "$color_prompt" = yes ]; then
PS1='${debian_chroot:+($debian_chroot)}\[\033[01;32m\]\u@\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]$(git_branch)$(hg_branch)\n\$ '
else
PS1='${debian_chroot:+($debian_chroot)}\u@\h:\w$(git_branch)$(hg_branch)\n\$ '
fi
unset color_prompt force_color_prompt

図で分かるgit-rebase

世間的に「Gitはコミットログを書き換えられてキモい」と言われ、肩身が狭いので git-rebase の説明を書いてみた。

git help から引用

まずは基本に忠実に、ヘルプを読みましょう。

  • git help rebase
SYNOPSIS
       git rebase [-i | --interactive] [options] [--onto <newbase>]
               <upstream> [<branch>]
       git rebase [-i | --interactive] [options] --onto <newbase>
               --root [<branch>]

       git rebase --continue | --skip | --abort

DESCRIPTION
       If <branch> is specified, git rebase will perform an automatic git checkout <branch> before doing anything else. Otherwise it remains
       on the current branch.

       All changes made by commits in the current branch but that are not in <upstream> are saved to a temporary area. This is the same set
       of commits that would be shown by git log <upstream>..HEAD (or git log HEAD, if --root is specified).

       The current branch is reset to <upstream>, or <newbase> if the --onto option was supplied. This has the exact same effect as git
       reset --hard <upstream> (or <newbase>). ORIG_HEAD is set to point at the tip of the branch before the reset.

       The commits that were previously saved into the temporary area are then reapplied to the current branch, one by one, in order. Note
       that any commits in HEAD which introduce the same textual changes as a commit in HEAD..<upstream> are omitted (i.e., a patch already
       accepted upstream with a different commit message or timestamp will be skipped).

       It is possible that a merge failure will prevent this process from being completely automatic. You will have to resolve any such
       merge failure and run git rebase --continue. Another option is to bypass the commit that caused the merge failure with git rebase
       --skip. To restore the original <branch> and remove the .git/rebase-apply working files, use the command git rebase --abort instead.

ヘルプ読んで、「なるほど!」と思った方は、このままブラウザのタブを閉じて大丈夫です。
この先にgit-help 以上の情報はありません。


git-helpを貼っただけだと「git-rebaseキモい」の世論は変わらないと思うので、基本的な所を分かりやすく日本語で書いてみました。

git-rebase の概要

まずはgit-rebase の引数について説明します。

  • 基本的な構文*1
$ git rebase [--onto <newbase>] <upstream> [<branch>]

引数は3つあります。
--onto

  • 省略可。省略時はと同じ。
  • 指定はオブジェクト(ブランチ名、タグ名、ハッシュ値、HEAD@{n}・・・など)

  • 必須。
  • 指定はオブジェクト(ブランチ名、タグ名、ハッシュ値、HEAD@{n}・・・など)

  • 省略可。省略時はcurrent branch(今いるブランチ)と同じ。
  • 指定はオブジェクト(ブランチ名、タグ名、ハッシュ値、HEAD@{n}・・・など)

git-rebase の処理の流れ

  1. の共通の祖先を探す。
  2. <共通の祖先>からまでコミットと同じ変更をに追加していく。
  3. がブランチ名なら、ブランチの参照をHEADに再設定する。

1つずつ、図を交えて、順番に説明します。

0. 最初の状態

例として、こんなコミットログがあるとします。

このとき、こんなコマンドを打ったとする。

$ git rebase master topic
1. 共通の祖先を探す

git-rebase は、まず共通の祖先(図の赤い箇所)を探します。

2. 変更をに追加

次に、<共通の祖先>からまでの変更内容(図のオレンジの箇所)をに追加していきます。
今回は --onto オプションを指定していないため、 と同じになります。

  • 変更箇所を


  • に追加する


3. ブランチの参照の更新

最後に、 がブランチ名の場合はブランチの参照が更新されます。

ハッシュ値などの場合、ブランチの参照の更新はされず、HEADがブランチでない場所(無名ブランチ)を指すことになります。

以上がgit-rebase がやっている処理です。

--onto がある場合

上述した「git-rebase の処理の流れ」と同じです。
実際にコマンドを打って、各自で確認してみてください。

$ git rebase --onto master topicA topicB
$ git log --oneline --decorate --graph --all

たぶん、コミットログが↓のように変わるはずです。

  • git-rebase前


  • git-rebase後


gitk

git-rebase に慣れていない間は、gitk を常に起動させておき、git-rebase前後を確認した方が良いです。

$ gitk --all &

他のオプションについて

他のオプションの説明は色んな方が既にブログなどに書いているので、グーグル先生に聞いてください。

*1:私は「git rebase <ココに> <ここから> <ここまで>」で覚えてます。「ここから、ここまでの変更を、ココに追加する」という感じで

.gitconfig に最近追加したエイリアス

2011/05/12のブログで.gitconfig を晒した後に増えたエイリアスを少し解説。
最新の.gitconfigはgithubsinsoku/dotfilesに置いてある。

git ft

ft = fetch -n --prune

タグは後述するfttで取得するため、-n(--no-tags)を設定している。
また、削除されたリモートブランチは自動的に削除するために、--pruneを設定している。*1

git pl

pl = pull --no-tags

タグは後述するfttで取得するため、-n(--no-tags)を設定している。
普段はftを使うので、ほとんど使わない。

git ftt

ftt = fetch -n --prune origin refs/heads/*:refs/remotes/origin/* refs/tags/*:refs/tags/origin/*

自分がローカルで打ったタグとoriginで打たれたタグを分けるために
originで付けられたタグはorigin/とリネームしている。

fetch --tagsという意味でfttのエイリアスに設定してる。

git l

l = log --pretty=format:\"%h (%an): %s\" --date=short -20

普段見るログは簡易に見られればいいので、フォーマットを変更している。
表示はこんな感じ。

d95e8e5 (sinsoku): lgraphの表示を20行に制限
65f1f28 (sinsoku): add .bashrc
33807a4 (sinsoku): .gitconfigにコメント付けたり、少し整理したり

たくさん見たい場合はログ数を指定すれば上書きされる。

$ git l -50

git ls

ls = log --stat -p -1

コミットを指定すると、そのコミットの詳細を表示する。
表示はこんな感じ。

$ git ls d95e8e5
commit d95e8e560fb5ab17ad8cefddd6d23a1e33c39e3a
Author: sinsoku <sinsoku.listy@gmail.com>
Date:   Sun Sep 4 16:28:04 2011 +0900

    lgraphの表示を20行に制限
---
 .gitconfig |    2 +-
 1 files changed, 1 insertions(+), 1 deletions(-)

diff --git a/.gitconfig b/.gitconfig
index 77fbe11..ff67361 100644
--- a/.gitconfig
+++ b/.gitconfig
@@ -27,7 +27,7 @@
        l = log --pretty=format:\"%h (%an): %s\" --date=short -20
        # lf -u でコンフリクトのファイルを表示
        lf = ls-files
-       lgraph = log --oneline --graph --all
+       lgraph = log --oneline --graph --all -20
        lp = log --oneline -p -20
        ls = log --stat -p -1
        mb = merge-base

git cih

cih = commit --amend -C HEAD

1つ前のコミットを修正したくて、ログメッセージを変えない場合に使う。
--amend の時にエディタが起動するのが煩わしい人向け。

git share

git-daemon のオプションがいつも覚えられないので、エイリアスに設定してる。

*1:ローカルにある追跡ブランチは消えない

Gitでコンフリクトした時のための備忘録

コンフリクトしたときに便利そうなので、備忘録を残しておく

Gitのコマンド

コンフリクトしているファイルの一覧を表示する。
$ git ls-files -u [<path>]
ファイルの状態(コンフリクト含む)を表示する。
  • -s でshort-format
  • で特定のディレクトリのファイル一覧を表示できる
$ git status [<path>]
手動で直す方法
  • 修正後にaddが必要
$ vi <path>
(手動でコンフリクトを直す)
$ git add <path>
mergetool でマージする方法
  • -y でデフォルトのマージツールが実行する
  • で1つずつ指定できる
$ git mergetool -y [<path>]
をHEADと同じ状態にする。
  • 修正後にaddが必要
$ git checkout --ours [<path>]
$ git add <path>
をマージで指定したブランチ*1と同じ状態にする。
  • 修正後にaddが必要
$ git checkout --theirs [<path>]
$ git add <path>

grep

admin が含まれているファイル一覧
$ git status -s | grep "admin"
view が含まれていないファイル一覧
$ git status -s | grep -v "view"

awk で部分文字列を取得する

コンフリクト(先頭が"UU")しているファイルの一覧
  • substr(string, m, n): 文字列 string 内の m 番目から始まる長さ n の部分文字列を返す
$ git status -s | grep "^UU" | awk '{print substr($0,4,1000)}'

bash

for文
$ for x in a, b, c; do echo x; done

上記の組み合わせ

コンフリクトしているファイルをHEADが正としてコンフリクトを解消する。
$ for f in `git status -s <path> | grep "^UU" | awk 'print substr($0,4,1000)}'`
do
git checkout --ours $f
git add $f
done

*1:git merge topic とマージしていた場合、topicのこと