ラベル ビルトイン の投稿を表示しています。 すべての投稿を表示
ラベル ビルトイン の投稿を表示しています。 すべての投稿を表示

2021年9月8日

Grail って何?

こちらで webbrowser ライブラリを紹介しましたが、そのソースコードを見てみると、対応しているブラウザが分かります。
色々なブラウザがサポートされており、(少なくとも私が)聞いたことのないブラウザもあります。

中でも目を引いたのが Grail です。
完全に初耳だったのですが、調べてみると、Python で書かれたブラウザとのことです。冷静に考えて Python がブラウザの実装言語(特にレンダリング)に向いているとは思えませんが、かつてはそのようなブラウザが存在したということが驚きです。

残念ながら(?)、1999年で開発は停止しており、webbrowser ライブラリでも

# Grail, the Python browser. Does anybody still use it?
if shutil.which("grail"):
    register("grail", Grail, None)
と書かれています。

2020年7月25日

Python 3.8 の細かな改善点

Python 3.8 の変更点をまとめたページに、以下のような内容があります。


どういうことかと言うと、以下のように finally節に continue があると、Python 3.7 まではコンパイルエラーが発生していました。
while True:
    try:
        pass
    finally:
        continue

SyntaxError: 'continue' not supported inside 'finally' clause
普通に考えれば、continue により while まで戻るのが当然です。が、実装上の都合として、コンパイルエラーが起こっていました。随分淡白な対応なように思います。また、break や return は finally節の中でも動作するわけで、continue のみコンパイルエラーというのは意外過ぎる対応であると言えます。

Python 3.8 からは、finally節の中の continue が許可されるようになりました。言語として、少し改善されたのではと思います。

2019年11月16日

issubclass() と str.startswith()

ビルトイン関数の issubclass() は、第2引数として type または type の tuple を受け入れます。
>>> issubclass(bool, int)  # 第2引数が type
True

>>> issubclass(bool, (int, float))  # 第2引数が type の tuple
True
複数の型でチェックをしたい時に、issubclass() を複数回呼ぶ必要がないので、単純に便利ですよね。

さらに、この第2引数は、tuple が入れ子になっていても受け入れられます。

>>> issubclass(bool, ((int, bool), float))  # tuple の中に tuple
True

>>> issubclass(bool, (((((int,),),),),))  # 5重の tuple
True



似たようなインターフェースは、str の startswith() にも用いられています。
>>> 'abc'.startswith('a')  # 引数が str
True

>>> 'abc'.startswith(('A', 'a'))  # 引数が str の tuple
True
ここまでは issubclass() と同様です。

ところが、str の startswith() は tuple の入れ子を受け入れません。

>>> 'abc'.startswith((('A', 'a'), 'b'))  # tuple の入れ子はエラー
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Can't convert 'tuple' object to str implicitly
この微妙な違いは、単なる実装の都合と思われます。issubclass() は第2引数が tuple であれば再帰呼び出しをするようになっていて、str の startswith() は再帰呼び出しをしない実装なのでしょう。ちょっとした不統一ですね。
isinstance() と str.endswith() についても同様です。


余談ですが、引数が空の tuple だった場合の動作は共通で、いずれも False が返ります。
>>> issubclass(bool, tuple())
False

>>> 'abc'.startswith(tuple())
False

2019年11月10日

dict の key の注意点

Python で dict の key の重複判定は、以下のように行われます。
(hash(key1) == hash(key2)) and (key1 == key2)
ハッシュによる高速化を実現しつつ、万が一(兆が一?)のハッシュの衝突に備え、== 演算子も併用しています。

== 演算子が併用されることで、仮にハッシュの衝突が起こったとしても、問題とならない場合が多いです。

>>> hash('W')  # ある環境での文字列'W' のハッシュ値
332122003784942950

>>> hash(332122003784942950)  # 同じハッシュ値を持つ整数型
332122003784942950

# 'W' と 332122003784942950 を key とする dict でも
# == 演算子のおかげで別々の key となる
>>> d = {'W': None, 332122003784942950: None}
>>> d
{'W': None, 332122003784942950: None}



他方で、意外にあっさりと困るパターンも見つかります。int, float, bool, complex はハッシュ値の計算、および ==演算子に共通性があるためです。
>>> hash(1)  # 整数 1 のハッシュ値は 1
1
>>> hash(1.0)  # 浮動小数 1.0 のハッシュ値は 1
1
>>> hash(True)  # 論理型 True のハッシュ値は 1
1
>>> hash(1 + 0j)  # 複素数 1 + 0j のハッシュ値は 1
1

>>> 1 == 1.0 == True == 1 + 0j  # 1 と 1.0 と True と 1 + 0j は同値
True

# 1 と 1.0 と True と 1 + 0j を key とする dict を作成すると、
# 重複 key と判定され、1つの key になる
>>> d = {1: None, 1.0: None, True: None, 1 + 0j: None}
>>> d
{1: None}

結論として、dict の key にあまり色々な型を混ぜるのは得策ではありません。可能なら、文字列型と整数型(およびそれを tuple で組み合わせたもの)だけにするのが良いでしょう。

2019年7月22日

演算子の話① NotImplemented とは?

Python のあまり有名でないと思われるビルトイン定数に NotImplemented があります。
これは演算子系の関数を呼び出した時に「戻り値」として使用されます。


簡単に NotImplemented を見る方法として、dict の __lt__() があります。
>>> ret = dict().__lt__(dict())
>>> ret
NotImplemented
>>> type(ret)
<class 'NotImplementedType'>
こちらで述べたように、dict は < 演算子に対応していません。対応していないのですが、dict.__lt__() という関数自体は存在しています。< 演算子から dict.__lt__() が呼び出され、戻り値としてビルトイン定数 NotImplemented(型は NotImplementedType)を返しているということになります。
では、ビルトイン定数 NotImplemented を返すことで、一体どのようなメリットがあるのでしょうか? 次回以降見ていきます。

2019年7月7日

str.isascii() の追加

Python 3.7 の変更点の一つに、str.isascii() の追加があります。

isascii() は文字通り、文字列が ASCIIコードであるかどうかを判定する関数です。
str には isalpha() や islower() といった is***() 系の関数がいくつかありますが、その系統に新たに isacii() が加えられたと考えれば良いと思います。

動作例です。

>>> 'Aa1+%!'.isascii()
True
>>> 'あ'.isascii()
False


なお、str.isascii() 実装前は、以下のような関数で同じことを実現するのが一般的でした。
def is_ascii(string):
    try:
        string.encode('ascii')
        return True
    except UnicodeEncodeError:
        return False

2019年7月6日

dict の要素順

Python 3.6 および 3.7 にかけて行われた大きな変更の一つが、「dict の要素順が挿入順となった」です。

従来の dict では、要素順は特に定められておらず、実装依存となっていました。
以下は CPython 3.5 の動作例です。

>>> d = dict()
>>> d['A'] = 10
>>> d['B'] = 11
>>> d['C'] = 12
>>> d['D'] = 13
>>> d['E'] = 14
>>> d['F'] = 15
>>> d
{'D': 13, 'A': 10, 'F': 15, 'C': 12, 'E': 14, 'B': 11}  # 要素順は規定なし

一方、CPython 3.6 だと以下の動作になります。
>>> d = dict()
>>> d['A'] = 10
>>> d['B'] = 11
>>> d['C'] = 12
>>> d['D'] = 13
>>> d['E'] = 14
>>> d['F'] = 15
>>> d
{'A': 10, 'B': 11, 'C': 12, 'D': 13, 'E': 14, 'F': 15}  # 要素順が挿入順
要素順が実装依存であるよりは、何らかの規則順である方が一般に扱いやすいと考えられ、意味のある変更ではないかと思います。



dict というプログラムの根幹を支える型に変更が加えられた経緯は、以下のようになります。

① 元々 PEP468 として、要素順を挿入順にしたいという提案があった。
https://www.python.org/dev/peps/pep-0468

② PyPy の dict の実装は、挿入順を保つようになっていた(さらに、省メモリ・高速)。

③ CPython 3.6 の dict は、PyPy の実装を参考に、挿入順を保つように変更。さらに省メモリ化・高速化も達成。(ただし、Python 3.6 時点での変更は CPython の実装のみ。)
https://docs.python.org/3.6/whatsnew/3.6.html

④ Python 3.7 からは、dict の仕様が変更されることとなり、挿入順を保つことが求められるようになった。
https://docs.python.org/3.7/library/stdtypes.html

なお、③の CPython の実装を行ったのは、INADA Naokiさんという日本人の方のようです。このような言語コア部分の改善を日本人の方が行っていると思うと胸アツですね。

2019年7月4日

Python で none of

Python とはやや縁遠い世界ですが、C++ の標準 C++11 では、以下の3つの関数が標準に追加されました。
  • bool std::all_of()
  • bool std::any_of()
  • bool std::none_of()

1つ目は Python で言うとビルトイン関数の all()、2つ目は Pythonで言うとビルトイン関数の any() にそれぞれ対応します。
3つ目の std::none_of() に関しては、Python には対応するビルトイン関数はありません。そこで、std::none_of() 相当の関数を作ってみたいと思います。

std::none_of() 相当の関数は、実は極簡単に作ることができます。any() の否定を返すだけで OK です。

def noneof(iterable):
    return not any(iterable)

>>> noneof([0, 1, 0])
False
>>> noneof([0, 0, 0])
True
>>> noneof(i % 2 == 0 for i in [1, 2, 3])
False
>>> noneof(i % 2 == 0 for i in [1, 3, 5])
True
いざ作るとすごく簡単なので、そもそも noneof() という関数が用意されなかったのかもしれませんね。

2016年4月25日

ベンチマーク13 tupleとlist(2)

tuple と list の動作速度について比較します。ここでは、tuple と list の要素へのアクセスにかかる速度を比較します。


ベンチマークのソースコードです。
for文で直接回すパターンと、[i]経由でアクセスするパターンについてそれぞれ計測します。

from benchmarker import Benchmarker

with Benchmarker(10000000, width=20, cycle=3, extra=1) as bench:
    @bench("tuple for")
    def _(bm):
        t = (1, 2, 3)
        for _ in bm:
            for i in t:
                pass

    @bench("list for")
    def _(bm):
        l = [1, 2, 3]
        for _ in bm:
            for i in l:
                pass

    @bench("tuple [i]")
    def _(bm):
        t = (1, 2, 3)
        for _ in bm:
            for i in range(len(t)):
                t[i]

    @bench("list [i]")
    def _(bm):
        l = [1, 2, 3]
        for _ in bm:
            for i in range(len(l)):
                l[i]

計測結果です。
## benchmarker:         release 4.0.1 (for python)
## python version:      3.4.0
## python compiler:     MSC v.1600 32 bit (Intel)
## python platform:     Windows-8-6.2.9200
...

## Ranking      real
list for      5.4142  ( 29.0) ******
tuple for     5.5591  ( 28.2) ******
list [i]     25.0635  (  6.3) *
tuple [i]    25.2364  (  6.2) *

計測結果では list へのアクセスの方が高速となりました。
しかし、その差は僅かであり、実際 tuple の方が高速な回もありました。なので、アクセス速度に関しては tuple と list で同等と見なす方が良さそうです(最低限、アクセス速度を意識して tuple を一旦 list に変換する、というような処理は行うべきではありません)。

2016年4月18日

ベンチマーク12 tupleとlist(1)

tuple と list の動作速度について比較します。ここでは、tuple と list の作成にかかる速度を比較します。


ベンチマークのソースコードです。

from benchmarker import Benchmarker

with Benchmarker(10000000, width=20, cycle=3, extra=1) as bench:
    @bench("tuple")
    def _(bm):
        for _ in bm:
            t = (1, 2, 3)

    @bench("list")
    def _(bm):
        for _ in bm:
            l = [1, 2, 3]

計測結果です。
## benchmarker:         release 4.0.1 (for python)
## python version:      3.4.0
## python compiler:     MSC v.1600 32 bit (Intel)
## python platform:     Windows-8-6.2.9200
...

## Ranking    real
tuple       1.5684  (100.0) ********************
list        6.5198  ( 24.1) *****

tuple の方が高速となりました。しかも、決して軽視できないレベルの差です。

結果より、以下のような変更されないことが分かっている定数を作る場合、tuple の方が動作速度の面で優れているということになります。
ちょっとしたことですが、意識していきたいですね。

# tupleを使用。こちらが推奨される
def accept_color(color):
    return color in ('RGB', 'RGBA')

# listを使用。動作が若干遅い
def accept_color(color):
    return color in ['RGB', 'RGBA']

2016年4月4日

str.center() の奇妙な振る舞い

プチネタを紹介します。

str型は center() という関数を持っています。これは、指定された長さに文字列を伸ばし、中央寄せをするものです。
実際の動作は以下のようになります。

>>> 'a'.center(3)
' a '

>>> 'a'.center(5)
'  a  '

>>> 'aa'.center(4)
' aa '

>>> 'aa'.center(6)
'  aa  '
上の4つの例では、(指定された長さ - 元の文字列の長さ)が偶数です。この場合、挿入される空白文字の数は左右で同じになります。


では、(指定された長さ - 元の文字列の長さ)が奇数の時はどうなるのでしょう。実際の動作を見てみます。

>>> 'a'.center(2)
'a '

>>> 'a'.center(4)
' a  '

>>> 'aa'.center(3)
' aa'

>>> 'aa'.center(5)
'  aa '
左の方が空白が1つ少ない場合と、右の方が空白が1つ少ない場合があります。
あまり統一された動作とは言えませんね。


この動作について、こちらでも議論となっています。
結論は「望ましくない動作である。しかし、古くから(2.x時代から)この動作なので、今になって変更すると却って混乱を招く恐れがある。よって、このままとする」となったようです。

2016年3月28日

クラスメソッド

Python にはクラスメソッドという概念があります。
こちらでスタティックメソッドを紹介しましたが、これをもう少し進めた概念になります。


クラスメソッドは、クラス内に定義したメソッドに @classmethodデコレータを付けることで定義します。
この時、第一引数にはそのクラスの型が渡されます(慣習的に引数名は cls にする)。

# クラスメソッドを持つクラスを定義
>>> class Sample(object):
...     def __init__(self, string):
...         self.string = string
... 
...     # クラスメソッドを定義
...     @classmethod
...     def create_from_file(cls, filename):
...         return cls(open(filename).read())
... 

# クラスメソッドの呼び出し
>>> s = Sample.create_from_file('text.txt')
>>> type(s)
<class '__main__.Sample'>

クラスメソッドとスタティックメソッドはかなり似ている機能です。唯一の違いは、クラスメソッドの「第一引数は必ず型である」という点です。この違いが重要となるのは、クラスメソッドを持つクラスを継承した場合です。

# クラスメソッドを持つクラスを継承
>>> class Derived(Sample):
...     pass
... 

# 継承先でクラスメソッドの呼び出し
>>> d = Derived.create_from_file('text.txt')
>>> type(d)
<class '__main__.Derived'>
上の例では、継承先で creat_from_file() を再定義しなくても、Derivedクラスのインスタンスを取得できています。
スタティックメソッドでこのような動作をさせるためには、継承先でスタティックメソッドをオーバーライドする必要があります。

2016年3月21日

クラスのスタティックメソッド

Python でクラスのスタティックメソッドを定義するには、専用の @staticmethodデコレータを使います。
ここでは @staticmethodデコレータの使い方を紹介します。


スタティックメソッドとして定義したいメソッドの前に @staticmethodデコレータを付けることで、スタティックメソッドとして定義できます。
通常のクラス内メソッドは第一引数を self にしますが、スタティックメソッドでは self は不要です。
定義したスタティックメソッドは、クラス名からでもインスタンスからでも呼び出し可能です。

# extension()をスタティックメソッド
# として持つクラスを定義。
>>> class Png(object):
...     @staticmethod
...     def extension():
...         return 'png'
...

# クラス名から呼ぶ
>>> Png.extension()
'png'

# インスタンスから呼ぶ
>>> png = Png()
>>> png.extension()
'png'

多くの場合、スタティックメソッドは通常の関数で置き換え可能です。スタティックメソッドを使うのは、以下のような場合になります。

  • スタティックメソッドの内容がクラスと密接に結びついており、クラス内に定義した方が見通しがよくなる。
  • インスタンス化にコストが掛かるので、インスタンス化しなくても呼び出し可能としたい。

2016年2月1日

yield from

Python 3.3 で追加された yield from 構文を紹介します。


yield from は、ジェネレータの中でサブイテレータを作る構文です。
最も簡単な例は以下のようなもので、g1() と g2() は同じ動作をします。

# yieldを使用
def g1():
    for i in range(10):
        yield i

# yield fromを使用
def g2():
    yield from range(10)

yield from *** の *** には iterable なオブジェクトを指定します。そして、そのオブジェクトは順に yield されていくことになります。
ジェネレータも iterable なオブジェクトですから、以下のような書き方が可能です。
# 2つのジェネレータを繋げる
def g3():
    yield from g1()
    yield from g2()
このように、ジェネレータを繋げていく時に yield from を使うと、ソースコードの記述が簡単で済みます。


と、ここまでであれば、yield from は「ジェネレータが強化されたのだなぁ」という程度の新機能です。
ところが、Python 3.4 で目玉として追加された asyncio では、ビックリするほどに yield を使いまくります。そして、その中では Python 3.3 で追加された yield from も当たり前のように使われます。
asyncio がどの程度広まっていくかは不明ですが(習得はかなり難しそう)、asyncio に取り掛かる前には、本項で取り上げた yield from を理解しておくべきと思います。

2016年1月11日

sum()関数

ビルトイン関数の1つである sum() のあまり知られていないと思われる仕様について紹介します。

sum() はその名の通り引数の合計を計算する関数です。
最も典型的な使い方は以下のようになります。

>>> c = [1, 2, 3]
>>> sum(c)
6

あまり知られていませんが、sum() は start という第二引数を持っており、そのデフォルト値は 0(int) です。
上例での sum() の中で実際に行われている処理は以下のようになります。
# sum(c)の実処理
>>> ((0 + 1) + 2) + 3
6
第二引数 start は最初の +演算の左辺値として使われます。第二引数を指定した例は以下のようになります。
>>> c = [1, 2, 3]
>>> sum(c, 4)
10

# sum()の実処理
# ((4 + 1) + 2) + 3


では、sum() の引数として二重リストを渡すとどうなるでしょうか。
>>> c = [[1, 2], [3], [4, 5]]
>>> sum(c)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'int' and 'list'

# sum()の実処理
# ((0 + [1, 2]) + [3]) + [4, 5]
int と list は +演算できないというエラーになりました。実処理の方に目をやると、当然の結果と言えますね。

ここで、第二引数にリストを指定すると、演算が可能となります。
第二引数として空リストを渡すと、二重リストのフラット化処理を一文で実現できます(※ 残念ながら動作速度は早くありません。リストのサイズが大きい場合には他の手段を用いるべきです)。

>>> sum(c, [])
[1, 2, 3, 4, 5]

# sum()の実処理
# (([] + [1, 2]) + [3]) + [4, 5]


さらに、sum() の第一引数として文字列のリストを、第二引数として空文字列を渡すとどうなるでしょうか。上のリストの例をから考えると、文字列を結合できそうですが、、、。
>>> c = ['Monty', ' ', 'Python']
>>> sum(c, '')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: sum() can't sum strings [use ''.join(seq) instead]
sum() は文字列に対応していない、というエラーになりました。

この sum() の挙動は、Python のいわば超法規的な仕様といえます。
自然な実装という観点からは、sum() は引数の型を意識すること無く +演算を呼ぶべきです。しかし、頻繁に使われる文字列の結合に関しては、より高速な ''.join() を使わせるために、特例としてエラーが発生するようになっています。
プログラム言語の仕様というと理論が先に立っているものが多いので、このような実を取る仕様は意外な印象を受けますね。

2015年12月19日

ベンチマーク8 localとbuilt-in

こちらでローカル変数とグローバル変数の呼び出し速度について比較しました。
ここでは、ローカル関数とビルトイン関数の呼び出し速度を比較します。


ベンチマークのソースコードです。
str から int へのキャストについて、ビルトインの int() と一旦ローカル変数に代入した int() の速度を計測します。

from benchmarker import Benchmarker

with Benchmarker(1000000, cycle=3, extra=1) as bench:
    s = '1'

    @bench("builtin")
    def _(bm):
        for _ in bm:
            int(s)

    @bench("local")
    def _(bm):
        local_int = int
        for _ in bm:
            local_int(s)

計測結果です。
## benchmarker:         release 4.0.1 (for python)
## python version:      3.4.0
## python compiler:     MSC v.1600 32 bit (Intel)
## python platform:     Windows-8-6.2.9200
...

## Ranking    real
local       1.3894  (100.0) ********************
builtin     1.4500  ( 95.8) *******************
一旦ローカル変数にした方が、若干ですが高速になりました。
何万回というオーダーで int() を呼び出すのが分かっているのであれば、ローカル変数化した方が良いようです。

2015年12月17日

リストに関するTip

set をアルゴリズム的に使ったテクニックを紹介します。

「リストの順番を保ったまま重複を削除する」という処理を考えます。処理の入出力例は以下のようになります。
 入力: [1, 1, 3, 3, 1, 2]
 出力: [1, 3, 2]

実装方法は色々とあるかと思いますが、set と sorted を使えば1行で実装可能です。

# 重複を削除する関数
>>> def remove_duplex(data):
...     return sorted(set(data), key=data.index)
... 

>>> remove_duplex([1, 1, 3, 3, 1, 2])
[1, 3, 2]  # 順番を保ったまま重複が削除される

2015年12月9日

str.isfloatのようなもの

文字列の内容が小数であるかどうかを確かめたい場合があります。
str型は isdecimal()、isdigit()、isnumeric() と文字列が数値であるかどうかを確認するメソッドを持っています。しかし、このいずれのメソッドも文字列が小数であった場合には False を返します。
>>> '1.0'.isdecimal()
False

>>> '1.0'.isdigit()
False

>>> '1.0'.isnumeric()
False


では、文字列が小数であるかどうかをどのように調べるべきでしょうか?
最も簡単なのは、以下の関数のようなやり方です。
>>> def isfloat(string):
...     try:
...         float(string)  # 文字列をfloatにキャスト
...         return True
...     except ValueError:
...         return False
...

>>> isfloat('1.0')
True

>>> isfloat('a')
False

注意点として、あくまで float へキャスト可能かどうかを調べているだけなので、文字列が整数であった場合や指数表記であった場合にも、関数は True を返します。
>>> isfloat('1')  # 整数
True

>>> isfloat('1e-1')  # 指数表記
True

2015年12月3日

NoneTypeの定義

NoneType の定義に関する注意点について述べます。

None の型は NoneType です。

>>> type(None)
<type 'NoneType'>

上記のように、None が NoneType であるというのは明らかな事実なのですが、NoneType を明示的に使おうとするとエラーになってしまいます。
>>> n = None

# NoneType を直接使う
>>> isinstance(n, NoneType)
Traceback (most recent call last):
  File "<stdin>", line 1, in 
NameError: name 'NoneType' is not defined

理由はよく分かりませんが、NoneType は直接使用できないようになっているようです。None チェックするには is None とすべき、という指針への暗黙の推奨なのかもしれません。
とは言え、NoneType を使いたい場合もあります。そのためには、type(None) と書く必要があります。
# type(None)を使用
>>> isinstance(n, type(None))
True

# type(None)を一旦変数に代入
>>> MyNoneType = type(None)
>>> isinstance(n, MyNoneType)
True



参考までに、Python 2 では typesライブラリの中に、NoneType が定義されていました。
# Python 2
>>> import types

>>> isinstance(None, types.NoneType)
True

2015年11月27日

グローバル変数の注意点

Python でグローバル変数を変更する際の注意点を述べます。


グローバル変数をローカルスコープから書き換えるには、スコープ内で "global 変数名" という宣言をする必要があります。
以下は具体的なソースコード例です。

>>> x = 0  # グローバル変数

>>> def func():
...     global x  # グローバル変数を
...               # 取り込むという宣言
...     x = 1
...     print(x)
...
>>> func()
1
>>> x  # グローバル変数も変化
1

この宣言自体はPythonの仕様なのですが、
  • globalという修飾子を使わない言語が多い
  • グローバル変数の読み込みなら宣言不要
上記のような理由のために、しばしば宣言忘れが起こってしまいます。

宣言忘れの結果、見つけにくいバグが埋め込まれることがあります。
以下は、グローバル変数を書き換えたと思ったらローカル変数を新規作成していた、という典型的なバグの例です。func()関数のローカルスコープ内では x が意図通りの振る舞いをするため、一見すると正しい動作に見える点がやっかいです。

>>> x = 0  # グローバル変数

>>> def func():
...     x = 1  # これはローカル
...            # 変数を新規作成
...     print(x)
...
>>> func()
1
>>> x  # グローバル変数は変化なし
0


グローバル変数をローカルスコープから書き換える際は、「スコープ内で global宣言を忘れないように!」というのが本項の結論です。
が、より根本的な話として、グローバル変数の変更はそもそも歓迎されません。「グローバル変数を変更する機会を可能な限り排除する!」これこそ真っ先にやるべきことです。