投稿日 2009年8月5日 水曜日 カテゴリ Python 投稿者 omaeコメント(0) » 

omae です。

今回は Python でタイムゾーンが扱いたくなり Trac 0.11 より利用されている pytz を使ってみました。

pytz はタイムゾーンのデータベースを中に取り込んでおり、プラットホームに依存せずにタイムゾーンを扱えるようになっています。同種のライブラリとして Ruby では TZInfo などがあるようです。

大抵の Linux ディストリビューションでは /usr/share/zoneinfo にタイムゾーンのデータベース(tzdata)がインストールされているので二重にデータベースを持っているのが個人的には少し気持ち悪い感じがします。(連携できないのかな?)

http://pypi.python.org/pypi/pytz には egg ファイルが用意されているので、すぐにインストールできるようになっています。ということでさくっとインストールしてみてください。今回使用したのは pytz-2009j です。それでは使ってみます。

まずは、日本のタイムゾーンを取得します。これには pytz.timezone を用います。

>>> from datetime import datetime, tzinfo
>>> import pytz
>>> tz_tokyo = pytz.timezone('Asia/Tokyo')
>>> tz_tokyo
<DstTzInfo 'Asia/Tokyo' CJT+9:00:00 STD>
>>> isinstance(tz_tokyo, tzinfo)
True

ここで返ってくるインスタンスは datetime.tzinfo のインスタンスになります。
次に UTC の現時刻を取得しますが、datetime.utcnow() は使わずに datetime.now(), pytz.utc を使ってタイムゾーンが指定された datetime インスタンスを取得しておきます。

>>> pytz.utc
<UTC>
>>> isinstance(pytz.utc, tzinfo)
True
>>> utcnow = datetime.now(pytz.utc)
>>> utcnow
datetime.datetime(2009, 8, 5, 14, 10, 10, 744246, tzinfo=<UTC>)
>>> format = '%Y-%m-%d %H:%M:%S %Z%z'
>>> utcnow.strftime(format)
'2009-08-05 14:11:26 UTC+0000'

datetime.utcnow() を使用しない理由ですが、tzinfo クラスのメソッドのいくつかは datetime インスタンスにタイムゾーンが指定されていないと動作しないものがあるためです。たとえば pytz.utc.fromutc() に datetime.utcnow() を渡すとエラーになってしまいます。

>>> pytz.utc.fromutc(datetime.utcnow())
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
ValueError: fromutc: dt.tzinfo is not self
>>> pytz.utc.fromutc(utcnow)
datetime.datetime(2009, 8, 5, 14, 11, 26, 183394, tzinfo=<UTC>)

さて、tz_tokyo と utcnow を使って日本時間を表示してみます。

>>> tz_tokyo.fromutc(utcnow)
datetime.datetime(2009, 8, 5, 23, 11, 26, 183394, tzinfo=<DstTzInfo 'Asia/Tokyo' JST+9:00:00 STD>)
>>> tz_tokyo.fromutc(utcnow).strftime(format)
'2009-08-05 23:11:26 JST+0900'

ちゃんと +9 時間になっています。ついでにスウェーデンの現地時刻も表示してみます。

>>> tz_sweden = pytz.timezone('Europe/Stockholm')
>>> tz_sweden
<DstTzInfo 'Europe/Stockholm' CET+1:00:00 STD>
>>> tz_sweden.fromutc(utcnow).strftime(format)
'2009-08-05 15:37:07 CEST+0200'

日本時間から見て -7 時間のようです。

次は pytz からタイムゾーンの一覧ですが、これには pytz.common_timezones を用います。pytz.all_timezones というのもありますが、こちらにはすでに廃止されたタイムゾーンなども含まれているので通常使う分には common_timezones で問題ないと思います。

>>> len(pytz.common_timezones)
393
>>> pytz.common_timezones[:10]
['Africa/Abidjan', 'Africa/Accra', 'Africa/Addis_Ababa', 'Africa/Algiers', 'Africa/Asmara', 'Africa/Bamako', 'Africa/Bangui', 'Africa/Banjul', 'Africa/Bissau', 'Africa/Blantyre']
>>> pytz.common_timezones[-10:]
['Pacific/Wake', 'Pacific/Wallis', 'US/Alaska', 'US/Arizona', 'US/Central', 'US/Eastern', 'US/Hawaii', 'US/Mountain', 'US/Pacific', 'UTC']

このようにタイムゾーン名のリストになっています。実際には利用者に 300 を超えるタイムゾーンの中から選択してもらうのは大変ですので、タイムゾーンごとに GMT からのオフセットを取得することを考えてみます。

tzinfo のリファレンスを参照してみると utcoffset() というメソッドがありオフセットを取得するよいように見えるのですが、pytz が返してくる tzinfo インスタンスに対して utcoffset() を使用すると間違った値を返してくるタイムゾーンが出てきます。

>>> tz_tokyo.utcoffset(utcnow)
datetime.timedelta(0, 32400)    # GMT+09:00
>>> tz_sweden.utcoffset(utcnow)
datetime.timedelta(0, 3600)     # GMT+02:00
>>> tz_fiji = pytz.timezone('Pacific/Fiji')
>>> tz_fiji.utcoffset(utcnow)
datetime.timedelta(0, 42840)    # GMT+11:54?

フィジーのタイムゾーンは http://ja.wikipedia.org/wiki/フィジー にある通り GMT+12:00 (43200) ですが GMT+11:54 (42840) という中途半端な値を返してきてしまいます。

この症状に似たのはないかとバグデータベースのほうを探してみると当てはまるものがあり workaround も示されていました。Bug #310606 in pytz: “Incorrect utcoffset returned from timezones” です。これに従いやり直してみると

>>> tmp = datetime.now()
>>> pytz.utc.localize(tmp) - tz_fiji.localize(tmp)
datetime.timedelta(0, 43200)    # GMT+12:00

となります。この式を用いて (GMT+12:00) Pacific/Fiji のような文字列に変換しオフセットでソートすれば少しは選択しやすくなると思います。

さらに選択しやすくするには

  • 国(ISO-3166)を選択してもらい pytz.country_timezones から該当するタイムゾーンだけを表示する (Google Calendar ではこのようになっています)
  • javascript を用いて new Date().getTimezoneOffset() に該当するタイムゾーンだけを表示する

などの方法があるかと思います。

日本にいると夏時間がなく使用するタイムゾーンは1つしかないので、タイムゾーンを意識してコードを書くことはほとんどありませんが、pytz を知っておけば今後どこかで役に立つことがあるかも知れません。

投稿日 2009年7月30日 木曜日 カテゴリ Python, テスト 投稿者 sugimotoコメント(0) » 

sugimoto です。

Pythonの単体テストのツール unittest を使ってみました。

1. まずはマニュアル通りに。。

Python – unittestのマニュアルから、TestSequenceFunctions を実行します。

# python testsequecefunctions.py
...
----------------------------------------------------------------------
Ran 3 tests in 0.002s

OK

普通に成功しましたね。

2. TestSuite を実行する

今度は複数のTestCaseからなる、TestSuiteを実行してみます。

TestCaseは同じものを使ったので、命名のセンスが悪くてすいません。。

2つのTestCaseをまとめて実行するTestSuite

[testsuite.py]

import unittest
import testsequecefunctions as TestSequenceFunctions
import testsequecefunctions2 as TestSequenceFunctions2

suite1 = unittest.TestLoader().loadTestsFromTestCase(TestSequenceFunctions.TestSequenceFunctions)
suite2 = unittest.TestLoader().loadTestsFromTestCase(TestSequenceFunctions2.TestSequenceFunctions2)
suite = unittest.TestSuite([suite1, suite2])
print suite.countTestCases()

runner = unittest.TextTestRunner()
runner.run(suite)

実行すると

# python testsuite.py
6
......
----------------------------------------------------------------------
Ran 6 tests in 0.002s

OK

6個のテストがすべてパスしました。

3. trac plugin をテストしてみる

trac のestimationtools pluginをテストしてみます。すでにテストコードも提供されているので、今回は実行するだけです。

# unzip estimationtoolsplugin-r6309.zip
# cd estimationtoolsplugin/trunk/estimationtools/tests
# ll
total 24
-rw-r--r-- 1 root root 9264 Jul 29 11:18 burndownchart.py
-rw-r--r-- 1 root root 2696 Jul 30 10:46 hoursremaining.py
-rw-r--r-- 1 root root 2283 Jan 26  2009 workloadchart.py

そのまま実行できるようにスクリプトの最後に以下のコードを追加しておきます。

if __name__ == "__main__":
    unittest.main()

そして実行

# python burndownchart.py
Traceback (most recent call last):
  File "burndownchart.py", line 3, in ?
    from estimationtools.burndownchart import BurndownChart
ImportError: No module named estimationtools.burndownchart

エラーになりました。。

estimationtools.burndownchart がないということですね。。

plugin 本体のディレクトリにパスを通しておきます。

# export PYTHONPATH=/root/plugins/estimationtoolsplugin/trunk/

再度実行します。

# python burndownchart.py
...FFF......
======================================================================
FAIL: test_calculate_timetable_with_closed_and_reopened_ticket (__main__.BurndownChartTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "burndownchart.py", line 140, in test_calculate_timetable_with_closed_and_reopened_ticket
    self.assertEqual(timetable, {day1: Decimal(10), day2: Decimal(0), day3: Decimal(0), day4: Decimal(5)})
AssertionError: {datetime.date(2009, 7, 31): Decimal("0"), datetime.date(2009, 7, 30): Decimal("0"), datetime.date(2009, 8, 1): Decimal("0"), datetime.date(2009, 8, 2): Decimal("5")} != {datetime.date(2009, 7, 31): Decimal("0"), datetime.date(2009, 7, 30): Decimal("10"), datetime.date(2009, 8, 1): Decimal("0"), datetime.date(2009, 8, 2): Decimal("5")}

======================================================================
FAIL: test_calculate_timetable_with_closed_ticket (__main__.BurndownChartTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "burndownchart.py", line 113, in test_calculate_timetable_with_closed_ticket
    self.assertEqual(timetable, {day1: Decimal(10), day2: Decimal(5), day3: Decimal(0)})
AssertionError: {datetime.date(2009, 7, 31): Decimal("0"), datetime.date(2009, 7, 30): Decimal("0"), datetime.date(2009, 8, 1): Decimal("0")} != {datetime.date(2009, 7, 31): Decimal("5"), datetime.date(2009, 7, 30): Decimal("10"), datetime.date(2009, 8, 1): Decimal("0")}

======================================================================
FAIL: test_calculate_timetable_with_closed_ticket2 (__main__.BurndownChartTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "burndownchart.py", line 126, in test_calculate_timetable_with_closed_ticket2
    self.assertEqual(timetable, {day1: Decimal(10), day2: Decimal(0), day3: Decimal(0)})
AssertionError: {datetime.date(2009, 7, 31): Decimal("0"), datetime.date(2009, 7, 30): Decimal("0"), datetime.date(2009, 8, 1): Decimal("0")} != {datetime.date(2009, 7, 31): Decimal("0"), datetime.date(2009, 7, 30): Decimal("10"), datetime.date(2009, 8, 1): Decimal("0")}

----------------------------------------------------------------------
Ran 12 tests in 0.478s

FAILED (failures=3)
#

今度はうまくいった。。と思ったら、なぜかテストが失敗しましたね。。

4. estimationtools plugin のtest suite を実行する

estimationtools plugin には3つのテストがあるので、すべて実行するためのスクリプトを作ります。

[testsuite.py]

import unittest
import burndownchart as burndownchart
import hoursremaining as hoursremaining
import workloadchart as workloadchart

runner = unittest.TextTestRunner()
suite = unittest.TestLoader().loadTestsFromTestCase(burndownchart.BurndownChartTestCase)
runner.run(suite)

suite = unittest.TestLoader().loadTestsFromTestCase(hoursremaining.HoursRemainingTestCase)
runner.run(suite)

suite = unittest.TestLoader().loadTestsFromTestCase(workloadchart.WorkloadChartTestCase)
runner.run(suite)

3つのテストをロードして、実行するだけです。簡単です。

実行してみます。

# python testsuite.py
...FFF......
======================================================================
FAIL: test_calculate_timetable_with_closed_and_reopened_ticket (burndownchart.BurndownChartTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/root/plugins/estimationtoolsplugin/trunk/estimationtools/tests/burndownchart.py", line 140, in test_calculate_timetable_with_closed_and_reopened_ticket
    self.assertEqual(timetable, {day1: Decimal(10), day2: Decimal(0), day3: Decimal(0), day4: Decimal(5)})
AssertionError: {datetime.date(2009, 7, 31): Decimal("0"), datetime.date(2009, 7, 30): Decimal("0"), datetime.date(2009, 8, 1): Decimal("0"), datetime.date(2009, 8, 2): Decimal("5")} != {datetime.date(2009, 7, 31): Decimal("0"), datetime.date(2009, 7, 30): Decimal("10"), datetime.date(2009, 8, 1): Decimal("0"), datetime.date(2009, 8, 2): Decimal("5")}

======================================================================
FAIL: test_calculate_timetable_with_closed_ticket (burndownchart.BurndownChartTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/root/plugins/estimationtoolsplugin/trunk/estimationtools/tests/burndownchart.py", line 113, in test_calculate_timetable_with_closed_ticket
    self.assertEqual(timetable, {day1: Decimal(10), day2: Decimal(5), day3: Decimal(0)})
AssertionError: {datetime.date(2009, 7, 31): Decimal("0"), datetime.date(2009, 7, 30): Decimal("0"), datetime.date(2009, 8, 1): Decimal("0")} != {datetime.date(2009, 7, 31): Decimal("5"), datetime.date(2009, 7, 30): Decimal("10"), datetime.date(2009, 8, 1): Decimal("0")}

======================================================================
FAIL: test_calculate_timetable_with_closed_ticket2 (burndownchart.BurndownChartTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/root/plugins/estimationtoolsplugin/trunk/estimationtools/tests/burndownchart.py", line 126, in test_calculate_timetable_with_closed_ticket2
    self.assertEqual(timetable, {day1: Decimal(10), day2: Decimal(0), day3: Decimal(0)})
AssertionError: {datetime.date(2009, 7, 31): Decimal("0"), datetime.date(2009, 7, 30): Decimal("0"), datetime.date(2009, 8, 1): Decimal("0")} != {datetime.date(2009, 7, 31): Decimal("0"), datetime.date(2009, 7, 30): Decimal("10"), datetime.date(2009, 8, 1): Decimal("0")}

----------------------------------------------------------------------
Ran 12 tests in 0.619s

FAILED (failures=3)
.....
----------------------------------------------------------------------
Ran 5 tests in 0.692s

OK
..
----------------------------------------------------------------------
Ran 2 tests in 0.126s

OK

1つめのテストケースはやっぱり失敗しますが、残りのテストケースはすべて成功しました。

今回は既存のplugin に添付されているテストケースを使いましたが、自分がpluginを作ったときもテストケースを書きたいと思います。

参考
投稿日 2009年7月23日 木曜日 カテゴリ Python 投稿者 morimotoコメント(0) » 

こんばんわ。morimotoです。

前回の数値判定の関数を一括してテストするためにテストクラスを書いてみました。 少々汚いですが暴動とかデモとか起こさないでください。。。

まずはテストクラス function_test.py

# -*- coding: utf-8 -*-
class FunctionTest:
    def display_all(self, tests={}):
        for namespace in tests:
            print ""
            print "*" * 15 + (namespace + " Tests").center(30) + "*" * 15
            print ""
            cases = tests.get(namespace, [])
            for case in cases:
                functions = case.get('functions', [])
                args_list = case.get('args_list', [])
                args_default = case.get('args_default', {})
                self.display(namespace, functions, args_list, args_default)

    def display(self, namespace, functions, args_list, args_default={}):
        if functions:
            format, sep = self.get_format(functions[:], args_list[:])
            if args_default:
                print " * set args default values : %s" % ",".join([" %s = %s"%(name, args_default[name],) for name in args_default])
            print sep
            print format % (('args',) + tuple(functions[:]))
            print sep
            for args in args_list:
                print format % ((str(args),) + tuple([self.execute(namespace, func, args, args_default) for func in functions[:]]))
            print sep
            print ''

    def get_format(self, functions, args_list):
        ph = lambda v: '%' + '%s'%v + 's'
        args_len = args_list and max([len(str(args)) for args in args_list[:]]) or 0
        func_lens = [len(str(func)) for func in functions[:]]
        format = '| ' + ph(args_len) + ' || ' + ' | '.join([ph(fl) for fl in func_lens[:]]) + ' |'
        sep = '+' + '-' * (args_len + 2) + '++' + '+'.join(['-' * (fl+2) for fl in func_lens[:]]) + '+'
        return format, sep

    def execute(self, namespace, function_name, args, args_default={}):
        my_module = __import__(namespace)
        my_func = getattr(my_module, function_name)
        return apply(my_func, self.args_to_tuple(args), self.args_to_dict(args, args_default))

    def args_to_tuple(self, args):
        if isinstance(args, tuple):
            return args
        if isinstance(args, list):
            return tuple(args)
        if isinstance(args, str):
            return tuple([args,])
        return tuple([])
    
    def args_to_dict(self, args, args_default={}):
        if isinstance(args, dict):
            args.update(args_default)
            return args
        return args_default

前回のutilities.py (try-catch でパースするのもテストしてみる)

# -*- coding: utf-8 -*-
import re

# Convert functions
def parseInt(value, default=None):
    (ret,) = validates_integer_of(value) and (int(value),) or \
            (validates_number_of(value) and (int(float(value)),) or (default,))
    return ret

def _parseInt(value, default=None):
    try:
        return int(value)
    except:
        return default

# Validate functions
def validates_number_of(value):
    return re.match(r'^(?![-+]0+$)[-+]?([1-9][0-9]*)?[0-9](\.[0-9]+)?$', '%s'%value) and True or False

def validates_integer_of(value):
    return re.match(r'^(?![-+]0*$)[-+]?([1-9][0-9]*)?0?$', '%s'%value) and True or False

def validates_float_of(value):
    return validates_number_of(value) and not validates_integer_of(value) or False

最後にテストするための test.py

# -*- coding: utf-8 -*-
from function_test import FunctionTest
from utilities import *

test_values = [
    "123456",
    "0",
    "-9",
    "+5",
    "+9",
    "0.0000",
    "1234.56",
    "-123.45",
    "+123.45",
    "-0.123",
    "123.00000",
    "+00000000",
    "-00000000",
    "+0",
    "-0",
    "ABCDEF",
    "1+23",
    "123-",
    "-.123",
    "123.",
    "00012",
    ".123",
    "+0000",
    "-0000",
    "+0000.22",
    "+",
    "-",
    ["12345"],
    ("12345",),
    {"value": "12345"},
]

test_functions = {
    'utilities': [
        {
            'functions': ['validates_number_of', 'validates_integer_of', 'validates_float_of', ],
            'args_list': test_values,
        },
        {
            'functions': ['parseInt', '_parseInt', ],
            'args_list': test_values,
            'args_default': { 'default': None, },
        },
   ]
}

ft = FunctionTest()
ft.display_all(test_functions)

  • test_functions の ‘utilities’ の部分は最初にimportしているテストしたい名前空間名。
  • 配列中の dictionary は引数が同じ(出来れば引数の数や名前が同じ)場合はまとめてしまって構わないです。
  • ‘functions’ は名前の通り関数名のリスト
  • ‘args_list’ はテストしたい引数のリスト。
  • ‘args_default’ は任意で値をセットしたい場合に引数名をキーに対して値を入れる。
  • test_values の下3つはテストクラスのテスト用(笑)
  • この3つのファイルを同じ階層に置いていただき、後はコンソールから

    python test.py
    

    を実行するだけです。

    結果

    >python test.py
    
    ***************       utilities Tests        ***************
    
    +--------------------++---------------------+----------------------+--------------------+
    |               args || validates_number_of | validates_integer_of | validates_float_of |
    +--------------------++---------------------+----------------------+--------------------+
    |             123456 ||                True |                 True |              False |
    |                  0 ||                True |                 True |              False |
    |                 -9 ||                True |                 True |              False |
    |                 +5 ||                True |                 True |              False |
    |                 +9 ||                True |                 True |              False |
    |             0.0000 ||                True |                False |               True |
    |            1234.56 ||                True |                False |               True |
    |            -123.45 ||                True |                False |               True |
    |            +123.45 ||                True |                False |               True |
    |             -0.123 ||                True |                False |               True |
    |          123.00000 ||                True |                False |               True |
    |          +00000000 ||               False |                False |              False |
    |          -00000000 ||               False |                False |              False |
    |                 +0 ||               False |                False |              False |
    |                 -0 ||               False |                False |              False |
    |             ABCDEF ||               False |                False |              False |
    |               1+23 ||               False |                False |              False |
    |               123- ||               False |                False |              False |
    |              -.123 ||               False |                False |              False |
    |               123. ||               False |                False |              False |
    |              00012 ||               False |                False |              False |
    |               .123 ||               False |                False |              False |
    |              +0000 ||               False |                False |              False |
    |              -0000 ||               False |                False |              False |
    |           +0000.22 ||               False |                False |              False |
    |                  + ||               False |                False |              False |
    |                  - ||               False |                False |              False |
    |          ['12345'] ||                True |                 True |              False |
    |         ('12345',) ||                True |                 True |              False |
    | {'value': '12345'} ||                True |                 True |              False |
    +--------------------++---------------------+----------------------+--------------------+
    
     * set args default values :  default = None
    +--------------------++----------+-----------+
    |               args || parseInt | _parseInt |
    +--------------------++----------+-----------+
    |             123456 ||   123456 |    123456 |
    |                  0 ||        0 |         0 |
    |                 -9 ||       -9 |        -9 |
    |                 +5 ||        5 |         5 |
    |                 +9 ||        9 |         9 |
    |             0.0000 ||        0 |      None |
    |            1234.56 ||     1234 |      None |
    |            -123.45 ||     -123 |      None |
    |            +123.45 ||      123 |      None |
    |             -0.123 ||        0 |      None |
    |          123.00000 ||      123 |      None |
    |          +00000000 ||     None |         0 |
    |          -00000000 ||     None |         0 |
    |                 +0 ||     None |         0 |
    |                 -0 ||     None |         0 |
    |             ABCDEF ||     None |      None |
    |               1+23 ||     None |      None |
    |               123- ||     None |      None |
    |              -.123 ||     None |      None |
    |               123. ||     None |      None |
    |              00012 ||     None |        12 |
    |               .123 ||     None |      None |
    |              +0000 ||     None |         0 |
    |              -0000 ||     None |         0 |
    |           +0000.22 ||     None |      None |
    |                  + ||     None |      None |
    |                  - ||     None |      None |
    |          ['12345'] ||    12345 |     12345 |
    |         ('12345',) ||    12345 |     12345 |
    | {'value': '12345'} ||    12345 |     12345 |
    +--------------------++----------+-----------+
    

    _parseInt のほうはボロボロでした。 int()よ、しっかりしてください・・・。

    以上morimotoでした。

    投稿日 2009年7月22日 水曜日 カテゴリ Python 投稿者 matsushitaComments Off 

    こんにちは。松下です。

    ごくごく最近からですが、macを使って開発を行っています。

    また、ごくごく最近からですが、pythonを使って開発を行っています。

    macには標準で、python2.5.1が入っていましたが、3.0を使いたかったのでmac portsで3.0をインストールしました。

    今度は2.4を仕事で使うことになったので、2.4をインストールしました。 用途によって、複数バージョンのpythonを使い分けていたのですが、結構面倒くさいです。

    で、何かいい方法をないか探したところ、複数バージョンのpythonを切り替えることができるpython_selectというものを発見しました。

    早速インストール。

    $sudo port install python_select

    以下のような方法で、バージョンを切り替えることができます。

    $python_select python24
    Selecting version "python24" for python

    バージョンを確認すると、見事に切り替わってます。

    $python -V
    Python 2.4.6

    バージョンの確認は以下のようにします。

    $ python_select -l
    Available versions:
    none python24 python25 python25-apple python30

    バージョンの切り替えが面倒だなと思っている方は是非お試しください。

    投稿日 2009年7月14日 火曜日 カテゴリ Python 投稿者 morimotoComments Off 

    morimotoです。

    今回は数値変換(int())を行う前に、数値変換できる文字列なのかを検証してみましょうという記事です。

    まず数値変換についてです。

    >>> s1 = "12345"
    >>> s2 = "abcde"
    >>> int(s1)
    12345
    >>> int(s2)
    Traceback (most recent call last):
      File "", line 1, in ?
    ValueError: invalid literal for int(): abcde

    s1は数値の文字列なので変換可能ですが、s2は当然数値ではないので変換は出来ません。

    エラーを吐かれたほうが親切な場合も多いですが、煩わしいと思う方も居るかもしれません。

    そこで、変換前に入力値が数値かどうかを判断してから数値を変換するメソッドを作ってみました。

    数値検証用メソッド

    # Validate functions
    # 数値の文字列ならTrueです(Long型 30L とかは対象外)
    def validates_number_of(value):
        return re.match(r'^(?![-+]0+$)[-+]?([1-9][0-9]*)?[0-9](\.[0-9]+)?$', '%s'%value) and True or False
    
    # 整数値かどうかの検証
    def validates_integer_of(value):
        return re.match(r'^(?![-+]0*$)[-+]?([1-9][0-9]*)?0?$', '%s'%value) and True or False
    
    # 数値でintegerじゃなかったらfloatよね?(ちょっと手抜き)
    def validates_float_of(value):
        return validates_number_of(value) and not validates_integer_of(value) or False

    テスト

    +--------------------++---------------------+----------------------+--------------------+
    |               args || validates_number_of | validates_integer_of | validates_float_of |
    +--------------------++---------------------+----------------------+--------------------+
    |             123456 ||                True |                 True |              False |
    |                  0 ||                True |                 True |              False |
    |                 -9 ||                True |                 True |              False |
    |                 +5 ||                True |                 True |              False |
    |                 +9 ||                True |                 True |              False |
    |             0.0000 ||                True |                False |               True |
    |            1234.56 ||                True |                False |               True |
    |            -123.45 ||                True |                False |               True |
    |            +123.45 ||                True |                False |               True |
    |             -0.123 ||                True |                False |               True |
    |          123.00000 ||                True |                False |               True |
    |          +00000000 ||               False |                False |              False |
    |          -00000000 ||               False |                False |              False |
    |                 +0 ||               False |                False |              False |
    |                 -0 ||               False |                False |              False |
    |             ABCDEF ||               False |                False |              False |
    |               1+23 ||               False |                False |              False |
    |               123- ||               False |                False |              False |
    |              -.123 ||               False |                False |              False |
    |               123. ||               False |                False |              False |
    |              00012 ||               False |                False |              False |
    |               .123 ||               False |                False |              False |
    |              +0000 ||               False |                False |              False |
    |              -0000 ||               False |                False |              False |
    |           +0000.22 ||               False |                False |              False |
    |                  + ||               False |                False |              False |
    |                  - ||               False |                False |              False |
    +--------------------++---------------------+----------------------+--------------------+

    数値変換メソッド

    上記検証用のメソッドを用いて作ります。

    # 汚くて申し訳ない!
    def to_int(value, default=None):
        (ret,) = validates_integer_of(value) and (int(value),) or \
                (validates_number_of(value) and (int(float(value)),) or (default,))
        return ret

    テスト

    +--------------------++--------+
    |               args || to_int |
    +--------------------++--------+
    |             123456 || 123456 |
    |                  0 ||      0 |
    |                 -9 ||     -9 |
    |                 +5 ||      5 |
    |                 +9 ||      9 |
    |             0.0000 ||      0 |
    |            1234.56 ||   1234 |
    |            -123.45 ||   -123 |
    |            +123.45 ||    123 |
    |             -0.123 ||      0 |
    |          123.00000 ||    123 |
    |          +00000000 ||   None |
    |          -00000000 ||   None |
    |                 +0 ||   None |
    |                 -0 ||   None |
    |             ABCDEF ||   None |
    |               1+23 ||   None |
    |               123- ||   None |
    |              -.123 ||   None |
    |               123. ||   None |
    |              00012 ||   None |
    |               .123 ||   None |
    |              +0000 ||   None |
    |              -0000 ||   None |
    |           +0000.22 ||   None |
    |                  + ||   None |
    |                  - ||   None |
    +--------------------++--------+

    入力値がintegerでなかったらNoneを返すメソッドができました。

    s = "123o123"
    i = to_int(s)
    if i:
        print "数値ですよ"
    else:
        print "数値じゃないよ"
    

    みたいな使い方もできるのかな?

    以上、morimotoでした。

    次のページ »