소개
이 랩에서는 Python 의 클래스 변수와 클래스 메서드에 대해 배우게 됩니다. 그들의 목적과 사용법을 이해하고, 클래스 메서드를 효과적으로 정의하고 사용하는 방법을 배우게 됩니다.
또한, 클래스 메서드를 사용하여 대체 생성자를 구현하고, 클래스 변수와 상속 간의 관계를 탐구하며, 유연한 데이터 읽기 유틸리티를 만들 것입니다. 이 랩 동안 stock.py 및 reader.py 파일이 수정될 것입니다.
이 랩에서는 Python 의 클래스 변수와 클래스 메서드에 대해 배우게 됩니다. 그들의 목적과 사용법을 이해하고, 클래스 메서드를 효과적으로 정의하고 사용하는 방법을 배우게 됩니다.
또한, 클래스 메서드를 사용하여 대체 생성자를 구현하고, 클래스 변수와 상속 간의 관계를 탐구하며, 유연한 데이터 읽기 유틸리티를 만들 것입니다. 이 랩 동안 stock.py 및 reader.py 파일이 수정될 것입니다.
이 첫 번째 단계에서는 Python 의 클래스 변수와 클래스 메서드 개념에 대해 자세히 알아보겠습니다. 이는 보다 효율적이고 체계적인 코드를 작성하는 데 도움이 되는 중요한 개념입니다. 클래스 변수와 클래스 메서드를 사용하기 전에 먼저 Stock 클래스의 인스턴스가 현재 어떻게 생성되는지 살펴보겠습니다. 이를 통해 기본적인 이해를 얻고 개선할 수 있는 부분을 파악할 수 있습니다.
클래스 변수는 Python 에서 특별한 유형의 변수입니다. 클래스의 모든 인스턴스 간에 공유됩니다. 이를 더 잘 이해하기 위해 인스턴스 변수와 비교해 보겠습니다. 인스턴스 변수는 클래스의 각 인스턴스에 고유합니다. 예를 들어, 클래스의 여러 인스턴스가 있는 경우 각 인스턴스는 인스턴스 변수에 대한 자체 값을 가질 수 있습니다. 반면에 클래스 변수는 클래스 수준에서 정의됩니다. 즉, 해당 클래스의 모든 인스턴스가 클래스 변수의 동일한 값에 액세스하고 공유할 수 있습니다.
클래스 메서드는 클래스의 개별 인스턴스가 아닌 클래스 자체에서 작동하는 메서드입니다. 클래스에 바인딩되어 있어 인스턴스를 생성하지 않고도 클래스에서 직접 호출할 수 있습니다. Python 에서 클래스 메서드를 정의하려면 @classmethod 데코레이터를 사용합니다. 그리고 인스턴스 (self) 를 첫 번째 매개변수로 사용하는 대신, 클래스 메서드는 클래스 (cls) 를 첫 번째 매개변수로 사용합니다. 이를 통해 클래스 수준의 데이터에서 작동하고 클래스 전체와 관련된 작업을 수행할 수 있습니다.
먼저 현재 Stock 클래스의 인스턴스를 어떻게 생성하는지 살펴보겠습니다. 편집기에서 stock.py 파일을 열어 현재 구현을 확인하십시오.
class Stock:
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price
def cost(self):
return self.shares * self.price
이 클래스의 인스턴스는 일반적으로 다음과 같은 방법 중 하나로 생성됩니다.
값으로 직접 초기화:
s = Stock('GOOG', 100, 490.1)
여기서는 name, shares, price 속성에 대한 값을 제공하여 Stock 클래스의 인스턴스를 직접 생성하고 있습니다. 이는 값을 미리 알고 있을 때 인스턴스를 생성하는 간단한 방법입니다.
CSV 파일에서 읽은 데이터로 생성:
import csv
with open('portfolio.csv') as f:
rows = csv.reader(f)
headers = next(rows) ## Skip the header
row = next(rows) ## Get the first data row
s = Stock(row[0], int(row[1]), float(row[2]))
CSV 파일에서 데이터를 읽을 때 값은 처음에 문자열 형식입니다. 따라서 CSV 데이터에서 Stock 인스턴스를 생성할 때 문자열 값을 적절한 유형으로 수동으로 변환해야 합니다. 예를 들어, shares 값은 정수로 변환해야 하고, price 값은 부동 소수점으로 변환해야 합니다.
이것을 시도해 봅시다. ~/project 디렉토리에 다음 내용으로 test_stock.py라는 새 Python 파일을 만듭니다.
## test_stock.py
from stock import Stock
import csv
## Method 1: Direct creation
s1 = Stock('GOOG', 100, 490.1)
print(f"Stock: {s1.name}, Shares: {s1.shares}, Price: {s1.price}")
print(f"Cost: {s1.cost()}")
## Method 2: Creation from CSV row
with open('portfolio.csv') as f:
rows = csv.reader(f)
headers = next(rows) ## Skip the header
row = next(rows) ## Get the first data row
s2 = Stock(row[0], int(row[1]), float(row[2]))
print(f"\nStock from CSV: {s2.name}, Shares: {s2.shares}, Price: {s2.price}")
print(f"Cost: {s2.cost()}")
이 파일을 실행하여 결과를 확인하십시오.
cd ~/project
python test_stock.py
다음과 유사한 출력이 표시되어야 합니다.
Stock: GOOG, Shares: 100, Price: 490.1
Cost: 49010.0
Stock from CSV: AA, Shares: 100, Price: 32.2
Cost: 3220.0
이 수동 변환은 작동하지만 몇 가지 단점이 있습니다. 데이터의 정확한 형식을 알아야 하고 CSV 데이터에서 인스턴스를 생성할 때마다 변환을 수행해야 합니다. 이는 오류가 발생하기 쉽고 시간이 많이 걸릴 수 있습니다. 다음 단계에서는 클래스 메서드를 사용하여 더 우아한 솔루션을 만들 것입니다.
이 단계에서는 클래스 메서드를 사용하여 대체 생성자를 구현하는 방법을 배우게 됩니다. 이를 통해 CSV 행 데이터에서 Stock 객체를 보다 우아한 방식으로 생성할 수 있습니다.
Python 에서 대체 생성자는 유용한 패턴입니다. 일반적으로 표준 __init__ 메서드를 사용하여 객체를 생성합니다. 그러나 대체 생성자는 객체를 생성하는 추가적인 방법을 제공합니다. 클래스 메서드는 클래스 자체에 액세스할 수 있으므로 대체 생성자를 구현하는 데 매우 적합합니다.
Stock 클래스에 클래스 변수 types와 클래스 메서드 from_row()를 추가합니다. 이렇게 하면 CSV 데이터에서 Stock 인스턴스를 생성하는 프로세스가 단순화됩니다.
강조 표시된 코드를 추가하여 stock.py 파일을 수정해 보겠습니다.
## stock.py
class Stock:
types = (str, int, float) ## Type conversions to apply to CSV data
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price
def cost(self):
return self.shares * self.price
@classmethod
def from_row(cls, row):
"""
Create a Stock instance from a row of CSV data.
Args:
row: A list of strings [name, shares, price]
Returns:
A new Stock instance
"""
values = [func(val) for func, val in zip(cls.types, row)]
return cls(*values)
## The rest of the file remains unchanged
이제 이 코드에서 무슨 일이 일어나는지 단계별로 이해해 보겠습니다.
types를 정의했습니다. 이는 유형 변환 함수 (str, int, float)를 포함하는 튜플입니다. 이러한 함수는 CSV 행의 데이터를 적절한 유형으로 변환하는 데 사용됩니다.from_row()를 추가했습니다. @classmethod 데코레이터는 이 메서드를 클래스 메서드로 표시합니다.cls입니다. 일반 메서드에서는 클래스의 인스턴스를 참조하기 위해 self를 사용하지만, 여기서는 클래스 메서드이므로 cls를 사용합니다.zip() 함수는 types의 각 유형 변환 함수를 row 목록의 해당 값과 쌍으로 연결하는 데 사용됩니다.row 목록의 해당 값에 적용합니다. 이렇게 하면 CSV 행의 문자열 데이터를 적절한 유형으로 변환합니다.Stock 클래스의 새 인스턴스를 생성하고 반환합니다.이제 새 클래스 메서드를 테스트하기 위해 test_class_method.py라는 새 파일을 만들 것입니다. 이를 통해 대체 생성자가 예상대로 작동하는지 확인할 수 있습니다.
## test_class_method.py
from stock import Stock
## Test the from_row() class method
row = ['AA', '100', '32.20']
s = Stock.from_row(row)
print(f"Stock: {s.name}")
print(f"Shares: {s.shares}")
print(f"Price: {s.price}")
print(f"Cost: {s.cost()}")
## Try with a different row
row2 = ['GOOG', '50', '1120.50']
s2 = Stock.from_row(row2)
print(f"\nStock: {s2.name}")
print(f"Shares: {s2.shares}")
print(f"Price: {s2.price}")
print(f"Cost: {s2.cost()}")
결과를 보려면 터미널에서 다음 명령을 실행하십시오.
cd ~/project
python test_class_method.py
다음과 유사한 출력이 표시되어야 합니다.
Stock: AA
Shares: 100
Price: 32.2
Cost: 3220.0
Stock: GOOG
Shares: 50
Price: 1120.5
Cost: 56025.0
이제 클래스 외부에서 수동으로 유형 변환을 수행하지 않고도 문자열 데이터에서 직접 Stock 인스턴스를 생성할 수 있습니다. 이렇게 하면 코드가 더 깔끔해지고 데이터 변환에 대한 책임이 클래스 자체 내에서 처리됩니다.
이 단계에서는 클래스 변수가 상속과 어떻게 상호 작용하는지, 그리고 사용자 정의 메커니즘으로 어떻게 사용될 수 있는지 살펴보겠습니다. Python 에서 상속을 사용하면 하위 클래스가 기본 클래스에서 속성과 메서드를 상속받을 수 있습니다. 클래스 변수는 클래스의 특정 인스턴스가 아닌 클래스 자체에 속하는 변수입니다. 이러한 것들이 함께 작동하는 방식을 이해하는 것은 유연하고 유지 관리 가능한 코드를 만드는 데 매우 중요합니다.
하위 클래스가 기본 클래스에서 상속될 때, 기본 클래스의 클래스 변수에 자동으로 액세스할 수 있습니다. 그러나 하위 클래스는 이러한 클래스 변수를 재정의할 수 있습니다. 그렇게 함으로써 하위 클래스는 기본 클래스에 영향을 주지 않고 동작을 변경할 수 있습니다. 이는 특정 요구 사항에 따라 하위 클래스의 동작을 사용자 정의할 수 있으므로 매우 강력한 기능입니다.
Stock 클래스의 하위 클래스를 만들어 보겠습니다. 이를 DStock이라고 부르겠습니다. 여기서 D 는 Decimal Stock 을 의미합니다. DStock과 일반 Stock 클래스의 주요 차이점은 DStock이 float 대신 가격 값에 Decimal 유형을 사용한다는 것입니다. 금융 계산에서 정밀도는 매우 중요하며, Decimal 유형은 float에 비해 더 정확한 십진수 산술 연산을 제공합니다.
이 하위 클래스를 만들려면 decimal_stock.py라는 새 파일을 만듭니다. 이 파일에 넣어야 할 코드는 다음과 같습니다.
## decimal_stock.py
from decimal import Decimal
from stock import Stock
class DStock(Stock):
"""
A specialized version of Stock that uses Decimal for prices
"""
types = (str, int, Decimal) ## Override the types class variable
## Test the subclass
if __name__ == "__main__":
## Create a DStock from row data
row = ['AA', '100', '32.20']
ds = DStock.from_row(row)
print(f"DStock: {ds.name}")
print(f"Shares: {ds.shares}")
print(f"Price: {ds.price} (type: {type(ds.price).__name__})")
print(f"Cost: {ds.cost()} (type: {type(ds.cost()).__name__})")
## For comparison, create a regular Stock from the same data
s = Stock.from_row(row)
print(f"\nRegular Stock: {s.name}")
print(f"Shares: {s.shares}")
print(f"Price: {s.price} (type: {type(s.price).__name__})")
print(f"Cost: {s.cost()} (type: {type(s.cost()).__name__})")
위 코드로 decimal_stock.py 파일을 만든 후에는 결과를 확인하기 위해 실행해야 합니다. 터미널을 열고 다음 단계를 따르세요.
cd ~/project
python decimal_stock.py
다음과 유사한 출력이 표시되어야 합니다.
DStock: AA
Shares: 100
Price: 32.20 (type: Decimal)
Cost: 3220.0 (type: Decimal)
Regular Stock: AA
Shares: 100
Price: 32.2 (type: float)
Cost: 3220.0 (type: float)
이 예제에서 몇 가지 중요한 결론을 도출할 수 있습니다.
DStock 클래스는 cost() 메서드와 같은 Stock 클래스의 모든 메서드를 다시 정의하지 않고 상속받습니다. 이는 상속의 주요 장점 중 하나이며, 중복된 코드를 작성하지 않아도 됩니다.types 클래스 변수를 간단히 재정의함으로써 DStock의 새 인스턴스를 생성할 때 데이터가 변환되는 방식을 변경했습니다. 이는 클래스 변수를 사용하여 하위 클래스의 동작을 사용자 정의하는 방법을 보여줍니다.Stock은 변경되지 않고 여전히 float 값을 사용합니다. 즉, 하위 클래스에 대한 변경 사항이 기본 클래스에 영향을 미치지 않으며, 이는 좋은 설계 원칙입니다.from_row() 클래스 메서드는 Stock 및 DStock 클래스 모두에서 올바르게 작동합니다. 이는 상속의 강력함을 보여주며, 동일한 메서드를 다른 하위 클래스에서 사용할 수 있습니다.이 예제는 클래스 변수를 구성 메커니즘으로 사용하는 방법을 명확하게 보여줍니다. 하위 클래스는 메서드를 다시 작성하지 않고도 이러한 변수를 재정의하여 동작을 사용자 정의할 수 있습니다.
__init__ 메서드에 유형 변환을 배치하는 대체 접근 방식을 고려해 보겠습니다.
class Stock:
def __init__(self, name, shares, price):
self.name = str(name)
self.shares = int(shares)
self.price = float(price)
이 접근 방식을 사용하면 다음과 같이 데이터 행에서 Stock 객체를 만들 수 있습니다.
row = ['AA', '100', '32.20']
s = Stock(*row)
이 접근 방식이 처음에는 더 간단해 보일 수 있지만 몇 가지 단점이 있습니다.
__init__ 메서드는 입력이 이미 올바른 유형인 경우에도 항상 입력을 변환하므로 덜 유연해집니다.__init__ 메서드에 포함된 경우 변환 로직을 변경하기가 더 어려워집니다.반면에 클래스 메서드 접근 방식은 이러한 문제를 분리합니다. 이렇게 하면 코드의 각 부분이 단일 책임을 갖게 되므로 코드를 더 유지 관리하고 유연하게 만들 수 있습니다.
이 마지막 단계에서는 범용 함수를 만들 것입니다. 이 함수는 CSV 파일을 읽고 from_row() 클래스 메서드를 구현한 모든 클래스의 객체를 생성할 수 있습니다. 이는 클래스 메서드를 균일한 인터페이스로 사용하는 것의 강력함을 보여줍니다. 균일한 인터페이스는 서로 다른 클래스를 동일한 방식으로 사용할 수 있음을 의미하며, 이는 코드를 더 유연하고 관리하기 쉽게 만듭니다.
먼저 stock.py 파일에서 read_portfolio() 함수를 업데이트합니다. 새로운 from_row() 클래스 메서드를 사용합니다. stock.py 파일을 열고 read_portfolio() 함수를 다음과 같이 변경합니다.
def read_portfolio(filename):
'''
Read a stock portfolio file into a list of Stock instances
'''
import csv
portfolio = []
with open(filename) as f:
rows = csv.reader(f)
headers = next(rows) ## Skip header
for row in rows:
portfolio.append(Stock.from_row(row))
return portfolio
이 함수의 새 버전은 더 간단합니다. 유형 변환의 책임을 실제로 속한 Stock 클래스에 부여합니다. 유형 변환은 문자열을 정수로 바꾸는 것과 같이 데이터를 한 유형에서 다른 유형으로 변경하는 것을 의미합니다. 이렇게 하면 코드를 더 체계적으로 만들고 이해하기 쉽게 만들 수 있습니다.
이제 reader.py 파일에서 더 범용적인 함수를 만들 것입니다. 이 함수는 CSV 데이터를 읽고 from_row() 클래스 메서드가 있는 모든 클래스의 인스턴스를 생성할 수 있습니다.
reader.py 파일을 열고 다음 함수를 추가합니다.
def read_csv_as_instances(filename, cls):
'''
Read a CSV file into a list of instances of the given class.
Args:
filename: Name of the CSV file
cls: Class to instantiate (must have from_row class method)
Returns:
List of class instances
'''
records = []
with open(filename) as f:
rows = csv.reader(f)
headers = next(rows) ## Skip header
for row in rows:
records.append(cls.from_row(row))
return records
이 함수는 파일 이름과 클래스라는 두 개의 입력을 받습니다. 그런 다음 CSV 파일의 데이터에서 생성된 해당 클래스의 인스턴스 목록을 반환합니다. 이는 from_row() 메서드가 있는 한 서로 다른 클래스에서 사용할 수 있으므로 매우 유용합니다.
범용 리더가 어떻게 작동하는지 확인하기 위해 테스트 파일을 만들어 보겠습니다. 다음 내용으로 test_csv_reader.py라는 파일을 만듭니다.
## test_csv_reader.py
from reader import read_csv_as_instances
from stock import Stock
from decimal_stock import DStock
## Read portfolio as Stock instances
portfolio = read_csv_as_instances('portfolio.csv', Stock)
print(f"Portfolio contains {len(portfolio)} stocks")
print(f"First stock: {portfolio[0].name}, {portfolio[0].shares} shares at ${portfolio[0].price}")
## Read portfolio as DStock instances (with Decimal prices)
decimal_portfolio = read_csv_as_instances('portfolio.csv', DStock)
print(f"\nDecimal portfolio contains {len(decimal_portfolio)} stocks")
print(f"First stock: {decimal_portfolio[0].name}, {decimal_portfolio[0].shares} shares at ${decimal_portfolio[0].price}")
## Define a new class for reading the bus data
class BusRide:
def __init__(self, route, date, daytype, rides):
self.route = route
self.date = date
self.daytype = daytype
self.rides = rides
@classmethod
def from_row(cls, row):
return cls(row[0], row[1], row[2], int(row[3]))
## Read some bus data (just the first 5 records for brevity)
print("\nReading bus data...")
import csv
with open('ctabus.csv') as f:
rows = csv.reader(f)
headers = next(rows) ## Skip header
bus_rides = []
for i, row in enumerate(rows):
if i >= 5: ## Only read 5 records for the example
break
bus_rides.append(BusRide.from_row(row))
## Display the bus data
for ride in bus_rides:
print(f"Route: {ride.route}, Date: {ride.date}, Type: {ride.daytype}, Rides: {ride.rides}")
결과를 보려면 이 파일을 실행하십시오. 터미널을 열고 다음 명령을 사용합니다.
cd ~/project
python test_csv_reader.py
포트폴리오 데이터가 Stock 및 DStock 인스턴스로 모두 로드되고 버스 노선 데이터가 BusRide 인스턴스로 로드되는 것을 보여주는 출력이 표시되어야 합니다. 이는 범용 리더가 서로 다른 클래스에서 작동함을 증명합니다.
이 접근 방식은 몇 가지 강력한 개념을 보여줍니다.
from_row() 메서드가 있는 한 범용 리더가 사용할 수 있습니다.Stock 또는 DStock을 사용하여 포트폴리오 데이터를 다르게 처리할 수 있습니다.이는 코드를 더 모듈화하고, 재사용 가능하며, 유지 관리 가능하게 만드는 Python 의 일반적인 패턴입니다.
클래스 메서드는 Python 에서 대체 생성자로 자주 사용됩니다. 일반적으로 이름에 "from"이라는 단어가 포함되어 있는지 여부로 구분할 수 있습니다. 예를 들면 다음과 같습니다.
## Some examples from Python's built-in types
dict.fromkeys(['a', 'b', 'c'], 0) ## Create a dict with default values
datetime.datetime.fromtimestamp(1627776000) ## Create datetime from timestamp
int.from_bytes(b'\x00\x01', byteorder='big') ## Create int from bytes
이 규칙을 따르면 코드를 더 읽기 쉽고 Python 의 내장 라이브러리와 일관되게 만들 수 있습니다. 이는 다른 개발자가 코드를 더 쉽게 이해하는 데 도움이 됩니다.
이 랩에서는 두 가지 중요한 Python 기능인 클래스 변수와 클래스 메서드에 대해 배웠습니다. 클래스 변수는 모든 클래스 인스턴스 간에 공유되며 구성을 위해 사용될 수 있습니다. 클래스 메서드는 @classmethod 데코레이터로 표시되어 클래스 자체에서 작동합니다. 클래스 메서드의 일반적인 사용 사례인 대체 생성자는 객체를 생성하는 다양한 방법을 제공합니다. 클래스 변수를 사용한 상속을 통해 하위 클래스는 이를 재정의하여 동작을 사용자 정의할 수 있으며, 클래스 메서드를 사용하면 유연한 코드 설계를 달성할 수 있습니다.
이러한 개념은 잘 구성되고 유연한 Python 코드를 만드는 데 강력합니다. 유형 변환을 클래스 내에 배치하고 클래스 메서드를 통해 균일한 인터페이스를 제공함으로써 더 범용적인 유틸리티를 작성할 수 있습니다. 학습을 확장하기 위해 더 많은 사용 사례를 탐색하고, 클래스 계층 구조를 만들고, 클래스 메서드를 사용하여 복잡한 데이터 처리 파이프라인을 구축할 수 있습니다.