Python:class 属性定义上的代码重复
Python: code duplication on class attribute definition
我正在尝试在 python 中实现一个简单的 ORM。我面临代码重复问题,但我不知道如何解决。
这是我项目中 class 的简化示例:
class Person:
TABLE_NAME = 'person'
FIELDS = [
('name', 'VARCHAR(50)'),
('age', 'INTEGER')
]
# CODE DUPLICATION: the two next lines shoudl be genereated with FIELDS not hard coded...
name: str
age: int
def __init__(self, **kwargs):
self.__dict__ = kwargs
@classmethod
def create_sql_table(cls):
# use TABLE_NAME and FIELDS to create sql table
pass
alice = Person(name='Alice', age=25)
print(alice.name)
如果我删除两行 name: str
和 age: int
我将失去自动完成功能并且在打印行上出现 mypy 错误(错误:Person 没有属性名称)
但是如果我保留它,我有代码重复(每个字段名我写了两次)。
有没有办法避免代码重复(例如通过使用 FIELDS 变量生成这两行)?
或者另一种实现此 class 的方法,避免代码重复(没有 mypy 错误和自动完成丢失)?
在 class Person 尝试在构造函数中添加数据类型
好的,我结束了
class Person:
# this is not full, you need to fill other types you use it with the correct relationship
types = {
str: 'VARCHAR(50)',
int: 'INTEGER',
} # you should extract that out if you use it elsewhere
TABLE_NAME = 'person'
# NOTE: the only annotated fields should be these. if you annotate anything else, It will break
name: str
age: int
def __init__(self, **kwargs):
self.__dict__ = kwargs
@property
def FIELDS(cls):
return [(key, cls.types[value]) for key, value in cls.__annotations__.items()]
alice = Person(name='Alice', age=25)
print(alice.FIELDS) # [('name', 'VARCHAR(50)'), ('age', 'INTEGER')]
和
>>> mypy <module>
>>> Success: no issues found in 1 source file
您可以使用描述符:
from typing import Generic, TypeVar, Any, overload, Union
T = TypeVar('T')
class Column(Generic[T]):
sql_type: str # the field type used for this column
def __init__(self) -> None:
self.name = '' # the name of the column
# this is called when the Person class (not the instance) is created
def __set_name__(self, owner: Any, name: str) -> None:
self.name = name # now contains the name of the attribute in the class
# the overload for the case: Person.name -> Column[str]
@overload
def __get__(self, instance: None, owner: Any) -> 'Column[T]': ...
# the overload for the case: Person().name -> str
@overload
def __get__(self, instance: Any, owner: Any) -> T: ...
# the implementation of attribute access
def __get__(self, instance: Any, owner: Any) -> Union[T, 'Column[T]']:
if instance is None:
return self
# implement your attribute access here
return getattr(instance, f'_{self.name}') # type: ignore
# the implementation for setting attributes
def __set__(self, instance: Any, value: T) -> None:
# maybe check here that the type matches
setattr(instance, f'_{self.name}', value)
现在我们可以为每种列类型创建专门化:
class Integer(Column[int]):
sql_type = 'INTEGER'
class VarChar(Column[str]):
def __init__(self, size: int) -> None:
self.sql_type = f'VARCHAR({size})'
super().__init__()
当您定义 Person
class 时,我们可以使用列类型
class Person:
TABLE_NAME = 'person'
name = VarChar(50)
age = Integer()
def __init__(self, **kwargs: Any) -> None:
for key, value in kwargs.items():
setattr(self, key, value)
@classmethod
def create_sql_table(cls) -> None:
print("CREATE TABLE", cls.TABLE_NAME)
for key, value in vars(cls).items():
if isinstance(value, Column):
print(key, value.sql_type)
Person.create_sql_table()
p = Person(age=10)
print(p.age)
p.age = 20
print(p.age)
这会打印:
CREATE TABLE person
name VARCHAR(50)
age INTEGER
10
20
您可能还应该创建一个基础 Model
class,其中包含 __init__
和 Person
的 class 方法
您还可以扩展 Column
class 以允许可为空的列并添加默认值。
Mypy 不会抱怨并且可以正确推断 Person.name
到 str 和 Person.age
到 int 的类型。
我正在尝试在 python 中实现一个简单的 ORM。我面临代码重复问题,但我不知道如何解决。 这是我项目中 class 的简化示例:
class Person:
TABLE_NAME = 'person'
FIELDS = [
('name', 'VARCHAR(50)'),
('age', 'INTEGER')
]
# CODE DUPLICATION: the two next lines shoudl be genereated with FIELDS not hard coded...
name: str
age: int
def __init__(self, **kwargs):
self.__dict__ = kwargs
@classmethod
def create_sql_table(cls):
# use TABLE_NAME and FIELDS to create sql table
pass
alice = Person(name='Alice', age=25)
print(alice.name)
如果我删除两行 name: str
和 age: int
我将失去自动完成功能并且在打印行上出现 mypy 错误(错误:Person 没有属性名称)
但是如果我保留它,我有代码重复(每个字段名我写了两次)。
有没有办法避免代码重复(例如通过使用 FIELDS 变量生成这两行)?
或者另一种实现此 class 的方法,避免代码重复(没有 mypy 错误和自动完成丢失)?
在 class Person 尝试在构造函数中添加数据类型
好的,我结束了
class Person:
# this is not full, you need to fill other types you use it with the correct relationship
types = {
str: 'VARCHAR(50)',
int: 'INTEGER',
} # you should extract that out if you use it elsewhere
TABLE_NAME = 'person'
# NOTE: the only annotated fields should be these. if you annotate anything else, It will break
name: str
age: int
def __init__(self, **kwargs):
self.__dict__ = kwargs
@property
def FIELDS(cls):
return [(key, cls.types[value]) for key, value in cls.__annotations__.items()]
alice = Person(name='Alice', age=25)
print(alice.FIELDS) # [('name', 'VARCHAR(50)'), ('age', 'INTEGER')]
和
>>> mypy <module>
>>> Success: no issues found in 1 source file
您可以使用描述符:
from typing import Generic, TypeVar, Any, overload, Union
T = TypeVar('T')
class Column(Generic[T]):
sql_type: str # the field type used for this column
def __init__(self) -> None:
self.name = '' # the name of the column
# this is called when the Person class (not the instance) is created
def __set_name__(self, owner: Any, name: str) -> None:
self.name = name # now contains the name of the attribute in the class
# the overload for the case: Person.name -> Column[str]
@overload
def __get__(self, instance: None, owner: Any) -> 'Column[T]': ...
# the overload for the case: Person().name -> str
@overload
def __get__(self, instance: Any, owner: Any) -> T: ...
# the implementation of attribute access
def __get__(self, instance: Any, owner: Any) -> Union[T, 'Column[T]']:
if instance is None:
return self
# implement your attribute access here
return getattr(instance, f'_{self.name}') # type: ignore
# the implementation for setting attributes
def __set__(self, instance: Any, value: T) -> None:
# maybe check here that the type matches
setattr(instance, f'_{self.name}', value)
现在我们可以为每种列类型创建专门化:
class Integer(Column[int]):
sql_type = 'INTEGER'
class VarChar(Column[str]):
def __init__(self, size: int) -> None:
self.sql_type = f'VARCHAR({size})'
super().__init__()
当您定义 Person
class 时,我们可以使用列类型
class Person:
TABLE_NAME = 'person'
name = VarChar(50)
age = Integer()
def __init__(self, **kwargs: Any) -> None:
for key, value in kwargs.items():
setattr(self, key, value)
@classmethod
def create_sql_table(cls) -> None:
print("CREATE TABLE", cls.TABLE_NAME)
for key, value in vars(cls).items():
if isinstance(value, Column):
print(key, value.sql_type)
Person.create_sql_table()
p = Person(age=10)
print(p.age)
p.age = 20
print(p.age)
这会打印:
CREATE TABLE person
name VARCHAR(50)
age INTEGER
10
20
您可能还应该创建一个基础 Model
class,其中包含 __init__
和 Person
您还可以扩展 Column
class 以允许可为空的列并添加默认值。
Mypy 不会抱怨并且可以正确推断 Person.name
到 str 和 Person.age
到 int 的类型。