0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Pythonを使ったオブジェクト指向の基礎

Posted at

■はじめに

Pythonの基礎構文を学んだので、備忘録として残します。

■クラス

class Person:
    name = 'Taro'
    age = 30
    country = 'Japan'

print(Person.name) # > Taro
print(Person.age) # > 30
print(Person.country) # > Japan

classを定義して、Person.nameでクラスのプロパティ(変数)にアクセスする。

class Person:
    name = 'Taro'
    age = 30
    country = 'Japan'

    def print_profile(self):
        print(f'名前: {self.name}, 年齢: {self.age}, 国籍: {self.country}')

person1 = Person()
print(person1.name)
print(person1.age)
print(person1.country)
person1.print_profile()

Person()でインスタンスを作成して、インスタンス変数やメソッドにアクセスできる。

メソッドの引数に指定したselfにはPersonインスタンスが自動で代入される。

class Person:
    name = 'Taro'
    age = 30
    country = 'Japan'

    def print_profile(self):
        print(f'名前: {self.name}, 年齢: {self.age}, 国籍: {self.country}')

    def set_age(self, new_age):
        self.age = new_age

person1 = Person()
person1.set_age(40)
print(person1.age) # > 40

set_ageメソッドを通して、Personインスタンスが持つageプロパティの値を更新する。

class Person:
    name = 'Taro'
    age = 30
    country = 'Japan'

    def __init__(self, name, age, country):
        self.name = name
        self.age = age
        self.country = country

    def print_profile(self):
        print(f'名前: {self.name}, 年齢: {self.age}, 国籍: {self.country}')

    def set_age(self, new_age):
        self.age = new_age

person1 = Person('Hanako', 20, 'Japan')
print(person1.name)
print(person1.age)
print(person1.country)

__init__がPythonのコンストラクタで、クラスのインスタンス作成時に自動で実行される。

■クラス変数

class Person:
    # クラス変数
    age = 20

# インスタンスの生成
person1 = Person()
person2 = Person()

# 別々のインスタンスであることを確認
print(id(person1), id(person2)) # > 4316664048 4316664048

# クラス変数はインスタスからもアクセス可能
print(person1.age, person2.age) # > 20 20

# クラス変数は同じオブジェクトを参照していることを確認
print(id(person1.age), id(person2.age)) # > 20 20

クラスの直下に変数を書いた場合は、クラス変数になります。

クラス変数にはインスタンスからもアクセスすることができます。

クラス変数は、クラスが持っている変数なので、同じオブジェクトを参照していることがわかります。

class Person:
    # クラス変数
    age = 20

# インスタンスの生成
person1 = Person()
person2 = Person()

# クラス変数を書き換えると、すべてのインスタンスに影響する
Person.age = 50
print(person1.age, person2.age) # > 50 50

クラス変数を書き換えると、クラスのインスタンスからクラス変数を呼び出した場合の値が変わっている事がわかります。

■インスタンス変数

class Person:
    # クラス変数
    age = 20

    # インスタンス変数の初期化
    def set_age(self, new_age):
        self.age = new_age

    # インスタンス変数の初期化
    def set_name(self, new_name):
        self.name = new_name

    def print_info(self):
        print(f'名前: {self.name}, 年齢: {self.age}')

# インスタンスの生成
person1 = Person()
person2 = Person()

person1.set_name('田中太郎')
person2.set_name('鈴木花子')

person1.set_age(50)
person2.set_age(30)

person1.print_info() # > 名前: 田中太郎, 年齢: 50
person2.print_info() # > 名前: 鈴木花子, 年齢: 30

インスタンス名.メソッド名で呼び出した場合、変数に値をセットするとインスタンス変数が初期化されます。

クラス変数とインスタンス変数で同じ変数名がある場合は、インスタンス変数にアクセスすることになります。

■コンストラクタ

class Registry:
    registries = []

    # コンストラクタ
    def __init__(self, name):
        print('コンストラクタの呼び出し')
        self.name = name
        Registry.registries.append(self.name)

    def print_name(self):
        print(f'レジストリ名: {self.name}')

a = Registry('レジストリA')
b = Registry('レジストリB')
a.print_name() # > レジストリ名: レジストリA
b.print_name() # > レジストリ名: レジストリB
print(f'現在の登録レジストリ一覧: {Registry.registries}') # > 現在の登録レジストリ一覧: ['レジストリA', 'レジストリB']

Pythonのコンストラクタ関数は__init__です。

オブジェクト生成時に自動で呼ばれる初期化用メソッドで、初期値の設定に用います。

■インスタンスメソッド

class Person:
    # クラス変数
    person_count = 0

    def __init__(self, name, age, country):
        # インスタンス変数
        self.name = name
        self.age = age
        self.country = country
        Person.person_count += 1

    # インスタンスメソッド
    def say_profile(self, like_food):
        print(f'名前: {self.name}, 年齢: {self.age}, 国: {self.country}, 好きな食べ物: {like_food}')

person1 = Person("太郎", 30, "日本")
person2 = Person("ジョン", 25, "アメリカ")

# インスタンスメソッドの呼び出し
person1.say_profile("寿司")  # > 名前: 太郎, 年齢: 30, 国: 日本, 好きな食べ物: 寿司
person2.say_profile("ピザ")  # > 名前: ジョン, 年齢: 25, 国: アメリカ, 好きな食べ物: ピザ

インスタンスメソッドは、クラスのインスタンスに関連付けられたメソッドです。
第一引数は、自動的にselfを受け取ります(selfはインスタンス自身を参照します。)
インスタンスメソッドは、インスタンス変数にアクセスできます。
インスタンスメソッドは、インスタンスを通じて呼び出します。

インスタンスメソッドが呼び出されるとき、Pythonは自動的にインスタンス自身を第一引数として渡す。

例えば、person1.say_profile("寿司")は、内部でPerson.say_profile(person1, ‘寿司’)に変換されで実行されています。

■クラスメソッド

class Person:
    # クラス変数
    person_count = 0

    def __init__(self, name, age, country):
        # インスタンス変数
        self.name = name
        self.age = age
        self.country = country
        Person.person_count += 1

    # クラスメソッド
    @classmethod
    def get_person_count(cls):
        return f"登録された人の数: {cls.person_count}"

    @classmethod
    def create_instance(cls, name, age, country):
        return cls(name, age, country)

person1 = Person("太郎", 30, "日本")
person2 = Person("ジョン", 25, "アメリカ")

# クラスメソッドの呼び出し
print(Person.get_person_count())  # > 登録された人の数: 2
print(person1.get_person_count()) # > 登録された人の数: 2
print(Person.create_instance("花子", 28, "日本"))  # > <__main__.Person object at 0x40b0e511cd0>

クラスメソッドは、クラス自体に関連付けられたメソッドです。
@classmethodデコレータで定義します。
第一引数として自動的にcls(クラス自身)を受け取ります。
クラス変数にアクセスできます。
インスタンスからもクラスからも呼び出せます。

■スタティックメソッド

class Person:
    # クラス変数
    person_count = 0

    def __init__(self, name, age, country):
        # インスタンス変数
        self.name = name
        self.age = age
        self.country = country
        Person.person_count += 1
	
		# スタティックメソッド
    @staticmethod
    def get_age_after_100_years(age):
        return age + 100

person1 = Person("太郎", 30, "日本")
person2 = Person("ジョン", 25, "アメリカ")

# スタティックメソッドの呼び出し
print(Person.get_age_after_100_years(10))  # > 110
print(person1.get_age_after_100_years(person1.age))  # > 130

スタティックメソッドは、クラスやインスタンスの状態に依存しないメソッドです。
@staticmethodデコレータで定義します。
selfやclsなど、自動的な第一引数はありません。
引数として渡さない限り、インスタンス変数やクラス変数にアクセスできません。
インスタンスからもクラスからも呼び出せます。

なので、スタティックメソッドは通常の関数と同様ですが、クラスの名前空間内に定義されています。

■特殊メソッドと特殊プロパティ

特殊メソッドは、クラス内で定義することで、特定の操作やふるまいをカスタマイズできる特別なメソッドです。

これらは通常、二重アンダースコア(__)で囲まれているため「ダンダーメソッド」とも呼ばれます。

◆__add__メソッド

class Person:
    def __init__(self, age):
        self.age = age

    def __add__(self, other):
        return Person(self.age + other.age)

p1 = Person(25)
p2 = Person(30)
p3 = p1 + p2
print(f'Combined age: {p3} ({p3.age})') # > Combined age: <__main__.Person object at 0x36842511cd0> (55)

__add__メソッドは、+演算子を使うと、Pythonは内部で自動で呼び出されるメソッドです。

p1 + p2で足し算をすると__add__メソッドが自動で呼ばれます。

selfにはp1が、otherにはp2インスタンスが渡ってきて、__add__メソッド内部の処理を実行する事ができます。

◆__str__メソッド

class Person:
    def __init__(self, age):
        self.age = age

    def __add__(self, other):
        return Person(self.age + other.age)

    def __str__(self):
        return f'Person(age={self.age})'

p1 = Person(25)
p2 = Person(30)
p3 = p1 + p2
print(p3) # > Person(age=55)
print(str(p3)) # > Person(age=55)

__str__メソッドは、オブジェクトを文字列で表現するための特殊メソッドです。

__str__メソッドは、print()を実行した際に呼び出されるメソッドです。

または、str(p3)とした場合も__str__メソッドが呼び出されます。

print(p3)とすると、__str__メソッドが実行されて、p3が__str__メソッドの引数に渡されます。

◆__class__プロパティ

class Person:
    def __init__(self, age):
        self.age = age

p1 = Person(25)

print(p1.__class__) # > <class '__main__.Person'>

__class__にアクセスすると、インスタンスのクラスを取得する事ができます。

◆__name__プロパティ

class Person:
    def __init__(self, age):
        self.age = age

p1 = Person(25)

print(Person.__name__) # > Person
print(p1.__class__.__name__) # > Person

__name__プロパティは、名前(クラス名・関数名・モジュール名)を取得する事ができます。

◆__repr__と__str__メソッド

どちらもオブジェクトを文字列で表現するための特殊メソッドです。

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __repr__(self):
        return f"{self.__class__.__name__}(name={self.name}, age={self.age})"

    def __str__(self):
        return f"{self.name} ({self.age}歳)"

person = Person("太郎", 30)
print(person) # > 太郎 (30歳) strがあればstrが優先される。ない場合はreprが使われる
print(repr(person)) # > Person(name=太郎, age=30)
print(str(person)) # > 太郎 (30歳)

repr は主に開発者向けの詳細情報を表示するときに使います。

デバッグや開発中の詳細情報を確認する時に便利です。

str は主にユーザー向けのわかりやすい情報を表示するときに使います。

◆__eq__メソッド

2つのオブジェクトが等しいかどうかを確認するメソッド定義です。

Pythonで==演算子を使用すると、内部的に__eq__メソッドが呼び出されます。

__eq__が定義されていない場合、デフォルトではオブジェクトIDの比較(is演算子と同じ)になります。

__eq__が定義されていると、__eq__の処理でオブジェクトを比較します。

class Animal:
    def __init__(self, name):
        self.name = name

animal_1 = Animal("")
animal_2 = Animal("")
print(animal_1 == animal_2)  # False

animal_1 == animal_2で比較すると、__eq__が定義されていないので、オブジェクトIDの比較を行います。

今回は違うオブジェクトなので、Falseと表示されます。

class Animal:
    def __init__(self, name):
        self.name = name

    def __eq__(self, other):
        return self.name == other.name

animal_1 = Animal("")
animal_2 = Animal("")
print(animal_1 == animal_2)  # True

__eq__を定義すると、__eq__の内部の定義に応じて比較を行います。

今回はnameの比較から同じなので、Trueと判定されます。

◆__call__メソッド

クラスのインスタンスを関数のように呼び出せるようにする特殊メソッドです。


class Logger:
    def __init__(self, level):
        self.level = level

    def __call__(self, message):
        print(f'[{self.level}] {message}')

logger = Logger("INFO")
logger("アプリケーションが起動しました")  # [INFO] アプリケーションが起動しました
logger("エラーが発生しました")  # [INFO] エラーが発生しました
logger("アプリケーションが終了しました")  # [INFO] アプリケーションが終了しました

logger("アプリケーションが起動しました")のように呼び出すと、__call__が自動で呼び出されます。

■クラスの継承

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def greet(self):
        print(f"こんにちは {self.name}")

    def say_age(self):
        print(f"年齢は {self.age} 歳です")

class Employee(Person):
    pass

man = Employee("山田太郎", 30)
man.greet() # > こんにちは 山田太郎
man.say_age() # > 年齢は 30 歳です

class Employee(Person):でclassを継承できます。

greet()メソッドなどは、親クラスのメソッドが呼び出されます。

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def greet(self):
        print(f"こんにちは {self.name}")

    def say_age(self):
        print(f"年齢は {self.age} 歳です")

class Employee(Person):
    def greet(self):
        print(f"お疲れ様です {self.name} さん")

man = Employee("山田太郎", 30)
man.greet() # > お疲れ様です 山田太郎 さん

親クラスのメソッドと同名のメソッドを定義することで、オーバーライドすることができます。

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def greet(self):
        print(f"こんにちは {self.name}")

    def say_age(self):
        print(f"年齢は {self.age} 歳です")

class Employee(Person):
    def greet(self):
        super().greet()
        print(f"お疲れ様です {self.name} さん")

man = Employee("山田太郎", 30)
man.greet()
man.say_age()

super().greet()で親クラスの同名のメソッドを呼び出すこともできます。

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def greet(self):
        print(f"こんにちは {self.name}")

    def say_age(self):
        print(f"年齢は {self.age} 歳です")

class Employee(Person):
    def __init__(self, name, age, phone_number):
        super().__init__(name, age)
        self.phone_number = phone_number

    def greet(self):
        super().greet()
        print(f"お疲れ様です {self.name} さん")
        print(f"電話番号は {self.phone_number} です")

man = Employee("山田太郎", 30, "090-1234-5678")
man.greet()

super().__init__(name, age)で親クラスのコンストラクタを呼び出して、親クラス側でInstance変数に値をセットします。

その後に、Employeeクラスのインスタンス自身に固有のInstance変数をもたせることもできます。

■多重継承

class FlyMixin:
    def fly(self):
        return "I can fly!"

class SwimMixin:
    def swim(self):
        return "I can swim!"

# 多重継承:2つのMixinを継承
class Duck(FlyMixin, SwimMixin):
    def sound(self):
        return "quack!"

duck = Duck()
print(duck.sound())  # quack!
print(duck.fly())    # I can fly!
print(duck.swim())   # I can swim!

多重継承では、一つの子クラスが複数の親クラスから継承できる仕組みです。

flyメソッドやswimメソッドは親クラスのメソッドを実行します。

class A:
    def hello(self):
        print("A")

class B:
    def hello(self):
        print("B")

class C(A, B):
    pass

c = C()
c.hello()  # A が呼ばれる(A → B の順)

print(C.mro())
# [<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <class 'object'>]

両方の親クラスにあるhelloメソッドについては、継承の順序で最初に指定されたAクラスのhelloメソッドを使用します。

Pythonは多重継承におけるメソッド呼び出しの順序を決定するために、C3リニアライゼーションというアルゴリズムを使用しています。

これを「メソッド解決順序」(Method Resolution Order: MRO)と呼びます。

C.mro()とすることで、メソッドを探す順番を確認できます。

今回の場合は、「<class '__main__.C'>」「<class '__main__.A'>」「<class '__main__.B'>」「<class 'object'>」という優先順位になります。

class A:
    def hello(self):
        print("A")

class B(A):
    def hello(self):
        print("B")

class C(A):
    def hello(self):
        print("C")

class D(B, C):
    pass

d = D()
d.hello()  # B が呼ばれる(B → C → A の順)
print(D.mro()) # [<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]

d.hello()を呼び出した場合、Dクラス自身にhelloメソッドは無いので、次の優先順位となるBクラスのhelloメソッドが呼び出されます。

class Animal:
    def __init__(self, name):
        self.name = name

class Dog(Animal):
    def __init__(self, name, breed):
        super().__init__(name)  # Animal.__init__を呼び出す
        self.breed = breed

dog = Dog("ポチ", "柴犬")
print(dog.name, dog.breed)

superで親クラスのメソッドを呼び出す事ができます。

class A:
    def greet(self):
        print("Hello from A")

class B:
    def greet(self):
        print("Hello from B")

class C(A, B):
    def greet(self):
        print("Hello from C")
        
        # 親クラスのメソッドを明示的に呼び出す
        A.greet(self) # > Hello from A
        B.greet(self) # > Hello from B

c = C()
c.greet()

継承関係が複雑な場合に、A.greet(self)のようにして 「クラス名」を指定すれば、MRO に関係なく特定のクラスのメソッドを呼びだす事ができます。

■ポリモフィズム

class Animal:
    def speak(self):
        raise NotImplementedError("サブクラスで実装してください")

class Dog(Animal):
    def speak(self):
        return "ワン!"

class Cat(Animal):
    def speak(self):
        return "ニャー!"

animals = [Dog(), Cat()]

for animal in animals:
    print(animal.speak())

animal がどのクラスのインスタンスでも、そのクラスに speak() があれば動作する。

つまり 同じメッセージ(speak)で異なるオブジェクトを扱える。

これが ポリモーフィズム(多態性)です。

■プラベート変数

Pythonでは外部からアクセスできないプライベート変数はないです。

しかしm命名規則で変数やメソッドの可視性を示す慣習があります。

class Human:
    def __init__(self, name, age):
        self._name = name
        self._age = age

    def get_info(self):
        return self._name, self._age

human = Human("太郎", 30)
print(human._name)
print(human._age)
human._name = "次郎" # 推奨されないが、アクセスは可能
print(human._name)

name, age = human.get_info()
print(name)
print(age)

単一のアンダースコアでの宣言(_variable)は「内部で使用する意味」の慣習で、言語によって強制されるわけではないです。
そのため、クラスの外からアクセスしたり、書き換えたりできます。

class Human:
    def __init__(self, name, age):
        self.__name = name
        self.__age = age

    def get_info(self):
        return self.__name, self.__age

human = Human("太郎", 30)
# print(human.__name) # AttributeError: 'Human' object has no attribute '__name'
print(human._Human__name) # 名前修飾を利用してアクセス

二重のアンダースコアでの宣言(__variable)は、「名前修飾」(name mangling)と呼ばれるメカニズムが働きます。

これにより、クラス名が変数名の前に付加され、外部からのアクセスを難しくなります。

しかし、human._Human__nameというようにすることでアクセスは可能です。

■カプセル化とsetter, getter

カプセル化は、データ(属性)とそのデータを操作するメソッドを一つのクラスにまとめることを指します。

カプセルの中にデータと操作を閉じ込めるイメージです。

  • データの保護
    • オブジェクト内部のデータを外部からの不適切なアクセスや変更から守ることができます。直接変更を制限することで、データの整合性を維持できます。
  • インターフェースの簡素化
    • 複雑な内部実装を隠し、必要最小限の操作だけを公開することで、内部実装の使用者にとって理解しやすく使いやすいコードになります。
  • データの検証
    • setterメソッドを通じてデータを設定することで、値の検証が可能になり、無効なデータの設定を防止できます。
  • 実装の独立性
    • 内部実装の詳細を隠蔽することで、外部のコードに影響を与えずに内部実装を変更できます。これにより保守性と拡張性が向上します。
  • モジュール性の向上
    • 関連するデータと機能を一つのクラスにまとめることで、コードの再利用性が高まり、プログラム全体の構造がより明確になります。
class Person:
    def __init__(self, name, age):
        self._name = name
        self._age = age
		
		# getterメソッド
    def get_name(self):
        return self._name
		
		# setterメソッド(バリデーションすることで、不正なデータがセットされることを防ぐ)
    def set_name(self, name):
        if isinstance(name, str) and name:
            self._name = name
        else:
            raise ValueError("名前は空でない文字列でなければなりません")

    def get_age(self):
        return self._age

    def set_age(self, age):
        if isinstance(age, int) and age > 0:
            self._age = age
        else:
            raise ValueError("年齢は正の整数でなければなりません")

person = Person("Alice", 30)

print(person.get_name())  # Alice
print(person.get_age())   # 30

try:
    person.set_name("")
    person.set_age(25)
except ValueError as e:
    print(e)

print(person.get_name())  # Bob
print(person.get_age())   # 25

_をつけた変数は慣習的にPrivate変数ですが、外部からアクセス可能です。

しかし、getterメソッドやsetterメソッドを通して、アクセスするようにすることで、カプセル化します。

class Person:
    def __init__(self, name):
        self._name = name  # _nameは実データ(非公開変数という慣習)

    @property
    def name(self):
        """name の getter"""
        print("getter が呼ばれました")
        return self._name

    @name.setter
    def name(self, value):
        """name の setter"""
        print("setter が呼ばれました")
        if not isinstance(value, str):
            raise ValueError("name は文字列である必要があります")
        self._name = value

p = Person("田中太郎")

print(p.name)    # getter が呼ばれます
p.name = "山田花子"  # setter が呼ばれます

print(p.name)

getter, setterはデコレーターを使って定義することもできます。

@propertyと書くとgetterメソッドになり、p.nameのようにプロパティにアクセスするとgetterメソッドが呼ばれます。

@プロパティ名.setterと書くとsetterメソッドを定義できます。

p.name = "山田花子"のように値を設定しようとすると、setterメソッドが呼ばれます。

■ファイルの入力

id,name,address,rent
1,パークハウス千代田六番町,東京都千代田区六番町,35万円
2,スペーシア秋葉原,東京都千代田区外神田4,12.8万円
3,チェスターコート御茶ノ水,東京都千代田区神田小川町3,13.9万円

まずは、csvファイルを用意します。

with + open + readを使ってCSVファイルを読み込む

# ファイル全体を一気に読み込む(read)
with open("sample.csv", "r", encoding="utf-8") as f:
    data = f.read()
    print("▼ read() 結果:")
    print(data)
    
# 結果
 read() 結果:
id,name,address,rent
1,パークハウス千代田六番町,東京都千代田区六番町,35万円
2,スペーシア秋葉原,東京都千代田区外神田4,12.8万円
3,チェスターコート御茶ノ水,東京都千代田区神田小川町3,13.9万円

read()はファイル全体を一度に読みます。

そのため、文字列(1つ)が戻り値になります。

小さいファイルをまとめて処理したいときに便利です。

注意点

read()は大きいファイルでは危険(メモリを大量消費)

CSV やログが巨大な場合、一気読みはメモリを圧迫します。

with open("sample.csv", "r", encoding="utf-8") as f:
    for line in f:
        print(line.strip())

そういう時は for line in f: で 1行ずつ読みこみます。

**for line in f:**を使うと、内部的には以下のように動いています:

  1. readline() を実行して 1行読む
  2. その行が line に入りprint(line.strip()) が実行される
  3. 次の行を readline() で読む
  4. 最後の行まで繰り返す

メモリに全部読み込まず「1行ずつ処理」します。

with + open + readlineを使ってCSVファイルを読み込む

# 1行だけ読む(readline)
with open("sample.csv", "r", encoding="utf-8") as f:
    while True:
        line = f.readline()  # 1行ずつ読み込む
        if not line:  # 空文字列なら EOF(ファイルの終わり)
            break
        print(line.strip())
        
# 結果
 readline() 結果:
id,name,address,rent
1,パークハウス千代田六番町,東京都千代田区六番町,35万円
2,スペーシア秋葉原,東京都千代田区外神田4,12.8万円
3,チェスターコート御茶ノ水,東京都千代田区神田小川町3,13.9万円

readline()は1行ずつ読み込みます。
戻り値は、その行の 文字列です。

行を順番に処理したいとき使います。

改行も文字として含まれるので、strip() で削除できます。

**for line in f:**を使う方法と同じように、メモリ効率が良いです。

自分でループする**for line in f:**を使う方がシンプルです。

with + open + readlinesを使ってCSVファイルを読み込む

# 行ごとにリストで読み込む(readlines)
with open("sample.csv", "r", encoding="utf-8") as f:
    lines = f.readlines()
    print("\n▼ readlines() 結果(リスト):")
    print(lines)
    
# 結果
 readlines() 結果リスト:
['id,name,address,rent\n', '1,ザ・パークハウス千代田六番町,東京都千代田区六番町,35万円\n', '2,スペーシア秋葉原,東京都千代田区外神田4,12.8万円\n', '3,チェスターコート御茶ノ水,東京都千代田区神田小川町3,13.9万円']

readlines()はファイルを行ごとのリストで読み込みます。

各行をループで処理したいときに使います。

しかし、readlines()はファイル全体をリストに読み込むのでファイルサイズが大きいと危険です。

◆csv モジュールを用いる

import csv

with open("sample.csv", encoding="utf-8") as f:
    print("\n▼ csv.reader() 結果:")
    reader = csv.reader(f)
    for row in reader:
        print(row)
        
# 結果
 csv.reader() 結果:
['id', 'name', 'address', 'rent']
['1', 'ザ・パークハウス千代田六番町', '東京都千代田区六番町', '35万円']
['2', 'スペーシア秋葉原', '東京都千代田区外神田4', '12.8万円']
['3', 'チェスターコート御茶ノ水', '東京都千代田区神田小川町3', '13.9万円']

CSVは単純な "A,B,C" だけではなく、例えば次のようなケースもあります。

"山田,太郎", 30, "東京\n"
  • 名前の中に カンマ が入っている
  • 住所の中に 改行 が入っている
  • 値が ダブルクォートで囲われている

これらを split(",") で処理すると壊れます。

そのため、csv.reader はすべて正しくパースできます。

また、csv.reader は

  • 大規模CSV(数GB)
  • 行数が100万行以上のログ

などでも 1行ずつ処理できるため、メモリを圧迫しません。

with open("sample.csv", "r", encoding="utf-8") as f:
    reader = csv.DictReader(f)

    print("▼ DictReader の読み込み結果")
    for row in reader:
        print(row)  # 各行が辞書として出力される
        
# 結果
 DictReader の読み込み結果
{'id': '1', 'name': 'ザ・パークハウス千代田六番町', 'address': '東京都千代田区六番町', 'rent': '35万円'}
{'id': '2', 'name': 'スペーシア秋葉原', 'address': '東京都千代田区外神田4', 'rent': '12.8万円'}
{'id': '3', 'name': 'チェスターコート御茶ノ水', 'address': '東京都千代田区神田小川町3', 'rent': '13.9万円'}

csv.DictReader を使うとディクショナリ形式で扱えます。

列の順番に依存しないので、ファイルのデータを扱いやすいです。

■ファイル出力

with open()を使ったファイル出力の基本

with open("file.csv", "w", encoding="utf-8") as f:
    ...

Python でファイルを書き込む場合は必ず with 文+open 関数を使います。

使う引数

  • "file.csv":書き込むファイル名
  • "w":書き込みモード(write)。既存内容は消える。
  • encoding="utf-8":文字化け防止

write() を使って CSV に文字列を書き込む

with open("data.csv", "w", encoding="utf-8") as f:
    f.write("name,age,city\n")   # 1行目
    f.write("Taro,30,Tokyo\n")   # 2行目
    f.write("Hanako,25,Osaka\n") # 3行目
項目 内容
メリット きわめてシンプル
デメリット 改行を自分で書く必要がある、カンマ区切りやエスケープが煩雑

writelines() を使って CSV を書き込む

lines = [
    "name,age,city\n",
    "Taro,30,Tokyo\n",
    "Hanako,25,Osaka\n",
]

with open("data.csv", "w", encoding="utf-8") as f:
    f.writelines(lines)
項目 内容
メリット 複数行を一気に書ける
デメリット 行末の改行 \n を自力で付ける必要がある

csv.writer を使ってCSV を書き込む

import csv

rows = [
    ["name", "age", "city"],
    ["Taro", 30, "Tokyo"],
    ["Hanako", 25, "Osaka"],
]

with open("data.csv", "w", encoding="utf-8", newline="") as f:
    writer = csv.writer(f)
    writer.writerows(rows)   # rows を一気に書き込み

CSV は文字列を手で組み立てると、

  • カンマを含む文字列
  • ダブルクォーテーション
  • 改行を含むセル

などが出た場合に壊れます。

なので、正しく CSV を出力するには csv 標準モジュールを使うと便利です。

  • writer.writerow([...]) → 1行だけ書く
  • writer.writerows([[...], [...], ...]) → 複数行を書く

CSV ファイルに追記(a モード)

import csv

with open("data.csv", "a", encoding="utf-8", newline="") as f:
    writer = csv.writer(f)
    writer.writerow(["Ken", 40, "Nagoya"])

既存の CSV に行を追加したい場合は "a" を使う。

辞書を CSV に書く:csv.DictWriter

import csv

data = [
    {"name": "Taro", "age": 30, "city": "Tokyo"},
    {"name": "Hanako", "age": 25, "city": "Osaka"},
    {"name": "Ken", "age": 40, "city": "Nagoya"},
]

with open("dict_output.csv", "w", encoding="utf-8", newline="") as f:
    fieldnames = ["name", "age", "city"]  # 列名
    writer = csv.DictWriter(f, fieldnames=fieldnames)

    writer.writeheader()       # ← ヘッダー行を書き込む
    writer.writerows(data)     # ← 複数行の辞書リストを書き込む

✔ DictWriter のメリット

メリット 内容
ヘッダーを自動で生成 辞書のキーがそのまま列名になる
順序を制御できる fieldnames を指定することで列順を統一
追加のプロパティを無視できる fieldnames にない項目は出力されない

✔ DictWriter のデメリット

デメリット 内容
fieldnames を手動で書く必要がある 辞書から自動抽出はされない
データ形式がバラバラだと例外が出る キーが欠けているとエラーになることも

■ファイルパスの指定方法

Python でファイル I/O を行うとき、「どのパスを基準にするか?」 は非常に重要です。

特に実務では、以下を考慮しないと致命的なバグにつながります:

  • 実行場所が変わる(VSCode / CLI / cron / systemd / Docker / CI など)
  • 他のメンバーの環境が異なる
  • サーバー上のディレクトリ構成が違う
  • シンボリックリンクの利用
  • 複数環境(dev / staging / prod)での差異

◆Path(file).resolve():実務での推奨方法

.
├── app
   └── main.py
├── data
   └── main.py

上のようなディレクトリ構成とします。

 pwd 
/Users/username/workspace/sample-dir

 python app/main.py

プロジェクトのrootにいると仮定します。

app/main.pyを実行します。

from pathlib import Path

# CSVファイルの内容を読み込む
data_path = Path(__file__).resolve().parent.parent / "data" / "sample.csv"

with open(data_path, "r", encoding="utf-8") as f:
    data = f.read()
    print("▼ read() 結果:")
    print(data)

Path(__file__).resolve()で実行ファイルまでのパスを絶対パスで取得します。

parent.parentでプロジェクトのrootに移動します。

その中のdata/sample.csvを実行します。

Path(__file__).resolve()にすると実行ファイルまでのパスが、絶対パスに変換されるのでOSの違いなどの影響を受けません。

また、currentディレクトリがどこであっても実行できます。

相対パス(カレントディレクトリ基準)

.
├── app
   └── main.py
└── sample.csv

上のようなディレクトリ構成とします。

with open("sample.csv", "r", encoding="utf-8") as f:
    data = f.read()
    print("▼ read() 結果:")
    print(data)

main.pyで、sample.csvを読み込みます。

 pwd
/Users/username/workspace/sample-dir

 python -u /Users/username/workspace/sample-dir/main.py

sample-dirにいる状態で、main.pyを実行します。

相対パスは、「プロセスの現在の作業ディレクトリ(CWD)を基準とするパス」なので、sample-dirが作業ディレクトリ(CWD)となり、sample-dirからみて直下にsample.csvがあるので実行できます。

 pwd
/Users/username/workspace/sample-dir/app

 python -u /Users/username/workspace/sample-dir/main.py

しかし、appディレクトリの中にいる状態で、main.pyを実行すると、CWDがappなので、appから見るとsample.csvが同階層になくエラーになります。

本番環境での事故例

  • systemd が / をカレントにして起動
  • cron が /home/user/ をカレントにして実行
  • Docker の WORKDIR が違う

結果:「data が見つかりません!」となります。

そのため、推奨されません。

絶対パス

ルートディレクトリ(一番システムの頭のディレクトリ)から始まる完全なパスです。

絶対パスの例

C:\Users\username\Documents\sample.csv # Windows
/home/username/documents/sample.csv # Mac, Linux

このやり方も推奨されません。

別の人にプロジェクトを渡した場合、同じパスにファイルがあると限らないので、実行できなくなります。

また、OSによっても変わるため、実行できなくなります。

  • 他の PC で動かない
  • 本番環境にデプロイすると死ぬ
  • Docker に乗せた途端パスが変わる
  • OS が違うと通らない
  • 移動しただけで壊れる

結論:絶対パスはハードコーディングなので絶対に NGです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?