関数に関するさらなる学習

PythonPythonBeginner
今すぐ練習

This tutorial is from open-source community. Access the source code

💡 このチュートリアルは英語版からAIによって翻訳されています。原文を確認するには、 ここをクリックしてください

はじめに

関数は以前から紹介されていますが、それが実際にどのように機能するかについては、より深いレベルでの詳細はほとんど提供されていません。このセクションでは、いくつかの空白を埋め、呼び出し規約、スコープ規則などについて議論します。

関数の呼び出し

この関数を考えてみましょう。

def read_prices(filename, debug):
  ...

位置引数を使って関数を呼び出すことができます。

prices = read_prices('prices.csv', True)

または、キーワード引数を使って関数を呼び出すこともできます。

prices = read_prices(filename='prices.csv', debug=True)

デフォルト引数

時には、引数を省略可能にしたい場合があります。その場合は、関数定義でデフォルト値を割り当てます。

def read_prices(filename, debug=False):
 ...

デフォルト値が割り当てられている場合、その引数は関数呼び出しで省略可能です。

d = read_prices('prices.csv')
e = read_prices('prices.dat', True)

注: デフォルト値のある引数は、引数リストの末尾に配置する必要があります (すべての必須引数は先頭に配置します)。

省略可能な引数にはキーワード引数を使うことを推奨

これらの2つの異なる呼び出しスタイルを比較してみましょう。

parse_data(data, False, True) #?????

parse_data(data, ignore_errors=True)
parse_data(data, debug=True)
parse_data(data, debug=True, ignore_errors=True)

ほとんどの場合、キーワード引数はコードの明確さを向上させます。特に、フラグとして機能する引数や省略可能な機能に関連する引数の場合です。

設計のベストプラクティス

関数の引数には常に短くて意味のある名前を付けましょう。

関数を使用する人は、キーワード呼び出しスタイルを使用したい場合があります。

d = read_prices('prices.csv', debug=True)

Pythonの開発ツールは、ヘルプ機能やドキュメントに名前を表示します。

値の返却

return 文は値を返します。

def square(x):
    return x * x

返却値が指定されていない場合、または return が欠けている場合、None が返されます。

def bar(x):
    statements
    return

a = bar(4)      ## a = None

## または
def foo(x):
    statements  ## return がない

b = foo(4)      ## b = None

複数の返却値

関数は1つの値のみを返すことができます。ただし、関数はタプルで値を返すことで複数の値を返すことができます。

def divide(a,b):
    q = a // b      ## 商
    r = a % b       ## 余り
    return q, r     ## タプルを返す

使用例:

x, y = divide(37,5) ## x = 7, y = 2

x = divide(37, 5)   ## x = (7, 2)

変数のスコープ

プログラムは変数に値を割り当てます。

x = value ## グローバル変数

def foo():
    y = value ## ローカル変数

変数の割り当ては関数定義の外と内で行われます。関数定義の外で定義された変数は「グローバル」です。関数内の変数は「ローカル」です。

ローカル変数

関数内で割り当てられた変数は非公開です。

def read_portfolio(filename):
    portfolio = []
    for line in open(filename):
        fields = line.split(',')
        s = (fields[0], int(fields[1]), float(fields[2]))
        portfolio.append(s)
    return portfolio

この例では、filenameportfoliolinefields、およびsはローカル変数です。これらの変数は関数呼び出しの後に保持されたり、アクセス可能ではありません。

>>> stocks = read_portfolio('portfolio.csv')
>>> fields
Traceback (most recent call last):
File "<stdin>", line 1, in?
NameError: name 'fields' is not defined
>>>

ローカル変数は他の場所で見つかる変数とも競合しません。

グローバル変数

関数は、同じファイル内で定義されたグローバル変数の値に自由にアクセスできます。

name = 'Dave'

def greeting():
    print('Hello', name)  ## `name` グローバル変数を使用

ただし、関数はグローバル変数を変更できません。

name = 'Dave'

def spam():
  name = 'Guido'

spam()
print(name) ## 'Dave' と表示されます

覚えておいてください: 関数内のすべての代入はローカルです。

グローバル変数の変更

グローバル変数を変更する必要がある場合は、そのように宣言する必要があります。

name = 'Dave'

def spam():
    global name
    name = 'Guido' ## 上のグローバルな name を変更

global 宣言はその使用の前に表示される必要があり、対応する変数は関数と同じファイル内に存在する必要があります。これを見てきたことから、これは不適切な形式であると考えられます。実際、できるだけ global を避けるようにしてください。関数が関数の外のある種の状態を変更する必要がある場合、代わりにクラスを使用する方が良いです(後ほど詳しく説明します)。

引数の渡し方

関数を呼び出すとき、引数の変数は渡された値を参照する名前です。これらの値はコピーではありません。可変データ型(たとえば、リスト、辞書)が渡された場合、それらは「その場で」変更できます。

def foo(items):
    items.append(42)    ## 入力オブジェクトを変更

a = [1, 2, 3]
foo(a)
print(a)                ## [1, 2, 3, 42]

重要なポイント: 関数は入力引数のコピーを受け取りません。

再代入と変更の違い

値を変更することと変数名を再代入することの微妙な違いを理解しておくことを確認しましょう。

def foo(items):
    items.append(42)    ## 入力オブジェクトを変更

a = [1, 2, 3]
foo(a)
print(a)                ## [1, 2, 3, 42]

## VS
def bar(items):
    items = [4,5,6]    ## ローカルの `items` 変数を別のオブジェクトに参照させるように変更

b = [1, 2, 3]
bar(b)
print(b)                ## [1, 2, 3]

メモ: 変数の代入は決してメモリを上書きしません。名前は単に新しい値にバインドされるだけです。

このセットの演習では、おそらくこのコースで最も強力で難しい部分を実装します。たくさんのステップがあり、過去の演習の多くの概念が一度にまとめられています。最終的なソリューションはコードで約25行程度ですが、ゆっくりと時間をかけて、各部分を理解していることを確認してください。

report.py プログラムの中心的な部分はCSVファイルの読み取りに焦点を当てています。たとえば、read_portfolio() 関数はポートフォリオデータの行が含まれるファイルを読み取り、read_prices() 関数は価格データの行が含まれるファイルを読み取ります。これらの関数の両方には、多くの低レベルの「面倒な」部分と同様の機能があります。たとえば、両方ともファイルを開き、csv モジュールでラップし、さまざまなフィールドを新しい型に変換します。

本当に多くのファイル解析を行う場合、おそらくこれらの一部を整理し、より汎用的なものにする必要があります。それが私たちの目標です。

この演習は、fileparse.py と呼ばれるファイルを開いて始めます。ここで私たちが作業を行う場所です。

演習3.3: CSVファイルの読み取り

まずは、CSVファイルを辞書のリストに読み込む問題に焦点を当てましょう。fileparse_3.3.py ファイルに、次のような関数を定義します。

## fileparse_3.3.py
import csv

def parse_csv(filename):
    '''
    CSVファイルをレコードのリストに解析する
    '''
    with open(filename) as f:
        rows = csv.reader(f)

        ## ファイルのヘッダーを読む
        headers = next(rows)
        records = []
        for row in rows:
            if not row:    ## データのない行をスキップ
                continue
            record = dict(zip(headers, row))
            records.append(record)

    return records

この関数は、ファイルを開く詳細、csv モジュールでラップすること、空行を無視することなどの詳細を隠しながら、CSVファイルを辞書のリストに読み込みます。

試してみましょう。

ヒント: python3 -i fileparse_3.3.py

>>> portfolio = parse_csv('/home/labex/project/portfolio.csv')
>>> portfolio
[{'name': 'AA','shares': '100', 'price': '32.20'}, {'name': 'IBM','shares': '50', 'price': '91.10'}, {'name': 'CAT','shares': '150', 'price': '83.44'}, {'name': 'MSFT','shares': '200', 'price': '51.23'}, {'name': 'GE','shares': '95', 'price': '40.37'}, {'name': 'MSFT','shares': '50', 'price': '65.10'}, {'name': 'IBM','shares': '100', 'price': '70.44'}]
>>>

これは良いですが、すべてが文字列として表されているため、データを使って何か有益な計算を行うことはできません。これをすぐに修正しますが、それに引き続き作り上げていきましょう。

✨ 解答を確認して練習

演習3.4: 列選択機能の作成

多くの場合、CSVファイルから選択された特定の列のみに興味があり、すべてのデータには興味がありません。parse_csv() 関数を次のように修正して、ユーザーが指定できる列を選択できるようにしましょう。

>>> ## すべてのデータを読む
>>> portfolio = parse_csv('/home/labex/project/portfolio.csv')
>>> portfolio
[{'name': 'AA','shares': '100', 'price': '32.20'}, {'name': 'IBM','shares': '50', 'price': '91.10'}, {'name': 'CAT','shares': '150', 'price': '83.44'}, {'name': 'MSFT','shares': '200', 'price': '51.23'}, {'name': 'GE','shares': '95', 'price': '40.37'}, {'name': 'MSFT','shares': '50', 'price': '65.10'}, {'name': 'IBM','shares': '100', 'price': '70.44'}]

>>> ## 一部のデータのみを読む
>>> shares_held = parse_csv('/home/labex/project/portfolio.csv', select=['name','shares'])
>>> shares_held
[{'name': 'AA','shares': '100'}, {'name': 'IBM','shares': '50'}, {'name': 'CAT','shares': '150'}, {'name': 'MSFT','shares': '200'}, {'name': 'GE','shares': '95'}, {'name': 'MSFT','shares': '50'}, {'name': 'IBM','shares': '100'}]
>>>

演習2.23で列選択機能の例が示されています。ただし、実装方法の1つは次の通りです。

## fileparse_3.4.py
import csv

def parse_csv(filename, select=None):
    '''
    CSVファイルをレコードのリストに解析する
    '''
    with open(filename) as f:
        rows = csv.reader(f)

        ## ファイルのヘッダーを読む
        headers = next(rows)

        ## 列選択機能が指定されている場合、指定された列のインデックスを見つけます。
        ## 結果の辞書に使用するヘッダーのセットも絞り込みます。
        if select:
            indices = [headers.index(colname) for colname in select]
            headers = select
        else:
            indices = []

        records = []
        for row in rows:
            if not row:    ## データのない行をスキップ
                continue
            ## 特定の列が選択されている場合、行をフィルタリングします
            if indices:
                row = [ row[index] for index in indices ]

            ## 辞書を作成
            record = dict(zip(headers, row))
            records.append(record)

    return records

この部分にはいくつかのポイントがあります。おそらく最も重要なのは、列選択を行インデックスにマッピングすることです。たとえば、入力ファイルに次のようなヘッダーがあったとしましょう。

>>> headers = ['name', 'date', 'time','shares', 'price']
>>>

次に、選択された列が次のようであったとします。

>>> select = ['name','shares']
>>>

適切な選択を行うには、選択された列名をファイル内の列インデックスにマッピングする必要があります。このステップが行っているのがこれです。

>>> indices = [headers.index(colname) for colname in select ]
>>> indices
[0, 3]
>>>

言い換えると、"name" は列0で、"shares" は列3です。ファイルからデータの行を読むとき、インデックスを使ってそれをフィルタリングします。

>>> row = ['AA', '6/11/2007', '9:50am', '100', '32.20' ]
>>> row = [ row[index] for index in indices ]
>>> row
['AA', '100']
>>>
✨ 解答を確認して練習

演習3.5: 型変換の実行

/home/labex/project/fileparse_3.5.py ディレクトリ内の parse_csv() 関数を修正して、返されるデータに対して任意で型変換を適用できるようにしましょう。たとえば:

>>> portfolio = parse_csv('/home/labex/project/portfolio.csv', types=[str, int, float])
>>> portfolio
[{'name': 'AA','shares': 100, 'price': 32.2}, {'name': 'IBM','shares': 50, 'price': 91.1}, {'name': 'CAT','shares': 150, 'price': 83.44}, {'name': 'MSFT','shares': 200, 'price': 51.23}, {'name': 'GE','shares': 95, 'price': 40.37}, {'name': 'MSFT','shares': 50, 'price': 65.1}, {'name': 'IBM','shares': 100, 'price': 70.44}]

>>> shares_held = parse_csv('/home/labex/project/portfolio.csv', select=['name','shares'], types=[str, int])
>>> shares_held
[{'name': 'AA','shares': 100}, {'name': 'IBM','shares': 50}, {'name': 'CAT','shares': 150}, {'name': 'MSFT','shares': 200}, {'name': 'GE','shares': 95}, {'name': 'MSFT','shares': 50}, {'name': 'IBM','shares': 100}]
>>>

演習2.24で既にこれを検討しています。コードの次のフラグメントをソリューションに挿入する必要があります。

...
if types:
    row = [func(val) for func, val in zip(types, row) ]
...
✨ 解答を確認して練習

演習3.6: ヘッダーなしでの作業

一部のCSVファイルにはヘッダー情報が含まれていません。たとえば、prices.csv ファイルは次のようになっています。

"AA",9.22
"AXP",24.85
"BA",44.85
"BAC",11.27
...

/home/labex/project/fileparse_3.6.pyparse_csv() 関数を修正して、このようなファイルに対応できるようにしましょう。代わりにタプルのリストを作成します。たとえば:

>>> prices = parse_csv('/home/labex/project/prices.csv', types=[str,float], has_headers=False)
>>> prices
[('AA', 9.22), ('AXP', 24.85), ('BA', 44.85), ('BAC', 11.27), ('C', 3.72), ('CAT', 35.46), ('CVX', 66.67), ('DD', 28.47), ('DIS', 24.22), ('GE', 13.48), ('GM', 0.75), ('HD', 23.16), ('HPQ', 34.35), ('IBM', 106.28), ('INTC', 15.72), ('JNJ', 55.16), ('JPM', 36.9), ('KFT', 26.11), ('KO', 49.16), ('MCD', 58.99), ('MMM', 57.1), ('MRK', 27.58), ('MSFT', 20.89), ('PFE', 15.19), ('PG', 51.94), ('T', 24.79), ('UTX', 52.61), ('VZ', 29.26), ('WMT', 49.74), ('XOM', 69.35)]
>>>

この変更を行うには、コードを修正して最初のデータ行をヘッダー行として解釈しないようにします。また、キーとして使用する列名がなくなったため、辞書を作成しないようにする必要があります。

✨ 解答を確認して練習

演習3.7: 異なる列区切り文字の選択

CSVファイルは非常に一般的ですが、タブやスペースなど、異なる列区切り文字を使用するファイルに遭遇する可能性もあります。たとえば、portfolio.dat ファイルは次のようになっています。

name shares price
"AA" 100 32.20
"IBM" 50 91.10
"CAT" 150 83.44
"MSFT" 200 51.23
"GE" 95 40.37
"MSFT" 50 65.10
"IBM" 100 70.44

csv.reader() 関数を使用すると、次のように異なる列区切り文字を指定できます。

rows = csv.reader(f, delimiter=' ')

/home/labex/project/fileparse_3.7.pyparse_csv() 関数を修正して、区切り文字の変更も可能にしましょう。

たとえば:

>>> portfolio = parse_csv('/home/labex/project/portfolio.dat', types=[str, int, float], delimiter=' ')
>>> portfolio
[{'name': 'AA','shares': 100, 'price': 32.2}, {'name': 'IBM','shares': 50, 'price': 91.1}, {'name': 'CAT','shares': 150, 'price': 83.44}, {'name': 'MSFT','shares': 200, 'price': 51.23}, {'name': 'GE','shares': 95, 'price': 40.37}, {'name': 'MSFT','shares': 50, 'price': 65.1}, {'name': 'IBM','shares': 100, 'price': 70.44}]
>>>
✨ 解答を確認して練習

解説

ここまでたどり着けたなら、本当に便利な素敵なライブラリ関数を作成したことになります。この関数を使えば、ファイルの内部構造やcsvモジュールの詳細についてあまり心配することなく、任意のCSVファイルを解析し、関心のある列を選択し、型変換を行うことができます。

まとめ

おめでとうございます!あなたは「関数に関するさらなる学習」の実験を完了しました。あなたのスキルを向上させるために、LabExでさらに多くの実験を行って練習することができます。