質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

88.90%

関数の引数を減らす方法(グローバル変数の利用?クロージャ?)

解決済

回答 4

投稿 編集

  • 評価
  • クリップ 2
  • VIEW 6,935

study

score 61

悩んでいること

自作の関数について引数が多いと使い勝手が悪いため、引数を少なくしたいです。

change_point(dir, distance, width, height, x, y)の引数を省略して
change_point(dir, distance)と減らしたい場合、理想的な方法を教えて頂けませんでしょうか?

追記
クロージャを使えばグローバル変数を減らせるという記述を見つけたのですが、今回の目的と関係あるでしょうか?
クロージャってどんなときに使うの? 

試したこと

自作の関数(以下にコード記載)について、
change_point(dir, distance, width, height, x, y)の引数の内(width, height, x, y)をグローバル変数にして、
change_point(dir, distance)と二つの引数だけに変更しました。おかげでテストする際も、

assert cahnge_point("RIGHT", 4, 8, 6, 2 ,1)==2 となって読み難かったものが、
assert cahnge_point("RIGHT", 4)==2 と読みやすくなりました。(グローバル変数は別途指定)

しかし、その後下記コードを別のテストファイルにimportしてテストをしようとすると、名前の衝突?のような問題があり、上手くテストが動きませんでした。このようなトラブルがあるので、手元のPythonの入門書にはグローバル変数は安易に使うなと説明されていました。

グローバル変数の使い方に気を付けていれば問題ないのかもしれませんが、引数を減らすもっと良い方法はないでしょうか?
あるいは冗長になってしまっても、関数中で使う変数については、引数として明示的に受け取るべきなのでしょうか?

実際のコード

1番目の入力として、width, height, x座標, y座標、
2番目の入力として、移動方向(UP,DOWN,RIGHT,LEFT)と移動量(-10~10)が与えられた際に、
移動後の座標を計算するコードを書きました。

change_point() 関数は2番目の入力を与えられた際に、移動後の座標を計算する関数です。

移動後の座標を計算するためには、移動方向(UP,DOWN,RIGHT,LEFT)と移動量(-10~10)に加え、
width, height, x座標, y座標が必要なので、それらを全て引数とすると、
change_point(dir, distance, width, height, x, y) と引数が非常に長くなってしまいます。

そこで(width, height, x, y)をグローバル変数にして、change_point(dir, distance) は引数を二つに省略しました。

該当のソースコード

loop_point関数のコードは省略していますが、widthやheightを超えた移動について、0から数えなおす関数です。

def change_point(Dir,Distance):  # ポイントを変更する関数
    global height, width, x, y
    if Dir =="UP":
        y = y + Distance
        y = loop_point(y, height)  # loop_point()はwidth,heightを超えたx,yを規定値に修正
        return y
    if Dir =="DOWN":
        y = y - Distance
        y = loop_point(y, height)
        return y
    if Dir =="RIGHT":
        x = x + Distance
        x = loop_point(x, width)
        return x
    if Dir =="LEFT":
        x = x - Distance
        x = loop_point(x, width)
        return x

def main():
    global width, height, x, y
    width, height, x, y = [int(x) for x in input().split()]
    Dir, Distance = input().split()
    Distance = int(Distance)
    change_point(Dir, Distance)
    print(x, y)

if __name__ == '__main__':
    main()
  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 4

checkベストアンサー

+6

いくつもの変数を複数の関数から扱う場合、クラス(オブジェクト)を使うことをおすすめします。
たとえばこんな感じです。

class Point(object):
    def __init__(self, width, height, x, y):
        """初期化メソッド."""
        self.width = width
        self.height = height
        self.x = x
        self.y = y

    def limit_range(self):
        """x,y を規定値に修正."""
        if self.x > self.width:
            self.x = self.width
        elif self.x < 0:
            self.x = 0
        if self.y > self.height:
            self.y = self.height
        elif self.y < 0:
            self.y = 0

    def move_up(self, distance):
        """UP方向に移動させるメソッド."""
        self.y += distance
        self.limit_range()

    def move_down(self, distance):
        """DOWN方向に移動させるメソッド."""
        self.y -= distance
        self.limit_range()

    def move_right(self, distance):
        """RIGHT方向に移動させるメソッド."""
        self.x += distance
        self.limit_range()

    def move_left(self, distance):
        """LEFT方向に移動させるメソッド."""
        self.x -= distance
        self.limit_range()


if __name__ == '__main__':
    width, height, x, y = [int(x) for x in input().split()]
    point = Point(width, height, x, y)
    Dir, distance = input().split()
    distance = int(distance)
    if Dir == "UP":
        point.move_up(distance)
    if Dir == "DOWN":
        point.move_down(distance)
    if Dir == "RIGHT":
        point.move_right(distance)
    if Dir == "LEFT":
        point.move_left(distance)
    print(point.x, point.y)

これは "Point" というクラスの中に、width, height, x, y  といった変数(インスタンス変数)と、ポイントを移動させる関数(インスタンスメソッド)をセットで納めています。

  • グローバル変数: どこでも使える。それゆえに衝突などのトラブルの原因に...
  • ローカル変数: その関数内だけで使える。なのでほかの関数に引数としていちいち渡さないといけない
  • インスタンス変数: そのクラス(インスタンス)内だけで使える。なので同クラス内の関数同士で共有できる。でもグローバルからは見えない

と、インスタンス変数は「必要な範囲で使い回せる」というグローバル変数やローカル変数の間を取った絶妙な特性を持っています。

またクラスを使うことで機能の拡張も簡単にできます。例えば、x,y を反転させるメソッドを追加するなら、こんなコードですみます。

    def reverse_xy(self):
        """x, yを反転させるメソッド."""
        self.x, self.y = self.y, self.x
        self.limit_range()

また、もし複数のポイントを同時に扱わないといけなくなった場合、元のコードだと処理が非常に複雑になってしまいますが、クラスを使うことでデータを呼び出し元(main)で管理しなくてよいので簡単に実現できます。

point1 = Point(width=640, height=480, x=10, y=20)
point2 = Point(width=640, height=480, x=100, y=50)
point3 = Point(width=640, height=480, x=0, y=0)

point1.move_up(100)
point2.move_left(-10)
point3.move_down(50)

print(point1.x, point1.y)
print(point2.x, point2.y)
print(point3.x, point3.y)

とても便利ですので、ぜひ学習してみてください。

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2017/05/05 00:29

    とても分かり易くご説明頂きありがとうございました。
    グローバル変数やクロージャという言葉にとらわれていましたが、もっと相応しい方法があったのですね。目から鱗が落ちた思いです。
    コード付きで丁寧にご説明頂いたおかげで、大変勉強になりました。
    今後クラスを上手に扱えるように練習してみます。

    キャンセル

+3

関数内でのグローバル変数の使用は最終手段です。安易にグローバル変数を使うと保守性が著しく低下し、バグの温床となります。グローバル変数を使う以外に手段がないという場合を除いて、極力避けるべきです。

widthとheightが固定であり、xとyが変化していきながら、方向と距離を次々と与えていくというのであれば、Pointというそれらを管理するクラスを作って、その中でwidthとheghitを保持、xとyを変化させた方が良いと思います。

# coding: utf-8
class Point:
    """ポインタを表すクラス"""
    def __init__(self, width, height, x, y):
        self.width = width
        self.height = height
        self.x = x
        self.y = y

    def change(self, direction, distance):
        """ポイントを変更するメソッド"""
        if direction == 'RIGHT':
            self.x += distance
        elif direction == 'LEFT':
            self.x -= distance
        elif direction == 'UP':
            self.y += distance
        elif direction == 'DOWN':
            self.y -= distance
        self.loop()

    def loop(self):
        """一周回って補正するメソッド、実際どうすべきかは知らない"""
        self.x = ((self.x + self.width) % (2 * self.width)) - self.width
        self.y = ((self.y + self.height) % (2 * self.height)) - self.height


def main():
    width, height, x, y = [int(x) for x in input().split()]
    point = Point(width, height, x, y)
    direction, distance_str = input().split()
    point.change(direction, int(distance_str))
    print(point.x, point.y)


if __name__ == '__main__':
    main()

このようにすることで次のような利点が得られます。

  • 同時に複数のPointを扱えます。A点とB点、それぞれ与えられて、それぞれ移動すると言った場合でも、独立したPointオブジェクトを作れば独立して処理できます。
  • 変更のメソッド呼び出しは方向と距離だけ渡すことになります。xやyは変更のメソッドによって自動的に変わっていきますので、複数の呼び出しにも対応しています。

参考されている記事の「グローバル変数の宣言をなるべく減らしたい場合」【利用場面1】についてですが、これはES5以前のJavaScriptにおける話だと思われます。

ES5以前のJavaScriptでは、トップレベルのローカル変数やグローバル変数(この二つは厳密には異なる)は読み込まれた全てのJavaScriptファイルに影響を及ぼすため、バグの温床になりやすい物です。それを防ぐために、とりあえず即時関数で囲むという手段がとられていました(ES2015以降は別方法があるので、必要性はなくなりました)。

Pythonのグローバル変数はファイルスコープであり、別のファイルに影響を及ぼすことはありません。そのため、単にスコープを狭くしたい場合にクロージャーを使うことは意味がありません。

むしろ、参考にすべきは【利用場面2】と【利用場面3】です。そして、この二つの利用方法に本質的に同じです。クラス版を高階関数版に書き換えると次のようになります(処理の内容は実質同じです)。

# coding: utf-8
def create_change_point(width, height, x, y):
    """ポインタを変更していく関数を作成する関数"""

    def change(direction, distance):
        """
        ポイントを変更する関数
        x, yはクロージャーによって束縛されてる。
        """
        nonlocal x, y
        if direction == 'RIGHT':
            x += distance
        elif direction == 'LEFT':
            x -= distance
        elif direction == 'UP':
            y += distance
        elif direction == 'DOWN':
            y -= distance
        loop()
        return (x, y)

    def loop():
        """
        一周回って補正するメソッド
        width, hight, x, yはクロージャーによって束縛されてる。
        """
        nonlocal x, y
        x = ((x + width) % (2 * width)) - width
        y = ((y + height) % (2 * height)) - height

    return change


def main():
    width, height, x, y = [int(x) for x in input().split()]
    change_point = create_change_point(width, height, x, y)
    direction, distance_str = input().split()
    (x, y) = change_point(direction, int(distance_str))
    print(x, y)


if __name__ == '__main__':
    main()

なお、クロージャーの利用方法の重要な物の一つとして、map等の関数を受け取る関数に対して、渡す関数をラムダ式等で記述して、その中でローカル変数を使用できるというものがあります(Javaのクロージャーは制限があるため、この方法ぐらいにしか使用できません)。その例が無い時点で、該当の記事は欠陥がある記事とは言えません。(記事の作者は他サイトの記事を適当に引用しているだけで、クロージャーを理解しているとは思えません。自分で考えたクロージャーのコードが1行も書いていない時点で、察することができるレベルです。蛇足としか思えない【参考】のコードはクロージャーとは関係無いばかりか、無限に呼び出せない時点で同等でも何でもありません。参考にはしない方がいいと思います。)

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2017/05/05 00:54

    蛇足を書いたら、投稿が出遅れた…。

    キャンセル

  • 2017/05/05 01:36

    raccyさん、いつも詳しくご説明頂きありがとうございます。
    手元の入門Python3ではクロージャーの利用例が少しだけしかなかったので、今回クロージャーを使った場合の実際のコードで説明して頂きとても勉強になりました。
    nonlocalの使用方法等についても、覚えておきたいと思います。ありがとうございました。

    キャンセル

+1

  1. 入力パラメータを配列もしくはオブジェクトにする

  2. 関数の機能が過剰と思われるので分割する

あたりでしょか。
このあたりのさじ加減はなかなか難しいので
よそのソースを参考にするなど…
--- 追記 ---
オブジェクトの利用に関しては他の方の回答をみていただくとして…

そのクラスが「何を扱うか」を常に意識しておかないと
ごちゃごちゃで分かりにくくて使いにくいものになります。
例えばこんな感じのはどうでしょう

class Gamen:
    def __init__(self, w, h):
        self.width = w
        self.height = h
    def loop_point(self, p):
        # 位置を補正
        # Pointオブジェクトを返す

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    def up(self, d):
        self.y = self.y + d
    def down(self, d):
        self.y = self.y - d
    def right(self, d):
        self.x = self.x + d
    def left(self, d):
        self.x = self.x - d

class Sousa:
    def __init__(self, s, d):
        self.dir = s
        self.distance = d

def change_point(g, p, s):
    if s.dir == "UP":
        p.up(s.distance)
    if s.dir == "DOWN":
        p.down(s.distance)
    if s.dir == "RIGHT":
        p.right(s.distance)
    if s.dir == "LEFT":
        p.left(s.distance)
    r = g.loop_point(p)
    return r

if __name__ == '__main__':
    width, height, x, y = [int(x) for x in input().split()]
    Dir, Distance = input().split()
    Distance = int(Distance)
    g = Gamen(width, height)
    p = Point(x, y)
    s = Sousa(Dir, Distance)
    p = change_point(g, p, s)
    print(p.x, p.y)

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2017/05/05 00:21

    コメント頂きありがとうございます。
    > 入力パラメータを配列もしくはオブジェクトにする
    入力パラメータをオブジェクトにするという部分が難しくて理解できないのですが、例えばテキストファイルにパラメータを書いて、ファイルオブジェクトとして受け取るという事でしょうか?

    キャンセル

  • 2017/05/08 01:18

    追加でのコメントありがとうございます。
    今までオブジェクト指向的な考え方ができていなかったのでとても参考になりました。

    キャンセル

0

関数の引数を少なくしたいのは見た目的な問題なのか
位置引数を覚えなければいけないから少なくしたいのかどちらかわかりませんが
後者であればデフォルト値を最初に決めておけばいいかと思います

x=5

def python(x,y=10,z=20):
    return x*y*z

print(python(x))

height, width, x, yも同じようにデフォルト値を決めておけばどうでしょうか

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2017/05/05 00:16

    引数を少なくしたいと思ったきっかけはテストの際に引数が多く、扱い難く読み難かったことです。
    ですから、見た目の問題と位置引数を覚えなければならない問題の両方が関係していると思います。
    コメントありがとうございました。

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 88.90%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

関連した質問

同じタグがついた質問を見る