Skip to content

Instantly share code, notes, and snippets.

@ged1959
Last active April 22, 2017 04:46
Show Gist options
  • Save ged1959/22f7c3c1ac146186b04e59cee07ed2c8 to your computer and use it in GitHub Desktop.
Save ged1959/22f7c3c1ac146186b04e59cee07ed2c8 to your computer and use it in GitHub Desktop.
書籍「Effective Python」について

この本の事例はなぜ特殊なのか?

draft, created: 2017/04/22;

あえて面倒にした?事例

Item 20: Use None and Docstrings to Specify Dynamic default Arguments

キーワードを使った引数の初期値(keyword argument's default value)を扱った節である。まず、以下の事例を挙げている。

import datetime
import time

def log(message, when=datetime.datetime.now()):
    print('%s: %s' % (when, message))

log('Hi there!')
time.sleep(1)
log('Hi again!')

なお、以下を修正。

# 追加
import datetime
import time

# 修正
def log(message, when=datetime.datetime.now()):
time.sleep(1)
# もとは以下
def log(message, when=datetime.now()):
sleep(0.1)

問題は、以下のような出力結果となること。

2017-04-22 09:23:27.583447: Hi there!
2017-04-22 09:23:27.583447: Hi again!

time.sleep(1)があるのに、出力時間が同じだ。def logを呼び出す度に現在時刻を呼び出し、whenに代入するコードに見えるのだが……。その理由は以下の通り、とのこと。

The timestamps are the same because datetime.now is only executed a single time;when the function is defined. Default argument values are evaluated only once per module load, which usually happens when a program starts up. After the module containing this code is loaded, the datetime.now default argument will never be evaluated again.

という訳で、以下の解決策を提示している。

import datetime
import time

def log(message, when=None):
    """Log a message with a timestamp.

    Args:
        message: Message to print.
        when: datetime of when the message occurred. Defaults to the present time.
    """
    when = datetime.datetime.now() if when is None else when
    print('%s: %s' % (when, message))

log('Hi! there,')
time.sleep(1)
log('Hi! again.')

ただねぇ、というのが正直な心境なのだ。こんな面倒なことをしなくても、以下。

import datetime
import time

def log(message):
    print('%s: %s' % (datetime.datetime.now(), message))

log('Hi! there,')
time.sleep(1)
log('Hi! again.')

結果として、「effective」じゃないよなぁ、と。それから、敢えてコードを面倒にするこの手法は、一体、どこで、どう使うんだ? そう思う。さらに次の事例が続く。

import json

def decode(data, default={}):
    try:
        return json.loads(data)
    except ValueError:
        return default

foo = decode('bad data')
foo['stuff']  = 5
bar = decode('also bad')
bar['meep']  = 1
print('Foo:', foo)
print('Bar:', bar)

なお、import jsonを追加してある。問題は結果。以下。

Foo: {'stuff': 5, 'meep': 1}
Bar: {'stuff': 5, 'meep': 1}

上のコードに以下を追加して実行する。

assert foo is bar
print('foo is bar')

結果は、以下。

Foo: {'stuff': 5, 'meep': 1}
Bar: {'stuff': 5, 'meep': 1}
foo is bar

出力結果が、やはり同じなのだ。なぜだろう。その前に、jsonのこと。以下のサイトを参考にして、復習しておこう。

  1. わかりやすい。[Python] JSONを扱う - YoheiM .NET
  2. 【Python入門】JSON形式データの扱い方 - Qiita
  3. 少し難しい。事例が複雑すぎる。非エンジニアに贈る「具体例でさらっと学ぶJSON」 | Developers.IO

で、jsonとは、である。3.から。

JSON は「JavaScript Object Notation」の略で、「JavaScript 言語の表記法をベースにしたデータ形式」と言えます。しかし、表記が JavaScript ベースなだけで、それ以外のさまざまな言語で利用できます。JSON では、ある数値と、その数値の名前であるキーのペアをコロンで対にして、それらをコンマで区切り、全体を波かっこで括って表現します。

その事例。やはり、3.から。

{
 "book1":{
"title": "Python Beginners",
 "year": 2005 ,
"page": 399
},
"book2":{
 "title": "Python Developers",
 "year": 2006 ,
"page": 650
 }
}

pythonでjsonを扱うには、1.から以下。JSON文字列からPythonオブジェクトに変換するコードは以下の通り。たぶん、正確には、jsonの形式で書かれたテキストデータを、pythonで扱えるオブジェクトに変換する、ということだと思う。

import json

# json文字列の記述方法はいろいろある。
good = ''' 
{
    "good": 333,
    "bad": 444
}
'''
good2 = ''' 
{"good": 333, "bad":444}
'''
good3 = '{"good": 333, "bad":444}'
good4 = '''{"good": 333, "bad":444}'''
foo = json.loads(good)
print(foo)
foo = json.loads(good2)
print(foo)
foo = json.loads(good3)
print(foo)
foo = json.loads(good4)
print(foo)

# もう少し、複雑なjson文字列。
book = '''
{
 "book1":{
"title": "Python Beginners",
 "year": 2005 ,
"page": 399
},
"book2":{
 "title": "Python Developers",
 "year": 2006 ,
"page": 650
 }
}
'''
bar = json.loads(book)
print(bar)

# ついでに、辞書型データのテスト。
print(bar["book2"])
print(bar["book2"]["year"])

出力結果は以下。

{'good': 333, 'bad': 444}
{'good': 333, 'bad': 444}
{'good': 333, 'bad': 444}
{'good': 333, 'bad': 444}
{'book1': {'title': 'Python Beginners', 'year': 2005, 'page': 399}, 'book2': {'title': 'Python Developers', 'year': 2006, 'page': 650}}
{'title': 'Python Developers', 'year': 2006, 'page': 650}
2006

そんなわけで、jsonを理解した上で、実験。問題の事例を改変。そもそも、

foo = decode('bad data')

が意味不明なので。json.loadで直接やってみた。

import json

def decode(data, default={}):
    try:
        return json.loads(data)
    except ValueError:
        return default

dame = json.loads('bad data')
print(dame)

foo = decode('bad data')
foo['stuff']  = 5
bar = decode('also bad')
bar['meep']  = 1
print('Foo:', foo)
print('Bar:', bar)

もちろん、エラー。なので、別の実験。

import json

def decode(data, default={}):
    try:
        return json.loads(data)
    except ValueError:
        return default

foo = decode('bad data')
print('Foo-1:', foo)
foo['stuff']  = 5
print('Foo-2:', foo)
bar = decode('also bad')
print('Foo-3:', foo)
bar['meep']  = 1
print('Foo:', foo)
print('Bar:', bar)

出力結果は以下。

Foo-1: {}
Foo-2: {'stuff': 5}
Foo-3: {'stuff': 5}
Foo: {'stuff': 5, 'meep': 1}
Bar: {'stuff': 5, 'meep': 1}

ということは、変なデータを入れてるのでエラーになり、その結果、return defaultになり、fooは、{}となる。そうであるなら、ちゃんとしたデータをdecodeに入れると、どうなるのだろう。実験。

import json

def decode(data, default={}):
    try:
        return json.loads(data)
    except ValueError:
        return default

good = ''' 
{"hoge": 333, "fuga":444}
'''
bad = ''' 
{"tako": 2, "hebi":6}
'''

foo = decode(good)
foo['stuff']  = 5
bar = decode(bad)
bar['meep']  = 1
print('Foo:', foo)
print('Bar:', bar)

出力は以下。

Foo: {'hoge': 333, 'fuga': 444, 'stuff': 5}
Bar: {'tako': 2, 'hebi': 6, 'meep': 1}

つまり、decodeにちゃんとしたデータが入力されると、順当な結果となる。という、まあ自然な話なのだが、「effective」は、Noneを持ち出して、ややこしくする。冒頭のコードを以下のように改良すべきだというのだ。

import json

def decode(data, default=None):
    """Load JSON data from a string.

    Args:
        data: JSON data to decode.
        default: Value to return if decoding fails. Defaults to an empty dictionary.
    """
    if default is None:
        default = {}
    try:
        return json.loads(data)
    except ValueError:
        return default

foo = decode('bad data')
foo['stuff'] = 5
bar = decode('also bad')
bar['meep'] = 1
print('Foo:', foo)
print('Bar:', bar)

結果は以下。defaultが常にNoneなので、decodeがエラーとなるような引数を入れると、default={}に設定される。つまり、毎回、空のオブジェクトが生成される。その結果、以下。

Foo: {'stuff': 5}
Bar: {'meep': 1}

想定していた出力となる。ただし、これも、最悪、以下でいいのではないか。

import json

def decode(data):
    try:
        return json.loads(data)
    except ValueError:
        return {}

foo = decode('bad data')
foo['stuff']  = 5
bar = decode('also bad')
bar['meep']  = 1
print('Foo:', foo)
print('Bar:', bar)

出力は、以下。想定通りである。

Foo: {'stuff': 5}
Bar: {'meep': 1}

それにしても、これが、どう使えるのか。不思議な事例である。

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